201 lines
5.4 KiB
Go
201 lines
5.4 KiB
Go
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
|
|
}
|