[llvm-branch-commits] [clang] [clang] Use uniform lifetime bounds under exceptions (PR #175817)

Paul Kirth via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Tue Jan 13 11:25:58 PST 2026


https://github.com/ilovepi created https://github.com/llvm/llvm-project/pull/175817

To do this we have to slightly modify how some expressions are handled
in Sema. Principally, we need to ensure that calls to new for
non-trivial types still have their destructors run. Generally this isn't
an issue, since these just get sunk into the surrounding scope. With
more lifetime annotations being produced for the expressions, we found
that some calls to `new` in an unreachable switch arm would not be
wrapped in ExprWithCleanups. As a result, they remain on the EhStack
when processing the default label, and since the dead arm doesn't
dominate the default label, we can end up with a case where the def-use
chain is broken (e.g. the def doesn't dominate all uses). Technically
this path would be impossible to reach due to the active bit, but it
still failed to satisfy a dominance relationship.

With that in place, we can remove the constraint on only using tighter
lifetimes when exceptions are disabled.

>From abfae081876ab6f5e0ada64ce64ed05e6b095255 Mon Sep 17 00:00:00 2001
From: Paul Kirth <paulkirth at google.com>
Date: Mon, 12 Jan 2026 16:14:15 -0800
Subject: [PATCH] [clang] Use uniform lifetime bounds under exceptions

To do this we have to slightly modify how some expressions are handled
in Sema. Principally, we need to ensure that calls to new for
non-trivial types still have their destructors run. Generally this isn't
an issue, since these just get sunk into the surrounding scope. With
more lifetime annotations being produced for the expressions, we found
that some calls to `new` in an unreachable switch arm would not be
wrapped in ExprWithCleanups. As a result, they remain on the EhStack
when processing the default label, and since the dead arm doesn't
dominate the default label, we can end up with a case where the def-use
chain is broken (e.g. the def doesn't dominate all uses). Technically
this path would be impossible to reach due to the active bit, but it
still failed to satisfy a dominance relationship.

With that in place, we can remove the constraint on only using tighter
lifetimes when exceptions are disabled.
---
 clang/lib/CodeGen/CGCall.cpp                  |  5 +--
 clang/lib/Sema/SemaExprCXX.cpp                | 16 ++++++-
 clang/test/CodeGen/lifetime-bug.cpp           | 35 ++++++++++++---
 clang/test/CodeGen/lifetime-invoke-c.c        |  8 ++--
 .../CodeGenCXX/aggregate-lifetime-invoke.cpp  | 43 +++++++++++++------
 5 files changed, 77 insertions(+), 30 deletions(-)

diff --git a/clang/lib/CodeGen/CGCall.cpp b/clang/lib/CodeGen/CGCall.cpp
index 212123f63ed68..051b7f0c206d5 100644
--- a/clang/lib/CodeGen/CGCall.cpp
+++ b/clang/lib/CodeGen/CGCall.cpp
@@ -4966,10 +4966,7 @@ void CodeGenFunction::EmitCallArg(CallArgList &args, const Expr *E,
   // will run at the end of the full-expression; emit matching lifetime
   // markers. For types which don't have a destructor, we use a narrower
   // lifetime bound.
-  // FIXME: This should work fine w/ exceptions, but somehow breaks the
-  // dominance relationship in the def-use chain.
-  if (!CGM.getLangOpts().Exceptions &&
-      hasAggregateEvaluationKind(E->getType())) {
+  if (hasAggregateEvaluationKind(E->getType())) {
     RawAddress ArgSlotAlloca = Address::invalid();
     ArgSlot = CreateAggTemp(E->getType(), "agg.tmp", &ArgSlotAlloca);
 
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index 91967a7a9ff97..2af55d4ccf60c 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -6686,8 +6686,22 @@ Expr *Sema::MaybeCreateExprWithCleanups(Expr *SubExpr) {
   assert(ExprCleanupObjects.size() >= FirstCleanup);
   assert(Cleanup.exprNeedsCleanups() ||
          ExprCleanupObjects.size() == FirstCleanup);
-  if (!Cleanup.exprNeedsCleanups())
+  if (!Cleanup.exprNeedsCleanups()) {
+    // If we have a 'new' expression with a non-trivial destructor, we need to
+    // wrap it in an ExprWithCleanups to ensure that the destructor is called
+    // if the constructor throws.
+    if (auto *NE = dyn_cast<CXXNewExpr>(SubExpr)) {
+      if (NE->getOperatorDelete() &&
+          !NE->getOperatorDelete()->isReservedGlobalPlacementOperator()) {
+        auto Cleanups = llvm::ArrayRef<ExprWithCleanups::CleanupObject>();
+        auto *E = ExprWithCleanups::Create(
+            Context, SubExpr, Cleanup.cleanupsHaveSideEffects(), Cleanups);
+        DiscardCleanupsInEvaluationContext();
+        return E;
+      }
+    }
     return SubExpr;
+  }
 
   auto Cleanups = llvm::ArrayRef(ExprCleanupObjects.begin() + FirstCleanup,
                                  ExprCleanupObjects.size() - FirstCleanup);
diff --git a/clang/test/CodeGen/lifetime-bug.cpp b/clang/test/CodeGen/lifetime-bug.cpp
index 641b3ae7762ae..a78ce38e69c0d 100644
--- a/clang/test/CodeGen/lifetime-bug.cpp
+++ b/clang/test/CodeGen/lifetime-bug.cpp
@@ -11,28 +11,49 @@ struct e {
 // CHECK-LABEL: define dso_local void @_Z1fv(
 // CHECK-SAME: ) #[[ATTR0:[0-9]+]] personality ptr @__gxx_personality_v0 {
 // CHECK-NEXT:  [[ENTRY:.*:]]
-// CHECK-NEXT:    [[AGG_TEMP:%.*]] = alloca [[STRUCT_B:%.*]], align 1
+// CHECK-NEXT:    [[AGG_TMP:%.*]] = alloca [[STRUCT_B:%.*]], align 1
 // CHECK-NEXT:    [[EXN_SLOT:%.*]] = alloca ptr, align 8
 // CHECK-NEXT:    [[EHSELECTOR_SLOT:%.*]] = alloca i32, align 4
+// CHECK-NEXT:    [[CLEANUP_ISACTIVE:%.*]] = alloca i1, align 1
 // CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr @d, align 4, !tbaa [[_ZTS1C_TBAA6:![0-9]+]]
 // CHECK-NEXT:    switch i32 [[TMP0]], label %[[SW_DEFAULT:.*]] [
 // CHECK-NEXT:      i32 1, label %[[SW_BB:.*]]
 // CHECK-NEXT:    ]
 // CHECK:       [[SW_BB]]:
-// CHECK-NEXT:    [[CALL:%.*]] = call noalias noundef nonnull ptr @_Znwm(i64 noundef 1) #[[ATTR4:[0-9]+]]
+// CHECK-NEXT:    [[CALL:%.*]] = call noalias noundef nonnull ptr @_Znwm(i64 noundef 1) #[[ATTR5:[0-9]+]]
+// CHECK-NEXT:    store i1 true, ptr [[CLEANUP_ISACTIVE]], align 1
+// CHECK-NEXT:    call void @llvm.lifetime.start.p0(ptr [[AGG_TMP]]) #[[ATTR6:[0-9]+]]
 // CHECK-NEXT:    invoke void @_ZN1eC1E1b(ptr noundef nonnull align 1 dereferenceable(1) [[CALL]])
-// CHECK-NEXT:            to label %[[INVOKE_CONT:.*]] unwind label %[[LPAD:.*]]
+// CHECK-NEXT:            to label %[[INVOKE_CONT:.*]] unwind label %[[LPAD1:.*]]
 // CHECK:       [[INVOKE_CONT]]:
+// CHECK-NEXT:    call void @llvm.lifetime.end.p0(ptr [[AGG_TMP]]) #[[ATTR6]]
+// CHECK-NEXT:    store i1 false, ptr [[CLEANUP_ISACTIVE]], align 1
 // CHECK-NEXT:    call void @_Z1av()
 // CHECK-NEXT:    br label %[[SW_EPILOG:.*]]
-// CHECK:       [[LPAD]]:
+// CHECK:       [[LPAD:.*:]]
 // CHECK-NEXT:    [[TMP1:%.*]] = landingpad { ptr, i32 }
 // CHECK-NEXT:            cleanup
 // CHECK-NEXT:    [[TMP2:%.*]] = extractvalue { ptr, i32 } [[TMP1]], 0
 // CHECK-NEXT:    store ptr [[TMP2]], ptr [[EXN_SLOT]], align 8
 // CHECK-NEXT:    [[TMP3:%.*]] = extractvalue { ptr, i32 } [[TMP1]], 1
 // CHECK-NEXT:    store i32 [[TMP3]], ptr [[EHSELECTOR_SLOT]], align 4
-// CHECK-NEXT:    call void @_ZdlPvm(ptr noundef [[CALL]], i64 noundef 1) #[[ATTR5:[0-9]+]]
+// CHECK-NEXT:    br label %[[EHCLEANUP:.*]]
+// CHECK:       [[LPAD1]]:
+// CHECK-NEXT:    [[TMP4:%.*]] = landingpad { ptr, i32 }
+// CHECK-NEXT:            cleanup
+// CHECK-NEXT:    [[TMP5:%.*]] = extractvalue { ptr, i32 } [[TMP4]], 0
+// CHECK-NEXT:    store ptr [[TMP5]], ptr [[EXN_SLOT]], align 8
+// CHECK-NEXT:    [[TMP6:%.*]] = extractvalue { ptr, i32 } [[TMP4]], 1
+// CHECK-NEXT:    store i32 [[TMP6]], ptr [[EHSELECTOR_SLOT]], align 4
+// CHECK-NEXT:    call void @llvm.lifetime.end.p0(ptr [[AGG_TMP]]) #[[ATTR6]]
+// CHECK-NEXT:    br label %[[EHCLEANUP]]
+// CHECK:       [[EHCLEANUP]]:
+// CHECK-NEXT:    [[CLEANUP_IS_ACTIVE:%.*]] = load i1, ptr [[CLEANUP_ISACTIVE]], align 1
+// CHECK-NEXT:    br i1 [[CLEANUP_IS_ACTIVE]], label %[[CLEANUP_ACTION:.*]], label %[[CLEANUP_DONE:.*]]
+// CHECK:       [[CLEANUP_ACTION]]:
+// CHECK-NEXT:    call void @_ZdlPvm(ptr noundef [[CALL]], i64 noundef 1) #[[ATTR7:[0-9]+]]
+// CHECK-NEXT:    br label %[[CLEANUP_DONE]]
+// CHECK:       [[CLEANUP_DONE]]:
 // CHECK-NEXT:    br label %[[EH_RESUME:.*]]
 // CHECK:       [[SW_DEFAULT]]:
 // CHECK-NEXT:    call void @_Z1av()
@@ -43,8 +64,8 @@ struct e {
 // CHECK-NEXT:    [[EXN:%.*]] = load ptr, ptr [[EXN_SLOT]], align 8
 // CHECK-NEXT:    [[SEL:%.*]] = load i32, ptr [[EHSELECTOR_SLOT]], align 4
 // CHECK-NEXT:    [[LPAD_VAL:%.*]] = insertvalue { ptr, i32 } poison, ptr [[EXN]], 0
-// CHECK-NEXT:    [[LPAD_VAL1:%.*]] = insertvalue { ptr, i32 } [[LPAD_VAL]], i32 [[SEL]], 1
-// CHECK-NEXT:    resume { ptr, i32 } [[LPAD_VAL1]]
+// CHECK-NEXT:    [[LPAD_VAL2:%.*]] = insertvalue { ptr, i32 } [[LPAD_VAL]], i32 [[SEL]], 1
+// CHECK-NEXT:    resume { ptr, i32 } [[LPAD_VAL2]]
 //
 void f() {
   switch (d) {
diff --git a/clang/test/CodeGen/lifetime-invoke-c.c b/clang/test/CodeGen/lifetime-invoke-c.c
index c7181b40f685c..896bd2baece13 100644
--- a/clang/test/CodeGen/lifetime-invoke-c.c
+++ b/clang/test/CodeGen/lifetime-invoke-c.c
@@ -27,10 +27,10 @@ void test() {
 
   // EXCEPTIONS: %[[AGG1:.*]] = alloca %struct.Trivial
   // EXCEPTIONS: %[[AGG2:.*]] = alloca %struct.Trivial
-  // EXCEPTIONS-NOT: call void @llvm.lifetime.start.p0(ptr %[[AGG1]])
-  // EXCEPTIONS-NOT: call void @llvm.lifetime.start.p0(ptr %[[AGG2]])
-  // EXCEPTIONS-NOT: call void @llvm.lifetime.end.p0(ptr %[[AGG2]])
-  // EXCEPTIONS-NOT: call void @llvm.lifetime.end.p0(ptr %[[AGG1]])
+  // EXCEPTIONS: call void @llvm.lifetime.start.p0(ptr %[[AGG1]])
+  // EXCEPTIONS: call void @llvm.lifetime.start.p0(ptr %[[AGG2]])
+  // EXCEPTIONS: call void @llvm.lifetime.end.p0(ptr %[[AGG2]])
+  // EXCEPTIONS: call void @llvm.lifetime.end.p0(ptr %[[AGG1]])
   func(gen());
   func(gen());
 }
diff --git a/clang/test/CodeGenCXX/aggregate-lifetime-invoke.cpp b/clang/test/CodeGenCXX/aggregate-lifetime-invoke.cpp
index 0c7ef09a4734b..286f01cf97950 100644
--- a/clang/test/CodeGenCXX/aggregate-lifetime-invoke.cpp
+++ b/clang/test/CodeGenCXX/aggregate-lifetime-invoke.cpp
@@ -1,5 +1,5 @@
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -o - %s -O1 -fexceptions -fcxx-exceptions | FileCheck %s
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -o - %s -O1 -fexceptions -fcxx-exceptions -sloppy-temporary-lifetimes | FileCheck %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -o - %s -O1 -fexceptions -fcxx-exceptions | FileCheck %s --check-prefixes=COMMON,TIGHT
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -o - %s -O1 -fexceptions -fcxx-exceptions -sloppy-temporary-lifetimes | FileCheck %s --check-prefixes=SLOPPY,COMMON
 
 // COM: Note that this test case would break if we allowed tighter lifetimes to
 // run when exceptions were enabled. If we make them work together this test
@@ -13,24 +13,39 @@ struct Trivial {
 
 void func_that_throws(Trivial t);
 
-// CHECK-LABEL: define{{.*}} void @test()
+// COMMON-LABEL: define{{.*}} void @test()
 void test() {
-  // CHECK: %[[AGG1:.*]] = alloca %struct.Trivial
-  // CHECK: %[[AGG2:.*]] = alloca %struct.Trivial
+  // COMMON: %[[AGG1:.*]] = alloca %struct.Trivial
+  // COMMON: %[[AGG2:.*]] = alloca %struct.Trivial
 
-  // CHECK: invoke void @func_that_throws(ptr{{.*}} %[[AGG1]])
-  // CHECK-NEXT: to label %[[CONT:.*]] unwind label %[[LPAD:.*]]
+  // TIGHT: call void @llvm.lifetime.start.p0(ptr{{.*}} %[[AGG1]])
+  // COMMON: invoke void @func_that_throws(ptr{{.*}} %[[AGG1]])
+  // TIGHT-NEXT: to label %[[CONT:.*]] unwind label %[[LPAD1:.*]]
 
-  // CHECK: [[CONT]]:
-  // CHECK: invoke void @func_that_throws(ptr{{.*}} %[[AGG2]])
-  // CHECK-NEXT: to label %[[CONT:.*]] unwind label %[[LPAD:.*]]
+  // TIGHT: [[CONT]]:
+  // TIGHT: call void @llvm.lifetime.end.p0(ptr{{.*}} %[[AGG1]])
+  // TIGHT: call void @llvm.lifetime.start.p0(ptr{{.*}} %[[AGG2]])
+  // COMMON: invoke void @func_that_throws(ptr{{.*}} %[[AGG2]])
+  // TIGHT-NEXT: to label %[[CONT2:.*]] unwind label %[[LPAD2:.*]]
 
-  // CHECK: [[LPAD1:lpad.*]]:
-  // CHECK: landingpad
+  // TIGHT: [[CONT2]]:
+  // TIGHT-NEXT: call void @llvm.lifetime.end.p0(ptr{{.*}} %[[AGG2]])
 
-  // CHECK-NOT: llvm.lifetime.start
-  // CHECK-NOT: llvm.lifetime.end
+  // TIGHT: [[LPAD1:lpad.*]]:
+  // TIGHT: landingpad
+  // TIGHT: br label %[[EHCLEANUP:.*]]
 
+  // TIGHT: [[LPAD2:lpad.*]]:
+  // TIGHT: landingpad
+  // TIGHT: call void @llvm.lifetime.end.p0(ptr{{.*}} %[[AGG2]])
+  // TIGHT: br label %[[EHCLEANUP]]
+
+  // TIGHT: [[EHCLEANUP]]:
+  // TIGHT: call void @llvm.lifetime.end.p0(ptr{{.*}} %[[AGG1]])
+
+
+  // SLOPPY-NOT: llvm.lifetime.start
+  // SLOPPY-NOT: llvm.lifetime.end
   try {
     func_that_throws(Trivial{0});
     func_that_throws(Trivial{0});



More information about the llvm-branch-commits mailing list