forward.go (7323B)
1 // Copyright 2026 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package dense 6 7 import ( 8 "container/heap" 9 "log" 10 "slices" 11 12 "honnef.co/go/tools/analysis/dfa" 13 "honnef.co/go/tools/internal/xtools-internal/graph" 14 ) 15 16 // Forward performs a forward monotone analysis over a control flow graph. 17 // 18 // The entry map provides initial state for entry blocks (blocks with zero 19 // predecessors). For each edge, it calls transfer(fact, edge), where fact is 20 // the analysis state on entry to edge.Pred. The transfer function must return 21 // the outgoing analysis state of the edge (which may be fact, if the edge has 22 // no effect on the analysis state). 23 func Forward[L dfa.Semilattice[Fact], Fact any, NodeID comparable](g graph.Graph[NodeID], entry map[NodeID]Fact, transfer func(from, to NodeID, fact Fact) Fact) *Analysis[Fact, NodeID] { 24 cg, nodeMap := graph.Compact(g) 25 26 nNodes := cg.NumNodes() 27 fb := &fwdBuilder[L, Fact, NodeID]{ 28 cfg: cg, 29 nodeMap: nodeMap, 30 transfer: transfer, 31 blocks: make([]blockInfo[Fact], nNodes), 32 } 33 fb.queue.init(cg) 34 35 // Initialize each node. 36 totalEdges := 0 37 for ni := range nNodes { 38 b := &fb.blocks[ni] 39 40 // Construct back-edges. 41 // 42 // I experimented with making Graph support iterating over in-edges, but 43 // in practice that just meant each Graph implementation had a copy of 44 // this logic. So instead we keep Graph as simple as possible and 45 // compute the auxiliary data in the algorithm. One drawback of this is 46 // that, for the [Transpose] graph, this information is redundant with 47 // the underlying graph. We could potentially special-case that. 48 outs := 0 49 for succID := range cg.Out(ni) { 50 succ := &fb.blocks[succID] 51 succ.preds = append(succ.preds, blockEdge{ni, outs}) 52 outs++ 53 totalEdges++ 54 } 55 56 // Initialize in & out states. 57 fact, ok := entry[nodeMap.Value(ni)] 58 if !ok { 59 fact = fb.l.Ident() 60 } 61 b.in = fact 62 b.out = slices.Repeat([]Fact{fb.l.Ident()}, outs) 63 64 // Enqueue block. 65 // 66 // It's tempting to enqueue only the entry blocks, but this is wrong. 67 // The entry map may be empty if there are no interesting entry states, 68 // but the transfer function may still introduce interesting states 69 // anywhere. 70 b.dirty = true 71 fb.queue.enqueue(ni) 72 } 73 74 // Propagate over blocks. 75 fb.propagate() 76 77 // Collect the final analysis results. 78 a := Analysis[Fact, NodeID]{ 79 nodeMap: nodeMap, 80 ins: make([]Fact, nNodes), 81 edges: make([]edgeFact[Fact], 0, totalEdges), 82 } 83 for pred := range nNodes { 84 a.ins[pred] = fb.blocks[pred].in 85 i := 0 86 for succ := range cg.Out(pred) { 87 edge := edge{pred, succ} 88 a.edges = append(a.edges, edgeFact[Fact]{edge, fb.blocks[pred].out[i]}) 89 i++ 90 } 91 } 92 slices.SortFunc(a.edges, func(a, b edgeFact[Fact]) int { return a.edge.compare(b.edge) }) 93 return &a 94 } 95 96 // fwdBuilder is the state used during [Forward] analysis. 97 type fwdBuilder[L dfa.Semilattice[Fact], Fact any, NodeID comparable] struct { 98 l L // Lattice 99 100 cfg graph.Graph[int] // Control flow graph (compact) 101 nodeMap *graph.Index[NodeID] // Map from cfg to original NodeIDs 102 103 // transfer is the edge transfer function. 104 transfer func(from, to NodeID, fact Fact) Fact 105 106 blocks []blockInfo[Fact] 107 108 queue nodeHeap 109 } 110 111 type blockInfo[Fact any] struct { 112 dirty bool // The in fact has never been propagated. 113 114 preds []blockEdge 115 116 in Fact 117 out []Fact // Corresponds to i'th out edge 118 } 119 120 type blockEdge struct { 121 node int 122 i int // Out edge index 123 } 124 125 // nodeHeap implements a heap of NodeIDs, ordered topologically. 126 // 127 // We use this ordering so forward analysis converges more quickly. 128 type nodeHeap struct { 129 heap []int // Remaining nodes in the current sweep 130 deferred []int // Nodes of next sweep 131 inQueue []int64 // Bitmap over node IDs 132 prio []int // NodeID -> priority 133 currentPrio int // Priority of last dequeued node, or -1 134 } 135 136 func (h *nodeHeap) init(g graph.Graph[int]) { 137 nNodes := g.NumNodes() 138 *h = nodeHeap{ 139 inQueue: make([]int64, (nNodes+63)/64), 140 prio: make([]int, nNodes), 141 currentPrio: -1, 142 } 143 for p, nid := range graph.ReversePostorder(g) { 144 h.prio[nid] = p 145 } 146 } 147 148 func (h *nodeHeap) enqueue(nid int) { 149 if h.inQueue[nid/64]&(1<<(nid%64)) != 0 { 150 return 151 } 152 h.inQueue[nid/64] |= 1 << (nid % 64) 153 154 if h.currentPrio >= 0 && h.prio[nid] <= h.currentPrio { 155 // This is a retreating edge, self-edge, or other update to a node 156 // already passed in this sweep. Coalesce it into the next sweep. 157 h.deferred = append(h.deferred, nid) 158 } else { 159 heap.Push(h, nid) 160 } 161 } 162 163 func (h *nodeHeap) dequeue() int { 164 if len(h.heap) == 0 { 165 // Start the next RPO sweep. 166 h.heap, h.deferred = h.deferred, h.heap[:0] 167 h.currentPrio = -1 168 heap.Init(h) 169 } 170 171 nid := h.heap[0] 172 heap.Pop(h) 173 h.inQueue[nid/64] &^= 1 << (nid % 64) 174 h.currentPrio = h.prio[nid] 175 return nid 176 } 177 178 func (h *nodeHeap) pending() bool { return len(h.heap) != 0 || len(h.deferred) != 0 } 179 func (h nodeHeap) Len() int { return len(h.heap) } 180 func (h nodeHeap) Less(i, j int) bool { return h.prio[h.heap[i]] < h.prio[h.heap[j]] } 181 func (h nodeHeap) Swap(i, j int) { h.heap[i], h.heap[j] = h.heap[j], h.heap[i] } 182 func (h *nodeHeap) Push(x any) { h.heap = append(h.heap, x.(int)) } 183 func (h *nodeHeap) Pop() any { 184 n := len(h.heap) 185 x := h.heap[n-1] 186 h.heap = h.heap[:n-1] 187 return x 188 } 189 190 func (fb *fwdBuilder[L, Fact, NodeID]) merge(a, b Fact) Fact { 191 if fb.l.Equals(a, b) { 192 return a 193 } 194 return fb.l.Merge(a, b) 195 } 196 197 func (fb *fwdBuilder[L, Fact, NodeID]) propagate() { 198 for fb.queue.pending() { 199 bi := fb.queue.dequeue() 200 block := &fb.blocks[bi] 201 202 // Merge predecessor facts to compute updated "in" fact. 203 var in Fact 204 first := true 205 for _, edge := range block.preds { 206 pred := &fb.blocks[edge.node] 207 var edgeFact Fact 208 if pred.dirty { 209 // We haven't visited this predecessor yet, so it doesn't have 210 // meaningful out facts. 211 edgeFact = fb.l.Ident() 212 } else { 213 edgeFact = pred.out[edge.i] 214 } 215 if first { 216 if debug { 217 log.Printf("propagate to node %d", bi) 218 } 219 in = edgeFact 220 first = false 221 } else { 222 in = fb.merge(in, edgeFact) 223 } 224 if debug { 225 log.Printf(" from node %d: %v", edge.node, edgeFact) 226 } 227 } 228 if first { 229 // No predecessors. 230 if debug { 231 log.Printf("node %d gets initial state", bi) 232 } 233 in = block.in 234 } 235 236 if !block.dirty && fb.l.Equals(in, block.in) { 237 // No change to block input, which means the transfer function 238 // results also won't change from the last time we ran it. 239 if debug { 240 log.Printf(" initial state unchanged: %v", in) 241 } 242 continue 243 } 244 if debug { 245 log.Printf(" new initial state: %v", in) 246 } 247 block.in = in 248 249 // Apply transfer function. 250 predID := fb.nodeMap.Value(bi) 251 i := 0 252 for succNum := range fb.cfg.Out(bi) { 253 edgeFact := fb.transfer(predID, fb.nodeMap.Value(succNum), in) 254 if block.dirty || !fb.l.Equals(block.out[i], edgeFact) { 255 // Out fact changed, so recompute the target block. 256 if debug { 257 log.Printf(" to node %d: %v", succNum, edgeFact) 258 } 259 block.out[i] = edgeFact 260 fb.queue.enqueue(succNum) 261 } else { 262 if debug { 263 log.Printf(" to node %d: no change", succNum) 264 } 265 } 266 i++ 267 } 268 269 block.dirty = false 270 } 271 }