[clang] [CIR] Classify empty records as Ignore in x86_64 callconv (PR #211078)

via cfe-commits cfe-commits at lists.llvm.org
Tue Jul 21 14:53:48 PDT 2026


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

>From 38e9bbb40755bb1653922a9bb7d3a018154bfa64 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Tue, 21 Jul 2026 11:07:20 -0700
Subject: [PATCH 1/3] [CIR] Classify empty records as Ignore in x86_64 callconv

The aggregate bridge rejected a zero-field record as NYI, even though the
SysV classifier already treats an empty record as NoClass/NoClass and the
rewriter already drops arguments and returns classified Ignore.  Dropping the
zero-field reject in isSupportedType lets a C empty struct classify as
Ignore, so it is dropped from the lowered signature.

The C++ empty class stays NYI: CIRGen lays it out as a single padded byte,
which the padded reject still catches.  Unions, packed, and all-float
aggregates remain NYI as before.
---
 .../Transforms/CallConvLoweringPass.cpp       | 12 ++---
 .../abi-lowering/x86_64-aggregate-nyi.cir     |  9 ----
 .../abi-lowering/x86_64-empty-record.cir      | 50 +++++++++++++++++++
 3 files changed, 56 insertions(+), 15 deletions(-)
 create mode 100644 clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir

diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index e1414d3b311e2..b8c98e448cf1a 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -115,13 +115,13 @@ static bool isSupportedType(mlir::Type ty) {
     // Unions and packed / padded records each need classification this bridge
     // does not implement (a union widen fixup and pad-aware eightbyte
     // classification), so reject them here and report NYI rather than
-    // misclassify.  Empty-for-ABI records classify as Ignore, which is also
-    // deferred: a C empty struct is a zero-field record, and CIRGen lays out
-    // an empty C++ class as a single padded byte (caught by the padded check).
+    // misclassify.  A zero-field record (a C empty struct) classifies as
+    // Ignore and is dropped from the lowered signature.  CIRGen lays out an
+    // empty C++ class as a single padded byte, which the padded check rejects.
     // A real one-byte struct such as `{char[1]}` has a field and is not
     // padded, so it is classified normally.
     if (recTy.isUnion() || !recTy.isComplete() || recTy.getPacked() ||
-        recTy.getPadded() || recTy.getMembers().empty())
+        recTy.getPadded())
       return false;
     return llvm::all_of(recTy.getMembers(),
                         [](mlir::Type m) { return isSupportedType(m); });
@@ -257,8 +257,8 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
 /// Indirect: an aggregate that does not fit in registers is passed via a
 /// pointer (sret for returns, byval for arguments).
 ///
-/// Ignore: a void return has no register or stack slot.  (Empty-for-ABI
-/// records are rejected by isSupportedType, so they never reach here.)
+/// Ignore: a void return has no register or stack slot, and a zero-field
+/// (empty) record is dropped from the signature.
 static std::optional<ArgClassification>
 convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
                   mlir::Type origTy) {
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir
index 87692e1a3e5a2..85f05fef9f96b 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-aggregate-nyi.cir
@@ -8,7 +8,6 @@
 !rec_P = !cir.struct<"P" packed {!s8i, !s32i}>
 !rec_Ov = !cir.struct<"Ov" padded {!s32i, !cir.array<!u8i x 12>}>
 !rec_E = !cir.struct<"E" padded {!u8i}>
-!rec_E0 = !cir.struct<"E0" {}>
 !rec_FF = !cir.struct<"FF" {!cir.float, !cir.float}>
 !rec_RetFF = !cir.struct<"RetFF" {!cir.float, !cir.float}>
 
@@ -50,14 +49,6 @@ module attributes {
 
   // CHECK: not yet implemented for type '!cir.struct<"E" padded
 
-  // A zero-field record (a C empty struct) also classifies as Ignore, which is
-  // deferred, so it is rejected rather than passed with the argument dropped.
-  cir.func @take_e0(%arg0: !rec_E0) {
-    cir.return
-  }
-
-  // CHECK: not yet implemented for type '!cir.struct<"E0"
-
   // An all-float struct classifies to an SSE vector coerce this bridge does
   // not represent, so it is reported NYI rather than passed unchanged.
   cir.func @take_ff(%arg0: !rec_FF) {
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir
new file mode 100644
index 0000000000000..04ba860f80b37
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir
@@ -0,0 +1,50 @@
+// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 | FileCheck %s
+
+!s32i = !cir.int<s, 32>
+!rec_E0 = !cir.struct<"E0" {}>
+
+module attributes {
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // A zero-field record (a C empty struct) classifies as Ignore: the argument
+  // is dropped from the lowered signature.
+  cir.func @take_empty(%arg0: !rec_E0) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_empty()
+  // CHECK-NEXT: cir.return
+
+  // The empty argument is dropped even when a real argument remains; the real
+  // argument shifts down to the first slot.
+  cir.func @take_mixed(%arg0: !rec_E0, %arg1: !s32i) -> !s32i {
+    cir.return %arg1 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_mixed(%arg0: !s32i) -> !s32i
+  // CHECK-NEXT: cir.return %arg0 : !s32i
+
+  // An empty-record return is dropped: the function returns void.
+  cir.func @ret_empty() -> !rec_E0 {
+    %0 = cir.alloca "r" align(1) : !cir.ptr<!rec_E0>
+    %1 = cir.load %0 : !cir.ptr<!rec_E0>, !rec_E0
+    cir.return %1 : !rec_E0
+  }
+
+  // CHECK: cir.func{{.*}} @ret_empty()
+  // CHECK-NOT: -> !rec_E0
+  // CHECK: cir.return
+
+  // The empty argument is also dropped at call sites: the caller's own empty
+  // argument is dropped, and the call to take_mixed drops it too.
+  cir.func @caller(%arg0: !rec_E0, %arg1: !s32i) -> !s32i {
+    %0 = cir.call @take_mixed(%arg0, %arg1) : (!rec_E0, !s32i) -> !s32i
+    cir.return %0 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @caller(%arg0: !s32i) -> !s32i
+  // CHECK: cir.call @take_mixed(%arg0) : (!s32i) -> !s32i
+}

>From 1212485e76642ddb5bf86a1016d4cb7b4c51c877 Mon Sep 17 00:00:00 2001
From: adams381 <adams at nvidia.com>
Date: Tue, 21 Jul 2026 15:51:35 -0500
Subject: [PATCH 2/3] Update
 clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir

Thanks!

Co-authored-by: Andy Kaylor <akaylor at nvidia.com>
---
 clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir
index 04ba860f80b37..1767fd1bc9bd6 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir
@@ -35,7 +35,7 @@ module attributes {
   }
 
   // CHECK: cir.func{{.*}} @ret_empty()
-  // CHECK-NOT: -> !rec_E0
+  // CHECK-NOT: !rec_E0
   // CHECK: cir.return
 
   // The empty argument is also dropped at call sites: the caller's own empty

>From 856fc6e0e0d5539537fb0c4cafb3a6398babc085 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Tue, 21 Jul 2026 14:52:07 -0700
Subject: [PATCH 3/3] [CIR] Test empty-record alloca survives callconv lowering

The x86_64 callconv lowering pass rewrites signatures and call operands
and leaves the empty-record alloca in place, so the storage slot
survives while the empty argument or return is dropped.  Check that the
slot survives in the return case, and add a local case that passes an
empty struct from an alloca through a call.
---
 .../abi-lowering/x86_64-empty-record.cir      | 20 ++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir
index 1767fd1bc9bd6..23254734f27e5 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-record.cir
@@ -27,7 +27,8 @@ module attributes {
   // CHECK: cir.func{{.*}} @take_mixed(%arg0: !s32i) -> !s32i
   // CHECK-NEXT: cir.return %arg0 : !s32i
 
-  // An empty-record return is dropped: the function returns void.
+  // An empty-record return is dropped: the function returns void.  The
+  // local storage slot survives the rewrite; only the return is dropped.
   cir.func @ret_empty() -> !rec_E0 {
     %0 = cir.alloca "r" align(1) : !cir.ptr<!rec_E0>
     %1 = cir.load %0 : !cir.ptr<!rec_E0>, !rec_E0
@@ -35,8 +36,8 @@ module attributes {
   }
 
   // CHECK: cir.func{{.*}} @ret_empty()
-  // CHECK-NOT: !rec_E0
-  // CHECK: cir.return
+  // CHECK: cir.alloca {{.*}} !cir.ptr<!rec_E0>
+  // CHECK: cir.return{{$}}
 
   // The empty argument is also dropped at call sites: the caller's own empty
   // argument is dropped, and the call to take_mixed drops it too.
@@ -47,4 +48,17 @@ module attributes {
 
   // CHECK: cir.func{{.*}} @caller(%arg0: !s32i) -> !s32i
   // CHECK: cir.call @take_mixed(%arg0) : (!s32i) -> !s32i
+
+  // A local empty struct lives in an alloca; the alloca survives the rewrite
+  // while the empty operand is dropped from the call.
+  cir.func @caller_local(%arg0: !s32i) -> !s32i {
+    %0 = cir.alloca "e" align(1) : !cir.ptr<!rec_E0>
+    %1 = cir.load %0 : !cir.ptr<!rec_E0>, !rec_E0
+    %2 = cir.call @take_mixed(%1, %arg0) : (!rec_E0, !s32i) -> !s32i
+    cir.return %2 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @caller_local(%arg0: !s32i) -> !s32i
+  // CHECK: cir.alloca "e" align(1) : !cir.ptr<!rec_E0>
+  // CHECK: cir.call @take_mixed(%arg0) : (!s32i) -> !s32i
 }



More information about the cfe-commits mailing list