[clang] [CIR] Drop res_attrs when a rewritten return has no result (PR #222465)

Adam Smith via cfe-commits cfe-commits at lists.llvm.org
Fri Sep 11 12:58:34 PDT 2026


https://github.com/adams381 updated https://github.com/llvm/llvm-project/pull/222465

>From 20744da19e9fd484d5c8cc267b384bf228948637 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Wed, 9 Sep 2026 15:01:21 -0700
Subject: [PATCH 1/3] [CIR] Drop res_attrs when a rewritten return has no
 result

An Indirect return moves the value to an sret pointer argument the pass
inserts, and an Ignore return drops it, so the rewritten func or call
has no result for the per-result `res_attrs` array to describe.  The
function rewrite and both call rewrites now remove it.

Assisted-by: Cursor / claude-opus-5
---
 .../TargetLowering/CIRABIRewriteContext.cpp   | 19 +++-
 .../abi-lowering/dropped-return-res-attrs.cir | 96 +++++++++++++++++++
 2 files changed, 112 insertions(+), 3 deletions(-)
 create mode 100644 clang/test/CIR/Transforms/abi-lowering/dropped-return-res-attrs.cir

diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index b850c4c0bce03..f7dfe0bb11996 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -1022,6 +1022,8 @@ void rewriteIndirectReturnCall(cir::CallOp call,
   for (mlir::NamedAttribute attr : call->getAttrs())
     if (!newCall->hasAttr(attr.getName()))
       newCall->setAttr(attr.getName(), attr.getValue());
+  // res_attrs arrived in the copy above, from the call that had a result.
+  newCall->removeAttr("res_attrs");
 
   // Shape the per-argument attrs exactly as the non-sret path does
   // (signext / zeroext for Extend, drop Ignore slots, byval / align for
@@ -1224,9 +1226,16 @@ mlir::LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
     }
   }
 
