https://github.com/adams381 created 
https://github.com/llvm/llvm-project/pull/214742

CIR lays a C++ empty class out as a record that is nothing but padding, and 
isSupportedType rejected every padded record, so any signature naming one 
failed the pass with an NYI. Ordinary C++ hits this constantly through tag 
dispatch, allocators, and empty bases.

Implement empty C++ class support. Record emptiness as an is_empty flag on 
cir.record_layout, filled by CIRGen from the AST, and have the pass read that 
flag rather than inspect the type. The type cannot answer on its own, because 
`struct { int : 3; }` and `struct { unsigned char c; }` lower to identical CIR, 
yet classic CodeGen drops the first from the signature and passes the second in 
a register. An empty record then maps with no fields, so the classifier drops 
it on its own.

Assisted-by: Cursor / claude-opus-5


>From a69432e9f418c8b3c96f21e0661988be5744775e Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Fri, 7 Aug 2026 07:11:50 -0700
Subject: [PATCH] [CIR] Handle an empty C++ class in x86_64 callconv lowering

CIR lays a C++ empty class out as a record that is nothing but padding, and
isSupportedType rejected every padded record, so any signature naming one
failed the pass with an NYI. Ordinary C++ hits this constantly through tag
dispatch, allocators, and empty bases.

Implement empty C++ class support. Record emptiness as an is_empty flag on
cir.record_layout, filled by CIRGen from the AST, and have the pass read that
flag rather than inspect the type. The type cannot answer on its own, because
`struct { int : 3; }` and `struct { unsigned char c; }` lower to identical
CIR, yet classic CodeGen drops the first from the signature and passes the
second in a register. An empty record then maps with no fields, so the
classifier drops it on its own.

Assisted-by: Cursor / claude-opus-5
---
 .../include/clang/CIR/Dialect/IR/CIRAttrs.td  |  16 +-
 .../CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp |   5 +-
 clang/lib/CIR/CodeGen/TargetInfo.cpp          |  56 ++++
 clang/lib/CIR/CodeGen/TargetInfo.h            |  14 +
 .../Transforms/CallConvLoweringPass.cpp       |  70 ++++-
 .../call-conv-lowering-x86_64-empty.cpp       | 149 +++++++++++
 .../test/CIR/CodeGen/record-type-metadata.cpp |  62 ++++-
 clang/test/CIR/IR/invalid-record-layout.cir   |   4 +-
 .../abi-lowering/x86_64-aggregate-nyi.cir     |  84 +++++-
 .../abi-lowering/x86_64-empty-class.cir       | 248 ++++++++++++++++++
 .../abi-lowering/x86_64-struct-indirect.cir   |   2 +-
 .../Transforms/abi-lowering/x86_64-union.cir  |   9 +-
 .../unittests/CIR/RecordTypeMetadataTest.cpp  |  24 +-
 13 files changed, 706 insertions(+), 37 deletions(-)
 create mode 100644 clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
 create mode 100644 
clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir

diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td 
b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index 71585cd83fb66..a10db4fa2419f 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
@@ -143,6 +143,10 @@ def CIR_RecordLayoutAttr : CIR_Attr<"RecordLayout", 
"record_layout", [
     - `record_align_in_bytes`: from `ASTRecordLayout::getAlignment()`.
       Needed because CIR's DataLayout cannot account for
       `__attribute__((aligned(N)))`.
+    - `is_empty`: whether the record carries no data for argument passing
+      (mirrors `isEmptyRecordForABI`).  The record type cannot answer this on
+      its own, because an empty class and a one-byte record of data can lower
+      to the same members.
 
     Example:
     ```
@@ -151,11 +155,13 @@ def CIR_RecordLayoutAttr : CIR_Attr<"RecordLayout", 
"record_layout", [
         "Trivial" = #cir.record_layout<
           arg_passing_kind = can_pass_in_regs,
           has_trivial_dtor = true,
-          record_align = 4>,
+          record_align = 4,
+          is_empty = false>,
         "NonTrivialDtor" = #cir.record_layout<
           arg_passing_kind = cannot_pass_in_regs,
           has_trivial_dtor = false,
-          record_align = 4>
+          record_align = 4,
+          is_empty = false>
       }
     }
     ```
@@ -164,14 +170,16 @@ def CIR_RecordLayoutAttr : CIR_Attr<"RecordLayout", 
"record_layout", [
   let parameters = (ins
     EnumParameter<CIR_ArgPassingKind>:$arg_passing_kind,
     "bool":$has_trivial_dtor,
-    "uint64_t":$record_align
+    "uint64_t":$record_align,
+    "bool":$is_empty
   );
 
   let assemblyFormat = [{
     `<`
       `arg_passing_kind` `=` $arg_passing_kind `,`
       `has_trivial_dtor` `=` $has_trivial_dtor `,`
-      `record_align` `=` $record_align
+      `record_align` `=` $record_align `,`
+      `is_empty` `=` $is_empty
     `>`
   }];
 
diff --git a/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp 
b/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
index e4476b88ec6f7..9d12b14db4d91 100644
--- a/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
@@ -13,6 +13,7 @@
 #include "CIRGenBuilder.h"
 #include "CIRGenModule.h"
 #include "CIRGenTypes.h"
+#include "TargetInfo.h"
 
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/Decl.h"
@@ -759,10 +760,12 @@ CIRGenTypes::computeRecordLayout(const RecordDecl *rd, 
cir::RecordType *ty) {
       hasTrivialDestructor = cxxRD->hasTrivialDestructor();
     const auto &astLayout = astContext.getASTRecordLayout(rd);
     uint64_t recordAlignInBytes = astLayout.getAlignment().getQuantity();
+    bool isEmpty =
+        isEmptyRecordForABI(astContext, astContext.getCanonicalTagType(rd));
 
     cgm.addRecordLayout(ty->getName(), cir::RecordLayoutAttr::get(
                                            mlirCtx, apk, hasTrivialDestructor,
-                                           recordAlignInBytes));
+                                           recordAlignInBytes, isEmpty));
   }
 
   auto rl = std::make_unique<CIRGenRecordLayout>(
diff --git a/clang/lib/CIR/CodeGen/TargetInfo.cpp 
b/clang/lib/CIR/CodeGen/TargetInfo.cpp
index ba7eeb29dd252..6847761ab5e5a 100644
--- a/clang/lib/CIR/CodeGen/TargetInfo.cpp
+++ b/clang/lib/CIR/CodeGen/TargetInfo.cpp
@@ -45,6 +45,62 @@ bool clang::CIRGen::isEmptyFieldForLayout(const ASTContext 
&context,
   return isEmptyRecordForLayout(context, fd->getType());
 }
 
+bool clang::CIRGen::isEmptyFieldForABI(const ASTContext &context,
+                                       const FieldDecl *fd) {
+  if (fd->isUnnamedBitField())
+    return true;
+
+  QualType ft = fd->getType();
+
+  // An array of empty records is empty, and a zero-length array always is.
+  bool wasArray = false;
+  while (const ConstantArrayType *at = context.getAsConstantArrayType(ft)) {
+    if (at->isZeroSize())
+      return true;
+    ft = at->getElementType();
+    wasArray = true;
+  }
+
+  const auto *rt = ft->getAsCanonical<RecordType>();
+  if (!rt)
+    return false;
+
+  // A C++ record field is never empty under the Itanium ABI unless
+  // [[no_unique_address]] makes it so, and that exception covers a record
+  // rather than an array of them.
+  if (isa<CXXRecordDecl>(rt->getDecl()) &&
+      (wasArray || !fd->hasAttr<NoUniqueAddressAttr>()))
+    return false;
+
+  return isEmptyRecordForABI(context, ft);
+}
+
+bool clang::CIRGen::isEmptyRecordForABI(const ASTContext &context, QualType t) 
{
+  const auto *rd = t->getAsRecordDecl();
+  if (!rd)
+    return false;
+  if (rd->hasFlexibleArrayMember())
+    return false;
+
+  if (const auto *cxxrd = dyn_cast<CXXRecordDecl>(rd)) {
+    // A vtable pointer is neither a base nor a field, so clang's predicate
+    // calls a polymorphic class empty and leans on its callers rejecting one 
as
+    // non-trivially-copyable beforehand.  This answer is recorded as metadata
+    // and read without that precondition, so rule it out here instead.
+    if (cxxrd->isDynamicClass())
+      return false;
+
+    for (const auto &i : cxxrd->bases())
+      if (!isEmptyRecordForABI(context, i.getType()))
+        return false;
+  }
+
+  for (const auto *i : rd->fields())
+    if (!isEmptyFieldForABI(context, i))
+      return false;
+  return true;
+}
+
 namespace {
 
 class AMDGPUABIInfo : public ABIInfo {
diff --git a/clang/lib/CIR/CodeGen/TargetInfo.h 
b/clang/lib/CIR/CodeGen/TargetInfo.h
index 308d472234f99..fca7df45a627f 100644
--- a/clang/lib/CIR/CodeGen/TargetInfo.h
+++ b/clang/lib/CIR/CodeGen/TargetInfo.h
@@ -37,6 +37,20 @@ bool isEmptyFieldForLayout(const ASTContext &context, const 
FieldDecl *fd);
 /// if the [[no_unique_address]] attribute would have made them empty.
 bool isEmptyRecordForLayout(const ASTContext &context, QualType t);
 
+/// isEmptyFieldForABI - Return true if the field is "empty" for argument
+/// passing.  An unnamed bit-field of any width qualifies, as does an empty
+/// record, though a C++ one only under [[no_unique_address]] and never through
+/// an array of them.  This differs from isEmptyFieldForLayout, which counts
+/// only a zero-width bit-field, because a narrower unnamed bit-field still
+/// occupies no ABI class even though it takes up layout space.
+bool isEmptyFieldForABI(const ASTContext &context, const FieldDecl *fd);
+
+/// isEmptyRecordForABI - Return true if a record contains only empty base
+/// classes and fields, and so contributes no data to argument passing.  What
+/// that means for a signature is the caller's to decide: an empty record small
+/// enough is dropped, but one past two eightbytes is still passed indirectly.
+bool isEmptyRecordForABI(const ASTContext &context, QualType t);
+
 class CIRGenFunction;
 
 class TargetCIRGenInfo {
diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp 
b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
index 193c2b6f4a9dc..bd53d4e9bcaa8 100644
--- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp
@@ -69,8 +69,9 @@ namespace {
 // dialect-agnostic mlir::abi::FunctionClassification that CIRABIRewriteContext
 // consumes.  Integer (including `_BitInt` up to 128 bits) / pointer / bool /
 // f32 / f64 scalars and struct / union / array aggregates are handled.
-// `_Complex`, vectors, wider floats, packed or padded records, and a union no
-// member of which spans its declared size are reported NYI by
+// `_Complex`, vectors, wider floats, packed records, padded records the layout
+// metadata does not call empty, a union no member of which spans its declared
+// size, and a union with an empty member are reported NYI by
 // classifyX86_64Function so an unsupported signature fails the pass instead of
 // being misclassified.
 
//===----------------------------------------------------------------------===//
@@ -86,6 +87,27 @@ static bool recordCanPassInRegs(ModuleOp modOp, 
cir::RecordType recTy) {
   return layout.getArgPassingKind() == cir::ArgPassingKind::CanPassInRegs;
 }
 
+/// Whether a record carries no data for argument passing.  The answer has to
+/// come from the module's record-layout metadata, because an empty C++ class
+/// and a record holding one byte of data lower to the same members.  A record
+/// with no entry, such as one CXXABILowering synthesized, is treated as
+/// carrying data.
+static bool recordIsEmpty(ModuleOp modOp, cir::RecordType recTy) {
+  auto layout = cir::tryGetRecordLayout(modOp, recTy.getName());
+  return layout && layout.getIsEmpty();
+}
+
+/// Whether a union member holds no data, either because it is an empty record
+/// or an array of them.  An array is stripped because the ABI library reduces
+/// a union to a single member and coerces from that member's bytes, and it
+/// cannot see that an array of empty records supplies none.
+static bool unionMemberIsEmpty(ModuleOp modOp, mlir::Type ty) {
+  while (auto arrTy = dyn_cast<cir::ArrayType>(ty))
+    ty = arrTy.getElementType();
+  auto recTy = dyn_cast<cir::RecordType>(ty);
+  return recTy && recordIsEmpty(modOp, recTy);
+}
+
 /// A record's declared alignment, which the ABI uses for the byval and sret
 /// alignment of an indirect argument.  DataLayout derives alignment from the
 /// members, so it cannot see `__attribute__((aligned(N)))`.  The declared 
value
@@ -105,7 +127,8 @@ static llvm::Align recordDeclaredAlign(ModuleOp modOp, 
cir::RecordType recTy,
 /// Aggregates: a complete struct or union whose members are all themselves
 /// supported, or an array of a supported element type.  Everything else is
 /// reported NYI at the reject() choke point in classifyX86_64Function.
-static bool isSupportedType(mlir::Type ty, const DataLayout &dl) {
+static bool isSupportedType(mlir::Type ty, const DataLayout &dl,
+                            ModuleOp modOp) {
   // A pointer is only handled in the default address space (null) or an
   // already-lowered target address space.  A LangAddressSpaceAttr must be
   // lowered before this pass, so reject it rather than silently dropping it.
@@ -130,7 +153,7 @@ static bool isSupportedType(mlir::Type ty, const DataLayout 
&dl) {
     return intTy.getWidth() <= 64 || intTy.getWidth() == 128;
   }
   if (auto arrTy = dyn_cast<cir::ArrayType>(ty))
-    return isSupportedType(arrTy.getElementType(), dl);
+    return isSupportedType(arrTy.getElementType(), dl, modOp);
   if (auto recTy = dyn_cast<cir::RecordType>(ty)) {
     // An incomplete record has no layout to classify, and a packed one needs
     // pad-aware eightbyte classification this bridge does not implement.
@@ -156,14 +179,25 @@ static bool isSupportedType(mlir::Type ty, const 
DataLayout &dl) {
         };
         if (!llvm::any_of(members, spansRecord))
           return false;
+        // Classic sizes a union's coercion from the bytes that hold data, so 
an
+        // empty member contributes none.  The library instead reduces the 
union
+        // to one member, picked by alignment and then by size, and coerces 
from
+        // that member: an empty one can win either comparison and widen the
+        // coercion past what classic emits.
+        auto isEmptyMember = [&](mlir::Type m) {
+          return unionMemberIsEmpty(modOp, m);
+        };
+        if (llvm::any_of(members, isEmptyMember))
+          return false;
       }
-    } else if (recTy.getPadded()) {
-      // A struct's padding is a member the classifier would have to recognize
-      // as padding rather than data, which is not implemented.
+    } else if (recTy.getPadded() && !recordIsEmpty(modOp, recTy)) {
+      // Padding the classifier would have to tell apart from data is not
+      // implemented.  An empty record has no data to confuse it with.
       return false;
     }
-    return llvm::all_of(recTy.getMembers(),
-                        [&](mlir::Type m) { return isSupportedType(m, dl); });
+    return llvm::all_of(recTy.getMembers(), [&](mlir::Type m) {
+      return isSupportedType(m, dl, modOp);
+    });
   }
   return false;
 }
@@ -251,6 +285,16 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
         llvm::TypeSize sizeBits = llvm::TypeSize::getFixed(
             dl.getTypeSizeInBits(type).getFixedValue());
         llvm::Align align = recordDeclaredAlign(modOp, recTy, dl);
+
+        // An empty record holds no user data, so map it with no fields and let
+        // the classifier reach Ignore on its own.  The size still decides
+        // between that and the memory class, which an empty class past two
+        // eightbytes lands in.
+        if (recordIsEmpty(modOp, recTy))
+          return tb.getRecordType(
+              /*Fields=*/{}, sizeBits, align, 
llvm::abi::StructPacking::Default,
+              /*BaseClasses=*/{}, /*VirtualBaseClasses=*/{}, flags);
+
         SmallVector<llvm::abi::FieldInfo> fields;
         fields.reserve(recTy.getMembers().size());
 
@@ -265,8 +309,8 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type,
                                  llvm::abi::StructPacking::Default, flags);
         }
 
-        // isSupportedType rejects packed and padded structs, so every field
-        // here sits at its naturally-aligned offset.
+        // An accepted non-empty struct is never padded, so every field here
+        // sits at its naturally-aligned offset.
         uint64_t offsetBits = 0;
         for (mlir::Type fieldTy : recTy.getMembers()) {
           const llvm::abi::Type *mappedField =
@@ -307,7 +351,7 @@ 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, or a zero-field record dropped from the signature.
+/// Ignore: a void return, or an empty record dropped from the signature.
 static std::optional<ArgClassification>
 convertABIArgInfo(const llvm::abi::ArgInfo &info, MLIRContext *ctx,
                   mlir::Type origTy) {
@@ -388,7 +432,7 @@ static std::optional<FunctionClassification> 
classifyX86_64Signature(
   bool voidRet = isa<cir::VoidType>(retCIR);
 
   auto reject = [&](mlir::Type t) -> bool {
-    if (isSupportedType(t, dl))
+    if (isSupportedType(t, dl, modOp))
       return false;
     emitError()
         << "x86_64 calling-convention lowering not yet implemented for type "
diff --git a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp 
b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
new file mode 100644
index 0000000000000..3927a8ce3b8ae
--- /dev/null
+++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp
@@ -0,0 +1,149 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fclangir \
+// RUN:   -clangir-enable-call-conv-lowering -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 -std=c++17 -fclangir \
+// RUN:   -clangir-enable-call-conv-lowering -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefixes=LLVM,LLVM-CIR --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -emit-llvm %s 
-o %t.ll
+// RUN: FileCheck --check-prefixes=LLVM,LLVM-OGCG --input-file=%t.ll %s
+
+struct Empty {};
+struct EmptyMem { Empty e; };
+struct HasEmptyBase : Empty {};
+struct Derived : EmptyMem { int i; };
+struct Aligned {} __attribute__((aligned(16)));
+struct NoUnique { [[no_unique_address]] Empty a, b, c; };
+struct NoUniqueOne { [[no_unique_address]] Empty e; };
+struct UnnamedBits { int : 3; };
+struct Reserved { unsigned : 32; };
+struct OneByte { unsigned char c; };
+struct ArrOfEmpty { Empty a[2]; };
+struct HasEmpty { int x; Empty e; };
+struct EmptyFirst { Empty e; int x; };
+struct alignas(32) Big32 {};
+union UBits { unsigned : 3; };
+union UNone {};
+
+// An empty class is passed in no register at all.
+int takeEmpty(Empty v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z9takeEmpty5Emptyi(%arg0: !s32i {{.*}}) -> (!s32i
+// LLVM: define dso_local noundef i32 @_Z9takeEmpty5Emptyi(i32 noundef 
%{{[^,]+}})
+
+// A plain empty member leaves the record non-empty under the Itanium rule, so
+// this is dropped because the member contributes no eightbyte.
+int takeEmptyMem(EmptyMem v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z12takeEmptyMem8EmptyMemi(%arg0: !s32i {{.*}}) -> 
(!s32i
+// LLVM: define dso_local noundef i32 @_Z12takeEmptyMem8EmptyMemi(i32 noundef 
%{{[^,]+}})
+
+// Emptiness does follow a base class.
+int takeHasEmptyBase(HasEmptyBase v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z16takeHasEmptyBase12HasEmptyBasei(%arg0: !s32i 
{{.*}}) -> (!s32i
+// LLVM: define dso_local noundef i32 @_Z16takeHasEmptyBase12HasEmptyBasei(i32 
noundef %{{[^,]+}})
+
+// The empty base takes layout space, so the int sits at offset 4 and the
+// eightbyte covering both coerces to i64.
+int takeDerived(Derived v) { return v.i; }
+
+// CIR: cir.func {{.*}}@_Z11takeDerived7Derived(%arg0: !u64i {{.*}}) -> (!s32i
+// LLVM: define dso_local noundef i32 @_Z11takeDerived7Derived(i64 %{{[^,]+}})
+
+// An alignment attribute widens the padding, and several [[no_unique_address]]
+// members do too, but neither makes the record carry data.
+int takeAligned(Aligned v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z11takeAligned7Alignedi(%arg0: !s32i {{.*}}) -> (!s32i
+// LLVM: define dso_local noundef i32 @_Z11takeAligned7Alignedi(i32 noundef 
%{{[^,]+}})
+
+int takeNoUnique(NoUnique v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z12takeNoUnique8NoUniquei(%arg0: !s32i {{.*}}) -> 
(!s32i
+// LLVM: define dso_local noundef i32 @_Z12takeNoUnique8NoUniquei(i32 noundef 
%{{[^,]+}})
+
+// [[no_unique_address]] is what lets a single empty member count as empty.
+int takeNoUniqueOne(NoUniqueOne v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z15takeNoUniqueOne11NoUniqueOnei(%arg0: !s32i {{.*}}) 
-> (!s32i
+// LLVM: define dso_local noundef i32 @_Z15takeNoUniqueOne11NoUniqueOnei(i32 
noundef %{{[^,]+}})
+
+// A record holding only unnamed bit-fields carries no data either, though it
+// occupies layout space.  It lowers to the same CIR type as OneByte below, so
+// the record-layout metadata is what separates them.
+int takeUnnamedBits(UnnamedBits v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z15takeUnnamedBits11UnnamedBitsi(%arg0: !s32i {{.*}}) 
-> (!s32i
+// LLVM: define dso_local noundef i32 @_Z15takeUnnamedBits11UnnamedBitsi(i32 
noundef %{{[^,]+}})
+
+int takeReserved(Reserved v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z12takeReserved8Reservedi(%arg0: !s32i {{.*}}) -> 
(!s32i
+// LLVM: define dso_local noundef i32 @_Z12takeReserved8Reservedi(i32 noundef 
%{{[^,]+}})
+
+// A byte of real data keeps its register.
+int takeOneByte(OneByte v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z11takeOneByte7OneBytei(%arg0: !u8i {{.*}}, %arg1: 
!s32i {{.*}}) -> (!s32i
+// LLVM: define dso_local noundef i32 @_Z11takeOneByte7OneBytei(i8 %{{[^,]+}}, 
i32 noundef %{{[^,]+}})
+
+// An array of empty records leaves its record non-empty, and the array still
+// contributes no eightbyte.
+int takeArrOfEmpty(ArrOfEmpty v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z14takeArrOfEmpty10ArrOfEmptyi(%arg0: !s32i {{.*}}) 
-> (!s32i
+// LLVM: define dso_local noundef i32 @_Z14takeArrOfEmpty10ArrOfEmptyi(i32 
noundef %{{[^,]+}})
+
+// An empty member contributes no eightbyte, so only the int is classified.
+int takeHasEmpty(HasEmpty v) { return v.x; }
+
+// CIR: cir.func {{.*}}@_Z12takeHasEmpty8HasEmpty(%arg0: !s32i {{.*}}) -> 
(!s32i
+// LLVM: define dso_local noundef i32 @_Z12takeHasEmpty8HasEmpty(i32 
%{{[^,]+}})
+
+int takeEmptyFirst(EmptyFirst v) { return v.x; }
+
+// CIR: cir.func {{.*}}@_Z14takeEmptyFirst10EmptyFirst(%arg0: !u64i {{.*}}) -> 
(!s32i
+// LLVM: define dso_local noundef i32 @_Z14takeEmptyFirst10EmptyFirst(i64 
%{{[^,]+}})
+
+// Past two eightbytes SysV says memory whatever the content, so an empty class
+// this size is passed indirectly at its declared alignment.
+int takeBig32(Big32 v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z9takeBig325Big32i(%arg0: !cir.ptr<!rec_Big32> 
{llvm.align = 32 : i64, llvm.byval = !rec_Big32, llvm.noalias, 
llvm.noundef}{{.*}}, %arg1: !s32i {{.*}}) -> (!s32i
+// LLVM-CIR: define dso_local noundef i32 @_Z9takeBig325Big32i(ptr noalias 
noundef byval(%struct.Big32) align 32 %{{[^,]+}}, i32 noundef %{{[^,]+}})
+// LLVM-OGCG: define dso_local noundef i32 @_Z9takeBig325Big32i(ptr noundef 
byval(%struct.Big32) align 32 %{{[^,]+}}, i32 noundef %{{[^,]+}})
+
+// The same class returned uses sret at that alignment.
+Big32 retBig32() { return Big32{}; }
+
+// CIR: cir.func {{.*}}@_Z8retBig32v(%arg0: !cir.ptr<!rec_Big32> {llvm.align = 
32 : i64, llvm.dead_on_unwind, llvm.noalias, llvm.sret = !rec_Big32, 
llvm.writable}
+// LLVM: define dso_local void @_Z8retBig32v(ptr dead_on_unwind noalias 
writable sret(%struct.Big32) align 32 %{{[^,]+}})
+
+// A union of only unnamed bit-fields holds no data, and neither does one with
+// no members at all.
+int takeUBits(UBits v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z9takeUBits5UBitsi(%arg0: !s32i {{.*}}) -> (!s32i
+// LLVM: define dso_local noundef i32 @_Z9takeUBits5UBitsi(i32 noundef 
%{{[^,]+}})
+
+int takeUNone(UNone v, int k) { return k; }
+
+// CIR: cir.func {{.*}}@_Z9takeUNone5UNonei(%arg0: !s32i {{.*}}) -> (!s32i
+// LLVM: define dso_local noundef i32 @_Z9takeUNone5UNonei(i32 noundef 
%{{[^,]+}})
+
+// An empty return is dropped to void.
+Empty retEmpty() { return Empty{}; }
+
+// CIR: cir.func {{.*}}@_Z8retEmptyv()
+// LLVM: define dso_local void @_Z8retEmptyv()
+
+// A call site drops the operand as well as the parameter.
+int caller(int k) {
+  Empty e;
+  return takeEmpty(e, k);
+}
+
+// CIR: cir.func {{.*}}@_Z6calleri(%arg0: !s32i {{.*}}) -> (!s32i
+// CIR:   cir.call @_Z9takeEmpty5Emptyi(%{{[0-9]+}}) : (!s32i {llvm.noundef}) 
-> (!s32i {llvm.noundef})
+// LLVM: define dso_local noundef i32 @_Z6calleri(i32 noundef %{{[^,]+}})
+// LLVM:   call noundef i32 @_Z9takeEmpty5Emptyi(i32 noundef %{{[^,]+}})
diff --git a/clang/test/CIR/CodeGen/record-type-metadata.cpp 
b/clang/test/CIR/CodeGen/record-type-metadata.cpp
index 46f823bce96b2..5904b6ca4305a 100644
--- a/clang/test/CIR/CodeGen/record-type-metadata.cpp
+++ b/clang/test/CIR/CodeGen/record-type-metadata.cpp
@@ -11,10 +11,45 @@ class NonTrivialDtor {
   ~NonTrivialDtor();
 };
 
+// A record whose only member is an unnamed bit-field carries no data for
+// argument passing, and one holding a byte of data lowers to the same members.
+struct UnnamedBits { int : 3; };
+struct OneByte { unsigned char c; };
+
+// Emptiness follows the base classes.  A C++ empty member counts only under
+// [[no_unique_address]], and never through an array of them, however many
+// elements it has: only a zero-length array is empty.
+struct HasEmptyBase : Empty {};
+struct HasDataBase : OneByte {};
+struct EmptyMem { Empty e; };
+struct NoUniqueOne { [[no_unique_address]] Empty e; };
+struct ArrOfEmpty { Empty a[2]; };
+struct NoUniqueArr { [[no_unique_address]] Empty a[2]; };
+struct ZeroArr { int a[0]; };
+
+// A vtable pointer is data, though it is neither a base nor a field.
+struct Poly { virtual void f(); };
+
+// A union carries no data when every member is empty, or when it has none.
+union UnnamedBitsUnion { unsigned : 3; };
+union NoMembers {};
+
 void takesTrivial(Trivial t) {}
 void takesEmpty(Empty e) {}
 void takesAligned(Aligned a) {}
 void takesNTD(NonTrivialDtor n) {}
+void takesUnnamedBits(UnnamedBits u) {}
+void takesOneByte(OneByte o) {}
+void takesHasEmptyBase(HasEmptyBase d) {}
+void takesHasDataBase(HasDataBase d) {}
+void takesEmptyMem(EmptyMem e) {}
+void takesNoUniqueOne(NoUniqueOne n) {}
+void takesArrOfEmpty(ArrOfEmpty a) {}
+void takesNoUniqueArr(NoUniqueArr n) {}
+void takesZeroArr(ZeroArr z) {}
+void takesPoly(Poly *p) {}
+void takesUnnamedBitsUnion(UnnamedBitsUnion u) {}
+void takesNoMembers(NoMembers n) {}
 
 // Record types should NOT contain ABI metadata keywords.
 // CIR-DAG: !rec_Trivial = !cir.struct<"Trivial" {!s32i, !s32i}>
@@ -22,8 +57,27 @@ void takesNTD(NonTrivialDtor n) {}
 // CIR-DAG: !rec_Aligned = !cir.struct<"Aligned" padded {!s32i, !s32i, 
!cir.array<!u8i x 8>}>
 // CIR-DAG: !rec_NonTrivialDtor = !cir.struct<class "NonTrivialDtor" {!s32i}>
 
+// UnnamedBits and OneByte are the same type, so only the metadata separates
+// the one that carries data from the one that does not.
+// CIR-DAG: !rec_UnnamedBits = !cir.struct<"UnnamedBits" {!u8i}>
+// CIR-DAG: !rec_OneByte = !cir.struct<"OneByte" {!u8i}>
+
 // ABI metadata lives in module-level cir.record_layouts attribute.
-// CIR-DAG: Trivial = #cir.record_layout<arg_passing_kind = can_pass_in_regs, 
has_trivial_dtor = true, record_align = 4>
-// CIR-DAG: Empty = #cir.record_layout<arg_passing_kind = can_pass_in_regs, 
has_trivial_dtor = true, record_align = 1>
-// CIR-DAG: Aligned = #cir.record_layout<arg_passing_kind = can_pass_in_regs, 
has_trivial_dtor = true, record_align = 16>
-// CIR-DAG: NonTrivialDtor = #cir.record_layout<arg_passing_kind = 
cannot_pass_in_regs, has_trivial_dtor = false, record_align = 4>
+// CIR-DAG: Trivial = #cir.record_layout<arg_passing_kind = can_pass_in_regs, 
has_trivial_dtor = true, record_align = 4, is_empty = false>
+// The leading separator keeps this from matching inside a name that ends in
+// "Empty", since every entry prints on one line.
+// CIR-DAG: {{[{,] }}Empty = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = true>
+// CIR-DAG: Aligned = #cir.record_layout<arg_passing_kind = can_pass_in_regs, 
has_trivial_dtor = true, record_align = 16, is_empty = false>
+// CIR-DAG: NonTrivialDtor = #cir.record_layout<arg_passing_kind = 
cannot_pass_in_regs, has_trivial_dtor = false, record_align = 4, is_empty = 
false>
+// CIR-DAG: UnnamedBits = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = true>
+// CIR-DAG: OneByte = #cir.record_layout<arg_passing_kind = can_pass_in_regs, 
has_trivial_dtor = true, record_align = 1, is_empty = false>
+// CIR-DAG: HasEmptyBase = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = true>
+// CIR-DAG: HasDataBase = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = false>
+// CIR-DAG: ArrOfEmpty = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = false>
+// CIR-DAG: NoUniqueArr = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = false>
+// CIR-DAG: ZeroArr = #cir.record_layout<arg_passing_kind = can_pass_in_regs, 
has_trivial_dtor = true, record_align = 4, is_empty = true>
+// CIR-DAG: EmptyMem = #cir.record_layout<arg_passing_kind = can_pass_in_regs, 
has_trivial_dtor = true, record_align = 1, is_empty = false>
+// CIR-DAG: NoUniqueOne = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = true>
+// CIR-DAG: Poly = #cir.record_layout<arg_passing_kind = cannot_pass_in_regs, 
has_trivial_dtor = true, record_align = 8, is_empty = false>
+// CIR-DAG: UnnamedBitsUnion = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = true>
+// CIR-DAG: NoMembers = #cir.record_layout<arg_passing_kind = 
can_pass_in_regs, has_trivial_dtor = true, record_align = 1, is_empty = true>
diff --git a/clang/test/CIR/IR/invalid-record-layout.cir 
b/clang/test/CIR/IR/invalid-record-layout.cir
index 61ebc9b37528c..ab45f263008a4 100644
--- a/clang/test/CIR/IR/invalid-record-layout.cir
+++ b/clang/test/CIR/IR/invalid-record-layout.cir
@@ -4,7 +4,7 @@ module attributes {
   cir.record_layouts = {
     // expected-error @below {{failed to verify that record_align must be a 
non-zero power of two}}
     S = #cir.record_layout<arg_passing_kind = can_pass_in_regs,
-                           has_trivial_dtor = true, record_align = 0>}
+                           has_trivial_dtor = true, record_align = 0, is_empty 
= false>}
 } {
 }
 
@@ -14,6 +14,6 @@ module attributes {
   cir.record_layouts = {
     // expected-error @below {{failed to verify that record_align must be a 
non-zero power of two}}
     S = #cir.record_layout<arg_passing_kind = can_pass_in_regs,
-                           has_trivial_dtor = true, record_align = 3>}
+                           has_trivial_dtor = true, record_align = 3, is_empty 
= false>}
 } {
 }
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 83d36ce22b970..a392c53cbe6a9 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
@@ -4,6 +4,7 @@
 !s16i = !cir.int<s, 16>
 !s32i = !cir.int<s, 32>
 !u8i = !cir.int<u, 8>
+!u16i = !cir.int<u, 16>
 !rec_UPacked = !cir.union<"UPacked" packed {!s32i, !cir.array<!s8i x 5>}, 
padding = {!u8i}>
 !rec_ULongDouble = !cir.union<"ULongDouble" {!cir.long_double<!cir.f80>, 
!s32i}>
 !rec_UFloats = !cir.union<"UFloats" {!cir.array<!cir.float x 2>, 
!cir.array<!cir.float x 2>}>
@@ -14,11 +15,30 @@
 !rec_UEmptyLarge = !cir.union<"UEmptyLarge" {}, padding = {!cir.array<!u8i x 
32>}>
 !rec_P = !cir.struct<"P" packed {!s8i, !s32i}>
 !rec_Ov = !cir.struct<"Ov" padded {!s32i, !cir.array<!u8i x 12>}>
+!rec_PadByte = !cir.struct<"PadByte" padded {!u8i, !cir.array<!u8i x 3>}>
+!rec_PadByte2 = !cir.struct<"PadByte2" padded {!u8i, !u8i}>
+!rec_NoEntry = !cir.struct<"NoEntry" padded {!u8i}>
+!rec_NotEmpty = !cir.struct<"NotEmpty" padded {!u8i}>
+!rec_UPadByte = !cir.union<"UPadByte" {!u8i}, padding = {!cir.array<!u8i x 3>}>
 !rec_E = !cir.struct<"E" padded {!u8i}>
+!rec_EOver = !cir.struct<"EOver" padded {!cir.array<!u8i x 16>}>
+!rec_UEmptyOnly = !cir.union<"UEmptyOnly" {!rec_E}>
+!rec_UEmptyOver = !cir.union<"UEmptyOver" {!rec_EOver, !s32i}, padding = 
{!cir.array<!u8i x 12>}>
+!rec_UArrEmpty = !cir.union<"UArrEmpty" {!cir.array<!rec_E x 2>, !s8i}>
 !rec_FF = !cir.struct<"FF" {!cir.float, !cir.float}>
 !rec_RetFF = !cir.struct<"RetFF" {!cir.float, !cir.float}>
 
 module attributes {
+  cir.record_layouts = {
+    NotEmpty = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 1, is_empty = false>,
+    E = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 1, is_empty = true>,
+    EOver = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 16, is_empty = true>},
   dlti.dl_spec = #dlti.dl_spec<
     #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
     #dlti.dl_entry<i64, dense<64>: vector<2xi64>>,
@@ -113,13 +133,69 @@ module attributes {
 
   // CHECK: not yet implemented for type '!cir.struct<"Ov" padded
 
-  // An empty C++ class is laid out as a single padded byte, so it is rejected
-  // by the padded check; its Ignore classification is deferred.
-  cir.func @take_empty(%arg0: !rec_E) {
+  // `struct { unsigned char c; } __attribute__((aligned(4)))` is byte-sized 
and
+  // padded throughout, but it carries data, so it stays rejected.
+  cir.func @take_padded_byte(%arg0: !rec_PadByte) {
     cir.return
   }
 
-  // CHECK: not yet implemented for type '!cir.struct<"E" padded
+  // CHECK: not yet implemented for type '!cir.struct<"PadByte" padded
+
+  // The same at `aligned(2)`, where the padding is a scalar rather than an
+  // array.
+  cir.func @take_padded_byte2(%arg0: !rec_PadByte2) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.struct<"PadByte2" padded
+
+  // A padded record the metadata calls non-empty is data whatever its members
+  // look like: this one is shaped exactly like an empty class.
+  cir.func @take_not_empty(%arg0: !rec_NotEmpty) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.struct<"NotEmpty" padded
+
+  // A record with no layout entry at all, which CXXABILowering can synthesize,
+  // is treated as carrying data rather than assumed empty.
+  cir.func @take_no_entry(%arg0: !rec_NoEntry) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.struct<"NoEntry" padded
+
+  // A union holds padding in a slot of its own, so its lone byte member is
+  // data.  Rejected by the union rule (no member spans the declared size),
+  // not the empty-class one.
+  cir.func @take_padded_byte_union(%arg0: !rec_UPadByte) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.union<"UPadByte"
+
+  // A union with an empty member coerces wider than classic, so it is left
+  // NYI rather than accepted along with the empty class itself.
+  cir.func @take_union_empty_over(%arg0: !rec_UEmptyOver) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.union<"UEmptyOver"
+
+  // Deferred with it, though this one holds no data at all.
+  cir.func @take_union_empty_only(%arg0: !rec_UEmptyOnly) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.union<"UEmptyOnly"
+
+  // The reduction picks by alignment and then by size, so an array of empty
+  // records can win over a data member that is smaller.
+  cir.func @take_union_arr_empty(%arg0: !rec_UArrEmpty) {
+    cir.return
+  }
+
+  // CHECK: not yet implemented for type '!cir.union<"UArrEmpty"
 
   // An all-float struct classifies to an SSE vector coerce this bridge does
   // not represent, so it is reported NYI rather than passed unchanged.
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir
new file mode 100644
index 0000000000000..9c1b094c49af3
--- /dev/null
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir
@@ -0,0 +1,248 @@
+// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 | FileCheck %s
+// RUN: cir-opt %s -cir-call-conv-lowering=target=x86_64 -cir-to-llvm -o - 
2>/dev/null \
+// RUN:   | mlir-translate -mlir-to-llvmir --allow-unregistered-dialect \
+// RUN:   | FileCheck %s --check-prefix=LLVM
+
+!s32i = !cir.int<s, 32>
+!u8i = !cir.int<u, 8>
+!rec_E = !cir.struct<"E" padded {!u8i}>
+!rec_EBlob = !cir.struct<"EBlob" padded {!cir.array<!u8i x 3>}>
+!rec_NTCE = !cir.struct<"NTCE" padded {!u8i}>
+!rec_Byte = !cir.struct<"Byte" {!u8i}>
+!rec_HasEmpty = !cir.struct<"HasEmpty" {!s32i, !rec_E}>
+!rec_HasEmptyFirst = !cir.struct<"HasEmptyFirst" {!rec_E, !s32i}>
+!rec_EmptyMem = !cir.struct<"EmptyMem" {!rec_E}>
+!rec_Base2 = !cir.struct<"Base2" {!s32i}>
+!rec_D2 = !cir.struct<"D2" {!rec_EmptyMem, !rec_Base2}>
+!rec_EMed = !cir.struct<"EMed" padded {!cir.array<!u8i x 16>}>
+!rec_EBig = !cir.struct<"EBig" padded {!cir.array<!u8i x 32>}>
+
+module attributes {
+  cir.triple = "x86_64-unknown-linux-gnu",
+  cir.record_layouts = {
+    E = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 1, is_empty = true>,
+    EBlob = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 1, is_empty = true>,
+    EmptyMem = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 1, is_empty = false>,
+    Byte = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 1, is_empty = false>,
+    NTCE = #cir.record_layout<
+      arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = false,
+      record_align = 1, is_empty = true>,
+    EMed = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 16, is_empty = true>,
+    EBig = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 32, is_empty = true>},
+  dlti.dl_spec = #dlti.dl_spec<
+    #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
+    #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
+    #dlti.dl_entry<i64, dense<64>: vector<2xi64>>>
+} {
+
+  // A C++ empty class is padding alone, so the argument is dropped.
+  cir.func @take_empty(%arg0: !rec_E) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_empty() {
+  // CHECK-NEXT: cir.return
+  // LLVM: define void @take_empty()
+
+  // Several [[no_unique_address]] members widen the padding past a byte.
+  cir.func @take_empty_blob(%arg0: !rec_EBlob) -> !s32i {
+    %0 = cir.const #cir.int<0> : !s32i
+    cir.return %0 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_empty_blob() -> !s32i {
+  // LLVM: define i32 @take_empty_blob()
+
+  // An empty return is dropped to void, and the local slot survives.
+  cir.func @ret_empty() -> !rec_E {
+    %0 = cir.alloca "r" align(1) : !cir.ptr<!rec_E>
+    %1 = cir.load %0 : !cir.ptr<!rec_E>, !rec_E
+    cir.return %1 : !rec_E
+  }
+
+  // CHECK: cir.func{{.*}} @ret_empty() {
+  // CHECK: cir.alloca "r" align(1) : !cir.ptr<!rec_E>
+  // CHECK: cir.return{{$}}
+  // LLVM: define void @ret_empty()
+
+  // A remaining real argument shifts down into the first slot.
+  cir.func @take_mixed(%arg0: !rec_E, %arg1: !s32i) -> !s32i {
+    cir.return %arg1 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_mixed(%arg0: !s32i) -> !s32i
+  // CHECK-NEXT: cir.return %arg0 : !s32i
+  // LLVM: define i32 @take_mixed(i32 %{{.*}})
+
+  // Dropping one from the middle keeps the order of the arguments around it.
+  cir.func @take_middle(%arg0: !s32i, %arg1: !rec_E, %arg2: !u8i) -> !s32i {
+    cir.return %arg0 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_middle(%arg0: !s32i, %arg1: !u8i 
{llvm.zeroext}) -> !s32i
+  // LLVM: define i32 @take_middle(i32 %{{.*}}, i8 zeroext %{{.*}})
+
+  // A byte-sized record needing no padding is data: the padded flag is the
+  // boundary, not the size.
+  cir.func @take_byte(%arg0: !rec_Byte) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_byte(%arg0: !u8i)
+  // LLVM: define void @take_byte(i8 %{{.*}})
+
+  // An empty member contributes no eightbyte, so only the int is classified.
+  cir.func @take_has_empty(%arg0: !rec_HasEmpty) -> !s32i {
+    %0 = cir.alloca "h" align(4) : !cir.ptr<!rec_HasEmpty>
+    cir.store %arg0, %0 : !rec_HasEmpty, !cir.ptr<!rec_HasEmpty>
+    %1 = cir.get_member %0[0] {name = "x"} : !cir.ptr<!rec_HasEmpty> -> 
!cir.ptr<!s32i>
+    %2 = cir.load %1 : !cir.ptr<!s32i>, !s32i
+    cir.return %2 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_has_empty(%arg0: !s32i) -> !s32i
+  // CHECK:   %[[SLOT:.*]] = cir.alloca "coerce" align(4) : 
!cir.ptr<!rec_HasEmpty>
+  // CHECK:   %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : 
!cir.ptr<!rec_HasEmpty> -> !cir.ptr<!s32i>
+  // CHECK:   cir.store %arg0, %[[CAST]] : !s32i, !cir.ptr<!s32i>
+  // CHECK:   %{{.*}} = cir.load %[[SLOT]] : !cir.ptr<!rec_HasEmpty>, 
!rec_HasEmpty
+  // LLVM: define i32 @take_has_empty(i32 %{{.*}})
+
+  // The empty member still takes layout space, so the int sits at offset 4 and
+  // the eightbyte spanning both coerces to i64.
+  cir.func @take_has_empty_first(%arg0: !rec_HasEmptyFirst) -> !s32i {
+    %0 = cir.alloca "h" align(4) : !cir.ptr<!rec_HasEmptyFirst>
+    cir.store %arg0, %0 : !rec_HasEmptyFirst, !cir.ptr<!rec_HasEmptyFirst>
+    %1 = cir.get_member %0[1] {name = "x"} : !cir.ptr<!rec_HasEmptyFirst> -> 
!cir.ptr<!s32i>
+    %2 = cir.load %1 : !cir.ptr<!s32i>, !s32i
+    cir.return %2 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_has_empty_first(%arg0: !u64i) -> !s32i
+  // CHECK:   %[[SLOT:.*]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK:   cir.store %arg0, %[[SLOT]] : !u64i, !cir.ptr<!u64i>
+  // CHECK:   %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!u64i> -> 
!cir.ptr<!rec_HasEmptyFirst>
+  // LLVM: define i32 @take_has_empty_first(i64 %{{.*}})
+
+  // A plain empty member leaves its record non-empty, matching the Itanium
+  // rule, so this one is dropped by composition rather than by the flag: the
+  // member maps to no fields and contributes no eightbyte.
+  cir.func @take_empty_mem(%arg0: !rec_EmptyMem) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_empty_mem() {
+  // CHECK-NEXT: cir.return
+  // LLVM: define void @take_empty_mem()
+
+  // Nested one level deeper, as the first base of a derived record: it adds no
+  // eightbyte, but its byte still places Base2 at offset 4.
+  cir.func @take_d2(%arg0: !rec_D2) -> !s32i {
+    %0 = cir.alloca "d" align(4) : !cir.ptr<!rec_D2>
+    cir.store %arg0, %0 : !rec_D2, !cir.ptr<!rec_D2>
+    %1 = cir.get_member %0[1] {name = "Base2"} : !cir.ptr<!rec_D2> -> 
!cir.ptr<!rec_Base2>
+    %2 = cir.get_member %1[0] {name = "i"} : !cir.ptr<!rec_Base2> -> 
!cir.ptr<!s32i>
+    %3 = cir.load %2 : !cir.ptr<!s32i>, !s32i
+    cir.return %3 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_d2(%arg0: !u64i) -> !s32i
+  // CHECK:   %[[SLOT:.*]] = cir.alloca "coerce" align(8) : !cir.ptr<!u64i>
+  // CHECK:   cir.store %arg0, %[[SLOT]] : !u64i, !cir.ptr<!u64i>
+  // LLVM: define i32 @take_d2(i64 %{{.*}})
+
+  // Being empty does not override the declared argument-passing kind.
+  cir.func @take_ntc_empty(%arg0: !rec_NTCE) {
+    cir.return
+  }
+
+  // CHECK: cir.func{{.*}} @take_ntc_empty(%arg0: !cir.ptr<!rec_NTCE> 
{llvm.align = 1 : i64, llvm.byref = !rec_NTCE})
+  // LLVM: define void @take_ntc_empty(ptr byref(%struct.NTCE) align 1 %{{.*}})
+
+  // The same record returned goes through sret rather than being dropped.
+  cir.func @ret_ntc_empty() -> !rec_NTCE {
+    %0 = cir.alloca "r" align(1) : !cir.ptr<!rec_NTCE>
+    %1 = cir.load %0 : !cir.ptr<!rec_NTCE>, !rec_NTCE
+    cir.return %1 : !rec_NTCE
+  }
+
+  // CHECK: cir.func{{.*}} @ret_ntc_empty(%arg0: !cir.ptr<!rec_NTCE> 
{llvm.align = 1 : i64, llvm.dead_on_unwind, llvm.noalias, llvm.sret = 
!rec_NTCE, llvm.writable})
+  // LLVM: define void @ret_ntc_empty(ptr dead_on_unwind noalias writable 
sret(%struct.NTCE) align 1 %{{.*}})
+
+  // At exactly two eightbytes an empty class is still dropped.
+  cir.func @take_empty_med(%arg0: !rec_EMed, %arg1: !s32i) -> !s32i {
+    cir.return %arg1 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_empty_med(%arg0: !s32i) -> !s32i
+  // LLVM: define i32 @take_empty_med(i32 %{{.*}})
+
+  // Returning one at that size is dropped to void as well.
+  cir.func @ret_empty_med() -> !rec_EMed {
+    %0 = cir.alloca "r" align(16) : !cir.ptr<!rec_EMed>
+    %1 = cir.load %0 : !cir.ptr<!rec_EMed>, !rec_EMed
+    cir.return %1 : !rec_EMed
+  }
+
+  // CHECK: cir.func{{.*}} @ret_empty_med() {
+  // CHECK: cir.return{{$}}
+  // LLVM: define void @ret_empty_med()
+
+  // Past two eightbytes SysV says memory whatever the content, so it is passed
+  // indirectly at its declared alignment.
+  cir.func @take_empty_big(%arg0: !rec_EBig, %arg1: !s32i) -> !s32i {
+    cir.return %arg1 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @take_empty_big(%arg0: !cir.ptr<!rec_EBig> 
{llvm.align = 32 : i64, llvm.byval = !rec_EBig, llvm.noalias, llvm.noundef}, 
%arg1: !s32i) -> !s32i
+  // LLVM: define i32 @take_empty_big(ptr noalias noundef byval(%struct.EBig) 
align 32 %{{.*}}, i32 %{{.*}})
+
+  // The same record returned uses sret at that alignment.
+  cir.func @ret_empty_big() -> !rec_EBig {
+    %0 = cir.alloca "r" align(32) : !cir.ptr<!rec_EBig>
+    %1 = cir.load %0 : !cir.ptr<!rec_EBig>, !rec_EBig
+    cir.return %1 : !rec_EBig
+  }
+
+  // CHECK: cir.func{{.*}} @ret_empty_big(%arg0: !cir.ptr<!rec_EBig> 
{llvm.align = 32 : i64, llvm.dead_on_unwind, llvm.noalias, llvm.sret = 
!rec_EBig, llvm.writable})
+  // LLVM: define void @ret_empty_big(ptr dead_on_unwind noalias writable 
sret(%struct.EBig) align 32 %{{.*}})
+
+  // A call site drops the operand as well as the parameter.
+  cir.func @caller(%arg0: !rec_E, %arg1: !s32i) -> !s32i {
+    %0 = cir.call @take_mixed(%arg0, %arg1) : (!rec_E, !s32i) -> !s32i
+    cir.return %0 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @caller(%arg0: !s32i) -> !s32i
+  // CHECK: cir.call @take_mixed(%arg0) : (!s32i) -> !s32i
+  // LLVM: define i32 @caller(i32 %{{.*}})
+
+  // A dropped return leaves the call with no result, so uses of it become
+  // poison rather than dangling.
+  cir.func private @mk_empty() -> !rec_E
+  cir.func @caller_ret_empty() -> !s32i {
+    %0 = cir.call @mk_empty() : () -> !rec_E
+    %1 = cir.alloca "e" align(1) : !cir.ptr<!rec_E>
+    cir.store %0, %1 : !rec_E, !cir.ptr<!rec_E>
+    %2 = cir.const #cir.int<7> : !s32i
+    cir.return %2 : !s32i
+  }
+
+  // CHECK: cir.func{{.*}} @caller_ret_empty() -> !s32i
+  // CHECK:   cir.call @mk_empty() : () -> ()
+  // CHECK:   %[[POISON:.*]] = cir.const #cir.poison : !rec_E
+  // CHECK:   cir.store %[[POISON]], %{{.*}} : !rec_E, !cir.ptr<!rec_E>
+  // LLVM: define i32 @caller_ret_empty()
+  // LLVM:   call void @mk_empty()
+}
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
index ddcd0690e94bb..9fdf0fd605221 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-struct-indirect.cir
@@ -11,7 +11,7 @@
 module attributes {
   cir.record_layouts = {NoRegs = #cir.record_layout<
     arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = false,
-    record_align = 4>},
+    record_align = 4, is_empty = false>},
   dlti.dl_spec = #dlti.dl_spec<
     #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
     #dlti.dl_entry<i32, dense<32>: vector<2xi64>>,
diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir 
b/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir
index cad6cfd37d7a4..5c092727b0114 100644
--- a/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir
+++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir
@@ -27,13 +27,16 @@ module attributes {
   cir.record_layouts = {
     UNoRegs = #cir.record_layout<
       arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = false,
-      record_align = 4>,
+      record_align = 4, is_empty = false>,
     UBigOverAligned = #cir.record_layout<
       arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
-      record_align = 32>,
+      record_align = 32, is_empty = false>,
     SOverAligned = #cir.record_layout<
       arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
-      record_align = 32>},
+      record_align = 32, is_empty = false>,
+    UEmpty = #cir.record_layout<
+      arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true,
+      record_align = 1, is_empty = true>},
   dlti.dl_spec = #dlti.dl_spec<
     #dlti.dl_entry<i8, dense<8>: vector<2xi64>>,
     #dlti.dl_entry<i16, dense<16>: vector<2xi64>>,
diff --git a/clang/unittests/CIR/RecordTypeMetadataTest.cpp 
b/clang/unittests/CIR/RecordTypeMetadataTest.cpp
index 8118055920886..f3f6289bd8521 100644
--- a/clang/unittests/CIR/RecordTypeMetadataTest.cpp
+++ b/clang/unittests/CIR/RecordTypeMetadataTest.cpp
@@ -33,7 +33,8 @@ class RecordLayoutAttrTest : public ::testing::Test {
 
 TEST_F(RecordLayoutAttrTest, CanPassInRegs) {
   auto attr =
-      RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 4);
+      RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 4,
+                            /*is_empty=*/false);
   EXPECT_EQ(attr.getArgPassingKind(), ArgPassingKind::CanPassInRegs);
   EXPECT_TRUE(attr.getHasTrivialDtor());
   EXPECT_EQ(attr.getRecordAlign(), 4u);
@@ -41,14 +42,15 @@ TEST_F(RecordLayoutAttrTest, CanPassInRegs) {
 
 TEST_F(RecordLayoutAttrTest, CannotPassInRegs) {
   auto attr = RecordLayoutAttr::get(&context, ArgPassingKind::CannotPassInRegs,
-                                    false, 4);
+                                    false, 4, /*is_empty=*/false);
   EXPECT_EQ(attr.getArgPassingKind(), ArgPassingKind::CannotPassInRegs);
   EXPECT_FALSE(attr.getHasTrivialDtor());
 }
 
 TEST_F(RecordLayoutAttrTest, CanNeverPassInRegs) {
   auto attr = RecordLayoutAttr::get(
-      &context, ArgPassingKind::CanNeverPassInRegs, false, 8);
+      &context, ArgPassingKind::CanNeverPassInRegs, false, 8,
+      /*is_empty=*/false);
   EXPECT_EQ(attr.getArgPassingKind(), ArgPassingKind::CanNeverPassInRegs);
   EXPECT_FALSE(attr.getHasTrivialDtor());
   EXPECT_EQ(attr.getRecordAlign(), 8u);
@@ -56,10 +58,20 @@ TEST_F(RecordLayoutAttrTest, CanNeverPassInRegs) {
 
 TEST_F(RecordLayoutAttrTest, HighAlignment) {
   auto attr =
-      RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 32);
+      RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 32,
+                            /*is_empty=*/false);
   EXPECT_EQ(attr.getRecordAlign(), 32u);
 }
 
+TEST_F(RecordLayoutAttrTest, IsEmpty) {
+  auto empty = RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs,
+                                     true, 1, /*is_empty=*/true);
+  EXPECT_TRUE(empty.getIsEmpty());
+  auto data = RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs,
+                                    true, 1, /*is_empty=*/false);
+  EXPECT_FALSE(data.getIsEmpty());
+}
+
 TEST_F(RecordLayoutAttrTest, RecordTypeUnchanged) {
   IntType i32 = IntType::get(&context, 32, true);
   auto ty = StructType::get(&context, getName("Foo"), /*is_class=*/false);
@@ -74,7 +86,8 @@ TEST_F(RecordLayoutAttrTest, ModuleLevelLookup) {
   auto module = mlir::ModuleOp::create(loc);
 
   auto layoutAttr =
-      RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 8);
+      RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 8,
+                            /*is_empty=*/false);
 
   llvm::SmallVector<mlir::NamedAttribute> entries;
   entries.push_back(mlir::NamedAttribute(getName("TestRecord"), layoutAttr));
@@ -85,6 +98,7 @@ TEST_F(RecordLayoutAttrTest, ModuleLevelLookup) {
   EXPECT_EQ(result.getArgPassingKind(), ArgPassingKind::CanPassInRegs);
   EXPECT_TRUE(result.getHasTrivialDtor());
   EXPECT_EQ(result.getRecordAlign(), 8u);
+  EXPECT_FALSE(result.getIsEmpty());
 
   module->erase();
 }

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to