| /* |
| * 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. |
| */ |
| |
| #include <memory> |
| #include <sstream> |
| #include <unordered_map> |
| #include <unordered_set> |
| #include <variant> |
| #include <vector> |
| |
| #include "cfg/wto.h" |
| #include "gtest/gtest.h" |
| |
| using namespace wasm; |
| |
| namespace { |
| |
| // The WTO utility is parameterized on the BasicBlock type associated with a |
| // CFG. Since BasicBlock itself contains a user-provided Contents type, there is |
| // no canonical BasicBlock type ready to use. Since we don't need a full |
| // CFGWalker, just create a mini version of CFGWalker for testing that has all |
| // the expected associated types and fields. |
| struct TestCFG { |
| struct Contents { |
| bool inQueue = false; |
| Index index = 0; |
| }; |
| |
| struct BasicBlock { |
| Contents contents; |
| std::vector<BasicBlock*> out; |
| std::vector<BasicBlock*> in; |
| }; |
| |
| std::vector<std::unique_ptr<BasicBlock>> basicBlocks; |
| std::vector<BasicBlock*> loopTops; |
| BasicBlock* entry = nullptr; |
| |
| explicit TestCFG(Index numBlocks) { |
| basicBlocks.reserve(numBlocks); |
| for (Index i = 0; i < numBlocks; ++i) { |
| auto block = std::make_unique<BasicBlock>(); |
| block->contents.index = i; |
| basicBlocks.push_back(std::move(block)); |
| } |
| if (numBlocks > 0) { |
| entry = basicBlocks[0].get(); |
| } |
| } |
| |
| void addEdge(Index u, Index v) { |
| assert(u < basicBlocks.size()); |
| assert(v < basicBlocks.size()); |
| basicBlocks[u]->out.push_back(basicBlocks[v].get()); |
| basicBlocks[v]->in.push_back(basicBlocks[u].get()); |
| if (u >= v) { |
| loopTops.push_back(basicBlocks[v].get()); |
| } |
| } |
| }; |
| |
| // Index-based mirror of a Weak Topological Ordering used in tests so that: |
| // 1. Expected orderings can be written concisely with block indices (e.g. |
| // `WTOList{0, C({1, 2}), 3}`) and pretty-printed on failure. |
| // 2. Test assertions remain independent of the internal representation of |
| // `WeakTopologicalOrdering` (which will be flattened into a contiguous |
| // entry array in a follow-on commit). |
| struct WTOCycle; |
| struct WTOElem; |
| using WTOList = std::vector<WTOElem>; |
| |
| struct WTOCycle { |
| WTOList elems; |
| |
| WTOCycle(std::initializer_list<WTOElem> list); |
| explicit WTOCycle(WTOList elems); |
| |
| Index head() const; |
| bool operator==(const WTOCycle& other) const; |
| }; |
| |
| struct WTOElem : std::variant<Index, WTOCycle> { |
| using Base = std::variant<Index, WTOCycle>; |
| using Base::Base; |
| WTOElem(Index v) : Base(v) {} |
| WTOElem(int v) : Base(Index(v)) {} |
| WTOElem(WTOCycle c) : Base(std::move(c)) {} |
| }; |
| |
| WTOCycle::WTOCycle(std::initializer_list<WTOElem> list) : elems(list) {} |
| WTOCycle::WTOCycle(WTOList elems) : elems(std::move(elems)) {} |
| Index WTOCycle::head() const { return std::get<Index>(elems.front()); } |
| bool WTOCycle::operator==(const WTOCycle& other) const { |
| return elems == other.elems; |
| } |
| |
| WTOCycle C(std::initializer_list<WTOElem> list) { return WTOCycle(list); } |
| |
| std::ostream& operator<<(std::ostream& os, const WTOElem& elem); |
| std::ostream& operator<<(std::ostream& os, const WTOList& list); |
| |
| std::ostream& operator<<(std::ostream& os, const WTOCycle& cycle) { |
| return os << "(" << cycle.elems << ")"; |
| } |
| |
| std::ostream& operator<<(std::ostream& os, const WTOElem& elem) { |
| if (auto* v = std::get_if<Index>(&elem)) { |
| return os << *v; |
| } |
| return os << std::get<WTOCycle>(elem); |
| } |
| |
| std::ostream& operator<<(std::ostream& os, const WTOList& list) { |
| for (size_t i = 0; i < list.size(); ++i) { |
| if (i > 0) { |
| os << " "; |
| } |
| os << list[i]; |
| } |
| return os; |
| } |
| |
| using BasicBlock = TestCFG::BasicBlock; |
| |
| WTOList toIndexWTO(const WeakTopologicalOrdering<BasicBlock>& wto) { |
| constexpr Index NoIndex = WeakTopologicalOrdering<BasicBlock>::NoIndex; |
| std::vector<std::pair<Index, WTOElem>> stack; |
| for (Index pc = 0; pc < wto.entries.size(); ++pc) { |
| const auto& entry = wto.entries[pc]; |
| if (entry.cycleTarget == NoIndex) { |
| stack.emplace_back(pc, WTOElem(entry.block->contents.index)); |
| } else { |
| EXPECT_EQ(entry.block, wto.entries[entry.cycleTarget].block); |
| size_t start = stack.size(); |
| while (stack[start - 1].first != entry.cycleTarget) { |
| --start; |
| } |
| WTOList cycleElems; |
| for (size_t i = start - 1; i < stack.size(); ++i) { |
| cycleElems.push_back(std::move(stack[i].second)); |
| } |
| stack.erase(stack.begin() + start, stack.end()); |
| stack.back() = {NoIndex, WTOElem(WTOCycle(std::move(cycleElems)))}; |
| } |
| } |
| WTOList dst; |
| for (auto& [_, elem] : stack) { |
| dst.push_back(std::move(elem)); |
| } |
| return dst; |
| } |
| |
| // Check the formal properties of a Weak Topological Ordering (Bourdoncle 1993, |
| // Definition 1) over the reachable subgraph of `cfg`. |
| void verifyWTOInvariants(const TestCFG& cfg, const WTOList& wto) { |
| std::vector<Index> flatOrder; |
| std::unordered_map<Index, size_t> pos; |
| std::unordered_map<Index, std::unordered_set<Index>> cycleMembers; |
| |
| auto walk = [&](auto& self, |
| const WTOList& list, |
| std::vector<Index>& activeHeads) -> void { |
| for (const auto& elem : list) { |
| if (auto* v = std::get_if<Index>(&elem)) { |
| // Every vertex appears at most once in the flattened ordering. |
| EXPECT_FALSE(pos.contains(*v)) << "Duplicate vertex " << *v; |
| pos[*v] = flatOrder.size(); |
| flatOrder.push_back(*v); |
| for (Index head : activeHeads) { |
| cycleMembers[head].insert(*v); |
| } |
| } else { |
| // Every cycle is non-empty and its head (first element) is a single |
| // vertex, not a nested cycle. |
| const auto& cycle = std::get<WTOCycle>(elem); |
| ASSERT_FALSE(cycle.elems.empty()) << "Empty cycle in WTO"; |
| ASSERT_TRUE(std::holds_alternative<Index>(cycle.elems.front())) |
| << "Cycle head must be a single vertex, not a nested cycle"; |
| Index head = cycle.head(); |
| activeHeads.push_back(head); |
| self(self, cycle.elems, activeHeads); |
| activeHeads.pop_back(); |
| } |
| } |
| }; |
| |
| std::vector<Index> activeHeads; |
| walk(walk, wto, activeHeads); |
| |
| // Check invariants for edges u -> v. |
| for (Index u : flatOrder) { |
| for (auto* succ : cfg.basicBlocks[u]->out) { |
| Index v = succ->contents.index; |
| // Every reachable vertex appears at least once in the ordering. |
| ASSERT_TRUE(pos.contains(v)) |
| << "Reachable vertex " << v << " missing from WTO"; |
| if (pos[u] >= pos[v]) { |
| // Backedges must be to the head of a cycle containing both v and u. |
| ASSERT_TRUE(cycleMembers.contains(v)) |
| << "Back-edge " << u << " -> " << v << " targets non-head vertex " |
| << v; |
| EXPECT_TRUE(cycleMembers[v].contains(u)) |
| << "Back-edge " << u << " -> " << v |
| << " is not enclosed in the cycle headed by " << v; |
| } |
| } |
| } |
| } |
| |
| WTOList getWTO(TestCFG& cfg) { |
| WeakTopologicalOrdering<BasicBlock> wto(cfg.basicBlocks); |
| auto list = toIndexWTO(wto); |
| EXPECT_EQ(list, list); |
| verifyWTOInvariants(cfg, list); |
| return list; |
| } |
| |
| } // namespace |
| |
| TEST(WTOTest, Empty) { |
| TestCFG cfg(0); |
| EXPECT_EQ(getWTO(cfg), WTOList{}); |
| } |
| |
| TEST(WTOTest, Singleton) { |
| TestCFG cfg(1); |
| EXPECT_EQ(getWTO(cfg), WTOList{0}); |
| } |
| |
| TEST(WTOTest, LinearChain) { |
| TestCFG cfg(3); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 2); |
| EXPECT_EQ(getWTO(cfg), (WTOList{0, 1, 2})); |
| } |
| |
| TEST(WTOTest, Diamond) { |
| { |
| TestCFG cfg(4); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(0, 2); |
| cfg.addEdge(1, 3); |
| cfg.addEdge(2, 3); |
| EXPECT_EQ(getWTO(cfg), (WTOList{0, 1, 2, 3})); |
| } |
| { |
| // Reversed edge insertion order at the split and join. |
| TestCFG cfg(4); |
| cfg.addEdge(0, 2); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(2, 3); |
| cfg.addEdge(1, 3); |
| EXPECT_EQ(getWTO(cfg), (WTOList{0, 1, 2, 3})); |
| } |
| { |
| // Asymmetric diamond (one arm has two blocks, the other has one) under both |
| // valid RPO block orderings. |
| TestCFG leftFirst(5); |
| leftFirst.addEdge(0, 1); |
| leftFirst.addEdge(1, 2); |
| leftFirst.addEdge(0, 3); |
| leftFirst.addEdge(2, 4); |
| leftFirst.addEdge(3, 4); |
| EXPECT_EQ(getWTO(leftFirst), (WTOList{0, 1, 2, 3, 4})); |
| |
| TestCFG rightFirst(5); |
| rightFirst.addEdge(0, 2); |
| rightFirst.addEdge(2, 3); |
| rightFirst.addEdge(0, 1); |
| rightFirst.addEdge(3, 4); |
| rightFirst.addEdge(1, 4); |
| EXPECT_EQ(getWTO(rightFirst), (WTOList{0, 1, 2, 3, 4})); |
| } |
| } |
| |
| TEST(WTOTest, SelfLoop) { |
| TestCFG cfg(3); |
| cfg.addEdge(0, 0); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 1); |
| cfg.addEdge(1, 2); |
| EXPECT_EQ(getWTO(cfg), (WTOList{C({0}), C({1}), 2})); |
| } |
| |
| TEST(WTOTest, SimpleCycle) { |
| TestCFG cfg(2); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 0); |
| EXPECT_EQ(getWTO(cfg), (WTOList{C({0, 1})})); |
| } |
| |
| TEST(WTOTest, ThreeNodeCycle) { |
| TestCFG cfg(3); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 2); |
| cfg.addEdge(2, 0); |
| EXPECT_EQ(getWTO(cfg), (WTOList{C({0, 1, 2})})); |
| } |
| |
| TEST(WTOTest, SharedLoopHeader) { |
| // Two loops sharing header 0: 0 -> 1 -> 0 and 0 -> 2 -> 0. |
| TestCFG cfg(3); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 0); |
| cfg.addEdge(0, 2); |
| cfg.addEdge(2, 0); |
| EXPECT_EQ(getWTO(cfg), (WTOList{C({0, 1, 2})})); |
| } |
| |
| TEST(WTOTest, BourdonclePaperExample) { |
| // The example control-flow graph from Bourdoncle's 1993 paper "Efficient |
| // chaotic iteration strategies with widenings", Figure 1 (0-indexed: vertices |
| // 0..7 correspond to 1..8 in the paper): |
| // 0 -> 1 |
| // 1 -> 2, 1 -> 7 |
| // 2 -> 3 |
| // 3 -> 4, 3 -> 6 |
| // 4 -> 5 |
| // 5 -> 4, 5 -> 6 |
| // 6 -> 2, 6 -> 7 |
| // Expected WTO from the paper: 0 1 (2 3 (4 5) 6) 7 |
| TestCFG cfg(8); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 2); |
| cfg.addEdge(1, 7); |
| cfg.addEdge(2, 3); |
| cfg.addEdge(3, 4); |
| cfg.addEdge(3, 6); |
| cfg.addEdge(4, 5); |
| cfg.addEdge(5, 4); |
| cfg.addEdge(5, 6); |
| cfg.addEdge(6, 2); |
| cfg.addEdge(6, 7); |
| auto wto = getWTO(cfg); |
| EXPECT_EQ(wto, (WTOList{0, 1, C({2, 3, C({4, 5}), 6}), 7})); |
| std::ostringstream ss; |
| ss << wto; |
| EXPECT_EQ(ss.str(), "0 1 (2 3 (4 5) 6) 7"); |
| } |
| |
| TEST(WTOTest, LoopHeaderDominatesExit) { |
| // Same as Bourdoncle's paper graph, except block 7 is only reachable from |
| // block 6 (no direct edge 1 -> 7). Block 2 dominates block 7 even though |
| // block 7 is outside the natural loop of 2. Block 7 must remain outside the |
| // cycle of 2. |
| TestCFG cfg(8); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 2); |
| cfg.addEdge(2, 3); |
| cfg.addEdge(3, 4); |
| cfg.addEdge(3, 6); |
| cfg.addEdge(4, 5); |
| cfg.addEdge(5, 4); |
| cfg.addEdge(5, 6); |
| cfg.addEdge(6, 2); |
| cfg.addEdge(6, 7); |
| EXPECT_EQ(getWTO(cfg), (WTOList{0, 1, C({2, 3, C({4, 5}), 6}), 7})); |
| } |
| |
| TEST(WTOTest, DiamondOfLoops) { |
| // 01 -> 23 -> 67 and 01 -> 45 -> 67, where each pair is a 2-block loop. |
| TestCFG cfg(8); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 0); |
| cfg.addEdge(1, 2); |
| cfg.addEdge(1, 4); |
| |
| cfg.addEdge(2, 3); |
| cfg.addEdge(3, 2); |
| cfg.addEdge(3, 6); |
| |
| cfg.addEdge(4, 5); |
| cfg.addEdge(5, 4); |
| cfg.addEdge(5, 6); |
| |
| cfg.addEdge(6, 7); |
| cfg.addEdge(7, 6); |
| |
| EXPECT_EQ(getWTO(cfg), (WTOList{C({0, 1}), C({2, 3}), C({4, 5}), C({6, 7})})); |
| } |
| |
| TEST(WTOTest, UnreachableBlocks) { |
| // Blocks 0, 1, 2 form a reachable loop 0 -> 1 -> 2 -> 1. |
| // Blocks 3, 4 form an unreachable cycle 3 -> 4 -> 3 with edges into 1 and 2. |
| TestCFG cfg(5); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 2); |
| cfg.addEdge(2, 1); |
| cfg.addEdge(3, 4); |
| cfg.addEdge(4, 3); |
| cfg.addEdge(3, 1); |
| cfg.addEdge(4, 2); |
| EXPECT_EQ(getWTO(cfg), (WTOList{0, C({1, 2})})); |
| } |
| |
| TEST(WTOTest, WorklistEvaluation) { |
| // Evaluate a chaotic iteration sequence on Bourdoncle's paper graph where the |
| // inner cycle (4 5) stabilizes in 2 iterations and the outer cycle |
| // (2 3 (4 5) 6) stabilizes in 2 iterations. |
| TestCFG cfg(8); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 2); |
| cfg.addEdge(1, 7); |
| cfg.addEdge(2, 3); |
| cfg.addEdge(3, 4); |
| cfg.addEdge(3, 6); |
| cfg.addEdge(4, 5); |
| cfg.addEdge(5, 4); |
| cfg.addEdge(5, 6); |
| cfg.addEdge(6, 2); |
| cfg.addEdge(6, 7); |
| |
| WTOWorklist<TestCFG> work(cfg); |
| work.push(cfg.entry); |
| |
| std::vector<Index> visits; |
| unsigned count4 = 0; |
| unsigned count2 = 0; |
| work.run([&](BasicBlock* block) { |
| Index id = block->contents.index; |
| visits.push_back(id); |
| for (auto* out : block->out) { |
| Index outId = out->contents.index; |
| if (id == 5 && outId == 4) { |
| if (++count4 < 2) { |
| work.push(out); |
| } |
| } else if (id == 6 && outId == 2) { |
| if (++count2 < 2) { |
| count4 = 0; |
| work.push(out); |
| } |
| } else { |
| work.push(out); |
| } |
| } |
| }); |
| |
| // Expected recursive evaluation order: |
| // 0, 1, |
| // first iteration of (2 3 (4 5) 6): 2, 3, 4, 5, 4, 5, 6, |
| // second iteration of (2 3 (4 5) 6): 2, 3, 4, 5, 4, 5, 6, |
| // 7 |
| EXPECT_EQ( |
| visits, |
| (std::vector<Index>{0, 1, 2, 3, 4, 5, 4, 5, 6, 2, 3, 4, 5, 4, 5, 6, 7})); |
| } |
| |
| TEST(WTOTest, WorklistSelectivePropagation) { |
| // In Bourdoncle's graph, test when block 3 only queues block 6 (skipping the |
| // inner cycle (4 5) completely) and block 6 does not re-queue block 2. |
| TestCFG cfg(8); |
| cfg.addEdge(0, 1); |
| cfg.addEdge(1, 2); |
| cfg.addEdge(1, 7); |
| cfg.addEdge(2, 3); |
| cfg.addEdge(3, 4); |
| cfg.addEdge(3, 6); |
| cfg.addEdge(4, 5); |
| cfg.addEdge(5, 4); |
| cfg.addEdge(5, 6); |
| cfg.addEdge(6, 2); |
| cfg.addEdge(6, 7); |
| |
| WTOWorklist<TestCFG> work(cfg); |
| work.push(cfg.entry); |
| |
| std::vector<Index> visits; |
| work.run([&](BasicBlock* block) { |
| Index id = block->contents.index; |
| visits.push_back(id); |
| if (id == 0) { |
| work.push(cfg.basicBlocks[1].get()); |
| } else if (id == 1) { |
| work.push(cfg.basicBlocks[2].get()); |
| } else if (id == 2) { |
| work.push(cfg.basicBlocks[3].get()); |
| } else if (id == 3) { |
| work.push(cfg.basicBlocks[6].get()); |
| } else if (id == 6) { |
| work.push(cfg.basicBlocks[7].get()); |
| } |
| }); |
| |
| EXPECT_EQ(visits, (std::vector<Index>{0, 1, 2, 3, 6, 7})); |
| } |