blob: c0c8bc56df06619fbbc60fe66255d910ddd88665 [file]
;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
;; Test converting calls in tail position to return calls.
;; RUN: wasm-opt %s --tail-call --enable-tail-call --enable-gc --enable-reference-types --enable-multivalue -S -o - | filecheck %s
(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)
(table $table 1 funcref)
;; 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: )
(func $value-callee (result i32)
(i32.const 1)
)
;; CHECK: (func $ref-callee (type $none-to-i32) (result i32)
;; CHECK-NEXT: (i32.const 2)
;; CHECK-NEXT: )
(func $ref-callee (result i32)
(i32.const 2)
)
;; 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)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $param-callee (param $x i32) (param $y i32) (result i32)
(i32.add
(local.get $x)
(local.get $y)
)
)
;; CHECK: (func $subtype-callee (type $8) (result (ref $struct))
;; CHECK-NEXT: (struct.new_default $struct)
;; CHECK-NEXT: )
(func $subtype-callee (result (ref $struct))
(struct.new_default $struct)
)
;; CHECK: (func $multivalue-callee (type $5) (result i32 i64)
;; CHECK-NEXT: (tuple.make 2
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: (i64.const 2)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $multivalue-callee (result i32 i64)
(tuple.make 2
(i32.const 1)
(i64.const 2)
)
)
(elem (i32.const 0) $value-callee)
;; CHECK: (func $direct-void (type $none-to-none)
;; CHECK-NEXT: (return_call $void-callee)
;; CHECK-NEXT: )
(func $direct-void
;; A direct void call at the end of a function becomes a return_call.
(call $void-callee)
)
;; CHECK: (func $direct-void-ret (type $none-to-none)
;; CHECK-NEXT: (return_call $void-callee)
;; CHECK-NEXT: (return)
;; CHECK-NEXT: )
(func $direct-void-ret
;; A void call immediately preceding a return becomes a return_call.
(call $void-callee)
(return)
)
;; CHECK: (func $direct-value (type $none-to-i32) (result i32)
;; CHECK-NEXT: (return
;; CHECK-NEXT: (return_call $value-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $direct-value (result i32)
;; A return wrapping a call becomes a return_call.
(return
(call $value-callee)
)
)
;; 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)
;; CHECK-NEXT: (block (result i32)
;; CHECK-NEXT: (global.set $g
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (call $value-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $call-with-params (param $x i32) (result i32)
;; Calls with parameters, including side-effecting parameter expressions,
;; are still converted to return_call because operands are evaluated before
;; the call transfers control.
(return
(call $param-callee
(local.get $x)
(block (result i32)
(global.set $g
(i32.const 1)
)
(call $value-callee)
)
)
)
)
;; CHECK: (func $subtype-return (type $9) (result anyref)
;; CHECK-NEXT: (return_call $subtype-callee)
;; CHECK-NEXT: )
(func $subtype-return (result anyref)
;; A call returning a subtype of the enclosing function's result type can be
;; converted to return_call.
(call $subtype-callee)
)
;; CHECK: (func $multivalue (type $5) (result i32 i64)
;; CHECK-NEXT: (return_call $multivalue-callee)
;; CHECK-NEXT: )
(func $multivalue (result i32 i64)
;; Multivalue return calls are supported.
(call $multivalue-callee)
)
;; CHECK: (func $conditional (type $2) (param $condition i32) (result i32)
;; CHECK-NEXT: (if
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: (then
;; CHECK-NEXT: (return_call $value-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: (else
;; CHECK-NEXT: (return_call $ref-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $conditional (param $condition i32) (result i32)
;; Both arms of an if in tail position are converted to return_call.
(if (result i32)
(local.get $condition)
(then
(call $value-callee)
)
(else
(call $ref-callee)
)
)
)
;; CHECK: (func $one-armed-if (type $i32-to-none) (param $condition i32)
;; CHECK-NEXT: (if
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: (then
;; CHECK-NEXT: (return_call $void-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $one-armed-if (param $condition i32)
;; In a void function, the arm of a one-armed if in tail position is
;; converted to return_call.
(if
(local.get $condition)
(then
(call $void-callee)
)
)
)
;; CHECK: (func $indirect (type $none-to-i32) (result i32)
;; CHECK-NEXT: (return_call_indirect $table (type $none-to-i32)
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $indirect (result i32)
;; An indirect call in tail position becomes a return_call_indirect.
(call_indirect $table (type $none-to-i32)
(i32.const 0)
)
)
;; CHECK: (func $ref (type $none-to-i32) (result i32)
;; CHECK-NEXT: (return_call_ref $none-to-i32
;; CHECK-NEXT: (ref.func $value-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $ref (result i32)
;; A call_ref in tail position becomes a return_call_ref.
(call_ref $none-to-i32
(ref.func $value-callee)
)
)
;; CHECK: (func $break (type $2) (param $condition i32) (result i32)
;; CHECK-NEXT: (block $out
;; CHECK-NEXT: (if
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: (then
;; CHECK-NEXT: (return_call $value-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (return_call $ref-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $break (param $condition i32) (result i32)
;; An unconditional branch to a tail block and the fallthrough of that block
;; are both in tail position.
(block $out (result i32)
(if
(local.get $condition)
(then
(br $out
(call $value-callee)
)
)
)
(call $ref-callee)
)
)
;; CHECK: (func $return-break (type $2) (param $condition i32) (result i32)
;; CHECK-NEXT: (return
;; CHECK-NEXT: (block $out
;; CHECK-NEXT: (if
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: (then
;; CHECK-NEXT: (return_call $value-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (return_call $ref-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $return-break (param $condition i32) (result i32)
;; A block wrapped in an explicit return is in tail position, so branches to
;; it are also in tail position.
(return
(block $out (result i32)
(if
(local.get $condition)
(then
(br $out
(call $value-callee)
)
)
)
(call $ref-callee)
)
)
)
;; 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)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $br-if-tail (param $condition i32) (result i32)
;; A conditional branch in tail position to a tail block is converted
;; because both taking the branch and falling through exit the function with
;; the call's result.
(block $out (result i32)
(br_if $out
(call $value-callee)
(local.get $condition)
)
)
)
;; 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
;; CHECK-NEXT: (call $value-callee)
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (i32.const 42)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $br-if-not-tail (param $condition i32) (result i32)
;; A conditional branch NOT in tail position must not be converted.
(block $out (result i32)
(drop
(br_if $out
(call $value-callee)
(local.get $condition)
)
)
(i32.const 42)
)
)
;; CHECK: (func $br-if-side-effect (type $none-to-i32) (result i32)
;; CHECK-NEXT: (block $out (result i32)
;; CHECK-NEXT: (br_if $out
;; CHECK-NEXT: (call $value-callee)
;; CHECK-NEXT: (block (result i32)
;; CHECK-NEXT: (global.set $g
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $br-if-side-effect (result i32)
;; A conditional branch in tail position whose condition has side effects
;; must not be converted because converting would skip the side effect.
(block $out (result i32)
(br_if $out
(call $value-callee)
(block (result i32)
(global.set $g
(i32.const 1)
)
(i32.const 0)
)
)
)
)
;; CHECK: (func $br-table-all-tail (type $2) (param $idx i32) (result i32)
;; CHECK-NEXT: (block $out1
;; CHECK-NEXT: (block $out2
;; CHECK-NEXT: (block
;; CHECK-NEXT: (return_call $value-callee)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $idx)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $br-table-all-tail (param $idx i32) (result i32)
;; A br_table where all targets exit the function can be converted.
(block $out1 (result i32)
(block $out2 (result i32)
(br_table $out1 $out2
(call $value-callee)
(local.get $idx)
)
)
)
)
;; 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)
;; CHECK-NEXT: (br_table $not-exit $exit
;; CHECK-NEXT: (call $value-callee)
;; CHECK-NEXT: (local.get $idx)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (i32.const 42)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $br-table-not-all-tail (param $idx i32) (result i32)
;; A br_table where not all targets exit the function must not be converted.
(block $exit (result i32)
(drop
(block $not-exit (result i32)
(br_table $not-exit $exit
(call $value-callee)
(local.get $idx)
)
)
)
(i32.const 42)
)
)
;; CHECK: (func $void-break (type $i32-to-none) (param $condition i32)
;; CHECK-NEXT: (block $out
;; CHECK-NEXT: (if
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: (then
;; CHECK-NEXT: (return_call $void-callee)
;; CHECK-NEXT: (br $out)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (return_call $void-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $void-break (param $condition i32)
;; In a void function, statements immediately preceding an unconditional
;; branch to a tail block or falling through a tail block are converted.
(block $out
(if
(local.get $condition)
(then
(call $void-callee)
(br $out)
)
)
(call $void-callee)
)
)
;; 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
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $void-br-if-tail (param $condition i32)
;; In a void function, a call preceding a conditional branch in tail
;; position targeting a tail block is converted.
(block $out
(call $void-callee)
(br_if $out
(local.get $condition)
)
)
)
;; 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
;; CHECK-NEXT: (local.get $condition)
;; CHECK-NEXT: )
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $void-br-if-not-tail (param $condition i32)
;; In a void function, a call preceding a conditional branch that is not in
;; tail position must not be converted.
(block $out
(call $void-callee)
(br_if $out
(local.get $condition)
)
(nop)
)
)
;; 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)
;; CHECK-NEXT: (br_table $out1 $out2
;; CHECK-NEXT: (local.get $idx)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $void-br-table-all-tail (param $idx i32)
;; In a void function, a call preceding a br_table whose targets are all tail
;; blocks is converted.
(block $out1
(block $out2
(call $void-callee)
(br_table $out1 $out2
(local.get $idx)
)
)
)
)
;; 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)
;; CHECK-NEXT: (br_table $not-exit $exit
;; CHECK-NEXT: (local.get $idx)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $void-br-table-not-all-tail (param $idx i32)
;; In a void function, a call preceding a br_table where not all targets are
;; tail blocks must not be converted.
(block $exit
(block $not-exit
(call $void-callee)
(br_table $not-exit $exit
(local.get $idx)
)
)
(nop)
)
)
;; 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)
;; CHECK-NEXT: )
;; CHECK-NEXT: (return_call $value-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $loop-tail (param $condition i32) (result i32)
;; A loop whose fallthrough is in tail position converts its fallthrough call
;; to a return_call.
(loop $l (result i32)
(br_if $l
(local.get $condition)
)
(call $value-callee)
)
)
;; CHECK: (func $loop-backedge-not-tail (type $none-to-none)
;; CHECK-NEXT: (loop $l
;; CHECK-NEXT: (call $void-callee)
;; CHECK-NEXT: (br $l)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $loop-backedge-not-tail
;; A branch to a loop header is a backedge rather than an exit, so a call
;; preceding a loop branch must not be converted.
(loop $l
(call $void-callee)
(br $l)
)
)
;; CHECK: (func $not-tail (type $none-to-none)
;; CHECK-NEXT: (call $void-callee)
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: )
(func $not-tail
;; A call followed by another instruction is not in tail position.
(call $void-callee)
(nop)
)
;; CHECK: (func $mismatched-result (type $none-to-none)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (call $value-callee)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $mismatched-result
;; A dropped call is not in tail position.
(drop
(call $value-callee)
)
)
;; CHECK: (func $dead-after-return (type $none-to-i32) (result i32)
;; CHECK-NEXT: (return
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (call $void-callee)
;; CHECK-NEXT: )
(func $dead-after-return (result i32)
;; A dead void call at the end of a block after an unreachable instruction
;; has a mismatched return type and is safely ignored.
(block
(return
(i32.const 1)
)
(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.without.effects is optimized when reachable.
(call $cwe
(i32.const 42)
(ref.func $i32-to-i32-callee)
)
)
)