https://github.com/E00N777 updated https://github.com/llvm/llvm-project/pull/206695
>From bcd87ec04965353797c79ef88a4f3b5e373e74b3 Mon Sep 17 00:00:00 2001 From: E00N777 <[email protected]> Date: Tue, 30 Jun 2026 17:40:25 +0800 Subject: [PATCH 1/4] [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 9a1546fe14e65..c1df47a5de637 100644 --- a/clang/include/clang/CIR/MissingFeatures.h +++ b/clang/include/clang/CIR/MissingFeatures.h @@ -227,6 +227,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 c1d2def6909d7..1bf4317347817 100644 --- a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp @@ -392,7 +392,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 && @@ -436,8 +436,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 ad572f23b3667..34ae84252c21c 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); @@ -1005,6 +1039,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 { @@ -1261,6 +1304,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, @@ -1317,3 +1364,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 6606cf74c7dea..be1f01c606a02 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp @@ -28,10 +28,30 @@ 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) { 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 b6a4a277fab92..5e3768a332042 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.h +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h @@ -688,6 +688,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 {}; @@ -769,6 +772,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) { @@ -1553,6 +1557,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); @@ -2699,6 +2706,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 827a2e7f41d4c3fdbe8cf4444aa3d99bd019da42 Mon Sep 17 00:00:00 2001 From: E00N777 <[email protected]> Date: Thu, 2 Jul 2026 18:32:46 +0800 Subject: [PATCH 2/4] [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 1bf4317347817..d13cf33884162 100644 --- a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp @@ -436,6 +436,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 34ae84252c21c..d6524bd05089e 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; } @@ -1039,15 +1033,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 { @@ -1304,10 +1289,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, @@ -1366,16 +1347,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 be1f01c606a02..ea904b197d962 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp @@ -46,12 +46,26 @@ 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) { ehStack.setCGF(this); - shouldEmitLifetimeOp = shouldEmitLifetimeMarkers(cgm.getCodeGenOpts(), - getContext().getLangOpts()); + shouldEmitLifetimeMarkers = CIRGen::shouldEmitLifetimeMarkers( + cgm.getCodeGenOpts(), getContext().getLangOpts()); } CIRGenFunction::~CIRGenFunction() {} @@ -791,6 +805,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 5e3768a332042..10adfa8e38b65 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.h +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h @@ -689,7 +689,7 @@ class CIRGenFunction : public CIRGenTypeCache { bool emittedAsOffload = false; /// True if lifetime op should be used. - bool useLifetimeOp = false; + bool useLifetimeMarkers = false; mlir::Value nrvoFlag{}; @@ -772,7 +772,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) { @@ -2706,10 +2705,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 8a5fe3c014d7e486472ffc9d62764ff7f7547e83 Mon Sep 17 00:00:00 2001 From: E00N777 <[email protected]> Date: Fri, 3 Jul 2026 17:28:54 +0800 Subject: [PATCH 3/4] temp --- clang/test/CIR/CodeGen/lifetime-marker.cpp | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp index 0e2642e2223f7..2ea6fc207ec3f 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,62 @@ 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]]) >From 4a21a1db0bb5be28baddb0884401ab002af34aa8 Mon Sep 17 00:00:00 2001 From: E00N777 <[email protected]> Date: Wed, 8 Jul 2026 14:33:22 +0800 Subject: [PATCH 4/4] [CIR] Suppress lifetime markers for loop condition variables and add tests --- clang/include/clang/CIR/MissingFeatures.h | 1 + clang/lib/CIR/CodeGen/CIRGenDecl.cpp | 3 +- clang/lib/CIR/CodeGen/CIRGenFunction.h | 6 ++++ clang/lib/CIR/CodeGen/CIRGenStmt.cpp | 17 ++++++++-- clang/test/CIR/CodeGen/lifetime-marker.cpp | 36 ++++++++++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/clang/include/clang/CIR/MissingFeatures.h b/clang/include/clang/CIR/MissingFeatures.h index c1df47a5de637..f97a7e37a6880 100644 --- a/clang/include/clang/CIR/MissingFeatures.h +++ b/clang/include/clang/CIR/MissingFeatures.h @@ -228,6 +228,7 @@ struct MissingFeatures { static bool emitConstrainedFPCall() { return false; } static bool emitLifetimeMarkers() { return false; } static bool lifetimeMarkersBypass() { return false; } + static bool lifetimeMarkersLoopCondVar() { return false; } static bool emitLValueAlignmentAssumption() { return false; } static bool emitNullCheckForDeleteCalls() { return false; } static bool emitNullabilityCheck() { return false; } diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp index d6524bd05089e..7ffdd50af733b 100644 --- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp @@ -147,7 +147,8 @@ 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 (shouldEmitLifetimeMarkers && haveInsertPoint() && !fnHasBypassStmt && + !suppressLoopCondVarLifetime) { emission.useLifetimeMarkers = emitLifetimeStartOp( loc, address.getUnderlyingAllocaOp().getResult()); } diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h index 10adfa8e38b65..e0853b4d50000 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.h +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h @@ -2709,6 +2709,12 @@ class CIRGenFunction : public CIRGenTypeCache { /// Set when the current function has a goto/switch that may bypass a local's /// init; lifetime markers are then suppressed. See functionMightHaveBypass. bool fnHasBypassStmt = false; + /// Set while emitting a loop condition variable's declaration. Such a + /// variable is destroyed and re-created on every iteration, 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 + /// yet, so lifetime markers are suppressed for these variables. + bool suppressLoopCondVarLifetime = false; class InlinedInheritingConstructorScope { public: diff --git a/clang/lib/CIR/CodeGen/CIRGenStmt.cpp b/clang/lib/CIR/CodeGen/CIRGenStmt.cpp index 47c94cb4ec535..34f43b7a20aad 100644 --- a/clang/lib/CIR/CodeGen/CIRGenStmt.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenStmt.cpp @@ -21,6 +21,7 @@ #include "clang/AST/StmtOpenACC.h" #include "clang/AST/StmtOpenMP.h" #include "clang/CIR/MissingFeatures.h" +#include "llvm/Support/SaveAndRestore.h" using namespace clang; using namespace clang::CIRGen; @@ -994,8 +995,14 @@ mlir::LogicalResult CIRGenFunction::emitForStmt(const ForStmt &s) { if (s.getCond()) { // If the for statement has a condition scope, // emit the local variable declaration. - if (s.getConditionVariable()) + if (s.getConditionVariable()) { + // A lifetime.end for the condition variable would have to run + // on both the back edge and the exit edge, which the cond + // region cannot express; suppress its lifetime markers. + assert(!cir::MissingFeatures::lifetimeMarkersLoopCondVar()); + llvm::SaveAndRestore suppress(suppressLoopCondVarLifetime, true); emitDecl(*s.getConditionVariable()); + } // C99 6.8.5p2/p4: The first substatement is executed if the // expression compares unequal to 0. The condition must be a // scalar type. @@ -1104,8 +1111,14 @@ mlir::LogicalResult CIRGenFunction::emitWhileStmt(const WhileStmt &s) { mlir::Value condVal; // If the for statement has a condition scope, // emit the local variable declaration. - if (s.getConditionVariable()) + if (s.getConditionVariable()) { + // A lifetime.end for the condition variable would have to run + // on both the back edge and the exit edge, which the cond + // region cannot express; suppress its lifetime markers. + assert(!cir::MissingFeatures::lifetimeMarkersLoopCondVar()); + llvm::SaveAndRestore suppress(suppressLoopCondVarLifetime, true); emitDecl(*s.getConditionVariable()); + } // C99 6.8.5p2/p4: The first substatement is executed if the // expression compares unequal to 0. The condition must be a // scalar type. diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp index 2ea6fc207ec3f..9fea56c5a307f 100644 --- a/clang/test/CIR/CodeGen/lifetime-marker.cpp +++ b/clang/test/CIR/CodeGen/lifetime-marker.cpp @@ -173,3 +173,39 @@ void eh_cleanup() { // 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 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
