| // 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 |
| } |
| } |