diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 7f47cf108eb..df53eb06584 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -19,9 +19,12 @@ // PassOptions structure; see more details there. // +#include + #include "ir/effects.h" #include "ir/module-utils.h" #include "pass.h" +#include "support/graph_traversal.h" #include "support/strongly_connected_components.h" #include "wasm.h" @@ -39,6 +42,9 @@ struct FuncInfo { // Directly-called functions from this function. std::unordered_set calledFunctions; + + // Types that are targets of indirect calls. + std::unordered_set indirectCalledTypes; }; std::map analyzeFuncs(Module& module, @@ -83,11 +89,21 @@ std::map analyzeFuncs(Module& module, if (auto* call = curr->dynCast()) { // Note the direct call. funcInfo.calledFunctions.insert(call->target); + } else if (effects.calls && options.closedWorld) { + HeapType type; + if (auto* callRef = curr->dynCast()) { + // call_ref on unreachable does not have a call effect, + // so this must be a HeapType. + type = callRef->target->type.getHeapType(); + } else if (auto* callIndirect = curr->dynCast()) { + type = callIndirect->heapType; + } else { + WASM_UNREACHABLE("Unexpected call type"); + } + + funcInfo.indirectCalledTypes.insert(type); } else if (effects.calls) { - // This is an indirect call of some sort, so we must assume the - // worst. To do so, clear the effects, which indicates nothing - // is known (so anything is possible). - // TODO: We could group effects by function type etc. + assert(!options.closedWorld); funcInfo.effects = UnknownEffects; } else { // No call here, but update throwing if we see it. (Only do so, @@ -107,22 +123,86 @@ std::map analyzeFuncs(Module& module, return std::move(analysis.map); } -using CallGraph = std::unordered_map>; +using CallGraphNode = std::variant; + +/* + Call graph for indirect and direct calls. + + key (caller) -> value (callee) + Function -> Function : direct call + Function -> HeapType : indirect call to the given HeapType + HeapType -> Function : The function `callee` has the type `caller`. The + HeapType may essentially 'call' any of its + potential implementations. + HeapType -> HeapType : `callee` is a subtype of `caller`. A call_ref + could target any subtype of the ref, so we need to + aggregate effects of subtypes of the target type. + + If we're running in an open world, we only include Function -> Function edges, + and don't compute effects for indirect calls, conservatively assuming the + worst. +*/ +using CallGraph = + std::unordered_map>; CallGraph buildCallGraph(const Module& module, - const std::map& funcInfos) { + const std::map& funcInfos, + bool closedWorld) { CallGraph callGraph; - for (const auto& [func, info] : funcInfos) { - if (info.calledFunctions.empty()) { - continue; + if (!closedWorld) { + for (const auto& [caller, callerInfo] : funcInfos) { + auto& callees = callGraph[caller]; + + // Function -> Function + for (Name calleeFunction : callerInfo.calledFunctions) { + callees.insert(module.getFunction(calleeFunction)); + } } - auto& callees = callGraph[func]; - for (Name callee : info.calledFunctions) { - callees.insert(module.getFunction(callee)); + return callGraph; + } + + std::unordered_set allFunctionTypes; + for (const auto& [caller, callerInfo] : funcInfos) { + auto& callees = callGraph[caller]; + + // Function -> Function + for (Name calleeFunction : callerInfo.calledFunctions) { + callees.insert(module.getFunction(calleeFunction)); + } + + // Function -> Type + allFunctionTypes.insert(caller->type.getHeapType()); + for (HeapType calleeType : callerInfo.indirectCalledTypes) { + callees.insert(calleeType); + + // Add the key to ensure the lookup doesn't fail for indirect calls to + // uninhabited types. + callGraph[calleeType]; } + + // Type -> Function + callGraph[caller->type.getHeapType()].insert(caller); } + // Type -> Type + // Do a DFS up the type heirarchy for all function implementations. + // We are essentially walking up each supertype chain and adding edges from + // super -> subtype, but doing it via DFS to avoid repeated work. + Graph superTypeGraph(allFunctionTypes.begin(), + allFunctionTypes.end(), + [&callGraph](auto&& push, HeapType t) { + // Not needed except that during lookup we expect the + // key to exist. + callGraph[t]; + + if (auto super = t.getDeclaredSuperType()) { + callGraph[*super].insert(t); + push(*super); + } + }); + (void)superTypeGraph.traverseDepthFirst(); + return callGraph; } @@ -152,63 +232,60 @@ void propagateEffects(const Module& module, const PassOptions& passOptions, std::map& funcInfos, const CallGraph& callGraph) { + // We only care about Functions that are roots, not types. + // A type would be a root if a function exists with that type, but no-one + // indirect calls the type. + auto funcNodes = std::views::keys(callGraph) | + std::views::filter([](auto node) { + return std::holds_alternative(node); + }) | + std::views::common; + using funcNodesType = decltype(funcNodes); + struct CallGraphSCCs - : SCCs::const_iterator, CallGraphSCCs> { + : SCCs, CallGraphSCCs> { + const std::map& funcInfos; - const std::unordered_map>& - callGraph; + const CallGraph& callGraph; const Module& module; - CallGraphSCCs( - const std::vector& funcs, - const std::map& funcInfos, - const std::unordered_map>& - callGraph, - const Module& module) - : SCCs::const_iterator, CallGraphSCCs>( - funcs.begin(), funcs.end()), + CallGraphSCCs(funcNodesType&& nodes, + const std::map& funcInfos, + const CallGraph& callGraph, + const Module& module) + : SCCs, CallGraphSCCs>( + std::ranges::begin(nodes), std::ranges::end(nodes)), funcInfos(funcInfos), callGraph(callGraph), module(module) {} - void pushChildren(Function* f) { - auto callees = callGraph.find(f); - if (callees == callGraph.end()) { - return; - } - - for (auto* callee : callees->second) { + void pushChildren(CallGraphNode node) { + for (CallGraphNode callee : callGraph.at(node)) { push(callee); } } }; - - std::vector allFuncs; - for (auto& [func, info] : funcInfos) { - allFuncs.push_back(func); - } - CallGraphSCCs sccs(allFuncs, funcInfos, callGraph, module); + CallGraphSCCs sccs(std::move(funcNodes), funcInfos, callGraph, module); std::vector> componentEffects; // Points to an index in componentEffects - std::unordered_map funcComponents; + std::unordered_map nodeComponents; for (auto ccIterator : sccs) { std::optional& ccEffects = componentEffects.emplace_back(std::in_place, passOptions, module); + std::vector cc(ccIterator.begin(), ccIterator.end()); - std::vector ccFuncs(ccIterator.begin(), ccIterator.end()); - - for (Function* f : ccFuncs) { - funcComponents.emplace(f, componentEffects.size() - 1); + std::vector ccFuncs; + for (CallGraphNode node : cc) { + nodeComponents.emplace(node, componentEffects.size() - 1); + if (auto** func = std::get_if(&node)) { + ccFuncs.push_back(*func); + } } std::unordered_set calleeSccs; - for (Function* caller : ccFuncs) { - auto callees = callGraph.find(caller); - if (callees == callGraph.end()) { - continue; - } - for (auto* callee : callees->second) { - calleeSccs.insert(funcComponents.at(callee)); + for (CallGraphNode caller : cc) { + for (CallGraphNode callee : callGraph.at(caller)) { + calleeSccs.insert(nodeComponents.at(callee)); } } @@ -219,11 +296,13 @@ void propagateEffects(const Module& module, } // Add trap effects for potential cycles. - if (ccFuncs.size() > 1) { + if (cc.size() > 1) { if (ccEffects != UnknownEffects) { ccEffects->trap = true; } - } else { + } else if (ccFuncs.size() == 1) { + // It's possible for a CC to only contain 1 type, but that is not a + // cycle in the call graph. auto* func = ccFuncs[0]; if (funcInfos.at(func).calledFunctions.contains(func->name)) { if (ccEffects != UnknownEffects) { @@ -267,7 +346,8 @@ struct GenerateGlobalEffects : public Pass { std::map funcInfos = analyzeFuncs(*module, getPassOptions()); - auto callGraph = buildCallGraph(*module, funcInfos); + auto callGraph = + buildCallGraph(*module, funcInfos, getPassOptions().closedWorld); propagateEffects(*module, getPassOptions(), funcInfos, callGraph); diff --git a/src/support/graph_traversal.h b/src/support/graph_traversal.h new file mode 100644 index 00000000000..cbe5b3a74a4 --- /dev/null +++ b/src/support/graph_traversal.h @@ -0,0 +1,74 @@ +/* + * 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 +#include +#include +#include + +namespace wasm { + +// SuccessorFunction should be an invocable that takes a 'push' function (which +// is an invocable that takes a `const T&`), and a `const T&`. i.e. +// SuccessorFunction should call `push` for each neighbor of the T that it's +// called with. +// TODO: We don't have a good way to write this with concepts today. +// Something like this should do it, but we hit an ICE on dwarf symbols in debug +// builds: requires requires(const SuccessorFunction& successors, const T& t) { +// successors([](const T&) { }, t); } +template class Graph { +public: + template Sen> + requires std::convertible_to, T> + Graph(It rootsBegin, Sen rootsEnd, SuccessorFunction successors) + : roots(rootsBegin, rootsEnd), successors(std::move(successors)) {} + + // Traverse the graph depth-first, calling `successors` exactly once for each + // node (unless the node appears multiple times in `roots`). Return the set of + // nodes visited. + std::unordered_set traverseDepthFirst() const { + std::vector stack(roots.begin(), roots.end()); + std::unordered_set visited(roots.begin(), roots.end()); + + auto maybePush = [&](const T& t) { + auto [_, inserted] = visited.insert(t); + if (inserted) { + stack.push_back(t); + } + }; + + while (!stack.empty()) { + auto curr = std::move(stack.back()); + stack.pop_back(); + + successors(maybePush, curr); + } + + return visited; + } + +private: + std::vector roots; + SuccessorFunction successors; +}; + +template Sen, + typename SuccessorFunction> +Graph(It, Sen, SuccessorFunction) + -> Graph, std::decay_t>; + +} // namespace wasm diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 41d16f28e92..54055cbaff1 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -12,6 +12,7 @@ set(unittest_SOURCES dataflow.cpp dfa_minimization.cpp disjoint_sets.cpp + graph.cpp leaves.cpp glbs.cpp interpreter.cpp diff --git a/test/gtest/graph.cpp b/test/gtest/graph.cpp new file mode 100644 index 00000000000..cf63417c777 --- /dev/null +++ b/test/gtest/graph.cpp @@ -0,0 +1,146 @@ +/* + * 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 + +#include "support/graph_traversal.h" +#include "gtest/gtest.h" + +using namespace wasm; + +TEST(GraphTest, Linear) { + // 0 -> 1 -> 2 + std::vector roots = {0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n < 2) { + push(n + 1); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1, 2}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1, 2}; + EXPECT_EQ(visited, expectedVisited); +} + +TEST(GraphTest, Cycle) { + // 0 -> 1 -> 0 + std::vector roots = {0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n == 0) { + push(1); + } else if (n == 1) { + push(0); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1}; + EXPECT_EQ(visited, expectedVisited); +} + +TEST(GraphTest, Diamond) { + // 0 -> 1, 2 + // 1 -> 3 + // 2 -> 3 + + std::vector roots = {0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n == 0) { + push(2); + push(1); + } else if (n == 1 || n == 2) { + push(3); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1, 3, 2}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1, 2, 3}; + EXPECT_EQ(visited, expectedVisited); +} + +TEST(GraphTest, DuplicateRoots) { + // 0 -> 1, 2 + // 1 -> 0 + // 2 -> 0 + // 0 is added as a root 3 times + + std::vector roots = {0, 0, 0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n == 0) { + push(2); + push(1); + } else if (n == 1 || n == 2) { + push(0); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1, 2, 0, 0}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1, 2}; + EXPECT_EQ(visited, expectedVisited); +} + +TEST(GraphTest, Disjoint) { + // 0 -> 1 + // 2 -> 3 + + std::vector roots = {2, 0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n == 0) { + push(1); + } else if (n == 2) { + push(3); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1, 2, 3}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1, 2, 3}; + EXPECT_EQ(visited, expectedVisited); +} diff --git a/test/lit/passes/global-effects-closed-world-simplify-locals.wast b/test/lit/passes/global-effects-closed-world-simplify-locals.wast new file mode 100644 index 00000000000..5dce4127d0b --- /dev/null +++ b/test/lit/passes/global-effects-closed-world-simplify-locals.wast @@ -0,0 +1,98 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt --enable-gc --enable-reference-types --closed-world --generate-global-effects --simplify-locals -S -o - | filecheck %s + +;; Tests for aggregating effects from indirect calls in GlobalEffects when +;; --closed-world is true. Continued from global-effects-closed-world.wast. + +(module + ;; CHECK: (type $indirect-type-super (sub (func (param i32)))) + (type $indirect-type-super (sub (func (param i32)))) + + ;; CHECK: (type $1 (func (param (ref $indirect-type-super)))) + + ;; CHECK: (type $indirect-type-sub (sub $indirect-type-super (func (param i32)))) + (type $indirect-type-sub (sub $indirect-type-super (func (param i32)))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) + (global $g1 (mut i32) (i32.const 0)) + ;; CHECK: (global $g2 (mut i32) (i32.const 0)) + (global $g2 (mut i32) (i32.const 0)) + ;; CHECK: (global $g3 (mut i32) (i32.const 0)) + (global $g3 (mut i32) (i32.const 0)) + + ;; CHECK: (export "impl1" (func $impl1)) + + ;; CHECK: (export "impl2" (func $impl2)) + + ;; CHECK: (func $impl1 (type $indirect-type-super) (param $i32 i32) + ;; CHECK-NEXT: (global.set $g1 + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $impl1 (export "impl1") (type $indirect-type-super) (param $i32 i32) + (global.set $g1 (local.get $i32)) + ) + + ;; CHECK: (func $impl2 (type $indirect-type-sub) (param $i32 i32) + ;; CHECK-NEXT: (global.set $g2 + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $impl2 (export "impl2") (type $indirect-type-sub) (param $i32 i32) + (global.set $g2 (local.get $i32)) + ) + + ;; CHECK: (func $caller (type $1) (param $ref (ref $indirect-type-super)) + ;; CHECK-NEXT: (call_ref $indirect-type-super + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $ref (ref $indirect-type-super)) + ;; This inherits effects from $impl1 and $impl2, so may mutate $g1 and $g2. + (call_ref $indirect-type-super (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $merges-multiple-effects (type $1) (param $ref (ref $indirect-type-super)) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local $z i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (global.get $g2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (call $caller + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $merges-multiple-effects (param $ref (ref $indirect-type-super)) + (local $x i32) + (local $y i32) + (local $z i32) + + (local.set $x (global.get $g1)) + (local.set $y (global.get $g2)) + (local.set $z (global.get $g3)) + + ;; This acts as a barrier for $x and $y, but not $z because + ;; $ref may write to $g1 (via $impl1) or $g2 (via $impl2) but not $g3. + ;; $z is optimized out and $x and $y are left alone. + (call $caller (local.get $ref)) + + (drop (local.get $x)) + (drop (local.get $y)) + (drop (local.get $z)) + ) +) diff --git a/test/lit/passes/global-effects-closed-world-tnh.wast b/test/lit/passes/global-effects-closed-world-tnh.wast new file mode 100644 index 00000000000..4c4558f8f95 --- /dev/null +++ b/test/lit/passes/global-effects-closed-world-tnh.wast @@ -0,0 +1,37 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --closed-world --traps-never-happen --generate-global-effects --vacuum -S -o - | filecheck %s + +;; Tests for aggregating effects from indirect calls in GlobalEffects when +;; --closed-world is true. Continued from global-effects-closed-world.wast. + +(module + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-nullable-ref (type $1) (param $ref (ref null $nopType)) + ;; CHECK-NEXT: (call_ref $nopType + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-nullable-ref (param $ref (ref null $nopType)) + (call_ref $nopType (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $1) (param $ref (ref null $nopType)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref null $nopType)) + ;; The only possible implementation of $nopType has no effects. + ;; $calls-nop-via-nullable-ref may trap from a null reference, but + ;; --traps-never-happen is enabled, so we're free to optimize this out. + (call $calls-nop-via-nullable-ref (local.get $ref)) + ) +) diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast new file mode 100644 index 00000000000..019f0676f2f --- /dev/null +++ b/test/lit/passes/global-effects-closed-world.wast @@ -0,0 +1,468 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --closed-world --generate-global-effects --vacuum -S -o - | filecheck %s + +;; Tests for aggregating effects from indirect calls in GlobalEffects when +;; --closed-world is true. Some more complicated tests are in +;; global-effects-closed-world-simplify-locals.wast. + +(module + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-ref (type $1) (param $ref (ref $nopType)) + ;; CHECK-NEXT: (call_ref $nopType + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-ref (param $ref (ref $nopType)) + ;; This can only possibly be a nop in closed-world. + ;; Ideally vacuum could optimize this out but we don't have a way to share + ;; this information with other passes today. + ;; For now, we can at least annotate that the call to this function in $f + ;; has no effects. + ;; TODO: This call_ref could be marked as having no effects, like the call below. + (call_ref $nopType (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $calls-nop-via-nullable-ref (type $2) (param $ref (ref null $nopType)) + ;; CHECK-NEXT: (call_ref $nopType + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-nullable-ref (param $ref (ref null $nopType)) + (call_ref $nopType (i32.const 1) (local.get $ref)) + ) + + + ;; CHECK: (func $f (type $1) (param $ref (ref $nopType)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $nopType)) + ;; $calls-nop-via-ref has no effects because we determined that it can only + ;; call $nop. We can optimize this call out. + (call $calls-nop-via-ref (local.get $ref)) + ) + + ;; CHECK: (func $g (type $2) (param $ref (ref null $nopType)) + ;; CHECK-NEXT: (call $calls-nop-via-nullable-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $g (param $ref (ref null $nopType)) + ;; Similar to $f, but we may still trap here because the ref is null, so we + ;; don't optimize. + (call $calls-nop-via-nullable-ref (local.get $ref)) + ) +) + +;; Same as the above but with call_indirect +(module + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + (table 1 1 funcref) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-ref (type $1) + ;; CHECK-NEXT: (call_indirect $0 (type $nopType) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-ref + ;; This can only possibly be a nop in closed-world. + ;; Ideally vacuum could optimize this out but we don't have a way to share + ;; this information with other passes today. + ;; For now, we can at least annotate that the call to this function in $f + ;; has no effects. + ;; TODO: This call_ref could be marked as having no effects, like the call below. + (call_indirect (type $nopType) (i32.const 1) (i32.const 0)) + ) + + ;; CHECK: (func $f (type $1) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f + ;; $calls-nop-via-ref has no effects because we determined that it can only + ;; call $nop. We can optimize this call out. + (call $calls-nop-via-ref) + ) +) + +(module + ;; CHECK: (type $maybe-has-effects (func (param i32))) + (type $maybe-has-effects (func (param i32))) + + ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32) + (unreachable) + ) + + ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32) + (nop) + ) + + ;; CHECK: (func $calls-effectful-function-via-ref (type $1) (param $ref (ref $maybe-has-effects)) + ;; CHECK-NEXT: (call_ref $maybe-has-effects + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-effectful-function-via-ref (param $ref (ref $maybe-has-effects)) + (call_ref $maybe-has-effects (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $1) (param $ref (ref $maybe-has-effects)) + ;; CHECK-NEXT: (call $calls-effectful-function-via-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $maybe-has-effects)) + ;; This may be a nop or it may trap depending on the ref. + ;; We don't know so don't optimize it out. + (call $calls-effectful-function-via-ref (local.get $ref)) + ) +) + +;; Same as above but with call_indirect +(module + (table 1 1 funcref) + + ;; CHECK: (type $maybe-has-effects (func (param i32))) + (type $maybe-has-effects (func (param i32))) + + ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32) + (unreachable) + ) + + ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32) + (nop) + ) + + ;; CHECK: (func $calls-effectful-function-via-ref (type $1) + ;; CHECK-NEXT: (call_indirect $0 (type $maybe-has-effects) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-effectful-function-via-ref + (call_indirect (type $maybe-has-effects) (i32.const 1) (i32.const 1)) + ) + + ;; CHECK: (func $f (type $1) + ;; CHECK-NEXT: (call $calls-effectful-function-via-ref) + ;; CHECK-NEXT: ) + (func $f + ;; This may be a nop or it may trap depending on the ref. + ;; We don't know so don't optimize it out. + (call $calls-effectful-function-via-ref) + ) +) + +(module + ;; CHECK: (type $uninhabited (func (param i32))) + (type $uninhabited (func (param i32))) + + ;; CHECK: (func $calls-uninhabited (type $1) (param $ref (ref $uninhabited)) + ;; CHECK-NEXT: (call_ref $uninhabited + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-uninhabited (param $ref (ref $uninhabited)) + (call_ref $uninhabited (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $calls-nullable-uninhabited (type $2) (param $ref (ref null $uninhabited)) + ;; CHECK-NEXT: (call_ref $uninhabited + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nullable-uninhabited (param $ref (ref null $uninhabited)) + ;; This must be null, so it's guaranteed to trap and can't be optimized out. + ;; TODO: try to optimize this to (unreachable) + (call_ref $uninhabited (i32.const 1) (local.get $ref)) + ) + + + ;; CHECK: (func $f (type $1) (param $ref (ref $uninhabited)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $uninhabited)) + ;; There's no function with this type, so it's impossible to create a ref to + ;; call this function with and there are no effects to aggregate. + ;; Remove this call. + (call $calls-uninhabited (local.get $ref)) + ) + + ;; CHECK: (func $g (type $2) (param $ref (ref null $uninhabited)) + ;; CHECK-NEXT: (call $calls-nullable-uninhabited + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $g (param $ref (ref null $uninhabited)) + ;; Similar to above but we have a nullable reference, so we may trap and + ;; can't optimize the call out. + (call $calls-nullable-uninhabited (local.get $ref)) + ) +) + +(module + ;; CHECK: (type $super (sub (func))) + (type $super (sub (func))) + ;; Subtype + ;; CHECK: (type $sub (sub $super (func))) + (type $sub (sub $super (func))) + + ;; CHECK: (func $nop-with-supertype (type $super) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop-with-supertype (export "nop-with-supertype") (type $super) + ) + + ;; CHECK: (func $effectful-with-subtype (type $sub) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $effectful-with-subtype (export "effectful-with-subtype") (type $sub) + (unreachable) + ) + + ;; CHECK: (func $calls-ref-with-supertype (type $1) (param $func (ref $super)) + ;; CHECK-NEXT: (call_ref $super + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-ref-with-supertype (param $func (ref $super)) + (call_ref $super (local.get $func)) + ) + + ;; CHECK: (func $f (type $1) (param $func (ref $super)) + ;; CHECK-NEXT: (call $calls-ref-with-supertype + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $func (ref $super)) + ;; Check that we account for subtyping correctly. + ;; $super has no effects (i.e. the union of all effects of functions with + ;; this type is empty). However, $sub does have effects, and we can call_ref + ;; with that subtype, so we need to include the unreachable effect and we + ;; can't optimize out this call. + (call $calls-ref-with-supertype (local.get $func)) + ) +) + +;; Same as above but this time our reference is the exact supertype +;; so we know not to aggregate effects from the subtype. +;; TODO: this case doesn't optimize today. Add exact ref support in the pass. +(module + ;; CHECK: (type $super (sub (func))) + (type $super (sub (func))) + + ;; CHECK: (type $sub (sub $super (func))) + (type $sub (sub $super (func))) + + ;; CHECK: (func $nop-with-supertype (type $super) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop-with-supertype (export "nop-with-supertype") (type $super) + ) + + ;; CHECK: (func $effectful-with-subtype (type $sub) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $effectful-with-subtype (export "effectful-with-subtype") (type $sub) + (unreachable) + ) + + ;; CHECK: (func $calls-ref-with-supertype (type $1) (param $func (ref (exact $super))) + ;; CHECK-NEXT: (call_ref $super + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-ref-with-supertype (param $func (ref (exact $super))) + (call_ref $super (local.get $func)) + ) + + ;; CHECK: (func $f (type $1) (param $func (ref (exact $super))) + ;; CHECK-NEXT: (call $calls-ref-with-supertype + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $func (ref (exact $super))) + (call $calls-ref-with-supertype (local.get $func)) + ) +) + +(module + ;; CHECK: (type $only-has-effects-in-not-addressable-function (func (param i32))) + (type $only-has-effects-in-not-addressable-function (func (param i32))) + + ;; CHECK: (func $nop (type $only-has-effects-in-not-addressable-function) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $only-has-effects-in-not-addressable-function) (param i32) + ) + + ;; CHECK: (func $has-effects-but-not-exported (type $only-has-effects-in-not-addressable-function) (param $0 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $has-effects-but-not-exported (type $only-has-effects-in-not-addressable-function) (param i32) + (unreachable) + ) + + ;; CHECK: (func $calls-type-with-effects-but-not-addressable (type $1) (param $ref (ref $only-has-effects-in-not-addressable-function)) + ;; CHECK-NEXT: (call_ref $only-has-effects-in-not-addressable-function + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-type-with-effects-but-not-addressable (param $ref (ref $only-has-effects-in-not-addressable-function)) + (call_ref $only-has-effects-in-not-addressable-function (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $1) (param $ref (ref $only-has-effects-in-not-addressable-function)) + ;; CHECK-NEXT: (call $calls-type-with-effects-but-not-addressable + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $only-has-effects-in-not-addressable-function)) + ;; The type $has-effects-but-not-exported doesn't have an address because + ;; it's not exported and it's never the target of a ref.func. + ;; We should be able to determine that $ref can only point to $nop. + ;; TODO: Only aggregate effects from functions that are addressed. + (call $calls-type-with-effects-but-not-addressable (local.get $ref)) + ) +) + +(module + ;; CHECK: (type $unreachable-via-direct-call (func (param i32))) + (type $unreachable-via-direct-call (func (param i32))) + + ;; CHECK: (elem declare func $calls-unreachable) + + ;; CHECK: (func $unreachable (type $0) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable + (unreachable) + ) + + ;; CHECK: (func $calls-unreachable (type $unreachable-via-direct-call) (param $0 i32) + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + (func $calls-unreachable (export "calls-unreachable") (param i32) + (call $unreachable) + ) + + ;; CHECK: (func $calls-unreachable-via-ref-and-direct-call-transtively (type $0) + ;; CHECK-NEXT: (call_ref $unreachable-via-direct-call + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.func $calls-unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-unreachable-via-ref-and-direct-call-transtively + (call_ref $unreachable-via-direct-call (i32.const 0) (ref.func $calls-unreachable)) + ) + + ;; CHECK: (func $f (type $0) + ;; CHECK-NEXT: (call $calls-unreachable-via-ref-and-direct-call-transtively) + ;; CHECK-NEXT: ) + (func $f + ;; Test that we can analyze longer call chains containing both indirect and + ;; direct calls. In this case the call chain hits an unreachable via an + ;; indirect call, then direct call, so we can't optimize this out. + (call $calls-unreachable-via-ref-and-direct-call-transtively) + ) +) + +(module + ;; CHECK: (type $t (func (param i32))) + (type $t (func (param i32))) + + ;; (import "" "" (func $imported-func (type $t))) + ;; CHECK: (import "" "" (func $imported-func (type $t) (param i32))) + (import "" "" (func $imported-func (type $t))) + + (elem declare $imported-func) + + ;; CHECK: (func $nop (type $t) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (param i32) + ) + + ;; CHECK: (func $indirect-calls (type $1) (param $ref (ref $t)) + ;; CHECK-NEXT: (call_ref $t + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $indirect-calls (param $ref (ref $t)) + (call_ref $t (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $1) (param $ref (ref $t)) + ;; CHECK-NEXT: (call $indirect-calls + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $t)) + ;; $indirect-calls might end up calling an imported function, + ;; so we don't know anything about effects here + (call $indirect-calls (local.get $ref)) + ) +) + +(module + (type $t (func (param i32))) + ;; CHECK: (func $calls-unreachable (type $0) + ;; CHECK-NEXT: (block ;; (replaces unreachable CallRef we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-unreachable (export "calls-unreachable") + (call_ref $t (unreachable)) + ) + + ;; CHECK: (func $f (type $0) + ;; CHECK-NEXT: (call $calls-unreachable) + ;; CHECK-NEXT: ) + (func $f + ;; $t looks like it has no effects, but unreachable is passed in, + ;; so preserve the trap. + (call $calls-unreachable) + ) +) diff --git a/test/lit/passes/global-effects.wast b/test/lit/passes/global-effects.wast index 1125f738e68..86e4988e92c 100644 --- a/test/lit/passes/global-effects.wast +++ b/test/lit/passes/global-effects.wast @@ -13,14 +13,22 @@ ;; INCLUDE: (type $void (func)) (type $void (func)) - ;; WITHOUT: (type $1 (func (result i32))) + ;; WITHOUT: (type $indirect-type (func (param f32))) + ;; INCLUDE: (type $indirect-type (func (param f32))) + (type $indirect-type (func (param f32))) - ;; WITHOUT: (type $2 (func (param i32))) + ;; WITHOUT: (type $2 (func (param (ref $indirect-type)))) + + ;; WITHOUT: (type $3 (func (result i32))) + + ;; WITHOUT: (type $4 (func (param i32))) ;; WITHOUT: (import "a" "b" (func $import (type $void))) - ;; INCLUDE: (type $1 (func (result i32))) + ;; INCLUDE: (type $2 (func (param (ref $indirect-type)))) + + ;; INCLUDE: (type $3 (func (result i32))) - ;; INCLUDE: (type $2 (func (param i32))) + ;; INCLUDE: (type $4 (func (param i32))) ;; INCLUDE: (import "a" "b" (func $import (type $void))) (import "a" "b" (func $import)) @@ -150,7 +158,7 @@ (call $unreachable) ) - ;; WITHOUT: (func $unimportant-effects (type $1) (result i32) + ;; WITHOUT: (func $unimportant-effects (type $3) (result i32) ;; WITHOUT-NEXT: (local $x i32) ;; WITHOUT-NEXT: (local.set $x ;; WITHOUT-NEXT: (i32.const 100) @@ -159,7 +167,7 @@ ;; WITHOUT-NEXT: (local.get $x) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $unimportant-effects (type $1) (result i32) + ;; INCLUDE: (func $unimportant-effects (type $3) (result i32) ;; INCLUDE-NEXT: (local $x i32) ;; INCLUDE-NEXT: (local.set $x ;; INCLUDE-NEXT: (i32.const 100) @@ -380,7 +388,7 @@ ) ) - ;; WITHOUT: (func $call-throw-or-unreachable-and-catch (type $2) (param $x i32) + ;; WITHOUT: (func $call-throw-or-unreachable-and-catch (type $4) (param $x i32) ;; WITHOUT-NEXT: (block $tryend ;; WITHOUT-NEXT: (try_table (catch_all $tryend) ;; WITHOUT-NEXT: (if @@ -395,7 +403,7 @@ ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $call-throw-or-unreachable-and-catch (type $2) (param $x i32) + ;; INCLUDE: (func $call-throw-or-unreachable-and-catch (type $4) (param $x i32) ;; INCLUDE-NEXT: (block $tryend ;; INCLUDE-NEXT: (try_table (catch_all $tryend) ;; INCLUDE-NEXT: (if @@ -473,4 +481,47 @@ (call $cycle-with-unknown-call) (call $import) ) + + + ;; WITHOUT: (func $nop-indirect (type $indirect-type) (param $0 f32) + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $nop-indirect (type $indirect-type) (param $0 f32) + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + (func $nop-indirect (type $indirect-type) (param f32) + ) + + ;; WITHOUT: (func $unknown-indirect-call (type $2) (param $ref (ref $indirect-type)) + ;; WITHOUT-NEXT: (call_ref $indirect-type + ;; WITHOUT-NEXT: (f32.const 1) + ;; WITHOUT-NEXT: (local.get $ref) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $unknown-indirect-call (type $2) (param $ref (ref $indirect-type)) + ;; INCLUDE-NEXT: (call_ref $indirect-type + ;; INCLUDE-NEXT: (f32.const 1) + ;; INCLUDE-NEXT: (local.get $ref) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $unknown-indirect-call (param $ref (ref $indirect-type)) + (call_ref $indirect-type (f32.const 1) (local.get $ref)) + ) + + ;; WITHOUT: (func $calls-unknown-indirect-call (type $2) (param $ref (ref $indirect-type)) + ;; WITHOUT-NEXT: (call $unknown-indirect-call + ;; WITHOUT-NEXT: (local.get $ref) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $calls-unknown-indirect-call (type $2) (param $ref (ref $indirect-type)) + ;; INCLUDE-NEXT: (call $unknown-indirect-call + ;; INCLUDE-NEXT: (local.get $ref) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $calls-unknown-indirect-call (param $ref (ref $indirect-type)) + ;; In a closed world, we could determine that the ref can only possibly be + ;; $nop-direct and optimize it out. See global-effects-closed-world.wast + ;; for related tests. + (call $unknown-indirect-call (local.get $ref)) + ) )