[clang] [CIR] Add dead_on_return for destructors (PR #235125)
via cfe-commits
cfe-commits at lists.llvm.org
Fri Oct 23 14:59:29 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clangir
Author: Andy Kaylor (andykaylor)
<details>
<summary>Changes</summary>
This change adds the dead_on_return attribute for the `this` argument to destructors unless `-fno-lifetime-dse` was used.
Assisted-by: Cursor / claude-opus-5.5
---
Full diff: https://github.com/llvm/llvm-project/pull/235125.diff
5 Files Affected:
- (modified) clang/lib/CIR/CodeGen/CIRGenCall.cpp (+21-8)
- (modified) clang/lib/CIR/CodeGen/CIRGenModule.h (+1-1)
- (modified) clang/test/CIR/CodeGen/call-conv-lowering-x86_64-non-byval.cpp (+5-10)
- (added) clang/test/CIR/CodeGen/destructor-dead-on-return.cpp (+121)
- (modified) clang/test/CIR/CodeGen/paren-init-list.cpp (+1-1)
``````````diff
diff --git a/clang/lib/CIR/CodeGen/CIRGenCall.cpp b/clang/lib/CIR/CodeGen/CIRGenCall.cpp
index 16087a04086ac..c832c7e94cb1d 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCall.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCall.cpp
@@ -574,8 +574,8 @@ void CIRGenModule::constructAttributeList(
// TODO(cir): Add loader-replaceable attribute here.
constructFunctionReturnAttributes(info, targetDecl, isThunk, retAttrs);
- constructFunctionArgumentAttributes(info, targetDecl, isThunk, attrOnCallSite,
- argAttrs);
+ constructFunctionArgumentAttributes(info, calleeInfo.getCalleeDecl(), isThunk,
+ attrOnCallSite, argAttrs);
}
bool CIRGenModule::hasStrictReturn(QualType retTy, const Decl *targetDecl) {
@@ -722,9 +722,10 @@ void CIRGenModule::constructFunctionReturnAttributes(
}
void CIRGenModule::constructFunctionArgumentAttributes(
- const CIRGenFunctionInfo &info, const Decl *targetDecl, bool isThunk,
+ const CIRGenFunctionInfo &info, GlobalDecl calleeDecl, bool isThunk,
bool attrOnCallSite, llvm::MutableArrayRef<mlir::NamedAttrList> argAttrs) {
assert(!cir::MissingFeatures::abiArgInfo());
+ const Decl *targetDecl = calleeDecl.getDecl();
// TODO(cir): classic codegen does a lot of work here based on the ABIArgInfo
// to set things based on calling convention.
@@ -756,11 +757,23 @@ void CIRGenModule::constructFunctionArgumentAttributes(
builder.getI64IntegerAttr(
getNaturalPointeeTypeAlignment(thisPtrTy).getQuantity()));
- // TODO(cir): the classic codegen has a recently-added bunch of logic for
- // 'dead_on_return' as an attribute. This both doesn't exist in the LLVM
- // dialect, and is 'too new' at the time of writing this to be considered
- // stable enough here. For now, we'll leave this as a TODO so that when
- // we come back, it is hopefully a more stabilized implementation.
+ // With strict lifetimes, the object's data is dead when a destructor
+ // returns, except for a deleting destructor, whose operator delete may
+ // still use the storage, and except where a virtual base outlives the
+ // destructor of the object holding it. An object with no data gets no
+ // attribute.
+ const auto *dd = dyn_cast_if_present<CXXDestructorDecl>(targetDecl);
+ if (dd && codeGenOpts.StrictLifetimes &&
+ calleeDecl.getDtorType() != CXXDtorType::Dtor_VectorDeleting &&
+ calleeDecl.getDtorType() != CXXDtorType::Dtor_Deleting) {
+ const CXXRecordDecl *classDecl = dd->getParent();
+ uint64_t dataSize = astContext.getASTRecordLayout(classDecl)
+ .getDataSize()
+ .getQuantity();
+ if (classDecl->getNumVBases() == 0 && dataSize != 0)
+ argAttrs[0].set(mlir::LLVM::LLVMDialect::getDeadOnReturnAttrName(),
+ builder.getI64IntegerAttr(dataSize));
+ }
}
}
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.h b/clang/lib/CIR/CodeGen/CIRGenModule.h
index 074ca5818ae2e..ed193267b7ee0 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.h
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.h
@@ -174,7 +174,7 @@ class CIRGenModule : public CIRGenTypeCache {
mlir::NamedAttrList &retAttrs);
/// A helper for constructAttributeList that handles argument attributes.
void constructFunctionArgumentAttributes(
- const CIRGenFunctionInfo &info, const clang::Decl *targetDecl,
+ const CIRGenFunctionInfo &info, clang::GlobalDecl calleeDecl,
bool isThunk, bool attrOnCallSite,
llvm::MutableArrayRef<mlir::NamedAttrList> argAttrs);
/// A helper function for constructAttributeList that determines whether a
diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-non-byval.cpp b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-non-byval.cpp
index 9e9ddfc405734..8acdb9833aa5e 100644
--- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-non-byval.cpp
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-non-byval.cpp
@@ -43,10 +43,8 @@ void callNonByval() {
// LLVM-LABEL: define dso_local void @_Z12callNonByvalv()
// LLVM: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %[[TMP:[^,]+]], ptr align 4 %[[T:[^,]+]], i64 4, i1 false)
// LLVM: call void @_Z12takeNonByval8WithDtor(ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP]])
-// LLVM-CIR: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[TMP]])
-// LLVM-CIR: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[T]])
-// OGCG: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP]])
-// OGCG: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[T]])
+// LLVM: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP]])
+// LLVM: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[T]])
// Each non-byval argument forwards its own temporary.
void callTwoNonByval() {
@@ -66,10 +64,8 @@ void callTwoNonByval() {
// LLVM: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %[[TMP_A:[^,]+]], ptr align 4 %{{[^,]+}}, i64 4, i1 false)
// LLVM: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %[[TMP_B:[^,]+]], ptr align 4 %{{[^,]+}}, i64 4, i1 false)
// LLVM: call void @_Z15takeTwoNonByval8WithDtorS_(ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP_A]], ptr nofreeobj noundef align 4 dereferenceable(4) %[[TMP_B]])
-// LLVM-CIR: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[TMP_B]])
-// LLVM-CIR: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[TMP_A]])
-// OGCG: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP_B]])
-// OGCG: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP_A]])
+// LLVM: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP_B]])
+// LLVM: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[TMP_A]])
// A non-trivial copy constructor also classifies non-byval: the constructor
// call populates the forwarded temporary directly, with no load in between.
@@ -151,8 +147,7 @@ void callInheritedCtor(WithDtor t) { Derived d(t); }
// LLVM: %[[INHTMP:.+]] = alloca %struct.WithDtor, align 4
// LLVM: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %[[INHTMP]], ptr align 4 %[[INHARG]], i64 4, i1 false)
// LLVM: call void @_ZN7DerivedCI14BaseE8WithDtor(ptr noundef nonnull align 1 dereferenceable(1) %{{.+}}, ptr nofreeobj noundef align 4 dereferenceable(4) %[[INHTMP]])
-// LLVM-CIR: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dereferenceable(4) %[[INHTMP]])
-// OGCG: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[INHTMP]])
+// LLVM: call void @_ZN8WithDtorD1Ev(ptr noundef nonnull align 4 dead_on_return(4) dereferenceable(4) %[[INHTMP]])
// Both inheriting constructor variants hand their own parameter on unchanged.
// CIR-LABEL: cir.func {{.*}}@_ZN7DerivedCI14BaseE8WithDtor
diff --git a/clang/test/CIR/CodeGen/destructor-dead-on-return.cpp b/clang/test/CIR/CodeGen/destructor-dead-on-return.cpp
new file mode 100644
index 0000000000000..35bd11e20f870
--- /dev/null
+++ b/clang/test/CIR/CodeGen/destructor-dead-on-return.cpp
@@ -0,0 +1,121 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -fno-lifetime-dse -emit-cir %s -o %t-nodse.cir
+// RUN: FileCheck --check-prefix=NODSE --input-file=%t-nodse.cir %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -fno-lifetime-dse -emit-llvm %s -o %t-nodse-cir.ll
+// RUN: FileCheck --check-prefix=NODSE --input-file=%t-nodse-cir.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fno-lifetime-dse -emit-llvm %s -o %t-nodse.ll
+// RUN: FileCheck --check-prefix=NODSE --input-file=%t-nodse.ll %s
+
+// A destructor's `this` is dead_on_return for the bytes of the object's data,
+// unless lifetime DSE is disabled.
+
+// NODSE-NOT: dead_on_return
+
+void opaque();
+void opaque(char *);
+
+class Foo {
+public:
+ ~Foo();
+
+private:
+ char fooVar[4];
+};
+
+Foo::~Foo() { opaque(fooVar); }
+
+// CIR: cir.func {{.*}} @_ZN3FooD2Ev(%arg0: !cir.ptr<!rec_Foo> {llvm.align = 1 : i64, llvm.dead_on_return = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nonnull, llvm.noundef}
+// CIR: cir.func {{.*}} @_ZN3FooD1Ev(%arg0: !cir.ptr<!rec_Foo> {llvm.align = 1 : i64, llvm.dead_on_return = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nonnull, llvm.noundef}
+// CIR: cir.call @_ZN3FooD2Ev(%{{.*}}) nothrow : (!cir.ptr<!rec_Foo> {llvm.align = 1 : i64, llvm.dead_on_return = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nonnull, llvm.noundef}) -> ()
+
+// LLVM: define dso_local void @_ZN3FooD2Ev(ptr noundef nonnull align 1 dead_on_return(4) dereferenceable(4) %{{.*}})
+// LLVM: define dso_local void @_ZN3FooD1Ev(ptr noundef nonnull align 1 dead_on_return(4) dereferenceable(4) %{{.*}})
+// LLVM: call void @_ZN3FooD2Ev(ptr noundef nonnull align 1 dead_on_return(4) dereferenceable(4) %{{.*}})
+
+// The data of a derived class includes its bases.
+class BarInheritsFoo : public Foo {
+public:
+ ~BarInheritsFoo();
+
+private:
+ char barVar[12];
+};
+
+BarInheritsFoo::~BarInheritsFoo() { opaque(barVar); }
+
+// CIR: cir.func {{.*}} @_ZN14BarInheritsFooD2Ev(%arg0: !cir.ptr<!rec_BarInheritsFoo> {llvm.align = 1 : i64, llvm.dead_on_return = 16 : i64, llvm.dereferenceable = 16 : i64, llvm.nonnull, llvm.noundef}
+// CIR: cir.call @_ZN3FooD2Ev(%{{.*}}) nothrow : (!cir.ptr<!rec_Foo> {llvm.align = 1 : i64, llvm.dead_on_return = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nonnull, llvm.noundef}) -> ()
+
+// LLVM: define dso_local void @_ZN14BarInheritsFooD2Ev(ptr noundef nonnull align 1 dead_on_return(16) dereferenceable(16) %{{.*}})
+// LLVM: call void @_ZN3FooD2Ev(ptr noundef nonnull align 1 dead_on_return(4) dereferenceable(4) %{{.*}})
+
+// A virtual base outlives the destructor of the object holding it.
+class BarVirtualInheritsFoo : public virtual Foo {
+public:
+ ~BarVirtualInheritsFoo();
+
+private:
+ char barVar[16];
+};
+
+BarVirtualInheritsFoo::~BarVirtualInheritsFoo() { opaque(barVar); }
+
+// CIR: cir.func {{.*}} @_ZN21BarVirtualInheritsFooD2Ev(%arg0: !cir.ptr<!rec_BarVirtualInheritsFoo> {llvm.align = 8 : i64, llvm.dereferenceable = 24 : i64, llvm.nonnull, llvm.noundef}{{.*}}, %arg1: !cir.ptr<!cir.ptr<!void>> {llvm.noundef}
+// CIR: cir.func {{.*}} @_ZN21BarVirtualInheritsFooD1Ev(%arg0: !cir.ptr<!rec_BarVirtualInheritsFoo> {llvm.align = 8 : i64, llvm.dereferenceable = 24 : i64, llvm.nonnull, llvm.noundef}
+
+// LLVM: define dso_local void @_ZN21BarVirtualInheritsFooD2Ev(ptr noundef nonnull align 8 dereferenceable(24) %{{.*}}, ptr noundef %{{.*}})
+// LLVM: define dso_local void @_ZN21BarVirtualInheritsFooD1Ev(ptr noundef nonnull align 8 dereferenceable(24) %{{.*}})
+
+// An object with no data gets no attribute.
+class Empty {
+public:
+ ~Empty();
+};
+
+Empty::~Empty() { opaque(); }
+
+// CIR: cir.func {{.*}} @_ZN5EmptyD2Ev(%arg0: !cir.ptr<!rec_Empty> {llvm.align = 1 : i64, llvm.dereferenceable = 1 : i64, llvm.nonnull, llvm.noundef}
+
+// LLVM: define dso_local void @_ZN5EmptyD2Ev(ptr noundef nonnull align 1 dereferenceable(1) %{{.*}})
+
+// The deleting destructor's operator delete may still use the storage.
+class Poly {
+public:
+ virtual ~Poly();
+
+private:
+ char polyVar[4];
+};
+
+Poly::~Poly() { opaque(polyVar); }
+
+// CIR: cir.func {{.*}} @_ZN4PolyD2Ev(%arg0: !cir.ptr<!rec_Poly> {llvm.align = 8 : i64, llvm.dead_on_return = 12 : i64, llvm.dereferenceable = 12 : i64, llvm.nonnull, llvm.noundef}
+// CIR: cir.func {{.*}} @_ZN4PolyD0Ev(%arg0: !cir.ptr<!rec_Poly> {llvm.align = 8 : i64, llvm.dereferenceable = 12 : i64, llvm.nonnull, llvm.noundef}
+// CIR: cir.call @_ZN4PolyD1Ev(%{{.*}}) nothrow : (!cir.ptr<!rec_Poly> {llvm.align = 8 : i64, llvm.dead_on_return = 12 : i64, llvm.dereferenceable = 12 : i64, llvm.nonnull, llvm.noundef}) -> ()
+
+// LLVM: define dso_local void @_ZN4PolyD2Ev(ptr noundef nonnull align 8 dead_on_return(12) dereferenceable(12) %{{.*}})
+// LLVM: define dso_local void @_ZN4PolyD0Ev(ptr noundef nonnull align 8 dereferenceable(12) %{{.*}})
+// LLVM: call void @_ZN4PolyD1Ev(ptr noundef nonnull align 8 dead_on_return(12) dereferenceable(12) %{{.*}})
+
+void callDtor() {
+ Foo f;
+}
+
+// CIR: cir.func {{.*}} @_Z8callDtorv()
+// CIR: cir.call @_ZN3FooD1Ev(%{{.*}}) nothrow : (!cir.ptr<!rec_Foo> {llvm.align = 1 : i64, llvm.dead_on_return = 4 : i64, llvm.dereferenceable = 4 : i64, llvm.nonnull, llvm.noundef}) -> ()
+
+// LLVM: define dso_local void @_Z8callDtorv()
+// LLVM: call void @_ZN3FooD1Ev(ptr noundef nonnull align 1 dead_on_return(4) dereferenceable(4) %{{.*}})
+
+void deletePoly(Poly *p) { delete p; }
+
+// CIR: cir.func {{.*}} @_Z10deletePolyP4Poly
+// CIR: cir.call %{{.*}}(%{{.*}}) nothrow : (!cir.ptr<!cir.func<(!cir.ptr<!rec_Poly>)>>, !cir.ptr<!rec_Poly> {llvm.align = 8 : i64, llvm.dereferenceable = 12 : i64, llvm.nonnull, llvm.noundef}) -> ()
+
+// LLVM: define dso_local void @_Z10deletePolyP4Poly
+// LLVM: call void %{{.*}}(ptr noundef nonnull align 8 dereferenceable(12) %{{.*}})
diff --git a/clang/test/CIR/CodeGen/paren-init-list.cpp b/clang/test/CIR/CodeGen/paren-init-list.cpp
index 48ca454cf1f91..6689d80e6a144 100644
--- a/clang/test/CIR/CodeGen/paren-init-list.cpp
+++ b/clang/test/CIR/CodeGen/paren-init-list.cpp
@@ -51,7 +51,7 @@ void test_init_list_with_dtor() {
// CIR: cir.cleanup.scope {
// CIR: cir.yield
// CIR: } cleanup normal {
-// CIR: cir.call @_ZN5OuterD1Ev(%[[O]]) nothrow : (!cir.ptr<!rec_Outer> {llvm.align = 4 : i64, llvm.dereferenceable = 8 : i64, llvm.nonnull, llvm.noundef}) -> ()
+// CIR: cir.call @_ZN5OuterD1Ev(%[[O]]) nothrow : (!cir.ptr<!rec_Outer> {llvm.align = 4 : i64, llvm.dead_on_return = 8 : i64, llvm.dereferenceable = 8 : i64, llvm.nonnull, llvm.noundef}) -> ()
// CIR: cir.yield
// CIR: }
// CIR: cir.return
``````````
</details>
https://github.com/llvm/llvm-project/pull/235125
More information about the cfe-commits
mailing list