blob: 8784d6bc253584d93dbe6d8de48a481e85643cd8 [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 ssa
import (
"container/heap"
"iter"
)
// DF(x), the dominance frontier of x, holds every block y such that x
// dominates a predecessor of y but does not strictly dominate y. Its
// transitive closure DF+ (also called the merge set) is where a phi
// may need to be placed for a variable defined in x. DF+ of a set of
// blocks S, denoted IDF(S) (iterated dominance frontier), is the union
// of the DF+ of the blocks in S.
// IterDomFrontierPlus iterates the DF+ of seeds: every block at which
// a phi may need to be placed if a variable were defined in the seed
// blocks. For each frontier block, it also yields the block whose
// outgoing edge discovered the frontier. Frontier blocks are yielded
// at most once, in a deterministic order; an early break stops the walk.
// Seed blocks themselves are not yielded as such, but a seed that is
// also a merge point (e.g. a loop header) is.
// seeds iterator is consumed in full before the walk starts (the current
// algorithm has to walk deeper roots first).
// CFG must not change while iteration is in progress; inserting
// values (like phis) is fine.
func (f *Func) IterDomFrontierPlus(seeds iter.Seq[*Block]) iter.Seq2[*Block, *Block] {
return func(yield func(*Block, *Block) bool) {
// Materialize the seeds into a pooled slice reused by walkDFPlus.
s := f.Cache.AllocBlockSlice(f.NumBlocks())[:0]
defer f.Cache.FreeBlockSlice(s[:cap(s)])
for b := range seeds {
s = append(s, b)
}
f.walkDFPlus(s, yield)
}
}
// Per-block state of a DF+ walk, packed into one flag byte per block.
// None of the bits is cleared during the walk. Each of the following happens at
// most once per block:
// - enters the work queue,
// - is banked as a root,
// - is yielded.
const (
// The block's subtree walk is done or pending on q.
flagQueued = 1 << iota
// The block has been added to the PiggyBank: a seed, or a block
// yielded earlier in this walk.
flagPiggyBanked
// The block has been yielded to the caller.
flagYielded
)
// walkDFPlus is the engine under IterDomFrontierPlus.
// The walk is the Sreedhar & Gao DJ-graph algorithm, "A Linear Time
// Algorithm for Placing Φ-Nodes". Work is proportional to the dominator
// subtrees walked (skipping subtrees already covered, deeper roots)
// plus the frontier found, and memory is O(f.NumBlocks()). The walk reads
// the CFG's edges and uses the cached dominator tree.
// The seeds slice is reused in place by the PiggyBank.
func (f *Func) walkDFPlus(seeds []*Block, yield func(*Block, *Block) bool) {
sdom := f.Sdom()
// Roots to process, deepest first.
piggyBank := blockHeap{t: sdom, a: seeds[:0]}
// The worklist is a pooled slice, freed after the walk is done.
// Each block enters it at most once, so it never outgrows its capacity.
q := f.Cache.AllocBlockSlice(f.NumBlocks())[:0]
defer f.Cache.FreeBlockSlice(q[:cap(q)])
// per-block walk state; see the flag constants above.
flags := f.Cache.AllocInt8Slice(f.NumBlocks())
defer f.Cache.FreeInt8Slice(flags)
// Bank the seeds as roots, compacting in place to drop duplicates.
for _, b := range seeds {
if flags[b.ID]&flagPiggyBanked == 0 {
flags[b.ID] |= flagPiggyBanked
piggyBank.a = append(piggyBank.a, b)
}
}
heap.Init(&piggyBank)
// Visit the roots from deepest to shallowest.
for len(piggyBank.a) > 0 {
currentRoot := heap.Pop(&piggyBank).(*Block)
// Walk the subtree below the root, skipping subtrees already
// covered by previous (deeper) roots, and find the edges
// exiting it: their targets are the dominance frontier.
// Roots are popped deepest first, so any block a later root's walk could
// queue lies strictly below that root and was already queued
// by its own root-push.
if flags[currentRoot.ID]&flagQueued != 0 {
f.Fatalf("root already in queue")
}
flags[currentRoot.ID] |= flagQueued
q = append(q, currentRoot)
for len(q) > 0 {
b := q[len(q)-1]
q = q[:len(q)-1]
currentRootLevel := sdom.Level(currentRoot)
for _, e := range b.Succs {
c := e.Block()
if sdom.Level(c) > currentRootLevel {
// a D-edge, or an edge whose target is in currentRoot's subtree.
continue
}
if flags[c.ID]&flagYielded != 0 {
continue
}
flags[c.ID] |= flagYielded
if flags[c.ID]&flagPiggyBanked == 0 {
// Bank c as a root; its subtree may find further frontier edges.
// Invariant: piggyBanked = seeds ∪ yielded
flags[c.ID] |= flagPiggyBanked
heap.Push(&piggyBank, c)
}
if !yield(c, b) {
return
}
}
// Visit children if they have not been visited yet.
for ch := sdom.Child(b); ch != nil; ch = sdom.Sibling(ch) {
if flags[ch.ID]&flagQueued == 0 {
flags[ch.ID] |= flagQueued
q = append(q, ch)
}
}
}
}
}
// A block heap is used as a priority queue to implement the PiggyBank
// from Sreedhar and Gao. That paper uses an array which is better
// asymptotically but worse in the common case when the PiggyBank
// holds a sparse set of blocks.
type blockHeap struct {
a []*Block // blocks in heap
t SparseTree // dominator tree; provides block levels for priority
}
func (h *blockHeap) Len() int { return len(h.a) }
func (h *blockHeap) Swap(i, j int) { a := h.a; a[i], a[j] = a[j], a[i] }
func (h *blockHeap) Push(x any) {
v := x.(*Block)
h.a = append(h.a, v)
}
func (h *blockHeap) Pop() any {
old := h.a
n := len(old)
x := old[n-1]
h.a = old[:n-1]
return x
}
func (h *blockHeap) Less(i, j int) bool {
return h.t.Level(h.a[i]) > h.t.Level(h.a[j])
}