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