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