diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index 167400a0137..9e69405ff7c 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -80,7 +80,11 @@ struct LinearExecutionWalker : public PostWalker { static void scan(SubType* self, Expression** currp) { Expression* curr = *currp; - auto handleCall = [&](bool mayThrow, bool isReturn) { + auto handleCall = [&](bool isReturn, bool refutesThrowEffect) { + 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 +160,54 @@ 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; + 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 && func->effects) { + refutesThrowEffect = !func->effects->throws_; } } - handleCall(mayThrow, call->isReturn); + handleCall(call->isReturn, refutesThrowEffect); 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(); + bool refutesThrowEffect = [&]() { + if (!self->getModule()) { + return false; + } + if (!callRef->target->type.isRef()) { + // This is an unreachable, so no throws effect. + return true; + } - handleCall(mayThrow, callRef->isReturn); + auto* effects = find_or_null(self->getModule()->indirectCallEffects, + callRef->target->type.getHeapType()); + if (!effects) { + return false; + } + return !(*effects)->throws_; + }(); + + handleCall(callRef->isReturn, refutesThrowEffect); 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); + bool refutesThrowEffect = false; + if (self->getModule()) { + if (auto* effects = find_or_null( + self->getModule()->indirectCallEffects, callIndirect->heapType); + effects) { + refutesThrowEffect = !(*effects)->throws_; + } + } + handleCall(callIndirect->isReturn, refutesThrowEffect); 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..f616208a9b1 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 $4)) (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 $5) (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 $2) (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 $3) (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,52 @@ (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 $3) (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) + ) + + ;; 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) )