From dc53f9578f0a4ac270a0cde87bdcfe46da3a75df Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 20 May 2026 18:50:21 +0000 Subject: [PATCH 01/14] LinearExecutionWalker indirect call effects --- src/ir/linear-execution.h | 59 ++++++++++++------- src/support/utilities.h | 8 +++ .../simplify-locals-global-effects-eh.wast | 57 +++++++++++------- 3 files changed, 83 insertions(+), 41 deletions(-) diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index 167400a0137..db0da814b73 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -80,7 +80,12 @@ struct LinearExecutionWalker : public PostWalker { static void scan(SubType* self, Expression** currp) { Expression* curr = *currp; - auto handleCall = [&](bool mayThrow, bool isReturn) { + auto handleCall = [&](bool isReturn, const EffectAnalyzer* effects) { + bool refutesThrowEffect = effects && !effects->throws_; + bool mayThrow = !self->getModule() || + self->getModule()->features.hasExceptionHandling(); + mayThrow = mayThrow && !refutesThrowEffect; + if (!self->connectAdjacentBlocks) { // Control is nonlinear if we return or throw. Traps don't need to be // taken into account since they don't break control flow in a way @@ -156,40 +161,52 @@ struct LinearExecutionWalker : public PostWalker { case Expression::Id::CallId: { auto* call = curr->cast(); - bool mayThrow = !self->getModule() || - self->getModule()->features.hasExceptionHandling(); - if (mayThrow && self->getModule()) { - auto* effects = - self->getModule()->getFunction(call->target)->effects.get(); - - if (effects && !effects->throws_) { - mayThrow = false; + const EffectAnalyzer* effects = nullptr; + if (self->getModule()) { + auto* func = self->getModule()->getFunctionOrNull(call->target); + if (func) { + effects = func->effects.get(); } } - handleCall(mayThrow, call->isReturn); + handleCall(call->isReturn, effects); break; } case Expression::Id::CallRefId: { auto* callRef = curr->cast(); - // TODO: Effect analysis for indirect calls isn't implemented yet. - // Assume any indirect call may throw for now. - bool mayThrow = !self->getModule() || - self->getModule()->features.hasExceptionHandling(); + const EffectAnalyzer* effects = [&]() -> const EffectAnalyzer* { + if (!self->getModule()) { + return nullptr; + } + if (!callRef->target->type.isRef()) { + return nullptr; + } - handleCall(mayThrow, callRef->isReturn); + auto* effects_ptr = + find_or_null(self->getModule()->indirectCallEffects, + callRef->target->type.getHeapType()); + if (!effects_ptr) { + return nullptr; + } + return effects_ptr->get(); + }(); + + handleCall(callRef->isReturn, effects); break; } case Expression::Id::CallIndirectId: { auto* callIndirect = curr->cast(); - // TODO: Effect analysis for indirect calls isn't implemented yet. - // Assume any indirect call may throw for now. - bool mayThrow = !self->getModule() || - self->getModule()->features.hasExceptionHandling(); - - handleCall(mayThrow, callIndirect->isReturn); + const EffectAnalyzer* effects = nullptr; + if (self->getModule()) { + if (const auto& effects_ptr = + find_or_null(self->getModule()->indirectCallEffects, + callIndirect->heapType)) { + effects = effects_ptr->get(); + } + } + handleCall(callIndirect->isReturn, effects); break; } case Expression::Id::TryId: { diff --git a/src/support/utilities.h b/src/support/utilities.h index ae8822bf4e2..0aad86c94e1 100644 --- a/src/support/utilities.h +++ b/src/support/utilities.h @@ -107,6 +107,14 @@ template struct overloaded : Ts... { template overloaded(Ts...) -> overloaded; +// Lookup a value from `map` and return a pointer to the underlying value +// or nullptr if not present. Returns a const pointer if `map` is const and +// non-const otherwise +auto* find_or_null(auto& map, const auto& key) { + auto it = map.find(key); + return it != map.end() ? &it->second : nullptr; +} + } // namespace wasm #endif // wasm_support_utilities_h diff --git a/test/lit/passes/simplify-locals-global-effects-eh.wast b/test/lit/passes/simplify-locals-global-effects-eh.wast index ff11e7487b6..3556c1394fa 100644 --- a/test/lit/passes/simplify-locals-global-effects-eh.wast +++ b/test/lit/passes/simplify-locals-global-effects-eh.wast @@ -56,10 +56,11 @@ ) (module + ;; CHECK: (type $throw-type (func (result f64))) + ;; CHECK: (type $const-type (func (result f32))) (type $const-type (func (result f32))) - ;; CHECK: (type $throw-type (func (result f64))) (type $throw-type (func (result f64))) ;; CHECK: (global $g (mut i32) (i32.const 0)) @@ -68,7 +69,7 @@ ;; CHECK: (table $t 2 2 funcref) (table $t 2 2 funcref) - ;; CHECK: (tag $t (type $2)) + ;; CHECK: (tag $t (type $3)) (tag $t) ;; CHECK: (func $const (type $const-type) (result f32) @@ -90,32 +91,48 @@ ) (elem declare $throws) - ;; CHECK: (func $read-g (type $3) (param $ref (ref null $const-type)) (result i32) + ;; CHECK: (func $read-g-with-nop-call-ref (type $4) (param $ref (ref null $const-type)) (result i32) ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (global.get $g) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (call_ref $const-type ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (global.get $g) ;; CHECK-NEXT: ) - (func $read-g (param $ref (ref null $const-type)) (result i32) + (func $read-g-with-nop-call-ref (param $ref (ref null $const-type)) (result i32) (local $x i32) (local.set $x (global.get $g)) - ;; With more precise effect analysis for indirect calls, we can determine - ;; that the only possible target for this ref is $const in a closed world, - ;; which wouldn't block our optimizations. - ;; TODO: Add effects analysis for indirect calls. + ;; With --closed-world enabled, we can tell that this can only possibly call + ;; $const, which doesn't block our optimizations. (drop (call_ref $const-type (local.get $ref))) (local.get $x) ) - ;; CHECK: (func $read-g-with-throw-in-between (type $4) (param $ref (ref $throw-type)) (result i32) + ;; CHECK: (func $read-g-with-nop-call-indirect (type $5) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_indirect $t (type $const-type) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + (func $read-g-with-nop-call-indirect (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; Similar to above with call_indirect instead of call_ref. + (drop (call_indirect (type $const-type) (i32.const 0))) + + (local.get $x) + ) + + ;; CHECK: (func $read-g-with-effectful-call-ref (type $2) (param $ref (ref $throw-type)) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (global.get $g) @@ -127,7 +144,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) - (func $read-g-with-throw-in-between (param $ref (ref $throw-type)) (result i32) + (func $read-g-with-effectful-call-ref (param $ref (ref $throw-type)) (result i32) (local $x i32) (local.set $x (global.get $g)) @@ -138,25 +155,25 @@ (local.get $x) ) - ;; CHECK: (func $read-g-with-call-indirect-in-between (type $5) (result i32) + ;; CHECK: (func $read-g-with-effectful-call-indirect (type $2) (param $ref (ref $throw-type)) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (global.get $g) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (call_indirect $t (type $const-type) + ;; CHECK-NEXT: (call_indirect $t (type $throw-type) ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) - (func $read-g-with-call-indirect-in-between (result i32) + (func $read-g-with-effectful-call-indirect (param $ref (ref $throw-type)) (result i32) (local $x i32) (local.set $x (global.get $g)) - ;; Similar to above with call_indirect instead of call_ref. - ;; TODO: Add effects analysis for indirect calls. - (drop (call_indirect (type $const-type) (i32.const 0))) + ;; Similar to above, except here we can tell that the indirect call may + ;; throw so optimization is halted. + (drop (call_indirect (type $throw-type) (i32.const 0))) (local.get $x) ) From 3cda58825832e5ebb403a958b32156b377c98dc1 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Thu, 21 May 2026 15:59:50 +0000 Subject: [PATCH 02/14] PR updates --- src/ir/linear-execution.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index db0da814b73..6abab7f28b5 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -164,6 +164,8 @@ struct LinearExecutionWalker : public PostWalker { const EffectAnalyzer* effects = nullptr; if (self->getModule()) { auto* func = self->getModule()->getFunctionOrNull(call->target); + // TODO: `func` might not exist here because of #8753. Fix this + // and remove the null check. if (func) { effects = func->effects.get(); } @@ -183,13 +185,13 @@ struct LinearExecutionWalker : public PostWalker { return nullptr; } - auto* effects_ptr = + auto* effectsPtr = find_or_null(self->getModule()->indirectCallEffects, callRef->target->type.getHeapType()); - if (!effects_ptr) { + if (!effectsPtr) { return nullptr; } - return effects_ptr->get(); + return effectsPtr->get(); }(); handleCall(callRef->isReturn, effects); @@ -200,10 +202,10 @@ struct LinearExecutionWalker : public PostWalker { const EffectAnalyzer* effects = nullptr; if (self->getModule()) { - if (const auto& effects_ptr = + if (const auto& effectsPtr = find_or_null(self->getModule()->indirectCallEffects, callIndirect->heapType)) { - effects = effects_ptr->get(); + effects = effectsPtr->get(); } } handleCall(callIndirect->isReturn, effects); From b1af02049ab51c022e78debe9d42e40ccb3b6c40 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Fri, 22 May 2026 22:29:24 +0000 Subject: [PATCH 03/14] Fix handling for throw effect with call_ref to (unreachable) --- src/ir/linear-execution.h | 43 +++++++++---------- .../simplify-locals-global-effects-eh.wast | 38 +++++++++++++--- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index 6abab7f28b5..9e69405ff7c 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -80,8 +80,7 @@ struct LinearExecutionWalker : public PostWalker { static void scan(SubType* self, Expression** currp) { Expression* curr = *currp; - auto handleCall = [&](bool isReturn, const EffectAnalyzer* effects) { - bool refutesThrowEffect = effects && !effects->throws_; + auto handleCall = [&](bool isReturn, bool refutesThrowEffect) { bool mayThrow = !self->getModule() || self->getModule()->features.hasExceptionHandling(); mayThrow = mayThrow && !refutesThrowEffect; @@ -161,54 +160,54 @@ struct LinearExecutionWalker : public PostWalker { case Expression::Id::CallId: { auto* call = curr->cast(); - const EffectAnalyzer* effects = nullptr; + bool refutesThrowEffect = false; if (self->getModule()) { auto* func = self->getModule()->getFunctionOrNull(call->target); // TODO: `func` might not exist here because of #8753. Fix this // and remove the null check. - if (func) { - effects = func->effects.get(); + if (func && func->effects) { + refutesThrowEffect = !func->effects->throws_; } } - handleCall(call->isReturn, effects); + handleCall(call->isReturn, refutesThrowEffect); break; } case Expression::Id::CallRefId: { auto* callRef = curr->cast(); - const EffectAnalyzer* effects = [&]() -> const EffectAnalyzer* { + bool refutesThrowEffect = [&]() { if (!self->getModule()) { - return nullptr; + return false; } if (!callRef->target->type.isRef()) { - return nullptr; + // This is an unreachable, so no throws effect. + return true; } - auto* effectsPtr = - find_or_null(self->getModule()->indirectCallEffects, - callRef->target->type.getHeapType()); - if (!effectsPtr) { - return nullptr; + auto* effects = find_or_null(self->getModule()->indirectCallEffects, + callRef->target->type.getHeapType()); + if (!effects) { + return false; } - return effectsPtr->get(); + return !(*effects)->throws_; }(); - handleCall(callRef->isReturn, effects); + handleCall(callRef->isReturn, refutesThrowEffect); break; } case Expression::Id::CallIndirectId: { auto* callIndirect = curr->cast(); - const EffectAnalyzer* effects = nullptr; + bool refutesThrowEffect = false; if (self->getModule()) { - if (const auto& effectsPtr = - find_or_null(self->getModule()->indirectCallEffects, - callIndirect->heapType)) { - effects = effectsPtr->get(); + if (auto* effects = find_or_null( + self->getModule()->indirectCallEffects, callIndirect->heapType); + effects) { + refutesThrowEffect = !(*effects)->throws_; } } - handleCall(callIndirect->isReturn, effects); + handleCall(callIndirect->isReturn, refutesThrowEffect); break; } case Expression::Id::TryId: { diff --git a/test/lit/passes/simplify-locals-global-effects-eh.wast b/test/lit/passes/simplify-locals-global-effects-eh.wast index 3556c1394fa..28d47c6acb9 100644 --- a/test/lit/passes/simplify-locals-global-effects-eh.wast +++ b/test/lit/passes/simplify-locals-global-effects-eh.wast @@ -69,7 +69,7 @@ ;; CHECK: (table $t 2 2 funcref) (table $t 2 2 funcref) - ;; CHECK: (tag $t (type $3)) + ;; CHECK: (tag $t (type $4)) (tag $t) ;; CHECK: (func $const (type $const-type) (result f32) @@ -91,7 +91,7 @@ ) (elem declare $throws) - ;; CHECK: (func $read-g-with-nop-call-ref (type $4) (param $ref (ref null $const-type)) (result i32) + ;; CHECK: (func $read-g-with-nop-call-ref (type $5) (param $ref (ref null $const-type)) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (drop @@ -112,7 +112,7 @@ (local.get $x) ) - ;; CHECK: (func $read-g-with-nop-call-indirect (type $5) (result i32) + ;; CHECK: (func $read-g-with-nop-call-indirect (type $2) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (drop @@ -132,7 +132,7 @@ (local.get $x) ) - ;; CHECK: (func $read-g-with-effectful-call-ref (type $2) (param $ref (ref $throw-type)) (result i32) + ;; CHECK: (func $read-g-with-effectful-call-ref (type $3) (param $ref (ref $throw-type)) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (global.get $g) @@ -155,7 +155,7 @@ (local.get $x) ) - ;; CHECK: (func $read-g-with-effectful-call-indirect (type $2) (param $ref (ref $throw-type)) (result i32) + ;; CHECK: (func $read-g-with-effectful-call-indirect (type $3) (param $ref (ref $throw-type)) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (global.get $g) @@ -177,4 +177,32 @@ (local.get $x) ) + + ;; CHECK: (func $read-g-with-unreachable-call-ref (type $2) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block ;; (replaces unreachable CallRef we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g-with-unreachable-call-ref (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; This is guaranteed to trap, and the type immediate doesn't matter. + ;; TODO: we should be able to optimize this, but something is likely missing + ;; in SimplifyGlobals (LinearExecutionWalker handles this case correctly). + (drop (call_ref $throw-type (unreachable))) + + (local.get $x) + ) + ) From 4bc9ee96925d9bb5089f666278be8384c75f6221 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Fri, 22 May 2026 23:44:58 +0000 Subject: [PATCH 04/14] Remove extra newline --- test/lit/passes/simplify-locals-global-effects-eh.wast | 1 - 1 file changed, 1 deletion(-) diff --git a/test/lit/passes/simplify-locals-global-effects-eh.wast b/test/lit/passes/simplify-locals-global-effects-eh.wast index 28d47c6acb9..f616208a9b1 100644 --- a/test/lit/passes/simplify-locals-global-effects-eh.wast +++ b/test/lit/passes/simplify-locals-global-effects-eh.wast @@ -204,5 +204,4 @@ (local.get $x) ) - ) From 703dcddf94588b0c496e9701e6d8f98e35884d17 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Sun, 19 Apr 2026 16:31:24 +0000 Subject: [PATCH 05/14] Account for addressed funcs --- src/passes/GlobalEffects.cpp | 72 ++++++++++++++++--- .../passes/global-effects-closed-world.wast | 4 -- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 88fb4c00907..73e05a8afc0 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -20,6 +20,7 @@ // #include "ir/effects.h" +#include "ir/element-utils.h" #include "ir/module-utils.h" #include "pass.h" #include "support/graph_traversal.h" @@ -44,8 +45,55 @@ struct FuncInfo { std::unordered_set indirectCalledTypes; }; +std::unordered_set getAddressedFuncs(Module& module) { + struct AddressedFuncsWalker + : PostWalker> { + const Module& module; + std::unordered_set& addressedFuncs; + + AddressedFuncsWalker(const Module& module, + std::unordered_set& addressedFuncs) + : module(module), addressedFuncs(addressedFuncs) {} + + void visitExpression(Expression* curr) { + if (auto* refFunc = curr->dynCast()) { + addressedFuncs.insert(module.getFunction(refFunc->func)); + } + } + }; + + std::unordered_set addressedFuncs; + AddressedFuncsWalker walker(module, addressedFuncs); + walker.walkModule(&module); + + ModuleUtils::iterImportedFunctions( + module, [&addressedFuncs, &module](Function* import) { + addressedFuncs.insert(module.getFunction(import->name)); + }); + + for (const auto& export_ : module.exports) { + if (export_->kind != ExternalKind::Function) { + continue; + } + + // TODO: internal or external? I think internal + // This might be why we failed to lookup the function earlier + // Maybe we can use Function* after all + addressedFuncs.insert(module.getFunction(*export_->getInternalName())); + } + + ElementUtils::iterAllElementFunctionNames( + &module, [&addressedFuncs, &module](Name func) { + addressedFuncs.insert(module.getFunction(func)); + }); + + return addressedFuncs; +} + std::map analyzeFuncs(Module& module, const PassOptions& passOptions) { + std::unordered_set addressedFuncs; ModuleUtils::ParallelFunctionAnalysis analysis( module, [&](Function* func, FuncInfo& funcInfo) { if (func->imported()) { @@ -76,10 +124,14 @@ std::map analyzeFuncs(Module& module, const PassOptions& options; FuncInfo& funcInfo; + std::unordered_set& addressedFuncs; + CallScanner(Module& wasm, const PassOptions& options, - FuncInfo& funcInfo) - : wasm(wasm), options(options), funcInfo(funcInfo) {} + FuncInfo& funcInfo, + std::unordered_set& addressedFuncs) + : wasm(wasm), options(options), funcInfo(funcInfo), + addressedFuncs(addressedFuncs) {} void visitExpression(Expression* curr) { ShallowEffectAnalyzer effects(options, wasm, curr); @@ -113,7 +165,7 @@ std::map analyzeFuncs(Module& module, } } }; - CallScanner scanner(module, passOptions, funcInfo); + CallScanner scanner(module, passOptions, funcInfo, addressedFuncs); scanner.walkFunction(func); } }); @@ -143,6 +195,7 @@ using CallGraph = CallGraph buildCallGraph(const Module& module, const std::map& funcInfos, + const std::unordered_set addressedFuncs, bool closedWorld) { CallGraph callGraph; if (!closedWorld) { @@ -178,7 +231,9 @@ CallGraph buildCallGraph(const Module& module, } // Type -> Function - callGraph[caller->type.getHeapType()].insert(caller); + if (addressedFuncs.contains(caller)) { + callGraph[caller->type.getHeapType()].insert(caller); + } } // Type -> Type @@ -346,11 +401,12 @@ void propagateEffects( struct GenerateGlobalEffects : public Pass { void run(Module* module) override { - std::map funcInfos = - analyzeFuncs(*module, getPassOptions()); + auto funcInfos = analyzeFuncs(*module, getPassOptions()); + + auto addressedFuncs = getAddressedFuncs(*module); - auto callGraph = - buildCallGraph(*module, funcInfos, getPassOptions().closedWorld); + auto callGraph = buildCallGraph( + *module, funcInfos, addressedFuncs, getPassOptions().closedWorld); propagateEffects(*module, getPassOptions(), diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 7b1945fc3a0..7dfe36108ca 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -203,10 +203,6 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $calls-type-with-effects-but-not-addressable (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_ref $only-has-effects-in-not-addressable-function (i32.const 1) (local.get $ref)) ) ) From bafa5f685c22ee87d1e498f99fd03f7ff3f1b87e Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Mon, 20 Apr 2026 21:32:19 +0000 Subject: [PATCH 06/14] PR updates --- src/passes/GlobalEffects.cpp | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 73e05a8afc0..bac3d6b6753 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -45,26 +45,33 @@ struct FuncInfo { std::unordered_set indirectCalledTypes; }; +/* + Only funcs that are 'addressed' may be the target of an indirect call. A + function is addressed if: + - It appears in a ref.func expression + - It appears in an `elem declare` statement (note that we already ignore `elem` + statements in our IR, but we check separately for funcs that appear in + `ref.func`). + - It's exported, because it may flow back to us as a reference. + - It's imported, which implies it is `elem declare`d. + + If a function doesn't meet any of these criteria, it can't be the target of + an indirect call and we don't need to include its effects in indirect calls. +*/ std::unordered_set getAddressedFuncs(Module& module) { - struct AddressedFuncsWalker - : PostWalker> { - const Module& module; + struct AddressedFuncsWalker : PostWalker { std::unordered_set& addressedFuncs; - AddressedFuncsWalker(const Module& module, - std::unordered_set& addressedFuncs) - : module(module), addressedFuncs(addressedFuncs) {} + AddressedFuncsWalker(std::unordered_set& addressedFuncs) + : addressedFuncs(addressedFuncs) {} - void visitExpression(Expression* curr) { - if (auto* refFunc = curr->dynCast()) { - addressedFuncs.insert(module.getFunction(refFunc->func)); - } + void visitRefFunc(RefFunc* refFunc) { + addressedFuncs.insert(getModule()->getFunction(refFunc->func)); } }; std::unordered_set addressedFuncs; - AddressedFuncsWalker walker(module, addressedFuncs); + AddressedFuncsWalker walker(addressedFuncs); walker.walkModule(&module); ModuleUtils::iterImportedFunctions( @@ -77,9 +84,6 @@ std::unordered_set getAddressedFuncs(Module& module) { continue; } - // TODO: internal or external? I think internal - // This might be why we failed to lookup the function earlier - // Maybe we can use Function* after all addressedFuncs.insert(module.getFunction(*export_->getInternalName())); } @@ -93,7 +97,6 @@ std::unordered_set getAddressedFuncs(Module& module) { std::map analyzeFuncs(Module& module, const PassOptions& passOptions) { - std::unordered_set addressedFuncs; ModuleUtils::ParallelFunctionAnalysis analysis( module, [&](Function* func, FuncInfo& funcInfo) { if (func->imported()) { @@ -124,14 +127,10 @@ std::map analyzeFuncs(Module& module, const PassOptions& options; FuncInfo& funcInfo; - std::unordered_set& addressedFuncs; - CallScanner(Module& wasm, const PassOptions& options, - FuncInfo& funcInfo, - std::unordered_set& addressedFuncs) - : wasm(wasm), options(options), funcInfo(funcInfo), - addressedFuncs(addressedFuncs) {} + FuncInfo& funcInfo) + : wasm(wasm), options(options), funcInfo(funcInfo) {} void visitExpression(Expression* curr) { ShallowEffectAnalyzer effects(options, wasm, curr); @@ -165,7 +164,7 @@ std::map analyzeFuncs(Module& module, } } }; - CallScanner scanner(module, passOptions, funcInfo, addressedFuncs); + CallScanner scanner(module, passOptions, funcInfo); scanner.walkFunction(func); } }); @@ -195,7 +194,7 @@ using CallGraph = CallGraph buildCallGraph(const Module& module, const std::map& funcInfos, - const std::unordered_set addressedFuncs, + const std::unordered_set& addressedFuncs, bool closedWorld) { CallGraph callGraph; if (!closedWorld) { @@ -401,7 +400,8 @@ void propagateEffects( struct GenerateGlobalEffects : public Pass { void run(Module* module) override { - auto funcInfos = analyzeFuncs(*module, getPassOptions()); + std::map funcInfos = + analyzeFuncs(*module, getPassOptions()); auto addressedFuncs = getAddressedFuncs(*module); From 7e39398eccffe1a2956bbed5442ff469856f800c Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Thu, 23 Apr 2026 16:15:26 +0000 Subject: [PATCH 07/14] Fix comment --- src/passes/GlobalEffects.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index bac3d6b6753..52f262efee6 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -49,9 +49,9 @@ struct FuncInfo { Only funcs that are 'addressed' may be the target of an indirect call. A function is addressed if: - It appears in a ref.func expression - - It appears in an `elem declare` statement (note that we already ignore `elem` + - It appears in an `elem` segment (note that we already ignore `elem declare` statements in our IR, but we check separately for funcs that appear in - `ref.func`). + `ref.func`). - It's exported, because it may flow back to us as a reference. - It's imported, which implies it is `elem declare`d. From 058a10ccc472a8a98308fe56b3efb2ffd8b981db Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 6 May 2026 18:03:35 +0000 Subject: [PATCH 08/14] PR updates --- src/passes/GlobalEffects.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 52f262efee6..bf2fd5aa395 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -236,12 +236,12 @@ CallGraph buildCallGraph(const Module& module, } // Type -> Type - // Do a DFS up the type heirarchy for all function implementations. + // Do a DFS up the type hierarchy for all function implementations. // We are essentially walking up each supertype chain and adding edges from // super -> subtype, but doing it via DFS to avoid repeated work. Graph superTypeGraph(allFunctionTypes.begin(), allFunctionTypes.end(), - [&callGraph](auto&& push, HeapType t) { + [&callGraph](const auto& push, HeapType t) { // Not needed except that during lookup we expect the // key to exist. callGraph[t]; From d746b6b1e833427a88ef8b6452dc889148e83a7b Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 20 May 2026 18:48:58 +0000 Subject: [PATCH 09/14] WIP test updates --- ...-effects-closed-world-simplify-locals.wast | 116 ++++++++++++++++-- .../passes/global-effects-closed-world.wast | 8 +- 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/test/lit/passes/global-effects-closed-world-simplify-locals.wast b/test/lit/passes/global-effects-closed-world-simplify-locals.wast index 23f5dc17362..0234ccf0625 100644 --- a/test/lit/passes/global-effects-closed-world-simplify-locals.wast +++ b/test/lit/passes/global-effects-closed-world-simplify-locals.wast @@ -8,11 +8,11 @@ ;; CHECK: (type $indirect-type-super (sub (func (param i32)))) (type $indirect-type-super (sub (func (param i32)))) - ;; CHECK: (type $1 (func (param (ref $indirect-type-super)))) - ;; CHECK: (type $indirect-type-sub (sub $indirect-type-super (func (param i32)))) (type $indirect-type-sub (sub $indirect-type-super (func (param i32)))) + ;; CHECK: (type $2 (func (param (ref $indirect-type-super)))) + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) (global $g1 (mut i32) (i32.const 0)) ;; CHECK: (global $g2 (mut i32) (i32.const 0)) @@ -42,18 +42,115 @@ (global.set $g2 (local.get $i32)) ) - ;; CHECK: (func $caller (type $1) (param $ref (ref $indirect-type-super)) + ;; CHECK: (func $merges-multiple-effects (type $2) (param $ref (ref $indirect-type-super)) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local $z i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (global.get $g2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (call_ref $indirect-type-super ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g3) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $caller (param $ref (ref $indirect-type-super)) - ;; This inherits effects from $impl1 and $impl2, so may mutate $g1 and $g2. + (func $merges-multiple-effects (param $ref (ref $indirect-type-super)) + (local $x i32) + (local $y i32) + (local $z i32) + + (local.set $x (global.get $g1)) + (local.set $y (global.get $g2)) + (local.set $z (global.get $g3)) + + ;; This acts as a barrier for $x and $y, but not $z because + ;; $ref may write to $g1 (via $impl1) or $g2 (via $impl2) but not $g3. + ;; $z is optimized out and $x and $y are left alone. (call_ref $indirect-type-super (i32.const 1) (local.get $ref)) + + (drop (local.get $x)) + (drop (local.get $y)) + (drop (local.get $z)) ) +) + +(module + ;; CHECK: (type $indirect-type (func (param i32))) + (type $indirect-type (func (param i32))) + + ;; CHECK: (type $1 (func)) - ;; CHECK: (func $merges-multiple-effects (type $1) (param $ref (ref $indirect-type-super)) + ;; CHECK: (type $2 (func (param (ref $indirect-type)))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) + (global $g1 (mut i32) (i32.const 0)) + ;; CHECK: (global $g2 (mut i32) (i32.const 0)) + (global $g2 (mut i32) (i32.const 0)) + ;; CHECK: (global $g3 (mut i32) (i32.const 0)) + (global $g3 (mut i32) (i32.const 0)) + + (table 1 1 funcref) + + ;; CHECK: (table $0 1 1 funcref) + + ;; CHECK: (elem $f3 func) + + ;; CHECK: (elem declare func $f2) + + ;; CHECK: (export "f1" (func $f1)) + + ;; CHECK: (func $f1 (type $indirect-type) (param $i32 i32) + ;; CHECK-NEXT: (global.set $g1 + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f1 (export "f1") (type $indirect-type) (param $i32 i32) + (global.set $g1 (local.get $i32)) + ) + + ;; CHECK: (func $f2 (type $indirect-type) (param $i32 i32) + ;; CHECK-NEXT: (global.set $g2 + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f2 (type $indirect-type) (param $i32 i32) + (global.set $g2 (local.get $i32)) + ) + (func + (drop (ref.func $f2)) + ) + + + ;; CHECK: (func $0 (type $1) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.func $f2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + + ;; CHECK: (func $f3 (type $indirect-type) (param $i32 i32) + ;; CHECK-NEXT: (global.set $g3 + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f3 (type $indirect-type) (param $i32 i32) + (global.set $g3 (local.get $i32)) + ) + (elem $f3) + + ;; CHECK: (func $merges-multiple-effects (type $2) (param $ref (ref $indirect-type)) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local $y i32) ;; CHECK-NEXT: (local $z i32) @@ -64,7 +161,8 @@ ;; CHECK-NEXT: (global.get $g2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (call $caller + ;; CHECK-NEXT: (call_ref $indirect-type + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop @@ -77,7 +175,7 @@ ;; CHECK-NEXT: (global.get $g3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $merges-multiple-effects (param $ref (ref $indirect-type-super)) + (func $merges-multiple-effects (param $ref (ref $indirect-type)) (local $x i32) (local $y i32) (local $z i32) @@ -89,7 +187,7 @@ ;; This acts as a barrier for $x and $y, but not $z because ;; $ref may write to $g1 (via $impl1) or $g2 (via $impl2) but not $g3. ;; $z is optimized out and $x and $y are left alone. - (call $caller (local.get $ref)) + (call_ref $indirect-type (i32.const 1) (local.get $ref)) (drop (local.get $x)) (drop (local.get $y)) diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 7dfe36108ca..774a72d054f 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -197,12 +197,12 @@ ) ;; 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: (nop) ;; CHECK-NEXT: ) (func $calls-type-with-effects-but-not-addressable (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. + ;; So the call_ref's only potential target is $nop which has no effects. (call_ref $only-has-effects-in-not-addressable-function (i32.const 1) (local.get $ref)) ) ) From 0af540264f3a2fb15086b8bc0731772466eba91a Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 27 May 2026 01:25:34 +0000 Subject: [PATCH 10/14] Updates --- src/passes/GlobalEffects.cpp | 3 +- ...-effects-closed-world-simplify-locals.wast | 174 +++++++++++++++--- 2 files changed, 146 insertions(+), 31 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index c0db5a8e3bf..67b2c27001e 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -59,7 +59,7 @@ struct FuncInfo { an indirect call and we don't need to include its effects in indirect calls. */ std::unordered_set getAddressedFuncs(Module& module) { - struct AddressedFuncsWalker : PostWalker { + struct AddressedFuncsWalker : WalkerPass> { std::unordered_set& addressedFuncs; AddressedFuncsWalker(std::unordered_set& addressedFuncs) @@ -72,6 +72,7 @@ std::unordered_set getAddressedFuncs(Module& module) { std::unordered_set addressedFuncs; AddressedFuncsWalker walker(addressedFuncs); + walker.walkModuleCode(&module); walker.walkModule(&module); ModuleUtils::iterImportedFunctions( diff --git a/test/lit/passes/global-effects-closed-world-simplify-locals.wast b/test/lit/passes/global-effects-closed-world-simplify-locals.wast index 0234ccf0625..49b1bc34878 100644 --- a/test/lit/passes/global-effects-closed-world-simplify-locals.wast +++ b/test/lit/passes/global-effects-closed-world-simplify-locals.wast @@ -4,6 +4,8 @@ ;; Tests for aggregating effects from indirect calls in GlobalEffects when ;; --closed-world is true. Continued from global-effects-closed-world.wast. +;; Test that effects are aggregated from both $indirect-type-super and +;; $indirect-type-sub for indirect calls to $indirect-type-super. (module ;; CHECK: (type $indirect-type-super (sub (func (param i32)))) (type $indirect-type-super (sub (func (param i32)))) @@ -13,6 +15,8 @@ ;; CHECK: (type $2 (func (param (ref $indirect-type-super)))) + ;; CHECK: (type $3 (func (param (ref $indirect-type-sub)))) + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) (global $g1 (mut i32) (i32.const 0)) ;; CHECK: (global $g2 (mut i32) (i32.const 0)) @@ -42,7 +46,7 @@ (global.set $g2 (local.get $i32)) ) - ;; CHECK: (func $merges-multiple-effects (type $2) (param $ref (ref $indirect-type-super)) + ;; CHECK: (func $merges-effects-from-super-and-sub (type $2) (param $ref (ref $indirect-type-super)) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local $y i32) ;; CHECK-NEXT: (local $z i32) @@ -67,7 +71,7 @@ ;; CHECK-NEXT: (global.get $g3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $merges-multiple-effects (param $ref (ref $indirect-type-super)) + (func $merges-effects-from-super-and-sub (param $ref (ref $indirect-type-super)) (local $x i32) (local $y i32) (local $z i32) @@ -81,12 +85,61 @@ ;; $z is optimized out and $x and $y are left alone. (call_ref $indirect-type-super (i32.const 1) (local.get $ref)) + (drop (local.get $x)) + (drop (local.get $y)) + (drop (local.get $z)) + ) + ;; CHECK: (func $merges-effects-from-sub-only (type $3) (param $ref (ref $indirect-type-sub)) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local $z i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (global.get $g2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (call_ref $indirect-type-sub + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $merges-effects-from-sub-only (param $ref (ref $indirect-type-sub)) + (local $x i32) + (local $y i32) + (local $z i32) + + (local.set $x (global.get $g1)) + (local.set $y (global.get $g2)) + (local.set $z (global.get $g3)) + + ;; Similar to above but here it's impossible to reach $impl1 + ;; (the supertype), so $x can safely be optimized out. + (call_ref $indirect-type-sub (i32.const 1) (local.get $ref)) + (drop (local.get $x)) (drop (local.get $y)) (drop (local.get $z)) ) ) +;; Test different ways of referencing functions to ensure that they're included +;; in indirect effects analysis. A function is considered 'addressed' if it's: +;; - imported (tested in the next test) +;; - exported +;; - referenced in a ref.func +;; - contained in an `elem` segment +;; Imported functions are tested in the next module to avoid +;; confounding this test because imports are assumed to have all possible +;; effects. (module ;; CHECK: (type $indirect-type (func (param i32))) (type $indirect-type (func (param i32))) @@ -101,12 +154,14 @@ (global $g2 (mut i32) (i32.const 0)) ;; CHECK: (global $g3 (mut i32) (i32.const 0)) (global $g3 (mut i32) (i32.const 0)) + ;; CHECK: (global $g4 (mut i32) (i32.const 0)) + (global $g4 (mut i32) (i32.const 0)) (table 1 1 funcref) ;; CHECK: (table $0 1 1 funcref) - ;; CHECK: (elem $f3 func) + ;; CHECK: (elem $0 (i32.const 0) $f3) ;; CHECK: (elem declare func $f2) @@ -129,16 +184,14 @@ (func $f2 (type $indirect-type) (param $i32 i32) (global.set $g2 (local.get $i32)) ) - (func - (drop (ref.func $f2)) - ) - - - ;; CHECK: (func $0 (type $1) + ;; CHECK: (func $reference-f2 (type $1) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.func $f2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + (func $reference-f2 + (drop (ref.func $f2)) + ) ;; CHECK: (func $f3 (type $indirect-type) (param $i32 i32) ;; CHECK-NEXT: (global.set $g3 @@ -148,49 +201,110 @@ (func $f3 (type $indirect-type) (param $i32 i32) (global.set $g3 (local.get $i32)) ) - (elem $f3) + (elem (i32.const 0) $f3) ;; CHECK: (func $merges-multiple-effects (type $2) (param $ref (ref $indirect-type)) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (local $y i32) - ;; CHECK-NEXT: (local $z i32) - ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (local $l1 i32) + ;; CHECK-NEXT: (local $l2 i32) + ;; CHECK-NEXT: (local $l3 i32) + ;; CHECK-NEXT: (local $l4 i32) + ;; CHECK-NEXT: (local.set $l1 ;; CHECK-NEXT: (global.get $g1) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (local.set $l2 ;; CHECK-NEXT: (global.get $g2) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $l3 + ;; CHECK-NEXT: (global.get $g3) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (call_ref $indirect-type ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $l1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: (local.get $l2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (global.get $g3) + ;; CHECK-NEXT: (local.get $l3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g4) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $merges-multiple-effects (param $ref (ref $indirect-type)) - (local $x i32) - (local $y i32) - (local $z i32) + (local $l1 i32) + (local $l2 i32) + (local $l3 i32) + (local $l4 i32) - (local.set $x (global.get $g1)) - (local.set $y (global.get $g2)) - (local.set $z (global.get $g3)) + (local.set $l1 (global.get $g1)) + (local.set $l2 (global.get $g2)) + (local.set $l3 (global.get $g3)) + (local.set $l4 (global.get $g4)) - ;; This acts as a barrier for $x and $y, but not $z because - ;; $ref may write to $g1 (via $impl1) or $g2 (via $impl2) but not $g3. - ;; $z is optimized out and $x and $y are left alone. + ;; This acts as a barrier for $l1, $l2, and $l3 but not $l4. + ;; $ref may write to $g1 via $f1, or $g2 via $f2, $g3 via $f3 but not $g4. + ;; $l4 is optimized out and the others are left alone. (call_ref $indirect-type (i32.const 1) (local.get $ref)) - (drop (local.get $x)) - (drop (local.get $y)) - (drop (local.get $z)) + (drop (local.get $l1)) + (drop (local.get $l2)) + (drop (local.get $l3)) + (drop (local.get $l4)) + ) +) + +(module + ;; CHECK: (type $indirect-type (func (param i32))) + + ;; CHECK: (type $1 (func (param (ref $indirect-type)))) + + ;; CHECK: (import "" "" (func $imported-func (type $indirect-type) (param i32))) + (import "" "" (func $imported-func (type $indirect-type))) + + (type $indirect-type (func (param i32))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) + (global $g1 (mut i32) (i32.const 0)) + ;; CHECK: (global $g2 (mut i32) (i32.const 0)) + (global $g2 (mut i32) (i32.const 0)) + + ;; CHECK: (func $merges-multiple-effects (type $1) (param $ref (ref $indirect-type)) + ;; CHECK-NEXT: (local $l1 i32) + ;; CHECK-NEXT: (local $l2 i32) + ;; CHECK-NEXT: (local.set $l1 + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $l2 + ;; CHECK-NEXT: (global.get $g2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_ref $indirect-type + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $l1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $l2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $merges-multiple-effects (param $ref (ref $indirect-type)) + (local $l1 i32) + (local $l2 i32) + + (local.set $l1 (global.get $g1)) + (local.set $l2 (global.get $g2)) + + ;; This can flow to an import, so we have to assume that $g1 and $g2 could + ;; be mutated, and nothing can be optimized. + (call_ref $indirect-type (i32.const 1) (local.get $ref)) + + (drop (local.get $l1)) + (drop (local.get $l2)) ) ) From c88fe17144e59f3807728eb320f38c0342738b2f Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 27 May 2026 18:20:43 +0000 Subject: [PATCH 11/14] Fix test --- test/lit/passes/simplify-locals-global-effects-eh.wast | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/lit/passes/simplify-locals-global-effects-eh.wast b/test/lit/passes/simplify-locals-global-effects-eh.wast index f616208a9b1..2fed91f128a 100644 --- a/test/lit/passes/simplify-locals-global-effects-eh.wast +++ b/test/lit/passes/simplify-locals-global-effects-eh.wast @@ -75,21 +75,19 @@ ;; CHECK: (func $const (type $const-type) (result f32) ;; CHECK-NEXT: (f32.const 1) ;; CHECK-NEXT: ) - (func $const (type $const-type) + (func $const (export "const") (type $const-type) (f32.const 1) ) - (elem declare $const) ;; CHECK: (func $throws (type $throw-type) (result f64) ;; CHECK-NEXT: (throw $t) ;; CHECK-NEXT: (f64.const 1) ;; CHECK-NEXT: ) - (func $throws (type $throw-type) + (func $throws (export "throws") (type $throw-type) (throw $t) (f64.const 1) ) - (elem declare $throws) ;; CHECK: (func $read-g-with-nop-call-ref (type $5) (param $ref (ref null $const-type)) (result i32) ;; CHECK-NEXT: (local $x i32) From 2d637ddb035b4a805e42def03997a16e08d430dd Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 27 May 2026 18:33:26 +0000 Subject: [PATCH 12/14] Updates --- src/passes/GlobalEffects.cpp | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 67b2c27001e..d035d709a50 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -45,19 +45,18 @@ struct FuncInfo { std::unordered_set indirectCalledTypes; }; -/* - Only funcs that are 'addressed' may be the target of an indirect call. A - function is addressed if: - - It appears in a ref.func expression - - It appears in an `elem` segment (note that we already ignore `elem declare` - statements in our IR, but we check separately for funcs that appear in - `ref.func`). - - It's exported, because it may flow back to us as a reference. - - It's imported, which implies it is `elem declare`d. - - If a function doesn't meet any of these criteria, it can't be the target of - an indirect call and we don't need to include its effects in indirect calls. -*/ +// Only funcs that are 'addressed' may be the target of an indirect call. A +// function is addressed if: +// - It appears in a ref.func expression +// - It appears in an `elem` segment (note that we already ignore `elem declare` +// statements in our IR, but we check separately for funcs that appear in +// `ref.func`). +// - It's exported, because it may flow back to us as a reference. +// - It's imported, which implies it can be addressed (see +// https://github.com/WebAssembly/spec/issues/2072). +// +// If a function doesn't meet any of these criteria, it can't be the target of +// an indirect call and we don't need to include its effects in indirect calls. std::unordered_set getAddressedFuncs(Module& module) { struct AddressedFuncsWalker : WalkerPass> { std::unordered_set& addressedFuncs; @@ -65,6 +64,8 @@ std::unordered_set getAddressedFuncs(Module& module) { AddressedFuncsWalker(std::unordered_set& addressedFuncs) : addressedFuncs(addressedFuncs) {} + bool isFunctionParallel() override { return true; } + void visitRefFunc(RefFunc* refFunc) { addressedFuncs.insert(getModule()->getFunction(refFunc->func)); } @@ -88,11 +89,6 @@ std::unordered_set getAddressedFuncs(Module& module) { addressedFuncs.insert(module.getFunction(*export_->getInternalName())); } - ElementUtils::iterAllElementFunctionNames( - &module, [&addressedFuncs, &module](Name func) { - addressedFuncs.insert(module.getFunction(func)); - }); - return addressedFuncs; } From d3f907e636f2ca9cc3416e7c8bf02e4b4d226697 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 27 May 2026 18:43:51 +0000 Subject: [PATCH 13/14] Small change --- .../passes/global-effects-closed-world-simplify-locals.wast | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/global-effects-closed-world-simplify-locals.wast b/test/lit/passes/global-effects-closed-world-simplify-locals.wast index 49b1bc34878..0def74034c8 100644 --- a/test/lit/passes/global-effects-closed-world-simplify-locals.wast +++ b/test/lit/passes/global-effects-closed-world-simplify-locals.wast @@ -89,6 +89,7 @@ (drop (local.get $y)) (drop (local.get $z)) ) + ;; CHECK: (func $merges-effects-from-sub-only (type $3) (param $ref (ref $indirect-type-sub)) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local $y i32) @@ -260,14 +261,13 @@ (module ;; CHECK: (type $indirect-type (func (param i32))) + (type $indirect-type (func (param i32))) ;; CHECK: (type $1 (func (param (ref $indirect-type)))) ;; CHECK: (import "" "" (func $imported-func (type $indirect-type) (param i32))) (import "" "" (func $imported-func (type $indirect-type))) - (type $indirect-type (func (param i32))) - ;; CHECK: (global $g1 (mut i32) (i32.const 0)) (global $g1 (mut i32) (i32.const 0)) ;; CHECK: (global $g2 (mut i32) (i32.const 0)) From 7406ec2a277284e032404f1fb201837fbaf0bb60 Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 27 May 2026 18:45:22 +0000 Subject: [PATCH 14/14] Remove unneeded include --- src/passes/GlobalEffects.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index d035d709a50..525e0c87e52 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -20,7 +20,6 @@ // #include "ir/effects.h" -#include "ir/element-utils.h" #include "ir/module-utils.h" #include "pass.h" #include "support/graph_traversal.h"