From cb4046f76ade9d60689cb58d05cf8eaacf009f6f Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Tue, 21 Apr 2026 20:39:26 +0000 Subject: [PATCH 1/2] Account for global effects in LinearExecutionWalker --- src/ir/linear-execution.h | 31 ++++++++-- .../simplify-locals-global-effects-eh.wast | 57 +++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 test/lit/passes/simplify-locals-global-effects-eh.wast diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index e8b1923aacf..663d6d91969 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -80,11 +80,10 @@ struct LinearExecutionWalker : public PostWalker { static void scan(SubType* self, Expression** currp) { Expression* curr = *currp; - auto handleCall = [&](bool isReturn) { + auto handleCall = [&](bool mayThrow, bool isReturn) { if (!self->connectAdjacentBlocks) { - // Control is nonlinear if we return, or if EH is enabled or may be. - if (isReturn || !self->getModule() || - self->getModule()->features.hasExceptionHandling()) { + // Control is nonlinear if we return or throw. + if (isReturn || !self->getModule() || mayThrow) { self->pushTask(SubType::doNoteNonLinear, currp); } } @@ -153,11 +152,31 @@ struct LinearExecutionWalker : public PostWalker { break; } case Expression::Id::CallId: { - handleCall(curr->cast()->isReturn); + 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; + } + } + + handleCall(mayThrow, call->isReturn); return; } case Expression::Id::CallRefId: { - handleCall(curr->cast()->isReturn); + auto* callRef = curr->cast(); + + // TODO: Effect analysis for indirect calls isn't implemented yet. + // Assume any indirect call my throw for now. + bool mayThrow = !self->getModule() || + self->getModule()->features.hasExceptionHandling(); + + handleCall(mayThrow, callRef->isReturn); return; } 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 new file mode 100644 index 00000000000..41efe09a0c2 --- /dev/null +++ b/test/lit/passes/simplify-locals-global-effects-eh.wast @@ -0,0 +1,57 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt --enable-exception-handling --generate-global-effects --simplify-locals -S -o - | filecheck %s + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (tag $t (type $0)) + (tag $t) + + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $nop + ;; CHECK-NEXT: ) + (func $nop + ) + + ;; CHECK: (func $throws + ;; CHECK-NEXT: (throw $t) + ;; CHECK-NEXT: ) + (func $throws + (throw $t) + ) + + ;; CHECK: (func $read-g (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (call $nop) + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + (func $read-g (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; With --global-effects, we can tell that this doesn't throw, so it + ;; doesn't act as a barrier to optimize. The local is optimized away. + (call $nop) + (local.get $x) + ) + + ;; CHECK: (func $read-g-with-throw-in-between (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $throws) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g-with-throw-in-between (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; A potential throw halts our optimizations. + (call $throws) + + (local.get $x) + ) +) From 0e31a1879916baa7478a4c77e487daf32169881d Mon Sep 17 00:00:00 2001 From: stevenfontanella Date: Wed, 22 Apr 2026 17:04:14 +0000 Subject: [PATCH 2/2] PR updates --- src/ir/linear-execution.h | 23 +++- .../simplify-locals-global-effects-eh.wast | 120 +++++++++++++++++- 2 files changed, 131 insertions(+), 12 deletions(-) diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index 663d6d91969..167400a0137 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -82,8 +82,10 @@ struct LinearExecutionWalker : public PostWalker { auto handleCall = [&](bool mayThrow, bool isReturn) { if (!self->connectAdjacentBlocks) { - // Control is nonlinear if we return or throw. - if (isReturn || !self->getModule() || mayThrow) { + // 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 + // that's observable. + if (mayThrow || isReturn) { self->pushTask(SubType::doNoteNonLinear, currp); } } @@ -166,18 +168,29 @@ struct LinearExecutionWalker : public PostWalker { } handleCall(mayThrow, call->isReturn); - return; + break; } case Expression::Id::CallRefId: { auto* callRef = curr->cast(); // TODO: Effect analysis for indirect calls isn't implemented yet. - // Assume any indirect call my throw for now. + // Assume any indirect call may throw for now. bool mayThrow = !self->getModule() || self->getModule()->features.hasExceptionHandling(); handleCall(mayThrow, callRef->isReturn); - return; + 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); + break; } case Expression::Id::TryId: { self->pushTask(SubType::doVisitTry, currp); diff --git a/test/lit/passes/simplify-locals-global-effects-eh.wast b/test/lit/passes/simplify-locals-global-effects-eh.wast index 41efe09a0c2..ff11e7487b6 100644 --- a/test/lit/passes/simplify-locals-global-effects-eh.wast +++ b/test/lit/passes/simplify-locals-global-effects-eh.wast @@ -1,27 +1,26 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: foreach %s %t wasm-opt --enable-exception-handling --generate-global-effects --simplify-locals -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --closed-world --enable-exception-handling --enable-gc --enable-reference-types --generate-global-effects --simplify-locals -S -o - | filecheck %s (module ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) ;; CHECK: (tag $t (type $0)) (tag $t) - (global $g (mut i32) (i32.const 0)) - - ;; CHECK: (func $nop + ;; CHECK: (func $nop (type $0) ;; CHECK-NEXT: ) (func $nop ) - ;; CHECK: (func $throws + ;; CHECK: (func $throws (type $0) ;; CHECK-NEXT: (throw $t) ;; CHECK-NEXT: ) (func $throws (throw $t) ) - ;; CHECK: (func $read-g (result i32) + ;; CHECK: (func $read-g (type $1) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (call $nop) @@ -37,7 +36,7 @@ (local.get $x) ) - ;; CHECK: (func $read-g-with-throw-in-between (result i32) + ;; CHECK: (func $read-g-with-throw-in-between (type $1) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (global.get $g) @@ -55,3 +54,110 @@ (local.get $x) ) ) + +(module + ;; 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)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 2 2 funcref) + (table $t 2 2 funcref) + + ;; CHECK: (tag $t (type $2)) + (tag $t) + + ;; CHECK: (func $const (type $const-type) (result f32) + ;; CHECK-NEXT: (f32.const 1) + ;; CHECK-NEXT: ) + (func $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) + (throw $t) + (f64.const 1) + ) + (elem declare $throws) + + ;; CHECK: (func $read-g (type $3) (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: (drop + ;; CHECK-NEXT: (call_ref $const-type + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g (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. + (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-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_ref $throw-type + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g-with-throw-in-between (param $ref (ref $throw-type)) (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; Similar to above, except here we can tell that the indirect call may + ;; throw so optimization is halted. + (drop (call_ref $throw-type (local.get $ref))) + + (local.get $x) + ) + + ;; CHECK: (func $read-g-with-call-indirect-in-between (type $5) (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: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g-with-call-indirect-in-between (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))) + + (local.get $x) + ) +)