diff --git a/import-test.wast b/import-test.wast new file mode 100644 index 00000000000..3be4e36a1ad --- /dev/null +++ b/import-test.wast @@ -0,0 +1,35 @@ +(module + ;; CHECK: (type $t (func (param i32))) + (type $t (func (param i32))) + + ;; CHECK: (import "" "" (func $imported-func (type $t) (param i32))) + ;; (import "" "" (func $imported-func (type $t))) + (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: (nop) + ;; 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)) + ) +) \ No newline at end of file diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h index 50b67df7cea..d6a696c9fff 100644 --- a/src/ir/module-utils.h +++ b/src/ir/module-utils.h @@ -271,7 +271,7 @@ template inline void iterModuleItems(Module& wasm, T visitor) { template using DefaultMap = std::map; template class MapT = DefaultMap> + template class MapT = DefaultMap> struct ParallelFunctionAnalysis { Module& wasm; diff --git a/src/ir/subtypes.h b/src/ir/subtypes.h index 912d61f878d..7977429ac77 100644 --- a/src/ir/subtypes.h +++ b/src/ir/subtypes.h @@ -172,7 +172,11 @@ struct SubTypes { // false, we stop. Returns the last value returned to it, that is, returns // true if we did not stop early, and false if we did. template - bool iterSubTypes(HeapType type, Index depth, F func) const { + bool iterSubTypes(HeapType type, Index depth, F func) const + requires requires(F func, HeapType subtype, Index depth) { + { func(subtype, depth) } -> std::same_as; + } + { // Start by traversing the type itself. if (!func(type, 0)) { return false; @@ -219,7 +223,12 @@ struct SubTypes { } // As above, but iterate to the maximum depth. - template bool iterSubTypes(HeapType type, F func) const { + template + bool iterSubTypes(HeapType type, F func) const + requires requires(F func, HeapType subtype, Index depth) { + { func(subtype, depth) } -> std::same_as; + } + { return iterSubTypes(type, std::numeric_limits::max(), func); } diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index ac17037902b..3d419157200 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -15,12 +15,15 @@ */ // -// Handle the computation of global effects. The effects are stored on the -// PassOptions structure; see more details there. +// Handle the computation of global effects. The effects are stored on +// Function::effects; see more details there. // #include "ir/effects.h" +#include "ir/element-utils.h" #include "ir/module-utils.h" +#include "ir/subtypes.h" +#include "ir/table-utils.h" #include "pass.h" #include "support/unique_deferring_queue.h" #include "wasm.h" @@ -39,12 +42,15 @@ 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, - const PassOptions& passOptions) { - ModuleUtils::ParallelFunctionAnalysis analysis( - module, [&](Function* func, FuncInfo& funcInfo) { +std::unordered_map +analyzeFuncs(Module& module, const PassOptions& passOptions) { + ModuleUtils::ParallelFunctionAnalysis + analysis(module, [&](Function* func, FuncInfo& funcInfo) { if (func->imported()) { // Imports can do anything, so we need to assume the worst anyhow, // which is the same as not specifying any effects for them in the @@ -84,11 +90,21 @@ std::map analyzeFuncs(Module& module, // Note the direct call. funcInfo.calledFunctions.insert(call->target); } 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. - funcInfo.effects = UnknownEffects; + if (!options.closedWorld) { + funcInfo.effects = UnknownEffects; + return; + } + + HeapType type; + if (auto* callRef = curr->dynCast()) { + type = callRef->target->type.getHeapType(); + } else if (auto* callIndirect = curr->dynCast()) { + type = callIndirect->heapType; + } else { + assert("Unexpected type of call"); + } + + funcInfo.indirectCalledTypes.insert(type); } else { // No call here, but update throwing if we see it. (Only do so, // however, if we have effects; if we cleared it - see before - @@ -107,48 +123,124 @@ std::map analyzeFuncs(Module& module, return std::move(analysis.map); } +using CallGraphNode = std::variant; + +// Build a call graph for indirect and direct calls. +// key (callee) -> value (caller) +// Name -> Name : callee is called directly by caller +// Name -> HeapType : callee is a potential target of a virtual call +// with this HeapType HeapType -> Name : callee is indirectly called by +// caller HeapType -> HeapType : callee is a subtype of caller If we're +// running in an open world, we only include Name -> Name edges. +std::unordered_map> +buildReverseCallGraph(Module& module, + const std::unordered_map& funcInfos, + bool closedWorld) { + // callee : caller + std::unordered_map> callers; + + if (!closedWorld) { + for (const auto& [func, info] : funcInfos) { + // Name -> Name for direct calls + for (const auto& callee : info.calledFunctions) { + callers[callee].insert(func->name); + } + } + + return callers; + } + + std::unordered_set allIndirectCalledTypes; + + for (const auto& [func, info] : funcInfos) { + // Name -> Name for direct calls + for (const auto& callee : info.calledFunctions) { + callers[callee].insert(func->name); + } + + // HeapType -> Name for indirect calls + for (const auto& calleeType : info.indirectCalledTypes) { + callers[calleeType].insert(func->name); + } + + // Name -> HeapType for function types + // TODO: only look at functions that are addressable + // i.e. appear in a (ref.func) or are exported + callers[func->name].insert(func->type.getHeapType()); + + allIndirectCalledTypes.insert(func->type.getHeapType()); + } + + SubTypes subtypes(module); + for (auto type : allIndirectCalledTypes) { + subtypes.iterSubTypes(type, [&callers, type](HeapType sub, Index _) { + // HeapType -> HeapType + // A subtype is a 'callee' of its supertype. + // Supertypes need to inherit effects from their subtypes since they may + // be called via a ref to the subtype. + callers[sub].insert(type); + return true; + }); + } + + return callers; +} + // Propagate effects from callees to callers transitively // e.g. if A -> B -> C (A calls B which calls C) // Then B inherits effects from C and A inherits effects from both B and C. void propagateEffects( const Module& module, - const std::unordered_map>& reverseCallGraph, - std::map& funcInfos) { + const std::unordered_map>& + reverseCallGraph, + std::unordered_map& funcInfos) { - UniqueNonrepeatingDeferredQueue> work; + using CallGraphEdge = std::pair; + UniqueNonrepeatingDeferredQueue work; for (const auto& [callee, callers] : reverseCallGraph) { + // We only care about roots that will lead to a Name -> Name connection + // If there's a HeapType with no Name callee, we don't need to process it + // anyway. + if (!std::holds_alternative(callee)) { + continue; + } for (const auto& caller : callers) { work.push(std::pair(callee, caller)); } } - auto propagate = [&](Name callee, Name caller) { + auto propagate = [&](const CallGraphNode& calleeNode, + const CallGraphNode& callerNode) { + if (!std::holds_alternative(calleeNode) || + !std::holds_alternative(callerNode)) { + return; + } + + Name callee = std::get(calleeNode); + Name caller = std::get(callerNode); auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects; const auto& calleeEffects = funcInfos.at(module.getFunction(callee)).effects; - if (!callerEffects) { + if (callerEffects == UnknownEffects) { return; } - if (!calleeEffects) { + if (calleeEffects == UnknownEffects) { callerEffects = UnknownEffects; return; } - callerEffects->mergeIn(*calleeEffects); + if (callee == caller) { + callerEffects->trap = true; + } else { + callerEffects->mergeIn(*calleeEffects); + } }; while (!work.empty()) { auto [callee, caller] = work.pop(); - if (callee == caller) { - auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects; - if (callerEffects) { - callerEffects->trap = true; - } - } - // Even if nothing changed, we still need to keep traversing the callers // to look for a potential cycle which adds a trap affect on the above // lines. @@ -159,7 +251,8 @@ void propagateEffects( continue; } - for (const Name& callerCaller : callerCallers->second) { + // TODO: handle exact refs here + for (const CallGraphNode& callerCaller : callerCallers->second) { work.push(std::pair(callee, callerCaller)); } } @@ -167,16 +260,13 @@ void propagateEffects( struct GenerateGlobalEffects : public Pass { void run(Module* module) override { - std::map funcInfos = + std::unordered_map funcInfos = analyzeFuncs(*module, getPassOptions()); // callee : caller - std::unordered_map> callers; - for (const auto& [func, info] : funcInfos) { - for (const auto& callee : info.calledFunctions) { - callers[callee].insert(func->name); - } - } + std::unordered_map> + callers = + buildReverseCallGraph(*module, funcInfos, getPassOptions().closedWorld); propagateEffects(*module, callers, funcInfos); @@ -184,7 +274,7 @@ struct GenerateGlobalEffects : public Pass { // known. for (auto& [func, info] : funcInfos) { func->effects.reset(); - if (!info.effects) { + if (info.effects == UnknownEffects) { continue; } 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..8d89c48fd09 --- /dev/null +++ b/test/lit/passes/global-effects-closed-world.wast @@ -0,0 +1,326 @@ +;; 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 + (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)) + ) +) + +(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)) + ) +) + +(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 $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)) + ) +) + +(module + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + ;; CHECK: (type $sub (sub $super (struct))) + (type $sub (sub $super (struct))) + + ;; Supertype + ;; CHECK: (type $func-with-sub-param (sub (func (param (ref $sub))))) + (type $func-with-sub-param (sub (func (param (ref $sub))))) + ;; Subtype + ;; CHECK: (type $func-with-super-param (sub $func-with-sub-param (func (param (ref $super))))) + (type $func-with-super-param (sub $func-with-sub-param (func (param (ref $super))))) + + ;; CHECK: (func $nop-with-supertype (type $func-with-sub-param) (param $0 (ref $sub)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop-with-supertype (export "nop-with-supertype") (type $func-with-sub-param) (param (ref $sub)) + ) + + ;; CHECK: (func $effectful-with-subtype (type $func-with-super-param) (param $0 (ref $super)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $effectful-with-subtype (export "effectful-with-subtype") (type $func-with-super-param) (param (ref $super)) + (unreachable) + ) + + ;; CHECK: (func $calls-ref-with-subtype (type $3) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK-NEXT: (call_ref $func-with-sub-param + ;; CHECK-NEXT: (local.get $sub) + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-ref-with-subtype (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + (call_ref $func-with-sub-param (local.get $sub) (local.get $func)) + ) + + ;; CHECK: (func $f (type $3) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK-NEXT: (call $calls-ref-with-subtype + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: (local.get $sub) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; Check that we account for subtyping correctly. + ;; The type $func-with-sub-param (the supertype) has no effects (i.e. the + ;; union of all effects of functions with this type is empty). + ;; However, a subtype of $func-with-sub-param ($func-with-super-param) 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-subtype (local.get $func) (local.get $sub)) + ) +) + +;; 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 (struct))) + (type $super (sub (struct))) + ;; CHECK: (type $sub (sub $super (struct))) + (type $sub (sub $super (struct))) + + ;; Supertype + ;; CHECK: (type $func-with-sub-param (sub (func (param (ref $sub))))) + (type $func-with-sub-param (sub (func (param (ref $sub))))) + ;; Subtype + ;; CHECK: (type $func-with-super-param (sub $func-with-sub-param (func (param (ref $super))))) + (type $func-with-super-param (sub $func-with-sub-param (func (param (ref $super))))) + + ;; CHECK: (func $nop-with-supertype (type $func-with-sub-param) (param $0 (ref $sub)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop-with-supertype (export "nop-with-supertype") (type $func-with-sub-param) (param (ref $sub)) + ) + + ;; CHECK: (func $effectful-with-subtype (type $func-with-super-param) (param $0 (ref $super)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $effectful-with-subtype (export "effectful-with-subtype") (type $func-with-super-param) (param (ref $super)) + (unreachable) + ) + + ;; CHECK: (func $calls-ref-with-subtype (type $3) (param $func (ref (exact $func-with-sub-param))) (param $sub (ref $sub)) + ;; CHECK-NEXT: (call_ref $func-with-sub-param + ;; CHECK-NEXT: (local.get $sub) + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-ref-with-subtype (param $func (ref (exact $func-with-sub-param))) (param $sub (ref $sub)) + (call_ref $func-with-sub-param (local.get $sub) (local.get $func)) + ) + + ;; CHECK: (func $f (type $3) (param $func (ref (exact $func-with-sub-param))) (param $sub (ref $sub)) + ;; CHECK-NEXT: (call $calls-ref-with-subtype + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: (local.get $sub) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $func (ref (exact $func-with-sub-param))) (param $sub (ref $sub)) + (call $calls-ref-with-subtype (local.get $func) (local.get $sub)) + ) +) + +(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 + (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)) + ) +) + + +;; TODO functions that are referenced other ways besides exporting diff --git a/test/lit/passes/global-effects.wast b/test/lit/passes/global-effects.wast index 1125f738e68..0691a411d9b 100644 --- a/test/lit/passes/global-effects.wast +++ b/test/lit/passes/global-effects.wast @@ -13,14 +13,23 @@ ;; 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 $2 (func (param i32))) + ;; INCLUDE: (type $3 (func (result i32))) + + ;; INCLUDE: (type $4 (func (param i32))) ;; INCLUDE: (import "a" "b" (func $import (type $void))) (import "a" "b" (func $import)) @@ -150,7 +159,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 +168,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 +389,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 +404,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 +482,46 @@ (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)) + ) )