From 7b3e7d19de509cc9a1d8335dc0288f22a21f3f93 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 16 Sep 2026 21:13:17 -0700 Subject: [PATCH 1/2] [TailCall] Fix intrinsic and unreachable calls Late in the development of the TailCall pass, we removed a run of DCE in a nested pass runner. It turns out that had been load bearing, because without it TailCall could optimize unreachable calls whose callee return types did not match the caller return type, producing invalid IR. Fix the bug by not optimizing unreachable calls, leaving them for a separate DCE pass to clean up instead. Also fix the lowering of call.without.effects when it is used as a tail call. Previously intrinsic lowering would turn such a tail call into a normal call, again producing invalid IR because the normal call is not unreachable and is not valid in all the places a tail call would be. --- src/passes/Intrinsics.cpp | 6 +- src/passes/TailCall.cpp | 6 +- test/lit/passes/intrinsic-lowering.wast | 26 ++++- test/lit/passes/tail-call.wast | 128 ++++++++++++++++++++---- 4 files changed, 139 insertions(+), 27 deletions(-) diff --git a/src/passes/Intrinsics.cpp b/src/passes/Intrinsics.cpp index faba23816bb..97a7936e987 100644 --- a/src/passes/Intrinsics.cpp +++ b/src/passes/Intrinsics.cpp @@ -39,9 +39,11 @@ struct IntrinsicLowering : public WalkerPass> { // forgets to optimize. Builder builder(*getModule()); if (auto* refFunc = target->dynCast()) { - replaceCurrent(builder.makeCall(refFunc->func, operands, curr->type)); + replaceCurrent(builder.makeCall( + refFunc->func, operands, curr->type, curr->isReturn)); } else { - replaceCurrent(builder.makeCallRef(target, operands, curr->type)); + replaceCurrent( + builder.makeCallRef(target, operands, curr->type, curr->isReturn)); } } } diff --git a/src/passes/TailCall.cpp b/src/passes/TailCall.cpp index 9f7a8581ace..6a1b58e3505 100644 --- a/src/passes/TailCall.cpp +++ b/src/passes/TailCall.cpp @@ -205,8 +205,10 @@ struct TailCall : public WalkerPass> { template void handleCall(CallType* call, bool isTail) { // A call in tail position can have a type incompatible with the function's // return type if it is dead code at the end of a block following an earlier - // unreachable instruction. - if (call->isReturn || !isTail || + // unreachable instruction. Also avoid optimizing unreachable calls (e.g. + // calls with unreachable operands), since their callee return type may not + // match the caller return type. + if (call->isReturn || !isTail || call->type == Type::unreachable || !Type::isSubType(call->type, getFunction()->getResults())) { return; } diff --git a/test/lit/passes/intrinsic-lowering.wast b/test/lit/passes/intrinsic-lowering.wast index e8897ff0b25..51ae5788c73 100644 --- a/test/lit/passes/intrinsic-lowering.wast +++ b/test/lit/passes/intrinsic-lowering.wast @@ -6,18 +6,18 @@ (type $none (func)) ;; call.without.effects with no params. - ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-v (type $1) (param funcref) (result i32))) + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-v (type $3) (param funcref) (result i32))) (import "binaryen-intrinsics" "call.without.effects" (func $cwe-v (param funcref) (result i32))) ;; call.without.effects with some params. - ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-dif (type $2) (param f64 i32 funcref) (result f32))) + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-dif (type $4) (param f64 i32 funcref) (result f32))) (import "binaryen-intrinsics" "call.without.effects" (func $cwe-dif (param f64) (param i32) (param funcref) (result f32))) ;; call.without.effects with no result. - ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-n (type $3) (param funcref))) + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe-n (type $5) (param funcref))) (import "binaryen-intrinsics" "call.without.effects" (func $cwe-n (param funcref))) - ;; CHECK: (func $test (type $4) (param $none (ref null $none)) + ;; CHECK: (func $test (type $1) (param $none (ref null $none)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (call $make-i32) ;; CHECK-NEXT: ) @@ -39,7 +39,7 @@ (call $cwe-n (local.get $none)) ) - ;; CHECK: (func $make-i32 (type $5) (result i32) + ;; CHECK: (func $make-i32 (type $2) (result i32) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) (func $make-i32 (result i32) @@ -53,4 +53,20 @@ ;; Helper function for the above. (unreachable) ) + + ;; CHECK: (func $test-return-call (type $2) (result i32) + ;; CHECK-NEXT: (return_call $make-i32) + ;; CHECK-NEXT: ) + (func $test-return-call (result i32) + (return_call $cwe-v (ref.func $make-i32)) + ) + + ;; CHECK: (func $test-return-call-ref (type $1) (param $none (ref null $none)) + ;; CHECK-NEXT: (return_call_ref $none + ;; CHECK-NEXT: (local.get $none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test-return-call-ref (param $none (ref null $none)) + (return_call $cwe-n (local.get $none)) + ) ) diff --git a/test/lit/passes/tail-call.wast b/test/lit/passes/tail-call.wast index 67ea18728f0..2a151f37733 100644 --- a/test/lit/passes/tail-call.wast +++ b/test/lit/passes/tail-call.wast @@ -6,11 +6,16 @@ (module ;; CHECK: (type $none-to-i32 (func (result i32))) + ;; CHECK: (type $i32-to-none (func (param i32))) + ;; CHECK: (type $none-to-none (func)) (type $none-to-none (func)) (type $none-to-i32 (func (result i32))) + (type $i32-to-none (func (param i32))) ;; CHECK: (type $struct (sub (struct))) (type $struct (sub (struct))) + ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $cwe (type $6) (param i32 funcref) (result i32))) + (import "binaryen-intrinsics" "call.without.effects" (func $cwe (param i32) (param funcref) (result i32))) ;; CHECK: (global $g (mut i32) (i32.const 0)) (global $g (mut i32) (i32.const 0)) ;; CHECK: (table $table 1 funcref) @@ -19,6 +24,15 @@ ;; CHECK: (func $void-callee (type $none-to-none) ;; CHECK-NEXT: ) (func $void-callee) + ;; CHECK: (func $void-arg-callee (type $i32-to-none) (param $x i32) + ;; CHECK-NEXT: ) + (func $void-arg-callee (param $x i32)) + ;; CHECK: (func $i32-to-i32-callee (type $2) (param $x i32) (result i32) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $i32-to-i32-callee (param $x i32) (result i32) + (local.get $x) + ) ;; CHECK: (func $value-callee (type $none-to-i32) (result i32) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) @@ -31,7 +45,7 @@ (func $ref-callee (result i32) (i32.const 2) ) - ;; CHECK: (func $param-callee (type $6) (param $x i32) (param $y i32) (result i32) + ;; CHECK: (func $param-callee (type $7) (param $x i32) (param $y i32) (result i32) ;; CHECK-NEXT: (i32.add ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: (local.get $y) @@ -43,7 +57,7 @@ (local.get $y) ) ) - ;; CHECK: (func $subtype-callee (type $7) (result (ref $struct)) + ;; CHECK: (func $subtype-callee (type $8) (result (ref $struct)) ;; CHECK-NEXT: (struct.new_default $struct) ;; CHECK-NEXT: ) (func $subtype-callee (result (ref $struct)) @@ -93,7 +107,7 @@ ) ) - ;; CHECK: (func $call-with-params (type $1) (param $x i32) (result i32) + ;; CHECK: (func $call-with-params (type $2) (param $x i32) (result i32) ;; CHECK-NEXT: (return ;; CHECK-NEXT: (return_call $param-callee ;; CHECK-NEXT: (local.get $x) @@ -123,7 +137,7 @@ ) ) - ;; CHECK: (func $subtype-return (type $8) (result anyref) + ;; CHECK: (func $subtype-return (type $9) (result anyref) ;; CHECK-NEXT: (return_call $subtype-callee) ;; CHECK-NEXT: ) (func $subtype-return (result anyref) @@ -140,7 +154,7 @@ (call $multivalue-callee) ) - ;; CHECK: (func $conditional (type $1) (param $condition i32) (result i32) + ;; CHECK: (func $conditional (type $2) (param $condition i32) (result i32) ;; CHECK-NEXT: (if ;; CHECK-NEXT: (local.get $condition) ;; CHECK-NEXT: (then @@ -164,7 +178,7 @@ ) ) - ;; CHECK: (func $one-armed-if (type $3) (param $condition i32) + ;; CHECK: (func $one-armed-if (type $i32-to-none) (param $condition i32) ;; CHECK-NEXT: (if ;; CHECK-NEXT: (local.get $condition) ;; CHECK-NEXT: (then @@ -207,7 +221,7 @@ ) ) - ;; CHECK: (func $break (type $1) (param $condition i32) (result i32) + ;; CHECK: (func $break (type $2) (param $condition i32) (result i32) ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (if ;; CHECK-NEXT: (local.get $condition) @@ -234,7 +248,7 @@ ) ) - ;; CHECK: (func $return-break (type $1) (param $condition i32) (result i32) + ;; CHECK: (func $return-break (type $2) (param $condition i32) (result i32) ;; CHECK-NEXT: (return ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (if @@ -265,7 +279,7 @@ ) ) - ;; CHECK: (func $br-if-tail (type $1) (param $condition i32) (result i32) + ;; CHECK: (func $br-if-tail (type $2) (param $condition i32) (result i32) ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (block ;; CHECK-NEXT: (return_call $value-callee) @@ -287,7 +301,7 @@ ) ) - ;; CHECK: (func $br-if-not-tail (type $1) (param $condition i32) (result i32) + ;; CHECK: (func $br-if-not-tail (type $2) (param $condition i32) (result i32) ;; CHECK-NEXT: (block $out (result i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (br_if $out @@ -340,7 +354,7 @@ ) ) - ;; CHECK: (func $br-table-all-tail (type $1) (param $idx i32) (result i32) + ;; CHECK: (func $br-table-all-tail (type $2) (param $idx i32) (result i32) ;; CHECK-NEXT: (block $out1 ;; CHECK-NEXT: (block $out2 ;; CHECK-NEXT: (block @@ -364,7 +378,7 @@ ) ) - ;; CHECK: (func $br-table-not-all-tail (type $1) (param $idx i32) (result i32) + ;; CHECK: (func $br-table-not-all-tail (type $2) (param $idx i32) (result i32) ;; CHECK-NEXT: (block $exit (result i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block $not-exit (result i32) @@ -392,7 +406,7 @@ ) ) - ;; CHECK: (func $void-break (type $3) (param $condition i32) + ;; CHECK: (func $void-break (type $i32-to-none) (param $condition i32) ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (if ;; CHECK-NEXT: (local.get $condition) @@ -419,7 +433,7 @@ ) ) - ;; CHECK: (func $void-br-if-tail (type $3) (param $condition i32) + ;; CHECK: (func $void-br-if-tail (type $i32-to-none) (param $condition i32) ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (return_call $void-callee) ;; CHECK-NEXT: (br_if $out @@ -438,7 +452,7 @@ ) ) - ;; CHECK: (func $void-br-if-not-tail (type $3) (param $condition i32) + ;; CHECK: (func $void-br-if-not-tail (type $i32-to-none) (param $condition i32) ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (call $void-callee) ;; CHECK-NEXT: (br_if $out @@ -459,7 +473,7 @@ ) ) - ;; CHECK: (func $void-br-table-all-tail (type $3) (param $idx i32) + ;; CHECK: (func $void-br-table-all-tail (type $i32-to-none) (param $idx i32) ;; CHECK-NEXT: (block $out1 ;; CHECK-NEXT: (block $out2 ;; CHECK-NEXT: (return_call $void-callee) @@ -482,7 +496,7 @@ ) ) - ;; CHECK: (func $void-br-table-not-all-tail (type $3) (param $idx i32) + ;; CHECK: (func $void-br-table-not-all-tail (type $i32-to-none) (param $idx i32) ;; CHECK-NEXT: (block $exit ;; CHECK-NEXT: (block $not-exit ;; CHECK-NEXT: (call $void-callee) @@ -507,7 +521,7 @@ ) ) - ;; CHECK: (func $loop-tail (type $1) (param $condition i32) (result i32) + ;; CHECK: (func $loop-tail (type $2) (param $condition i32) (result i32) ;; CHECK-NEXT: (loop $l ;; CHECK-NEXT: (br_if $l ;; CHECK-NEXT: (local.get $condition) @@ -579,4 +593,82 @@ (call $void-callee) ) ) + + ;; CHECK: (func $unreachable-arg-mismatched-call (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (call $void-arg-callee + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreachable-arg-mismatched-call (result i32) + ;; Unreachable calls (e.g. due to an unreachable operand) are not converted + ;; to return calls, avoiding return type mismatches and optimizing dead code. + (call $void-arg-callee + (unreachable) + ) + ) + + ;; CHECK: (func $unreachable-arg-matching-call (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (call $param-callee + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreachable-arg-matching-call (result i32) + (call $param-callee + (unreachable) + (i32.const 1) + ) + ) + + ;; CHECK: (func $unreachable-arg-mismatched-call-indirect (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (call_indirect $table (type $i32-to-none) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreachable-arg-mismatched-call-indirect (result i32) + (call_indirect $table (type $i32-to-none) + (unreachable) + (i32.const 0) + ) + ) + + ;; CHECK: (func $unreachable-arg-mismatched-call-ref (type $10) (param $f (ref $i32-to-none)) (result i32) + ;; CHECK-NEXT: (call_ref $i32-to-none + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (local.get $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreachable-arg-mismatched-call-ref (param $f (ref $i32-to-none)) (result i32) + (call_ref $i32-to-none + (unreachable) + (local.get $f) + ) + ) + + ;; CHECK: (func $unreachable-arg-mismatched-cwe (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (call $cwe + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (ref.func $void-arg-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreachable-arg-mismatched-cwe (result i32) + (call $cwe + (unreachable) + (ref.func $void-arg-callee) + ) + ) + + ;; CHECK: (func $tail-cwe (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (return_call $cwe + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (ref.func $i32-to-i32-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $tail-cwe (result i32) + (call $cwe + (i32.const 42) + (ref.func $i32-to-i32-callee) + ) + ) ) From 6a2132202825ecbf34e19c8aaffcf8586969557e Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 17 Sep 2026 09:26:12 -0700 Subject: [PATCH 2/2] Update test/lit/passes/tail-call.wast Co-authored-by: Alon Zakai --- test/lit/passes/tail-call.wast | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lit/passes/tail-call.wast b/test/lit/passes/tail-call.wast index 2a151f37733..c0c8bc56df0 100644 --- a/test/lit/passes/tail-call.wast +++ b/test/lit/passes/tail-call.wast @@ -666,6 +666,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $tail-cwe (result i32) + ;; call.without.effects is optimized when reachable. (call $cwe (i32.const 42) (ref.func $i32-to-i32-callee)