| # ClangIR Cleanup and Exception Handling Design | |
| ## 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`. | |
| ```text | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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. | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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. | |
| ```mlir | |
| 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 | |
| ```mlir | |
| 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. | |
| ```mlir | |
| %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. | |
| ```mlir | |
| 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. | |
| ```mlir | |
| ^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. | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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`. | |
| ```mlir | |
| %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** | |
| ```mlir | |
| 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** | |
| ```text | |
| 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** | |
| ```mlir | |
| 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** | |
| ```text | |
| 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`. | |
| ```mlir | |
| %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. | |
| ```mlir | |
| %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. | |
| ```mlir | |
| 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). | |
| ```text | |
| %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. | |
| ```mlir | |
| 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** | |
| ```mlir | |
| 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** | |
| ```text | |
| 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** | |
| ```mlir | |
| 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** | |
| ```text | |
| 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. | |
| ## Dynamic Exception Specifications | |
| A dynamic exception specification (`throw(T...)`, and `throw()` before | |
| C++17) constrains the set of exception types that a function is allowed | |
| to propagate to its caller. If an exception of any other type would | |
| escape the function, `std::unexpected()` must be called instead | |
| ([except.spec]). Dynamic exception specifications were removed in C++17, | |
| so this representation is only produced for earlier language modes. | |
| Functions declared `noexcept`, and `throw()` in C++17 and later, are handled | |
| differently. | |
| Because the constraint applies to every exception that could escape the | |
| function, it is represented as an exception handler that encloses the | |
| entire function body. This section describes that representation used by the | |
| high-level CIR produced by CIR generation, the flattened form produced by `cir::FlattenCFG`, and the ABI-specific form produced by EH ABI lowering. | |
| ### High-level CIR representation | |
| A function with a dynamic exception specification has its entire body | |
| wrapped in a `cir.try` operation with two handlers, a `filter` handler that | |
| holds the path taken when the in-flight exception is permitted, and an | |
| `unexpected` handler, identified that holds the path taken when the | |
| specification is violated. | |
| ```mlir | |
| cir.try { | |
| // function body | |
| cir.yield | |
| } filter [@_ZTIi] (%eh_token : !cir.eh_token) { | |
| cir.resume %eh_token : !cir.eh_token | |
| } unexpected (%eh_token.1 : !cir.eh_token) { | |
| cir.eh.unexpected %eh_token.1 : !cir.eh_token | |
| } | |
| ``` | |
| Both attributes occupy a slot in the try operation's handler type list, | |
| in the same way that a `#cir.global_view` catch type, `catch all`, or | |
| `unwind` does. Like `unwind`, and unlike a catch handler, neither region | |
| begins with `cir.begin_catch`. Neither handler catches the exception. | |
| Together they only decide whether the exception is permitted to continue | |
| unwinding. | |
| The test that decides whether the in-flight exception matches the filter | |
| is implicit in the handler type, in the same way that the type test for | |
| a catch handler is implicit in its `#cir.global_view` handler type. | |
| Neither test is expressed in a handler region. Both are materialized | |
| during ABI lowering. The two regions therefore describe only the | |
| outcomes of that test. The filter region contains a single `cir.resume` | |
| operation to continue unwinding to the caller, and the unexpected region | |
| contains a single `cir.eh.unexpected` operation. | |
| The `cir.eh.unexpected` operation is a terminator that signals that the | |
| in-flight exception violated the exception specification of the | |
| enclosing function and that `std::unexpected()` must be called. Like | |
| `cir.eh.terminate`, it takes an `!cir.eh_token`, it is ABI-agnostic, and | |
| it is replaced with target-specific code during EH ABI lowering. | |
| A filter handler and an unexpected handler must appear together, with | |
| the filter first, and the two must be the only handlers on the try | |
| operation. The filter try operation wraps the entire function body and | |
| exists only to check the exception specification, while each try | |
| statement written in the source becomes a separate `cir.try` operation | |
| nested inside it. A function-try-block on a function that also has an | |
| exception specification is nested the same way. | |
| An empty type list represents `throw()` before C++17. No exception is | |
| permitted by such a specification, so there is no permitted path to | |
| describe and the filter region is terminated with `cir.unreachable` | |
| instead of `cir.resume`. This matches Clang's LLVM IR codegen, which | |
| generates no resume path at all for a function whose exception | |
| specification permits nothing. The unexpected region is the same in | |
| both cases. | |
| #### Example: Simple dynamic exception specification | |
| **C++** | |
| ```c++ | |
| void external(); | |
| void target() throw(int) { | |
| external(); | |
| } | |
| void target2() throw() { | |
| external(); | |
| } | |
| ``` | |
| **CIR** | |
| ```mlir | |
| cir.func @_Z6targetv() personality(@__gxx_personality_v0) { | |
| cir.try { | |
| cir.call @_Z8externalv() : () -> () | |
| cir.yield | |
| } filter [@_ZTIi] (%eh_token : !cir.eh_token) { | |
| cir.resume %eh_token : !cir.eh_token | |
| } unexpected (%eh_token.1 : !cir.eh_token) { | |
| cir.eh.unexpected %eh_token.1 : !cir.eh_token | |
| } | |
| cir.return | |
| } | |
| cir.func @_Z7target2v() personality(@__gxx_personality_v0) | |
| attributes {nothrow} { | |
| cir.try { | |
| cir.call @_Z8externalv() : () -> () | |
| cir.yield | |
| } filter [] (%eh_token : !cir.eh_token) { | |
| cir.unreachable | |
| } unexpected (%eh_token.1 : !cir.eh_token) { | |
| cir.eh.unexpected %eh_token.1 : !cir.eh_token | |
| } | |
| cir.return | |
| } | |
| ``` | |
| In `target()`, if `external()` throws an `int`, the exception is | |
| permitted by the specification and unwinding continues to the caller | |
| through the filter handler's `cir.resume` operation. If it throws any | |
| other type, the specification is violated and the unexpected handler | |
| calls `std::unexpected()`. | |
| In `target2()`, the specification permits nothing, so any exception | |
| thrown by `external()` violates it and the unexpected handler is always | |
| the one reached. There is no permitted path, which is why the filter | |
| region holds a `cir.unreachable` rather than a `cir.resume`. The | |
| function itself is marked `nothrow`, because no exception can escape it. | |
| #### Example: Try-catch within an exception specification | |
| **C++** | |
| ```c++ | |
| void external(); | |
| void inner() throw(int) { | |
| external(); | |
| } | |
| void outer() throw() { | |
| try { | |
| inner(); | |
| } catch (int) { | |
| } | |
| } | |
| ``` | |
| **CIR** | |
| ```mlir | |
| cir.func @_Z5innerv() personality(@__gxx_personality_v0) { | |
| cir.try { | |
| cir.call @_Z8externalv() : () -> () | |
| cir.yield | |
| } filter [@_ZTIi] (%eh_token : !cir.eh_token) { | |
| cir.resume %eh_token : !cir.eh_token | |
| } unexpected (%eh_token.1 : !cir.eh_token) { | |
| cir.eh.unexpected %eh_token.1 : !cir.eh_token | |
| } | |
| cir.return | |
| } | |
| cir.func @_Z5outerv() personality(@__gxx_personality_v0) | |
| attributes {nothrow} { | |
| cir.try { | |
| cir.scope { | |
| %0 = cir.alloca "" align(4) : !cir.ptr<!s32i> | |
| cir.try { | |
| cir.call @_Z5innerv() : () -> () | |
| cir.yield | |
| } catch [type #cir.global_view<@_ZTIi> : !cir.ptr<!u8i>] | |
| (%eh_token : !cir.eh_token) { | |
| %catch_token, %exn_ptr = cir.begin_catch %eh_token | |
| : !cir.eh_token -> (!cir.catch_token, !cir.ptr<!void>) | |
| cir.cleanup.scope { | |
| cir.init_catch_param scalar %exn_ptr to %0 | |
| : !cir.ptr<!void>, !cir.ptr<!s32i> | |
| cir.yield | |
| } cleanup all { | |
| cir.end_catch %catch_token : !cir.catch_token | |
| cir.yield | |
| } | |
| cir.yield | |
| } unwind (%eh_token.1 : !cir.eh_token) { | |
| cir.resume %eh_token.1 : !cir.eh_token | |
| } | |
| } | |
| cir.yield | |
| } filter [] (%eh_token.2 : !cir.eh_token) { | |
| cir.unreachable | |
| } unexpected (%eh_token.3 : !cir.eh_token) { | |
| cir.eh.unexpected %eh_token.3 : !cir.eh_token | |
| } | |
| cir.return | |
| } | |
| ``` | |
| In this example the exception specification try operation encloses the | |
| try-catch statement written in the source. | |
| If `inner()` throws an `int`, the inner try operation's catch handler | |
| runs and execution continues after the try statement. The exception | |
| specification of `outer()` is never consulted, because the exception | |
| does not escape the function. | |
| If `inner()` throws any other type, the inner try operation's `unwind` | |
| handler is reached. Its `cir.resume` operation exits the region of the | |
| enclosing filter try operation, so, following the rules described above | |
| for `cir.resume` within an enclosing scope, unwinding continues into | |
| that operation's specification check rather than leaving the function. | |
| The exception is checked against the specification of `outer()`, which | |
| permits nothing, so the unexpected handler is reached and | |
| `std::unexpected()` is called. | |
| ### CFG Flattening | |
| Flattening a filter try operation introduces a `filter` clause on the | |
| `cir.eh.dispatch` operation. Both handler regions become ordinary | |
| blocks, so flattening only has to inline them and wire up the dispatch | |
| operation's successors. It does not synthesize any new operation. | |
| ```mlir | |
| cir.eh.dispatch %eh_token : !cir.eh_token [ | |
| // Taken when the exception is *not* one of the permitted types. | |
| filter(@_ZTIi) : ^bb4, | |
| // Taken when it is. | |
| unwind : ^bb5 | |
| ] | |
| ``` | |
| A `filter` clause names the permitted types, but its destination is | |
| taken on the types it does *not* name. This is the opposite polarity | |
| from a `catch` clause, whose destination is taken when the exception | |
| does match the named type, so the two clause kinds cannot be read the | |
| same way. The polarity comes from the Itanium personality routine, | |
| which reports a filter *failure* by selecting the filter clause of the | |
| landing pad, and it is preserved in the flattened form so that the | |
| dispatch operation maps directly onto the landing pad it lowers to. | |
| Unlike `catch_all` and `unwind`, a `filter` clause also does not take | |
| the place of the dispatch operation's default destination. A filter has | |
| two outgoing edges rather than one. Either the exception violates the | |
| specification, in which case control transfers to the filter clause's | |
| destination, or it does not, in which case control continues along the | |
| dispatch operation's normal `unwind` edge. A `cir.eh.dispatch` operation | |
| carrying a `filter` clause therefore always carries an `unwind` clause | |
| as well. | |
| The two clauses correspond directly to the two handler regions, with the | |
| polarity inversion visible in the pairing. The unexpected region becomes | |
| the destination of the `filter` clause, and the filter region, which | |
| describes the permitted path, becomes the `unwind` destination. | |
| ```mlir | |
| ^bb4(%eh_token : !cir.eh_token): // Flattened unexpected region | |
| cir.eh.unexpected %eh_token : !cir.eh_token | |
| ^bb5(%eh_token.1 : !cir.eh_token): // Flattened filter region | |
| cir.resume %eh_token.1 : !cir.eh_token | |
| ``` | |
| Because the filter and unexpected handlers are the only handlers on the | |
| try operation, such a dispatch never carries catch clauses of its own. | |
| The catch clauses of a try statement nested inside the specification | |
| belong to that statement's own dispatch operation, which is chained | |
| ahead of this one. | |
| The shape is the same when the filter type list is empty. The filter | |
| region's `cir.unreachable` becomes the `unwind` destination | |
| and the dispatch operation still carries both clauses. ABI lowering then | |
| makes the branch to the filter destination unconditional, which leaves | |
| that `unwind` destination unreachable and dead. | |
| #### Example: Simple dynamic exception specification | |
| **High-level CIR** | |
| ```mlir | |
| cir.func @_Z6targetv() personality(@__gxx_personality_v0) { | |
| cir.try { | |
| cir.call @_Z8externalv() : () -> () | |
| cir.yield | |
| } filter [@_ZTIi] (%eh_token : !cir.eh_token) { | |
| cir.resume %eh_token : !cir.eh_token | |
| } unexpected (%eh_token.1 : !cir.eh_token) { | |
| cir.eh.unexpected %eh_token.1 : !cir.eh_token | |
| } | |
| cir.return | |
| } | |
| ``` | |
| **Flattened CIR** | |
| ```mlir | |
| cir.func @_Z6targetv() personality(@__gxx_personality_v0) { | |
| cir.try_call @_Z8externalv() ^bb1, ^bb2 : () -> () | |
| ^bb1: // Normal continue (from entry block) | |
| cir.br ^bb6 | |
| ^bb2: // EH (from entry block) | |
| %0 = cir.eh.initiate : !cir.eh_token | |
| cir.br ^bb3(%0 : !cir.eh_token) | |
| ^bb3(%eh_token : !cir.eh_token): // Exception specification dispatch | |
| cir.eh.dispatch %eh_token : !cir.eh_token [ | |
| filter(@_ZTIi) : ^bb4, // Not an int: specification violated | |
| unwind : ^bb5 // An int: permitted, keep unwinding | |
| ] | |
| ^bb4(%eh_token.1 : !cir.eh_token): // Specification violated | |
| cir.eh.unexpected %eh_token.1 : !cir.eh_token | |
| ^bb5(%eh_token.2 : !cir.eh_token): // Exception is permitted | |
| cir.resume %eh_token.2 : !cir.eh_token | |
| ^bb6: // Normal continue (from ^bb1) | |
| cir.return | |
| } | |
| ``` | |
| If `external()` throws, control transfers to `^bb2`, which initiates | |
| exception handling and branches to the dispatch block (`^bb3`). If the | |
| exception is not one of the permitted types, control transfers to `^bb4` | |
| and `cir.eh.unexpected` terminates the block. Otherwise control | |
| transfers to `^bb5` and unwinding continues to the caller. | |
| ### Itanium ABI Lowering | |
| The Itanium representation of an exception specification is a `filter` | |
| clause on the landing pad. Accordingly, the `cir.eh.inflight_exception` | |
| operation gains a `filter` clause carrying the permitted type info | |
| symbols. | |
| ```mlir | |
| %exception_ptr, %type_id = cir.eh.inflight_exception filter [@_ZTIi] | |
| ``` | |
| This corresponds directly to the `filter` clause of the LLVM IR | |
| landingpad instruction. An empty list lowers to a zero-length filter | |
| clause, which the personality routine treats as permitting nothing. | |
| The clause list of a landing pad is built from the handlers that the | |
| exception reaching that landing pad can arrive at, listed innermost | |
| first. Since the try operation for an exception specification encloses | |
| the entire function body, its filter clause is always last, after any | |
| catch clauses contributed by try operations written in the source. A | |
| filter clause terminates the clause list in the same way that a | |
| catch-all clause does, because no handler outside the function can be | |
| reached. A filter clause and the `cleanup` attribute may both appear on | |
| the same landing pad, since cleanups within the function still have to | |
| run before the specification is checked. | |
| The `filter` clause of a `cir.eh.dispatch` operation is lowered to a | |
| signed comparison of the type id against zero. The personality routine | |
| reports that an exception failed a filter by returning a *negative* | |
| selector value, which is why the type id produced by | |
| `cir.eh.inflight_exception` is a signed integer. Catch matching only | |
| compares the type id for equality and is therefore indifferent to its | |
| signedness, but filter checking is not. When the filter type list is | |
| empty, no comparison is generated and the filter destination is branched | |
| to unconditionally. | |
| The `cir.eh.unexpected` operation is lowered to a call to | |
| `__cxa_call_unexpected`, marked `noreturn`, followed by a | |
| `cir.unreachable` operation. | |
| #### Example: Simple dynamic exception specification | |
| **Flattened CIR** | |
| ```mlir | |
| cir.func @_Z6targetv() personality(@__gxx_personality_v0) { | |
| cir.try_call @_Z8externalv() ^bb1, ^bb2 : () -> () | |
| ^bb1: // Normal continue (from entry block) | |
| cir.br ^bb6 | |
| ^bb2: // EH (from entry block) | |
| %0 = cir.eh.initiate : !cir.eh_token | |
| cir.br ^bb3(%0 : !cir.eh_token) | |
| ^bb3(%eh_token : !cir.eh_token): // Exception specification dispatch | |
| cir.eh.dispatch %eh_token : !cir.eh_token [ | |
| filter(@_ZTIi) : ^bb4, // Not an int: specification violated | |
| unwind : ^bb5 // An int: permitted, keep unwinding | |
| ] | |
| ^bb4(%eh_token.1 : !cir.eh_token): // Specification violated | |
| cir.eh.unexpected %eh_token.1 : !cir.eh_token | |
| ^bb5(%eh_token.2 : !cir.eh_token): // Exception is permitted | |
| cir.resume %eh_token.2 : !cir.eh_token | |
| ^bb6: // Normal continue (from ^bb1) | |
| cir.return | |
| } | |
| ``` | |
| **ABI-lowered CIR** | |
| ```mlir | |
| cir.func @_Z6targetv() personality(@__gxx_personality_v0) { | |
| cir.try_call @_Z8externalv() ^bb1, ^bb2 : () -> () | |
| ^bb1: // Normal continue (from entry block) | |
| cir.br ^bb6 | |
| ^bb2: // Landing pad (from entry block) | |
| %exception_ptr, %type_id = cir.eh.inflight_exception filter [@_ZTIi] | |
| cir.br ^bb3(%exception_ptr, %type_id : !cir.ptr<!void>, !s32i) | |
| ^bb3(%0: !cir.ptr<!void>, %1: !s32i): // Exception specification dispatch | |
| %2 = cir.const #cir.int<0> : !s32i | |
| %3 = cir.cmp lt %1, %2 : !s32i | |
| cir.brcond %3 ^bb4(%0 : !cir.ptr<!void>), | |
| ^bb5(%0, %1 : !cir.ptr<!void>, !s32i) | |
| ^bb4(%4: !cir.ptr<!void>): // Specification violated | |
| cir.call @__cxa_call_unexpected(%4) {noreturn} : (!cir.ptr<!void>) -> () | |
| cir.unreachable | |
| ^bb5(%5: !cir.ptr<!void>, %6: !s32i): // Exception is permitted | |
| cir.resume.flat %5, %6 | |
| ^bb6: // Normal continue (from ^bb1) | |
| cir.return | |
| } | |
| ``` | |
| In this example the landing pad (`^bb2`) carries a filter clause listing | |
| the single permitted type. The personality routine selects that clause | |
| only when the in-flight exception is *not* an `int`, and signals this by | |
| returning a negative selector value. The dispatch block (`^bb3`) | |
| therefore tests the selector for a negative value and transfers control | |
| to `^bb4` to call `__cxa_call_unexpected` when the test succeeds, or to | |
| `^bb5` to continue unwinding when it fails. | |
| For `target2()`, whose specification permits nothing, the landing pad | |
| carries an empty filter clause and no comparison is needed. | |
| ```mlir | |
| ^bb2: // Landing pad (from entry block) | |
| %exception_ptr, %type_id = cir.eh.inflight_exception filter [] | |
| cir.br ^bb3(%exception_ptr : !cir.ptr<!void>) | |
| ^bb3(%0: !cir.ptr<!void>): // Specification violated | |
| cir.call @__cxa_call_unexpected(%0) {noreturn} : (!cir.ptr<!void>) -> () | |
| cir.unreachable | |
| ``` | |
| In the try-catch example above, the exception thrown by `inner()` can | |
| reach both the catch handler of the try statement in `outer()` and the | |
| filter of the exception specification of `outer()`, so the landing pad | |
| for the call to `inner()` carries both clauses, with the filter clause | |
| last. | |
| ```mlir | |
| %exception_ptr, %type_id = | |
| cir.eh.inflight_exception [@_ZTIi] filter [] | |
| ``` | |
| ### Microsoft C++ ABI Lowering | |
| The Microsoft C++ ABI has no runtime support for dynamic exception | |
| specifications. As in Clang's LLVM IR codegen, no exception | |
| specification try operation is generated when targeting that ABI, and | |
| the specification has no effect on the generated code. |