[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