[clang] 593605c - [CIR] Don't create EH paths for lifetime-marker-only cleanups (#224829)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Sep 23 20:00:15 PDT 2026
Author: Jiahao Guo
Date: 2026-09-24T11:00:09+08:00
New Revision: 593605cd2335b782cd9e02de961b326131b5add5
URL: https://github.com/llvm/llvm-project/commit/593605cd2335b782cd9e02de961b326131b5add5
DIFF: https://github.com/llvm/llvm-project/commit/593605cd2335b782cd9e02de961b326131b5add5.diff
LOG: [CIR] Don't create EH paths for lifetime-marker-only cleanups (#224829)
### summary
fix https://github.com/llvm/llvm-project/issues/224821
A lifetime marker is not a real cleanup, so it must never be the reason
an unwind edge exists.
Assisted by : Claude Opus5
Added:
Modified:
clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
clang/test/CIR/CodeGen/lifetime-marker.cpp
clang/test/CIR/Transforms/flatten-cleanup-scope-musttail.cir
Removed:
################################################################################
diff --git a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
index 8506985f4203f..4005124a054ac 100644
--- a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
@@ -844,6 +844,51 @@ static cir::AllocaOp getOrCreateCleanupDestSlot(cir::FuncOp funcOp,
/// Shared EH flattening utilities used by both CIRCleanupScopeOpFlattening
/// and CIRTryOpFlattening.
+// Lifetime markers participate in existing EH paths, but do not require an
+// unwind edge on their own. Conservatively treat other cleanup code as real
+// cleanup, including regions with control flow.
+static bool isLifetimeMarkerOnly(mlir::Region ®ion) {
+ return llvm::hasSingleElement(region) &&
+ llvm::any_of(
+ region.front(),
+ [](mlir::Operation &op) { return isa<cir::LifetimeEndOp>(op); }) &&
+ llvm::all_of(region.front(), [](mlir::Operation &op) {
+ return isa<cir::LifetimeEndOp, cir::YieldOp>(op);
+ });
+}
+
+// Look for an enclosing handler or non-marker EH cleanup that protects this
+// operation. Follow regions, rather than just parent ops: a try does not catch
+// exceptions from its own handlers, and a cleanup does not protect itself.
+static bool hasEnclosingEHRequirement(mlir::Operation *op) {
+ for (mlir::Region *region = op->getParentRegion(); region;
+ region = region->getParentRegion()) {
+ mlir::Operation *parent = region->getParentOp();
+ if (!parent || isa<cir::FuncOp>(parent))
+ break;
+ if (auto tryOp = dyn_cast<cir::TryOp>(parent)) {
+ auto handlers = tryOp.getHandlerTypesAttr();
+ if (region == &tryOp.getTryRegion() && handlers &&
+ llvm::any_of(handlers, [](mlir::Attribute handler) {
+ return !isa<cir::UnwindAttr>(handler);
+ }))
+ return true;
+ } else if (auto cleanupOp = dyn_cast<cir::CleanupScopeOp>(parent)) {
+ if (region == &cleanupOp.getBodyRegion() &&
+ cleanupOp.getCleanupKindAttr().isEH() &&
+ !isLifetimeMarkerOnly(cleanupOp.getCleanupRegion()))
+ return true;
+ } else if (auto loopOp = dyn_cast<cir::LoopOpInterface>(parent)) {
+ // The enclosing loop may not yet have been rewritten to a cleanup scope.
+ mlir::Region *cleanup = loopOp.maybeGetCleanup();
+ if (cleanup && region != cleanup && loopOp.maybeGetCleanupKind().isEH() &&
+ !isLifetimeMarkerOnly(*cleanup))
+ return true;
+ }
+ }
+ return false;
+}
+
// Collect all function calls in a region that may throw exceptions and need
// to be replaced with try_call operations. Skips calls marked nothrow.
// Nested cleanup scopes and try ops are always flattened before their
@@ -1678,7 +1723,7 @@ class CIRCleanupScopeOpFlattening
if (hasNestedOpsToFlatten(cleanupOp.getBodyRegion()))
return mlir::failure();
- cir::CleanupKind cleanupKind = cleanupOp.getCleanupKind();
+ bool hasEHCleanup = cleanupOp.getCleanupKindAttr().isEH();
// Collect all exits from the body region.
llvm::SmallVector<CleanupExit> exits;
@@ -1696,19 +1741,21 @@ class CIRCleanupScopeOpFlattening
#endif
// Collect non-nothrow calls and throws that need to be converted to
- // try_call/try_throw. This is only needed for EH and All cleanup kinds,
- // but the vectors will simply be empty for Normal cleanup.
+ // try_call/try_throw. A marker-only cleanup must not introduce an unwind
+ // edge unless an enclosing handler or real EH cleanup requires one.
llvm::SmallVector<cir::CallOp> callsToRewrite;
llvm::SmallVector<cir::ThrowOp> throwsToRewrite;
- if (cleanupKind != cir::CleanupKind::Normal) {
+ if (hasEHCleanup && (!isLifetimeMarkerOnly(cleanupOp.getCleanupRegion()) ||
+ hasEnclosingEHRequirement(cleanupOp))) {
collectThrowingCalls(cleanupOp.getBodyRegion(), callsToRewrite);
collectThrows(cleanupOp.getBodyRegion(), throwsToRewrite);
}
// Collect resume ops from already-flattened inner cleanup scopes that
- // need to chain through this cleanup's EH handler.
+ // need to chain through this cleanup's EH handler, including lifetime
+ // markers even when they did not introduce any unwind edges themselves.
llvm::SmallVector<cir::ResumeOp> resumeOpsToChain;
- if (cleanupKind != cir::CleanupKind::Normal)
+ if (hasEHCleanup)
collectResumeOps(cleanupOp.getBodyRegion(), resumeOpsToChain);
return flattenCleanup(cleanupOp, exits, callsToRewrite, throwsToRewrite,
diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp
index dde66fac13d96..12ac98b3d5343 100644
--- a/clang/test/CIR/CodeGen/lifetime-marker.cpp
+++ b/clang/test/CIR/CodeGen/lifetime-marker.cpp
@@ -134,10 +134,8 @@ void if_body(int n) {
// LLVM: [[IF_END]]:
// LLVM: call void @_Z3usei
-// With exceptions enabled the scope cleanup runs on both the normal and the
-// exceptional edge, so the cleanup kind is "all" and lifetime.end is emitted in
-// the EH cleanup handler (the landing pad) as well as on the normal path. The
-// may_throw() call is what forces an unwind edge.
+// Lifetime cleanups retain the "all" kind, but must not introduce an unwind
+// edge on their own. Only the normal-path marker is needed here.
void may_throw();
void eh_cleanup() {
@@ -156,14 +154,11 @@ void eh_cleanup() {
// CIR-EH: }
// LLVM-EH-LABEL: define{{.*}} void @_Z10eh_cleanupv()
+// LLVM-EH-NOT: personality
// LLVM-EH: %[[X:.*]] = alloca i32
// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[X]])
-// LLVM-EH: invoke void @_Z9may_throwv()
-// The normal-path end marker.
-// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[X]])
-// The EH cleanup handler runs the same end marker on the unwind path.
-// LLVM-EH: landingpad { ptr, i32 }
-// LLVM-EH-NEXT: cleanup
+// LLVM-EH: call void @_Z9may_throwv()
+// LLVM-EH: call void @_Z3usei
// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[X]])
// A loop condition variable is destroyed and re-created on every iteration
@@ -196,11 +191,11 @@ void while_condvar() {
// CIR-EH: } cleanup all {
// CIR-EH: cir.lifetime.end %[[C]] : !cir.ptr<!s32i>
-// LLVM-EH-LABEL: define{{.*}} void @_Z13while_condvarv
+// LLVM-EH-LABEL: define{{.*}} void @_Z13while_condvarv()
+// LLVM-EH-NOT: personality
// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
-// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
-// LLVM-EH: landingpad { ptr, i32 }
-// LLVM-EH-NEXT: cleanup
+// LLVM-EH: call{{.*}} i32 @_Z6sourcev()
+// LLVM-EH: call void @_Z3usei
// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
void for_condvar() {
@@ -230,11 +225,11 @@ void for_condvar() {
// CIR-EH: } cleanup all {
// CIR-EH: cir.lifetime.end %[[C]] : !cir.ptr<!s32i>
-// LLVM-EH-LABEL: define{{.*}} void @_Z11for_condvarv
+// LLVM-EH-LABEL: define{{.*}} void @_Z11for_condvarv()
+// LLVM-EH-NOT: personality
// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
-// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
-// LLVM-EH: landingpad { ptr, i32 }
-// LLVM-EH-NEXT: cleanup
+// LLVM-EH: call{{.*}} i32 @_Z6sourcev()
+// LLVM-EH: call void @_Z3usei
// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
struct LoopCond {
@@ -341,4 +336,156 @@ void catch_by_value() {
// LLVM-EH: call void @__cxa_end_catch()
// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
+// An enclosing catch requires an unwind edge and the marker must run before
+// entering the handler.
+void lifetime_in_try() {
+ try {
+ int x = 1;
+ may_throw();
+ use(x);
+ } catch (...) {
+ }
+}
+
+// CIR-EH-LABEL: cir.func{{.*}} @_Z15lifetime_in_tryv()
+// CIR-EH: %[[X:.*]] = cir.alloca "x"
+// CIR-EH: cir.try {
+// CIR-EH: cir.lifetime.start %[[X]]
+// CIR-EH: cir.cleanup.scope {
+// CIR-EH: cir.call @_Z9may_throwv()
+// CIR-EH: cir.call @_Z3usei
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[X]]
+// CIR-EH: } catch all
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z15lifetime_in_tryv()
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[X:.*]])
+// LLVM-EH: invoke void @_Z9may_throwv()
+// LLVM-EH: invoke void @_Z3usei
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[X]])
+// LLVM-EH: landingpad { ptr, i32 }
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[X]])
+// LLVM-EH: call ptr @__cxa_begin_catch
+
+// A real outer cleanup requires the inner lifetime marker on the EH path.
+void lifetime_in_dtor_scope() {
+ S s;
+ int x = 1;
+ use(x);
+}
+
+// CIR-EH-LABEL: cir.func{{.*}} @_Z22lifetime_in_dtor_scopev()
+// CIR-EH: %[[S:.*]] = cir.alloca "s"
+// CIR-EH: %[[X:.*]] = cir.alloca "x"
+// CIR-EH: cir.lifetime.start %[[S]]
+// CIR-EH: cir.lifetime.start %[[X]]
+// CIR-EH: cir.call @_Z3usei
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[X]]
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.call @_ZN1SD1Ev(%[[S]])
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[S]]
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z22lifetime_in_dtor_scopev()
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[S:.*]])
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[X:.*]])
+// LLVM-EH: invoke void @_Z3usei
+// LLVM-EH: landingpad { ptr, i32 }
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[X]])
+// LLVM-EH: call void @_ZN1SD1Ev(ptr {{.*}} %[[S]])
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[S]])
+
+// The first call needs no unwind edge. The inner destructor's existing EH
+// path must still pass through both outer lifetime markers.
+void lifetime_around_dtor_scope() {
+ int x = 1;
+ use(x);
+ S s;
+ use(x);
+}
+
+// CIR-EH-LABEL: cir.func{{.*}} @_Z26lifetime_around_dtor_scopev()
+// CIR-EH: %[[X:.*]] = cir.alloca "x"
+// CIR-EH: %[[S:.*]] = cir.alloca "s"
+// CIR-EH: cir.lifetime.start %[[X]]
+// CIR-EH: cir.call @_Z3usei
+// CIR-EH: cir.lifetime.start %[[S]]
+// CIR-EH: cir.call @_Z3usei
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.call @_ZN1SD1Ev(%[[S]])
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[S]]
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[X]]
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z26lifetime_around_dtor_scopev()
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[X:.*]])
+// LLVM-EH: call void @_Z3usei
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[S:.*]])
+// LLVM-EH: invoke void @_Z3usei
+// LLVM-EH: landingpad { ptr, i32 }
+// LLVM-EH: call void @_ZN1SD1Ev(ptr {{.*}} %[[S]])
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[S]])
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[X]])
+
+// The outer loop's destructor is still a loop cleanup region when the inner
+// lifetime scope is flattened.
+void lifetime_in_loop() {
+ while (LoopCond c{}) {
+ int x = 1;
+ use(x);
+ }
+}
+
+// CIR-EH-LABEL: cir.func{{.*}} @_Z16lifetime_in_loopv()
+// CIR-EH: %[[C:.*]] = cir.alloca "c"
+// CIR-EH: cir.while {
+// CIR-EH: } do {
+// CIR-EH: %[[X:.*]] = cir.alloca "x"
+// CIR-EH: cir.lifetime.start %[[X]]
+// CIR-EH: cir.call @_Z3usei
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[X]]
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.call @_ZN8LoopCondD1Ev(%[[C]])
+// CIR-EH: cir.lifetime.end %[[C]]
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z16lifetime_in_loopv()
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[X:.*]])
+// LLVM-EH: invoke void @_Z3usei
+// LLVM-EH: landingpad { ptr, i32 }
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[X]])
+// LLVM-EH: call void @_ZN8LoopCondD1Ev(ptr {{.*}} %[[C]])
+
+// A marker-only loop cleanup also participates in an enclosing try's EH path.
+void lifetime_loop_in_try() {
+ try {
+ while (int c = source())
+ use(c);
+ } catch (...) {
+ }
+}
+
+// CIR-EH-LABEL: cir.func{{.*}} @_Z20lifetime_loop_in_tryv()
+// CIR-EH: cir.try {
+// CIR-EH: %[[C:.*]] = cir.alloca "c"
+// CIR-EH: cir.while {
+// CIR-EH: cir.call @_Z6sourcev()
+// CIR-EH: } do {
+// CIR-EH: cir.call @_Z3usei
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[C]]
+// CIR-EH: } catch all
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z20lifetime_loop_in_tryv()
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
+// LLVM-EH: invoke{{.*}} i32 @_Z6sourcev()
+// LLVM-EH: invoke void @_Z3usei
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
+// LLVM-EH: landingpad { ptr, i32 }
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
+// LLVM-EH: call ptr @__cxa_begin_catch
+
#endif // __EXCEPTIONS
diff --git a/clang/test/CIR/Transforms/flatten-cleanup-scope-musttail.cir b/clang/test/CIR/Transforms/flatten-cleanup-scope-musttail.cir
index 453336a6823aa..0cd686759e59a 100644
--- a/clang/test/CIR/Transforms/flatten-cleanup-scope-musttail.cir
+++ b/clang/test/CIR/Transforms/flatten-cleanup-scope-musttail.cir
@@ -200,20 +200,14 @@ cir.func @test_musttail_in_eh_cleanup(%arg0: !s32i) -> !s32i {
cir.unreachable
}
+// A lifetime marker alone does not require an unwind edge for Other.
// CHECK-LABEL: cir.func @test_musttail_in_eh_cleanup
// CHECK: %[[LOCAL:.*]] = cir.alloca "local"
// CHECK: cir.br ^[[BODY:bb[0-9]+]]
// CHECK: ^[[BODY]]:
-// CHECK: cir.try_call @Other(%{{.*}}) ^[[NORMAL:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]]
-// CHECK: ^[[NORMAL]]:
+// CHECK: %[[OTHER:.*]] = cir.call @Other(%{{.*}})
+// CHECK: cir.store %[[OTHER]], %[[LOCAL]]
// CHECK: %[[TAIL_ARG:.*]] = cir.load %[[LOCAL]]
// CHECK-NEXT: %[[TAIL_RET:.*]] = cir.call @Bar(%[[TAIL_ARG]]) musttail
// CHECK-NEXT: cir.return %[[TAIL_RET]] : !s32i
-// CHECK: ^[[UNWIND]]:
-// CHECK: %[[TOKEN:.*]] = cir.eh.initiate cleanup : !cir.eh_token
-// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[TOKEN]] : !cir.eh_token)
-// CHECK: ^[[EH_CLEANUP]](%[[EH_TOKEN:.*]]: !cir.eh_token):
-// CHECK: cir.begin_cleanup %[[EH_TOKEN]]
-// CHECK: cir.lifetime.end %[[LOCAL]]
-// CHECK: cir.end_cleanup
-// CHECK: cir.resume %[[EH_TOKEN]]
+// CHECK-NOT: cir.eh.initiate
More information about the cfe-commits
mailing list