From ca88c5b5be139e8dd8d01694b8569996e5dcb769 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 6 Apr 2026 23:20:57 +0000 Subject: [PATCH 01/11] Add tests for indirect calls --- .../passes/global-effects-closed-world.wast | 83 +++++++++++++++++++ 1 file changed, 83 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..6ace6cb6bfc --- /dev/null +++ b/test/lit/passes/global-effects-closed-world.wast @@ -0,0 +1,83 @@ +;; 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))) + + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + (type $maybe-has-effects (func (param i32 i32))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (type $nopType) + (nop) + ) + + ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) (param $1 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $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 (type $maybe-has-effects) (param i32 i32) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-ref (type $2) (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 $3) (param $ref (ref $nopType)) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $nopType)) (result i32) + ;; $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)) + (i32.const 1) + ) + + ;; CHECK: (func $calls-effectful-function-via-ref (type $4) (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 $5) (param $ref (ref $maybe-has-effects)) (result i32) + ;; CHECK-NEXT: (call $calls-effectful-function-via-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $g (param $ref (ref $maybe-has-effects)) (result i32) + ;; 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)) + (i32.const 1) + ) +) From 513362cccfb83910f55111c0e844309195407662 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 6 Apr 2026 23:23:59 +0000 Subject: [PATCH 02/11] Start changing --- src/passes/GlobalEffects.cpp | 182 ++++++++++-------- .../passes/global-effects-closed-world.wast | 3 + 2 files changed, 104 insertions(+), 81 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index ef0977d12fa..e4bec6cb98f 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -26,99 +26,117 @@ #include "wasm.h" namespace wasm { +namespace { -struct GenerateGlobalEffects : public Pass { - void run(Module* module) override { - // First, we do a scan of each function to see what effects they have, - // including which functions they call directly (so that we can compute - // transitive effects later). +struct FuncInfo { + // Effects in this function. + std::optional effects; - struct FuncInfo { - // Effects in this function. - std::optional effects; - - // Directly-called functions from this function. - std::unordered_set calledFunctions; - }; - - 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 - // map (which we do by not setting funcInfo.effects). - return; - } + // 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) { + 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 + // map (which we do by not setting funcInfo.effects). + return; + } - // Gather the effects. - funcInfo.effects.emplace(getPassOptions(), *module, func); - - if (funcInfo.effects->calls) { - // There are calls in this function, which we will analyze in detail. - // Clear the |calls| field first, and we'll handle calls of all sorts - // below. - funcInfo.effects->calls = false; - - // Clear throws as well, as we are "forgetting" calls right now, and - // want to forget their throwing effect as well. If we see something - // else that throws, below, then we'll note that there. - funcInfo.effects->throws_ = false; - - struct CallScanner - : public PostWalker> { - Module& wasm; - PassOptions& options; - FuncInfo& funcInfo; - - CallScanner(Module& wasm, PassOptions& options, FuncInfo& funcInfo) - : wasm(wasm), options(options), funcInfo(funcInfo) {} - - void visitExpression(Expression* curr) { - ShallowEffectAnalyzer effects(options, wasm, curr); - if (auto* call = curr->dynCast()) { - // 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.reset(); + // Gather the effects. + funcInfo.effects.emplace(passOptions, module, func); + + if (funcInfo.effects->calls) { + // There are calls in this function, which we will analyze in detail. + // Clear the |calls| field first, and we'll handle calls of all sorts + // below. + funcInfo.effects->calls = false; + + // Clear throws as well, as we are "forgetting" calls right now, and + // want to forget their throwing effect as well. If we see something + // else that throws, below, then we'll note that there. + funcInfo.effects->throws_ = false; + + struct CallScanner + : public PostWalker> { + Module& wasm; + const PassOptions& options; + FuncInfo& funcInfo; + + CallScanner(Module& wasm, + const PassOptions& options, + FuncInfo& funcInfo) + : wasm(wasm), options(options), funcInfo(funcInfo) {} + + void visitExpression(Expression* curr) { + ShallowEffectAnalyzer effects(options, wasm, curr); + if (auto* call = curr->dynCast()) { + // Note the direct call. + funcInfo.calledFunctions.insert(call->target); + } else if (effects.calls) { + HeapType type; + if (auto* callRef = curr->dynCast()) { + type = callRef->target->type.getHeapType(); + } else if (auto* callIndirect = curr->dynCast()) { + type = callIndirect->heapType; } 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 - - // then we assume the worst anyhow, and have nothing to update.) - if (effects.throws_ && funcInfo.effects) { - funcInfo.effects->throws_ = true; - } + assert(false && "Unexpected type of call"); + } + + funcInfo.indirectCalledTypes.insert(type); + funcInfo.effects.reset(); + } 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 - + // then we assume the worst anyhow, and have nothing to update.) + if (effects.throws_ && funcInfo.effects) { + funcInfo.effects->throws_ = true; } } - }; - CallScanner scanner(*module, getPassOptions(), funcInfo); - scanner.walkFunction(func); - } - }); + } + }; + CallScanner scanner(module, passOptions, funcInfo); + scanner.walkFunction(func); + } + }); - // Compute the transitive closure of effects. To do so, first construct for - // each function a list of the functions that it is called by (so we need to - // propagate its effects to them), and then we'll construct the closure of - // that. - // - // callers[foo] = [func that calls foo, another func that calls foo, ..] - // - std::unordered_map> callers; + return analysis.map; +} + +struct GenerateGlobalEffects : public Pass { + void run(Module* module) override { + // First, we do a scan of each function to see what effects they have, + // including which functions they call directly (so that we can compute + // transitive effects later). + auto funcInfos = analyzeFuncs(*module, getPassOptions()); // Our work queue contains info about a new call pair: a call from a caller // to a called function, that is information we then apply and propagate. using CallPair = std::pair; // { caller, called } UniqueDeferredQueue work; - for (auto& [func, info] : analysis.map) { + for (auto& [func, info] : funcInfos) { for (auto& called : info.calledFunctions) { work.push({func->name, called}); } } + // Compute the transitive closure of effects. To do so, first construct for + // each function a list of the functions that it is called by (so we need to + // propagate its effects to them), and then we'll construct the closure of + // that. + // + // callers[foo] = [func that calls foo, another func that calls foo, ..] + // + std::unordered_map> callers; + // Compute the transitive closure of the call graph, that is, fill out // |callers| so that it contains the list of all callers - even through a // chain - of each function. @@ -138,7 +156,7 @@ struct GenerateGlobalEffects : public Pass { // // caller => called => called by called // - auto& calledInfo = analysis.map[module->getFunction(called)]; + auto& calledInfo = funcInfos[module->getFunction(called)]; for (auto calledByCalled : calledInfo.calledFunctions) { if (!callers[calledByCalled].contains(caller)) { work.push({caller, calledByCalled}); @@ -150,7 +168,7 @@ struct GenerateGlobalEffects : public Pass { // information. First, apply infinite recursion: if a function can call // itself then it might recurse infinitely, which we consider an effect (a // trap). - for (auto& [func, info] : analysis.map) { + for (auto& [func, info] : funcInfos) { if (callers[func->name].contains(func->name)) { if (info.effects) { info.effects->trap = true; @@ -159,11 +177,11 @@ struct GenerateGlobalEffects : public Pass { } // Next, apply function effects to their callers. - for (auto& [func, info] : analysis.map) { + for (auto& [func, info] : funcInfos) { auto& funcEffects = info.effects; for (auto& caller : callers[func->name]) { - auto& callerEffects = analysis.map[module->getFunction(caller)].effects; + auto& callerEffects = funcInfos[module->getFunction(caller)].effects; if (!callerEffects) { // Nothing is known for the caller, which is already the worst case. continue; @@ -183,7 +201,7 @@ struct GenerateGlobalEffects : public Pass { // Generate the final data, starting from a blank slate where nothing is // known. - for (auto& [func, info] : analysis.map) { + for (auto& [func, info] : funcInfos) { func->effects.reset(); if (!info.effects) { continue; @@ -202,6 +220,8 @@ struct DiscardGlobalEffects : public Pass { } }; +} // namespace + Pass* createGenerateGlobalEffectsPass() { return new GenerateGlobalEffects(); } Pass* createDiscardGlobalEffectsPass() { return new DiscardGlobalEffects(); } diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 6ace6cb6bfc..e6d2cc81740 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -48,6 +48,9 @@ ) ;; CHECK: (func $f (type $3) (param $ref (ref $nopType)) (result i32) + ;; CHECK-NEXT: (call $calls-nop-via-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) (func $f (param $ref (ref $nopType)) (result i32) From 7fd7418c39bb12c80df7cc66c0d140847f96ce94 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 7 Apr 2026 18:46:29 +0000 Subject: [PATCH 03/11] Done but not working --- src/passes/GlobalEffects.cpp | 170 ++++++++++++++---- .../passes/global-effects-closed-world.wast | 6 - test/lit/passes/global-effects.wast | 12 -- 3 files changed, 133 insertions(+), 55 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index e4bec6cb98f..9adfe933155 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" @@ -92,7 +94,7 @@ std::map analyzeFuncs(Module& module, } funcInfo.indirectCalledTypes.insert(type); - funcInfo.effects.reset(); + // funcInfo.effects.reset(); } 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 - @@ -111,6 +113,87 @@ std::map analyzeFuncs(Module& module, return analysis.map; } +std::unordered_map> +typeToFunctionNames(const Module& module) { + std::unordered_map> ret; + + for (const auto& func : module.functions) { + ret[func->type.getHeapType()].insert(func->name); + } + + return ret; +} + +std::unordered_map> +transitiveClosure(const Module& module, + const std::map& funcInfos) { + // Compute the transitive closure of effects. To do so, first construct for + // each function a list of the functions that it is called by (so we need to + // propogate its effects to them), and then we'll construct the closure of + // that. + // + // callers[foo] = [func that calls foo, another func that calls foo, ..] + // + std::unordered_map> callers; + + // Our work queue contains info about a new call pair: a call from a caller + // to a called function, that is information we then apply and propagate. + using CallPair = std::pair; // { caller, called } + UniqueDeferredQueue work; + for (auto& [func, info] : funcInfos) { + for (auto& called : info.calledFunctions) { + work.push({func->name, called}); + } + } + + // Compute the transitive closure of the call graph, that is, fill out + // |callers| so that it contains the list of all callers - even through a + // chain - of each function. + while (!work.empty()) { + auto [caller, called] = work.pop(); + + // We must not already have an entry for this call (that would imply we + // are doing wasted work). + assert(!callers[called].contains(caller)); + + // Apply the new call information. + callers[called].insert(caller); + + // We just learned that |caller| calls |called|. It also calls + // transitively, which we need to propagate to all places unaware of that + // information yet. + // + // caller => called => called by called + // + auto& calledInfo = funcInfos.at(module.getFunction(called)); + for (auto calledByCalled : calledInfo.calledFunctions) { + if (!callers[calledByCalled].contains(caller)) { + work.push({caller, calledByCalled}); + } + } + } + + return callers; +} + +std::unordered_map> transitiveClosure( + const Module& module, + const std::unordered_map>& funcInfos) { + std::map other; + auto _ = + funcInfos | std::views::transform( + [&](const auto& pair) -> std::pair { + auto& [k, v] = pair; + + auto& func = module.getFunction(k); + FuncInfo info; + info.calledFunctions = v; + return {func->name, info}; + }); + + return transitiveClosure(module, other); +} + struct GenerateGlobalEffects : public Pass { void run(Module* module) override { // First, we do a scan of each function to see what effects they have, @@ -118,16 +201,6 @@ struct GenerateGlobalEffects : public Pass { // transitive effects later). auto funcInfos = analyzeFuncs(*module, getPassOptions()); - // Our work queue contains info about a new call pair: a call from a caller - // to a called function, that is information we then apply and propagate. - using CallPair = std::pair; // { caller, called } - UniqueDeferredQueue work; - for (auto& [func, info] : funcInfos) { - for (auto& called : info.calledFunctions) { - work.push({func->name, called}); - } - } - // Compute the transitive closure of effects. To do so, first construct for // each function a list of the functions that it is called by (so we need to // propagate its effects to them), and then we'll construct the closure of @@ -135,35 +208,36 @@ struct GenerateGlobalEffects : public Pass { // // callers[foo] = [func that calls foo, another func that calls foo, ..] // - std::unordered_map> callers; - - // Compute the transitive closure of the call graph, that is, fill out - // |callers| so that it contains the list of all callers - even through a - // chain - of each function. - while (!work.empty()) { - auto [caller, called] = work.pop(); - - // We must not already have an entry for this call (that would imply we - // are doing wasted work). - assert(!callers[called].contains(caller)); - - // Apply the new call information. - callers[called].insert(caller); - - // We just learned that |caller| calls |called|. It also calls - // transitively, which we need to propagate to all places unaware of that - // information yet. - // - // caller => called => called by called - // - auto& calledInfo = funcInfos[module->getFunction(called)]; - for (auto calledByCalled : calledInfo.calledFunctions) { - if (!callers[calledByCalled].contains(caller)) { - work.push({caller, calledByCalled}); + std::unordered_map> callers = + transitiveClosure(*module, funcInfos); + + const auto functionsWithType = typeToFunctionNames(*module); + std::unordered_map> + indirectCallersNonTransitive; + for (auto& [func, info] : funcInfos) { + for (auto& calledType : info.indirectCalledTypes) { + // auto asdf = functionsWithType.at(calledType); + // auto foo = indirectCallersNonTransitive[func->name]; + // asdf.merge(foo); + // foo.merge(asdf); + + if (auto it = functionsWithType.find(calledType); + it != functionsWithType.end()) { + indirectCallersNonTransitive[func->name].insert(it->second.begin(), + it->second.end()); } + // indirectCallersNonTransitive[func->name].merge(functionsWithType.at(calledType)); } + // for (const auto& name : functionsWitType[]) + // for () + // info.indirectCalledTypes[func->name] } + // indirectCallers[foo] = [func that indirect calls something with the same + // type as foo, ..] + const std::unordered_map> indirectCallers = + transitiveClosure(*module, indirectCallersNonTransitive); + // Now that we have transitively propagated all static calls, apply that // information. First, apply infinite recursion: if a function can call // itself then it might recurse infinitely, which we consider an effect (a @@ -180,7 +254,29 @@ struct GenerateGlobalEffects : public Pass { for (auto& [func, info] : funcInfos) { auto& funcEffects = info.effects; - for (auto& caller : callers[func->name]) { + for (const auto& caller : callers[func->name]) { + auto& callerEffects = funcInfos[module->getFunction(caller)].effects; + if (!callerEffects) { + // Nothing is known for the caller, which is already the worst case. + continue; + } + + if (!funcEffects) { + // Nothing is known for the called function, which means nothing is + // known for the caller either. + callerEffects.reset(); + continue; + } + + // Add func's effects to the caller. + callerEffects->mergeIn(*funcEffects); + } + + auto indirectCallersOfThisFunction = indirectCallers.find(func->name); + if (indirectCallersOfThisFunction == indirectCallers.end()) { + continue; + } + for (Name caller : indirectCallersOfThisFunction->second) { auto& callerEffects = funcInfos[module->getFunction(caller)].effects; if (!callerEffects) { // Nothing is known for the caller, which is already the worst case. diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index e6d2cc81740..24fb6dcdb65 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -48,9 +48,6 @@ ) ;; CHECK: (func $f (type $3) (param $ref (ref $nopType)) (result i32) - ;; CHECK-NEXT: (call $calls-nop-via-ref - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) (func $f (param $ref (ref $nopType)) (result i32) @@ -72,9 +69,6 @@ ) ;; CHECK: (func $g (type $5) (param $ref (ref $maybe-has-effects)) (result i32) - ;; CHECK-NEXT: (call $calls-effectful-function-via-ref - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) (func $g (param $ref (ref $maybe-has-effects)) (result i32) diff --git a/test/lit/passes/global-effects.wast b/test/lit/passes/global-effects.wast index 1125f738e68..5361628979d 100644 --- a/test/lit/passes/global-effects.wast +++ b/test/lit/passes/global-effects.wast @@ -315,19 +315,7 @@ ;; INCLUDE-NEXT: (call $return-call-throw-and-catch) ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (block $tryend0 - ;; INCLUDE-NEXT: (try_table (catch_all $tryend0) - ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) - ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (block $tryend1 - ;; INCLUDE-NEXT: (try_table (catch_all $tryend1) - ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) - ;; 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) ;; INCLUDE-NEXT: ) (func $call-return-call-throw-and-catch (block $tryend From 2fbd478ed900fbb8050c0053c6854867eb57d03a Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 7 Apr 2026 18:51:52 +0000 Subject: [PATCH 04/11] Working in closed world test --- src/passes/GlobalEffects.cpp | 35 +++++++++++++------ .../passes/global-effects-closed-world.wast | 3 ++ test/lit/passes/global-effects.wast | 12 +++++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 9adfe933155..f2a02ce32af 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -165,7 +165,16 @@ transitiveClosure(const Module& module, // // caller => called => called by called // - auto& calledInfo = funcInfos.at(module.getFunction(called)); + auto it = funcInfos.find(module.getFunction(called)); + + // TODO: this should never be missing? + if (it == funcInfos.end()) { + std::cout << "missing key " << called << "\n"; + throw(1); + // assert(false && ("missing key " + called)); + } + auto& calledInfo = it->second; + // auto& calledInfo = funcInfos.at(module.getFunction(called)); for (auto calledByCalled : calledInfo.calledFunctions) { if (!callers[calledByCalled].contains(caller)) { work.push({caller, calledByCalled}); @@ -179,18 +188,17 @@ transitiveClosure(const Module& module, std::unordered_map> transitiveClosure( const Module& module, const std::unordered_map>& funcInfos) { - std::map other; - auto _ = + auto transformed = funcInfos | std::views::transform( [&](const auto& pair) -> std::pair { auto& [k, v] = pair; - auto& func = module.getFunction(k); + auto* func = module.getFunction(k); FuncInfo info; info.calledFunctions = v; - return {func->name, info}; + return {func, info}; }); - + std::map other(transformed.begin(), transformed.end()); return transitiveClosure(module, other); } @@ -215,6 +223,7 @@ struct GenerateGlobalEffects : public Pass { std::unordered_map> indirectCallersNonTransitive; for (auto& [func, info] : funcInfos) { + indirectCallersNonTransitive[func->name]; for (auto& calledType : info.indirectCalledTypes) { // auto asdf = functionsWithType.at(calledType); // auto foo = indirectCallersNonTransitive[func->name]; @@ -226,11 +235,7 @@ struct GenerateGlobalEffects : public Pass { indirectCallersNonTransitive[func->name].insert(it->second.begin(), it->second.end()); } - // indirectCallersNonTransitive[func->name].merge(functionsWithType.at(calledType)); } - // for (const auto& name : functionsWitType[]) - // for () - // info.indirectCalledTypes[func->name] } // indirectCallers[foo] = [func that indirect calls something with the same @@ -238,6 +243,15 @@ struct GenerateGlobalEffects : public Pass { const std::unordered_map> indirectCallers = transitiveClosure(*module, indirectCallersNonTransitive); + std::cout << "indirectCallers\n"; + for (auto [callee, callers] : indirectCallers) { + std::cout << callee << "\n"; + for (auto caller : callers) { + std::cout << "\t" << caller << "\n"; + } + std::cout << "\n"; + } + // Now that we have transitively propagated all static calls, apply that // information. First, apply infinite recursion: if a function can call // itself then it might recurse infinitely, which we consider an effect (a @@ -303,6 +317,7 @@ struct GenerateGlobalEffects : public Pass { continue; } + std::cout << func->name << " has effects " << *info.effects << "\n"; func->effects = std::make_shared(*info.effects); } } diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 24fb6dcdb65..6ace6cb6bfc 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -69,6 +69,9 @@ ) ;; CHECK: (func $g (type $5) (param $ref (ref $maybe-has-effects)) (result i32) + ;; CHECK-NEXT: (call $calls-effectful-function-via-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) (func $g (param $ref (ref $maybe-has-effects)) (result i32) diff --git a/test/lit/passes/global-effects.wast b/test/lit/passes/global-effects.wast index 5361628979d..1125f738e68 100644 --- a/test/lit/passes/global-effects.wast +++ b/test/lit/passes/global-effects.wast @@ -315,7 +315,19 @@ ;; INCLUDE-NEXT: (call $return-call-throw-and-catch) ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (block $tryend0 + ;; INCLUDE-NEXT: (try_table (catch_all $tryend0) + ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (block $tryend1 + ;; INCLUDE-NEXT: (try_table (catch_all $tryend1) + ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) + ;; 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) ;; INCLUDE-NEXT: ) (func $call-return-call-throw-and-catch (block $tryend From 225564fac47253b7dd6d1e2b532c2e3b461633f2 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 7 Apr 2026 20:45:53 +0000 Subject: [PATCH 05/11] Work on subtyping --- .../passes/global-effects-closed-world.wast | 86 +++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 6ace6cb6bfc..47d37163294 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -3,11 +3,26 @@ (module ;; CHECK: (type $maybe-has-effects (func (param i32 i32))) + (type $maybe-has-effects (func (param i32 i32))) ;; CHECK: (type $nopType (func (param i32))) (type $nopType (func (param i32))) - (type $maybe-has-effects (func (param i32 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 (func (param (ref $sub)))) + + ;; CHECK: (type $uninhabited (func (param f32))) + (type $uninhabited (func (param f32))) + + ;; Subtype + ;; CHECK: (type $func-with-super-param (func (param (ref $super)))) + (type $func-with-super-param (func (param (ref $super)))) + ;; Supertype + (type $func-with-sub-param (func (param (ref $sub)))) ;; CHECK: (global $g (mut i32) (i32.const 0)) (global $g (mut i32) (i32.const 0)) @@ -33,7 +48,7 @@ (nop) ) - ;; CHECK: (func $calls-nop-via-ref (type $2) (param $ref (ref $nopType)) + ;; CHECK: (func $calls-nop-via-ref (type $6) (param $ref (ref $nopType)) ;; CHECK-NEXT: (call_ref $nopType ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (local.get $ref) @@ -47,17 +62,16 @@ (call_ref $nopType (i32.const 1) (local.get $ref)) ) - ;; CHECK: (func $f (type $3) (param $ref (ref $nopType)) (result i32) - ;; CHECK-NEXT: (i32.const 1) + ;; CHECK: (func $f (type $6) (param $ref (ref $nopType)) + ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $f (param $ref (ref $nopType)) (result i32) + (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)) - (i32.const 1) ) - ;; CHECK: (func $calls-effectful-function-via-ref (type $4) (param $ref (ref $maybe-has-effects)) + ;; 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) @@ -68,7 +82,7 @@ (call_ref $maybe-has-effects (i32.const 1) (i32.const 2) (local.get $ref)) ) - ;; CHECK: (func $g (type $5) (param $ref (ref $maybe-has-effects)) (result i32) + ;; CHECK: (func $g (type $10) (param $ref (ref $maybe-has-effects)) (result i32) ;; CHECK-NEXT: (call $calls-effectful-function-via-ref ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) @@ -80,4 +94,60 @@ (call $calls-effectful-function-via-ref (local.get $ref)) (i32.const 1) ) + + ;; CHECK: (func $calls-uninhabited (type $7) (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 $7) (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 (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 (type $func-with-super-param) (param (ref $super)) + (unreachable) + ) + + ;; CHECK: (func $calls-ref-with-subtype (type $8) (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 $8) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK-NEXT: (nop) + ;; 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)) + ) ) From 17f779f278aa92b923783e9d612b73ab21af4e8d Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 7 Apr 2026 21:19:36 +0000 Subject: [PATCH 06/11] Try adding subtyping, not working --- src/passes/GlobalEffects.cpp | 22 ++++++++----------- .../passes/global-effects-closed-world.wast | 11 +++++----- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index f2a02ce32af..cd94631beec 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -23,6 +23,7 @@ #include "ir/effects.h" #include "ir/module-utils.h" +#include "ir/subtypes.h" #include "pass.h" #include "support/unique_deferring_queue.h" #include "wasm.h" @@ -117,8 +118,15 @@ std::unordered_map> typeToFunctionNames(const Module& module) { std::unordered_map> ret; + // TODO + SubTypes s(*const_cast(&module)); + for (const auto& func : module.functions) { - ret[func->type.getHeapType()].insert(func->name); + s.iterSubTypes(func->type.getHeapType(), [&ret, &func](HeapType type, int _) { + std::cout<<"subtype "<< func->type.getHeapType() << " " << type<< "\n"; + ret[type].insert(func->name); + }); + // ret[func->type.getHeapType()].insert(func->name); } return ret; @@ -166,13 +174,6 @@ transitiveClosure(const Module& module, // caller => called => called by called // auto it = funcInfos.find(module.getFunction(called)); - - // TODO: this should never be missing? - if (it == funcInfos.end()) { - std::cout << "missing key " << called << "\n"; - throw(1); - // assert(false && ("missing key " + called)); - } auto& calledInfo = it->second; // auto& calledInfo = funcInfos.at(module.getFunction(called)); for (auto calledByCalled : calledInfo.calledFunctions) { @@ -225,11 +226,6 @@ struct GenerateGlobalEffects : public Pass { for (auto& [func, info] : funcInfos) { indirectCallersNonTransitive[func->name]; for (auto& calledType : info.indirectCalledTypes) { - // auto asdf = functionsWithType.at(calledType); - // auto foo = indirectCallersNonTransitive[func->name]; - // asdf.merge(foo); - // foo.merge(asdf); - if (auto it = functionsWithType.find(calledType); it != functionsWithType.end()) { indirectCallersNonTransitive[func->name].insert(it->second.begin(), diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 47d37163294..145385cb00a 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -13,16 +13,17 @@ ;; CHECK: (type $sub (sub $super (struct))) (type $sub (sub $super (struct))) - ;; CHECK: (type $func-with-sub-param (func (param (ref $sub)))) + ;; CHECK: (type $func-with-sub-param (sub (func (param (ref $sub))))) ;; CHECK: (type $uninhabited (func (param f32))) (type $uninhabited (func (param f32))) ;; Subtype - ;; CHECK: (type $func-with-super-param (func (param (ref $super)))) - (type $func-with-super-param (func (param (ref $super)))) - ;; Supertype - (type $func-with-sub-param (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: (global $g (mut i32) (i32.const 0)) (global $g (mut i32) (i32.const 0)) From e0d287f71aaf4632994e99d7ef8863245450fe31 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 8 Apr 2026 20:42:24 +0000 Subject: [PATCH 07/11] Done?? --- src/passes/GlobalEffects.cpp | 168 +++++++++++++----- .../passes/global-effects-closed-world.wast | 5 +- 2 files changed, 126 insertions(+), 47 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index cd94631beec..94b0f3b9790 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -114,22 +114,48 @@ std::map analyzeFuncs(Module& module, return analysis.map; } -std::unordered_map> -typeToFunctionNames(const Module& module) { - std::unordered_map> ret; +// std::unordered_map> +// typeToFunctionNames(const Module& module) { +// std::unordered_map> ret; + +// // TODO +// SubTypes s(*const_cast(&module)); + +// for (const auto& func : module.functions) { +// s.iterSubTypes(func->type.getHeapType(), [&ret, &func](HeapType type, int _) { +// std::cout<<"subtype "<< func->type.getHeapType() << " " << type<< "\n"; +// ret[type].insert(func->name); +// }); +// // ret[func->type.getHeapType()].insert(func->name); +// } + +// return ret; +// } + +template +std::unordered_map> transitiveClosure2(const std::unordered_map>& in) { + UniqueNonrepeatingDeferredQueue> work; + + for (const auto& [curr, neighbors] : in) { + for (const auto& neighbor : neighbors) { + work.push({curr, neighbor}); + } + } + + std::unordered_map> closure; + while (!work.empty()) { + auto [curr, neighbor] = work.pop(); - // TODO - SubTypes s(*const_cast(&module)); + closure[curr].insert(neighbor); - for (const auto& func : module.functions) { - s.iterSubTypes(func->type.getHeapType(), [&ret, &func](HeapType type, int _) { - std::cout<<"subtype "<< func->type.getHeapType() << " " << type<< "\n"; - ret[type].insert(func->name); - }); - // ret[func->type.getHeapType()].insert(func->name); + auto neighborNeighbors = in.find(neighbor); + if (neighborNeighbors == in.end()) continue; + for (const auto& neighborNeighbor : neighborNeighbors->second) { + work.push({curr, neighborNeighbor}); + } } - return ret; + return closure; } std::unordered_map> @@ -186,21 +212,32 @@ transitiveClosure(const Module& module, return callers; } -std::unordered_map> transitiveClosure( - const Module& module, - const std::unordered_map>& funcInfos) { - auto transformed = - funcInfos | std::views::transform( - [&](const auto& pair) -> std::pair { - auto& [k, v] = pair; - - auto* func = module.getFunction(k); - FuncInfo info; - info.calledFunctions = v; - return {func, info}; - }); - std::map other(transformed.begin(), transformed.end()); - return transitiveClosure(module, other); +// std::unordered_map> transitiveClosure( +// const Module& module, +// const std::unordered_map>& funcInfos) { +// auto transformed = +// funcInfos | std::views::transform( +// [&](const auto& pair) -> std::pair { +// auto& [k, v] = pair; + +// auto* func = module.getFunction(k); +// FuncInfo info; +// info.calledFunctions = v; +// return {func, info}; +// }); +// std::map other(transformed.begin(), transformed.end()); +// return transitiveClosure(module, other); +// } + +template +std::unordered_map> flip(const std::unordered_map>& in) { + std::unordered_map> flipped; + for (const auto& [k, vs] : in) { + for (const auto& v : vs) { + flipped[v].insert(k); + } + } + return flipped; } struct GenerateGlobalEffects : public Pass { @@ -220,34 +257,73 @@ struct GenerateGlobalEffects : public Pass { std::unordered_map> callers = transitiveClosure(*module, funcInfos); - const auto functionsWithType = typeToFunctionNames(*module); - std::unordered_map> + // const auto functionsWithType = typeToFunctionNames(*module); + std::unordered_map> indirectCallersNonTransitive; + std::unordered_map> indirectCalls; for (auto& [func, info] : funcInfos) { - indirectCallersNonTransitive[func->name]; + auto& set = indirectCallersNonTransitive[func->name]; + auto& indirectCallsSet = indirectCalls[func->type.getHeapType()]; for (auto& calledType : info.indirectCalledTypes) { - if (auto it = functionsWithType.find(calledType); - it != functionsWithType.end()) { - indirectCallersNonTransitive[func->name].insert(it->second.begin(), - it->second.end()); - } + // if (auto it = functionsWithType.find(calledType); + // it != functionsWithType.end()) { + set.insert(calledType); + indirectCallsSet.insert(calledType); } } // indirectCallers[foo] = [func that indirect calls something with the same // type as foo, ..] - const std::unordered_map> indirectCallers = - transitiveClosure(*module, indirectCallersNonTransitive); - - std::cout << "indirectCallers\n"; - for (auto [callee, callers] : indirectCallers) { - std::cout << callee << "\n"; - for (auto caller : callers) { - std::cout << "\t" << caller << "\n"; + // const std::unordered_map> indirectCallers = + // transitiveClosure(*module, indirectCallersNonTransitive); + + // TODO: need to take subtypes into account here + // we can pretend that each type 'indirect calls' its subtypes + // This is good enough because when querying the particular function Name that + // indirect calls someone we want to take its indirect calls into account anyway + // So just pretend that it indirect calls its subtypes. + + SubTypes subtypes(*module); + std::unordered_map> subTypesToAdd; + + for (const auto& [type, _] : indirectCalls) { + subtypes.iterSubTypes(type, [&subTypesToAdd, type](HeapType sub, int _) { subTypesToAdd[type].insert(sub); return true; }); + } + + for (const auto& [k, v] : subTypesToAdd) { + auto it = indirectCalls.find(k); + + // This is possible but if it happens it means there was no function with this type anyway + // So no effects to include and no harm in forgetting it. + if (it == indirectCalls.end()) continue; + + it->second.insert(v.begin(), v.end()); + } + + auto a = transitiveClosure2(indirectCalls); + + for (const auto& [k, v]: indirectCallersNonTransitive) { + for (const auto& x : v) { + auto y = a[x]; + + // we're leaving what was already there but should be fine + // since it's covered under transitive calls anyway + indirectCallersNonTransitive[k].insert(y.begin(), y.end()); } - std::cout << "\n"; } + std::unordered_map> flipped = flip(indirectCallersNonTransitive); + // indirectCalls; + + // std::cout << "indirectCallers\n"; + // for (auto [callee, callers] : indirectCallers) { + // std::cout << callee << "\n"; + // for (auto caller : callers) { + // std::cout << "\t" << caller << "\n"; + // } + // std::cout << "\n"; + // } + // Now that we have transitively propagated all static calls, apply that // information. First, apply infinite recursion: if a function can call // itself then it might recurse infinitely, which we consider an effect (a @@ -282,8 +358,8 @@ struct GenerateGlobalEffects : public Pass { callerEffects->mergeIn(*funcEffects); } - auto indirectCallersOfThisFunction = indirectCallers.find(func->name); - if (indirectCallersOfThisFunction == indirectCallers.end()) { + auto indirectCallersOfThisFunction = flipped.find(func->type.getHeapType()); + if (indirectCallersOfThisFunction == flipped.end()) { continue; } for (Name caller : indirectCallersOfThisFunction->second) { diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 145385cb00a..06a45418c7c 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -140,7 +140,10 @@ ) ;; CHECK: (func $asdf (type $8) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) - ;; CHECK-NEXT: (nop) + ;; 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. From 3311a776b00f81fcb3968891fe219c8e9b3aeb30 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 8 Apr 2026 21:07:33 +0000 Subject: [PATCH 08/11] Start cleaning up --- src/passes/GlobalEffects.cpp | 66 ++++++++++++------------------------ 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 94b0f3b9790..04609e8e4e3 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -95,7 +95,6 @@ std::map analyzeFuncs(Module& module, } funcInfo.indirectCalledTypes.insert(type); - // funcInfo.effects.reset(); } 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 - @@ -114,24 +113,6 @@ std::map analyzeFuncs(Module& module, return analysis.map; } -// std::unordered_map> -// typeToFunctionNames(const Module& module) { -// std::unordered_map> ret; - -// // TODO -// SubTypes s(*const_cast(&module)); - -// for (const auto& func : module.functions) { -// s.iterSubTypes(func->type.getHeapType(), [&ret, &func](HeapType type, int _) { -// std::cout<<"subtype "<< func->type.getHeapType() << " " << type<< "\n"; -// ret[type].insert(func->name); -// }); -// // ret[func->type.getHeapType()].insert(func->name); -// } - -// return ret; -// } - template std::unordered_map> transitiveClosure2(const std::unordered_map>& in) { UniqueNonrepeatingDeferredQueue> work; @@ -240,6 +221,8 @@ std::unordered_map> flip(const std::unordered_map> + struct GenerateGlobalEffects : public Pass { void run(Module* module) override { // First, we do a scan of each function to see what effects they have, @@ -247,17 +230,10 @@ struct GenerateGlobalEffects : public Pass { // transitive effects later). auto funcInfos = analyzeFuncs(*module, getPassOptions()); - // Compute the transitive closure of effects. To do so, first construct for - // each function a list of the functions that it is called by (so we need to - // propagate its effects to them), and then we'll construct the closure of - // that. - // - // callers[foo] = [func that calls foo, another func that calls foo, ..] - // - std::unordered_map> callers = - transitiveClosure(*module, funcInfos); - - // const auto functionsWithType = typeToFunctionNames(*module); + // Find the 'entry points' of indirect calls Name -> HeapType. + // At the same time, start recording connections for the transitive closure of indirect calls + // HeapType -> HeapType. This will be used to find the set of HeapTypes that are reachable via any sequence of indirect calls from a given function + // (Name -> HeapType) std::unordered_map> indirectCallersNonTransitive; std::unordered_map> indirectCalls; @@ -265,8 +241,6 @@ struct GenerateGlobalEffects : public Pass { auto& set = indirectCallersNonTransitive[func->name]; auto& indirectCallsSet = indirectCalls[func->type.getHeapType()]; for (auto& calledType : info.indirectCalledTypes) { - // if (auto it = functionsWithType.find(calledType); - // it != functionsWithType.end()) { set.insert(calledType); indirectCallsSet.insert(calledType); } @@ -293,8 +267,8 @@ struct GenerateGlobalEffects : public Pass { for (const auto& [k, v] : subTypesToAdd) { auto it = indirectCalls.find(k); - // This is possible but if it happens it means there was no function with this type anyway - // So no effects to include and no harm in forgetting it. + // No need to add this. It wasn't in the map because no function has this + // type, so there are no effects to aggregate and we can forget about it. if (it == indirectCalls.end()) continue; it->second.insert(v.begin(), v.end()); @@ -302,6 +276,9 @@ struct GenerateGlobalEffects : public Pass { auto a = transitiveClosure2(indirectCalls); + // Pretend that each subtype is indirect called by its supertype. + // This might not be the case but it's accurate enough since any caller that + // may indirect call a given type may also indirect call its subtype. for (const auto& [k, v]: indirectCallersNonTransitive) { for (const auto& x : v) { auto y = a[x]; @@ -313,16 +290,17 @@ struct GenerateGlobalEffects : public Pass { } std::unordered_map> flipped = flip(indirectCallersNonTransitive); - // indirectCalls; - - // std::cout << "indirectCallers\n"; - // for (auto [callee, callers] : indirectCallers) { - // std::cout << callee << "\n"; - // for (auto caller : callers) { - // std::cout << "\t" << caller << "\n"; - // } - // std::cout << "\n"; - // } + + // Compute the transitive closure of effects. To do so, first construct for + // each function a list of the functions that it is called by (so we need to + // propagate its effects to them), and then we'll construct the closure of + // that. + // + // callers[foo] = [func that calls foo, another func that calls foo, ..] + // + std::unordered_map> callers = + transitiveClosure(*module, funcInfos); + // Now that we have transitively propagated all static calls, apply that // information. First, apply infinite recursion: if a function can call From 89fa1188cd1bc570cadb6bfd555c50c2acdf84d3 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 8 Apr 2026 21:12:05 +0000 Subject: [PATCH 09/11] more cleanup --- src/passes/GlobalEffects.cpp | 148 ++++++++++++++++------------------- 1 file changed, 66 insertions(+), 82 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 04609e8e4e3..14f8c518900 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -113,8 +113,9 @@ std::map analyzeFuncs(Module& module, return analysis.map; } -template -std::unordered_map> transitiveClosure2(const std::unordered_map>& in) { +template +std::unordered_map> +transitiveClosure2(const std::unordered_map>& in) { UniqueNonrepeatingDeferredQueue> work; for (const auto& [curr, neighbors] : in) { @@ -130,7 +131,8 @@ std::unordered_map> transitiveClosure2(const std::unord closure[curr].insert(neighbor); auto neighborNeighbors = in.find(neighbor); - if (neighborNeighbors == in.end()) continue; + if (neighborNeighbors == in.end()) + continue; for (const auto& neighborNeighbor : neighborNeighbors->second) { work.push({curr, neighborNeighbor}); } @@ -193,25 +195,9 @@ transitiveClosure(const Module& module, return callers; } -// std::unordered_map> transitiveClosure( -// const Module& module, -// const std::unordered_map>& funcInfos) { -// auto transformed = -// funcInfos | std::views::transform( -// [&](const auto& pair) -> std::pair { -// auto& [k, v] = pair; - -// auto* func = module.getFunction(k); -// FuncInfo info; -// info.calledFunctions = v; -// return {func, info}; -// }); -// std::map other(transformed.begin(), transformed.end()); -// return transitiveClosure(module, other); -// } - -template -std::unordered_map> flip(const std::unordered_map>& in) { +template +std::unordered_map> +flip(const std::unordered_map>& in) { std::unordered_map> flipped; for (const auto& [k, vs] : in) { for (const auto& v : vs) { @@ -221,75 +207,73 @@ std::unordered_map> flip(const std::unordered_map> - -struct GenerateGlobalEffects : public Pass { - void run(Module* module) override { - // First, we do a scan of each function to see what effects they have, - // including which functions they call directly (so that we can compute - // transitive effects later). - auto funcInfos = analyzeFuncs(*module, getPassOptions()); - - // Find the 'entry points' of indirect calls Name -> HeapType. - // At the same time, start recording connections for the transitive closure of indirect calls - // HeapType -> HeapType. This will be used to find the set of HeapTypes that are reachable via any sequence of indirect calls from a given function - // (Name -> HeapType) - std::unordered_map> - indirectCallersNonTransitive; - std::unordered_map> indirectCalls; - for (auto& [func, info] : funcInfos) { - auto& set = indirectCallersNonTransitive[func->name]; - auto& indirectCallsSet = indirectCalls[func->type.getHeapType()]; - for (auto& calledType : info.indirectCalledTypes) { - set.insert(calledType); - indirectCallsSet.insert(calledType); - } +std::unordered_map> +callersOfHeapType(std::map funcInfos, + const SubTypes& subtypes) { + // Find the 'entry points' of indirect calls Name -> HeapType. + // At the same time, start recording connections for the transitive closure of + // indirect calls HeapType -> HeapType. This will be used to find the set of + // HeapTypes that are reachable via any sequence of indirect calls from a + // given function (Name -> HeapType) + std::unordered_map> + indirectCallersNonTransitive; + std::unordered_map> indirectCalls; + for (auto& [func, info] : funcInfos) { + auto& set = indirectCallersNonTransitive[func->name]; + auto& indirectCallsSet = indirectCalls[func->type.getHeapType()]; + for (auto& calledType : info.indirectCalledTypes) { + set.insert(calledType); + indirectCallsSet.insert(calledType); } + } - // indirectCallers[foo] = [func that indirect calls something with the same - // type as foo, ..] - // const std::unordered_map> indirectCallers = - // transitiveClosure(*module, indirectCallersNonTransitive); + std::unordered_map> subTypesToAdd; - // TODO: need to take subtypes into account here - // we can pretend that each type 'indirect calls' its subtypes - // This is good enough because when querying the particular function Name that - // indirect calls someone we want to take its indirect calls into account anyway - // So just pretend that it indirect calls its subtypes. + for (const auto& [type, _] : indirectCalls) { + subtypes.iterSubTypes(type, [&subTypesToAdd, type](HeapType sub, int _) { + subTypesToAdd[type].insert(sub); + return true; + }); + } - SubTypes subtypes(*module); - std::unordered_map> subTypesToAdd; + for (const auto& [k, v] : subTypesToAdd) { + auto it = indirectCalls.find(k); - for (const auto& [type, _] : indirectCalls) { - subtypes.iterSubTypes(type, [&subTypesToAdd, type](HeapType sub, int _) { subTypesToAdd[type].insert(sub); return true; }); - } + // No need to add this. It wasn't in the map because no function has this + // type, so there are no effects to aggregate and we can forget about it. + if (it == indirectCalls.end()) + continue; - for (const auto& [k, v] : subTypesToAdd) { - auto it = indirectCalls.find(k); + it->second.insert(v.begin(), v.end()); + } - // No need to add this. It wasn't in the map because no function has this - // type, so there are no effects to aggregate and we can forget about it. - if (it == indirectCalls.end()) continue; + auto a = transitiveClosure2(indirectCalls); - it->second.insert(v.begin(), v.end()); - } + // Pretend that each subtype is indirect called by its supertype. + // This might not be the case but it's accurate enough since any caller that + // may indirect call a given type may also indirect call its subtype. + for (const auto& [k, v] : indirectCallersNonTransitive) { + for (const auto& x : v) { + auto y = a[x]; - auto a = transitiveClosure2(indirectCalls); + // we're leaving what was already there but should be fine + // since it's covered under transitive calls anyway + indirectCallersNonTransitive[k].insert(y.begin(), y.end()); + } + } - // Pretend that each subtype is indirect called by its supertype. - // This might not be the case but it's accurate enough since any caller that - // may indirect call a given type may also indirect call its subtype. - for (const auto& [k, v]: indirectCallersNonTransitive) { - for (const auto& x : v) { - auto y = a[x]; + return flip(indirectCallersNonTransitive); +} - // we're leaving what was already there but should be fine - // since it's covered under transitive calls anyway - indirectCallersNonTransitive[k].insert(y.begin(), y.end()); - } - } +struct GenerateGlobalEffects : public Pass { + void run(Module* module) override { + // First, we do a scan of each function to see what effects they have, + // including which functions they call directly (so that we can compute + // transitive effects later). + auto funcInfos = analyzeFuncs(*module, getPassOptions()); - std::unordered_map> flipped = flip(indirectCallersNonTransitive); + SubTypes subtypes(*module); + auto indirectCallers = callersOfHeapType(funcInfos, subtypes); // Compute the transitive closure of effects. To do so, first construct for // each function a list of the functions that it is called by (so we need to @@ -301,7 +285,6 @@ struct GenerateGlobalEffects : public Pass { std::unordered_map> callers = transitiveClosure(*module, funcInfos); - // Now that we have transitively propagated all static calls, apply that // information. First, apply infinite recursion: if a function can call // itself then it might recurse infinitely, which we consider an effect (a @@ -336,8 +319,9 @@ struct GenerateGlobalEffects : public Pass { callerEffects->mergeIn(*funcEffects); } - auto indirectCallersOfThisFunction = flipped.find(func->type.getHeapType()); - if (indirectCallersOfThisFunction == flipped.end()) { + auto indirectCallersOfThisFunction = + indirectCallers.find(func->type.getHeapType()); + if (indirectCallersOfThisFunction == indirectCallers.end()) { continue; } for (Name caller : indirectCallersOfThisFunction->second) { From 8f03f50119b2f13fc11eb1c73c6dc8f61d27a539 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 8 Apr 2026 22:55:56 +0000 Subject: [PATCH 10/11] Account for functions that don't have their address taken --- src/passes/GlobalEffects.cpp | 23 +++++++++++++++ .../passes/global-effects-closed-world.wast | 28 +++++++++---------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 14f8c518900..16f752e1017 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -24,6 +24,8 @@ #include "ir/effects.h" #include "ir/module-utils.h" #include "ir/subtypes.h" +#include "ir/element-utils.h" +#include "ir/table-utils.h" #include "pass.h" #include "support/unique_deferring_queue.h" #include "wasm.h" @@ -297,6 +299,22 @@ struct GenerateGlobalEffects : public Pass { } } + 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) { + funcsWithAddress.insert(export_->name); + } + + std::cout<<"funcsWithAddress\n"; + for (auto name : funcsWithAddress) { + std::cout<mergeIn(*funcEffects); } + if (!funcsWithAddress.contains(func->name)) { + // This function hasn't had its address taken, so no-one can indirect call it + continue; + } + auto indirectCallersOfThisFunction = indirectCallers.find(func->type.getHeapType()); if (indirectCallersOfThisFunction == indirectCallers.end()) { diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 06a45418c7c..b69d730e816 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -25,27 +25,29 @@ ;; 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))))) + (table 10 funcref) + ;; CHECK: (global $g (mut i32) (i32.const 0)) (global $g (mut i32) (i32.const 0)) ;; CHECK: (func $nop (type $nopType) (param $0 i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $nop (type $nopType) + (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 (type $maybe-has-effects) (param i32 i32) + (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 (type $maybe-has-effects) (param i32 i32) + (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32 i32) (nop) ) @@ -72,7 +74,7 @@ (call $calls-nop-via-ref (local.get $ref)) ) - ;; CHECK: (func $calls-effectful-function-via-ref (type $9) (param $ref (ref $maybe-has-effects)) + ;; CHECK: (func $calls-effectful-function-via-ref (type $7) (param $ref (ref $maybe-has-effects)) ;; CHECK-NEXT: (call_ref $maybe-has-effects ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (i32.const 2) @@ -83,20 +85,18 @@ (call_ref $maybe-has-effects (i32.const 1) (i32.const 2) (local.get $ref)) ) - ;; CHECK: (func $g (type $10) (param $ref (ref $maybe-has-effects)) (result i32) + ;; CHECK: (func $g (type $7) (param $ref (ref $maybe-has-effects)) ;; CHECK-NEXT: (call $calls-effectful-function-via-ref ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) - (func $g (param $ref (ref $maybe-has-effects)) (result i32) + (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)) - (i32.const 1) ) - ;; CHECK: (func $calls-uninhabited (type $7) (param $ref (ref $uninhabited)) + ;; CHECK: (func $calls-uninhabited (type $8) (param $ref (ref $uninhabited)) ;; CHECK-NEXT: (call_ref $uninhabited ;; CHECK-NEXT: (f32.const 0) ;; CHECK-NEXT: (local.get $ref) @@ -106,7 +106,7 @@ (call_ref $uninhabited (f32.const 0) (local.get $ref)) ) - ;; CHECK: (func $h (type $7) (param $ref (ref $uninhabited)) + ;; CHECK: (func $h (type $8) (param $ref (ref $uninhabited)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $h (param $ref (ref $uninhabited)) @@ -119,17 +119,17 @@ ;; CHECK: (func $nop-with-supertype (type $func-with-sub-param) (param $0 (ref $sub)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $nop-with-supertype (type $func-with-sub-param) (param (ref $sub)) + (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 (type $func-with-super-param) (param (ref $super)) + (func $effectful-with-subtype (export "effectful-with-subtype") (type $func-with-super-param) (param (ref $super)) (unreachable) ) - ;; CHECK: (func $calls-ref-with-subtype (type $8) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK: (func $calls-ref-with-subtype (type $9) (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) @@ -139,7 +139,7 @@ (call_ref $func-with-sub-param (local.get $sub) (local.get $func)) ) - ;; CHECK: (func $asdf (type $8) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK: (func $asdf (type $9) (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) From 4cbc6b25e30aa0af6e4e75fc09f218810e814c00 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 8 Apr 2026 23:41:10 +0000 Subject: [PATCH 11/11] More tests --- .../passes/global-effects-closed-world.wast | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index b69d730e816..85823bf43dd 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -5,6 +5,8 @@ ;; 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))) @@ -25,6 +27,8 @@ ;; 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)) @@ -51,7 +55,7 @@ (nop) ) - ;; CHECK: (func $calls-nop-via-ref (type $6) (param $ref (ref $nopType)) + ;; CHECK: (func $calls-nop-via-ref (type $7) (param $ref (ref $nopType)) ;; CHECK-NEXT: (call_ref $nopType ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (local.get $ref) @@ -65,7 +69,7 @@ (call_ref $nopType (i32.const 1) (local.get $ref)) ) - ;; CHECK: (func $f (type $6) (param $ref (ref $nopType)) + ;; CHECK: (func $f (type $7) (param $ref (ref $nopType)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $f (param $ref (ref $nopType)) @@ -74,7 +78,7 @@ (call $calls-nop-via-ref (local.get $ref)) ) - ;; CHECK: (func $calls-effectful-function-via-ref (type $7) (param $ref (ref $maybe-has-effects)) + ;; CHECK: (func $calls-effectful-function-via-ref (type $8) (param $ref (ref $maybe-has-effects)) ;; CHECK-NEXT: (call_ref $maybe-has-effects ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (i32.const 2) @@ -85,7 +89,7 @@ (call_ref $maybe-has-effects (i32.const 1) (i32.const 2) (local.get $ref)) ) - ;; CHECK: (func $g (type $7) (param $ref (ref $maybe-has-effects)) + ;; CHECK: (func $g (type $8) (param $ref (ref $maybe-has-effects)) ;; CHECK-NEXT: (call $calls-effectful-function-via-ref ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) @@ -96,7 +100,7 @@ (call $calls-effectful-function-via-ref (local.get $ref)) ) - ;; CHECK: (func $calls-uninhabited (type $8) (param $ref (ref $uninhabited)) + ;; CHECK: (func $calls-uninhabited (type $9) (param $ref (ref $uninhabited)) ;; CHECK-NEXT: (call_ref $uninhabited ;; CHECK-NEXT: (f32.const 0) ;; CHECK-NEXT: (local.get $ref) @@ -106,7 +110,7 @@ (call_ref $uninhabited (f32.const 0) (local.get $ref)) ) - ;; CHECK: (func $h (type $8) (param $ref (ref $uninhabited)) + ;; CHECK: (func $h (type $9) (param $ref (ref $uninhabited)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $h (param $ref (ref $uninhabited)) @@ -129,7 +133,7 @@ (unreachable) ) - ;; CHECK: (func $calls-ref-with-subtype (type $9) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK: (func $calls-ref-with-subtype (type $10) (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) @@ -139,7 +143,7 @@ (call_ref $func-with-sub-param (local.get $sub) (local.get $func)) ) - ;; CHECK: (func $asdf (type $9) (param $func (ref $func-with-sub-param)) (param $sub (ref $sub)) + ;; CHECK: (func $asdf (type $10) (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) @@ -154,4 +158,35 @@ ;; 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 $11) (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 $11) (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)) + ) )