Files
clang-p2996/llvm/test/CodeGen/WebAssembly/exception.ll
Heejin Ahn 492812f613 [WebAssembly] Fix rethrow's index calculation (#114693)
So far we have assumed that we only rethrow the exception caught in the
innermost EH pad. This is true in code we directly generate, but after
inlining this may not be the case. For example, consider this code:
```ll
ehcleanup:
  %0 = cleanuppad ...
  call @destructor
  cleanupret from %0 unwind label %catch.dispatch
```

If `destructor` gets inlined into this function, the code can be like
```ll
ehcleanup:
  %0 = cleanuppad ...
  invoke @throwing_func
    to label %unreachale unwind label %catch.dispatch.i

catch.dispatch.i:
  catchswitch ... [ label %catch.start.i ]

catch.start.i:
  %1 = catchpad ...
  invoke @some_function
    to label %invoke.cont.i unwind label %terminate.i

invoke.cont.i:
  catchret from %1 to label %destructor.exit

destructor.exit:
  cleanupret from %0 unwind label %catch.dispatch
```

We lower a `cleanupret` into `rethrow`, which assumes it rethrows the
exception caught by the nearest dominating EH pad. But after the
inlining, the nearest dominating EH pad is not `ehcleanup` but
`catch.start.i`.

The problem exists in the same manner in the new (exnref) EH, because it
assumes the exception comes from the nearest EH pad and saves an exnref
from that EH pad and rethrows it (using `throw_ref`).

This problem can be fixed easily if `cleanupret` has the basic block
where its matching `cleanuppad` is. The bitcode instruction `cleanupret`
kind of has that info (it has a token from the `cleanuppad`), but that
info is lost when when we enter ISel, because `TargetSelectionDAG.td`'s
`cleanupret` node does not have any arguments:

5091a359d9/llvm/include/llvm/Target/TargetSelectionDAG.td (L700)
Note that `catchret` already has two basic block arguments, even though
neither of them means `catchpad`'s BB.

This PR adds the `cleanuppad`'s BB as an argument to `cleanupret` node
in ISel and uses it in the Wasm backend. Because this node is also used
in X86 backend we need to note its argument there too but nothing more
needs to change there as long as X86 doesn't need it.

---

- Details about changes in the Wasm backend:

After this PR, our pseudo `RETHROW` instruction takes a BB, which means
the EH pad whose exception it needs to rethrow. There are currently two
ways to generate a `RETHROW`: one is from `llvm.wasm.rethrow` intrinsic
and the other is from `CLEANUPRET` we discussed above. In case of
`llvm.wasm.rethrow`, we add a '0' as a placeholder argument when it is
lowered to a `RETHROW`, and change it to a BB in LateEHPrepare. As
written in the comments, this PR doesn't change how this BB is computed.
The BB argument will be converted to an immediate argument as with other
control flow instructions in CFGStackify.

In case of `CLEANUPRET`, it already has a BB argument pointing to an EH
pad, so it is just converted to a `RETHROW` with the same BB argument in
LateEHPrepare. This will also be lowered to an immediate in CFGStackify
with other control flow instructions.

---

Fixes #114600.
2024-11-05 21:45:13 -08:00

591 lines
20 KiB
LLVM

