diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 7f47cf108eb..927129a733d 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -19,6 +19,8 @@ // PassOptions structure; see more details there. // +#include + #include "ir/effects.h" #include "ir/module-utils.h" #include "pass.h" @@ -39,6 +41,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 +88,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,19 +122,66 @@ 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()) { + + 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)); + } + + if (!closedWorld) { continue; } - auto& callees = callGraph[func]; - for (Name callee : info.calledFunctions) { - callees.insert(module.getFunction(callee)); + // Function -> Type + allFunctionTypes.insert(caller->type.getHeapType()); + for (HeapType calleeType : callerInfo.indirectCalledTypes) { + callees.insert(calleeType); + allFunctionTypes.insert(calleeType); + } + + // Type -> Function + callGraph[caller->type.getHeapType()].insert(caller); + } + + // Type -> Type + for (HeapType type : allFunctionTypes) { + // Not needed except that during lookup we expect the key to exist. + callGraph[type]; + + HeapType curr = type; + while (std::optional super = curr.getDeclaredSuperType()) { + callGraph[*super].insert(type); + curr = *super; } } @@ -152,63 +214,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(decltype(funcNodes)&& 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 +278,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 +328,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/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..d71fde032ea --- /dev/null +++ b/test/lit/passes/global-effects-closed-world-tnh.wast @@ -0,0 +1,34 @@ +;; 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 + +(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..51858cb1f2f --- /dev/null +++ b/test/lit/passes/global-effects-closed-world.wast @@ -0,0 +1,464 @@ +;; 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 + +(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)) + ) )