| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package flow | |
| import ( | |
| "container/heap" | |
| "log" | |
| "slices" | |
| "golang.org/x/tools/internal/graph" | |
| ) | |
| // Forward performs a forward monotone analysis over a control flow graph. | |
| // | |
| // The entry map provides initial state for entry blocks (blocks with zero | |
| // predecessors). For each edge, it calls transfer(fact, edge), where fact is | |
| // the analysis state on entry to edge.Pred. The transfer function must return | |
| // the outgoing analysis state of the edge (which may be fact, if the edge has | |
| // no effect on the analysis state). | |
| func Forward[L 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] { | |
| cg, nodeMap := graph.Compact(g) | |
| nNodes := cg.NumNodes() | |
| fb := &fwdBuilder[L, Fact, NodeID]{ | |
| cfg: cg, | |
| nodeMap: nodeMap, | |
| transfer: transfer, | |
| blocks: make([]blockInfo[Fact], nNodes), | |
| } | |
| fb.queue.init(cg) | |
| // Initialize each node. | |
| totalEdges := 0 | |
| for ni := range nNodes { | |
| b := &fb.blocks[ni] | |
| // Construct back-edges. | |
| // | |
| // I experimented with making Graph support iterating over in-edges, but | |
| // in practice that just meant each Graph implementation had a copy of | |
| // this logic. So instead we keep Graph as simple as possible and | |
| // compute the auxiliary data in the algorithm. One drawback of this is | |
| // that, for the [Transpose] graph, this information is redundant with | |
| // the underlying graph. We could potentially special-case that. | |
| outs := 0 | |
| for succID := range cg.Out(ni) { | |
| succ := &fb.blocks[succID] | |
| succ.preds = append(succ.preds, blockEdge{ni, outs}) | |
| outs++ | |
| totalEdges++ | |
| } | |
| // Initialize in & out states. | |
| fact, ok := entry[nodeMap.Value(ni)] | |
| if !ok { | |
| fact = fb.l.Ident() | |
| } | |
| b.in = fact | |
| b.out = slices.Repeat([]Fact{fb.l.Ident()}, outs) | |
| // Enqueue block. | |
| // | |
| // It's tempting to enqueue only the entry blocks, but this is wrong. | |
| // The entry map may be empty if there are no interesting entry states, | |
| // but the transfer function may still introduce interesting states | |
| // anywhere. | |
| b.dirty = true | |
| fb.queue.enqueue(ni) | |
| } | |
| // Propagate over blocks. | |
| fb.propagate() | |
| // Collect the final analysis results. | |
| a := Analysis[Fact, NodeID]{ | |
| nodeMap: nodeMap, | |
| ins: make([]Fact, nNodes), | |
| edges: make([]edgeFact[Fact], 0, totalEdges), | |
| } | |
| for pred := range nNodes { | |
| a.ins[pred] = fb.blocks[pred].in | |
| i := 0 | |
| for succ := range cg.Out(pred) { | |
| edge := edge{pred, succ} | |
| a.edges = append(a.edges, edgeFact[Fact]{edge, fb.blocks[pred].out[i]}) | |
| i++ | |
| } | |
| } | |
| slices.SortFunc(a.edges, func(a, b edgeFact[Fact]) int { return a.edge.compare(b.edge) }) | |
| return &a | |
| } | |
| // fwdBuilder is the state used during [Forward] analysis. | |
| type fwdBuilder[L Semilattice[Fact], Fact any, NodeID comparable] struct { | |
| l L // Lattice | |
| cfg graph.Graph[int] // Control flow graph (compact) | |
| nodeMap *graph.Index[NodeID] // Map from cfg to original NodeIDs | |
| // transfer is the edge transfer function. | |
| transfer func(from, to NodeID, fact Fact) Fact | |
| blocks []blockInfo[Fact] | |
| queue nodeHeap | |
| } | |
| type blockInfo[Fact any] struct { | |
| dirty bool // The in fact has never been propagated. | |
| preds []blockEdge | |
| in Fact | |
| out []Fact // Corresponds to i'th out edge | |
| } | |
| type blockEdge struct { | |
| node int | |
| i int // Out edge index | |
| } | |
| // nodeHeap implements a heap of NodeIDs, ordered topologically. | |
| // | |
| // We use this ordering so forward analysis converges more quickly. | |
| type nodeHeap struct { | |
| heap []int | |
| inQueue []int64 // Bitmap over node IDs | |
| prio []int // NodeID -> priority | |
| } | |
| func (h *nodeHeap) init(g graph.Graph[int]) { | |
| nNodes := g.NumNodes() | |
| *h = nodeHeap{ | |
| inQueue: make([]int64, (nNodes+63)/64), | |
| prio: make([]int, nNodes), | |
| } | |
| for p, nid := range graph.ReversePostorder(g) { | |
| h.prio[nid] = p | |
| } | |
| } | |
| func (h *nodeHeap) enqueue(nid int) { | |
| if h.inQueue[nid/64]&(1<<(nid%64)) != 0 { | |
| return | |
| } | |
| h.inQueue[nid/64] |= 1 << (nid % 64) | |
| heap.Push(h, nid) | |
| } | |
| func (h *nodeHeap) dequeue() int { | |
| nid := h.heap[0] | |
| heap.Pop(h) | |
| h.inQueue[nid/64] &^= 1 << (nid % 64) | |
| return nid | |
| } | |
| func (h nodeHeap) Len() int { return len(h.heap) } | |
| func (h nodeHeap) Less(i, j int) bool { return h.prio[h.heap[i]] < h.prio[h.heap[j]] } | |
| func (h nodeHeap) Swap(i, j int) { h.heap[i], h.heap[j] = h.heap[j], h.heap[i] } | |
| func (h *nodeHeap) Push(x any) { h.heap = append(h.heap, x.(int)) } | |
| func (h *nodeHeap) Pop() any { | |
| n := len(h.heap) | |
| x := h.heap[n-1] | |
| h.heap = h.heap[:n-1] | |
| return x | |
| } | |
| func (fb *fwdBuilder[L, Fact, NodeID]) merge(a, b Fact) Fact { | |
| if fb.l.Equals(a, b) { | |
| return a | |
| } | |
| return fb.l.Merge(a, b) | |
| } | |
| func (fb *fwdBuilder[L, Fact, NodeID]) propagate() { | |
| for fb.queue.Len() > 0 { | |
| bi := fb.queue.dequeue() | |
| block := &fb.blocks[bi] | |
| // Merge predecessor facts to compute updated "in" fact. | |
| var in Fact | |
| first := true | |
| for _, edge := range block.preds { | |
| pred := &fb.blocks[edge.node] | |
| var edgeFact Fact | |
| if pred.dirty { | |
| // We haven't visited this predecessor yet, so it doesn't have | |
| // meaningful out facts. | |
| edgeFact = fb.l.Ident() | |
| } else { | |
| edgeFact = pred.out[edge.i] | |
| } | |
| if first { | |
| if debug { | |
| log.Printf("propagate to node %d", bi) | |
| } | |
| in = edgeFact | |
| first = false | |
| } else { | |
| in = fb.merge(in, edgeFact) | |
| } | |
| if debug { | |
| log.Printf(" from node %d: %v", edge.node, edgeFact) | |
| } | |
| } | |
| if first { | |
| // No predecessors. | |
| if debug { | |
| log.Printf("node %d gets initial state", bi) | |
| } | |
| in = block.in | |
| } | |
| if !block.dirty && fb.l.Equals(in, block.in) { | |
| // No change to block input, which means the transfer function | |
| // results also won't change from the last time we ran it. | |
| if debug { | |
| log.Printf(" initial state unchanged: %v", in) | |
| } | |
| continue | |
| } | |
| if debug { | |
| log.Printf(" new initial state: %v", in) | |
| } | |
| block.in = in | |
| // Apply transfer function. | |
| predID := fb.nodeMap.Value(bi) | |
| i := 0 | |
| for succNum := range fb.cfg.Out(bi) { | |
| edgeFact := fb.transfer(predID, fb.nodeMap.Value(succNum), in) | |
| if block.dirty || !fb.l.Equals(block.out[i], edgeFact) { | |
| // Out fact changed, so recompute the target block. | |
| if debug { | |
| log.Printf(" to node %d: %v", succNum, edgeFact) | |
| } | |
| block.out[i] = edgeFact | |
| fb.queue.enqueue(succNum) | |
| } else { | |
| if debug { | |
| log.Printf(" to node %d: no change", succNum) | |
| } | |
| } | |
| i++ | |
| } | |
| block.dirty = false | |
| } | |
| } |