llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT--> @llvm/pr-subscribers-clang Author: Andy Kaylor (andykaylor) <details> <summary>Changes</summary> This change adds a document describing a new design for C++ cleanups and exception handling in CIR. --- Patch is 48.43 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/177625.diff 2 Files Affected: - (added) clang/docs/ClangIRCleanupAndEHDesign.rst (+1258) - (modified) clang/docs/index.rst (+1) ``````````diff diff --git a/clang/docs/ClangIRCleanupAndEHDesign.rst b/clang/docs/ClangIRCleanupAndEHDesign.rst new file mode 100644 index 0000000000000..036db3094bca0 --- /dev/null +++ b/clang/docs/ClangIRCleanupAndEHDesign.rst @@ -0,0 +1,1258 @@ +============================================= +ClangIR Cleanup and Exception Handling Design +============================================= + +.. contents:: + :local: + +Overview +======== + +This document describes the proposed new 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``. + +.. code-block:: + + cir.cleanup.scope { + // body region + } cleanup [eh_only] { + // 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``) 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. + +When an exception is thrown from within a cleanup 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. + +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++** + +.. code-block:: c++ + + void someFunc() { + SomeClass c; + c.doSomething(); + } + +**CIR** + +.. code-block:: + + 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>) -> () + } cleanup { + cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () + } + 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++** + +.. code-block:: c++ + + void someFunc() { + SomeClass c; + SomeClass c2; + c.doSomething(); + SomeClass c3; + c3.doSomething(); + } + +**CIR** + +.. code-block:: + + 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>) -> () + } cleanup { + cir.call @_ZN9SomeClassD1Ev(%2) : (!cir.ptr<!rec_SomeClass>) -> () + } + } cleanup { + cir.call @_ZN9SomeClassD1Ev(%1) : (!cir.ptr<!rec_SomeClass>) -> () + } + } cleanup { + cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () + } + 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++** + +.. code-block:: c++ + + int someFunc() { + int i = 0; + while (true) { + SomeClass c; + if (i == 3) + continue; + if (i == 7) + break; + i = c.get(); + } + return i; + } + +**CIR** + +.. code-block:: + + 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> + } cleanup { + cir.call @_ZN9SomeClassD1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> () + } + } + 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++** + +.. code-block:: c++ + + class Base { + public: + Base(); + ~Base(); + }; + + class Derived : public Base { + public: + Derived() : Base() { f(); } + ~Derived(); + }; + +**CIR** + +.. code-block:: + + 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_only { + %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.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. + +.. code-block:: + + cir.try { + cir.call exception @function() : () -> () + cir.yield + } catch [type #cir.global_view<@_ZTIPf> : !cir.ptr<!u8i>] { + ... + cir.yield + } unwind { + cir.resume + } + +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++** + +.. code-block:: c++ + + void someFunc() { + try { + f(); + } catch (std::exception &e) { + // Do nothing + } + } + +**CIR** + +.. code-block:: + + cir.func @someFunc(){ + cir.scope { + cir.try { + cir.call exception @_Z1fv() : () -> () + cir.yield + } catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] { + cir.yield + } unwind { + cir.resume + } + } + 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++** + +.. code-block:: c++ + + void someFunc() { + try { + f(); + } catch (std::exception &e) { + // Do nothing + } catch (...) { + // Do nothing + } + } + +**CIR** + +.. code-block:: + + cir.func @someFunc(){ + cir.scope { + cir.try { + cir.call exception @_Z1fv() : () -> () + cir.yield + } catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] { + cir.yield + } catch all { + 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++** + +.. code-block:: c++ + + void someFunc() { + try { + SomeClass c; + c.doSomething(); + } catch (...) { + // Do nothing + } + } + +**CIR** + +.. code-block:: + + 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>) -> () + } cleanup { + cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () + } + } catch all { + 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++** + +.. code-block:: c++ + + void someFunc() { + SomeClass c; + try { + c.doSomething(); + } catch (std::exception& e) { + // Do nothing + } + } + +**CIR** + +.. code-block:: + + 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.scope { + cir.try { + cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> () + } catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] { + cir.yield + } unwind { + cir.resume + } + } + } cleanup { + cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () + } + 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. + +.. code-block:: + + 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 + +.. code-block:: + + 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.... [truncated] `````````` </details> https://github.com/llvm/llvm-project/pull/177625 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
