From bf88eae09338e35734feb23bd1c8b6745e2bfed4 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 13 Apr 2026 20:20:13 +0000 Subject: [PATCH 1/7] Support indirect call effects --- src/ir/subtypes.h | 7 ++- src/passes/GlobalEffects.cpp | 84 +++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/ir/subtypes.h b/src/ir/subtypes.h index 912d61f878d..fcd7e881e65 100644 --- a/src/ir/subtypes.h +++ b/src/ir/subtypes.h @@ -172,7 +172,8 @@ 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 +220,9 @@ 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..9dcbc7284a5 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -15,10 +15,11 @@ */ // -// 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/subtypes.h" #include "ir/effects.h" #include "ir/module-utils.h" #include "pass.h" @@ -39,6 +40,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, @@ -84,11 +88,16 @@ 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; + HeapType type; + if (auto* callRef = curr->dynCast()) { + type = callRef->target->type.getHeapType(); + } else if (auto* callIndirect = curr->dynCast()) { + type = callIndirect->heapType; + } else { + assert(false && "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,15 +116,18 @@ std::map analyzeFuncs(Module& module, return std::move(analysis.map); } +using CallGraphNode = std::variant; + // 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, + const std::unordered_map>& reverseCallGraph, std::map& funcInfos) { - UniqueNonrepeatingDeferredQueue> work; + using CallGraphEdge = std::pair; + UniqueNonrepeatingDeferredQueue work; for (const auto& [callee, callers] : reverseCallGraph) { for (const auto& caller : callers) { @@ -123,27 +135,35 @@ void propagateEffects( } } - auto propagate = [&](Name callee, Name caller) { - auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects; + auto propagate = [&](Name* callee, Name* caller) { + if (callee == nullptr || caller == nullptr) { + return; + } + + auto& callerEffects = funcInfos.at(module.getFunction(*caller)).effects; const auto& calleeEffects = - funcInfos.at(module.getFunction(callee)).effects; - if (!callerEffects) { + funcInfos.at(module.getFunction(*callee)).effects; + 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 (std::get_if(&callee) == std::get_if(&caller) && std::holds_alternative(callee)) { + auto& callerEffects = funcInfos.at(module.getFunction(std::get(caller))).effects; if (callerEffects) { callerEffects->trap = true; } @@ -152,14 +172,14 @@ void propagateEffects( // 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. - propagate(callee, caller); + propagate(std::get_if(&callee), std::get_if(&caller)); const auto& callerCallers = reverseCallGraph.find(caller); if (callerCallers == reverseCallGraph.end()) { continue; } - for (const Name& callerCaller : callerCallers->second) { + for (const CallGraphNode& callerCaller : callerCallers->second) { work.push(std::pair(callee, callerCaller)); } } @@ -171,11 +191,35 @@ struct GenerateGlobalEffects : public Pass { analyzeFuncs(*module, getPassOptions()); // callee : caller - std::unordered_map> callers; + std::unordered_map> 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 + 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, int _) { + // HeapType -> HeapType + // A subtype is a 'callee' of its supertype. Supertypes need to inherit effects from their subtypes + // See the example in (TODO) + callers[sub].insert(type); + return true; + }); } propagateEffects(*module, callers, funcInfos); From 7d1b989f89cd932ab304cdc75fa7ef9efaa1d56a Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 13 Apr 2026 23:29:26 +0000 Subject: [PATCH 2/7] Add test --- .../passes/global-effects-closed-world.wast | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 test/lit/passes/global-effects-closed-world.wast 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..aae2f48b690 --- /dev/null +++ b/test/lit/passes/global-effects-closed-world.wast @@ -0,0 +1,232 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s -all --closed-world --generate-global-effects --vacuum -S -o - | filecheck %s + +(module + ;; CHECK: (type $maybe-has-effects (func (param i32 i32))) + (type $maybe-has-effects (func (param i32 i32))) + + ;; CHECK: (type $only-has-effects-in-non-exported-function (func (param f32 f32))) + + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + ;; CHECK: (type $sub (sub $super (struct))) + (type $sub (sub $super (struct))) + + ;; CHECK: (type $func-with-sub-param (sub (func (param (ref $sub))))) + + ;; CHECK: (type $uninhabited (func (param f32))) + (type $uninhabited (func (param f32))) + + ;; Subtype + (type $func-with-sub-param (sub (func (param (ref $sub))))) + + ;; Subtype + ;; CHECK: (type $ttt (func (param i64))) + + ;; 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))))) + + (type $only-has-effects-in-non-exported-function (func (param f32 f32))) + + (table 10 funcref) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (elem declare func $A) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) (param $1 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32 i32) + (unreachable) + ) + + ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) (param $1 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32 i32) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-ref (type $8) (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 $f has no effects. + (call_ref $nopType (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $8) (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 $calls-effectful-function-via-ref (type $9) (param $ref (ref $maybe-has-effects)) + ;; CHECK-NEXT: (call_ref $maybe-has-effects + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 2) + ;; 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) (i32.const 2) (local.get $ref)) + ) + + ;; CHECK: (func $g (type $9) (param $ref (ref $maybe-has-effects)) + ;; CHECK-NEXT: (call $calls-effectful-function-via-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $g (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)) + ) + + ;; CHECK: (func $calls-uninhabited (type $10) (param $ref (ref $uninhabited)) + ;; CHECK-NEXT: (call_ref $uninhabited + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-uninhabited (param $ref (ref $uninhabited)) + (call_ref $uninhabited (f32.const 0) (local.get $ref)) + ) + + ;; CHECK: (func $h (type $10) (param $ref (ref $uninhabited)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $h (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 $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 $11) (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 $asdf (type $11) (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 $asdf (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)) + ) + + ;; CHECK: (func $no-effects (type $only-has-effects-in-non-exported-function) (param $0 f32) (param $1 f32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $no-effects (type $only-has-effects-in-non-exported-function) (param f32 f32) + ) + + ;; CHECK: (func $has-effects-but-not-exported (type $only-has-effects-in-non-exported-function) (param $0 f32) (param $1 f32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $has-effects-but-not-exported (type $only-has-effects-in-non-exported-function) (param f32 f32) + (unreachable) + ) + + ;; CHECK: (func $calls-type-with-effects-but-not-addressable (type $12) (param $ref (ref $only-has-effects-in-non-exported-function)) + ;; CHECK-NEXT: (call_ref $only-has-effects-in-non-exported-function + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-type-with-effects-but-not-addressable (param $ref (ref $only-has-effects-in-non-exported-function)) + (call_ref $only-has-effects-in-non-exported-function (f32.const 0) (f32.const 0) (local.get $ref)) + ) + + ;; CHECK: (func $i (type $12) (param $ref (ref $only-has-effects-in-non-exported-function)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $i (param $ref (ref $only-has-effects-in-non-exported-function)) + (call $calls-type-with-effects-but-not-addressable (local.get $ref)) + ) + + ;; CHECK: (func $B (type $6) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $B + (unreachable) + ) + + (type $ttt (func (param i64))) + + ;; CHECK: (func $A (type $ttt) (param $0 i64) + ;; CHECK-NEXT: (call $B) + ;; CHECK-NEXT: ) + (func $A (param i64) + (call $B) + ;; (call_ref $ttt (ref.func $u)) + ) + + (elem declare $two) + + ;; CHECK: (func $C (type $6) + ;; CHECK-NEXT: (call_ref $ttt + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: (ref.func $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $C + (call_ref $ttt (i64.const 0) (ref.func $A)) + ) + + ;; CHECK: (func $foo (type $6) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $foo + (call $C) + ) +) From 01198f85d0b44ebe86dd17d1e9bfb9180946aed9 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 13 Apr 2026 23:29:57 +0000 Subject: [PATCH 3/7] Finish? --- src/passes/GlobalEffects.cpp | 21 ++++++++++++++++++- .../passes/global-effects-closed-world.wast | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 9dcbc7284a5..6c664539614 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -19,10 +19,12 @@ // Function::effects; see more details there. // +#include "ir/table-utils.h" #include "ir/subtypes.h" #include "ir/effects.h" #include "ir/module-utils.h" #include "pass.h" +#include "ir/element-utils.h" #include "support/unique_deferring_queue.h" #include "wasm.h" @@ -194,6 +196,20 @@ struct GenerateGlobalEffects : public Pass { std::unordered_map> callers; std::unordered_set allIndirectCalledTypes; + + std::unordered_set funcsWithAddress; + + auto refFuncs = TableUtils::getFunctionsNeedingElemDeclare(*module); + funcsWithAddress.insert(refFuncs.begin(), refFuncs.end()); + ElementUtils::iterAllElementFunctionNames(module, [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); + for (const auto& export_ : module->exports) { + if (export_->kind == ExternalKind::Function) { + // This exported function might flow back to us even in a closed world, + // so it's essentially addressed. + funcsWithAddress.insert(export_->name); + } + } + for (const auto& [func, info] : funcInfos) { // Name -> Name for direct calls for (const auto& callee : info.calledFunctions) { @@ -206,7 +222,9 @@ struct GenerateGlobalEffects : public Pass { } // Name -> HeapType for function types - callers[func->name].insert(func->type.getHeapType()); + if (funcsWithAddress.contains(func->name)) { + callers[func->name].insert(func->type.getHeapType()); + } allIndirectCalledTypes.insert(func->type.getHeapType()); } @@ -218,6 +236,7 @@ struct GenerateGlobalEffects : public Pass { // A subtype is a 'callee' of its supertype. Supertypes need to inherit effects from their subtypes // See the example in (TODO) callers[sub].insert(type); + // callers[type].insert(sub); return true; }); } diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index aae2f48b690..a46588e3a20 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -206,7 +206,7 @@ ;; CHECK: (func $A (type $ttt) (param $0 i64) ;; CHECK-NEXT: (call $B) ;; CHECK-NEXT: ) - (func $A (param i64) + (func $A (export "A") (param i64) (call $B) ;; (call_ref $ttt (ref.func $u)) ) @@ -224,7 +224,7 @@ ) ;; CHECK: (func $foo (type $6) - ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (call $C) ;; CHECK-NEXT: ) (func $foo (call $C) From c0543a19bdc2eb3e7b4c425ae40bee60551441c4 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 14 Apr 2026 00:18:13 +0000 Subject: [PATCH 4/7] Add more tests + split up --- src/ir/subtypes.h | 10 +- src/passes/GlobalEffects.cpp | 30 +-- .../passes/global-effects-closed-world.wast | 192 +++++++++--------- 3 files changed, 121 insertions(+), 111 deletions(-) diff --git a/src/ir/subtypes.h b/src/ir/subtypes.h index fcd7e881e65..7977429ac77 100644 --- a/src/ir/subtypes.h +++ b/src/ir/subtypes.h @@ -173,7 +173,10 @@ struct SubTypes { // true if we did not stop early, and false if we did. template bool iterSubTypes(HeapType type, Index depth, F func) const - requires requires(F func, HeapType subtype, Index depth) { { func(subtype, depth) } -> std::same_as; } { + 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; @@ -222,7 +225,10 @@ struct SubTypes { // As above, but iterate to the maximum depth. template bool iterSubTypes(HeapType type, F func) const - requires requires(F func, HeapType subtype, Index depth) { { func(subtype, depth) } -> std::same_as; } { + 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 6c664539614..342404b7555 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -19,12 +19,12 @@ // Function::effects; see more details there. // -#include "ir/table-utils.h" -#include "ir/subtypes.h" #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 "ir/element-utils.h" #include "support/unique_deferring_queue.h" #include "wasm.h" @@ -125,7 +125,8 @@ using CallGraphNode = std::variant; // 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, + const std::unordered_map>& + reverseCallGraph, std::map& funcInfos) { using CallGraphEdge = std::pair; @@ -164,8 +165,10 @@ void propagateEffects( while (!work.empty()) { auto [callee, caller] = work.pop(); - if (std::get_if(&callee) == std::get_if(&caller) && std::holds_alternative(callee)) { - auto& callerEffects = funcInfos.at(module.getFunction(std::get(caller))).effects; + if (std::get_if(&callee) == std::get_if(&caller) && + std::holds_alternative(callee)) { + auto& callerEffects = + funcInfos.at(module.getFunction(std::get(caller))).effects; if (callerEffects) { callerEffects->trap = true; } @@ -193,7 +196,8 @@ struct GenerateGlobalEffects : public Pass { analyzeFuncs(*module, getPassOptions()); // callee : caller - std::unordered_map> callers; + std::unordered_map> + callers; std::unordered_set allIndirectCalledTypes; @@ -201,7 +205,9 @@ struct GenerateGlobalEffects : public Pass { auto refFuncs = TableUtils::getFunctionsNeedingElemDeclare(*module); funcsWithAddress.insert(refFuncs.begin(), refFuncs.end()); - ElementUtils::iterAllElementFunctionNames(module, [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); + ElementUtils::iterAllElementFunctionNames( + module, + [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); for (const auto& export_ : module->exports) { if (export_->kind == ExternalKind::Function) { // This exported function might flow back to us even in a closed world, @@ -233,10 +239,10 @@ struct GenerateGlobalEffects : public Pass { for (auto type : allIndirectCalledTypes) { subtypes.iterSubTypes(type, [&callers, type](HeapType sub, int _) { // HeapType -> HeapType - // A subtype is a 'callee' of its supertype. Supertypes need to inherit effects from their subtypes - // See the example in (TODO) + // 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); - // callers[type].insert(sub); return true; }); } @@ -247,7 +253,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 index a46588e3a20..a9862d0cd7f 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -1,43 +1,10 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: wasm-opt %s -all --closed-world --generate-global-effects --vacuum -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt -all --closed-world --generate-global-effects --vacuum -S -o - | filecheck %s (module - ;; CHECK: (type $maybe-has-effects (func (param i32 i32))) - (type $maybe-has-effects (func (param i32 i32))) - - ;; CHECK: (type $only-has-effects-in-non-exported-function (func (param f32 f32))) - ;; CHECK: (type $nopType (func (param i32))) (type $nopType (func (param i32))) - ;; CHECK: (type $super (sub (struct))) - (type $super (sub (struct))) - ;; CHECK: (type $sub (sub $super (struct))) - (type $sub (sub $super (struct))) - - ;; CHECK: (type $func-with-sub-param (sub (func (param (ref $sub))))) - - ;; CHECK: (type $uninhabited (func (param f32))) - (type $uninhabited (func (param f32))) - - ;; Subtype - (type $func-with-sub-param (sub (func (param (ref $sub))))) - - ;; Subtype - ;; CHECK: (type $ttt (func (param i64))) - - ;; 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))))) - - (type $only-has-effects-in-non-exported-function (func (param f32 f32))) - - (table 10 funcref) - - ;; CHECK: (global $g (mut i32) (i32.const 0)) - (global $g (mut i32) (i32.const 0)) - - ;; CHECK: (elem declare func $A) - ;; CHECK: (func $nop (type $nopType) (param $0 i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) @@ -45,21 +12,7 @@ (nop) ) - ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) (param $1 i32) - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32 i32) - (unreachable) - ) - - ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) (param $1 i32) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: ) - (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32 i32) - (nop) - ) - - ;; CHECK: (func $calls-nop-via-ref (type $8) (param $ref (ref $nopType)) + ;; 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) @@ -69,11 +22,12 @@ ;; 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 $f has no effects. + ;; 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 $8) (param $ref (ref $nopType)) + ;; CHECK: (func $f (type $1) (param $ref (ref $nopType)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $f (param $ref (ref $nopType)) @@ -81,48 +35,85 @@ ;; 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 $9) (param $ref (ref $maybe-has-effects)) + ;; 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: (i32.const 2) ;; 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) (i32.const 2) (local.get $ref)) + (call_ref $maybe-has-effects (i32.const 1) (local.get $ref)) ) - ;; CHECK: (func $g (type $9) (param $ref (ref $maybe-has-effects)) + ;; 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 $g (param $ref (ref $maybe-has-effects)) + (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 $10) (param $ref (ref $uninhabited)) + ;; CHECK: (func $calls-uninhabited (type $1) (param $ref (ref $uninhabited)) ;; CHECK-NEXT: (call_ref $uninhabited - ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $calls-uninhabited (param $ref (ref $uninhabited)) - (call_ref $uninhabited (f32.const 0) (local.get $ref)) + (call_ref $uninhabited (i32.const 1) (local.get $ref)) ) - ;; CHECK: (func $h (type $10) (param $ref (ref $uninhabited)) + ;; CHECK: (func $f (type $1) (param $ref (ref $uninhabited)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $h (param $ref (ref $uninhabited)) + (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) @@ -137,7 +128,7 @@ (unreachable) ) - ;; CHECK: (func $calls-ref-with-subtype (type $11) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; 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) @@ -147,13 +138,13 @@ (call_ref $func-with-sub-param (local.get $sub) (local.get $func)) ) - ;; CHECK: (func $asdf (type $11) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; 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 $asdf (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + (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). @@ -162,71 +153,78 @@ ;; include the unreachable effect and we can't optimize out this call. (call $calls-ref-with-subtype (local.get $func) (local.get $sub)) ) +) - ;; CHECK: (func $no-effects (type $only-has-effects-in-non-exported-function) (param $0 f32) (param $1 f32) +(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 $no-effects (type $only-has-effects-in-non-exported-function) (param f32 f32) + (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-non-exported-function) (param $0 f32) (param $1 f32) + ;; 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-non-exported-function) (param f32 f32) + (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 $12) (param $ref (ref $only-has-effects-in-non-exported-function)) - ;; CHECK-NEXT: (call_ref $only-has-effects-in-non-exported-function - ;; CHECK-NEXT: (f32.const 0) - ;; CHECK-NEXT: (f32.const 0) + ;; 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-non-exported-function)) - (call_ref $only-has-effects-in-non-exported-function (f32.const 0) (f32.const 0) (local.get $ref)) + (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 $i (type $12) (param $ref (ref $only-has-effects-in-non-exported-function)) + ;; CHECK: (func $f (type $1) (param $ref (ref $only-has-effects-in-not-addressable-function)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $i (param $ref (ref $only-has-effects-in-non-exported-function)) + (func $f (param $ref (ref $only-has-effects-in-not-addressable-function)) (call $calls-type-with-effects-but-not-addressable (local.get $ref)) ) +) - ;; CHECK: (func $B (type $6) +(module + ;; CHECK: (type $unreachable-via-direct-call (func (param i32))) + (type $unreachable-via-direct-call (func (param i32))) + + ;; CHECK: (func $unreachable (type $0) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) - (func $B + (func $unreachable (unreachable) ) - (type $ttt (func (param i64))) - - ;; CHECK: (func $A (type $ttt) (param $0 i64) - ;; CHECK-NEXT: (call $B) + ;; CHECK: (func $calls-unreachable (type $unreachable-via-direct-call) (param $0 i32) + ;; CHECK-NEXT: (call $unreachable) ;; CHECK-NEXT: ) - (func $A (export "A") (param i64) - (call $B) - ;; (call_ref $ttt (ref.func $u)) + (func $calls-unreachable (export "calls-unreachable") (param i32) + (call $unreachable) ) - (elem declare $two) - - ;; CHECK: (func $C (type $6) - ;; CHECK-NEXT: (call_ref $ttt - ;; CHECK-NEXT: (i64.const 0) - ;; CHECK-NEXT: (ref.func $A) + ;; 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 $C - (call_ref $ttt (i64.const 0) (ref.func $A)) + (func $calls-unreachable-via-ref-and-direct-call-transtively + (call_ref $unreachable-via-direct-call (i32.const 0) (ref.func $calls-unreachable)) ) - ;; CHECK: (func $foo (type $6) - ;; CHECK-NEXT: (call $C) + ;; CHECK: (func $f (type $0) + ;; CHECK-NEXT: (call $calls-unreachable-via-ref-and-direct-call-transtively) ;; CHECK-NEXT: ) - (func $foo - (call $C) + (func $f + (call $calls-unreachable-via-ref-and-direct-call-transtively) ) ) + +;; TODO exact types +;; TODO functions that are referenced other ways besides exporting From 8fcba01f4f9788e07670acb3b684a5dfccfeb957 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 14 Apr 2026 00:35:38 +0000 Subject: [PATCH 5/7] Start cleaning up --- src/passes/GlobalEffects.cpp | 126 ++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 55 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 342404b7555..6cf5d19a2d0 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -120,6 +120,68 @@ std::map analyzeFuncs(Module& module, 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 +std::unordered_map> buildReverseCallGraph(Module& module, const std::map funcInfos) { + // callee : caller + std::unordered_map> + callers; + + std::unordered_set allIndirectCalledTypes; + + std::unordered_set funcsWithAddress; + + auto refFuncs = TableUtils::getFunctionsNeedingElemDeclare(module); + funcsWithAddress.insert(refFuncs.begin(), refFuncs.end()); + ElementUtils::iterAllElementFunctionNames( + &module, + [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); + for (const auto& export_ : module.exports) { + if (export_->kind == ExternalKind::Function) { + // This exported function might flow back to us even in a closed world, + // so it's essentially addressed. + funcsWithAddress.insert(export_->name); + } + } + + 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 + if (funcsWithAddress.contains(func->name)) { + 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, int _) { + // 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. @@ -138,14 +200,16 @@ void propagateEffects( } } - auto propagate = [&](Name* callee, Name* caller) { - if (callee == nullptr || caller == nullptr) { + auto propagate = [&](const CallGraphNode& calleeNode, const CallGraphNode& callerNode) { + if (!std::get_if(&calleeNode) || !std::get_if(&callerNode)) { return; } - auto& callerEffects = funcInfos.at(module.getFunction(*caller)).effects; + 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; + funcInfos.at(module.getFunction(callee)).effects; if (callerEffects == UnknownEffects) { return; } @@ -155,7 +219,7 @@ void propagateEffects( return; } - if (*callee == *caller) { + if (callee == caller) { callerEffects->trap = true; } else { callerEffects->mergeIn(*calleeEffects); @@ -177,7 +241,7 @@ void propagateEffects( // 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. - propagate(std::get_if(&callee), std::get_if(&caller)); + propagate(callee, caller); const auto& callerCallers = reverseCallGraph.find(caller); if (callerCallers == reverseCallGraph.end()) { @@ -197,55 +261,7 @@ struct GenerateGlobalEffects : public Pass { // callee : caller std::unordered_map> - callers; - - std::unordered_set allIndirectCalledTypes; - - std::unordered_set funcsWithAddress; - - auto refFuncs = TableUtils::getFunctionsNeedingElemDeclare(*module); - funcsWithAddress.insert(refFuncs.begin(), refFuncs.end()); - ElementUtils::iterAllElementFunctionNames( - module, - [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); - for (const auto& export_ : module->exports) { - if (export_->kind == ExternalKind::Function) { - // This exported function might flow back to us even in a closed world, - // so it's essentially addressed. - funcsWithAddress.insert(export_->name); - } - } - - 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 - if (funcsWithAddress.contains(func->name)) { - 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, int _) { - // 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; - }); - } + callers = buildReverseCallGraph(*module, funcInfos); propagateEffects(*module, callers, funcInfos); From 2760fc2937b93e9adb96820f2dfc81d842352db3 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 14 Apr 2026 01:00:08 +0000 Subject: [PATCH 6/7] ? --- import-test.wast | 35 ++++++++++ src/passes/GlobalEffects.cpp | 65 +++++++++++++++---- .../passes/global-effects-closed-world.wast | 38 +++++++++++ test/lit/passes/global-effects-eh-legacy.wast | 16 ----- 4 files changed, 124 insertions(+), 30 deletions(-) create mode 100644 import-test.wast 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/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 6cf5d19a2d0..5fcf8cf1d32 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -90,6 +90,11 @@ std::map analyzeFuncs(Module& module, // Note the direct call. funcInfo.calledFunctions.insert(call->target); } else if (effects.calls) { + if (!options.closedWorld) { + funcInfo.effects = UnknownEffects; + return; + } + HeapType type; if (auto* callRef = curr->dynCast()) { type = callRef->target->type.getHeapType(); @@ -118,6 +123,36 @@ std::map analyzeFuncs(Module& module, return std::move(analysis.map); } +// Funcs that can be the target of a virtual call +// These are either: +// - Part of an (elem declare ...) or (elem ...) directive +// - Exported, since they may flow back to us from the host +std::unordered_set getFuncsWithAddress(Module& module) { + std::unordered_set funcsWithAddress; + for (const auto& fun : module.functions) { + funcsWithAddress.insert(fun->name); + } + return funcsWithAddress; + + // { + // auto refFuncs = TableUtils::getFunctionsNeedingElemDeclare(module); + // funcsWithAddress.insert(refFuncs.begin(), refFuncs.end()); + // } + + // ElementUtils::iterAllElementFunctionNames( + // &module, + // [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); + // for (const auto& export_ : module.exports) { + // if (export_->kind == ExternalKind::Function) { + // // This exported function might flow back to us even in a closed world, + // // so it's essentially addressed. + // funcsWithAddress.insert(export_->name); + // } + // } + + // return funcsWithAddress; +} + using CallGraphNode = std::variant; // Build a call graph for indirect and direct calls. @@ -126,6 +161,8 @@ using CallGraphNode = std::variant; // 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 + +// TODO: only track indirect calls in closed world std::unordered_map> buildReverseCallGraph(Module& module, const std::map funcInfos) { // callee : caller std::unordered_map> @@ -133,20 +170,11 @@ std::unordered_map> buildRevers std::unordered_set allIndirectCalledTypes; - std::unordered_set funcsWithAddress; - - auto refFuncs = TableUtils::getFunctionsNeedingElemDeclare(module); - funcsWithAddress.insert(refFuncs.begin(), refFuncs.end()); - ElementUtils::iterAllElementFunctionNames( - &module, - [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); - for (const auto& export_ : module.exports) { - if (export_->kind == ExternalKind::Function) { - // This exported function might flow back to us even in a closed world, - // so it's essentially addressed. - funcsWithAddress.insert(export_->name); - } - } + // Funcs that can be the target of a virtual call + // These are either: + // - Part of an (elem declare ...) or (elem ...) directive + // - Exported, since they may flow back to us from the host + std::unordered_set funcsWithAddress = getFuncsWithAddress(module); for (const auto& [func, info] : funcInfos) { // Name -> Name for direct calls @@ -263,6 +291,15 @@ struct GenerateGlobalEffects : public Pass { std::unordered_map> callers = buildReverseCallGraph(*module, funcInfos); + // for (const auto& [callee, callers] : callers) { + // for (const auto& caller : callers) { + // const auto* calleeName = std::get_if(&callee); + // const auto* callerName = std::get_if(&caller); + // if (!calleeName || !callerName) continue; + // std::cout<<*calleeName<<"\t\t->\t\t"<<*callerName<<"\n"; + // } + // } + propagateEffects(*module, callers, funcInfos); // Generate the final data, starting from a blank slate where nothing is diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index a9862d0cd7f..659f997ae88 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -194,6 +194,8 @@ ;; 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: ) @@ -226,5 +228,41 @@ ) ) +(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: (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)) + ) +) + ;; TODO exact types ;; TODO functions that are referenced other ways besides exporting diff --git a/test/lit/passes/global-effects-eh-legacy.wast b/test/lit/passes/global-effects-eh-legacy.wast index dae61274e10..65cd8db372e 100644 --- a/test/lit/passes/global-effects-eh-legacy.wast +++ b/test/lit/passes/global-effects-eh-legacy.wast @@ -327,22 +327,6 @@ ;; WITHOUT-NEXT: (call $return-call-ref-throw-and-catch) ;; WITHOUT-NEXT: ) ;; INCLUDE: (func $call-return-call-throw-and-catch (type $void) - ;; INCLUDE-NEXT: (try - ;; INCLUDE-NEXT: (do - ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) - ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (catch_all - ;; INCLUDE-NEXT: (nop) - ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (try - ;; INCLUDE-NEXT: (do - ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) - ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (catch_all - ;; INCLUDE-NEXT: (nop) - ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: (call $return-call-throw-and-catch) ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) From c86c680cb200f7217eb582279bb544ba381695d8 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 14 Apr 2026 19:52:55 +0000 Subject: [PATCH 7/7] done? --- src/ir/module-utils.h | 2 +- src/passes/GlobalEffects.cpp | 120 +++++++----------- .../passes/global-effects-closed-world.wast | 64 +++++++++- test/lit/passes/global-effects-eh-legacy.wast | 16 +++ test/lit/passes/global-effects.wast | 67 ++++++++-- 5 files changed, 181 insertions(+), 88 deletions(-) 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/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 5fcf8cf1d32..3d419157200 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -47,10 +47,10 @@ struct FuncInfo { 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 @@ -101,7 +101,7 @@ std::map analyzeFuncs(Module& module, } else if (auto* callIndirect = curr->dynCast()) { type = callIndirect->heapType; } else { - assert(false && "Unexpected type of call"); + assert("Unexpected type of call"); } funcInfo.indirectCalledTypes.insert(type); @@ -123,58 +123,34 @@ std::map analyzeFuncs(Module& module, return std::move(analysis.map); } -// Funcs that can be the target of a virtual call -// These are either: -// - Part of an (elem declare ...) or (elem ...) directive -// - Exported, since they may flow back to us from the host -std::unordered_set getFuncsWithAddress(Module& module) { - std::unordered_set funcsWithAddress; - for (const auto& fun : module.functions) { - funcsWithAddress.insert(fun->name); - } - return funcsWithAddress; - - // { - // auto refFuncs = TableUtils::getFunctionsNeedingElemDeclare(module); - // funcsWithAddress.insert(refFuncs.begin(), refFuncs.end()); - // } - - // ElementUtils::iterAllElementFunctionNames( - // &module, - // [&funcsWithAddress](Name name) { funcsWithAddress.insert(name); }); - // for (const auto& export_ : module.exports) { - // if (export_->kind == ExternalKind::Function) { - // // This exported function might flow back to us even in a closed world, - // // so it's essentially addressed. - // funcsWithAddress.insert(export_->name); - // } - // } - - // return funcsWithAddress; -} - 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 - -// TODO: only track indirect calls in closed world -std::unordered_map> buildReverseCallGraph(Module& module, const std::map funcInfos) { +// 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; + std::unordered_map> callers; - std::unordered_set allIndirectCalledTypes; + 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; + } - // Funcs that can be the target of a virtual call - // These are either: - // - Part of an (elem declare ...) or (elem ...) directive - // - Exported, since they may flow back to us from the host - std::unordered_set funcsWithAddress = getFuncsWithAddress(module); + std::unordered_set allIndirectCalledTypes; for (const auto& [func, info] : funcInfos) { // Name -> Name for direct calls @@ -188,16 +164,16 @@ std::unordered_map> buildRevers } // Name -> HeapType for function types - if (funcsWithAddress.contains(func->name)) { - callers[func->name].insert(func->type.getHeapType()); - } + // 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, int _) { + 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 @@ -217,19 +193,27 @@ void propagateEffects( const Module& module, const std::unordered_map>& reverseCallGraph, - std::map& funcInfos) { + std::unordered_map& funcInfos) { 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 = [&](const CallGraphNode& calleeNode, const CallGraphNode& callerNode) { - if (!std::get_if(&calleeNode) || !std::get_if(&callerNode)) { + auto propagate = [&](const CallGraphNode& calleeNode, + const CallGraphNode& callerNode) { + if (!std::holds_alternative(calleeNode) || + !std::holds_alternative(callerNode)) { return; } @@ -257,15 +241,6 @@ void propagateEffects( while (!work.empty()) { auto [callee, caller] = work.pop(); - if (std::get_if(&callee) == std::get_if(&caller) && - std::holds_alternative(callee)) { - auto& callerEffects = - funcInfos.at(module.getFunction(std::get(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. @@ -276,6 +251,7 @@ void propagateEffects( continue; } + // TODO: handle exact refs here for (const CallGraphNode& callerCaller : callerCallers->second) { work.push(std::pair(callee, callerCaller)); } @@ -284,21 +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 = buildReverseCallGraph(*module, funcInfos); - - // for (const auto& [callee, callers] : callers) { - // for (const auto& caller : callers) { - // const auto* calleeName = std::get_if(&callee); - // const auto* callerName = std::get_if(&caller); - // if (!calleeName || !callerName) continue; - // std::cout<<*calleeName<<"\t\t->\t\t"<<*callerName<<"\n"; - // } - // } + callers = + buildReverseCallGraph(*module, funcInfos, getPassOptions().closedWorld); propagateEffects(*module, callers, funcInfos); diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 659f997ae88..8d89c48fd09 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -155,6 +155,56 @@ ) ) +;; 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))) @@ -183,9 +233,15 @@ ) ;; CHECK: (func $f (type $1) (param $ref (ref $only-has-effects-in-not-addressable-function)) - ;; CHECK-NEXT: (nop) + ;; 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)) ) ) @@ -255,7 +311,9 @@ ) ;; CHECK: (func $f (type $1) (param $ref (ref $t)) - ;; CHECK-NEXT: (nop) + ;; 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, @@ -264,5 +322,5 @@ ) ) -;; TODO exact types + ;; TODO functions that are referenced other ways besides exporting diff --git a/test/lit/passes/global-effects-eh-legacy.wast b/test/lit/passes/global-effects-eh-legacy.wast index 65cd8db372e..dae61274e10 100644 --- a/test/lit/passes/global-effects-eh-legacy.wast +++ b/test/lit/passes/global-effects-eh-legacy.wast @@ -327,6 +327,22 @@ ;; WITHOUT-NEXT: (call $return-call-ref-throw-and-catch) ;; WITHOUT-NEXT: ) ;; INCLUDE: (func $call-return-call-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (try + ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (catch_all + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (try + ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (catch_all + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: (call $return-call-throw-and-catch) ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) 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)) + ) )