Files

263 lines
7.5 KiB
Go
Raw Permalink Normal View History

2026-10-19 15:16:07 +00:00
package alias
import (
"fmt"
"sort"
"strings"
)
type head struct {
control int
symbol symbol
}
type popKey struct {
from head
to int
}
type popSummary struct {
rules map[int]struct{}
}
type popWork struct {
key popKey
summary popSummary
}
type transitionUse struct {
key transitionKey
transition transition
}
type graphEdge struct {
to head
rules map[int]struct{}
}
// NonTerminationError identifies mappings that participate in an alias loop.
type NonTerminationError struct {
Mappings []Mapping
}
func (err *NonTerminationError) Error() string {
if err == nil || len(err.Mappings) == 0 {
return "command alias rewriting does not terminate"
}
parts := make([]string, 0, len(err.Mappings))
for _, mapping := range err.Mappings {
parts = append(parts, fmt.Sprintf("%q → %q", mapping.Source.String(), mapping.Target.String()))
}
return "command aliases do not terminate: " + strings.Join(parts, "; ")
}
// ValidateTermination rejects mapping sets whose deterministic prefix
// rewriting can repeat forever for some finite command path.
func ValidateTermination(mappings []Mapping) error {
_, machine, err := Compile(mappings)
if err != nil {
return err
}
if cycle := findHeadCycle(machine); cycle != nil {
return &NonTerminationError{Mappings: cycle}
}
return nil
}
func findHeadCycle(machine *Machine) []Mapping {
// Pop summaries exactly describe finite paths that remove one top symbol
// without inspecting below it. The head graph then represents nonempty
// paths that never pop below their starting level. Its cycle witnesses a
// repeatable computation even when the complete command path keeps growing.
pop := saturatePopSummaries(machine)
graph := buildHeadGraph(machine, pop)
colors := make(map[head]uint8, len(graph))
path := make([]head, 0)
pathEdges := make([]graphEdge, 0)
pathIndex := make(map[head]int)
var cycleRules map[int]struct{}
var visit func(head) bool
visit = func(current head) bool {
colors[current] = 1
pathIndex[current] = len(path)
path = append(path, current)
for _, edge := range graph[current] {
switch colors[edge.to] {
case 0:
pathEdges = append(pathEdges, edge)
if visit(edge.to) {
return true
}
pathEdges = pathEdges[:len(pathEdges)-1]
case 1:
start := pathIndex[edge.to]
cycleRules = make(map[int]struct{})
for _, cycleEdge := range pathEdges[start:] {
mergeRules(cycleRules, cycleEdge.rules)
}
mergeRules(cycleRules, edge.rules)
return true
}
}
path = path[:len(path)-1]
delete(pathIndex, current)
colors[current] = 2
return false
}
for current := range graph {
if colors[current] == 0 && visit(current) {
indices := make([]int, 0, len(cycleRules))
for index := range cycleRules {
if index >= 0 {
indices = append(indices, index)
}
}
sort.Ints(indices)
mappings := make([]Mapping, 0, len(indices))
for _, index := range indices {
if index < len(machine.mappings) {
mapping := machine.mappings[index]
mappings = append(mappings, Mapping{Source: mapping.Source.Clone(), Target: mapping.Target.Clone()})
}
}
return mappings
}
}
return nil
}
func saturatePopSummaries(machine *Machine) map[head]map[int]popSummary {
// Zero-symbol pushes seed the relation. Each newly found summary composes
// with transitions that push one symbol and with either side of transitions
// that push two symbols until no new finite pop path can be derived.
byHead := make(map[head]map[int]popSummary)
queue := make([]popWork, 0)
add := func(key popKey, rules map[int]struct{}) {
if byHead[key.from] == nil {
byHead[key.from] = make(map[int]popSummary)
}
if _, exists := byHead[key.from][key.to]; exists {
return
}
summary := popSummary{rules: cloneRules(rules)}
byHead[key.from][key.to] = summary
queue = append(queue, popWork{key: key, summary: summary})
}
ones := make(map[head][]transitionUse)
twosByPrefix := make(map[head][]transitionUse)
twosBySuffix := make(map[symbol][]transitionUse)
for key, operation := range machine.transitions {
switch len(operation.push) {
case 0:
rules := make(map[int]struct{})
addRule(rules, operation.rule)
add(popKey{from: head{control: key.control, symbol: key.read}, to: operation.next}, rules)
case 1:
ones[head{control: operation.next, symbol: operation.push[0]}] = append(ones[head{control: operation.next, symbol: operation.push[0]}], transitionUse{key: key, transition: operation})
case 2:
prefix := head{control: operation.next, symbol: operation.push[0]}
twosByPrefix[prefix] = append(twosByPrefix[prefix], transitionUse{key: key, transition: operation})
twosBySuffix[operation.push[1]] = append(twosBySuffix[operation.push[1]], transitionUse{key: key, transition: operation})
}
}
for cursor := 0; cursor < len(queue); cursor++ {
work := queue[cursor]
for _, use := range ones[work.key.from] {
add(popKey{
from: head{control: use.key.control, symbol: use.key.read},
to: work.key.to,
}, combineRules(use.transition.rule, work.summary.rules))
}
for _, use := range twosByPrefix[work.key.from] {
suffix := head{control: work.key.to, symbol: use.transition.push[1]}
finals := make([]int, 0, len(byHead[suffix]))
for final := range byHead[suffix] {
finals = append(finals, final)
}
sort.Ints(finals)
for _, final := range finals {
summary := byHead[suffix][final]
add(popKey{
from: head{control: use.key.control, symbol: use.key.read},
to: final,
}, combineRules(use.transition.rule, work.summary.rules, summary.rules))
}
}
for _, use := range twosBySuffix[work.key.from.symbol] {
if work.key.from.symbol != use.transition.push[1] {
continue
}
prefix := head{control: use.transition.next, symbol: use.transition.push[0]}
if first, exists := byHead[prefix][work.key.from.control]; exists {
add(popKey{
from: head{control: use.key.control, symbol: use.key.read},
to: work.key.to,
}, combineRules(use.transition.rule, first.rules, work.summary.rules))
}
}
}
return byHead
}
func buildHeadGraph(machine *Machine, pop map[head]map[int]popSummary) map[head][]graphEdge {
graph := make(map[head][]graphEdge)
for key, operation := range machine.transitions {
source := head{control: key.control, symbol: key.read}
rules := make(map[int]struct{})
addRule(rules, operation.rule)
switch len(operation.push) {
case 1:
graph[source] = append(graph[source], graphEdge{
to: head{control: operation.next, symbol: operation.push[0]},
rules: cloneRules(rules),
})
case 2:
graph[source] = append(graph[source], graphEdge{
to: head{control: operation.next, symbol: operation.push[0]},
rules: cloneRules(rules),
})
prefix := head{control: operation.next, symbol: operation.push[0]}
for nextControl, summary := range pop[prefix] {
graph[source] = append(graph[source], graphEdge{
to: head{control: nextControl, symbol: operation.push[1]},
rules: combineRules(operation.rule, summary.rules),
})
}
}
}
return graph
}
func addRule(rules map[int]struct{}, rule int) {
if rule >= 0 {
rules[rule] = struct{}{}
}
}
func cloneRules(rules map[int]struct{}) map[int]struct{} {
copy := make(map[int]struct{}, len(rules))
mergeRules(copy, rules)
return copy
}
func combineRules(rule int, groups ...map[int]struct{}) map[int]struct{} {
combined := make(map[int]struct{})
addRule(combined, rule)
for _, group := range groups {
mergeRules(combined, group)
}
return combined
}
func mergeRules(target, source map[int]struct{}) {
for rule := range source {
target[rule] = struct{}{}
}
}