blob: d2cb27ce5a7dc7ad9b85ec7a4d2678045a0e7f97 [file]
// 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
}
}