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