CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
1588 lines
57 KiB
Markdown
1588 lines
57 KiB
Markdown
# ClangIR Cleanup and Exception Handling Design
|
|
|
|
```{contents}
|
|
---
|
|
local:
|
|
---
|
|
```
|
|
|
|
## Overview
|
|
|
|
This document describes the design for C++ cleanups and exception
|
|
handling representation and lowering in the CIR dialect. The initial CIR
|
|
generation will follow the general structure of the cleanup and
|
|
exception handling code in Clang's LLVM IR generation. In particular,
|
|
we will continue to use the `EHScopeStack` with pushing and popping of
|
|
`EHScopeStack::Cleanup` objects to drive the creation of cleanup scopes
|
|
within CIR.
|
|
|
|
However, the LLVM IR generated by Clang is fundamentally unstructured
|
|
and therefore isn't well suited to the goals of CIR. Therefore, we are
|
|
proposing a high-level representation that follows MLIR's structured
|
|
control flow model.
|
|
|
|
The `cir::LowerCFG` pass will lower this high-level representation to a
|
|
different form where control flow is block-based and explicit. This form
|
|
will more closely resemble the LLVM IR used when Clang is generating
|
|
LLVM IR directly. However, this form will still be ABI-agnostic.
|
|
|
|
An additional pass will be introduced to lower the flattened form to an
|
|
ABI-specific representation. This ABI-specific form will have a direct
|
|
correspondence to the LLVM IR exception handling representation for a
|
|
given target.
|
|
|
|
## High-level CIR representation
|
|
|
|
### Normal and EH cleanups
|
|
|
|
Scopes that require normal or EH cleanup will be represented using a new
|
|
operation, `cir.cleanup.scope`.
|
|
|
|
```
|
|
cir.cleanup.scope {
|
|
// body region
|
|
} cleanup [normal|eh|all] {
|
|
// cleanup instructions
|
|
}
|
|
```
|
|
|
|
Execution begins with the first operation in the body region and
|
|
continues according to normal control flow semantics until a terminating
|
|
operation (`cir.yield`, `cir.break`, `cir.return`, `cir.continue`) is
|
|
encountered or an exception is thrown.
|
|
|
|
If the cleanup region is marked as `eh_only`, normal control flow exits
|
|
from the body region skip the cleanup region and continue to their
|
|
normal destination according to the semantics of the operation. If the
|
|
cleanup region is not marked as `eh_only`, normal control flow exits
|
|
from the body region must execute the cleanup region before control is
|
|
transferred to the destination implied by the operation.
|
|
|
|
If a `cir.goto` operation occurs within a cleanup scope, the behavior
|
|
depends on the target of the operation. If the target is within the
|
|
same cleanup scope, control is transferred to the target block directly.
|
|
If the target is not within the cleanup scope, control is transferred to
|
|
the cleanup region according to the rules described above for normal
|
|
exits before branching to the destination of the goto operation.
|
|
|
|
While we do not expect to encounter `cir.br` or `cir.brcond` operations
|
|
that exit a cleanup scope, if such a thing did happen, it would follow
|
|
the rules described above for `cir.goto` operations.
|
|
|
|
The `cir.indirect_br` operation is not permitted within a cleanup scope.
|
|
|
|
When an exception is thrown from within a cleanup scope and not caught
|
|
within the scope, the cleanup region must be executed before handling of
|
|
the exception continues. If the cleanup scope is nested within another
|
|
cleanup scope, the cleanup region of the inner scope is executed,
|
|
followed by the cleanup region of the outer scope, and handling
|
|
continues according to these rules. If the cleanup scope is nested
|
|
within a try operation, the cleanup region is executed before control is
|
|
transferred to the catch handlers. If an exception is thrown from within
|
|
a cleanup region that is not nested within either another cleanup region
|
|
or a try operation, the cleanup region is executed and then exception
|
|
unwinding continues as if a `cir.resume` operation had been executed.
|
|
|
|
If a `cir.resume` operation occurs within a cleanup scope, for example,
|
|
if the scope contains a try operation with uncaught exception types, the
|
|
`cir.resume` operation will unwind to the cleanup region of the enclosing
|
|
cleanup scope.
|
|
|
|
Note that this design eliminates the need for synthetic try operations,
|
|
such as were used to represent calls within a cleanup scope in the
|
|
ClangIR incubator project.
|
|
|
|
#### Implementation notes
|
|
|
|
The `cir.cleanup.scope` must be created when we call `pushCleanup`. We
|
|
will need to set the insertion point at that time. When each cleanup
|
|
block is popped, we will need to set the insertion point to immediately
|
|
following the cleanup scope operation. If `forceCleanups()` is called,
|
|
it will pop cleanup blocks, which is good.
|
|
|
|
#### Example: Automatic storage object cleanup
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
void someFunc() {
|
|
SomeClass c;
|
|
c.doSomething();
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc() {
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
} cleanup normal {
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, we create an instance of `SomeClass` which has a
|
|
constructor and a destructor. If an exception occurs within the
|
|
constructor call, it unwinds without any handling in this function. The
|
|
cleanup scope is not entered in that case. Once the object has been
|
|
constructed, we enter a cleanup scope which continues until the object
|
|
goes out of scope, in this case for the remainder of the function.
|
|
|
|
If an exception is thrown from within the `doSomething()` function, we
|
|
execute the cleanup region, calling the `SomeClass` destructor before
|
|
continuing to unwind the exception. If the call to `doSomething()`
|
|
completes successfully, the object goes out of scope and we execute the
|
|
cleanup region, calling the destructor, before continuing to the return
|
|
operation.
|
|
|
|
#### Example: Multiple automatic objects
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
void someFunc() {
|
|
SomeClass c;
|
|
SomeClass c2;
|
|
c.doSomething();
|
|
SomeClass c3;
|
|
c3.doSomething();
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc() {
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
%1 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c2", init]
|
|
%2 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c3", init]
|
|
cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call @_ZN9SomeClassC1Ev(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.call @_ZN9SomeClassC1Ev(%2) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call @_ZN9SomeClass11doSomethingEv(%2) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
} cleanup normal {
|
|
cir.call @_ZN9SomeClassD1Ev(%2) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
} cleanup normal {
|
|
cir.call @_ZN9SomeClassD1Ev(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
} cleanup normal {
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, we have three objects with automatic storage duration.
|
|
The destructor must be called for each object that has been constructed,
|
|
and the destructors must be called in reverse order of object creation.
|
|
We guarantee that by creating nested cleanup scopes as each object is
|
|
constructed.
|
|
|
|
Normal execution control flows through the body region of each of the
|
|
nested cleanup scopes until the body of the innermost scope. Next, the
|
|
cleanup scopes are visited, calling the destructor once in each cleanup
|
|
scope, in reverse order of the object construction.
|
|
|
|
#### Implementation notes
|
|
|
|
Branch through cleanups will be handled during flattening. In the
|
|
structured CIR representation, an operation like `cir.break`,
|
|
`cir.return`, or `cir.continue` has well-defined behavior. We will need
|
|
to define the semantics such that they include visiting the cleanup
|
|
region before continuing to their currently defined destination.
|
|
|
|
#### Example: Branch through cleanup
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
int someFunc() {
|
|
int i = 0;
|
|
while (true) {
|
|
SomeClass c;
|
|
if (i == 3)
|
|
continue;
|
|
if (i == 7)
|
|
break;
|
|
i = c.get();
|
|
}
|
|
return i;
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc() -> !s32i {
|
|
%0 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__retval"]
|
|
%1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["i", init]
|
|
%2 = cir.const #cir.int<0> : !s32i
|
|
cir.store align(4) %2, %1 : !s32i, !cir.ptr<!s32i>
|
|
cir.scope {
|
|
cir.while {
|
|
%5 = cir.const #true
|
|
cir.condition(%5)
|
|
} do {
|
|
cir.scope {
|
|
%5 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.call @_ZN9SomeClassC1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.scope { // This is a scope for the `if`, unrelated to cleanups
|
|
%7 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i
|
|
%8 = cir.const #cir.int<3> : !s32i
|
|
%9 = cir.cmp(eq, %7, %8) : !s32i, !cir.bool
|
|
cir.if %9 {
|
|
cir.continue // This implicitly branches through the cleanup region
|
|
}
|
|
}
|
|
cir.scope { // This is a scope for the `if`, unrelated to cleanups
|
|
%7 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i
|
|
%8 = cir.const #cir.int<7> : !s32i
|
|
%9 = cir.cmp(eq, %7, %8) : !s32i, !cir.bool
|
|
cir.if %9 {
|
|
cir.break // This implicitly branches through the cleanup region
|
|
}
|
|
}
|
|
%6 = cir.call @_ZN9SomeClass3getEv(%5) : (!cir.ptr<!rec_SomeClass>) -> !s32i
|
|
cir.store align(4) %6, %1 : !s32i, !cir.ptr<!s32i>
|
|
cir.yield
|
|
} cleanup normal {
|
|
cir.call @_ZN9SomeClassD1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
}
|
|
cir.yield
|
|
}
|
|
}
|
|
%3 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i
|
|
cir.store %3, %0 : !s32i, !cir.ptr<!s32i>
|
|
%4 = cir.load %0 : !cir.ptr<!s32i>, !s32i
|
|
cir.return %4 : !s32i
|
|
}
|
|
```
|
|
|
|
In this example we have a cleanup scope inside the body of a
|
|
`while-loop`, and multiple instructions that may exit the loop body with
|
|
different destinations. When the `cir.continue` operation is executed,
|
|
it will transfer control to the cleanup region, which calls the object
|
|
destructor before transferring control to the while condition region
|
|
according to the semantics of the `cir.continue` operation.
|
|
|
|
When the `cir.break` operation is executed, it will transfer control to
|
|
the cleanup region, which calls the object destructor before
|
|
transferring control to the operation following the while loop according
|
|
to the semantics of the `cir.break` operation.
|
|
|
|
If neither the `cir.continue` or `cir.break` operations are executed
|
|
during an iteration of the loop, when the end of the cleanup scope's
|
|
body region is reached, control will be transferred to the cleanup
|
|
region, which calls the object destructor before transferring control to
|
|
the next operation following the cleanup scope, in this case falling
|
|
through to the `cir.yield` operation to complete the loop iteration.
|
|
|
|
This control flow is implicit in the semantics of the CIR operations at
|
|
this point. When this CIR is flattened, explicit branches and a switch
|
|
on destination slots will be created, matching the LLVM IR control flow
|
|
for cleanup block sharing.
|
|
|
|
#### Example: EH-only cleanup
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
class Base {
|
|
public:
|
|
Base();
|
|
~Base();
|
|
};
|
|
|
|
class Derived : public Base {
|
|
public:
|
|
Derived() : Base() { f(); }
|
|
~Derived();
|
|
};
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @_ZN7DerivedC2Ev(%arg0: !cir.ptr<!rec_Derived>) {
|
|
%0 = cir.alloca !cir.ptr<!rec_Derived>, !cir.ptr<!cir.ptr<!rec_Derived>>, ["this", init]
|
|
cir.store %arg0, %0 : !cir.ptr<!rec_Derived>, !cir.ptr<!cir.ptr<!rec_Derived>>
|
|
%1 = cir.load %0 : !cir.ptr<!cir.ptr<!rec_Derived>>, !cir.ptr<!rec_Derived>
|
|
%2 = cir.base_class_addr %1 : !cir.ptr<!rec_Derived> nonnull [0] -> !cir.ptr<!rec_Base>
|
|
cir.call @_ZN4BaseC2Ev(%2) : (!cir.ptr<!rec_Base>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call exception @_Z1fv() : () -> ()
|
|
cir.yield
|
|
} cleanup eh {
|
|
%3 = cir.base_class_addr %1 : !cir.ptr<!rec_Derived> nonnull [0] -> !cir.ptr<!rec_Base>
|
|
cir.call @_ZN4BaseD2Ev(%3) : (!cir.ptr<!rec_Base>) -> ()
|
|
cir.yield
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, the `Derived` constructor calls the `Base` constructor
|
|
and then calls a function which may throw an exception. If an exception
|
|
is thrown, we must call the `Base` destructor before continuing to
|
|
unwind the exception. However, if no exception is thrown, we do not call
|
|
the destructor. Therefore, this cleanup handler is marked as eh_only.
|
|
|
|
### Try Operations and Exception Handling
|
|
|
|
Try-catch blocks will be represented, as they are in the ClangIR
|
|
incubator project, using a `cir.try` operation.
|
|
|
|
Each catch handler region and unwind region in a `cir.try` operation
|
|
receives a `!cir.eh_token` argument representing the inflight exception.
|
|
|
|
The `cir.begin_catch` operation takes a `!cir.eh_token` as an argument
|
|
and returns two values: a `!cir.catch_token` that uniquely identifies
|
|
this catch handler, and a pointer to the exception object. When the
|
|
catch handler includes a source variable representing the exception
|
|
object, the pointer returned by `cir.begin_catch` will be stored to an
|
|
alloca object for the source variable. If the handler is a catch-all,
|
|
the `cir.begin_catch` operation will return a pointer to void, but this
|
|
cannot be captured by a source variable.
|
|
|
|
The `cir.end_catch` operation takes a `!cir.catch_token` as an argument,
|
|
marking the end of the catch handler. All paths through the catch
|
|
handler must converge on a single `cir.end_catch` operation.
|
|
|
|
The first operation in a catch handler region must be a `cir.begin_catch`
|
|
operation. This must be followed by a `cir.cleanup.scope` operation,
|
|
with the `cir.end_catch` operation in its cleanup region.
|
|
|
|
```
|
|
cir.try {
|
|
cir.call exception @function() : () -> ()
|
|
cir.yield
|
|
} catch [type #cir.global_view<@_ZTIPf> : !cir.ptr<!u8i>] (%eh_token : !cir.eh_token) {
|
|
%catch_token, %exn_ptr = cir.begin_catch %eh_token -> (!cir.catch_token, !cir.ptr<!cir.float>)
|
|
cir.cleanup.scope {
|
|
...
|
|
cir.yield
|
|
} cleanup eh {
|
|
cir.end_catch %catch_token
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
} unwind (%eh_token : !cir.eh_token) {
|
|
cir.resume %eh_token : !cir.eh_token
|
|
}
|
|
```
|
|
|
|
The operation consists of a try region, which contains the operations to
|
|
be executed during normal execution, and one or more handler regions,
|
|
which represent catch handlers or the fallback unwind for uncaught
|
|
exceptions.
|
|
|
|
#### Example: Simple try-catch
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
void someFunc() {
|
|
try {
|
|
f();
|
|
} catch (std::exception &e) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !cir.ptr<!rec_std3A3Aexception>, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>, ["e"]
|
|
cir.scope {
|
|
cir.try {
|
|
cir.call exception @_Z1fv() : () -> ()
|
|
cir.yield
|
|
} catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] (%eh_token : !cir.eh_token) {
|
|
%catch_token, %1 = cir.begin_catch %eh_token -> (!cir.catch_token, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>)
|
|
cir.cleanup.scope {
|
|
%2 = cir.load align(8) %1 : !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>, !cir.ptr<!rec_std3A3Aexception>
|
|
cir.store align(8) %2, %0 : !cir.ptr<!rec_std3A3Aexception>, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>
|
|
cir.yield
|
|
} cleanup eh {
|
|
cir.end_catch %catch_token
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
} unwind (%eh_token : !cir.eh_token) {
|
|
cir.resume %eh_token : !cir.eh_token
|
|
}
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
If the call to `f()` throws an exception that matches the handled type
|
|
(`std::exception&`), control will be transferred to the catch handler
|
|
for that type, which simply yields, continuing execution immediately
|
|
after the try operation.
|
|
|
|
If the call to `f()` throws any other type of exception, control will be
|
|
transferred to the unwind region, which simply continues unwinding the
|
|
exception at the next level, in this case, the handlers (if any) for the
|
|
function that called `someFunc()`.
|
|
|
|
#### Example: Try-catch with catch all
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
void someFunc() {
|
|
try {
|
|
f();
|
|
} catch (std::exception &e) {
|
|
// Do nothing
|
|
} catch (...) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !cir.ptr<!rec_std3A3Aexception>, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>, ["e"]
|
|
cir.scope {
|
|
cir.try {
|
|
cir.call exception @_Z1fv() : () -> ()
|
|
cir.yield
|
|
} catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] (%eh_token : !cir.eh_token) {
|
|
%catch_token, %1 = cir.begin_catch %eh_token -> (!cir.catch_token, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>)
|
|
cir.cleanup.scope {
|
|
%2 = cir.load align(8) %1 : !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>, !cir.ptr<!rec_std3A3Aexception>
|
|
cir.store align(8) %2, %0 : !cir.ptr<!rec_std3A3Aexception>, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>
|
|
cir.yield
|
|
} cleanup eh {
|
|
cir.end_catch %catch_token
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
} catch all (%eh_token : !cir.eh_token) {
|
|
%catch_token.1, %3 = cir.begin_catch %eh_token -> (!cir.catch_token, !cir.ptr<!void>)
|
|
cir.cleanup.scope {
|
|
cir.yield
|
|
} cleanup eh {
|
|
cir.end_catch %catch_token.1
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
}
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this case, if the call to `f()` throws an exception that matches the
|
|
handled type (`std::exception&`), everything works exactly as in the
|
|
previous example. Control will be transferred to the catch handler for
|
|
that type, which simply yields, continuing execution immediately after
|
|
the try operation.
|
|
|
|
If the call to `f()` throws any other type of exception, control will be
|
|
transferred to the catch all region, which also yields, continuing
|
|
execution immediately after the try operation.
|
|
|
|
#### Example: Try-catch with cleanup
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
void someFunc() {
|
|
try {
|
|
SomeClass c;
|
|
c.doSomething();
|
|
} catch (...) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
cir.scope {
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.try {
|
|
cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
} cleanup all {
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
} catch all (%eh_token : !cir.eh_token) {
|
|
%catch_token, %1 = cir.begin_catch %eh_token -> (!cir.catch_token, !cir.ptr<!void>)
|
|
cir.cleanup.scope {
|
|
cir.yield
|
|
} cleanup eh {
|
|
cir.end_catch %catch_token
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
}
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this case, an object that requires cleanup is instantiated inside the
|
|
try block scope. If the call to `doSomething()` throws an exception, the
|
|
cleanup region will be executed before control is transferred to the
|
|
catch handler.
|
|
|
|
#### Example: Try-catch within a cleanup region
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
void someFunc() {
|
|
SomeClass c;
|
|
try {
|
|
c.doSomething();
|
|
} catch (std::exception& e) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
%1 = cir.alloca !cir.ptr<!rec_std3A3Aexception>, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>, ["e"]
|
|
cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.scope {
|
|
cir.try {
|
|
cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
} catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] (%eh_token : !cir.eh_token) {
|
|
%catch_token, %2 = cir.begin_catch %eh_token -> (!cir.catch_token, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>)
|
|
cir.cleanup.scope {
|
|
%3 = cir.load align(8) %2 : !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>, !cir.ptr<!rec_std3A3Aexception>
|
|
cir.store align(8) %3, %1 : !cir.ptr<!rec_std3A3Aexception>, !cir.ptr<!cir.ptr<!rec_std3A3Aexception>>
|
|
cir.yield
|
|
} cleanup eh {
|
|
cir.end_catch %catch_token
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
} unwind (%eh_token : !cir.eh_token) {
|
|
cir.resume %eh_token : !cir.eh_token
|
|
}
|
|
}
|
|
cir.yield
|
|
} cleanup all {
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this case, the object that requires cleanup is instantiated outside
|
|
the try block scope, and not all exception types have catch handlers.
|
|
|
|
If the call to `doSomething()` throws an exception of type
|
|
`std::exception&`, control will be transferred to the catch handler,
|
|
which will simply continue execution at the point immediately following
|
|
the try operation, and the cleanup handler will be executed when the
|
|
cleanup scope is exited normally.
|
|
|
|
If the call to `doSomething()` throws any other exception of type,
|
|
control will be transferred to the unwind region, which executes
|
|
`cir.resume` to continue unwinding the exception. However, the cleanup
|
|
region of the cleanup scope will be executed before exception unwinding
|
|
continues because we are exiting the scope via the `cir.resume`
|
|
operation.
|
|
|
|
### Partial Array Cleanup
|
|
|
|
Partial array cleanup is a special case because the details of array
|
|
construction and deletion are already encapsulated within high-level CIR
|
|
operations. When an array of objects is constructed, the constructor for
|
|
each object is called sequentially. If one of the constructors throws an
|
|
exception, we must call the destructor for each object that was
|
|
previously constructed in reverse order of their construction. In the
|
|
high-level CIR representation, we have a single operation,
|
|
`cir.array.ctor` to represent the array construction. Because the
|
|
cleanup needed is entirely within the scope of this operation, we can
|
|
represent the cleanup by adding a cleanup region to this operation.
|
|
|
|
```
|
|
cir.array.ctor(%0 : !cir.ptr<!cir.array<!rec_SomeClass x 16>>) {
|
|
^bb0(%arg0: !cir.ptr<!rec_SomeClass>):
|
|
cir.call @_ZN9SomeClassC1Ev(%arg0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
} cleanup {
|
|
^bb0(%arg0: !cir.ptr<!rec_SomeClass>):
|
|
cir.call @_ZN9SomeClassD1Ev(%arg0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
```
|
|
|
|
This representation shows how a single instance of the object is
|
|
initialized and cleaned up. When the operation is transformed to a
|
|
low-level form (during `cir::LoweringPrepare`), these two regions will
|
|
be expanded to a loop within a `cir.cleanup.scope` for the
|
|
initialization, and a loop within the cleanup scope's cleanup region to
|
|
perform the partial array cleanup, as follows
|
|
|
|
```
|
|
cir.scope {
|
|
%1 = cir.const #cir.int<16> : !u64i
|
|
%2 = cir.cast array_to_ptrdecay %0 : !cir.ptr<!cir.array<!rec_SomeClass x 16>> -> !cir.ptr<!rec_SomeClass>
|
|
%3 = cir.ptr_stride %2, %1 : (!cir.ptr<!rec_SomeClass>, !u64i) -> !cir.ptr<!rec_SomeClass>
|
|
%4 = cir.alloca !cir.ptr<!rec_SomeClass>, !cir.ptr<!cir.ptr<!rec_SomeClass>>, ["__array_idx"]
|
|
cir.store %2, %4 : !cir.ptr<!rec_SomeClass>, !cir.ptr<!cir.ptr<!rec_SomeClass>>
|
|
cir.cleanup.scope {
|
|
cir.do {
|
|
%5 = cir.load %4 : !cir.ptr<!cir.ptr<!rec_SomeClass>>, !cir.ptr<!rec_SomeClass>
|
|
cir.call @_ZN9SomeClassC1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
%6 = cir.const #cir.int<1> : !u64i
|
|
%7 = cir.ptr_stride %5, %6 : (!cir.ptr<!rec_SomeClass>, !u64i) -> !cir.ptr<!rec_SomeClass>
|
|
cir.store %7, %4 : !cir.ptr<!rec_SomeClass>, !cir.ptr<!cir.ptr<!rec_SomeClass>>
|
|
cir.yield
|
|
} while {
|
|
%5 = cir.load %4 : !cir.ptr<!cir.ptr<!rec_SomeClass>>, !cir.ptr<!rec_SomeClass>
|
|
%6 = cir.cmp(ne, %5, %3) : !cir.ptr<!rec_SomeClass>, !cir.bool
|
|
cir.condition(%6)
|
|
}
|
|
} cleanup eh {
|
|
cir.while {
|
|
%5 = cir.load %4 : !cir.ptr<!cir.ptr<!rec_SomeClass>>, !cir.ptr<!rec_SomeClass>
|
|
%6 = cir.cmp(ne, %5, %2) : !cir.ptr<!rec_SomeClass>, !cir.bool
|
|
cir.condition(%6)
|
|
} cir.do {
|
|
%5 = cir.load %4 : !cir.ptr<!cir.ptr<!rec_SomeClass>>, !cir.ptr<!rec_SomeClass>
|
|
%6 = cir.const #cir.int<-1> : !s64i
|
|
%7 = cir.ptr_stride %5, %6 : (!cir.ptr<!rec_SomeClass>, !s64i) -> !cir.ptr<!rec_SomeClass>
|
|
cir.call @_ZN9SomeClassD1Ev(%7) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.store %7, %4 : !cir.ptr<!rec_SomeClass>, !cir.ptr<!cir.ptr<!rec_SomeClass>>
|
|
cir.yield
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Here, both the construction and cleanup loops use the same temporary
|
|
pointer variable to track their location. If an exception is thrown by
|
|
one of the constructor, the `__array_idx` variable will point to the
|
|
object that was being constructed when the exception was thrown. If the
|
|
exception was thrown during construction of the first object,
|
|
`__array_idx` will point to the start of the array, and so no destructor
|
|
will be called. If an exception is thrown during the constructor call
|
|
for any other object, `__array_idx` will not point to the start of the
|
|
array, and so the cleanup region will decrement the pointer, call the
|
|
destructor for the previous object, and so on until we reach the
|
|
beginning of the array. This corresponds to the way that partial array
|
|
destruction is handled in Clang's LLVM IR codegen.
|
|
|
|
## CFG Flattening
|
|
|
|
Before CIR can be lowered to the LLVM dialect, the CFG must be
|
|
flattened. That is, functions must not contain nested regions, and all
|
|
blocks in the function must belong to the parent region. This state is
|
|
formed by the `cir::FlattenCFG` pass. This pass will need to transform
|
|
the high-level CIR representation described above to a flat form where
|
|
cleanups and exception handling are explicitly routed through blocks,
|
|
which are shared as needed.
|
|
|
|
The CIR representation will remain ABI agnostic after the flattening
|
|
pass. The flattening pass will implement the semantics for branching
|
|
through cleanup regions using the same slot and dispatch mechanism used
|
|
in Clang's LLVM IR codegen.
|
|
|
|
### Exception Handling
|
|
|
|
Flattening the CIR for exception handling, including any cleanups that
|
|
must be performed during exception unwinding, requires some specialized
|
|
CIR operations. The operations that were used in the ClangIR incubator
|
|
project were closely matched to the Itanium exception handling ABI. In
|
|
order to achieve a representation that also works well for other ABIs,
|
|
the following new operations are being proposed: `cir.eh.initiate`,
|
|
`cir.eh.dispatch`, `cir.eh.terminate`, `cir.begin_cleanup`, and
|
|
`cir.end_cleanup`. The `cir.begin_catch` and `cir.end_catch` operations,
|
|
described above, are also used in the flattened form.
|
|
|
|
Any time a cir.call operation that may throw and exception appears
|
|
within the try region of a `cir.try` operation or within the body region
|
|
of a `cir.cleanup.scope` with a cleanup region marked as an exception
|
|
cleanup, the call will be converted to a `cir.try_call` operation, with
|
|
normal and unwind destinations. The first operation in the unwind
|
|
destination block must be a `cir.eh.initiate` operation.
|
|
|
|
`%eh_token = cir.eh.initiate [cleanup]`
|
|
|
|
If this destination includes cleanup code, the cleanup keyword will be
|
|
present, and the cleanup code will be executed before the exception is
|
|
dispatched to any handlers. The `cir.eh.initiate` operation returns a
|
|
value of type `!cir.eh_token`. This is an opaque value that will be used
|
|
during ABI-lowering. At this phase, it conceptually represents the
|
|
exception that was thrown and is passed as the argument to the
|
|
`cir.begin_cleanup`, `cir.begin_catch`, and `cir.eh.dispatch`
|
|
operations.
|
|
|
|
```
|
|
cir.eh.dispatch %eh_token : !cir.eh_token [
|
|
catch (#cir.global_view<@_ZTIi> : !u32i) : ^bb6
|
|
catch_all : ^bb7
|
|
]
|
|
|
|
cir.eh.dispatch %eh_token : !cir.eh_token [
|
|
catch (#cir.global_view<@_ZTIi> : !u32i) : ^bb6
|
|
unwind : ^bb7
|
|
]
|
|
```
|
|
|
|
The `cir.eh.dispatch` operation behaves similarly to the LLVM IR switch
|
|
instruction. It takes as an argument a token that was returned by a
|
|
previous `cir.eh.initiate` operation. It then has a list of key-value
|
|
pairs, where the key is either a type identifier, the keyword catch_all,
|
|
or the keyword unwind and the value is a block to which execution should
|
|
be transferred if the key is matched. Although the example above shows
|
|
both the catch_all and unwind keyword, in practice only one or the other
|
|
will be present, but the operation is required to have one of these
|
|
values.
|
|
|
|
When we are unwinding an exception with cleanups, the `cir.eh.initiate`
|
|
operation will be marked with the cleanup attribute and will be followed
|
|
by a branch to the cleanup block, passing the EH token as an operand to
|
|
the block. The cleanup block will begin with a call to
|
|
`cir.begin_cleanup` which returns a cleanup token.
|
|
|
|
```
|
|
^bb4 (%eh_token : !cir.eh_token):
|
|
%cleanup_token = cir.begin_cleanup %eh_token : !cir.eh_token -> !cir.cleanup_token
|
|
```
|
|
|
|
This is followed by the operations to perform the cleanup and then a
|
|
cir.end_cleanup operation.
|
|
|
|
`cir.end_cleanup(%cleanup_token : !cir.cleanup_token)`
|
|
|
|
Finally, the cleanup block either branches to a catch dispatch block or
|
|
executes a `cir.resume` operation to continue unwinding the exception.
|
|
|
|
When an exception is caught, the catch block will receive the eh token
|
|
for the exception being caught as an argument. The `cir.begin_catch`
|
|
and `cir.end_catch` operations, described above in the high-level
|
|
representation, continue to be used in the flattened form. In the
|
|
flattened form, the `eh_token` argument to `cir.begin_catch` comes
|
|
from the block argument rather than a region argument, and the
|
|
`cir.end_catch` operation appears directly in the catch block rather
|
|
than within a `cir.cleanup.scope` cleanup region.
|
|
|
|
#### Example: Try-catch with cleanup
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
void someFunc() {
|
|
try {
|
|
SomeClass c;
|
|
c.doSomething();
|
|
} catch (...) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
```
|
|
|
|
**High-level CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
cir.scope {
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.try {
|
|
cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
} cleanup all {
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
} catch all (%eh_token : !cir.eh_token) {
|
|
%catch_token, %1 = cir.begin_catch %eh_token -> (!cir.catch_token, !cir.ptr<!void>)
|
|
cir.cleanup.scope {
|
|
cir.yield
|
|
} cleanup eh {
|
|
cir.end_catch %catch_token
|
|
cir.yield
|
|
}
|
|
cir.yield
|
|
}
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
**Flattened CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb1
|
|
cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb2 // Normal cleanup
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.br ^bb8
|
|
^bb3 // EH catch (from entry block)
|
|
%1 = cir.eh.initiate : !cir.eh_token
|
|
cir.br ^bb6(%1 : !cir.eh_token)
|
|
^bb4 // EH cleanup (from ^bb1)
|
|
%2 = cir.eh.initiate cleanup : !cir.eh_token
|
|
cir.br ^bb5(%2 : !cir.eh_token)
|
|
^bb5(%eh_token : !cir.eh_token)
|
|
%3 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.end_cleanup(%3 : !cir.cleanup_token)
|
|
cir.br ^bb6(%eh_token : !cir.eh_token)
|
|
^bb6(%eh_token.1 : !cir.eh_token) // Catch dispatch (from ^bb3 or ^bb4)
|
|
cir.eh.dispatch %eh_token.1 : !cir.eh_token [
|
|
catch_all : ^bb7
|
|
]
|
|
^bb7(%eh_token.2 : !cir.eh_token)
|
|
%catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb8
|
|
^bb8 // Normal continue (from ^bb2 or ^bb6)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, the normal cleanup is performed in a different block
|
|
than the EH cleanup. This follows the pattern established by Clang's
|
|
LLVM IR codegen. Only the EH cleanup requires `cir.begin_cleanup` and
|
|
`cir.end_cleanup` operations.
|
|
|
|
If the `SomeClass` constructor throws an exception, it unwinds to an EH
|
|
catch block (`^bb3`), which has excecutes a `cir.eh.initiate` operation
|
|
before branching to a shared catch dispatch block (`^bb6`).
|
|
|
|
If the `doSomething()` function throws an exception, it unwinds to an EH
|
|
block `^bb4` that performs cleanup before branching to the shared catch
|
|
dispatch block (`^bb5`).
|
|
|
|
#### Example: Cleanup with unhandled exception
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
void someFunc() {
|
|
SomeClass c;
|
|
c.doSomething();
|
|
}
|
|
```
|
|
|
|
**High-level CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
} cleanup all {
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
**Flattened CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb1, ^bb2 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb1 // Normal cleanup
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.br ^bb4
|
|
^bb2 // EH cleanup (from entry block)
|
|
%1 = cir.eh.initiate cleanup : !cir.eh_token
|
|
cir.br ^bb3(%1 : !cir.eh_token)
|
|
^bb3(%eh_token : !cir.eh_token) // Perform cleanup
|
|
%2 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.end_cleanup(%2 : !cir.cleanup_token)
|
|
cir.resume %eh_token : !cir.eh_token // Unwind to caller
|
|
^bb4 // Normal continue (from ^bb1)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, if `doSomething()` throws an exception, it unwinds to
|
|
the EH cleanup block (`^bb2`), which branches to `^bb3` to perform the
|
|
cleanup, but because we have no catch handler, we execute `cir.resume`
|
|
after the cleanup to unwind to the function that called `someFunc()`.
|
|
|
|
#### Throwing Calls in Cleanup Regions
|
|
|
|
When a call in an EH cleanup region may throw an exception, it requires
|
|
special handling. The C++ standard requires that if an exception is
|
|
thrown during exception cleanup (i.e., while unwinding a previous
|
|
exception), the program must call `std::terminate()`. In the flattened
|
|
CIR, such calls are replaced with `cir.try_call` operations whose
|
|
unwind destination contains a `cir.eh.initiate` followed by a
|
|
`cir.eh.terminate` operation.
|
|
|
|
The `cir.eh.terminate` operation is a terminator that signals the need
|
|
for program termination due to an exception thrown during cleanup. It
|
|
takes the `!cir.eh_token` returned by `cir.eh.initiate` and is further
|
|
processed during EH ABI lowering, where it is replaced with target-specific
|
|
termination code.
|
|
|
|
#### Example: Cleanup with throwing destructor
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
struct ThrowingDtor {
|
|
~ThrowingDtor() noexcept(false);
|
|
};
|
|
|
|
void someFunc() {
|
|
ThrowingDtor c;
|
|
c.doSomething();
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !rec_ThrowingDtor, !cir.ptr<!rec_ThrowingDtor>, ["c", init]
|
|
cir.call @_ZN12ThrowingDtorC1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.call @_ZN12ThrowingDtor11doSomethingEv(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
|
|
cir.yield
|
|
} cleanup all {
|
|
cir.call @_ZN12ThrowingDtorD1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
|
|
cir.yield
|
|
}
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
**Flattened CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !rec_ThrowingDtor, !cir.ptr<!rec_ThrowingDtor>, ["c", init]
|
|
cir.call @_ZN12ThrowingDtorC1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
|
|
cir.try_call @_ZN12ThrowingDtor11doSomethingEv(%0) ^bb1, ^bb2 : (!cir.ptr<!rec_ThrowingDtor>) -> ()
|
|
^bb1 // Normal cleanup
|
|
cir.call @_ZN12ThrowingDtorD1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
|
|
cir.br ^bb6
|
|
^bb2 // EH cleanup (from entry block)
|
|
%1 = cir.eh.initiate cleanup : !cir.eh_token
|
|
cir.br ^bb3(%1 : !cir.eh_token)
|
|
^bb3(%eh_token : !cir.eh_token) // Perform cleanup
|
|
%2 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token
|
|
cir.try_call @_ZN12ThrowingDtorD1Ev(%0) ^bb4, ^bb5 : (!cir.ptr<!rec_ThrowingDtor>) -> ()
|
|
^bb4 // Destructor completed: continue unwinding
|
|
cir.end_cleanup(%2 : !cir.cleanup_token)
|
|
cir.resume %eh_token : !cir.eh_token
|
|
^bb5 // Destructor threw: terminate
|
|
%3 = cir.eh.initiate : !cir.eh_token
|
|
cir.eh.terminate %3 : !cir.eh_token
|
|
^bb6 // Normal continue (from ^bb1)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, the destructor for `ThrowingDtor` may throw. In the
|
|
normal cleanup path (`^bb1`), the destructor is a regular `cir.call`
|
|
since the exception would propagate normally. In the EH cleanup path
|
|
(`^bb3`), the destructor call is a `cir.try_call` because if the
|
|
destructor throws during exception unwinding, the program must
|
|
terminate. If the destructor completes normally, the exception
|
|
continues unwinding via `cir.resume`. If the destructor throws, control
|
|
transfers to `^bb5`, which initiates exception handling and immediately
|
|
terminates.
|
|
|
|
#### Example: Shared cleanups
|
|
|
|
**C++**
|
|
|
|
``` c++
|
|
int someFunc() {
|
|
int i = 0;
|
|
while (true) {
|
|
SomeClass c;
|
|
if (i == 3)
|
|
continue;
|
|
if (i == 7)
|
|
break;
|
|
i = c.get();
|
|
}
|
|
return i;
|
|
}
|
|
```
|
|
|
|
**CIR**
|
|
|
|
```
|
|
cir.func @someFunc() -> !s32i {
|
|
%0 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__retval"]
|
|
%1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["i", init]
|
|
%2 = cir.const #cir.int<0> : !s32i
|
|
cir.store align(4) %2, %1 : !s32i, !cir.ptr<!s32i>
|
|
cir.scope {
|
|
cir.while {
|
|
%5 = cir.const #true
|
|
cir.condition(%5)
|
|
} do {
|
|
cir.scope {
|
|
%5 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.call @_ZN9SomeClassC1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanup.scope {
|
|
cir.scope {
|
|
%7 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i
|
|
%8 = cir.const #cir.int<3> : !s32i
|
|
%9 = cir.cmp(eq, %7, %8) : !s32i, !cir.bool
|
|
cir.if %9 {
|
|
cir.continue
|
|
}
|
|
}
|
|
cir.scope {
|
|
%7 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i
|
|
%8 = cir.const #cir.int<7> : !s32i
|
|
%9 = cir.cmp(eq, %7, %8) : !s32i, !cir.bool
|
|
cir.if %9 {
|
|
cir.break
|
|
}
|
|
}
|
|
%6 = cir.call @_ZN9SomeClass3getEv(%5) : (!cir.ptr<!rec_SomeClass>) -> !s32i
|
|
cir.store align(4) %6, %1 : !s32i, !cir.ptr<!s32i>
|
|
cir.yield
|
|
} cleanup all {
|
|
cir.call @_ZN9SomeClassD1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.yield
|
|
}
|
|
}
|
|
cir.yield
|
|
}
|
|
}
|
|
%3 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i
|
|
cir.store %3, %0 : !s32i, !cir.ptr<!s32i>
|
|
%4 = cir.load %0 : !cir.ptr<!s32i>, !s32i
|
|
cir.return %4 : !s32i
|
|
}
|
|
```
|
|
|
|
**Flattened CIR**
|
|
|
|
```
|
|
cir.func @someFunc() -> !s32i {
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
%1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__cleanup_dest_slot "]
|
|
%2 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__retval"]
|
|
%3 = cir.alloca !s32i, !cir.ptr<!s32i>, ["i", init]
|
|
%4 = cir.const #cir.int<0> : !s32i
|
|
cir.store align(4) %4, %3 : !s32i, !cir.ptr<!s32i>
|
|
cir.br ^bb1
|
|
^bb1: // 3 preds: ^bb0, ^bb9, ^bb11
|
|
%5 = cir.const #true
|
|
cir.brcond %5 ^bb2, ^bb12
|
|
^bb2: // pred: ^bb1
|
|
cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.br ^bb3
|
|
^bb3: // pred: ^bb2
|
|
%6 = cir.load align(4) %3 : !cir.ptr<!s32i>, !s32i
|
|
%7 = cir.const #cir.int<3> : !s32i
|
|
%8 = cir.cmp(eq, %6, %7) : !s32i, !cir.bool
|
|
cir.brcond %8 ^bb4, ^bb5
|
|
^bb4: // pred: ^bb3
|
|
// Set the destination slot and branch through cleanup
|
|
%9 = cir.const #cir.int<0> : !s32i
|
|
cir.store %9, %1 : !s32i, !cir.ptr<!s32i>
|
|
cir.br ^bb9
|
|
^bb5: // pred: ^bb3
|
|
%10 = cir.load align(4) %3 : !cir.ptr<!s32i>, !s32i
|
|
%11 = cir.const #cir.int<7> : !s32i
|
|
%12 = cir.cmp(eq, %10, %11) : !s32i, !cir.bool
|
|
cir.brcond %12 ^bb6, ^bb7
|
|
^bb6: // pred: ^bb5
|
|
// Set the destination slot and branch through cleanup
|
|
%13 = cir.const #cir.int<1> : !s32i
|
|
cir.store %13, %1 : !s32i, !cir.ptr<!s32i>
|
|
cir.br ^bb9
|
|
^bb7: // pred: ^bb5
|
|
%14 = cir.call @_ZN9SomeClass3getEv(%0) : (!cir.ptr<!rec_SomeClass>) -> !s32i
|
|
cir.store align(4) %14, %3 : !s32i, !cir.ptr<!s32i>
|
|
cir.br ^bb8
|
|
^bb8: // pred: ^bb7
|
|
// Set the destination slot and branch through cleanup
|
|
%15 = cir.const #cir.int<2> : !s32i
|
|
cir.store %15, %1 : !s32i, !cir.ptr<!s32i>
|
|
cir.br ^bb9
|
|
^bb9: // pred
|
|
// Shared cleanup
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
%16 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i
|
|
cir.switch.flat %16 : !s32i, ^bb10 [
|
|
0: ^bb1 // continue
|
|
1: ^bb12 // break
|
|
2: ^bb11 // end of loop
|
|
]
|
|
^bb10: // preds: ^bb9
|
|
cir.unreachable
|
|
^bb11: // pred: ^bb9
|
|
cir.br ^bb1
|
|
^bb12: // pred: ^bb1
|
|
%17 = cir.load align(4) %3 : !cir.ptr<!s32i>, !s32i
|
|
cir.store align(4) %17, %2 : !s32i, !cir.ptr<!s32i>
|
|
%18 = cir.load align(4) %2 : !cir.ptr<!s32i>, !s32i
|
|
cir.return %18 : !s32i
|
|
}
|
|
```
|
|
|
|
In this example we have a cleanup scope inside the body of a while loop,
|
|
and multiple instructions that may exit the loop body with different
|
|
destinations. For simplicity, the example is shown without exception
|
|
handling.
|
|
|
|
When any of the conditions that exit a loop iteration occur (continue,
|
|
break, or completion of an iteration), we set a cleanup destination slot
|
|
to a unique value and branch to a shared normal cleanup block. That
|
|
block performs the cleanup and then compares the cleanup destination
|
|
slot value to the set of expected constants and branches to the
|
|
corresponding destination.
|
|
|
|
For example, when the continue instruction is reached, we set the
|
|
cleanup destination slot (`%1`) to zero, branch to the shared cleanup
|
|
block (`^bb9`), which calls the `SomeClass` destructor, then uses
|
|
`cir.switch.flat` to switch on the cleanup destination slot value and,
|
|
finding it to be zero, branches to the loop condition block (`^bb1`).
|
|
|
|
If none of the expected values is matched, the `cir.switch.flat`
|
|
branches to a block with a `cir.unreachable` operation. This corresponds
|
|
to the behavior of Clang's LLVM IR codegen.
|
|
|
|
## ABI Lowering
|
|
|
|
A new pass will be introduced to lower the flattened representation to
|
|
lower the ABI-agnostic flattened CIR representation to an ABI-specific
|
|
form. This will be a separate pass from the main CXXABI lowering pass,
|
|
which runs before CFG flattening. The ABI lowering pass will introduce
|
|
personality functions and ABI-specific exception handling operations.
|
|
|
|
This new pass will make use of the `cir::CXXABI` interface class and
|
|
ABI-specific subclasses, but it will introduce a new set of interface
|
|
methods for use with the exception handling ABI.
|
|
|
|
For each supported exception handling ABI, the operations and function
|
|
calls used will have a direct correspondence to the LLVM IR instructions
|
|
and runtime library functions used for that ABI. The LLVM IR exception
|
|
handling model is described in detail here: [LLVM Exception
|
|
Handling](https://llvm.org/docs/ExceptionHandling.html).
|
|
|
|
A personality function attribute will be added to functions that require
|
|
it during the ABI lowering phase.
|
|
|
|
### Itanium ABI Lowering
|
|
|
|
The Itanium exception handling ABI representation replaces the
|
|
`cir.eh.initiate` and `cir.eh.dispatch` operations with a
|
|
`cir.eh.landingpad` operation and a series of `cir.compare` and
|
|
`cir.brcond` operations to model the correct handling based on type IDs
|
|
for the catch handlers. The `cir.begin_cleanup` and `cir.end_cleanup`
|
|
operations are simply dropped. The `cir.begin_catch` operation becomes a
|
|
call to `__cxa_begin_catch`. The `cir.end_catch` operation becomes a
|
|
call to `__cxa_end_catch`. The `cir.eh.terminate` operation becomes a
|
|
call to `__clang_call_terminate` (which calls `__cxa_begin_catch`
|
|
followed by `std::terminate()`) and then an unreachable operation.
|
|
|
|
The only operation that is specific to Itanium exception handling is
|
|
`cir.eh.landingpad`.
|
|
|
|
`%exn_ptr_0, %type_id = cir.eh.landingpad [@_ZTISt9exception] : !cir.ptr<!void>, !u32i`
|
|
|
|
This operation corresponds directly to the LLVM IR landingpad
|
|
instruction. It may have a list of type IDs that the handler can catch
|
|
(or null for \"catch all\") or it may have the cleanup attribute if the
|
|
handler performs cleanup but does not catch any exceptions.
|
|
|
|
#### Example: Try-catch with cleanup
|
|
|
|
**Flattened CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb1
|
|
cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb2 // Normal cleanup
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.br ^bb8
|
|
^bb3 // EH catch (from entry block)
|
|
%1 = cir.eh.initiate : !cir.eh_token
|
|
cir.br ^bb6(%1 : !cir.eh_token)
|
|
^bb4 // EH cleanup (from ^bb1)
|
|
%2 = cir.eh.initiate cleanup : !cir.eh_token
|
|
cir.br ^bb5(%2 : !cir.eh_token)
|
|
^bb5(%eh_token : !cir.eh_token)
|
|
%3 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.end_cleanup(%3 : !cir.cleanup_token)
|
|
cir.br ^bb6(%eh_token : !cir.eh_token)
|
|
^bb6(%eh_token.1 : !cir.eh_token) // Catch dispatch (from ^bb3 or ^bb4)
|
|
cir.eh.dispatch %eh_token.1 : !cir.eh_token [
|
|
catch_all : ^bb7
|
|
]
|
|
^bb7(%eh_token.2 : !cir.eh_token)
|
|
%catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb8
|
|
^bb8 // Normal continue (from ^bb2 or ^bb6)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
**ABI-lowered CIR**
|
|
|
|
```
|
|
cir.func @someFunc() #personality_fn = @__gxx_personality_v0 {
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb1
|
|
cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb2 // Normal cleanup
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.br ^bb8
|
|
^bb3 // EH catch (from entry block)
|
|
%exn, %type_id = cir.eh.landingpad [null] : (!cir.ptr<!void>, !u32i)
|
|
cir.br ^bb6(%exn, &type_id : !cir.ptr<!void>, !u32i)
|
|
^bb4 // EH cleanup (from ^bb1)
|
|
%exn.1, %type_id.1 = cir.eh.landingpad cleanup [null] : (!cir.ptr<!void>, !u32i)
|
|
cir.br ^bb5(%exn, %type_id : !cir.ptr<!void>, !u32i)
|
|
^bb5(%1: !cir.ptr<!void>, %2: !u32i)
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.br ^bb6(%1, %2 : !cir.ptr<!void>, !u32i)
|
|
^bb6(%3: !cir.ptr<!void>, %4: !u32i) // Catch dispatch (from ^bb3 or ^bb4)
|
|
cir.br ^bb7(%3, %4 : !cir.ptr<!void>, !u32i)
|
|
^bb7(%5: !cir.ptr<!void>, %6: !u32i) // Catch all handler
|
|
%7 = cir.call @__cxa_begin_catch(%5 : !cir.ptr<!void>)
|
|
cir.call @__cxa_end_catch()
|
|
cir.br ^bb8
|
|
^bb8 // Normal continue (from ^bb2 or ^bb6)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, if an exception is thrown by the `SomeClass`
|
|
constructor, it unwinds to a landing pad block (`^bb3`), which branches
|
|
to the shared catch dispatch block (`^bb6`), which branches to the catch
|
|
all handler block (`^bb7`). The catch all handler calls
|
|
`__cxa_begin_catch` and `__cxa_end_catch` and then continues to the
|
|
normal continuation block (`^bb8`).
|
|
|
|
#### Example: Try-catch with multiple catch handlers
|
|
|
|
**Flattened CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
cir.try_call @f() ^bb1, ^bb2
|
|
^bb1
|
|
cir.br ^bb7
|
|
^bb2 // EH catch (from entry block)
|
|
%1 = cir.eh.initiate : !cir.eh_token
|
|
cir.br ^bb3(%1 : !cir.eh_token)
|
|
^bb3(%eh_token : !cir.eh_token) // Catch dispatch (from ^bb2)
|
|
cir.eh.dispatch %eh_token : !cir.eh_token [
|
|
catch (#cir.global_view<@_ZTIi> : !u32i) : ^bb4
|
|
catch (#cir.global_view<@_ZTIf> : !u32i) : ^bb5
|
|
catch_all : ^bb6
|
|
]
|
|
^bb4(%eh_token.1 : !cir.eh_token) // Catch handler for int exception
|
|
%catch.token = cir.begin_catch(%eh_token.1 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb7
|
|
^bb5(%eh_token.2 : !cir.eh_token) // Catch handler for float exception
|
|
%catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb7
|
|
^bb6(%eh_token.3 : !cir.eh_token) // Catch all handler
|
|
%catch.token = cir.begin_catch(%eh_token.3 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb7
|
|
^bb7 // Normal continue (from ^bb1, ^bb4, ^bb5, or ^bb6)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
**ABI-lowered CIR**
|
|
|
|
```
|
|
cir.func @someFunc() #personality_fn = @__gxx_personality_v0 {
|
|
cir.try_call @f() ^bb1, ^bb2
|
|
^bb1
|
|
cir.br ^bb8
|
|
^bb2 // EH catch (from entry block)
|
|
%exn, %type_id = cir.eh.landingpad [null] : (!cir.ptr<!void>, !u32i)
|
|
cir.br ^bb3(%exn, &type_id : !cir.ptr<!void>, !u32i)
|
|
^bb3(%0: !cir.ptr<!void>, %1: !u32i) // Catch compare for int exception
|
|
%2 = cir.eh.typeid @_ZTIi : !u32i
|
|
%3 = cir.cmp(eq, %1, %2) : !u32i, !cir.bool
|
|
cir.brcond %3 ^bb4(%0 : !cir.ptr<!void>), ^bb5(%0, %1 : !cir.ptr<!void>, !u32i)
|
|
^bb4(%4: !cir.ptr<!void>, %5: !u32i) // Catch all handler for int exception
|
|
%6 = cir.call @__cxa_begin_catch(%4 : !cir.ptr<!void>)
|
|
cir.call @__cxa_end_catch()
|
|
cir.br ^bb8
|
|
^bb5(%7: !cir.ptr<!void>, %8: !u32i) // Catch compare for float exception
|
|
%9 = cir.eh.typeid @_ZTIf : !u32i
|
|
%10 = cir.cmp(eq, %8, %9) : !u32i, !cir.bool
|
|
cir.brcond %10 ^bb7(%7 : !cir.ptr<!void>), ^bb8(%7 : !cir.ptr<!void>)
|
|
^bb6(%11: !cir.ptr<!void>, %12: !u32i) // Catch all handler for float exception
|
|
%13 = cir.call @__cxa_begin_catch(%11 : !cir.ptr<!void>)
|
|
cir.call @__cxa_end_catch()
|
|
cir.br ^bb8
|
|
^bb7(%14: !cir.ptr<!void>) // Catch all handler
|
|
%15 = cir.call @__cxa_begin_catch(%14 : !cir.ptr<!void>)
|
|
cir.call @__cxa_end_catch()
|
|
cir.br ^bb8
|
|
^bb8 // Normal continue (from ^bb1, ^bb4, ^bb6, or ^bb7)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, if an exception is thrown by the `f()` call, it unwinds
|
|
to a landing pad block (`^bb2`), which uses the `cir.eh.landingpad`
|
|
operation to capture the exception pointer and its type id, then branches
|
|
to `^bb3` to begin searching for a catch handler that handles the type id
|
|
of the exception. Each catch handler simply consumes the exception by
|
|
calling `__cxa_begin_catch` and `__cxa_end_catch` and then continues to
|
|
the normal continuation block (`^bb8`).
|
|
|
|
### Microsoft C++ ABI Lowering
|
|
|
|
The Microsoft C++ exception handling ABI representation drops the
|
|
`cir.eh.initiate` operation and replaces the `cir.eh.dispatch` operation
|
|
with `cir.eh.catchswitch` operation. The `cir.begin_cleanup` and
|
|
`cir.end_cleanup` operations are replaced with `cir.cleanuppad` and
|
|
`cir.cleanupret` respectively, and the `cir.begin_catch` and
|
|
`cir.end_catch` operations are replaced with `cir.catchpad` and
|
|
`cir.catchret`.
|
|
|
|
Each of these operations corresponds directly to a similarly named
|
|
instruction in LLVM IR and have the same semantics. The first operation
|
|
in the unwind destination of a `cir.try_call` must be either
|
|
`cir.eh.catchswitch` or `cir.cleanuppad`.
|
|
|
|
`%4 = cir.eh.catchswitch within none [^bb2, ^bb3] unwind to caller`
|
|
|
|
The `cir.eh.catchswitch` operation takes an operand which specifies the
|
|
parent token, which may either be none or the token returned by a
|
|
previous `cir.catchpad` operation. This is followed by a list of blocks
|
|
which contain catch handlers. Each block in this list must begin with a
|
|
`cir.catchpad` operation. Finally, the unwind destination is provided to
|
|
specify where excution continues if the exception is not caught by any
|
|
of the handlers, with unwind to caller indicating that the unwind is not
|
|
handled further in the current function. This operation returns a token
|
|
that is used as the operand for `cir.catchpad` operations associated
|
|
with this switch.
|
|
|
|
`%5 = cir.cleanuppad within none []`
|
|
|
|
The `cir.cleanuppad` operation takes an operand which specifies the
|
|
parent token, which may either be none or the token returned by a
|
|
previous `cir.catchpad` operation. This is followed by a arguments
|
|
required by the personality function. In the case of C++ exception
|
|
handlers, the personality function will be `__CxxFrameHandler3` and the
|
|
argument list will be empty. This operation returns a token that is used
|
|
as the operand for the associated `cir.cleanupret` operation.
|
|
|
|
`cir.cleanupret from %5 unwind to ^bb7`
|
|
|
|
The `cir.cleanupret` operation takes an operand which specifies the
|
|
`cir.cleanuppad` operation which is completed by this operation and a
|
|
block at which unwinding of the current exception continues (or unwind
|
|
to caller if there is no catch handling in the current function).
|
|
|
|
`%8 = cir.catchpad within %4 [ptr @"??_R0H@8", i32 0, ptr %e]`
|
|
|
|
The `cir.catchpad` operation takes an operand which specifies the parent
|
|
token, which must have been return by a previous `cir.catchswitch`
|
|
operation. This is followed by a list of arguments, beginning with the
|
|
typeid for the type of exception being caught (or null for catch all),
|
|
followed by a type info flag value, followed by a pointer to the
|
|
in-flight exception. This operation returns a token that is used as the
|
|
operand for the associated `cir.catchret` operation or as the parent for
|
|
any `cir.catchswitch` or `cir.cleanuppad` operations that are nested
|
|
within this catch handler.
|
|
|
|
`cir.catchret from %8 to ^bb8`
|
|
|
|
The `cir.catchret` operation takes an operand which specifies the
|
|
`cir.catchpad` operation which is completed by this operation and a
|
|
block at which excution should be resumed.
|
|
|
|
#### Example: Try-catch with cleanup
|
|
|
|
**Flattened CIR**
|
|
|
|
```
|
|
cir.func @someFunc() {
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb1
|
|
cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb2 // Normal cleanup
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.br ^bb8
|
|
^bb3 // EH catch (from entry block)
|
|
%1 = cir.eh.initiate : !cir.eh_token
|
|
cir.br ^bb6(%1 : !cir.eh_token)
|
|
^bb4 // EH cleanup (from ^bb1)
|
|
%2 = cir.eh.initiate cleanup : !cir.eh_token
|
|
cir.br ^bb5(%2 : !cir.eh_token)
|
|
^bb5(%eh_token : !cir.eh_token)
|
|
%3 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.end_cleanup(%3 : !cir.cleanup_token)
|
|
cir.br ^bb6(%eh_token : !cir.eh_token)
|
|
^bb6(%eh_token.1 : !cir.eh_token) // Catch dispatch (from ^bb3 or ^bb4)
|
|
cir.eh.dispatch %eh_token.1 : !cir.eh_token [
|
|
catch_all : ^bb7
|
|
]
|
|
^bb7(%eh_token.2 : !cir.eh_token)
|
|
%catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb8
|
|
^bb8 // Normal continue (from ^bb2 or ^bb6)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
**ABI-lowered CIR**
|
|
|
|
```
|
|
cir.func @someFunc() #personality_fn = @ __CxxFrameHandler3 {
|
|
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
|
|
cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb1
|
|
cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
^bb2 // Normal cleanup
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.br ^bb6
|
|
^bb3 // EH cleanup (from ^bb1)
|
|
%1 = cir.cleanuppad within none : !cir.cleanup_token
|
|
cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
|
|
cir.cleanupret from %1 unwind to ^bb4
|
|
^bb4 // Catch dispatch (from ^bb3 or ^bb4)
|
|
%2 = cir.catchswitch within none [^bb5] unwind to caller
|
|
^bb5
|
|
%catch.token = cir.catchpad within %2 [null : !cir.ptr<!void>] : !cir.catch_token
|
|
cir.catchret within %catch.token to ^bb6
|
|
^bb6 // Normal continue (from ^bb2 or ^bb6)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
#### Example: Try-catch with multiple catch handlers
|
|
|
|
**Flattened CIR**
|
|
|
|
```
|
|
cir.func @someFunc(){
|
|
cir.try_call @f() ^bb1, ^bb2
|
|
^bb1
|
|
cir.br ^bb7
|
|
^bb2 // EH catch (from entry block)
|
|
%1 = cir.eh.initiate : !cir.eh_token
|
|
cir.br ^bb3(%1 : !cir.eh_token)
|
|
^bb3(%eh_token : !cir.eh_token) // Catch dispatch (from ^bb2)
|
|
cir.eh.dispatch %eh_token : !cir.eh_token [
|
|
catch (#cir.global_view<@_ZTIi> : !u32i) : ^bb4
|
|
catch (#cir.global_view<@_ZTIf> : !u32i) : ^bb5
|
|
catch_all : ^bb6
|
|
]
|
|
^bb4(%eh_token.1 : !cir.eh_token) // Catch handler for int exception
|
|
%catch.token = cir.begin_catch(%eh_token.1 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb7
|
|
^bb5(%eh_token.2 : !cir.eh_token) // Catch handler for float exception
|
|
%catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb7
|
|
^bb6(%eh_token.3 : !cir.eh_token) // Catch all handler
|
|
%catch.token = cir.begin_catch(%eh_token.3 : !cir.eh_token) : !cir.catch_token
|
|
cir.end_catch(%catch.token : !cir.catch_token)
|
|
cir.br ^bb7
|
|
^bb7 // Normal continue (from ^bb1, ^bb4, ^bb5, or ^bb6)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
**ABI-lowered CIR**
|
|
|
|
```
|
|
cir.func @someFunc() #personality_fn = @__CxxFrameHandler3 {
|
|
cir.try_call @f() ^bb1, ^bb2
|
|
^bb1
|
|
cir.br ^bb6
|
|
^bb2 // EH catch (from entry block)
|
|
%0 = cir.catchswitch within none [^bb3, ^bb4, ^bb5] unwind to caller
|
|
^bb3(%0: !cir.ptr<!void>) // Catch handler for int exception
|
|
%1 = cir.catchpad within %0 [eh.typeid @"??_R0H@8", 0, %0 : (!cir.ptr<!void>, !u32i, !cir.ptr<!void>)] : !cir.catch_token
|
|
cir.catchret from %1 to ^bb6
|
|
^bb4(%2: !cir.ptr<!void>) // Catch compare for float exception
|
|
%2 = cir.catchpad within %0 [eh.typeid @"??_R0M@8", 0, %0 : (!cir.ptr<!void>, !u32i, !cir.ptr<!void>)] : !cir.catch_token
|
|
cir.catchret from %2 to ^bb6
|
|
^bb5(%3: !cir.ptr<!void>) // Catch all handler
|
|
%4 = cir.catchpad within %0 [null, 64, null : (!cir.ptr<!void>, !u32i, !cir.ptr<!void>)] : !cir.catch_token
|
|
cir.catchret from %4 to ^bb6
|
|
^bb6 // Normal continue (from ^bb1, ^bb3, ^bb4, or ^bb5)
|
|
cir.return
|
|
}
|
|
```
|
|
|
|
In this example, if an exception is thrown by the `f()` call, it unwinds
|
|
to a catch dispatch block (`^bb2`), which uses the `cir.catchswitch`
|
|
operation to dispatch to a catch handler (`^bb3`, `^bb4`, or `^bb5`)
|
|
based on the type id of the exception. The actual comparisons in this
|
|
case will be handled by the personality function, using tables that are
|
|
generated from the `cir.catchpad` operations. Each catch handler simply
|
|
continues to the normal continuation block (`^bb6`) using the
|
|
`cir.catchret` operation.
|