Skip to content

[codegen] Emit missing cleanups for stmt-expr and coro suspensions [take-2] #85398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Apr 10, 2024

Conversation

usx95
Copy link
Contributor

@usx95 usx95 commented Mar 15, 2024

Fixes #63818 for control flow out of an expressions.

Background

A control flow could happen in the middle of an expression due to stmt-expr and coroutine suspensions.

Due to branch-in-expr, we miss running cleanups for the temporaries constructed in the expression before the branch.
Previously, these cleanups were only added as EHCleanup during the expression and as normal expression after the full expression.

Examples of such deferred cleanups include:

ParenList/InitList: Cleanups for fields are performed by the destructor of the object being constructed.
Array init: Cleanup for elements of an array is included in the array cleanup.
Lifetime-extended temporaries: reference-binding temporaries in braced-init are lifetime extended to the parent scope.
Lambda capture init: init in the lambda capture list is destroyed by the lambda object.


In this PR

In this PR, we change some of the EHCleanups cleanups to NormalAndEHCleanups to make sure these are emitted when we see a branch inside an expression (through statement expressions or coroutine suspensions).

These are supposed to be deactivated after full expression and destroyed later as part of the destructor of the aggregate or array being constructed. To simplify deactivating cleanups, we add two utilities as well:

  • DeferredDeactivationCleanupStack: A stack to remember cleanups with deferred deactivation.
  • CleanupDeactivationScope: RAII for deactivating cleanups added to the above stack.

Deactivating normal cleanups

These were previously EHCleanups and not Normal and deactivation of required Normal cleanups had some bugs. These specifically include deactivating Normal cleanups which are not the top of EHStack source1, 2. This has not been part of our test suite (maybe it was never required before statement expressions). In this PR, we also fix the emission of required-deactivated-normal cleanups.


Test ideas

I have tried out printf-style ctor-dtor calls in the following cases manually and verified that the correct dtors are invoked: https://godbolt.org/z/jMYTK7Kxe
I would be open to ideas for better tests than just lit-test for LLVM IR. We might benefit from some integration tests which can run the program and assert

@usx95 usx95 changed the title [codegen] Emit cleanups for branch in stmt-expr and coro suspensions [codegen] Emit cleanups for branch in stmt-expr and coro suspensions [take-2] Mar 15, 2024
@usx95 usx95 marked this pull request as ready for review March 15, 2024 15:36
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:codegen IR generation bugs: mangling, exceptions, etc. coroutines C++20 coroutines labels Mar 15, 2024
@llvmbot
Copy link
Member

llvmbot commented Mar 15, 2024

@llvm/pr-subscribers-coroutines

@llvm/pr-subscribers-clang-codegen

Author: Utkarsh Saxena (usx95)

Changes

Partially addresses #63818 for control flow in expressions.

A control flow could happen in the middle of an expression due to stmt-expr and coroutine suspensions.

Due to branch-in-expr, we miss running cleanups for the temporaries constructed in the expression before the branch.
Previously these cleanups were only added as EHOnlyCleanup during the expression and as normal expression after the full expression.

Examples of such deferred cleanups include:

ParenList/InitList: Cleanups for fields are performed by the destructor of the object being constructed.
Array init: Cleanup for elements of an array is included in the array cleanup.
Lifetime-extended temporaries: reference-binding temporaries in braced-init are lifetime extended to the parent scope.
Lambda capture init: init in lambda capture list are destroyed by the lambda object.

In this PR, we change some of the EHOnly cleanups to NormalAndEHCleanups to make sure these are emitted when we see a branch inside an expression (through statement expressions or coroutine suspensions).

These are supposed deactivated after the full expression and destroyed later as part of the destructor of the aggregate /array being constructed.

Since these were previously EHOnly cleanups and not Normal, deactivation of required Normal but used cleanups had some bugs and was never tested (maybe it was never required before statement expressions). In this PR, we also fix the emission of required-deactivated-normal cleanups.

TODO: Add more tests here.

I will change some more EHCleanups to Normal in separate changes later. Array initialization and lifetime extension cleanups are intentionally left out as they need more attention but are very similar.


Full diff: https://github.com/llvm/llvm-project/pull/85398.diff

3 Files Affected:

  • (modified) clang/lib/CodeGen/CGCleanup.cpp (+5-2)
  • (modified) clang/lib/CodeGen/CGExprAgg.cpp (+7-6)
  • (added) clang/test/CodeGenCoroutines/control-flow-in-expr-cleanup.cpp (+172)
