package engine import ( "container/heap" "sort" ) // dependencyKey is an immutable tuple used to order dependency-graph nodes. // Callers provide unique keys and valid edge endpoints. type dependencyKey []string // dependencyEdge points from a prerequisite to the node that depends on it. type dependencyEdge struct { prerequisite int dependent int } type dependencyReadyHeap struct { nodes []int keys []dependencyKey } func (h dependencyReadyHeap) Len() int { return len(h.nodes) } func (h dependencyReadyHeap) Less(i, j int) bool { return dependencyKeyLess(h.keys[h.nodes[i]], h.keys[h.nodes[j]]) } func (h dependencyReadyHeap) Swap(i, j int) { h.nodes[i], h.nodes[j] = h.nodes[j], h.nodes[i] } func (h *dependencyReadyHeap) Push(value any) { h.nodes = append(h.nodes, value.(int)) } func (h *dependencyReadyHeap) Pop() any { last := len(h.nodes) - 1 node := h.nodes[last] h.nodes = h.nodes[:last] return node } // dependencyKeyLess compares tuple components bytewise, with a shorter tuple // sorting first when all of its components match the corresponding prefix. func dependencyKeyLess(left, right dependencyKey) bool { for i := 0; i < len(left) && i < len(right); i++ { if left[i] != right[i] { return left[i] < right[i] } } return len(left) < len(right) } // scheduleDependencies applies Kahn's algorithm, always selecting the // smallest currently-ready key. Duplicate edges count only once. If scheduling // stalls, an explicit depth-first traversal of the residual graph returns an // actual directed cycle, rotated to its smallest key and closed by repeating // its first node; blocked non-cycle dependents are excluded. // // Nodes are visited by precomputed key rank, which also makes each adjacency // list deterministic without sorting edges. The work is O(E + V log V), with // O(V + E) additional memory, apart from key copying and tuple comparisons. func scheduleDependencies(keys []dependencyKey, edges []dependencyEdge) (order, cycle []int) { ownedKeys := make([]dependencyKey, len(keys)) for i, key := range keys { ownedKeys[i] = append(dependencyKey(nil), key...) } nodeOrder := make([]int, len(ownedKeys)) for i := range nodeOrder { nodeOrder[i] = i } sort.Slice(nodeOrder, func(i, j int) bool { return dependencyKeyLess(ownedKeys[nodeOrder[i]], ownedKeys[nodeOrder[j]]) }) incoming := make([][]int, len(ownedKeys)) for _, edge := range edges { incoming[edge.dependent] = append(incoming[edge.dependent], edge.prerequisite) } outgoing := make([][]int, len(ownedKeys)) indegree := make([]int, len(ownedKeys)) for _, dependent := range nodeOrder { seen := make(map[int]struct{}, len(incoming[dependent])) for _, prerequisite := range incoming[dependent] { if _, duplicate := seen[prerequisite]; duplicate { continue } seen[prerequisite] = struct{}{} indegree[dependent]++ // Dependents are visited by key rank, so each outgoing list is // already ordered by key. outgoing[prerequisite] = append(outgoing[prerequisite], dependent) } } ready := dependencyReadyHeap{keys: ownedKeys} for _, node := range nodeOrder { if indegree[node] == 0 { ready.nodes = append(ready.nodes, node) } } heap.Init(&ready) order = make([]int, 0, len(ownedKeys)) for ready.Len() != 0 { node := heap.Pop(&ready).(int) order = append(order, node) for _, dependent := range outgoing[node] { indegree[dependent]-- if indegree[dependent] == 0 { heap.Push(&ready, dependent) } } } if len(order) == len(ownedKeys) { return order, nil } return nil, dependencyCycle(ownedKeys, nodeOrder, outgoing, indegree) } type dependencyDFSFrame struct { node int next int } // dependencyCycle searches the residual graph iteratively to avoid recursion // depth depending on input size. func dependencyCycle(keys []dependencyKey, nodeOrder []int, outgoing [][]int, indegree []int) []int { const ( unvisited = uint8(0) active = uint8(1) finished = uint8(2) ) state := make([]uint8, len(keys)) position := make([]int, len(keys)) for i := range position { position[i] = -1 } path := make([]int, 0, len(keys)) frames := make([]dependencyDFSFrame, 0, len(keys)) for _, root := range nodeOrder { if indegree[root] == 0 || state[root] != unvisited { continue } state[root] = active position[root] = len(path) path = append(path, root) frames = append(frames, dependencyDFSFrame{node: root}) for len(frames) != 0 { frame := &frames[len(frames)-1] if frame.next == len(outgoing[frame.node]) { state[frame.node] = finished position[frame.node] = -1 path = path[:len(path)-1] frames = frames[:len(frames)-1] continue } next := outgoing[frame.node][frame.next] frame.next++ if indegree[next] == 0 { continue } switch state[next] { case unvisited: state[next] = active position[next] = len(path) path = append(path, next) frames = append(frames, dependencyDFSFrame{node: next}) case active: return canonicalDependencyCycle(keys, path[position[next]:]) } } } // A stalled Kahn traversal always leaves a cycle in the residual graph. return nil } func canonicalDependencyCycle(keys []dependencyKey, path []int) []int { start := 0 for i := 1; i < len(path); i++ { if dependencyKeyLess(keys[path[i]], keys[path[start]]) { start = i } } cycle := make([]int, len(path)+1) for i := range path { cycle[i] = path[(start+i)%len(path)] } cycle[len(path)] = cycle[0] return cycle }