cuelang.org/go@v0.13.0/internal/core/toposort/graph.go (about) 1 // Copyright 2024 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package toposort 16 17 import ( 18 "cmp" 19 "slices" 20 21 "cuelang.org/go/internal/core/adt" 22 ) 23 24 const ( 25 NodeUnsorted = -1 26 ) 27 28 type Graph struct { 29 nodes Nodes 30 } 31 32 type Node struct { 33 Feature adt.Feature 34 Outgoing Nodes 35 Incoming Nodes 36 structMeta *structMeta 37 // temporary state for calculating the Strongly Connected 38 // Components of a graph. 39 sccNodeState *sccNodeState 40 position int 41 } 42 43 func (n *Node) IsSorted() bool { 44 return n.position >= 0 45 } 46 47 // SafeName returns a string useful for debugging, regardless of the 48 // type of the feature. So for IntLabels, you'll get back `1`, `10` 49 // etc; for identifiers, you may get back a string with quotes in it, 50 // eg `"runs-on"`. So this is not useful for comparisons, but it is 51 // useful (and safe) for debugging. 52 func (n *Node) SafeName(index adt.StringIndexer) string { 53 return n.Feature.SelectorString(index) 54 } 55 56 type Nodes []*Node 57 58 func (nodes Nodes) Features() []adt.Feature { 59 features := make([]adt.Feature, len(nodes)) 60 for i, node := range nodes { 61 features[i] = node.Feature 62 } 63 return features 64 } 65 66 type edge struct { 67 from adt.Feature 68 to adt.Feature 69 } 70 71 type GraphBuilder struct { 72 allowEdges bool 73 edgesSet map[edge]struct{} 74 nodesByFeature map[adt.Feature]*Node 75 } 76 77 // NewGraphBuilder is the constructor for GraphBuilder. 78 // 79 // If you disallow edges, then nodes can still be added to the graph, 80 // and the [GraphBuilder.AddEdge] method will not error, but edges 81 // will never be added between nodes. This has the effect that 82 // topological ordering is not possible. 83 func NewGraphBuilder(allowEdges bool) *GraphBuilder { 84 return &GraphBuilder{ 85 allowEdges: allowEdges, 86 edgesSet: make(map[edge]struct{}), 87 nodesByFeature: make(map[adt.Feature]*Node), 88 } 89 } 90 91 // Adds an edge between the two features. Nodes for the features will 92 // be created if they don't already exist. This method is idempotent: 93 // multiple calls with the same arguments will not create multiple 94 // edges, nor error. 95 func (builder *GraphBuilder) AddEdge(from, to adt.Feature) { 96 if !builder.allowEdges { 97 builder.EnsureNode(from) 98 builder.EnsureNode(to) 99 return 100 } 101 102 edge := edge{from: from, to: to} 103 if _, found := builder.edgesSet[edge]; found { 104 return 105 } 106 107 builder.edgesSet[edge] = struct{}{} 108 fromNode := builder.EnsureNode(from) 109 toNode := builder.EnsureNode(to) 110 fromNode.Outgoing = append(fromNode.Outgoing, toNode) 111 toNode.Incoming = append(toNode.Incoming, fromNode) 112 } 113 114 // Ensure that a node for this feature exists. This is necessary for 115 // features that are not necessarily connected to any other feature. 116 func (builder *GraphBuilder) EnsureNode(feature adt.Feature) *Node { 117 node, found := builder.nodesByFeature[feature] 118 if !found { 119 node = &Node{Feature: feature, position: NodeUnsorted} 120 builder.nodesByFeature[feature] = node 121 } 122 return node 123 } 124 125 func (builder *GraphBuilder) Build() *Graph { 126 nodesByFeature := builder.nodesByFeature 127 nodes := make(Nodes, 0, len(nodesByFeature)) 128 for _, node := range nodesByFeature { 129 nodes = append(nodes, node) 130 } 131 return &Graph{nodes: nodes} 132 } 133 134 type indexComparison struct{ adt.StringIndexer } 135 136 func (index *indexComparison) compareNodeByName(a, b *Node) int { 137 aFeature, bFeature := a.Feature, b.Feature 138 aIsInt, bIsInt := aFeature.Typ() == adt.IntLabel, bFeature.Typ() == adt.IntLabel 139 140 switch { 141 case aIsInt && bIsInt: 142 return cmp.Compare(aFeature.Index(), bFeature.Index()) 143 case aIsInt: 144 return -1 145 case bIsInt: 146 return 1 147 default: 148 return cmp.Compare(aFeature.RawString(index), bFeature.RawString(index)) 149 } 150 } 151 152 func (index *indexComparison) compareComponentsByNodes(a, b *StronglyConnectedComponent) int { 153 return slices.CompareFunc(a.Nodes, b.Nodes, index.compareNodeByName) 154 } 155 156 // Sort the features of the graph into a single slice. 157 // 158 // As far as possible, a topological sort is used. We first calculate 159 // the strongly-connected-components (SCCs) of the graph. If the graph 160 // has no cycles then there will be 1 SCC per graph node, which we 161 // then walk topologically. When there is a choice as to which SCC to 162 // enter into next, a lexicographical comparison is done, and minimum 163 // feature chosen. 164 // 165 // If the graph has cycles, then there will be at least one SCC 166 // containing several nodes. When we choose to enter this SCC, we use 167 // a lexicographical ordering of its nodes. This avoids the need for 168 // expensive and complex analysis of cycles: the maximum possible 169 // number of cycles rises with the factorial of the number of nodes in 170 // a component. 171 func (graph *Graph) Sort(index adt.StringIndexer) []adt.Feature { 172 indexCmp := &indexComparison{index} 173 174 nodesSorted := make(Nodes, 0, len(graph.nodes)) 175 176 scc := graph.StronglyConnectedComponents() 177 var sccReady []*StronglyConnectedComponent 178 for _, component := range scc { 179 component.visited = false 180 slices.SortFunc(component.Nodes, indexCmp.compareNodeByName) 181 if len(component.Incoming) == 0 { 182 sccReady = append(sccReady, component) 183 } 184 } 185 slices.SortFunc(sccReady, indexCmp.compareComponentsByNodes) 186 187 sccVisitedCount := 0 188 for sccVisitedCount != len(scc) { 189 sccCurrent := sccReady[0] 190 sccReady = sccReady[1:] 191 if sccCurrent.visited { 192 continue 193 } 194 sccCurrent.visited = true 195 sccVisitedCount++ 196 debug("scc current: %p %v\n", sccCurrent, sccCurrent) 197 198 nodesSorted = appendNodes(nodesSorted, sccCurrent.Nodes) 199 200 sccReadyNeedsSorting := false 201 SccNextOutgoing: 202 for _, next := range sccCurrent.Outgoing { 203 for _, required := range next.Incoming { 204 if !required.visited { 205 continue SccNextOutgoing 206 } 207 } 208 sccReady = append(sccReady, next) 209 sccReadyNeedsSorting = true 210 } 211 if sccReadyNeedsSorting { 212 slices.SortFunc(sccReady, indexCmp.compareComponentsByNodes) 213 } 214 } 215 216 return nodesSorted.Features() 217 } 218 219 func appendNodes(nodesSorted, nodesReady Nodes) Nodes { 220 for i, node := range nodesReady { 221 node.position = len(nodesSorted) + i 222 } 223 nodesSorted = append(nodesSorted, nodesReady...) 224 return nodesSorted 225 } 226 227 func debug(formatting string, args ...any) { 228 // fmt.Printf(formatting, args...) 229 }