; RUN: llc < %s -asm-verbose=false -wasm-enable-eh -exception-model=wasm -mattr=+exception-handling -wasm-enable-exnref -verify-machineinstrs | FileCheck --implicit-check-not=ehgcr -allow-deprecated-dag-overlap %s
; RUN: llc < %s -asm-verbose=false -wasm-enable-eh -exception-model=wasm -mattr=+exception-handling -wasm-enable-exnref -verify-machineinstrs -O0
; RUN: llc < %s -wasm-enable-eh -exception-model=wasm -mattr=+exception-handling -wasm-enable-exnref
; RUN: llc < %s -wasm-enable-eh -exception-model=wasm -mattr=+exception-handling -wasm-enable-exnref -filetype=obj
target triple = "wasm32-unknown-unknown"
%struct.Temp = type { i8 }
@_ZTIi = external dso_local constant ptr
; CHECK: .tagtype __cpp_exception i32
; CHECK-LABEL: throw:
; CHECK: throw __cpp_exception
; CHECK-NOT: unreachable
define void @throw(ptr %p) {
call void @llvm.wasm.throw(i32 0, ptr %p)
ret void
}
; Simple test with a try-catch
;
; void foo();
; void catch() {
; try {
; foo();
; } catch (int) {
; }
; }
; CHECK-LABEL: catch:
; CHECK: global.get __stack_pointer
; CHECK: local.set 0
; CHECK: block
; CHECK: block () -> (i32, exnref)
; CHECK: try_table (catch_ref __cpp_exception 0)
; CHECK: call foo
; CHECK: br 2
; CHECK: end_try_table
; CHECK: end_block
; CHECK: local.set 2
; CHECK: local.get 0
; CHECK: global.set __stack_pointer
; CHECK: i32.store __wasm_lpad_context
; CHECK: call _Unwind_CallPersonality
; CHECK: block
; CHECK: br_if 0
; CHECK: call __cxa_begin_catch
; CHECK: call __cxa_end_catch
; CHECK: br 1
; CHECK: end_block
; CHECK: local.get 2
; CHECK: throw_ref
; CHECK: end_block
define void @catch() personality ptr @__gxx_wasm_personality_v0 {
entry:
invoke void @foo()
to label %try.cont unwind label %catch.dispatch
catch.dispatch: ; preds = %entry
%0 = catchswitch within none [label %catch.start] unwind to caller
catch.start: ; preds = %catch.dispatch
%1 = catchpad within %0 [ptr @_ZTIi]
%2 = call ptr @llvm.wasm.get.exception(token %1)
%3 = call i32 @llvm.wasm.get.ehselector(token %1)
%4 = call i32 @llvm.eh.typeid.for(ptr @_ZTIi)
%matches = icmp eq i32 %3, %4
br i1 %matches, label %catch, label %rethrow
catch: ; preds = %catch.start
%5 = call ptr @__cxa_begin_catch(ptr %2) [ "funclet"(token %1) ]
call void @__cxa_end_catch() [ "funclet"(token %1) ]
catchret from %1 to label %try.cont
rethrow: ; preds = %catch.start
call void @llvm.wasm.rethrow() [ "funclet"(token %1) ]
unreachable
try.cont: ; preds = %catch, %entry
ret void
}
; Destructor (cleanup) test
;
; void foo();
; struct Temp {
; ~Temp() {}
; };
; void cleanup() {
; Temp t;
; foo();
; }
; CHECK-LABEL: cleanup:
; CHECK: block
; CHECK: block exnref
; CHECK: try_table (catch_all_ref 0)
; CHECK: call foo
; CHECK: br 2
; CHECK: end_try_table
; CHECK: end_block
; CHECK: local.set 1
; CHECK: global.set __stack_pointer
; CHECK: call _ZN4TempD2Ev
; CHECK: local.get 1
; CHECK: throw_ref
; CHECK: end_block
; CHECK: call _ZN4TempD2Ev
define void @cleanup() personality ptr @__gxx_wasm_personality_v0 {
entry:
%t = alloca %struct.Temp, align 1
invoke void @foo()
to label %invoke.cont unwind label %ehcleanup
invoke.cont: ; preds = %entry
%call = call ptr @_ZN4TempD2Ev(ptr %t)
ret void
ehcleanup: ; preds = %entry
%0 = cleanuppad within none []
%call1 = call ptr @_ZN4TempD2Ev(ptr %t) [ "funclet"(token %0) ]
cleanupret from %0 unwind to caller
}
; Calling a function that may throw within a 'catch (...)' generates a
; terminatepad, because __cxa_end_catch() also can throw within 'catch (...)'.
;
; void foo();
; void terminatepad() {
; try {
; foo();
; } catch (...) {
; foo();
; }
; }
; CHECK-LABEL: terminatepad
; CHECK: block
; CHECK: block i32
; CHECK: try_table (catch __cpp_exception 0)
; CHECK: call foo
; CHECK: br 2
; CHECK: end_try_table
; CHECK: end_block
; CHECK: call __cxa_begin_catch
; CHECK: block
; CHECK: block exnref
; CHECK: try_table (catch_all_ref 0)
; CHECK: call foo
; CHECK: br 2
; CHECK: end_try_table
; CHECK: end_block
; CHECK: local.set 2
; CHECK: block
; CHECK: block
; CHECK: try_table (catch_all 0)
; CHECK: call __cxa_end_catch
; CHECK: br 2
; CHECK: end_try_table
; CHECK: end_block
; CHECK: call _ZSt9terminatev
; CHECK: unreachable
; CHECK: end_block
; CHECK: local.get 2
; CHECK: throw_ref
; CHECK: end_block
; CHECK: call __cxa_end_catch
; CHECK: end_block
define void @terminatepad() personality ptr @__gxx_wasm_personality_v0 {
entry:
invoke void @foo()
to label %try.cont unwind label %catch.dispatch
catch.dispatch: ; preds = %entry
%0 = catchswitch within none [label %catch.start] unwind to caller
catch.start: ; preds = %catch.dispatch
%1 = catchpad within %0 [ptr null]
%2 = call ptr @llvm.wasm.get.exception(token %1)
%3 = call i32 @llvm.wasm.get.ehselector(token %1)
%4 = call ptr @__cxa_begin_catch(ptr %2) [ "funclet"(token %1) ]
invoke void @foo() [ "funclet"(token %1) ]
to label %invoke.cont1 unwind label %ehcleanup
invoke.cont1: ; preds = %catch.start
call void @__cxa_end_catch() [ "funclet"(token %1) ]
catchret from %1 to label %try.cont
try.cont: ; preds = %invoke.cont1, %entry
ret void
ehcleanup: ; preds = %catch.start
%5 = cleanuppad within %1 []
invoke void @__cxa_end_catch() [ "funclet"(token %5) ]
to label %invoke.cont2 unwind label %terminate
invoke.cont2: ; preds = %ehcleanup
cleanupret from %5 unwind to caller
terminate: ; preds = %ehcleanup
%6 = cleanuppad within %5 []
call void @_ZSt9terminatev() [ "funclet"(token %6) ]
unreachable
}
; Tests prologues and epilogues are not generated within EH scopes.
; They should not be treated as funclets; BBs starting with a catch instruction
; should not have a prologue, and BBs ending with a catchret/cleanupret should
; not have an epilogue. This is separate from __stack_pointer restoring
; instructions after a catch instruction.
;
; void bar(int) noexcept;
; void no_prolog_epilog_in_ehpad() {
; int stack_var = 0;
; bar(stack_var);
; try {
; foo();
; } catch (int) {
; foo();
; }
; }
; CHECK-LABEL: no_prolog_epilog_in_ehpad
; CHECK: call bar
; CHECK: block
; CHECK: block () -> (i32, exnref)
; CHECK: try_table (catch_ref __cpp_exception 0)
; CHECK: call foo
; CHECK: br 2
; CHECK: end_try_table
; CHECK: end_block
; CHECK: local.set 2
; CHECK-NOT: global.get __stack_pointer
; CHECK: global.set __stack_pointer
; CHECK: block
; CHECK: block
; CHECK: br_if 0
; CHECK: call __cxa_begin_catch
; CHECK: block exnref
; CHECK: try_table (catch_all_ref 0)
; CHECK: call foo
; CHECK: br 3
; CHECK: end_try_table
; CHECK: end_block
; CHECK: local.set 2
; CHECK-NOT: global.get __stack_pointer
; CHECK: global.set __stack_pointer
; CHECK: call __cxa_end_catch
; CHECK: local.get 2
; CHECK: throw_ref
; CHECK-NOT: global.set __stack_pointer
; CHECK: end_block
; CHECK: local.get 2
; CHECK: throw_ref
; CHECK: end_block
; CHECK-NOT: global.set __stack_pointer
; CHECK: call __cxa_end_catch
; CHECK: end_block
define void @no_prolog_epilog_in_ehpad() personality ptr @__gxx_wasm_personality_v0 {
entry:
%stack_var = alloca i32, align 4
call void @bar(ptr %stack_var)
invoke void @foo()
to label %try.cont unwind label %catch.dispatch
catch.dispatch: ; preds = %entry
%0 = catchswitch within none [label %catch.start] unwind to caller
catch.start: ; preds = %catch.dispatch
%1 = catchpad within %0 [ptr @_ZTIi]
%2 = call ptr @llvm.wasm.get.exception(token %1)
%3 = call i32 @llvm.wasm.get.ehselector(token %1)
%4 = call i32 @llvm.eh.typeid.for(ptr @_ZTIi)
%matches = icmp eq i32 %3, %4
br i1 %matches, label %catch, label %rethrow
catch: ; preds = %catch.start
%5 = call ptr @__cxa_begin_catch(ptr %2) [ "funclet"(token %1) ]
%6 = load float, ptr %5, align 4
invoke void @foo() [ "funclet"(token %1) ]
to label %invoke.cont1 unwind label %ehcleanup
invoke.cont1: ; preds = %catch
call void @__cxa_end_catch() [ "funclet"(token %1) ]
catchret from %1 to label %try.cont
rethrow: ; preds = %catch.start
call void @llvm.wasm.rethrow() [ "funclet"(token %1) ]
unreachable
try.cont: ; preds = %invoke.cont1, %entry
ret void
ehcleanup: ; preds = %catch
%7 = cleanuppad within %1 []
call void @__cxa_end_catch() [ "funclet"(token %7) ]
cleanupret from %7 unwind to caller
}
; When a function does not have stack-allocated objects, it does not need to
; store SP back to __stack_pointer global at the epilog.
;
; void foo();
; void no_sp_writeback() {
; try {
; foo();
; } catch (...) {
; }
; }
; CHECK-LABEL: no_sp_writeback
; CHECK: block
; CHECK: block i32
; CHECK: try_table (catch __cpp_exception 0)
; CHECK: call foo
; CHECK: br 2
; CHECK: end_try_table
; CHECK: end_block
; CHECK: call __cxa_begin_catch
; CHECK: call __cxa_end_catch
; CHECK: end_block
; CHECK-NOT: global.set __stack_pointer
; CHECK: end_function
define void @no_sp_writeback() personality ptr @__gxx_wasm_personality_v0 {
entry:
invoke void @foo()
to label %try.cont unwind label %catch.dispatch
catch.dispatch: ; preds = %entry
%0 = catchswitch within none [label %catch.start] unwind to caller
catch.start: ; preds = %catch.dispatch
%1 = catchpad within %0 [ptr null]
%2 = call ptr @llvm.wasm.get.exception(token %1)
%3 = call i32 @llvm.wasm.get.ehselector(token %1)
%4 = call ptr @__cxa_begin_catch(ptr %2) [ "funclet"(token %1) ]
call void @__cxa_end_catch() [ "funclet"(token %1) ]
catchret from %1 to label %try.cont
try.cont: ; preds = %catch.start, %entry
ret void
}
; When the result of @llvm.wasm.get.exception is not used. This is created to
; fix a bug in LateEHPrepare and this should not crash.
define void @get_exception_wo_use() personality ptr @__gxx_wasm_personality_v0 {
entry:
invoke void @foo()
to label %try.cont unwind label %catch.dispatch
catch.dispatch: ; preds = %entry
%0 = catchswitch within none [label %catch.start] unwind to caller
catch.start: ; preds = %catch.dispatch
%1 = catchpad within %0 [ptr null]
%2 = call ptr @llvm.wasm.get.exception(token %1)
%3 = call i32 @llvm.wasm.get.ehselector(token %1)
catchret from %1 to label %try.cont
try.cont: ; preds = %catch.start, %entry
ret void
}
; Tests a case when a cleanup region (cleanuppad ~ clanupret) contains another
; catchpad
define void @complex_cleanup_region() personality ptr @__gxx_wasm_personality_v0 {
entry:
invoke void @foo()
to label %invoke.cont unwind label %ehcleanup
invoke.cont: ; preds = %entry
ret void
ehcleanup: ; preds = %entry
%0 = cleanuppad within none []
invoke void @foo() [ "funclet"(token %0) ]
to label %ehcleanupret unwind label %catch.dispatch
catch.dispatch: ; preds = %ehcleanup
%1 = catchswitch within %0 [label %catch.start] unwind label %ehcleanup.1
catch.start: ; preds = %catch.dispatch
%2 = catchpad within %1 [ptr null]
%3 = call ptr @llvm.wasm.get.exception(token %2)
%4 = call i32 @llvm.wasm.get.ehselector(token %2)
catchret from %2 to label %ehcleanupret
ehcleanup.1: ; preds = %catch.dispatch
%5 = cleanuppad within %0 []
unreachable
ehcleanupret: ; preds = %catch.start, %ehcleanup
cleanupret from %0 unwind to caller
}
; Regression test for the bug that 'rethrow' was not treated correctly as a
; terminator in isel.
define void @rethrow_terminator() personality ptr @__gxx_wasm_personality_v0 {
entry:
invoke void @foo()
to label %try.cont unwind label %catch.dispatch
catch.dispatch: ; preds = %entry
%0 = catchswitch within none [label %catch.start] unwind label %ehcleanup
catch.start: ; preds = %catch.dispatch
%1 = catchpad within %0 [ptr @_ZTIi]
%2 = call ptr @llvm.wasm.get.exception(token %1)
%3 = call i32 @llvm.wasm.get.ehselector(token %1)
%4 = call i32 @llvm.eh.typeid.for.p0(ptr @_ZTIi)
%matches = icmp eq i32 %3, %4
br i1 %matches, label %catch, label %rethrow
catch: ; preds = %catch.start
%5 = call ptr @__cxa_begin_catch(ptr %2) [ "funclet"(token %1) ]
%6 = load i32, ptr %5, align 4
call void @__cxa_end_catch() [ "funclet"(token %1) ]
catchret from %1 to label %try.cont
rethrow: ; preds = %catch.start
invoke void @llvm.wasm.rethrow() #1 [ "funclet"(token %1) ]
to label %unreachable unwind label %ehcleanup
try.cont: ; preds = %entry, %catch
ret void
ehcleanup: ; preds = %rethrow, %catch.dispatch
; 'rethrow' BB is this BB's predecessor, and its
; 'invoke void @llvm.wasm.rethrow()' is lowered down to a 'RETHROW' in Wasm
; MIR. And this 'phi' creates 'CONST_I32' instruction in the predecessor
; 'rethrow' BB. If 'RETHROW' is not treated correctly as a terminator, it can
; create a BB like
; bb.3.rethrow:
; RETHROW 0
; %0 = CONST_I32 20
; BR ...
%tmp = phi i32 [ 10, %catch.dispatch ], [ 20, %rethrow ]
%7 = cleanuppad within none []
call void @take_i32(i32 %tmp) [ "funclet"(token %7) ]
cleanupret from %7 unwind to caller
unreachable: ; preds = %rethrow
unreachable
}
; The bitcode below is generated when the code below is compiled and
; Temp::~Temp() is inlined into inlined_cleanupret():
;
; void inlined_cleanupret() {
; try {
; Temp t;
; throw 2;
; } catch (...)
; }
;
; Temp::~Temp() {
; try {
; throw 1;
; } catch (...) {
; }
; }
;
; ~Temp() generates cleanupret, which is lowered to a 'throw_ref' later. That
; throw_ref's argument should correctly target the top-level cleanuppad
; (catch_all_ref). This is a regression test for the bug where we did not
; compute throw_ref's argument correctly.
; CHECK-LABEL: inlined_cleanupret:
; CHECK: block exnref
; CHECK: block
; CHECK: block exnref
; CHECK: try_table (catch_all_ref 0)
; CHECK: call __cxa_throw
; CHECK: end_try_table
; CHECK: end_block
; try_table (catch_all_ref 0)'s caught exception is stored in local 2
; CHECK: local.set 2
; CHECK: block
; CHECK: try_table (catch_all 0)
; CHECK: block
; CHECK: block i32
; CHECK: try_table (catch __cpp_exception 0)
; CHECK: call __cxa_throw
; CHECK: end_try_table
; CHECK: end_block
; CHECK: call __cxa_end_catch
; CHECK: block i32
; CHECK: try_table (catch_all_ref 5)
; CHECK: try_table (catch __cpp_exception 1)
; Note that the throw_ref below targets the top-level catch_all_ref (local 2)
; CHECK: local.get 2
; CHECK: throw_ref
; CHECK: end_try_table
; CHECK: end_try_table
; CHECK: end_block
; CHECK: try_table (catch_all_ref 4)
; CHECK: call __cxa_end_catch
; CHECK: end_try_table
; CHECK: return
; CHECK: end_block
; CHECK: end_try_table
; CHECK: end_block
; CHECK: call _ZSt9terminatev
; CHECK: end_block
; CHECK: end_block
; CHECK: throw_ref
define void @inlined_cleanupret() personality ptr @__gxx_wasm_personality_v0 {
entry:
%exception = tail call ptr @__cxa_allocate_exception(i32 4)
store i32 2, ptr %exception, align 16
invoke void @__cxa_throw(ptr nonnull %exception, ptr nonnull @_ZTIi, ptr null)
to label %unreachable unwind label %ehcleanup
ehcleanup: ; preds = %entry
%0 = cleanuppad within none []
%exception.i = call ptr @__cxa_allocate_exception(i32 4) [ "funclet"(token %0) ]
store i32 1, ptr %exception.i, align 16
invoke void @__cxa_throw(ptr nonnull %exception.i, ptr nonnull @_ZTIi, ptr null) [ "funclet"(token %0) ]
to label %unreachable unwind label %catch.dispatch.i
catch.dispatch.i: ; preds = %ehcleanup
%1 = catchswitch within %0 [label %catch.start.i] unwind label %terminate.i
catch.start.i: ; preds = %catch.dispatch.i
%2 = catchpad within %1 [ptr null]
%3 = tail call ptr @llvm.wasm.get.exception(token %2)
%4 = tail call i32 @llvm.wasm.get.ehselector(token %2)
%5 = call ptr @__cxa_begin_catch(ptr %3) [ "funclet"(token %2) ]
invoke void @__cxa_end_catch() [ "funclet"(token %2) ]
to label %invoke.cont.i unwind label %terminate.i
invoke.cont.i: ; preds = %catch.start.i
catchret from %2 to label %_ZN4TempD2Ev.exit
terminate.i: ; preds = %catch.start.i, %catch.dispatch.i
%6 = cleanuppad within %0 []
call void @_ZSt9terminatev() [ "funclet"(token %6) ]
unreachable
_ZN4TempD2Ev.exit: ; preds = %invoke.cont.i
cleanupret from %0 unwind label %catch.dispatch
catch.dispatch: ; preds = %_ZN4TempD2Ev.exit
%7 = catchswitch within none [label %catch.start] unwind to caller
catch.start: ; preds = %catch.dispatch
%8 = catchpad within %7 [ptr null]
%9 = tail call ptr @llvm.wasm.get.exception(token %8)
%10 = tail call i32 @llvm.wasm.get.ehselector(token %8)
%11 = call ptr @__cxa_begin_catch(ptr %9) #8 [ "funclet"(token %8) ]
call void @__cxa_end_catch() [ "funclet"(token %8) ]
catchret from %8 to label %try.cont
try.cont: ; preds = %catch.start
ret void
unreachable: ; preds = %entry
unreachable
}
declare void @foo()
declare void @bar(ptr)
declare void @take_i32(i32)
declare i32 @__gxx_wasm_personality_v0(...)
; Function Attrs: noreturn
declare void @llvm.wasm.throw(i32, ptr) #1
; Function Attrs: nounwind
declare ptr @llvm.wasm.get.exception(token) #0
; Function Attrs: nounwind
declare i32 @llvm.wasm.get.ehselector(token) #0
; Function Attrs: noreturn
declare void @llvm.wasm.rethrow() #1
; Function Attrs: nounwind
declare i32 @llvm.eh.typeid.for(ptr) #0
; Function Attrs: nounwind
declare ptr @__cxa_allocate_exception(i32) #0
declare ptr @__cxa_begin_catch(ptr)
declare void @__cxa_end_catch()
; Function Attrs: noreturn
declare void @__cxa_throw(ptr, ptr, ptr) #1
declare void @_ZSt9terminatev()
declare ptr @_ZN4TempD2Ev(ptr returned)
attributes #0 = { nounwind }
attributes #1 = { noreturn }
; CHECK: __cpp_exception: