| /* | |
| * 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})); | |
| } |