diff --git a/clang/lib/CodeGen/CGCleanup.cpp b/clang/lib/CodeGen/CGCleanup.cpp
index f87caf050eeaa7..c22d4da0fefc3c 100644
--- a/clang/lib/CodeGen/CGCleanup.cpp
+++ b/clang/lib/CodeGen/CGCleanup.cpp
@@ -694,7 +694,8 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough) {
 
   // - whether there's a fallthrough
   llvm::BasicBlock *FallthroughSource = Builder.GetInsertBlock();
-  bool HasFallthrough = (FallthroughSource != nullptr && IsActive);
+  bool HasFallthrough =
+      FallthroughSource != nullptr && (IsActive || HasExistingBranches);
 
   // Branch-through fall-throughs leave the insertion point set to the
   // end of the last cleanup, which points to the current scope.  The
@@ -719,7 +720,9 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough) {
 
   // If we have a prebranched fallthrough into an inactive normal
   // cleanup, rewrite it so that it leads to the appropriate place.
-  if (Scope.isNormalCleanup() && HasPrebranchedFallthrough && !IsActive) {
+  if (Scope.isNormalCleanup() && HasPrebranchedFallthrough &&
+      !RequiresNormalCleanup) {
+    assert(!IsActive);
     llvm::BasicBlock *prebranchDest;
 
     // If the prebranch is semantically branching through the next
diff --git a/clang/lib/CodeGen/CGExprAgg.cpp b/clang/lib/CodeGen/CGExprAgg.cpp
index 5190b22bcc1622..7e62599089b8df 100644
--- a/clang/lib/CodeGen/CGExprAgg.cpp
+++ b/clang/lib/CodeGen/CGExprAgg.cpp
@@ -15,6 +15,7 @@
 #include "CodeGenFunction.h"
 #include "CodeGenModule.h"
 #include "ConstantEmitter.h"
+#include "EHScopeStack.h"
 #include "TargetInfo.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/Attr.h"
@@ -1389,15 +1390,15 @@ AggExprEmitter::VisitLambdaExpr(LambdaExpr *E) {
     if (QualType::DestructionKind DtorKind =
             CurField->getType().isDestructedType()) {
       assert(LV.isSimple());
-      if (CGF.needsEHCleanup(DtorKind)) {
+      if (DtorKind) {
         if (!CleanupDominator)
           CleanupDominator = CGF.Builder.CreateAlignedLoad(
               CGF.Int8Ty,
               llvm::Constant::getNullValue(CGF.Int8PtrTy),
               CharUnits::One()); // placeholder
 
-        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), CurField->getType(),
-                        CGF.getDestroyer(DtorKind), false);
+        CGF.pushDestroy(NormalAndEHCleanup, LV.getAddress(CGF),
+                        CurField->getType(), CGF.getDestroyer(DtorKind), false);
         Cleanups.push_back(CGF.EHStack.stable_begin());
       }
     }
@@ -1806,9 +1807,9 @@ void AggExprEmitter::VisitCXXParenListOrInitListExpr(
     if (QualType::DestructionKind dtorKind
           = field->getType().isDestructedType()) {
       assert(LV.isSimple());
-      if (CGF.needsEHCleanup(dtorKind)) {
-        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), field->getType(),
-                        CGF.getDestroyer(dtorKind), false);
+      if (dtorKind) {
+        CGF.pushDestroy(NormalAndEHCleanup, LV.getAddress(CGF),
+                        field->getType(), CGF.getDestroyer(dtorKind), false);
         addCleanup(CGF.EHStack.stable_begin());
         pushedCleanup = true;
       }
diff --git a/clang/test/CodeGenCoroutines/control-flow-in-expr-cleanup.cpp b/clang/test/CodeGenCoroutines/control-flow-in-expr-cleanup.cpp
new file mode 100644
index 00000000000000..d703532b5a10b9
--- /dev/null
+++ b/clang/test/CodeGenCoroutines/control-flow-in-expr-cleanup.cpp
@@ -0,0 +1,172 @@
+// RUN: %clang_cc1 --std=c++20 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s
+
+#include "Inputs/coroutine.h"
+
+struct Printy {
+  Printy(const char *name) : name(name) {}
+  ~Printy() {}
+  const char *name;
+};
+
+struct coroutine {
+  struct promise_type;
+  std::coroutine_handle<promise_type> handle;
+  ~coroutine() {
+    if (handle) handle.destroy();
+  }
+};
+
+struct coroutine::promise_type {
+  coroutine get_return_object() {
+    return {std::coroutine_handle<promise_type>::from_promise(*this)};
+  }
+  std::suspend_never initial_suspend() noexcept { return {}; }
+  std::suspend_always final_suspend() noexcept { return {}; }
+  void return_void() {}
+  void unhandled_exception() {}
+};
+
+struct Awaiter : std::suspend_always {
+  Printy await_resume() { return {"awaited"}; }
+};
+
+int foo() { return 2; }
+
+struct PrintiesCopy {
+  Printy a;
+  Printy b;
+  Printy c;
+};
+
+void ParenInit() {
+  // CHECK: define dso_local void @_Z9ParenInitv()
+  // CHECK: [[CLEANUP_DEST:%.+]] = alloca i32, align 4
+  PrintiesCopy ps(Printy("a"), 
+                  // CHECK: call void @_ZN6PrintyC1EPKc
+                  ({
+                    if (foo()) return;
+                    // CHECK:     if.then:
+                    // CHECK-NEXT:   store i32 1, ptr [[CLEANUP_DEST]], align 4
+                    // CHECK-NEXT:   br label %cleanup
+                    Printy("b");
+                    // CHECK:     if.end:
+                    // CHECK-NEXT:  call void @_ZN6PrintyC1EPKc
+                  }),
+                  ({
+                    if (foo()) return;
+                    // CHECK:     if.then{{.*}}:
+                    // CHECK-NEXT:  store i32 1, ptr [[CLEANUP_DEST]], align 4
+                    // CHECK-NEXT:  call void @_ZN6PrintyD1Ev
+                    // CHECK-NEXT:  br label %cleanup
+                    Printy("c");
+                    // CHECK:     if.end{{.*}}:
+                    // CHECK-NEXT:  call void @_ZN6PrintyC1EPKc
+                    // CHECK-NEXT:  call void @_ZN12PrintiesCopyD1Ev
+                    // CHECK-NEXT:  br label %return
+                  }));
+  // CHECK:     cleanup:
+  // CHECK-NEXT:  call void @_ZN6PrintyD1Ev
+  // CHECK-NEXT:  br label %return
+}
+
+coroutine ParenInitCoro() {
+  // CHECK: define dso_local void @_Z13ParenInitCorov
+  // CHECK: [[ACTIVE1:%.+]] = alloca i1, align 1
+  // CHECK: [[ACTIVE2:%.+]] = alloca i1, align 1
+  PrintiesCopy ps(Printy("a"), Printy("b"),
+    // CHECK:       call void @_ZN6PrintyC1EPKc
+    // CHECK-NEXT:  store i1 true, ptr [[ACTIVE1]].reload.addr, align 1
+    // CHECK-NEXT:  store i1 true, ptr [[ACTIVE2]].reload.addr, align 1
+    // CHECK:       call void @_ZN6PrintyC1EPKc
+    co_await Awaiter{}
+
+    // CHECK:     await.cleanup:
+    // CHECK-NEXT:  br label %[[CLEANUP:.+]].from.await.cleanup
+
+    // CHECK:     [[CLEANUP]].from.await.cleanup:
+    // CHECK:       br label %[[CLEANUP]]
+
+    // CHECK:     await.ready:
+    // CHECK:       store i1 false, ptr [[ACTIVE1]].reload.addr, align 1
+    // CHECK-NEXT:  store i1 false, ptr [[ACTIVE2]].reload.addr, align 1 
+    // CHECK-NEXT:  br label %[[CLEANUP]].from.await.ready
+
+    // CHECK:     [[CLEANUP]].from.await.ready:
+    // CHECK:       br label %[[CLEANUP]]
+
+    // CHECK:     [[CLEANUP]]:
+    // CHECK:       [[IS_ACTIVE1:%.+]] = load i1, ptr [[ACTIVE1]].reload.addr, align 1
+    // CHECK-NEXT:  br i1 [[IS_ACTIVE1]], label %[[ACTION1:.+]], label %[[DONE1:.+]]
+
+    // CHECK:     [[ACTION1]]:
+    // CHECK:       call void @_ZN6PrintyD1Ev
+    // CHECK:       br label %[[DONE1]]
+
+    // CHECK:     [[DONE1]]:
+    // CHECK:       [[IS_ACTIVE2:%.+]] = load i1, ptr [[ACTIVE2]].reload.addr, align 1
+    // CHECK-NEXT:  br i1 [[IS_ACTIVE2]], label %[[ACTION2:.+]], label %[[DONE2:.+]]
+
+    // CHECK:     [[ACTION2]]:
+    // CHECK:       call void @_ZN6PrintyD1Ev
+    // CHECK:       br label %[[DONE2]]
+  );
+}
+
+// TODO: Add more assertions after preliminary review.
+// struct S {
+//   Printy arr1[2];
+//   Printy arr2[2];
+//   Printy p;
+// };
+
+// void ArraySubobjects() {
+//   S s{{Printy("a"), Printy("b")},
+//       {Printy("a"), ({
+//          if (foo() == 1) {
+//            return;
+//          }
+//          Printy("b");
+//        })},
+//       ({
+//         if (foo() == 2) {
+//           return;
+//         }
+//         Printy("b");
+//       })};
+// }
+
+// coroutine ArraySubobjectsCoro() {
+//   S s{{Printy("a"), Printy("b")},
+//       {Printy("a"), co_await Awaiter{}},
+//       co_await Awaiter{}};
+// }
+
+// struct A {
+//   Printy a;
+// };
+// struct B : A {
+//   Printy b;
+//   Printy c;
+// };
+
+// void BaseClassCtors() {
+//   auto S = B({Printy("a")}, Printy("b"), ({
+//                return;
+//                Printy("c");
+//              }));
+// }
+
+// coroutine BaseClassCtorsCoro() {
+//   auto S = B({Printy("a")}, Printy("b"), co_await Awaiter{});
+// }
+
+// void LambdaInit() {
+//   auto S = [a = Printy("a"), b = ({
+//                                return;
+//                                Printy("b");
+//                              })]() { return a; };
+// }
+
+// coroutine LambdaInitCoro() {
+//   auto S = [a = Printy("a"), b = co_await Awaiter{}]() { return a; };
+// }

@llvmbot
Copy link
Member

llvmbot commented Mar 15, 2024

@llvm/pr-subscribers-clang

Author: Utkarsh Saxena (usx95)

Changes

Partially addresses #63818 for control flow in expressions.

A control flow could happen in the middle of an expression due to stmt-expr and coroutine suspensions.

Due to branch-in-expr, we miss running cleanups for the temporaries constructed in the expression before the branch.
Previously these cleanups were only added as EHOnlyCleanup during the expression and as normal expression after the full expression.

Examples of such deferred cleanups include:

ParenList/InitList: Cleanups for fields are performed by the destructor of the object being constructed.
Array init: Cleanup for elements of an array is included in the array cleanup.
Lifetime-extended temporaries: reference-binding temporaries in braced-init are lifetime extended to the parent scope.
Lambda capture init: init in lambda capture list are destroyed by the lambda object.

In this PR, we change some of the EHOnly cleanups to NormalAndEHCleanups to make sure these are emitted when we see a branch inside an expression (through statement expressions or coroutine suspensions).

These are supposed deactivated after the full expression and destroyed later as part of the destructor of the aggregate /array being constructed.

Since these were previously EHOnly cleanups and not Normal, deactivation of required Normal but used cleanups had some bugs and was never tested (maybe it was never required before statement expressions). In this PR, we also fix the emission of required-deactivated-normal cleanups.

TODO: Add more tests here.

I will change some more EHCleanups to Normal in separate changes later. Array initialization and lifetime extension cleanups are intentionally left out as they need more attention but are very similar.


Full diff: https://github.com/llvm/llvm-project/pull/85398.diff

3 Files Affected:

  • (modified) clang/lib/CodeGen/CGCleanup.cpp (+5-2)
  • (modified) clang/lib/CodeGen/CGExprAgg.cpp (+7-6)
  • (added) clang/test/CodeGenCoroutines/control-flow-in-expr-cleanup.cpp (+172)
diff --git a/clang/lib/CodeGen/CGCleanup.cpp b/clang/lib/CodeGen/CGCleanup.cpp
index f87caf050eeaa7..c22d4da0fefc3c 100644
--- a/clang/lib/CodeGen/CGCleanup.cpp
+++ b/clang/lib/CodeGen/CGCleanup.cpp
@@ -694,7 +694,8 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough) {
 
   // - whether there's a fallthrough
   llvm::BasicBlock *FallthroughSource = Builder.GetInsertBlock();
-  bool HasFallthrough = (FallthroughSource != nullptr && IsActive);
+  bool HasFallthrough =
+      FallthroughSource != nullptr && (IsActive || HasExistingBranches);
 
   // Branch-through fall-throughs leave the insertion point set to the
   // end of the last cleanup, which points to the current scope.  The
@@ -719,7 +720,9 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough) {
 
   // If we have a prebranched fallthrough into an inactive normal
   // cleanup, rewrite it so that it leads to the appropriate place.
-  if (Scope.isNormalCleanup() && HasPrebranchedFallthrough && !IsActive) {
+  if (Scope.isNormalCleanup() && HasPrebranchedFallthrough &&
+      !RequiresNormalCleanup) {
+    assert(!IsActive);
     llvm::BasicBlock *prebranchDest;
 
     // If the prebranch is semantically branching through the next
diff --git a/clang/lib/CodeGen/CGExprAgg.cpp b/clang/lib/CodeGen/CGExprAgg.cpp
index 5190b22bcc1622..7e62599089b8df 100644
--- a/clang/lib/CodeGen/CGExprAgg.cpp
+++ b/clang/lib/CodeGen/CGExprAgg.cpp
@@ -15,6 +15,7 @@
 #include "CodeGenFunction.h"
 #include "CodeGenModule.h"
 #include "ConstantEmitter.h"
+#include "EHScopeStack.h"
 #include "TargetInfo.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/Attr.h"
@@ -1389,15 +1390,15 @@ AggExprEmitter::VisitLambdaExpr(LambdaExpr *E) {
     if (QualType::DestructionKind DtorKind =
             CurField->getType().isDestructedType()) {
       assert(LV.isSimple());
-      if (CGF.needsEHCleanup(DtorKind)) {
+      if (DtorKind) {
         if (!CleanupDominator)
           CleanupDominator = CGF.Builder.CreateAlignedLoad(
               CGF.Int8Ty,
               llvm::Constant::getNullValue(CGF.Int8PtrTy),
               CharUnits::One()); // placeholder
 
-        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), CurField->getType(),
-                        CGF.getDestroyer(DtorKind), false);
+        CGF.pushDestroy(NormalAndEHCleanup, LV.getAddress(CGF),
+                        CurField->getType(), CGF.getDestroyer(DtorKind), false);
         Cleanups.push_back(CGF.EHStack.stable_begin());
       }
     }
@@ -1806,9 +1807,9 @@ void AggExprEmitter::VisitCXXParenListOrInitListExpr(
     if (QualType::DestructionKind dtorKind
           = field->getType().isDestructedType()) {
       assert(LV.isSimple());
-      if (CGF.needsEHCleanup(dtorKind)) {
-        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), field->getType(),
-                        CGF.getDestroyer(dtorKind), false);
+      if (dtorKind) {
+        CGF.pushDestroy(NormalAndEHCleanup, LV.getAddress(CGF),
+                        field->getType(), CGF.getDestroyer(dtorKind), false);
         addCleanup(CGF.EHStack.stable_begin());
         pushedCleanup = true;
       }
diff --git a/clang/test/CodeGenCoroutines/control-flow-in-expr-cleanup.cpp b/clang/test/CodeGenCoroutines/control-flow-in-expr-cleanup.cpp
new file mode 100644
index 00000000000000..d703532b5a10b9
--- /dev/null
+++ b/clang/test/CodeGenCoroutines/control-flow-in-expr-cleanup.cpp
@@ -0,0 +1,172 @@
+// RUN: %clang_cc1 --std=c++20 -triple x86_64-linux-gnu -emit-llvm %s -o - | FileCheck %s
+
+#include "Inputs/coroutine.h"
+
+struct Printy {
+  Printy(const char *name) : name(name) {}
+  ~Printy() {}
+  const char *name;
+};
+
+struct coroutine {
+  struct promise_type;
+  std::coroutine_handle<promise_type> handle;
+  ~coroutine() {
+    if (handle) handle.destroy();
+  }
+};
+
+struct coroutine::promise_type {
+  coroutine get_return_object() {
+    return {std::coroutine_handle<promise_type>::from_promise(*this)};
+  }
+  std::suspend_never initial_suspend() noexcept { return {}; }
+  std::suspend_always final_suspend() noexcept { return {}; }
+  void return_void() {}
+  void unhandled_exception() {}
+};
+
+struct Awaiter : std::suspend_always {
+  Printy await_resume() { return {"awaited"}; }
+};
+
+int foo() { return 2; }
+
+struct PrintiesCopy {
+  Printy a;
+  Printy b;
+  Printy c;
+};
+
+void ParenInit() {
+  // CHECK: define dso_local void @_Z9ParenInitv()
+  // CHECK: [[CLEANUP_DEST:%.+]] = alloca i32, align 4
+  PrintiesCopy ps(Printy("a"), 
+                  // CHECK: call void @_ZN6PrintyC1EPKc
+                  ({
+                    if (foo()) return;
+                    // CHECK:     if.then:
+                    // CHECK-NEXT:   store i32 1, ptr [[CLEANUP_DEST]], align 4
+                    // CHECK-NEXT:   br label %cleanup
+                    Printy("b");
+                    // CHECK:     if.end:
+                    // CHECK-NEXT:  call void @_ZN6PrintyC1EPKc
+                  }),
+                  ({
+                    if (foo()) return;
+                    // CHECK:     if.then{{.*}}:
+                    // CHECK-NEXT:  store i32 1, ptr [[CLEANUP_DEST]], align 4
+                    // CHECK-NEXT:  call void @_ZN6PrintyD1Ev
+                    // CHECK-NEXT:  br label %cleanup
+                    Printy("c");
+                    // CHECK:     if.end{{.*}}:
+                    // CHECK-NEXT:  call void @_ZN6PrintyC1EPKc
+                    // CHECK-NEXT:  call void @_ZN12PrintiesCopyD1Ev
+                    // CHECK-NEXT:  br label %return
+                  }));
+  // CHECK:     cleanup:
+  // CHECK-NEXT:  call void @_ZN6PrintyD1Ev
+  // CHECK-NEXT:  br label %return
+}
+
+coroutine ParenInitCoro() {
+  // CHECK: define dso_local void @_Z13ParenInitCorov
+  // CHECK: [[ACTIVE1:%.+]] = alloca i1, align 1
+  // CHECK: [[ACTIVE2:%.+]] = alloca i1, align 1
+  PrintiesCopy ps(Printy("a"), Printy("b"),
+    // CHECK:       call void @_ZN6PrintyC1EPKc
+    // CHECK-NEXT:  store i1 true, ptr [[ACTIVE1]].reload.addr, align 1
+    // CHECK-NEXT:  store i1 true, ptr [[ACTIVE2]].reload.addr, align 1
+    // CHECK:       call void @_ZN6PrintyC1EPKc
+    co_await Awaiter{}
+
+    // CHECK:     await.cleanup:
+    // CHECK-NEXT:  br label %[[CLEANUP:.+]].from.await.cleanup
+
+    // CHECK:     [[CLEANUP]].from.await.cleanup:
+    // CHECK:       br label %[[CLEANUP]]
+
+    // CHECK:     await.ready:
+    // CHECK:       store i1 false, ptr [[ACTIVE1]].reload.addr, align 1
+    // CHECK-NEXT:  store i1 false, ptr [[ACTIVE2]].reload.addr, align 1 
+    // CHECK-NEXT:  br label %[[CLEANUP]].from.await.ready
+
+    // CHECK:     [[CLEANUP]].from.await.ready:
+    // CHECK:       br label %[[CLEANUP]]
+
+    // CHECK:     [[CLEANUP]]:
+    // CHECK:       [[IS_ACTIVE1:%.+]] = load i1, ptr [[ACTIVE1]].reload.addr, align 1
+    // CHECK-NEXT:  br i1 [[IS_ACTIVE1]], label %[[ACTION1:.+]], label %[[DONE1:.+]]
+
+    // CHECK:     [[ACTION1]]:
+    // CHECK:       call void @_ZN6PrintyD1Ev
+    // CHECK:       br label %[[DONE1]]
+
+    // CHECK:     [[DONE1]]:
+    // CHECK:       [[IS_ACTIVE2:%.+]] = load i1, ptr [[ACTIVE2]].reload.addr, align 1
+    // CHECK-NEXT:  br i1 [[IS_ACTIVE2]], label %[[ACTION2:.+]], label %[[DONE2:.+]]
+
+    // CHECK:     [[ACTION2]]:
+    // CHECK:       call void @_ZN6PrintyD1Ev
+    // CHECK:       br label %[[DONE2]]
+  );
+}
+
+// TODO: Add more assertions after preliminary review.
+// struct S {
+//   Printy arr1[2];
+//   Printy arr2[2];
+//   Printy p;
+// };
+
+// void ArraySubobjects() {
+//   S s{{Printy("a"), Printy("b")},
+//       {Printy("a"), ({
+//          if (foo() == 1) {
+//            return;
+//          }
+//          Printy("b");
+//        })},
+//       ({
+//         if (foo() == 2) {
+//           return;
+//         }
+//         Printy("b");
+//       })};
+// }
+
+// coroutine ArraySubobjectsCoro() {
+//   S s{{Printy("a"), Printy("b")},
+//       {Printy("a"), co_await Awaiter{}},
+//       co_await Awaiter{}};
+// }
+
+// struct A {
+//   Printy a;
+// };
+// struct B : A {
+//   Printy b;
+//   Printy c;
+// };
+
+// void BaseClassCtors() {
+//   auto S = B({Printy("a")}, Printy("b"), ({
+//                return;
+//                Printy("c");
+//              }));
+// }
+
+// coroutine BaseClassCtorsCoro() {
+//   auto S = B({Printy("a")}, Printy("b"), co_await Awaiter{});
+// }
+
+// void LambdaInit() {
+//   auto S = [a = Printy("a"), b = ({
+//                                return;
+//                                Printy("b");
+//                              })]() { return a; };
+// }
+
+// coroutine LambdaInitCoro() {
+//   auto S = [a = Printy("a"), b = co_await Awaiter{}]() { return a; };
+// }

@llvmbot llvmbot added the clang:frontend Language frontend issues, e.g. anything involving "Sema" label Mar 18, 2024
@usx95 usx95 changed the title [codegen] Emit cleanups for branch in stmt-expr and coro suspensions [take-2] [codegen] Emit missing cleanups for stmt-expr and coro suspensions [take-2] Mar 20, 2024
@usx95
Copy link
Contributor Author

usx95 commented Mar 20, 2024

Friendly ping for review: @jyknight @zygoloid @efriedma-quic

@usx95
Copy link
Contributor Author

usx95 commented Mar 25, 2024

Ping for review: @AaronBallman @jyknight @efriedma-quic @zygoloid
Could you please suggest other reviewers if you do not have the bandwidth for the same?

@Sirraide
Copy link
Member

I’m the wrong person to ping if it’s anything codegen-related; I’ll leave reviewing this to people more familiar w/ that part of Clang.

@usx95 usx95 removed the request for review from Sirraide March 25, 2024 11:22
@AaronBallman AaronBallman requested a review from rjmccall March 25, 2024 12:35
Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully @rjmccall or @efriedma-quic can take a look at the codegen bits. I did have some questions about other kinds of flow control statements we might want to cover.

Copy link

✅ With the latest revision this PR passed the C/C++ code formatter.

@usx95 usx95 changed the base branch from main to release/18.x March 31, 2024 21:20
@usx95 usx95 changed the base branch from release/18.x to main March 31, 2024 21:20
@usx95 usx95 requested a review from efriedma-quic April 9, 2024 18:46
Copy link
Collaborator

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ayermolo
Copy link
Contributor

I think this commit is causing build failure when building clangd in debug mode with clang built in release mode.

Instruction does not dominate all uses!
  %K1104 = getelementptr inbounds %"struct.llvm::json::Object::KV", ptr %arrayinit.begin1100, i32 0, i32 0, !dbg !93928
  call void @_ZN4llvm4json9ObjectKeyD2Ev(ptr noundef nonnull align 8 dereferenceable(24) %K1104) #21, !dbg !93937
fatal error: error in backend: Broken module found, compilation aborted!
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace, preprocessed source, and associated run script.
Stack dump:
0.      Program arguments: /home/ayermolo/local/llvm-build-upstream-release/bin/clang++ -D_DEBUG -D_GLIBCXX_ASSERTIONS -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -Itools/clang/tools/extra/clangd -I/home/ayermolo/local/upstream-llvm/llvm-project/clang-tools-extra/clangd -I/home/ayermolo/local/upstream-llvm/llvm-project/clang-tools-extra/clangd/../include-cleaner/include -Itools/clang/tools/extra/clangd/../clang-tidy -I/home/ayermolo/local/upstream-llvm/llvm-project/clang/include -Itools/clang/include -Iinclude -I/home/ayermolo/local/upstream-llvm/llvm-project/llvm/include -I/home/ayermolo/local/upstream-llvm/llvm-project/clang-tools-extra/pseudo/lib/../include -isystem/home/ayermolo/local/llvm-build-upstream-llvm/build/lib/clang/19/include -fPIC -fno-semantic-interposition -fvisibility-inlines-hidden -Werror=date-time -Werror=unguarded-availability-new -Wall -Wextra -Wno-unused-parameter -Wwrite-strings -Wcast-qual -Wmissing-field-initializers -pedantic -Wno-long-long -Wc++98-compat-extra-semi -Wimplicit-fallthrough -Wcovered-switch-default -Wno-noexcept-type -Wnon-virtual-dtor -Wdelete-non-virtual-dtor -Wsuggest-override -Wstring-conversion -Wmisleading-indentation -Wctad-maybe-unsupported -fdiagnostics-color -fno-common -Woverloaded-virtual -Wno-nested-anon-types -g -std=c++17 -MD -MT /home/ayermolo/local/llvm-build-upstream-debug/tools/clang/tools/extra/clangd/CMakeFiles/obj.clangDaemon.dir/ClangdLSPServer.cpp.o -MF /home/ayermolo/local/llvm-build-upstream-debug/tools/clang/tools/extra/clangd/CMakeFiles/obj.clangDaemon.dir/ClangdLSPServer.cpp.o.d -o /home/ayermolo/local/llvm-build-upstream-debug/tools/clang/tools/extra/clangd/CMakeFiles/obj.clangDaemon.dir/ClangdLSPServer.cpp.o -c /home/ayermolo/local/upstream-llvm/llvm-project/clang-tools-extra/clangd/ClangdLSPServer.cpp

Dropping this commit and running above clang build command makes it pass.
build command for easier reading:

COMP=/home/ayermolo/local/llvm-build-upstream-release/bin
$COMP/clang++ -D_DEBUG -D_GLIBCXX_ASSERTIONS -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS \
  -Itools/clang/tools/extra/clangd \
  -I/home/ayermolo/local/upstream-llvm/llvm-project/clang-tools-extra/clangd \
  -I/home/ayermolo/local/upstream-llvm/llvm-project/clang-tools-extra/clangd/../include-cleaner/include \
  -Itools/clang/tools/extra/clangd/../clang-tidy \
  -I/home/ayermolo/local/upstream-llvm/llvm-project/clang/include \
  -Itools/clang/include \
  -Iinclude \
  -I/home/ayermolo/local/upstream-llvm/llvm-project/llvm/include \
  -I/home/ayermolo/local/upstream-llvm/llvm-project/clang-tools-extra/pseudo/lib/../include \
  -isystem/home/ayermolo/local/llvm-build-upstream-llvm/build/lib/clang/19/include \
  -fPIC -fno-semantic-interposition -fvisibility-inlines-hidden -Werror=date-time -Werror=unguarded-availability-new -Wall -Wextra \
  -Wno-unused-parameter -Wwrite-strings -Wcast-qual -Wmissing-field-initializers -pedantic -Wno-long-long -Wc++98-compat-extra-semi \
  -Wimplicit-fallthrough -Wcovered-switch-default -Wno-noexcept-type -Wnon-virtual-dtor -Wdelete-non-virtual-dtor -Wsuggest-override \
  -Wstring-conversion -Wmisleading-indentation -Wctad-maybe-unsupported -fdiagnostics-color -fno-common -Woverloaded-virtual \
  -Wno-nested-anon-types -g -std=c++17 -MD \
  -MT /home/ayermolo/local/llvm-build-upstream-debug/tools/clang/tools/extra/clangd/CMakeFiles/obj.clangDaemon.dir/ClangdLSPServer.cpp.o \
  -MF /home/ayermolo/local/llvm-build-upstream-debug/tools/clang/tools/extra/clangd/CMakeFiles/obj.clangDaemon.dir/ClangdLSPServer.cpp.o.d \
  -o  /home/ayermolo/local/llvm-build-upstream-debug/tools/clang/tools/extra/clangd/CMakeFiles/obj.clangDaemon.dir/ClangdLSPServer.cpp.o \
  -c  /home/ayermolo/local/upstream-llvm/llvm-project/clang-tools-extra/clangd/ClangdLSPServer.cpp

@ayermolo
Copy link
Contributor

@usx95 can you repro?
Also is there ETA on a fix, and if not can you revert this?

@usx95
Copy link
Contributor Author

usx95 commented Apr 14, 2024

@ayermolo Thanks for the reproducer. I was able to reproduce it. Sent out fix #88670

usx95 added a commit to usx95/llvm-project that referenced this pull request Apr 16, 2024
usx95 added a commit that referenced this pull request Apr 29, 2024
)

Latest diff:
https://github.com/llvm/llvm-project/pull/89154/files/f1ab4c2677394bbfc985d9680d5eecd7b2e6a882..adf9bc902baddb156c83ce0f8ec03c142e806d45

We address two additional bugs here: 

### Problem 1: Deactivated normal cleanup still runs, leading to
double-free
Consider the following:
```cpp

struct A { };

struct B { B(const A&); };

struct S {
  A a;
  B b;
};

int AcceptS(S s);

void Accept2(int x, int y);

void Test() {
  Accept2(AcceptS({.a = A{}, .b = A{}}), ({ return; 0; }));
}
```
We add cleanups as follows:
1. push dtor for field `S::a`
2. push dtor for temp `A{}` (used by ` B(const A&)` in `.b = A{}`)
3. push dtor for field `S::b`
4. Deactivate 3 `S::b`-> This pops the cleanup.
5. Deactivate 1 `S::a` -> Does not pop the cleanup as *2* is top. Should
create _active flag_!!
6. push dtor for `~S()`.
7. ...

It is important to deactivate **5** using active flags. Without the
active flags, the `return` will fallthrough it and would run both `~S()`
and dtor `S::a` leading to **double free** of `~A()`.
In this patch, we unconditionally emit active flags while deactivating
normal cleanups. These flags are deleted later by the `AllocaTracker` if
the cleanup is not emitted.

### Problem 2: Missing cleanup for conditional lifetime extension
We push 2 cleanups for lifetime-extended cleanup. The first cleanup is
useful if we exit from the middle of the expression (stmt-expr/coro
suspensions). This is deactivated after full-expr, and a new cleanup is
pushed, extending the lifetime of the temporaries (to the scope of the
reference being initialized).
If this lifetime extension happens to be conditional, then we use active
flags to remember whether the branch was taken and if the object was
initialized.
Previously, we used a **single** active flag, which was used by both
cleanups. This is wrong because the first cleanup will be forced to
deactivate after the full-expr and therefore this **active** flag will
always be **inactive**. The dtor for the lifetime extended entity would
not run as it always sees an **inactive** flag.

In this patch, we solve this using two separate active flags for both
cleanups. Both of them are activated if the conditional branch is taken,
but only one of them is deactivated after the full-expr.

---

Fixes #63818
Fixes #88478

---

Previous PR logs:
1. #85398
2. #88670
3. #88751
4. #88884
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:codegen IR generation bugs: mangling, exceptions, etc. clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category coroutines C++20 coroutines
Projects
None yet
Development

Successfully merging this pull request may close these issues.

destructors not always properly run when a coroutine is suspended and then destroyed
7 participants