-  // Rebuild res_attrs: layer llvm.signext / llvm.zeroext onto an Extend
-  // return.
-  if (fc.returnInfo.kind == ArgKind::Extend) {
+  // Whatever emptied the result list, an sret pointer taking the value or an
+  // Ignore return dropping it, res_attrs no longer describes anything.  Keyed
+  // on the result count rather than the kind so a future kind that voids the
+  // return cannot slip past.
+  bool returnDropped =
+      !oldResultTypes.empty() && mlir::isa<cir::VoidType>(newRetTy);
+  if (returnDropped) {
+    funcOp->removeAttr("res_attrs");
+  } else if (fc.returnInfo.kind == ArgKind::Extend) {
+    // Layer llvm.signext / llvm.zeroext onto an Extend return.
     auto existing = funcOp->getAttrOfType<mlir::ArrayAttr>("res_attrs");
     funcOp->setAttr("res_attrs", updateResAttrs(ctx, existing, fc.returnInfo));
   }
@@ -1424,6 +1433,10 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
   if (fc.returnInfo.kind == ArgKind::Extend) {
     auto existing = call->getAttrOfType<mlir::ArrayAttr>("res_attrs");
     newCall->setAttr("res_attrs", updateResAttrs(ctx, existing, fc.returnInfo));
+  } else if (hasResult && mlir::isa<cir::VoidType>(callRetTy)) {
+    // The copy above brought res_attrs over from a call that had a result,
+    // and this one no longer does.
+    newCall->removeAttr("res_attrs");
   }
 
   if (hasResult && fc.returnInfo.kind == ArgKind::Ignore) {
diff --git a/clang/test/CIR/Transforms/abi-lowering/dropped-return-res-attrs.cir b/clang/test/CIR/Transforms/abi-lowering/dropped-return-res-attrs.cir
new file mode 100644
index 0000000000000..3c9438db7d8b3
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/dropped-return-res-attrs.cir
@@ -0,0 +1,96 @@
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN:   | FileCheck %s
+// RUN: cir-opt %s -cir-call-conv-lowering="classification-attr=test_classify" \
+// RUN:   --mlir-print-op-generic | FileCheck %s --check-prefix=GENERIC \
+// RUN:   --implicit-check-not=res_attrs
+
+!s32i = !cir.int<s, 32>
+!s64i = !cir.int<s, 64>
+!rec_Big = !cir.struct<"Big" {data !s64i, data !s64i, data !s64i, data !s64i}>
+
+#indirect_return = {
+  return = { kind = "indirect", indirect_align = 8 },
+  args   = [ ]
+}
+
+#ignore_return = {
+  return = { kind = "ignore" },
+  args   = [ ]
+}
+
+#caller_cls = {
+  return = { kind = "direct" },
+  args   = [ ]
+}
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // The llvm.noundef on this return has nowhere to go once the result count
+  // drops to zero, and a stale res_attrs fails the func verifier.
+  cir.func @returns_big_res_attr() -> (!rec_Big {llvm.noundef})
+      attributes { test_classify = #indirect_return } {
+    %0 = cir.alloca "__retval" align(8) : !cir.ptr<!rec_Big>
+    %z = cir.const #cir.zero : !rec_Big
+    cir.store %z, %0 : !rec_Big, !cir.ptr<!rec_Big>
+    %1 = cir.load %0 : !cir.ptr<!rec_Big>, !rec_Big
+    cir.return %1 : !rec_Big
+  }
+
+  // CHECK:      cir.func{{.*}} @returns_big_res_attr(%[[SRET:.*]]: !cir.ptr<!rec_Big> {
+  // CHECK-SAME:     llvm.sret = !rec_Big
+  // CHECK-NOT:    -> !rec_Big
+  // CHECK:        cir.store %{{.*}}, %[[SRET]] : !rec_Big, !cir.ptr<!rec_Big>
+  // CHECK:        cir.return
+  // GENERIC:    sym_name = "returns_big_res_attr"
+
+  cir.func private @returns_big() -> !rec_Big
+      attributes { test_classify = #indirect_return }
+
+  // cir.call prints res_attrs through its result list, which is empty once
+  // the call returns void, so only the generic form catches a stale one.
+  cir.func @caller_res_attr() -> !s32i
+      attributes { test_classify = #caller_cls } {
+    %a = cir.alloca "a" align(8) : !cir.ptr<!rec_Big>
+    %b = cir.alloca "b" align(8) : !cir.ptr<!rec_Big>
+    %0 = cir.call @returns_big() : () -> (!rec_Big {llvm.noundef})
+    cir.store %0, %a : !rec_Big, !cir.ptr<!rec_Big>
+    cir.store %0, %b : !rec_Big, !cir.ptr<!rec_Big>
+    %z = cir.const #cir.int<0> : !s32i
+    cir.return %z : !s32i
+  }
+
+  // CHECK:      cir.call @returns_big(%{{.*}}) : (!cir.ptr<!rec_Big>
+  // CHECK-SAME:     llvm.sret = !rec_Big
+  // CHECK-SAME:     -> ()
+  // GENERIC:    callee = @returns_big
+
+  // An Ignore return empties the result list without an sret pointer, so it
+  // reaches the same state by a different route.
+  cir.func @ignored_res_attr() -> (!s32i {llvm.noundef})
+      attributes { test_classify = #ignore_return } {
+    %0 = cir.const #cir.int<0> : !s32i
+    cir.return %0 : !s32i
+  }
+
+  // CHECK:      cir.func{{.*}} @ignored_res_attr()
+  // CHECK-NOT:    -> !s32i
+  // CHECK:        cir.return
+  // GENERIC:    sym_name = "ignored_res_attr"
+
+  cir.func private @ignored_ret() -> !s32i
+      attributes { test_classify = #ignore_return }
+
+  cir.func @caller_ignored_res_attr() -> !s32i
+      attributes { test_classify = #caller_cls } {
+    %0 = cir.call @ignored_ret() : () -> (!s32i {llvm.noundef})
+    %z = cir.const #cir.int<0> : !s32i
+    cir.return %z : !s32i
+  }
+
+  // CHECK:      cir.call @ignored_ret() : () -> ()
+  // GENERIC:    callee = @ignored_ret
+}

>From e6942b484ec55398066fdc48068fa9392e591929 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Wed, 9 Sep 2026 17:41:49 -0700
Subject: [PATCH 2/3] [CIR] Add a C test for the sret res_attrs drop

Assisted-by: Cursor / claude-opus-5
---
 ...call-conv-lowering-x86_64-sret-res-attrs.c | 41 +++++++++++++++++++
 1 file changed, 41 insertions(+)
 create mode 100644 clang/test/CIR/CodeGen/call-conv-lowering-x86_64-sret-res-attrs.c

diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-sret-res-attrs.c b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-sret-res-attrs.c
new file mode 100644
index 0000000000000..b8dd060606bff
--- /dev/null
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-sret-res-attrs.c
@@ -0,0 +1,41 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir \
+// RUN:   -fno-clangir-call-conv-lowering -menable-no-infs -menable-no-nans \
+// RUN:   -emit-cir %s -o %t-before.cir
+// RUN: FileCheck --check-prefix=BEFORE --input-file=%t-before.cir %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -menable-no-infs \
+// RUN:   -menable-no-nans -emit-cir %s -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s \
+// RUN:   --implicit-check-not=llvm.nofpclass
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -menable-no-infs \
+// RUN:   -menable-no-nans -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 -menable-no-infs \
+// RUN:   -menable-no-nans -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t.ll %s
+
+// A return with a floating representation gets llvm.nofpclass under
+// -menable-no-infs or -menable-no-nans, and a _Complex __float128 is returned
+// indirectly, so the attribute describes a result the sret rewrite removes.
+// The BEFORE lines pin that the attribute is there to remove, so the CIR
+// checks cannot pass by it never having been attached.
+_Complex __float128 ret_cf128(void) { return 1.0Q; }
+
+// BEFORE: cir.func{{.*}} @ret_cf128() -> (!cir.complex<!cir.f128> {llvm.nofpclass = 519 : i64})
+
+// CIR:      cir.func{{.*}} @ret_cf128(%arg0: !cir.ptr<!cir.complex<!cir.f128>>
+// CIR-SAME:   llvm.sret = !cir.complex<!cir.f128>
+
+// LLVM: define dso_local void @ret_cf128(ptr dead_on_unwind noalias writable sret({ fp128, fp128 }) align 16 %{{.+}})
+
+_Complex __float128 ret_cf128_decl(void);
+void call_cf128(void) { _Complex __float128 x = ret_cf128_decl(); (void)x; }
+
+// BEFORE: cir.call @ret_cf128_decl() : () -> (!cir.complex<!cir.f128> {llvm.nofpclass = 519 : i64})
+// BEFORE: cir.func private @ret_cf128_decl() -> (!cir.complex<!cir.f128> {llvm.nofpclass = 519 : i64})
+
+// CIR:      cir.call @ret_cf128_decl(%{{.+}}) : (!cir.ptr<!cir.complex<!cir.f128>>
+// CIR-SAME:   llvm.sret = !cir.complex<!cir.f128>
+// CIR-SAME:   -> ()
+
+// LLVM: call void @ret_cf128_decl(ptr dead_on_unwind writable sret({ fp128, fp128 }) align 16 %{{.+}})
+// LLVM: declare void @ret_cf128_decl(ptr dead_on_unwind writable sret({ fp128, fp128 }) align 16)

>From c7a7a3b980144b6b9ee036d2749d4ef864e162a0 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 10 Sep 2026 10:41:35 -0700
Subject: [PATCH 3/3] [CIR] Inline the res_attrs void-return condition

A cir.func with no results can only have res_attrs absent or empty, so
the emptiness guard this drops was protecting an invariant the result
attribute count check already enforces.

Assisted-by: Cursor / claude-opus-5
---
 .../TargetLowering/CIRABIRewriteContext.cpp           | 11 +----------
 1 file changed, 1 insertion(+), 10 deletions(-)

diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
index f7dfe0bb11996..997f9b0d59f27 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRABIRewriteContext.cpp
@@ -1022,7 +1022,6 @@ void rewriteIndirectReturnCall(cir::CallOp call,
   for (mlir::NamedAttribute attr : call->getAttrs())
     if (!newCall->hasAttr(attr.getName()))
       newCall->setAttr(attr.getName(), attr.getValue());
-  // res_attrs arrived in the copy above, from the call that had a result.
   newCall->removeAttr("res_attrs");
 
   // Shape the per-argument attrs exactly as the non-sret path does
@@ -1226,13 +1225,7 @@ mlir::LogicalResult CIRABIRewriteContext::rewriteFunctionDefinition(
     }
   }
 
-  // Whatever emptied the result list, an sret pointer taking the value or an
-  // Ignore return dropping it, res_attrs no longer describes anything.  Keyed
-  // on the result count rather than the kind so a future kind that voids the
-  // return cannot slip past.
-  bool returnDropped =
-      !oldResultTypes.empty() && mlir::isa<cir::VoidType>(newRetTy);
-  if (returnDropped) {
+  if (mlir::isa<cir::VoidType>(newRetTy)) {
     funcOp->removeAttr("res_attrs");
   } else if (fc.returnInfo.kind == ArgKind::Extend) {
     // Layer llvm.signext / llvm.zeroext onto an Extend return.
@@ -1434,8 +1427,6 @@ CIRABIRewriteContext::rewriteCallSite(mlir::Operation *callOp,
     auto existing = call->getAttrOfType<mlir::ArrayAttr>("res_attrs");
     newCall->setAttr("res_attrs", updateResAttrs(ctx, existing, fc.returnInfo));
   } else if (hasResult && mlir::isa<cir::VoidType>(callRetTy)) {
-    // The copy above brought res_attrs over from a call that had a result,
-    // and this one no longer does.
     newCall->removeAttr("res_attrs");
   }
 



More information about the cfe-commits mailing list