| /* |
| * Copyright 2026 WebAssembly Community Group participants |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // |
| // Weak Topological Ordering (WTO) and worklist runner for forward data flow |
| // analysis over reducible CFGs. |
| // |
| // A Weak Topological Ordering (Bourdoncle, "Efficient chaotic iteration |
| // strategies with widenings", 1993) is a hierarchical ordering of the reachable |
| // blocks of a directed graph in which strongly connected components (loops) are |
| // parenthesized into nested cycles. The first element of each cycle is its |
| // "head" (loop header). Formally, the WTO of a directed graph is a hierarchical |
| // ordering of its vertices such that for every edge u -> v, either: |
| // |
| // 1. u < v (i.e. this is a forward edge) and v is not the head of a cycle |
| // containing u. |
| // 2. u >= v (i.e. this is a backedge) and v is the head of a cycle containing |
| // u. |
| // |
| // Examples (writing `(h ...)` for a cycle with head `h`): |
| // |
| // - Diamond (0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3): |
| // 0 1 2 3 |
| // |
| // - Simple loop (0 -> 1 -> 2 -> 1, 2 -> 3): |
| // 0 (1 2) 3 |
| // Here 1 is the cycle head, 1 and 2 form the cycle body, and the exit |
| // block 3 is outside the cycle. |
| // |
| // - Nested loops (0 -> 1 -> 2 -> 3 -> 2, 3 -> 4 -> 1, 4 -> 5): |
| // 0 (1 (2 3) 4) 5 |
| // Here outer cycle (1 (2 3) 4) with head 1 encloses inner cycle (2 3) with |
| // head 2. |
| // |
| // During forward dataflow analysis, elements of the WTO are evaluated |
| // left-to-right. When a cycle `(h ...)` is reached, its elements are evaluated |
| // repeatedly in order until the head `h` is no longer re-queued by a backedge. |
| // Inner cycles therefore stabilize completely on each iteration of an enclosing |
| // outer cycle before flow values propagate past the cycle. |
| // |
| // Algorithm sketch: |
| // |
| // In a reducible CFG whose blocks are ordered in reverse postorder (RPO, as |
| // produced by cfg-traversal.h), every cycle is a natural loop headed by a |
| // single entry block that dominates all blocks in the cycle, and every |
| // backedge `p -> h` satisfies `h` dominates `p` (with `h <= p` in RPO). We |
| // construct the WTO directly from the dominator tree in three steps: |
| // |
| // 1. Compute the dominator tree (`DomTree`) over the RPO-indexed blocks. |
| // 2. Discover natural loops from innermost to outermost by scanning candidate |
| // headers `h` in reverse RPO order (N - 1 down to 0). For each `h` that |
| // has at least one backedge `p -> h` (where `h` dominates `p`), run a |
| // backward DFS over predecessors starting from `p` and stopping at `h` to |
| // visit every block in `h`'s natural loop. Because inner loop headers have |
| // larger RPO indices than outer loop headers and are processed first, the |
| // first loop that visits a block `b != h` is its immediately enclosing |
| // loop (`loopParent[b] = h`). As each loop body is discovered, we collapse |
| // its blocks into `h` using union-find so that outer loops skip over |
| // already-collapsed inner loop bodies instead of re-traversing them (both |
| // during the backward DFS and when walking the dominator tree). |
| // 3. Link each reachable block into the child list of its `loopParent` in |
| // increasing RPO order, then walk the resulting loop nesting forest to |
| // emit each loop header `h` and its children as a nested `Cycle`. |
| // |
| |
| #ifndef cfg_wto_h |
| #define cfg_wto_h |
| |
| #include <cassert> |
| #include <memory> |
| #include <vector> |
| |
| #include "cfg/domtree.h" |
| #include "wasm.h" |
| |
| namespace wasm { |
| |
| // The BasicBlock type is assumed to have an `in` vector of predecessor block |
| // pointers and a `contents.index` field of type `Index`. |
| template<typename BasicBlock> struct WeakTopologicalOrdering { |
| static constexpr Index NoIndex = Index(-1); |
| |
| // `entries` is the flattened sequence of blocks in weak topological order, |
| // plus markers for the end of cycles. |
| // |
| // For normal block entries: |
| // - cycleTarget == NoIndex |
| // - block is the corresponding basic block |
| // |
| // For end of cycle entries: |
| // - cycleTarget is the index in `entries` of the head of the cycle, i.e. |
| // where we will go next if we need to process the cycle again. |
| // - block is the basic block at the head of the cycle, used to check whether |
| // we need to process the cycle again (by checking contents.inQueue). |
| // |
| struct Entry { |
| BasicBlock* block; |
| Index cycleTarget; |
| }; |
| |
| std::vector<Entry> entries; |
| |
| WeakTopologicalOrdering(std::vector<std::unique_ptr<BasicBlock>>& blocks); |
| }; |
| |
| template<typename BasicBlock> |
| WeakTopologicalOrdering<BasicBlock>::WeakTopologicalOrdering( |
| std::vector<std::unique_ptr<BasicBlock>>& blocks) { |
| Index numBlocks = blocks.size(); |
| if (numBlocks == 0) { |
| return; |
| } |
| |
| DomTree<BasicBlock> domTree(blocks); |
| |
| auto isReachable = [&](Index i) { |
| return i == 0 || domTree.iDoms[i] != domTree.nonsense; |
| }; |
| |
| struct Node { |
| // The innermost loop header for the cycle containing this block. |
| Index loopParent = NoIndex; |
| // For loop headers, the index of their first child (i.e. the head of a |
| // linked list of children). |
| Index firstChild = NoIndex; |
| // A linked list edge to the next child with the same loop header. |
| Index nextSibling = NoIndex; |
| // The parent in the union-find forest used to collapse inner loops into |
| // their headers as they are discovered. This is either `NoIndex` if this |
| // node is the representative element of its set, or otherwise there is a |
| // path of parent pointers leading to the representative element of the set. |
| Index ufParent = NoIndex; |
| bool isLoopHeader = false; |
| }; |
| std::vector<Node> nodes(numBlocks); |
| |
| auto find = [&](Index x) { |
| Index root = x; |
| while (nodes[root].ufParent != NoIndex) { |
| root = nodes[root].ufParent; |
| } |
| // Path compression. |
| while (x != root) { |
| Index next = nodes[x].ufParent; |
| nodes[x].ufParent = root; |
| x = next; |
| } |
| return root; |
| }; |
| |
| auto dominates = [&](Index dom, Index node) { |
| assert(isReachable(dom)); |
| // Since blocks are indexed in RPO, dominators always precede the blocks |
| // they dominate. |
| if (node < dom || !isReachable(node)) { |
| return false; |
| } |
| // Walk up the dominator tree, using `find` to skip over already-collapsed |
| // inner loops. |
| Index curr = node; |
| while (curr > dom) { |
| curr = find(domTree.iDoms[curr]); |
| } |
| return curr == dom; |
| }; |
| |
| // Discover natural loops from innermost to outermost (reverse RPO order). |
| // Because inner loops are processed before outer loops, collapsing each loop |
| // body into its header with union-find records each block's immediately |
| // enclosing loop while avoiding re-traversing inner loop bodies. |
| std::vector<Index> worklist; |
| for (Index i = numBlocks; i > 0; --i) { |
| Index h = i - 1; |
| if (!isReachable(h)) { |
| continue; |
| } |
| // Check if h is the head of a loop. It is a loop header if and only if it |
| // dominates one of its predecessors. (We assume the CFG is reducible, so |
| // loop headers dominate all blocks in the loop bodies, including those that |
| // branch back to the header.) |
| for (auto* pred : blocks[h]->in) { |
| Index p = pred->contents.index; |
| if (dominates(h, p)) { |
| nodes[h].isLoopHeader = true; |
| Index rep = find(p); |
| if (rep != h) { |
| nodes[rep].loopParent = h; |
| nodes[rep].ufParent = h; |
| worklist.push_back(rep); |
| } |
| } |
| } |
| // We've initialized the worklist with all the loop tails that branch |
| // directly back to the loop header. DFS from those loop tails back to the |
| // loop header (but no further). All the blocks we find during the DFS are |
| // part of the loop body. |
| while (!worklist.empty()) { |
| Index curr = worklist.back(); |
| worklist.pop_back(); |
| for (auto* pred : blocks[curr]->in) { |
| Index p = pred->contents.index; |
| if (isReachable(p)) { |
| Index rep = find(p); |
| if (rep != h) { |
| assert(dominates(h, rep) && "Expected reducible CFG"); |
| nodes[rep].loopParent = h; |
| nodes[rep].ufParent = h; |
| worklist.push_back(rep); |
| } |
| } |
| } |
| } |
| } |
| |
| // Link each reachable block into its parent loop's intrusive child list. |
| // Prepending in reverse RPO order yields increasing RPO order. |
| Index topFirstChild = NoIndex; |
| for (Index i = numBlocks; i > 0; --i) { |
| Index idx = i - 1; |
| if (!isReachable(idx)) { |
| continue; |
| } |
| Index parent = nodes[idx].loopParent; |
| if (parent == NoIndex) { |
| // Prepend to top-level list. |
| nodes[idx].nextSibling = topFirstChild; |
| topFirstChild = idx; |
| } else { |
| // Prepend to loop header's list. |
| nodes[idx].nextSibling = nodes[parent].firstChild; |
| nodes[parent].firstChild = idx; |
| } |
| } |
| |
| // Traverse the linked lists of children, materializing them as WTO entries. |
| // Loop depth should be limited, so doing this recursively should be fine. If |
| // it ever causes an issue, we can un-recurse this. |
| entries.reserve(numBlocks * 2); |
| auto emitList = [&](auto& self, Index firstChild) -> void { |
| for (Index curr = firstChild; curr != NoIndex; |
| curr = nodes[curr].nextSibling) { |
| auto* block = blocks[curr].get(); |
| if (nodes[curr].isLoopHeader) { |
| Index startPc = entries.size(); |
| entries.push_back({block, NoIndex}); |
| self(self, nodes[curr].firstChild); |
| entries.push_back({block, startPc}); |
| } else { |
| entries.push_back({block, NoIndex}); |
| } |
| } |
| }; |
| |
| emitList(emitList, topFirstChild); |
| } |
| |
| // Given a CFG in reverse postorder (e.g. from cfg-traversal), run a forward |
| // fixed-point analysis over its basic blocks using a Weak Topological Ordering. |
| // |
| // Usage: |
| // 1. Construct `WTOWorklist work(cfg);` (which initializes `inQueue` on each |
| // block's `contents`). |
| // 2. Seed the initial block(s) to evaluate via `work.push(cfg.entry);`. |
| // 3. Call `work.run([&](BasicBlock* block) { ... });`. Inside the visitor |
| // callback, evaluate the transfer function for `block` and call |
| // `work.push(next)` for any successor whose input state changed and needs |
| // to be (re-)evaluated. |
| // |
| // The BasicBlock `contents` of the CFG must contain two fields: |
| // |
| // bool inQueue; // whether scheduled for visitation |
| // Index index; // basic block index in RPO |
| // |
| template<typename CFG> struct WTOWorklist { |
| using BasicBlock = typename CFG::BasicBlock; |
| |
| CFG& cfg; |
| |
| WTOWorklist(CFG& cfg) : cfg(cfg) { |
| for (auto& block : cfg.basicBlocks) { |
| block->contents.inQueue = false; |
| } |
| } |
| |
| void push(BasicBlock* block) { block->contents.inQueue = true; } |
| |
| template<typename VisitFn> void run(VisitFn&& visit) { |
| // If the CFG has no loops, a single reverse-postorder pass visits every |
| // reachable block in topological order without constructing DomTree or WTO. |
| if (cfg.loopTops.empty()) { |
| for (auto& block : cfg.basicBlocks) { |
| if (block->contents.inQueue) { |
| block->contents.inQueue = false; |
| visit(block.get()); |
| } |
| } |
| return; |
| } |
| // Iterate through the flattened WTO entries in reverse postorder. Visit |
| // blocks that are in the queue, which may push later blocks or loop headers |
| // to the queue. At the end of a cycle, jump back to the cycle header entry |
| // if the header was re-queued so the cycle repeats until it stabilizes. |
| WeakTopologicalOrdering<BasicBlock> wto(cfg.basicBlocks); |
| const auto& entries = wto.entries; |
| Index pc = 0; |
| Index end = entries.size(); |
| while (pc < end) { |
| const auto& entry = entries[pc]; |
| if (entry.cycleTarget == WeakTopologicalOrdering<BasicBlock>::NoIndex) { |
| if (entry.block->contents.inQueue) { |
| entry.block->contents.inQueue = false; |
| visit(entry.block); |
| } |
| ++pc; |
| } else if (entry.block->contents.inQueue) { |
| pc = entry.cycleTarget; |
| } else { |
| ++pc; |
| } |
| } |
| } |
| }; |
| |
| } // namespace wasm |
| |
| #endif // cfg_wto_h |