Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 292 additions & 0 deletions src/cfg/wto.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
/*
* 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), and the ordering satisfies two properties:
//
// 1. Every non-cycle edge u -> v goes forward in the flattened ordering
// (u appears before v).
// 2. Every backedge u -> v targets the head v of a cycle that encloses both
// u and v.
//
// 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`).
// 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 <variant>
#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 {
struct Cycle;
using Element = std::variant<BasicBlock*, Cycle>;
using List = std::vector<Element>;

struct Cycle {
List elems;

BasicBlock* head() const { return std::get<BasicBlock*>(elems.front()); }
bool operator==(const Cycle&) const = default;
};

List elems;

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;
}

for (Index i = 0; i < numBlocks; ++i) {
blocks[i]->contents.index = i;
}

// TODO: Avoid building an unordered_map of block indices in DomTree when
// BasicBlock already stores its RPO index on `contents`.
DomTree<BasicBlock> domTree(blocks);

auto isReachable = [&](Index i) {
return i == 0 || domTree.iDoms[i] != domTree.nonsense;
};

auto dominates = [&](Index dom, Index node) {
assert(isReachable(dom));
if (!isReachable(node)) {
return false;
}
Index curr = node;
while (curr > dom) {
curr = domTree.iDoms[curr];
}
return curr == dom;
};

static constexpr Index NoIndex = Index(-1);
struct Node {
Index loopParent = NoIndex;
Index firstChild = NoIndex;
Index nextSibling = NoIndex;
Index lastVisitedBy = NoIndex;
bool isLoopHeader = false;
};
std::vector<Node> nodes(numBlocks);

// Discover natural loops from innermost to outermost (reverse RPO order).
// Because inner loops are processed before outer loops, the first loop whose
// natural loop body contains a block is its immediately enclosing loop.
//
// TODO: Collapse inner loops with union-find during natural loop discovery so
// outer loops do not re-traverse inner loop bodies.
std::vector<Index> worklist;
for (Index i = numBlocks; i > 0; --i) {
Index h = i - 1;
if (!isReachable(h)) {
continue;
}
nodes[h].lastVisitedBy = h;
for (auto* pred : blocks[h]->in) {
Index p = pred->contents.index;
if (dominates(h, p)) {
nodes[h].isLoopHeader = true;
if (nodes[p].lastVisitedBy != h) {
nodes[p].lastVisitedBy = h;
worklist.push_back(p);
}
}
}
while (!worklist.empty()) {
Index curr = worklist.back();
worklist.pop_back();
if (nodes[curr].loopParent == NoIndex) {
nodes[curr].loopParent = h;
}
for (auto* pred : blocks[curr]->in) {
Index p = pred->contents.index;
if (isReachable(p) && nodes[p].lastVisitedBy != h) {
assert(dominates(h, p) && "Expected reducible CFG");
nodes[p].lastVisitedBy = h;
worklist.push_back(p);
}
}
}
}

// 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) {
nodes[idx].nextSibling = topFirstChild;
topFirstChild = idx;
} else {
nodes[idx].nextSibling = nodes[parent].firstChild;
nodes[parent].firstChild = idx;
}
}

// TODO: Flatten the WTO into a single contiguous vector of entries with cycle
// jump targets to avoid per-cycle vector allocations and recursion.
auto buildList = [&](auto& self, Index firstChild, List& out) -> void {
for (Index curr = firstChild; curr != NoIndex;
curr = nodes[curr].nextSibling) {
auto* block = blocks[curr].get();
if (nodes[curr].isLoopHeader) {
Cycle cycle;
cycle.elems.emplace_back(block);
self(self, nodes[curr].firstChild, cycle.elems);
out.emplace_back(std::move(cycle));
} else {
out.emplace_back(block);
}
}
};

buildList(buildList, topFirstChild, elems);
}

// 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` and
// `index` 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) {
auto& basicBlocks = cfg.basicBlocks;
for (Index i = 0; i < basicBlocks.size(); ++i) {
auto& contents = basicBlocks[i]->contents;
contents.inQueue = false;
contents.index = i;
}
}

void push(BasicBlock* block) { block->contents.inQueue = true; }

template<typename VisitFn> void run(VisitFn&& visit) {
// TODO: Track the number of queued blocks to stop early once the worklist
// is empty.
// TODO: Fast-path initial entry singletons and CFGs without backedges
// without building DomTree or WTO, using CFGWalker::loopTops.
WeakTopologicalOrdering<BasicBlock> wto(cfg.basicBlocks);
auto evalList =
[&](auto& self,
const typename WeakTopologicalOrdering<BasicBlock>::List& list)
-> void {
for (const auto& elem : list) {
if (auto* block = std::get_if<BasicBlock*>(&elem)) {
if ((*block)->contents.inQueue) {
(*block)->contents.inQueue = false;
visit(*block);
}
} else {
const auto& cycle =
std::get<typename WeakTopologicalOrdering<BasicBlock>::Cycle>(elem);
BasicBlock* head = cycle.head();
do {
self(self, cycle.elems);
} while (head->contents.inQueue);
}
}
};
evalList(evalList, wto.elems);
}
};

} // namespace wasm

#endif // cfg_wto_h
14 changes: 6 additions & 8 deletions src/passes/ConstraintAnalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
#include <algorithm>

#include "cfg/cfg-traversal.h"
#include "cfg/rpo.h"
#include "cfg/wto.h"
#include "ir/constraint.h"
#include "ir/drop.h"
#include "ir/eh-utils.h"
Expand Down Expand Up @@ -97,7 +97,7 @@ namespace {

// Information in a basic block.
struct Info {
// For RPOQueue
// For WTOWorklist
bool inQueue;
Index index;

Expand Down Expand Up @@ -258,12 +258,10 @@ struct ConstraintAnalysis
}

// Starting from the entry, keep going while we find something new.
RPOQueue<ConstraintAnalysis> work(*this);
WTOWorklist<ConstraintAnalysis> work(*this);
work.push(entry);

while (!work.empty()) {
auto* block = work.pop();

work.run([&](BasicBlock* block) {
// Start at the top of the block, then go through, applying things.
BasicBlockConstraintMap constraints = block->contents.startConstraints;

Expand All @@ -280,7 +278,7 @@ struct ConstraintAnalysis

if (constraints.unreachable) {
// Nothing to send.
continue;
return;
}

#if CONSTRAINT_DEBUG
Expand Down Expand Up @@ -323,7 +321,7 @@ struct ConstraintAnalysis
}
}
}
}
});
}

// If we change types, we must refinalize.
Expand Down
18 changes: 9 additions & 9 deletions src/passes/RedundantSetElimination.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
//

#include <cfg/cfg-traversal.h>
#include <cfg/rpo.h>
#include <cfg/wto.h>
#include <ir/literal-utils.h>
#include <ir/numbering.h>
#include <ir/properties.h>
Expand All @@ -58,7 +58,7 @@ namespace {

// information in a basic block
struct Info {
// For RPOQueue
// For WTOWorklist
bool inQueue;
Index index;

Expand Down Expand Up @@ -225,12 +225,12 @@ struct RedundantSetElimination
end[i] = unseenValue;
}
}
// Keep working while stuff is flowing, in reverse-postorder so that we
// reach code after its predecessors, avoiding wasted recomputation.
RPOQueue<RedundantSetElimination> work(*this);
// Keep working while stuff is flowing, in weak topological order so that we
// reach code after its predecessors and stabilize inner loops before outer
// loops, avoiding wasted recomputation.
WTOWorklist<RedundantSetElimination> work(*this);
work.push(entry);
while (!work.empty()) {
auto* curr = work.pop();
work.run([&](BasicBlock* curr) {
#if RSE_DEBUG
std::cout << "flow block " << curr << '\n';
#endif
Expand Down Expand Up @@ -325,7 +325,7 @@ struct RedundantSetElimination
// note that the first iteration this is always not the case,
// since end contains unseen (and then the comparison ends on
// the first element)
continue;
return;
}
// update the end state and update children
#ifndef NDEBUG
Expand All @@ -342,7 +342,7 @@ struct RedundantSetElimination
for (auto* next : curr->out) {
work.push(next);
}
}
});
}

// optimizing
Expand Down
Loading
Loading