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

Reply via email to