https://github.com/adams381 updated https://github.com/llvm/llvm-project/pull/214742
>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 1/2] [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(); } >From 789dfe87ff5704363cf3c8a42fbc8f453ea1add8 Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Mon, 17 Aug 2026 11:55:10 -0700 Subject: [PATCH 2/2] [CIR] Answer record emptiness from the member marks The record-level `is_empty` flag on `cir.record_layout` was added because the CIR type could not tell an empty class from a byte of data. `struct { int : 3; }` and `struct { unsigned char c; }` lowered to the same members. The marks stack ended that, so `RecordType::isEmptyForABI()` answers it from the type, which is the calculation the review asked for. Retire the flag. The parameter, the CIRGen code that filled it from the AST, and the tests that pinned it all go, and the x86_64 bridge queries the type at the three points that care: accepting a padded record whose members hold no data, rejecting a union with an empty-record member, and mapping an empty record with no fields so the classifier reaches Ignore on its own. This also corrects records the filter already accepted. A record marked only `empty`, which is how a run of unnamed bit-fields lowers, never set the padded flag, so it passed the filter and had its storage mapped as a real field. `struct { unsigned : 32; }` was passed in a register where classic drops it. Assisted-by: Cursor / claude-opus-5 --- .../include/clang/CIR/Dialect/IR/CIRAttrs.td | 16 ++--- .../CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp | 4 +- .../Transforms/CallConvLoweringPass.cpp | 44 ++++++++++--- .../call-conv-lowering-x86_64-empty.cpp | 40 ++++++++++-- .../test/CIR/CodeGen/record-type-metadata.cpp | 62 ++----------------- clang/test/CIR/IR/invalid-record-layout.cir | 4 +- .../abi-lowering/x86_64-aggregate-nyi.cir | 54 ++++++++++++++-- .../abi-lowering/x86_64-empty-class.cir | 56 +++++++++++------ .../abi-lowering/x86_64-struct-indirect.cir | 2 +- .../Transforms/abi-lowering/x86_64-union.cir | 9 +-- .../unittests/CIR/RecordTypeMetadataTest.cpp | 24 ++----- 11 files changed, 178 insertions(+), 137 deletions(-) diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td index c58593f761ea4..0871d48506cf8 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td +++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td @@ -143,10 +143,6 @@ 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: ``` @@ -155,13 +151,11 @@ 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, - is_empty = false>, + record_align = 4>, "NonTrivialDtor" = #cir.record_layout< arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = false, - record_align = 4, - is_empty = false> + record_align = 4> } } ``` @@ -170,16 +164,14 @@ 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, - "bool":$is_empty + "uint64_t":$record_align ); let assemblyFormat = [{ `<` `arg_passing_kind` `=` $arg_passing_kind `,` `has_trivial_dtor` `=` $has_trivial_dtor `,` - `record_align` `=` $record_align `,` - `is_empty` `=` $is_empty + `record_align` `=` $record_align `>` }]; diff --git a/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp b/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp index d0e0cb99e1715..1ce70091d9f4b 100644 --- a/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp @@ -856,12 +856,10 @@ 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, isEmpty)); + recordAlignInBytes)); } auto rl = std::make_unique<CIRGenRecordLayout>( diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp index 6d3249ef9e5b6..70ad91dca6bc0 100644 --- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp +++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp @@ -76,9 +76,10 @@ namespace { // consumes. Integer (including `_BitInt` up to 128 bits) / pointer / bool / // floating-point scalars are handled, as are struct / union / array aggregates, // `_Complex`, and a fixed-width vector whose width is a power of two. Other -// vectors, packed or padded records, and a union no member of which spans its -// declared size are reported NYI by classifyX86_64Function so an unsupported -// signature fails the pass instead of being misclassified. +// vectors, packed records, a padded record that holds data, a union no member +// of which spans its declared size, and a union with an empty-record member are +// reported NYI by classifyX86_64Function so an unsupported signature fails the +// pass instead of being misclassified. //===----------------------------------------------------------------------===// /// Whether a struct's declared argument-passing kind (from the module's @@ -92,6 +93,15 @@ static bool recordCanPassInRegs(ModuleOp modOp, cir::RecordType recTy) { return layout.getArgPassingKind() == cir::ArgPassingKind::CanPassInRegs; } +/// Whether a union member is an empty record, looking through arrays, since an +/// array of empty records supplies no bytes either. +static bool unionMemberIsEmpty(mlir::Type ty) { + while (auto arrTy = dyn_cast<cir::ArrayType>(ty)) + ty = arrTy.getElementType(); + auto recTy = dyn_cast<cir::RecordType>(ty); + return recTy && recTy.isEmptyForABI(); +} + /// 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 @@ -198,10 +208,17 @@ 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. + if (llvm::any_of(members, unionMemberIsEmpty)) + 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() && !recTy.isEmptyForABI()) { + // A pad member 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(), @@ -312,6 +329,15 @@ 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); + + // Mapped with no fields, an empty record reaches Ignore on its own. + // The size still matters: past two eightbytes SysV says memory whatever + // the content. + if (recTy.isEmptyForABI()) + return tb.getRecordType( + /*Fields=*/{}, sizeBits, align, llvm::abi::StructPacking::Default, + /*BaseClasses=*/{}, /*VirtualBaseClasses=*/{}, flags); + SmallVector<llvm::abi::FieldInfo> fields; fields.reserve(recTy.getMembers().size()); @@ -326,8 +352,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 = @@ -368,7 +394,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) { 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 index 3927a8ce3b8ae..126470331ce01 100644 --- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp +++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64-empty.cpp @@ -1,8 +1,8 @@ // 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: -fclangir-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: -fclangir-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 @@ -20,6 +20,10 @@ struct OneByte { unsigned char c; }; struct ArrOfEmpty { Empty a[2]; }; struct HasEmpty { int x; Empty e; }; struct EmptyFirst { Empty e; int x; }; +struct EmptySecond { long a; Empty e; }; +struct EmptySSE { double a; Empty e; }; +struct FloatEmpty { float a; Empty e; }; +struct FloatEmptyFirst { Empty e; float a; }; struct alignas(32) Big32 {}; union UBits { unsigned : 3; }; union UNone {}; @@ -68,9 +72,8 @@ 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. +// Unnamed bit-field storage is marked empty rather than pad, so a record of +// nothing but unnamed bit-fields carries no data either. int takeUnnamedBits(UnnamedBits v, int k) { return k; } // CIR: cir.func {{.*}}@_Z15takeUnnamedBits11UnnamedBitsi(%arg0: !s32i {{.*}}) -> (!s32i @@ -105,6 +108,33 @@ int takeEmptyFirst(EmptyFirst v) { return v.x; } // CIR: cir.func {{.*}}@_Z14takeEmptyFirst10EmptyFirst(%arg0: !u64i {{.*}}) -> (!s32i // LLVM: define dso_local noundef i32 @_Z14takeEmptyFirst10EmptyFirst(i64 %{{[^,]+}}) +// The empty member owns the second eightbyte alone, which classifies NoClass +// and is dropped rather than merged into a register of its own. +int takeEmptySecond(EmptySecond v) { return 0; } + +// CIR: cir.func {{.*}}@_Z15takeEmptySecond11EmptySecond(%arg0: !s64i {{.*}}) -> (!s32i +// LLVM: define dso_local noundef i32 @_Z15takeEmptySecond11EmptySecond(i64 %{{[^,]+}}) + +// The same with an SSE eightbyte below it. +double takeEmptySSE(EmptySSE v) { return v.a; } + +// CIR: cir.func {{.*}}@_Z12takeEmptySSE8EmptySSE(%arg0: !cir.double {{.*}}) -> (!cir.double +// LLVM: define dso_local noundef double @_Z12takeEmptySSE8EmptySSE(double %{{[^,]+}}) + +// Here the empty member shares eightbyte 0 with the float rather than owning +// one, so NoClass has to survive a merge against SSE instead of standing alone. +float takeFloatEmpty(FloatEmpty v) { return v.a; } + +// CIR: cir.func {{.*}}@_Z14takeFloatEmpty10FloatEmpty(%arg0: !cir.float {{.*}}) -> (!cir.float +// LLVM: define dso_local noundef float @_Z14takeFloatEmpty10FloatEmpty(float %{{[^,]+}}) + +// The empty member first pushes the float to offset 4, and the eightbyte +// covering both coerces to double. +float takeFloatEmptyFirst(FloatEmptyFirst v) { return v.a; } + +// CIR: cir.func {{.*}}@_Z19takeFloatEmptyFirst15FloatEmptyFirst(%arg0: !cir.double {{.*}}) -> (!cir.float +// LLVM: define dso_local noundef float @_Z19takeFloatEmptyFirst15FloatEmptyFirst(double %{{[^,]+}}) + // 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; } diff --git a/clang/test/CIR/CodeGen/record-type-metadata.cpp b/clang/test/CIR/CodeGen/record-type-metadata.cpp index 69b3b8742bd0c..8bc0ce148080c 100644 --- a/clang/test/CIR/CodeGen/record-type-metadata.cpp +++ b/clang/test/CIR/CodeGen/record-type-metadata.cpp @@ -14,45 +14,10 @@ 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" {data !s32i, data !s32i}> @@ -60,27 +25,8 @@ void takesNoMembers(NoMembers n) {} // CIR-DAG: !rec_Aligned = !cir.struct<"Aligned" {data !s32i, data !s32i, pad !cir.array<!u8i x 8>}> // CIR-DAG: !rec_NonTrivialDtor = !cir.struct<class "NonTrivialDtor" {data !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, 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> +// 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> diff --git a/clang/test/CIR/IR/invalid-record-layout.cir b/clang/test/CIR/IR/invalid-record-layout.cir index ab45f263008a4..61ebc9b37528c 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, is_empty = false>} + has_trivial_dtor = true, record_align = 0>} } { } @@ -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, is_empty = false>} + has_trivial_dtor = true, record_align = 3>} } { } 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 73153a9a596c3..22d8fb3d221b1 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 @@ -13,6 +13,13 @@ !rec_P = !cir.struct<"P" packed {data !s8i, data !s32i}> !rec_Ov = !cir.struct<"Ov" {data !s32i, pad !cir.array<!u8i x 12>}> !rec_E = !cir.struct<"E" {pad !u8i}> +!rec_EOver = !cir.struct<"EOver" {pad !cir.array<!u8i x 16>}> +!rec_PadByte = !cir.struct<"PadByte" {data !u8i, pad !cir.array<!u8i x 3>}> +!rec_PadByte2 = !cir.struct<"PadByte2" {data !u8i, pad !u8i}> +!rec_UPadByte = !cir.union<"UPadByte" {data !u8i}, padding = {!cir.array<!u8i x 3>}> +!rec_UEmptyOnly = !cir.union<"UEmptyOnly" {data !rec_E}> +!rec_UEmptyOver = !cir.union<"UEmptyOver" {data !rec_EOver, data !s32i}, padding = {!cir.array<!u8i x 12>}> +!rec_UArrEmpty = !cir.union<"UArrEmpty" {data !cir.array<!rec_E x 2>, data !s8i}> !rec_S1 = !cir.struct<"S1" {data !s16i, data !s16i, data !s16i}> !rec_AtomicWrapper = !cir.struct<{data !rec_S1, pad !cir.array<!s8i x 2>}> @@ -95,13 +102,52 @@ module attributes { // CHECK: not yet implemented for type '!cir.struct<"Ov" {data !cir.int<s, 32>, pad - // 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 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" {pad + // CHECK: not yet implemented for type '!cir.struct<"PadByte" {data + + // 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" {data + + // 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" + + // An array of empty records supplies no bytes the union coercion can read. + cir.func @take_union_arr_empty(%arg0: !rec_UArrEmpty) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.union<"UArrEmpty" // CIRGen wraps an atomic whose value is narrower than its lock-free size in // an anonymous record with a trailing pad member, which the padded check 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 index 9c1b094c49af3..6a0dddc601f5a 100644 --- a/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir +++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-empty-class.cir @@ -5,42 +5,44 @@ !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>}> +!rec_E = !cir.struct<"E" {pad !u8i}> +!rec_EBlob = !cir.struct<"EBlob" {pad !cir.array<!u8i x 3>}> +!rec_NTCE = !cir.struct<"NTCE" {pad !u8i}> +!rec_Byte = !cir.struct<"Byte" {data !u8i}> +!rec_HasEmpty = !cir.struct<"HasEmpty" {data !s32i, data !rec_E}> +!rec_HasEmptyFirst = !cir.struct<"HasEmptyFirst" {data !rec_E, data !s32i}> +!rec_EmptyMem = !cir.struct<"EmptyMem" {data !rec_E}> +!rec_Base2 = !cir.struct<"Base2" {data !s32i}> +!rec_D2 = !cir.struct<"D2" {data !rec_EmptyMem, data !rec_Base2}> +!rec_EMed = !cir.struct<"EMed" {pad !cir.array<!u8i x 16>}> +!rec_EBig = !cir.struct<"EBig" {pad !cir.array<!u8i x 32>}> +!rec_EBits = !cir.struct<"EBits" {empty !u8i}> +!rec_MixPE = !cir.struct<"MixPE" {pad !u8i, empty !u8i}> 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>, + record_align = 1>, EBlob = #cir.record_layout< arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true, - record_align = 1, is_empty = true>, + record_align = 1>, EmptyMem = #cir.record_layout< arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true, - record_align = 1, is_empty = false>, + record_align = 1>, Byte = #cir.record_layout< arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true, - record_align = 1, is_empty = false>, + record_align = 1>, NTCE = #cir.record_layout< arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = false, - record_align = 1, is_empty = true>, + record_align = 1>, EMed = #cir.record_layout< arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true, - record_align = 16, is_empty = true>, + record_align = 16>, EBig = #cir.record_layout< arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true, - record_align = 32, is_empty = true>}, + record_align = 32>}, dlti.dl_spec = #dlti.dl_spec< #dlti.dl_entry<i8, dense<8>: vector<2xi64>>, #dlti.dl_entry<i32, dense<32>: vector<2xi64>>, @@ -56,6 +58,24 @@ module attributes { // CHECK-NEXT: cir.return // LLVM: define void @take_empty() + // Unnamed bit-field storage is marked empty rather than pad, and a record of + // nothing else is dropped the same way. This spelling does not set the pad + // flag, so before the marks it was passed as a register of data. + cir.func @take_empty_bits(%arg0: !rec_EBits, %arg1: !s32i) -> !s32i { + cir.return %arg1 : !s32i + } + + // CHECK: cir.func{{.*}} @take_empty_bits(%arg0: !s32i) -> !s32i + // LLVM: define i32 @take_empty_bits(i32 %{{[^,)]+}}) + + // Emptiness is the absence of a data member, not the presence of a pad one. + cir.func @take_mixed_pad_empty(%arg0: !rec_MixPE, %arg1: !s32i) -> !s32i { + cir.return %arg1 : !s32i + } + + // CHECK: cir.func{{.*}} @take_mixed_pad_empty(%arg0: !s32i) -> !s32i + // LLVM: define i32 @take_mixed_pad_empty(i32 %{{[^,)]+}}) + // 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 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 08fe570be179f..95609d59b4f35 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, is_empty = false>}, + record_align = 4>}, 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 2c88e5b7a8c38..da0d9e20529a9 100644 --- a/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir +++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir @@ -27,16 +27,13 @@ module attributes { cir.record_layouts = { UNoRegs = #cir.record_layout< arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = false, - record_align = 4, is_empty = false>, + record_align = 4>, UBigOverAligned = #cir.record_layout< arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true, - record_align = 32, is_empty = false>, + record_align = 32>, SOverAligned = #cir.record_layout< arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true, - 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>}, + record_align = 32>}, 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 a434a297795d6..3e7b0d9054970 100644 --- a/clang/unittests/CIR/RecordTypeMetadataTest.cpp +++ b/clang/unittests/CIR/RecordTypeMetadataTest.cpp @@ -33,8 +33,7 @@ class RecordLayoutAttrTest : public ::testing::Test { TEST_F(RecordLayoutAttrTest, CanPassInRegs) { auto attr = - RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 4, - /*is_empty=*/false); + RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 4); EXPECT_EQ(attr.getArgPassingKind(), ArgPassingKind::CanPassInRegs); EXPECT_TRUE(attr.getHasTrivialDtor()); EXPECT_EQ(attr.getRecordAlign(), 4u); @@ -42,15 +41,14 @@ TEST_F(RecordLayoutAttrTest, CanPassInRegs) { TEST_F(RecordLayoutAttrTest, CannotPassInRegs) { auto attr = RecordLayoutAttr::get(&context, ArgPassingKind::CannotPassInRegs, - false, 4, /*is_empty=*/false); + false, 4); EXPECT_EQ(attr.getArgPassingKind(), ArgPassingKind::CannotPassInRegs); EXPECT_FALSE(attr.getHasTrivialDtor()); } TEST_F(RecordLayoutAttrTest, CanNeverPassInRegs) { auto attr = RecordLayoutAttr::get( - &context, ArgPassingKind::CanNeverPassInRegs, false, 8, - /*is_empty=*/false); + &context, ArgPassingKind::CanNeverPassInRegs, false, 8); EXPECT_EQ(attr.getArgPassingKind(), ArgPassingKind::CanNeverPassInRegs); EXPECT_FALSE(attr.getHasTrivialDtor()); EXPECT_EQ(attr.getRecordAlign(), 8u); @@ -58,20 +56,10 @@ TEST_F(RecordLayoutAttrTest, CanNeverPassInRegs) { TEST_F(RecordLayoutAttrTest, HighAlignment) { auto attr = - RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 32, - /*is_empty=*/false); + RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 32); 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); @@ -87,8 +75,7 @@ TEST_F(RecordLayoutAttrTest, ModuleLevelLookup) { auto module = mlir::ModuleOp::create(loc); auto layoutAttr = - RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 8, - /*is_empty=*/false); + RecordLayoutAttr::get(&context, ArgPassingKind::CanPassInRegs, true, 8); llvm::SmallVector<mlir::NamedAttribute> entries; entries.push_back(mlir::NamedAttribute(getName("TestRecord"), layoutAttr)); @@ -99,7 +86,6 @@ 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
