From dc53f9578f0a4ac270a0cde87bdcfe46da3a75df Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 20 May 2026 18:50:21 +0000 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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) ) - )