[clang] [CIR] Emit lifetime markers for automatic variables (PR #206695)
Jiahao Guo via cfe-commits
cfe-commits at lists.llvm.org
Tue Aug 11 06:55:41 PDT 2026
https://github.com/E00N777 updated https://github.com/llvm/llvm-project/pull/206695
>From 3889e950b6e9e61716f6d888ebe79836779fc67e Mon Sep 17 00:00:00 2001
From: E00N777 <E0N_gjh at 163.com>
Date: Tue, 30 Jun 2026 17:40:25 +0800
Subject: [PATCH 1/5] [CIR] Emit lifetime markers for automatic variables
---
clang/include/clang/CIR/MissingFeatures.h | 1 +
clang/lib/CIR/CodeGen/CIRGenCleanup.cpp | 5 +-
clang/lib/CIR/CodeGen/CIRGenCleanup.h | 1 +
clang/lib/CIR/CodeGen/CIRGenDecl.cpp | 62 ++++++++++++++++++++++
clang/lib/CIR/CodeGen/CIRGenFunction.cpp | 20 +++++++
clang/lib/CIR/CodeGen/CIRGenFunction.h | 12 +++++
clang/test/CIR/CodeGen/lifetime-marker.cpp | 54 +++++++++++++++++++
7 files changed, 152 insertions(+), 3 deletions(-)
create mode 100644 clang/test/CIR/CodeGen/lifetime-marker.cpp
diff --git a/clang/include/clang/CIR/MissingFeatures.h b/clang/include/clang/CIR/MissingFeatures.h
index 02475b70c5dcd..20369d1331eb9 100644
--- a/clang/include/clang/CIR/MissingFeatures.h
+++ b/clang/include/clang/CIR/MissingFeatures.h
@@ -228,6 +228,7 @@ struct MissingFeatures {
static bool emitCondLikelihoodViaExpectIntrinsic() { return false; }
static bool emitConstrainedFPCall() { return false; }
static bool emitLifetimeMarkers() { return false; }
+ static bool lifetimeMarkersBypass() { return false; }
static bool emitLValueAlignmentAssumption() { return false; }
static bool emitNullCheckForDeleteCalls() { return false; }
static bool emitNullabilityCheck() { return false; }
diff --git a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
index 07cbe34409ea4..15aec51f724be 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
@@ -406,7 +406,7 @@ void *EHScopeStack::pushCleanup(CleanupKind kind, size_t size) {
innermostEHScope = stable_begin();
if (isLifetimeMarker)
- cgf->cgm.errorNYI("push lifetime marker cleanup");
+ scope->setLifetimeMarker();
// With Windows -EHa, Invoke llvm.seh.scope.begin() for EHCleanup
if (cgf->getLangOpts().EHAsynch && isEHCleanup && !isLifetimeMarker &&
@@ -450,8 +450,7 @@ bool EHScopeStack::requiresCatchOrCleanup() const {
for (stable_iterator si = getInnermostEHScope(); si != stable_end();) {
if (auto *cleanup = dyn_cast<EHCleanupScope>(&*find(si))) {
if (cleanup->isLifetimeMarker()) {
- // Skip lifetime markers and continue from the enclosing EH scope
- assert(!cir::MissingFeatures::emitLifetimeMarkers());
+ si = cleanup->getEnclosingEHScope();
continue;
}
}
diff --git a/clang/lib/CIR/CodeGen/CIRGenCleanup.h b/clang/lib/CIR/CodeGen/CIRGenCleanup.h
index bae04a2452006..46f1382bced7d 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCleanup.h
+++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.h
@@ -157,6 +157,7 @@ class alignas(EHScopeStack::ScopeStackAlignment) EHCleanupScope
void setActive(bool isActive) { cleanupBits.isActive = isActive; }
bool isLifetimeMarker() const { return cleanupBits.isLifetimeMarker; }
+ void setLifetimeMarker() { cleanupBits.isLifetimeMarker = true; }
bool hasActiveFlag() const { return activeFlag.isValid(); }
Address getActiveFlag() const { return activeFlag; }
diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
index e17550a8c1668..25a67d3bd027a 100644
--- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
@@ -10,9 +10,11 @@
//
//===----------------------------------------------------------------------===//
+#include "Address.h"
#include "CIRGenCleanup.h"
#include "CIRGenConstantEmitter.h"
#include "CIRGenFunction.h"
+#include "EHScopeStack.h"
#include "mlir/IR/Location.h"
#include "clang/AST/Attr.h"
#include "clang/AST/Attrs.inc"
@@ -28,6 +30,20 @@
using namespace clang;
using namespace clang::CIRGen;
+/// Does the statement tree rooted at \p s contain a label, switch, or indirect
+/// goto that could bypass a local's initialization? A coarse stand-in for
+/// classic CodeGen's per-decl bypass analysis (PR28267).
+static bool functionMightHaveBypass(const Stmt *s) {
+ if (!s)
+ return false;
+ if (isa<LabelStmt, SwitchStmt, IndirectGotoStmt>(s))
+ return true;
+ for (const Stmt *child : s->children())
+ if (functionMightHaveBypass(child))
+ return true;
+ return false;
+}
+
CIRGenFunction::AutoVarEmission
CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
mlir::OpBuilder::InsertPoint ip) {
@@ -129,6 +145,21 @@ CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
/*arraySize=*/nullptr, /*alloca=*/nullptr, ip);
declare(address.getPointer(), &d, ty, getLoc(d.getSourceRange()),
alignment);
+ // A goto/switch that bypasses the init splits the lifetime across IR
+ // regions and miscompiles under stack coloring (PR28267). Lacking
+ // classic's per-decl bypass analysis, drop markers for the whole
+ // function if any such statement is present.
+ assert(!cir::MissingFeatures::lifetimeMarkersBypass());
+ if (shouldEmitLifetimeOp && haveInsertPoint()) {
+ if (!fnHasBypassStmt.has_value())
+ fnHasBypassStmt = functionMightHaveBypass(
+ curFuncDecl ? curFuncDecl->getBody() : nullptr);
+ // Peel address-space casts to the alloca so the op verifier sees a
+ // value produced by cir.alloca.
+ if (!*fnHasBypassStmt)
+ emission.useLifetimeOp = emitLifetimeStartOp(
+ loc, address.getUnderlyingAllocaOp().getResult());
+ }
}
} else {
// Non-constant size type
@@ -165,6 +196,9 @@ CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
assert(!cir::MissingFeatures::generateDebugInfo());
}
+ if (emission.useLifetimeOp)
+ pushLifetimeEnd(address);
+
emission.addr = address;
setAddrOfLocalVar(&d, address);
@@ -1063,6 +1097,15 @@ struct CallStackRestore final : EHScopeStack::Cleanup {
}
};
+struct CallLifetimeEnd final : EHScopeStack::Cleanup {
+ Address addr;
+ CallLifetimeEnd(Address addr) : addr(addr) {}
+ void emit(CIRGenFunction &cgf, Flags flags) override {
+ mlir::Value allocaPtr = addr.getUnderlyingAllocaOp().getResult();
+ cgf.emitLifetimeEndOp(allocaPtr.getLoc(), allocaPtr);
+ }
+};
+
/// A cleanup which performs a partial array destroy where the end pointer is
/// irregularly determined and must be loaded from a local.
struct IrregularPartialArrayDestroy final : EHScopeStack::Cleanup {
@@ -1319,6 +1362,10 @@ void CIRGenFunction::pushStackRestore(CleanupKind kind, Address spMem) {
ehStack.pushCleanup<CallStackRestore>(kind, spMem);
}
+void CIRGenFunction::pushLifetimeEnd(Address addr) {
+ ehStack.pushCleanup<CallLifetimeEnd>(NormalEHLifetimeMarker, addr);
+}
+
/// Enter a destroy cleanup for the given local variable.
void CIRGenFunction::emitAutoVarTypeCleanup(
const CIRGenFunction::AutoVarEmission &emission,
@@ -1375,3 +1422,18 @@ void CIRGenFunction::maybeEmitDeferredVarDeclInit(const VarDecl *vd) {
emitVarDecl(*hd);
}
}
+
+bool CIRGenFunction::emitLifetimeStartOp(mlir::Location loc, mlir::Value addr) {
+ if (!shouldEmitLifetimeOp)
+ return false;
+
+ cir::LifetimeStartOp::create(builder, loc, addr);
+ return true;
+}
+
+void CIRGenFunction::emitLifetimeEndOp(mlir::Location loc, mlir::Value addr) {
+ if (!shouldEmitLifetimeOp)
+ return;
+
+ cir::LifetimeEndOp::create(builder, loc, addr);
+}
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
index 66e7b6d5061df..c729dceb8cddf 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
@@ -28,11 +28,31 @@
namespace clang::CIRGen {
+/// shouldEmitLifetimeMarkers - Decide whether we need emit the life-time
+/// markers. Mirror of CodeGenFunction::shouldEmitLifetimeMarkers.
+static bool shouldEmitLifetimeMarkers(const CodeGenOptions &cgOpts,
+ const LangOptions &langOpts) {
+
+ if (cgOpts.DisableLifetimeMarkers)
+ return false;
+
+ // Sanitizers may use markers.
+ if (cgOpts.SanitizeAddressUseAfterScope ||
+ langOpts.Sanitize.has(SanitizerKind::HWAddress) ||
+ langOpts.Sanitize.has(SanitizerKind::Memory) ||
+ langOpts.Sanitize.has(SanitizerKind::MemtagStack))
+ return true;
+
+ return cgOpts.OptimizationLevel != 0;
+}
+
CIRGenFunction::CIRGenFunction(CIRGenModule &cgm, CIRGenBuilderTy &builder,
bool suppressNewContext)
: CIRGenTypeCache(cgm), cgm{cgm}, builder(builder),
curFPFeatures(cgm.getLangOpts()) {
ehStack.setCGF(this);
+ shouldEmitLifetimeOp = shouldEmitLifetimeMarkers(cgm.getCodeGenOpts(),
+ getContext().getLangOpts());
}
CIRGenFunction::~CIRGenFunction() {}
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index d318338187f12..d5b5aa0aa9305 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -723,6 +723,9 @@ class CIRGenFunction : public CIRGenTypeCache {
/// have the same sort of alloca initialization.
bool emittedAsOffload = false;
+ /// True if lifetime op should be used.
+ bool useLifetimeOp = false;
+
mlir::Value nrvoFlag{};
struct Invalid {};
@@ -795,6 +798,7 @@ class CIRGenFunction : public CIRGenTypeCache {
}
void pushStackRestore(CleanupKind kind, Address spMem);
+ void pushLifetimeEnd(Address addr);
/// Set the address of a local variable.
void setAddrOfLocalVar(const clang::VarDecl *vd, Address addr) {
@@ -1632,6 +1636,9 @@ class CIRGenFunction : public CIRGenTypeCache {
int64_t alignment,
mlir::Value offsetValue = nullptr);
+ bool emitLifetimeStartOp(mlir::Location loc, mlir::Value addr);
+ void emitLifetimeEndOp(mlir::Location loc, mlir::Value addr);
+
private:
void emitAndUpdateRetAlloca(clang::QualType type, mlir::Location loc,
clang::CharUnits alignment);
@@ -2796,6 +2803,11 @@ class CIRGenFunction : public CIRGenTypeCache {
private:
QualType getVarArgType(const Expr *arg);
+ bool shouldEmitLifetimeOp = false;
+ /// Set when the current function has a goto/switch that may bypass a local's
+ /// init; lifetime markers are then suppressed. See functionMightHaveBypass.
+ std::optional<bool> fnHasBypassStmt;
+
class InlinedInheritingConstructorScope {
public:
InlinedInheritingConstructorScope(CIRGenFunction &cgf, GlobalDecl gd)
diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp
new file mode 100644
index 0000000000000..3e8e54dbce096
--- /dev/null
+++ b/clang/test/CIR/CodeGen/lifetime-marker.cpp
@@ -0,0 +1,54 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s --check-prefix=CIR
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fclangir -emit-llvm -disable-llvm-passes %s -o %t.ll
+// RUN: FileCheck --input-file=%t.ll %s --check-prefix=LLVM
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t-o0.cir
+// RUN: FileCheck --input-file=%t-o0.cir %s --check-prefix=O0
+
+void use(int);
+
+// A scalar automatic variable gets a lifetime.start at its declaration and a
+// matching lifetime.end when its scope is left.
+void f() {
+ int x;
+ use(x);
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z1fv()
+// CIR: %[[X:.*]] = cir.alloca "x" {{.*}} : !cir.ptr<!s32i>
+// CIR: cir.lifetime.start %[[X]] : !cir.ptr<!s32i>
+// CIR: cir.cleanup.scope {
+// CIR: } cleanup normal {
+// CIR: cir.lifetime.end %[[X]] : !cir.ptr<!s32i>
+// CIR: }
+
+// LLVM-LABEL: define{{.*}} void @_Z1fv()
+// LLVM: %[[X:.*]] = alloca i32
+// LLVM: call void @llvm.lifetime.start.p0(ptr %[[X]])
+// LLVM: call void @llvm.lifetime.end.p0(ptr %[[X]])
+
+struct S {
+ ~S();
+};
+
+// The destructor runs before lifetime.end: the end marker is the outermost
+// cleanup, so it is emitted after the destructor call. FileCheck matches in
+// order, which pins the relative ordering.
+void g() {
+ S s;
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z1gv()
+// CIR: %[[S:.*]] = cir.alloca "s" {{.*}} : !cir.ptr<!rec_S>
+// CIR: cir.lifetime.start %[[S]] : !cir.ptr<!rec_S>
+// CIR: cir.call @_ZN1SD1Ev(%[[S]])
+// CIR: cir.lifetime.end %[[S]] : !cir.ptr<!rec_S>
+
+// LLVM-LABEL: define{{.*}} void @_Z1gv()
+// LLVM: %[[S:.*]] = alloca %struct.S
+// LLVM: call void @llvm.lifetime.start.p0(ptr %[[S]])
+// LLVM: call void @_ZN1SD1Ev(ptr {{.*}} %[[S]])
+// LLVM: call void @llvm.lifetime.end.p0(ptr %[[S]])
+
+// Without optimization no lifetime markers are emitted at all.
+// O0-NOT: cir.lifetime
>From c04cbcefe935258a89508cf332c9d21104571dc0 Mon Sep 17 00:00:00 2001
From: E00N777 <E0N_gjh at 163.com>
Date: Thu, 2 Jul 2026 18:32:46 +0800
Subject: [PATCH 2/5] [CIR] Address review feedback for lifetime markers
---
clang/lib/CIR/CodeGen/CIRGenCleanup.cpp | 1 +
clang/lib/CIR/CodeGen/CIRGenDecl.cpp | 69 +++++++++-------------
clang/lib/CIR/CodeGen/CIRGenFunction.cpp | 21 ++++++-
clang/lib/CIR/CodeGen/CIRGenFunction.h | 7 +--
clang/test/CIR/CodeGen/lifetime-marker.cpp | 62 ++++++++++++++++++-
5 files changed, 112 insertions(+), 48 deletions(-)
diff --git a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
index 15aec51f724be..51d41cbd4fcd7 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
@@ -450,6 +450,7 @@ bool EHScopeStack::requiresCatchOrCleanup() const {
for (stable_iterator si = getInnermostEHScope(); si != stable_end();) {
if (auto *cleanup = dyn_cast<EHCleanupScope>(&*find(si))) {
if (cleanup->isLifetimeMarker()) {
+ // Skip lifetime markers and continue from the enclosing EH scope
si = cleanup->getEnclosingEHScope();
continue;
}
diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
index 25a67d3bd027a..d7e4e0cb03da3 100644
--- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
@@ -30,19 +30,16 @@
using namespace clang;
using namespace clang::CIRGen;
-/// Does the statement tree rooted at \p s contain a label, switch, or indirect
-/// goto that could bypass a local's initialization? A coarse stand-in for
-/// classic CodeGen's per-decl bypass analysis (PR28267).
-static bool functionMightHaveBypass(const Stmt *s) {
- if (!s)
- return false;
- if (isa<LabelStmt, SwitchStmt, IndirectGotoStmt>(s))
- return true;
- for (const Stmt *child : s->children())
- if (functionMightHaveBypass(child))
- return true;
- return false;
-}
+struct CallLifetimeEnd final : EHScopeStack::Cleanup {
+ // The raw alloca pointer (in the alloca address space). Mirrors classic
+ // CodeGen's CallLifetimeEnd, which stores the llvm::Value pointer rather
+ // than an Address.
+ mlir::Value addr;
+ CallLifetimeEnd(mlir::Value addr) : addr(addr) {}
+ void emit(CIRGenFunction &cgf, Flags flags) override {
+ cgf.emitLifetimeEndOp(addr.getLoc(), addr);
+ }
+};
CIRGenFunction::AutoVarEmission
CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
@@ -150,15 +147,9 @@ CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
// classic's per-decl bypass analysis, drop markers for the whole
// function if any such statement is present.
assert(!cir::MissingFeatures::lifetimeMarkersBypass());
- if (shouldEmitLifetimeOp && haveInsertPoint()) {
- if (!fnHasBypassStmt.has_value())
- fnHasBypassStmt = functionMightHaveBypass(
- curFuncDecl ? curFuncDecl->getBody() : nullptr);
- // Peel address-space casts to the alloca so the op verifier sees a
- // value produced by cir.alloca.
- if (!*fnHasBypassStmt)
- emission.useLifetimeOp = emitLifetimeStartOp(
- loc, address.getUnderlyingAllocaOp().getResult());
+ if (shouldEmitLifetimeMarkers && haveInsertPoint() && !fnHasBypassStmt) {
+ emission.useLifetimeMarkers = emitLifetimeStartOp(
+ loc, address.getUnderlyingAllocaOp().getResult());
}
}
} else {
@@ -196,12 +187,15 @@ CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
assert(!cir::MissingFeatures::generateDebugInfo());
}
- if (emission.useLifetimeOp)
- pushLifetimeEnd(address);
-
emission.addr = address;
setAddrOfLocalVar(&d, address);
+ // The lifetime marker must reference the original alloca, so peel any
+ // address-space cast back to it.
+ if (emission.useLifetimeMarkers)
+ ehStack.pushCleanup<CallLifetimeEnd>(
+ NormalEHLifetimeMarker, address.getUnderlyingAllocaOp().getResult());
+
return emission;
}
@@ -1097,15 +1091,6 @@ struct CallStackRestore final : EHScopeStack::Cleanup {
}
};
-struct CallLifetimeEnd final : EHScopeStack::Cleanup {
- Address addr;
- CallLifetimeEnd(Address addr) : addr(addr) {}
- void emit(CIRGenFunction &cgf, Flags flags) override {
- mlir::Value allocaPtr = addr.getUnderlyingAllocaOp().getResult();
- cgf.emitLifetimeEndOp(allocaPtr.getLoc(), allocaPtr);
- }
-};
-
/// A cleanup which performs a partial array destroy where the end pointer is
/// irregularly determined and must be loaded from a local.
struct IrregularPartialArrayDestroy final : EHScopeStack::Cleanup {
@@ -1362,10 +1347,6 @@ void CIRGenFunction::pushStackRestore(CleanupKind kind, Address spMem) {
ehStack.pushCleanup<CallStackRestore>(kind, spMem);
}
-void CIRGenFunction::pushLifetimeEnd(Address addr) {
- ehStack.pushCleanup<CallLifetimeEnd>(NormalEHLifetimeMarker, addr);
-}
-
/// Enter a destroy cleanup for the given local variable.
void CIRGenFunction::emitAutoVarTypeCleanup(
const CIRGenFunction::AutoVarEmission &emission,
@@ -1424,16 +1405,24 @@ void CIRGenFunction::maybeEmitDeferredVarDeclInit(const VarDecl *vd) {
}
bool CIRGenFunction::emitLifetimeStartOp(mlir::Location loc, mlir::Value addr) {
- if (!shouldEmitLifetimeOp)
+ if (!shouldEmitLifetimeMarkers)
return false;
+ assert(mlir::cast<cir::PointerType>(addr.getType()).getAddrSpace() ==
+ cir::normalizeDefaultAddressSpace(getCIRAllocaAddressSpace()) &&
+ "Pointer should be in alloca address space");
+
cir::LifetimeStartOp::create(builder, loc, addr);
return true;
}
void CIRGenFunction::emitLifetimeEndOp(mlir::Location loc, mlir::Value addr) {
- if (!shouldEmitLifetimeOp)
+ if (!shouldEmitLifetimeMarkers)
return;
+ assert(mlir::cast<cir::PointerType>(addr.getType()).getAddrSpace() ==
+ cir::normalizeDefaultAddressSpace(getCIRAllocaAddressSpace()) &&
+ "Pointer should be in alloca address space");
+
cir::LifetimeEndOp::create(builder, loc, addr);
}
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
index c729dceb8cddf..d31f639cde908 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
@@ -46,13 +46,27 @@ static bool shouldEmitLifetimeMarkers(const CodeGenOptions &cgOpts,
return cgOpts.OptimizationLevel != 0;
}
+/// Does the statement tree rooted at \p s contain a label, switch, or indirect
+/// goto that could bypass a local's initialization? A coarse stand-in for
+/// classic CodeGen's per-decl bypass analysis (PR28267).
+static bool functionMightHaveBypass(const Stmt *s) {
+ if (!s)
+ return false;
+ if (isa<LabelStmt, SwitchStmt, IndirectGotoStmt>(s))
+ return true;
+ for (const Stmt *child : s->children())
+ if (functionMightHaveBypass(child))
+ return true;
+ return false;
+}
+
CIRGenFunction::CIRGenFunction(CIRGenModule &cgm, CIRGenBuilderTy &builder,
bool suppressNewContext)
: CIRGenTypeCache(cgm), cgm{cgm}, builder(builder),
curFPFeatures(cgm.getLangOpts()) {
ehStack.setCGF(this);
- shouldEmitLifetimeOp = shouldEmitLifetimeMarkers(cgm.getCodeGenOpts(),
- getContext().getLangOpts());
+ shouldEmitLifetimeMarkers = CIRGen::shouldEmitLifetimeMarkers(
+ cgm.getCodeGenOpts(), getContext().getLangOpts());
}
CIRGenFunction::~CIRGenFunction() {}
@@ -765,6 +779,9 @@ cir::FuncOp CIRGenFunction::generateCode(clang::GlobalDecl gd, cir::FuncOp fn,
if (body && isa_and_nonnull<CoroutineBodyStmt>(body))
llvm::append_range(fnArgs, funcDecl->parameters());
+ if (shouldEmitLifetimeMarkers)
+ fnHasBypassStmt = functionMightHaveBypass(body);
+
if (isa<CXXDestructorDecl>(funcDecl)) {
emitDestructorBody(args);
} else if (isa<CXXConstructorDecl>(funcDecl)) {
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index d5b5aa0aa9305..cc26f7decfbd0 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -724,7 +724,7 @@ class CIRGenFunction : public CIRGenTypeCache {
bool emittedAsOffload = false;
/// True if lifetime op should be used.
- bool useLifetimeOp = false;
+ bool useLifetimeMarkers = false;
mlir::Value nrvoFlag{};
@@ -798,7 +798,6 @@ class CIRGenFunction : public CIRGenTypeCache {
}
void pushStackRestore(CleanupKind kind, Address spMem);
- void pushLifetimeEnd(Address addr);
/// Set the address of a local variable.
void setAddrOfLocalVar(const clang::VarDecl *vd, Address addr) {
@@ -2803,10 +2802,10 @@ class CIRGenFunction : public CIRGenTypeCache {
private:
QualType getVarArgType(const Expr *arg);
- bool shouldEmitLifetimeOp = false;
+ bool shouldEmitLifetimeMarkers = false;
/// Set when the current function has a goto/switch that may bypass a local's
/// init; lifetime markers are then suppressed. See functionMightHaveBypass.
- std::optional<bool> fnHasBypassStmt;
+ bool fnHasBypassStmt = false;
class InlinedInheritingConstructorScope {
public:
diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp
index 3e8e54dbce096..0e2642e2223f7 100644
--- a/clang/test/CIR/CodeGen/lifetime-marker.cpp
+++ b/clang/test/CIR/CodeGen/lifetime-marker.cpp
@@ -27,6 +27,11 @@ void f() {
// LLVM: call void @llvm.lifetime.start.p0(ptr %[[X]])
// LLVM: call void @llvm.lifetime.end.p0(ptr %[[X]])
+// Without optimization no lifetime markers are emitted. Checked per function so
+// a regression in a single function can't hide behind a passing global check.
+// O0-LABEL: cir.func{{.*}} @_Z1fv()
+// O0-NOT: cir.lifetime
+
struct S {
~S();
};
@@ -50,5 +55,58 @@ void g() {
// LLVM: call void @_ZN1SD1Ev(ptr {{.*}} %[[S]])
// LLVM: call void @llvm.lifetime.end.p0(ptr %[[S]])
-// Without optimization no lifetime markers are emitted at all.
-// O0-NOT: cir.lifetime
+// O0-LABEL: cir.func{{.*}} @_Z1gv()
+// O0-NOT: cir.lifetime
+
+// A statement that can bypass a local's initialization -- switch, label, or
+// indirect goto -- miscompiles under stack coloring (PR28267). Lacking classic
+// CodeGen's per-decl bypass analysis, we conservatively drop lifetime markers
+// for the *whole* function whenever any such statement is present, even at -O2
+// and even for locals (like `x` below) that are not themselves bypassed.
+
+void bypass_switch(int n) {
+ int x;
+ use(x);
+ switch (n) {
+ case 0:
+ return;
+ }
+}
+
+// CIR-LABEL: cir.func{{.*}}bypass_switch
+// CIR-NOT: cir.lifetime
+
+// LLVM-LABEL: define{{.*}}bypass_switch
+// LLVM-NOT: call void @llvm.lifetime
+
+// O0-LABEL: cir.func{{.*}}bypass_switch
+// O0-NOT: cir.lifetime
+
+void bypass_label(int n) {
+ int x;
+ use(x);
+target:
+ if (n)
+ goto target;
+}
+
+// CIR-LABEL: cir.func{{.*}}bypass_label
+// CIR-NOT: cir.lifetime
+
+// O0-LABEL: cir.func{{.*}}bypass_label
+// O0-NOT: cir.lifetime
+
+void bypass_indirect_goto() {
+ int x;
+ use(x);
+ void *p = &⌖
+ goto *p;
+target:
+ return;
+}
+
+// CIR-LABEL: cir.func{{.*}}bypass_indirect_goto
+// CIR-NOT: cir.lifetime
+
+// O0-LABEL: cir.func{{.*}}bypass_indirect_goto
+// O0-NOT: cir.lifetime
>From 7080a1b13258348107bd6bc5d923a8ce81d2e191 Mon Sep 17 00:00:00 2001
From: E00N777 <E0N_gjh at 163.com>
Date: Wed, 8 Jul 2026 15:22:05 +0800
Subject: [PATCH 3/5] [CIR] Add lifetime marker tests for if-body/EH paths and
suppress markers for loop condition variables
---
clang/lib/CIR/CodeGen/CIRGenDecl.cpp | 23 ++---
clang/lib/CIR/CodeGen/CIRGenFunction.h | 4 +
clang/lib/CIR/CodeGen/CIRGenStmt.cpp | 26 +++---
clang/test/CIR/CodeGen/lifetime-marker.cpp | 99 ++++++++++++++++++++++
4 files changed, 129 insertions(+), 23 deletions(-)
diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
index d7e4e0cb03da3..f00bfc672a866 100644
--- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
@@ -147,7 +147,7 @@ CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
// classic's per-decl bypass analysis, drop markers for the whole
// function if any such statement is present.
assert(!cir::MissingFeatures::lifetimeMarkersBypass());
- if (shouldEmitLifetimeMarkers && haveInsertPoint() && !fnHasBypassStmt) {
+ if (shouldEmitLifetimeMarkersForAutoVar() && haveInsertPoint()) {
emission.useLifetimeMarkers = emitLifetimeStartOp(
loc, address.getUnderlyingAllocaOp().getResult());
}
@@ -390,10 +390,10 @@ void CIRGenFunction::emitLoopConditionVariable(
const VarDecl &d, DeferredLoopConditionCleanup &condCleanup) {
// A condition variable always has automatic storage duration, so this
// mirrors the auto-var path of emitVarDecl/emitAutoVarDecl. The alloca and
- // initializer are emitted with capturing disabled so that any cleanups they
- // introduce get their normal cir.cleanup.scope handling; only the variable's
- // own destructor cleanup is captured for the loop's per-iteration cleanup
- // region.
+ // initializer is emitted with capturing disabled so that any cleanups it
+ // introduces get their normal cir.cleanup.scope handling. The variable's
+ // lifetime-end and destructor cleanups are captured for the loop's
+ // per-iteration cleanup region.
assert(d.hasLocalStorage() && "loop condition variable is not local");
// Mirror the diagnostic emitted by emitVarDecl on the automatic-storage path.
@@ -404,7 +404,10 @@ void CIRGenFunction::emitLoopConditionVariable(
"emitLoopConditionVariable: OpenCL local address space");
CIRGenFunction::VarDeclContext varDeclCtx{*this, &d};
- CIRGenFunction::AutoVarEmission emission = emitAutoVarAlloca(d);
+ CIRGenFunction::AutoVarEmission emission = [&] {
+ DeferredLoopConditionCleanup::CaptureScope capture(condCleanup);
+ return emitAutoVarAlloca(d);
+ }();
// The condition variable's destructor is captured into the loop op's
// per-iteration cleanup region, which structurally spans the initializer.
@@ -416,8 +419,6 @@ void CIRGenFunction::emitLoopConditionVariable(
// completes. The flag is stored to on every iteration, so it also resets
// correctly across iterations.
bool needsCleanup = d.needsDestruction(getContext()) != QualType::DK_none;
- // We will also need cleanup if lifetime markers are enabled.
- assert(!cir::MissingFeatures::emitLifetimeMarkers());
Address activeFlag = Address::invalid();
if (needsCleanup) {
mlir::Location loc = getLoc(d.getSourceRange());
@@ -436,8 +437,10 @@ void CIRGenFunction::emitLoopConditionVariable(
builder.createFlagStore(loc, true, activeFlag.getPointer());
}
- DeferredLoopConditionCleanup::CaptureScope capture(condCleanup);
- emitAutoVarCleanups(emission);
+ {
+ DeferredLoopConditionCleanup::CaptureScope capture(condCleanup);
+ emitAutoVarCleanups(emission);
+ }
if (needsCleanup)
initFullExprCleanupWithFlag(activeFlag);
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index cc26f7decfbd0..f54a4bb09959c 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -2807,6 +2807,10 @@ class CIRGenFunction : public CIRGenTypeCache {
/// init; lifetime markers are then suppressed. See functionMightHaveBypass.
bool fnHasBypassStmt = false;
+ bool shouldEmitLifetimeMarkersForAutoVar() const {
+ return shouldEmitLifetimeMarkers && !fnHasBypassStmt;
+ }
+
class InlinedInheritingConstructorScope {
public:
InlinedInheritingConstructorScope(CIRGenFunction &cgf, GlobalDecl gd)
diff --git a/clang/lib/CIR/CodeGen/CIRGenStmt.cpp b/clang/lib/CIR/CodeGen/CIRGenStmt.cpp
index ceda5811cd065..97537f899b9bc 100644
--- a/clang/lib/CIR/CodeGen/CIRGenStmt.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenStmt.cpp
@@ -1002,15 +1002,15 @@ mlir::LogicalResult CIRGenFunction::emitForStmt(const ForStmt &s) {
return mlir::failure();
assert(!cir::MissingFeatures::loopInfoStack());
- // If the condition variable has a non-trivial destructor, its lifetime is
- // a single iteration, so capture its cleanup and emit it into the loop's
+ // A condition variable's lifetime is a single iteration, so capture its
+ // destructor and lifetime-end cleanups and emit them into the loop's
// per-iteration cleanup region. This scope is constructed after the
- // init-statement so its cleanups are not captured.
+ // init-statement so the init-statement's cleanups are not captured.
const VarDecl *condVar = s.getConditionVariable();
- bool needsCondCleanup =
- condVar && condVar->needsDestruction(getContext()) != QualType::DK_none;
- // We will also need cleanup if lifetime markers are enabled.
- assert(!cir::MissingFeatures::emitLifetimeMarkers());
+ bool needsCondCleanup = condVar &&
+ (condVar->needsDestruction(getContext()) !=
+ QualType::DK_none ||
+ shouldEmitLifetimeMarkersForAutoVar());
DeferredLoopConditionCleanup loopCondScope(*this, needsCondCleanup);
auto condBuilder = [&](mlir::OpBuilder &b, mlir::Location loc) {
@@ -1135,14 +1135,14 @@ mlir::LogicalResult CIRGenFunction::emitWhileStmt(const WhileStmt &s) {
mlir::LogicalResult loopRes = mlir::success();
assert(!cir::MissingFeatures::loopInfoStack());
- // If the condition variable has a non-trivial destructor, its lifetime is
- // a single iteration, so capture its cleanup and emit it into the loop's
+ // A condition variable's lifetime is a single iteration, so capture its
+ // destructor and lifetime-end cleanups and emit them into the loop's
// per-iteration cleanup region.
const VarDecl *condVar = s.getConditionVariable();
- bool needsCondCleanup =
- condVar && condVar->needsDestruction(getContext()) != QualType::DK_none;
- // We will also need cleanup if lifetime markers are enabled.
- assert(!cir::MissingFeatures::emitLifetimeMarkers());
+ bool needsCondCleanup = condVar &&
+ (condVar->needsDestruction(getContext()) !=
+ QualType::DK_none ||
+ shouldEmitLifetimeMarkersForAutoVar());
DeferredLoopConditionCleanup loopCondScope(*this, needsCondCleanup);
auto condBuilder = [&](mlir::OpBuilder &b, mlir::Location loc) {
diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp
index 0e2642e2223f7..9fea56c5a307f 100644
--- a/clang/test/CIR/CodeGen/lifetime-marker.cpp
+++ b/clang/test/CIR/CodeGen/lifetime-marker.cpp
@@ -4,6 +4,10 @@
// RUN: FileCheck --input-file=%t.ll %s --check-prefix=LLVM
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t-o0.cir
// RUN: FileCheck --input-file=%t-o0.cir %s --check-prefix=O0
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fcxx-exceptions -fexceptions -fclangir -emit-cir %s -o %t-eh.cir
+// RUN: FileCheck --input-file=%t-eh.cir %s --check-prefix=CIR-EH
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fcxx-exceptions -fexceptions -fclangir -emit-llvm -disable-llvm-passes %s -o %t-eh.ll
+// RUN: FileCheck --input-file=%t-eh.ll %s --check-prefix=LLVM-EH
void use(int);
@@ -110,3 +114,98 @@ void bypass_indirect_goto() {
// O0-LABEL: cir.func{{.*}}bypass_indirect_goto
// O0-NOT: cir.lifetime
+
+// A local declared inside the body region of an if statement is scoped to that
+// region: its lifetime.start/end are nested in the region and the end marker
+// is the region's cleanup, not the function's.
+void if_body(int n) {
+ if (n) {
+ int x;
+ use(x);
+ }
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z7if_bodyi
+// CIR: cir.if
+// CIR: %[[X:.*]] = cir.alloca "x" {{.*}} : !cir.ptr<!s32i>
+// CIR: cir.lifetime.start %[[X]] : !cir.ptr<!s32i>
+// CIR: cir.cleanup.scope {
+// CIR: } cleanup normal {
+// CIR: cir.lifetime.end %[[X]] : !cir.ptr<!s32i>
+// CIR: }
+
+// LLVM-LABEL: define{{.*}} void @_Z7if_bodyi
+// LLVM: %[[X:.*]] = alloca i32
+// LLVM: call void @llvm.lifetime.start.p0(ptr %[[X]])
+// LLVM: call void @llvm.lifetime.end.p0(ptr %[[X]])
+
+// O0-LABEL: cir.func{{.*}} @_Z7if_bodyi
+// O0-NOT: cir.lifetime
+
+// 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.
+void may_throw();
+
+void eh_cleanup() {
+ int x;
+ may_throw();
+ use(x);
+}
+
+// CIR-EH-LABEL: cir.func{{.*}} @_Z10eh_cleanupv
+// CIR-EH: %[[X:.*]] = cir.alloca "x" {{.*}} : !cir.ptr<!s32i>
+// CIR-EH: cir.lifetime.start %[[X]] : !cir.ptr<!s32i>
+// CIR-EH: cir.cleanup.scope {
+// CIR-EH: cir.call @_Z9may_throwv()
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[X]] : !cir.ptr<!s32i>
+// CIR-EH: }
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z10eh_cleanupv()
+// 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 @llvm.lifetime.end.p0(ptr %[[X]])
+
+// A loop condition variable is destroyed and re-created on every iteration
+// (C++ [stmt.while]p2), so its lifetime.end would have to run on both the
+// loop's back edge and its exit edge. The structured cond region cannot
+// express a cleanup on both edges (a cleanup scope would trap the loop's
+// condition terminator inside it), so lifetime markers are suppressed for
+// variables declared in a while or for condition.
+int source();
+
+void while_condvar() {
+ while (int c = source())
+ use(c);
+}
+
+// CIR-LABEL: cir.func{{.*}}while_condvar
+// CIR-NOT: cir.lifetime
+
+// LLVM-LABEL: define{{.*}}while_condvar
+// LLVM-NOT: call void @llvm.lifetime
+
+// O0-LABEL: cir.func{{.*}}while_condvar
+// O0-NOT: cir.lifetime
+
+// CIR-EH-LABEL: cir.func{{.*}}while_condvar
+// CIR-EH-NOT: cir.lifetime
+
+void for_condvar() {
+ for (; int c = source();)
+ use(c);
+}
+
+// CIR-LABEL: cir.func{{.*}}for_condvar
+// CIR-NOT: cir.lifetime
+
+// O0-LABEL: cir.func{{.*}}for_condvar
+// O0-NOT: cir.lifetime
>From 1475eb24bc53974bffc41d0ae60ecd74e6f1ba94 Mon Sep 17 00:00:00 2001
From: E00N777 <E0N_gjh at 163.com>
Date: Fri, 7 Aug 2026 21:37:55 +0800
Subject: [PATCH 4/5] [CIR] Add lifetime marker tests and support loop
condition variables
---
clang/lib/CIR/CodeGen/CIRGenCleanup.cpp | 12 ++-
clang/lib/CIR/CodeGen/CIRGenDecl.cpp | 10 +-
clang/lib/CIR/CodeGen/CIRGenFunction.h | 8 +-
clang/lib/CIR/CodeGen/CIRGenStmt.cpp | 16 +--
clang/test/CIR/CodeGen/lifetime-marker.cpp | 119 ++++++++++++++++++---
5 files changed, 128 insertions(+), 37 deletions(-)
diff --git a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
index 51d41cbd4fcd7..8103ef37f9225 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
@@ -364,7 +364,7 @@ void *EHScopeStack::pushCleanup(CleanupKind kind, size_t size) {
}
// While emitting a loop's condition variable, suppress cir.cleanup.scope
- // creation. The variable's destructor is captured on the EH stack and later
+ // creation. The variable's cleanups are captured on the EH stack and later
// emitted into the loop op's per-iteration cleanup region.
if (capturingLoopConditionCleanups)
skipCleanupScope = true;
@@ -743,10 +743,12 @@ void CIRGenFunction::emitLoopConditionCleanups(
if (scope.isEHCleanup())
cleanupFlags.setIsEHCleanupKind();
- // The condition variable's cleanup is guarded by an active flag that is
- // false while its initializer runs, so a throwing initializer does not
- // destroy the not-yet-constructed variable. The single guarded emission
- // serves both the normal per-iteration exit and the EH unwind path.
+ // A condition variable's destructor cleanup is guarded by an active flag
+ // that is false while its initializer runs, so a throwing initializer does
+ // not destroy the not-yet-constructed variable. The lifetime-end cleanup
+ // has no flag because its lifetime starts before initialization. Each
+ // emission serves both the normal per-iteration exit and the EH unwind
+ // path.
Address activeFlag = scope.getActiveFlag();
// Copy the cleanup emission data out before popping, since popCleanup
diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
index f00bfc672a866..a7c909ea408ef 100644
--- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
@@ -389,11 +389,11 @@ void CIRGenFunction::emitAutoVarDecl(const VarDecl &d) {
void CIRGenFunction::emitLoopConditionVariable(
const VarDecl &d, DeferredLoopConditionCleanup &condCleanup) {
// A condition variable always has automatic storage duration, so this
- // mirrors the auto-var path of emitVarDecl/emitAutoVarDecl. The alloca and
- // initializer is emitted with capturing disabled so that any cleanups it
- // introduces get their normal cir.cleanup.scope handling. The variable's
- // lifetime-end and destructor cleanups are captured for the loop's
- // per-iteration cleanup region.
+ // mirrors the auto-var path of emitVarDecl/emitAutoVarDecl. Capture the
+ // lifetime-end cleanup pushed while emitting the alloca, but emit the
+ // initializer with capturing disabled so its own cleanups get their normal
+ // cir.cleanup.scope handling. The variable's destructor cleanup is captured
+ // separately after initialization.
assert(d.hasLocalStorage() && "loop condition variable is not local");
// Mirror the diagnostic emitted by emitVarDecl on the automatic-storage path.
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index f54a4bb09959c..125a8a58f7e18 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -1353,8 +1353,8 @@ class CIRGenFunction : public CIRGenTypeCache {
void operator=(const FullExprCleanupScope &) = delete;
};
- /// Captures the destructor cleanup for a loop's condition variable so that it
- /// can be emitted into the loop op's per-iteration cleanup region.
+ /// Captures cleanups for a loop's condition variable so that they can be
+ /// emitted into the loop op's per-iteration cleanup region.
class DeferredLoopConditionCleanup {
CIRGenFunction &cgf;
EHScopeStack::stable_iterator depth;
@@ -1374,8 +1374,8 @@ class CIRGenFunction : public CIRGenTypeCache {
public:
explicit CaptureScope(DeferredLoopConditionCleanup &scope)
: ehStack(scope.cgf.ehStack) {
- // Capturing wraps only the condition variable's own destructor push,
- // which emits no nested code, so it can never already be active.
+ // Capture scopes deliberately wrap individual cleanup-producing
+ // operations, so they must never nest.
assert(!ehStack.isCapturingLoopConditionCleanups() &&
"loop condition cleanup capturing should not nest");
if (scope.active)
diff --git a/clang/lib/CIR/CodeGen/CIRGenStmt.cpp b/clang/lib/CIR/CodeGen/CIRGenStmt.cpp
index 97537f899b9bc..61f51619efbff 100644
--- a/clang/lib/CIR/CodeGen/CIRGenStmt.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenStmt.cpp
@@ -1007,10 +1007,10 @@ mlir::LogicalResult CIRGenFunction::emitForStmt(const ForStmt &s) {
// per-iteration cleanup region. This scope is constructed after the
// init-statement so the init-statement's cleanups are not captured.
const VarDecl *condVar = s.getConditionVariable();
- bool needsCondCleanup = condVar &&
- (condVar->needsDestruction(getContext()) !=
- QualType::DK_none ||
- shouldEmitLifetimeMarkersForAutoVar());
+ bool needsCondCleanup =
+ condVar &&
+ (condVar->needsDestruction(getContext()) != QualType::DK_none ||
+ shouldEmitLifetimeMarkersForAutoVar());
DeferredLoopConditionCleanup loopCondScope(*this, needsCondCleanup);
auto condBuilder = [&](mlir::OpBuilder &b, mlir::Location loc) {
@@ -1139,10 +1139,10 @@ mlir::LogicalResult CIRGenFunction::emitWhileStmt(const WhileStmt &s) {
// destructor and lifetime-end cleanups and emit them into the loop's
// per-iteration cleanup region.
const VarDecl *condVar = s.getConditionVariable();
- bool needsCondCleanup = condVar &&
- (condVar->needsDestruction(getContext()) !=
- QualType::DK_none ||
- shouldEmitLifetimeMarkersForAutoVar());
+ bool needsCondCleanup =
+ condVar &&
+ (condVar->needsDestruction(getContext()) != QualType::DK_none ||
+ shouldEmitLifetimeMarkersForAutoVar());
DeferredLoopConditionCleanup loopCondScope(*this, needsCondCleanup);
auto condBuilder = [&](mlir::OpBuilder &b, mlir::Location loc) {
diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp
index 9fea56c5a307f..f8e23c4854305 100644
--- a/clang/test/CIR/CodeGen/lifetime-marker.cpp
+++ b/clang/test/CIR/CodeGen/lifetime-marker.cpp
@@ -175,11 +175,8 @@ void eh_cleanup() {
// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[X]])
// A loop condition variable is destroyed and re-created on every iteration
-// (C++ [stmt.while]p2), so its lifetime.end would have to run on both the
-// loop's back edge and its exit edge. The structured cond region cannot
-// express a cleanup on both edges (a cleanup scope would trap the loop's
-// condition terminator inside it), so lifetime markers are suppressed for
-// variables declared in a while or for condition.
+// (C++ [stmt.while]p2). Its lifetime starts in the condition region and ends in
+// the loop cleanup region, which runs on both the back edge and the exit edge.
int source();
void while_condvar() {
@@ -187,25 +184,117 @@ void while_condvar() {
use(c);
}
-// CIR-LABEL: cir.func{{.*}}while_condvar
-// CIR-NOT: cir.lifetime
+// CIR-LABEL: cir.func{{.*}} @_Z13while_condvarv
+// CIR: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!s32i>
+// CIR: cir.while {
+// CIR: cir.lifetime.start %[[C]] : !cir.ptr<!s32i>
+// CIR: } do {
+// CIR: } cleanup normal {
+// CIR: cir.lifetime.end %[[C]] : !cir.ptr<!s32i>
-// LLVM-LABEL: define{{.*}}while_condvar
-// LLVM-NOT: call void @llvm.lifetime
+// LLVM-LABEL: define{{.*}} void @_Z13while_condvarv
+// LLVM: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
+// LLVM: call void @llvm.lifetime.end.p0(ptr %[[C]])
-// O0-LABEL: cir.func{{.*}}while_condvar
+// O0-LABEL: cir.func{{.*}} @_Z13while_condvarv
// O0-NOT: cir.lifetime
-// CIR-EH-LABEL: cir.func{{.*}}while_condvar
-// CIR-EH-NOT: cir.lifetime
+// CIR-EH-LABEL: cir.func{{.*}} @_Z13while_condvarv
+// CIR-EH: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!s32i>
+// CIR-EH: cir.while {
+// CIR-EH: cir.lifetime.start %[[C]] : !cir.ptr<!s32i>
+// CIR-EH: } do {
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[C]] : !cir.ptr<!s32i>
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z13while_condvarv
+// 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 void @llvm.lifetime.end.p0(ptr %[[C]])
void for_condvar() {
for (; int c = source();)
use(c);
}
-// CIR-LABEL: cir.func{{.*}}for_condvar
-// CIR-NOT: cir.lifetime
+// CIR-LABEL: cir.func{{.*}} @_Z11for_condvarv
+// CIR: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!s32i>
+// CIR: cir.for : cond {
+// CIR: cir.lifetime.start %[[C]] : !cir.ptr<!s32i>
+// CIR: } body {
+// CIR: } step {
+// CIR: } cleanup normal {
+// CIR: cir.lifetime.end %[[C]] : !cir.ptr<!s32i>
+
+// LLVM-LABEL: define{{.*}} void @_Z11for_condvarv
+// LLVM: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
+// LLVM: call void @llvm.lifetime.end.p0(ptr %[[C]])
+
+// O0-LABEL: cir.func{{.*}} @_Z11for_condvarv
+// O0-NOT: cir.lifetime
+
+// CIR-EH-LABEL: cir.func{{.*}} @_Z11for_condvarv
+// CIR-EH: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!s32i>
+// CIR-EH: cir.for : cond {
+// CIR-EH: cir.lifetime.start %[[C]] : !cir.ptr<!s32i>
+// CIR-EH: } body {
+// CIR-EH: } step {
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.lifetime.end %[[C]] : !cir.ptr<!s32i>
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z11for_condvarv
+// 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 void @llvm.lifetime.end.p0(ptr %[[C]])
+
+struct LoopCond {
+ operator bool() const;
+ ~LoopCond();
+};
-// O0-LABEL: cir.func{{.*}}for_condvar
+LoopCond make_loop_cond();
+
+// A non-trivial condition variable runs its destructor before lifetime.end in
+// the loop cleanup region.
+void while_record_condvar() {
+ while (LoopCond c = make_loop_cond()) {}
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z20while_record_condvarv
+// CIR: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!rec_LoopCond>
+// CIR: cir.while {
+// CIR: cir.lifetime.start %[[C]] : !cir.ptr<!rec_LoopCond>
+// CIR: } do {
+// CIR: } cleanup normal {
+// CIR: cir.call @_ZN8LoopCondD1Ev(%[[C]])
+// CIR: cir.lifetime.end %[[C]] : !cir.ptr<!rec_LoopCond>
+
+// LLVM-LABEL: define{{.*}} void @_Z20while_record_condvarv
+// LLVM: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
+// LLVM: call void @_ZN8LoopCondD1Ev(ptr {{.*}} %[[C]])
+// LLVM: call void @llvm.lifetime.end.p0(ptr %[[C]])
+
+// O0-LABEL: cir.func{{.*}} @_Z20while_record_condvarv
// O0-NOT: cir.lifetime
+
+// CIR-EH-LABEL: cir.func{{.*}} @_Z20while_record_condvarv
+// CIR-EH: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!rec_LoopCond>
+// CIR-EH: cir.while {
+// CIR-EH: cir.lifetime.start %[[C]] : !cir.ptr<!rec_LoopCond>
+// CIR-EH: } do {
+// CIR-EH: } cleanup all {
+// CIR-EH: cir.call @_ZN8LoopCondD1Ev(%[[C]])
+// CIR-EH: cir.lifetime.end %[[C]] : !cir.ptr<!rec_LoopCond>
+
+// LLVM-EH-LABEL: define{{.*}} void @_Z20while_record_condvarv
+// LLVM-EH: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
+// LLVM-EH: call void @_ZN8LoopCondD1Ev(ptr {{.*}} %[[C]])
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
+// LLVM-EH: landingpad { ptr, i32 }
+// LLVM-EH-NEXT: cleanup
+// LLVM-EH: call void @_ZN8LoopCondD1Ev(ptr {{.*}} %[[C]])
+// LLVM-EH: call void @llvm.lifetime.end.p0(ptr %[[C]])
>From 2998a7580b7d9195a2edcbe3765f9e16760e9054 Mon Sep 17 00:00:00 2001
From: E00N777 <E0N_gjh at 163.com>
Date: Tue, 11 Aug 2026 21:11:20 +0800
Subject: [PATCH 5/5] [CIR] Strengthen lifetime marker test checks
---
clang/test/CIR/CodeGen/lifetime-marker.cpp | 51 +++++++---------------
1 file changed, 16 insertions(+), 35 deletions(-)
diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp
index f8e23c4854305..ce661aafe4498 100644
--- a/clang/test/CIR/CodeGen/lifetime-marker.cpp
+++ b/clang/test/CIR/CodeGen/lifetime-marker.cpp
@@ -3,7 +3,7 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fclangir -emit-llvm -disable-llvm-passes %s -o %t.ll
// RUN: FileCheck --input-file=%t.ll %s --check-prefix=LLVM
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t-o0.cir
-// RUN: FileCheck --input-file=%t-o0.cir %s --check-prefix=O0
+// RUN: FileCheck --input-file=%t-o0.cir %s --implicit-check-not "cir.lifetime"
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fcxx-exceptions -fexceptions -fclangir -emit-cir %s -o %t-eh.cir
// RUN: FileCheck --input-file=%t-eh.cir %s --check-prefix=CIR-EH
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fcxx-exceptions -fexceptions -fclangir -emit-llvm -disable-llvm-passes %s -o %t-eh.ll
@@ -22,6 +22,7 @@ void f() {
// CIR: %[[X:.*]] = cir.alloca "x" {{.*}} : !cir.ptr<!s32i>
// CIR: cir.lifetime.start %[[X]] : !cir.ptr<!s32i>
// CIR: cir.cleanup.scope {
+// CIR: cir.call @_Z3usei
// CIR: } cleanup normal {
// CIR: cir.lifetime.end %[[X]] : !cir.ptr<!s32i>
// CIR: }
@@ -29,13 +30,9 @@ void f() {
// LLVM-LABEL: define{{.*}} void @_Z1fv()
// LLVM: %[[X:.*]] = alloca i32
// LLVM: call void @llvm.lifetime.start.p0(ptr %[[X]])
+// LLVM: call void @_Z3usei
// LLVM: call void @llvm.lifetime.end.p0(ptr %[[X]])
-// Without optimization no lifetime markers are emitted. Checked per function so
-// a regression in a single function can't hide behind a passing global check.
-// O0-LABEL: cir.func{{.*}} @_Z1fv()
-// O0-NOT: cir.lifetime
-
struct S {
~S();
};
@@ -59,9 +56,6 @@ void g() {
// LLVM: call void @_ZN1SD1Ev(ptr {{.*}} %[[S]])
// LLVM: call void @llvm.lifetime.end.p0(ptr %[[S]])
-// O0-LABEL: cir.func{{.*}} @_Z1gv()
-// O0-NOT: cir.lifetime
-
// A statement that can bypass a local's initialization -- switch, label, or
// indirect goto -- miscompiles under stack coloring (PR28267). Lacking classic
// CodeGen's per-decl bypass analysis, we conservatively drop lifetime markers
@@ -83,9 +77,6 @@ void bypass_switch(int n) {
// LLVM-LABEL: define{{.*}}bypass_switch
// LLVM-NOT: call void @llvm.lifetime
-// O0-LABEL: cir.func{{.*}}bypass_switch
-// O0-NOT: cir.lifetime
-
void bypass_label(int n) {
int x;
use(x);
@@ -97,9 +88,6 @@ void bypass_label(int n) {
// CIR-LABEL: cir.func{{.*}}bypass_label
// CIR-NOT: cir.lifetime
-// O0-LABEL: cir.func{{.*}}bypass_label
-// O0-NOT: cir.lifetime
-
void bypass_indirect_goto() {
int x;
use(x);
@@ -112,9 +100,6 @@ void bypass_indirect_goto() {
// CIR-LABEL: cir.func{{.*}}bypass_indirect_goto
// CIR-NOT: cir.lifetime
-// O0-LABEL: cir.func{{.*}}bypass_indirect_goto
-// O0-NOT: cir.lifetime
-
// A local declared inside the body region of an if statement is scoped to that
// region: its lifetime.start/end are nested in the region and the end marker
// is the region's cleanup, not the function's.
@@ -123,24 +108,31 @@ void if_body(int n) {
int x;
use(x);
}
+ use(n);
}
// CIR-LABEL: cir.func{{.*}} @_Z7if_bodyi
-// CIR: cir.if
+// CIR: cir.if %{{.*}} {
// CIR: %[[X:.*]] = cir.alloca "x" {{.*}} : !cir.ptr<!s32i>
// CIR: cir.lifetime.start %[[X]] : !cir.ptr<!s32i>
// CIR: cir.cleanup.scope {
+// CIR: cir.call @_Z3usei
// CIR: } cleanup normal {
// CIR: cir.lifetime.end %[[X]] : !cir.ptr<!s32i>
-// CIR: }
+// CIR-NEXT: cir.yield
+// CIR-NEXT: }
+// CIR-NEXT: }
+// CIR: cir.call @_Z3usei
// LLVM-LABEL: define{{.*}} void @_Z7if_bodyi
// LLVM: %[[X:.*]] = alloca i32
+// LLVM: br i1 %{{.*}}, label %[[IF_BODY:[0-9]+]], label %[[IF_END:[0-9]+]]
+// LLVM: [[IF_BODY]]:
// LLVM: call void @llvm.lifetime.start.p0(ptr %[[X]])
+// LLVM: call void @_Z3usei
// LLVM: call void @llvm.lifetime.end.p0(ptr %[[X]])
-
-// O0-LABEL: cir.func{{.*}} @_Z7if_bodyi
-// O0-NOT: cir.lifetime
+// 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
@@ -196,9 +188,6 @@ void while_condvar() {
// LLVM: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
// LLVM: call void @llvm.lifetime.end.p0(ptr %[[C]])
-// O0-LABEL: cir.func{{.*}} @_Z13while_condvarv
-// O0-NOT: cir.lifetime
-
// CIR-EH-LABEL: cir.func{{.*}} @_Z13while_condvarv
// CIR-EH: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!s32i>
// CIR-EH: cir.while {
@@ -232,9 +221,6 @@ void for_condvar() {
// LLVM: call void @llvm.lifetime.start.p0(ptr %[[C:.*]])
// LLVM: call void @llvm.lifetime.end.p0(ptr %[[C]])
-// O0-LABEL: cir.func{{.*}} @_Z11for_condvarv
-// O0-NOT: cir.lifetime
-
// CIR-EH-LABEL: cir.func{{.*}} @_Z11for_condvarv
// CIR-EH: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!s32i>
// CIR-EH: cir.for : cond {
@@ -256,12 +242,10 @@ struct LoopCond {
~LoopCond();
};
-LoopCond make_loop_cond();
-
// A non-trivial condition variable runs its destructor before lifetime.end in
// the loop cleanup region.
void while_record_condvar() {
- while (LoopCond c = make_loop_cond()) {}
+ while (LoopCond c{}) {}
}
// CIR-LABEL: cir.func{{.*}} @_Z20while_record_condvarv
@@ -278,9 +262,6 @@ void while_record_condvar() {
// LLVM: call void @_ZN8LoopCondD1Ev(ptr {{.*}} %[[C]])
// LLVM: call void @llvm.lifetime.end.p0(ptr %[[C]])
-// O0-LABEL: cir.func{{.*}} @_Z20while_record_condvarv
-// O0-NOT: cir.lifetime
-
// CIR-EH-LABEL: cir.func{{.*}} @_Z20while_record_condvarv
// CIR-EH: %[[C:.*]] = cir.alloca "c" {{.*}} : !cir.ptr<!rec_LoopCond>
// CIR-EH: cir.while {
More information about the cfe-commits
mailing list