https://github.com/adams381 created https://github.com/llvm/llvm-project/pull/214129
Passing a union to a function does not work. The x86_64 bridge rejects every one, so the pass fails on any signature naming a union. Additionally, indirect arguments have their byval and sret alignment wrong, because `mapCIRType` asks DataLayout for it, and DataLayout only sees a record's members, never `__attribute__((aligned(N)))`. Unions now go through the ABI library's union type, which puts every member at offset zero and sizes each eightbyte from the union rather than from a single member. The alignment comes from the record-layout metadata the AST already fills in, which fixes over-aligned structs too, since they share that lookup. Assisted-by: Cursor / claude-opus-5 >From b4754df8f2ea91af527a938100ca854e49581375 Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Tue, 4 Aug 2026 20:50:26 -0700 Subject: [PATCH] [CIR] Accept unions in x86_64 calling-convention lowering Passing a union to a function does not work. The x86_64 bridge rejects every one, so the pass fails on any signature naming a union. Additionally, indirect arguments have their byval and sret alignment wrong, because `mapCIRType` asks DataLayout for it, and DataLayout only sees a record's members, never `__attribute__((aligned(N)))`. Unions now go through the ABI library's union type, which puts every member at offset zero and sizes each eightbyte from the union rather than from a single member. The alignment comes from the record-layout metadata the AST already fills in, which fixes over-aligned structs too, since they share that lookup. Assisted-by: Cursor / claude-opus-5 --- .../include/clang/CIR/Dialect/IR/CIRDialect.h | 5 + clang/lib/CIR/Dialect/IR/CIRAttrs.cpp | 16 +- .../Transforms/CallConvLoweringPass.cpp | 123 +++++--- .../CIR/CodeGen/call-conv-lowering-x86_64.c | 84 ++++++ .../abi-lowering/x86_64-aggregate-nyi.cir | 82 ++++- .../x86_64-union-coerce-shapes.cir | 80 +++++ .../Transforms/abi-lowering/x86_64-union.cir | 280 ++++++++++++++++++ 7 files changed, 617 insertions(+), 53 deletions(-) create mode 100644 clang/test/CIR/Transforms/abi-lowering/x86_64-union-coerce-shapes.cir create mode 100644 clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h index c6f6c80206bfe..8cd70bf47ce5a 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h +++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h @@ -84,6 +84,11 @@ class FenvOpTrait : public mlir::OpTrait::TraitBase<ConcreteType, FenvOpTrait> { /// Look up the RecordLayoutAttr for a named record in the module's /// cir.record_layouts dictionary. Asserts if the entry is missing. RecordLayoutAttr getRecordLayout(mlir::ModuleOp module, mlir::StringAttr name); + +/// Same lookup as getRecordLayout, but returns a null attribute instead of +/// asserting when the record has no layout entry. +RecordLayoutAttr tryGetRecordLayout(mlir::ModuleOp module, + mlir::StringAttr name); } // namespace cir // TableGen'erated files for MLIR dialects require that a macro be defined when diff --git a/clang/lib/CIR/Dialect/IR/CIRAttrs.cpp b/clang/lib/CIR/Dialect/IR/CIRAttrs.cpp index 264e836718c81..74a3deb5b2508 100644 --- a/clang/lib/CIR/Dialect/IR/CIRAttrs.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRAttrs.cpp @@ -920,12 +920,20 @@ LogicalResult DynamicCastInfoAttr::verify( // RecordLayout lookup //===----------------------------------------------------------------------===// -RecordLayoutAttr cir::getRecordLayout(mlir::ModuleOp module, - mlir::StringAttr name) { +RecordLayoutAttr cir::tryGetRecordLayout(mlir::ModuleOp module, + mlir::StringAttr name) { + if (!name) + return {}; auto dict = module->getAttrOfType<mlir::DictionaryAttr>( CIRDialect::getRecordLayoutsAttrName()); - assert(dict && "module missing cir.record_layouts attribute"); - auto attr = dict.getAs<RecordLayoutAttr>(name); + if (!dict) + return {}; + return dict.getAs<RecordLayoutAttr>(name); +} + +RecordLayoutAttr cir::getRecordLayout(mlir::ModuleOp module, + mlir::StringAttr name) { + RecordLayoutAttr attr = tryGetRecordLayout(module, name); assert(attr && "record layout entry missing for named record"); return attr; } diff --git a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp index 54c4487db6195..1baa145251bc9 100644 --- a/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp +++ b/clang/lib/CIR/Dialect/Transforms/CallConvLoweringPass.cpp @@ -68,10 +68,11 @@ namespace { // SysV x86_64 classifier, and converts the result back into the // dialect-agnostic mlir::abi::FunctionClassification that CIRABIRewriteContext // consumes. Integer (including `_BitInt` up to 128 bits) / pointer / bool / -// f32 / f64 scalars and struct / array aggregates are handled. Unions, -// `_Complex`, vectors, wider floats, and packed or padded records are reported -// NYI by classifyX86_64Function so an unsupported signature fails the pass -// instead of being misclassified. +// 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 +// 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 @@ -79,27 +80,32 @@ namespace { /// no layout entry (e.g. an anonymous struct) has no C++ non-trivial reason to /// be forced to memory, so it defaults to can-pass-in-registers. static bool recordCanPassInRegs(ModuleOp modOp, cir::RecordType recTy) { - mlir::StringAttr name = recTy.getName(); - if (!name) - return true; - auto dict = modOp->getAttrOfType<DictionaryAttr>( - cir::CIRDialect::getRecordLayoutsAttrName()); - if (!dict) - return true; - auto layout = dict.getAs<cir::RecordLayoutAttr>(name); + auto layout = cir::tryGetRecordLayout(modOp, recTy.getName()); if (!layout) return true; return layout.getArgPassingKind() == cir::ArgPassingKind::CanPassInRegs; } +/// 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 +/// comes from the module's record-layout metadata instead. CIRGen emits an +/// entry for every record it names, so the computed fallback only serves +/// hand-written CIR. +static llvm::Align recordDeclaredAlign(ModuleOp modOp, cir::RecordType recTy, + const DataLayout &dl) { + auto layout = cir::tryGetRecordLayout(modOp, recTy.getName()); + if (!layout) + return llvm::Align(dl.getTypeABIAlignment(recTy)); + return llvm::Align(layout.getRecordAlign()); +} + /// The CIR types the x86_64 bridge handles. Scalars: an integer up to 128 /// bits (including `_BitInt` and `__int128`), pointer, bool, void, f32, or f64. -/// Aggregates: a complete struct whose fields are all themselves supported, or -/// an array of a supported element type. A `_BitInt` wider than 128 bits, -/// unions, `_Complex`, vectors, wider floats, and packed or padded records are -/// not handled and are reported NYI at the reject() choke point in -/// classifyX86_64Function. -static bool isSupportedType(mlir::Type ty) { +/// 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) { // 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. @@ -124,21 +130,40 @@ static bool isSupportedType(mlir::Type ty) { return intTy.getWidth() <= 64 || intTy.getWidth() == 128; } if (auto arrTy = dyn_cast<cir::ArrayType>(ty)) - return isSupportedType(arrTy.getElementType()); + return isSupportedType(arrTy.getElementType(), dl); if (auto recTy = dyn_cast<cir::RecordType>(ty)) { - // Unions and packed / padded records each need classification this bridge - // does not implement (a union widen fixup and pad-aware eightbyte - // classification), so reject them here and report NYI rather than - // misclassify. A zero-field record (a C empty struct) classifies as - // Ignore and is dropped from the lowered signature. CIRGen lays out an - // empty C++ class as a single padded byte, which the padded check rejects. - // A real one-byte struct such as `{char[1]}` has a field and is not - // padded, so it is classified normally. - if (recTy.isUnion() || !recTy.isComplete() || recTy.getPacked() || - recTy.getPadded()) + // An incomplete record has no layout to classify, and a packed one needs + // pad-aware eightbyte classification this bridge does not implement. + if (!recTy.isComplete() || recTy.getPacked()) return false; + if (recTy.isUnion()) { + // The classifier sizes a union's eightbytes from the union itself, which + // is only sound when some member spans that size. Short of that, the + // remaining bytes are either tail padding or the rest of a bitfield + // storage unit, and the CIR type cannot tell those apart even though + // classic CodeGen coerces them to i32 and i8 respectively. + llvm::ArrayRef<mlir::Type> members = recTy.getMembers(); + uint64_t recordBits = dl.getTypeSizeInBits(recTy).getFixedValue(); + if (members.empty()) { + // A member-less union is all padding, which classifies Ignore up to two + // eightbytes. Past that SysV says MEMORY regardless of content, and + // there is no member here to build the Indirect coercion from. + if (recordBits > 128) + return false; + } else { + auto spansRecord = [&](mlir::Type m) { + return dl.getTypeSizeInBits(m).getFixedValue() == recordBits; + }; + if (!llvm::any_of(members, spansRecord)) + 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. + return false; + } return llvm::all_of(recTy.getMembers(), - [](mlir::Type m) { return isSupportedType(m); }); + [&](mlir::Type m) { return isSupportedType(m, dl); }); } return false; } @@ -220,11 +245,28 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type, dl.getTypeSizeInBits(type).getFixedValue()); }) .Case([&](cir::RecordType recTy) -> const llvm::abi::Type * { - // isSupportedType rejects unions, packed / padded, and empty-for-ABI - // records, so this handles a plain struct: map each field at its - // naturally-aligned offset. + llvm::abi::RecordFlags flags = llvm::abi::RecordFlags::None; + if (recordCanPassInRegs(modOp, recTy)) + flags = flags | llvm::abi::RecordFlags::CanPassInRegisters; + llvm::TypeSize sizeBits = llvm::TypeSize::getFixed( + dl.getTypeSizeInBits(type).getFixedValue()); + llvm::Align align = recordDeclaredAlign(modOp, recTy, dl); SmallVector<llvm::abi::FieldInfo> fields; fields.reserve(recTy.getMembers().size()); + + // The size passed here spans the tail padding, so an eightbyte covers + // the whole union rather than just the member the classifier reduces + // it to. + if (recTy.isUnion()) { + for (mlir::Type fieldTy : recTy.getMembers()) + fields.push_back(llvm::abi::FieldInfo( + mapCIRType(fieldTy, typeMapper, dl, modOp))); + return tb.getUnionType(fields, sizeBits, align, + llvm::abi::StructPacking::Default, flags); + } + + // isSupportedType rejects packed and padded structs, 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 = @@ -234,16 +276,9 @@ static const llvm::abi::Type *mapCIRType(mlir::Type type, fields.push_back(llvm::abi::FieldInfo(mappedField, offsetBits)); offsetBits += dl.getTypeSizeInBits(fieldTy).getFixedValue(); } - llvm::abi::RecordFlags flags = llvm::abi::RecordFlags::None; - if (recordCanPassInRegs(modOp, recTy)) - flags = flags | llvm::abi::RecordFlags::CanPassInRegisters; - return tb.getRecordType(fields, - llvm::TypeSize::getFixed( - dl.getTypeSizeInBits(type).getFixedValue()), - llvm::Align(dl.getTypeABIAlignment(type)), - llvm::abi::StructPacking::Default, - /*BaseClasses=*/{}, /*VirtualBaseClasses=*/{}, - flags); + return tb.getRecordType( + fields, sizeBits, align, llvm::abi::StructPacking::Default, + /*BaseClasses=*/{}, /*VirtualBaseClasses=*/{}, flags); }) .Default([](mlir::Type) -> const llvm::abi::Type * { llvm_unreachable( @@ -388,7 +423,7 @@ static std::optional<FunctionClassification> classifyX86_64Signature( bool voidRet = isa<cir::VoidType>(retCIR); auto reject = [&](mlir::Type t) -> bool { - if (isSupportedType(t)) + if (isSupportedType(t, dl)) 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.c b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c index 6931198a90c3d..8382b15bb5d9e 100644 --- a/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c +++ b/clang/test/CIR/CodeGen/call-conv-lowering-x86_64.c @@ -11,6 +11,11 @@ typedef struct { long a, b, c, d; } Big; typedef struct { long a; double b; } IntSSE; typedef struct { double a; double b; } SSE2; typedef struct { } Empty; +typedef union { int i; float f; } UIntFloat; +typedef union { float f; float g; } UFloats; +typedef union { int i; char c[8]; } UNarrowStorage; +typedef union { char c[32]; } UBig; +typedef union { char c[32]; } __attribute__((aligned(32))) UBigOverAligned; // Narrow signed integer sign-extended in a register. signed char ext_schar(signed char c) { return c; } @@ -79,3 +84,82 @@ void take_big(Big b) { (void)b; } // CIR: cir.func {{.*}}@take_big(%arg0: !cir.ptr<!rec_Big> {{.*}}llvm.byval = !rec_Big{{.*}}) // LLVM-CIR: define dso_local void @take_big(ptr noalias noundef byval(%struct.Big) align 8 %{{.+}}) // LLVM-OGCG: define dso_local void @take_big(ptr noundef byval(%struct.Big) align 8 %{{.+}}) + +// Union members all start at offset zero, so a 4-byte union takes one INTEGER +// eightbyte and coerces to i32. +void take_union(UIntFloat u) { (void)u; } + +// CIR: cir.func {{.*}}@take_union(%arg0: !s32i{{.*}}) +// LLVM: define dso_local void @take_union(i32 %{{.+}}) + +// A union of floats classifies SSE, so it coerces to a float register. +void take_union_floats(UFloats u) { (void)u; } + +// CIR: cir.func {{.*}}@take_union_floats(%arg0: !cir.float{{.*}}) +// LLVM: define dso_local void @take_union_floats(float %{{.+}}) + +// The union's highest-aligned member is the 4-byte int, but its size comes +// from the 8-byte array, and the eightbyte is sized from the union. +void take_union_narrow_storage(UNarrowStorage u) { (void)u; } + +// CIR: cir.func {{.*}}@take_union_narrow_storage(%arg0: !u64i{{.*}}) +// LLVM: define dso_local void @take_union_narrow_storage(i64 %{{.+}}) + +// A coerced union return round-trips through the coercion type. +UIntFloat ret_union(int a) { UIntFloat u; u.i = a; return u; } + +// CIR: cir.func {{.*}}@ret_union(%arg0: !s32i {{.*}}) -> !s32i +// LLVM: define dso_local i32 @ret_union(i32 noundef %{{.+}}) + +// A union too large for registers is passed byval, with the same noalias +// divergence as a large struct. +void take_union_big(UBig u) { (void)u; } + +// CIR: cir.func {{.*}}@take_union_big(%arg0: !cir.ptr<!rec_UBig> {{.*}}llvm.byval = !rec_UBig{{.*}}) +// LLVM-CIR: define dso_local void @take_union_big(ptr noalias noundef byval(%union.UBig) align 8 %{{.+}}) +// LLVM-OGCG: define dso_local void @take_union_big(ptr noundef byval(%union.UBig) align 8 %{{.+}}) + +// The byval alignment follows the union's declared alignment, not the alignment +// its members imply, which is 1 here. +void take_union_big_over_aligned(UBigOverAligned u) { (void)u; } + +// CIR: cir.func {{.*}}@take_union_big_over_aligned(%arg0: !cir.ptr<!rec_UBigOverAligned> {{.*}}llvm.align = 32 : i64{{.*}}llvm.byval = !rec_UBigOverAligned{{.*}}) +// LLVM-CIR: define dso_local void @take_union_big_over_aligned(ptr noalias noundef byval(%union.UBigOverAligned) align 32 %{{.+}}) +// LLVM-OGCG: define dso_local void @take_union_big_over_aligned(ptr noundef byval(%union.UBigOverAligned) align 32 %{{.+}}) + +void call_union(UIntFloat u) { take_union(u); } + +// CIR: cir.func {{.*}}@call_union(%arg0: !s32i +// CIR: cir.call @take_union(%{{.+}}) : (!s32i) -> () +// LLVM: define dso_local void @call_union(i32 %{{.+}}) +// LLVM: call void @take_union(i32 %{{.+}}) + +void call_union_big_over_aligned(UBigOverAligned u) { + take_union_big_over_aligned(u); +} + +// CIR: cir.func {{.*}}@call_union_big_over_aligned(%arg0: !cir.ptr<!rec_UBigOverAligned> {{.*}}llvm.align = 32 : i64{{.*}}) +// CIR: cir.call @take_union_big_over_aligned(%{{.+}}) : (!cir.ptr<!rec_UBigOverAligned> {{.*}}llvm.align = 32 : i64{{.*}}) -> () +// LLVM-CIR: define dso_local void @call_union_big_over_aligned(ptr noalias noundef byval(%union.UBigOverAligned) align 32 %{{.+}}) +// LLVM-CIR: alloca %union.UBigOverAligned, i64 1, align 32 +// LLVM-CIR: call void @take_union_big_over_aligned(ptr noalias noundef byval(%union.UBigOverAligned) align 32 %{{.+}}) +// LLVM-OGCG: define dso_local void @call_union_big_over_aligned(ptr noundef byval(%union.UBigOverAligned) align 32 %{{.+}}) +// LLVM-OGCG: call void @take_union_big_over_aligned(ptr noundef byval(%union.UBigOverAligned) align 32 %{{.+}}) + +// The declared alignment reaches the sret slot of an indirect return too, not +// just a byval argument. +UBigOverAligned ret_union_big_over_aligned(void); +void call_ret_union_big_over_aligned(void) { (void)ret_union_big_over_aligned(); } + +// CIR: cir.func {{.*}}@ret_union_big_over_aligned(!cir.ptr<!rec_UBigOverAligned> {{.*}}llvm.align = 32 : i64{{.*}}llvm.sret = !rec_UBigOverAligned{{.*}}) +// LLVM: declare void @ret_union_big_over_aligned(ptr dead_on_unwind writable sret(%union.UBigOverAligned) align 32) + +// The same declared-alignment source feeds an over-aligned struct, since +// mapCIRType's alignment lookup is on the shared record path, not a +// union-specific one. +typedef struct { char c[32]; } __attribute__((aligned(32))) SOverAligned; +void take_struct_over_aligned(SOverAligned s) { (void)s; } + +// CIR: cir.func {{.*}}@take_struct_over_aligned(%arg0: !cir.ptr<!rec_SOverAligned> {{.*}}llvm.align = 32 : i64{{.*}}llvm.byval = !rec_SOverAligned{{.*}}) +// LLVM-CIR: define dso_local void @take_struct_over_aligned(ptr noalias noundef byval(%struct.SOverAligned) align 32 %{{.+}}) +// LLVM-OGCG: define dso_local void @take_struct_over_aligned(ptr noundef byval(%struct.SOverAligned) align 32 %{{.+}}) 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 85f05fef9f96b..83d36ce22b970 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 @@ -1,10 +1,17 @@ // RUN: not cir-opt %s -cir-call-conv-lowering=target=x86_64 2>&1 | FileCheck %s !s8i = !cir.int<s, 8> +!s16i = !cir.int<s, 16> !s32i = !cir.int<s, 32> !u8i = !cir.int<u, 8> -!u32i = !cir.int<u, 32> -!rec_U = !cir.union<"U" {!s32i, !u32i}> +!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>}> +!rec_UOverAligned = !cir.union<"UOverAligned" {!s32i}, padding = {!cir.array<!u8i x 12>}> +!rec_UShortStorage = !cir.union<"UShortStorage" {!s16i, !cir.array<!s8i x 3>}, padding = {!cir.array<!u8i x 2>}> +!rec_UByteBlobs = !cir.union<"UByteBlobs" {!u8i, !u8i}, padding = {!cir.array<!u8i x 3>}> +!rec_SWrapsOverAligned = !cir.struct<"SWrapsOverAligned" {!cir.double, !rec_UOverAligned}> +!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_E = !cir.struct<"E" padded {!u8i}> @@ -19,12 +26,77 @@ module attributes { #dlti.dl_entry<f64, dense<64>: vector<2xi64>>> } { - // A union is rejected: its register coercion needs a widen fixup. - cir.func @take_union(%arg0: !rec_U) { + // A packed union is rejected for the same reason a packed struct is: its + // members no longer sit at their natural alignment. + cir.func @take_packed_union(%arg0: !rec_UPacked) { cir.return } - // CHECK: not yet implemented for type '!cir.union<"U" + // CHECK: not yet implemented for type '!cir.union<"UPacked" packed + + // A union member the bridge does not map keeps the whole union unsupported. + cir.func @take_union_long_double(%arg0: !rec_ULongDouble) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.union<"ULongDouble" + + // A union whose highest-aligned member is an all-float array classifies to + // an SSE vector coerce this bridge does not represent, so it is reported NYI + // rather than passed unchanged. + cir.func @take_union_float_arrays(%arg0: !rec_UFloats) { + cir.return + } + + // CHECK: not yet implemented for the ABI coercion of type '!cir.union<"UFloats" + + // No member of this union spans its 16-byte declared size, so the bytes past + // the int cannot be told apart from the rest of a wider storage unit, and the + // eightbyte the classifier would build from the union's size is a guess. + cir.func @take_over_aligned_union(%arg0: !rec_UOverAligned) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.union<"UOverAligned" + + // Same rule one eightbyte down: the widest member covers 3 of the union's 4 + // declared bytes. + cir.func @take_short_storage_union(%arg0: !rec_UShortStorage) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.union<"UShortStorage" + + // This is the shape CIRGen produces for a union of narrow bitfields, where + // the byte-sized members understate a wider storage unit that is all user + // data. It is also the shape of a union of two `unsigned char` members + // carrying an alignment attribute, where the same bytes really are padding. + // The two coerce differently in classic CodeGen and are identical here, which + // is why neither is accepted. + cir.func @take_byte_blob_union(%arg0: !rec_UByteBlobs) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.union<"UByteBlobs" + + // The reject propagates out of an enclosing struct rather than being silently + // dropped at the member level. + cir.func @take_struct_wrapping_over_aligned(%arg0: !rec_SWrapsOverAligned) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.struct<"SWrapsOverAligned" + + // A member-less union past two eightbytes still classifies Indirect in + // classic CodeGen (SysV's MEMORY rule applies unconditionally above that + // size, regardless of content), but this bridge has no member to build an + // Indirect coercion from. Below the threshold the same shape is accepted + // and classifies Ignore, matching classic; see x86_64-union.cir take_empty. + cir.func @take_empty_large_union(%arg0: !rec_UEmptyLarge) { + cir.return + } + + // CHECK: not yet implemented for type '!cir.union<"UEmptyLarge" // A packed struct is rejected: it needs pad-aware classification. cir.func @take_packed(%arg0: !rec_P) { diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-union-coerce-shapes.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-union-coerce-shapes.cir new file mode 100644 index 0000000000000..e7cc09d739de1 --- /dev/null +++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-union-coerce-shapes.cir @@ -0,0 +1,80 @@ +// 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 + +!s8i = !cir.int<s, 8> +!u8i = !cir.int<u, 8> +!s32i = !cir.int<s, 32> +!s64i = !cir.int<s, 64> +!s128i = !cir.int<s, 128> +!rec_U128 = !cir.union<"U128" {!s128i, !cir.array<!s8i x 16>}> +!rec_UMixed = !cir.union<"UMixed" {!cir.array<!cir.double x 2>, !s64i}> +!rec_UD2 = !cir.union<"UD2" {!cir.array<!cir.double x 2>}> +!rec_U12 = !cir.union<"U12" {!cir.array<!s8i x 12>, !s32i}, padding = {!cir.array<!u8i x 8>}> +!rec_UEmpty16 = !cir.union<"UEmpty16" {}, padding = {!cir.array<!u8i x 16>}> + +module attributes { + cir.triple = "x86_64-unknown-linux-gnu", + dlti.dl_spec = #dlti.dl_spec< + #dlti.dl_entry<i8, dense<8>: vector<2xi64>>, + #dlti.dl_entry<i64, dense<64>: vector<2xi64>>, + #dlti.dl_entry<i128, dense<128>: vector<2xi64>>, + #dlti.dl_entry<f64, dense<64>: vector<2xi64>>> +} { + + // A 16-byte union whose spanning member is one 128-bit integer coerces to that + // integer rather than to a pair of eightbytes. + cir.func @take_u128(%arg0: !rec_U128) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_u128(%arg0: !s128i) + + // The two eightbytes need not share a class. The first covers half of the + // double array and merges to INTEGER against the long, while the second is all + // double and stays SSE. + cir.func @take_umixed(%arg0: !rec_UMixed) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_umixed(%arg0: !u64i, %arg1: !cir.double) + + // With no integer member to merge against, both eightbytes stay SSE. + cir.func @take_ud2(%arg0: !rec_UD2) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_ud2(%arg0: !cir.double, %arg1: !cir.double) + + // The second eightbyte is partial. The classifier reduces this union to its + // 4-byte integer, so an eightbyte sized from that member would cover only the + // first four bytes and drop the rest of the array. + cir.func @take_u12(%arg0: !rec_U12) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_u12(%arg0: !u64i, %arg1: !u32i) + + // Two eightbytes in return position, flattened to an anonymous struct. + cir.func @ret_umixed(%arg0: !rec_UMixed) -> !rec_UMixed { + cir.return %arg0 : !rec_UMixed + } + + // CHECK: cir.func{{.*}} @ret_umixed(%arg0: !u64i, %arg1: !cir.double) -> !rec_anon_struct1 + + // A member-less union at exactly two eightbytes still classifies Ignore. One + // byte more is MEMORY, which x86_64-aggregate-nyi.cir covers. + cir.func @take_empty16(%arg0: !rec_UEmpty16) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_empty16() +} + +// LLVM: define void @take_u128(i128 %{{.+}}) +// LLVM: define void @take_umixed(i64 %{{.+}}, double %{{.+}}) +// LLVM: define void @take_ud2(double %{{.+}}, double %{{.+}}) +// LLVM: define void @take_u12(i64 %{{.+}}, i32 %{{.+}}) +// LLVM: define { i64, double } @ret_umixed(i64 %{{.+}}, double %{{.+}}) +// LLVM: define void @take_empty16() diff --git a/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir b/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir new file mode 100644 index 0000000000000..cad6cfd37d7a4 --- /dev/null +++ b/clang/test/CIR/Transforms/abi-lowering/x86_64-union.cir @@ -0,0 +1,280 @@ +// 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 + +!s8i = !cir.int<s, 8> +!s32i = !cir.int<s, 32> +!s64i = !cir.int<s, 64> +!u8i = !cir.int<u, 8> +!rec_UIntFloat = !cir.union<"UIntFloat" {!s32i, !cir.float}> +!rec_UFloatInt = !cir.union<"UFloatInt" {!cir.float, !s32i}> +!rec_ULongDouble = !cir.union<"ULongDouble" {!s64i, !cir.double}> +!rec_UDoubleLong = !cir.union<"UDoubleLong" {!cir.double, !s64i}> +!rec_UFloats = !cir.union<"UFloats" {!cir.float, !cir.float}> +!rec_UThree = !cir.union<"UThree" {!cir.array<!s8i x 3>}> +!rec_UNarrowStorage = !cir.union<"UNarrowStorage" {!s32i, !cir.array<!s8i x 8>}, padding = {!cir.array<!u8i x 4>}> +!rec_UTwoEightbytes = !cir.union<"UTwoEightbytes" {!s64i, !cir.array<!s8i x 16>}, padding = {!cir.array<!u8i x 8>}> +!rec_UBig = !cir.union<"UBig" {!cir.array<!s8i x 32>}> +!rec_UBigOverAligned = !cir.union<"UBigOverAligned" {!cir.array<!s8i x 32>}> +!rec_SOverAligned = !cir.struct<"SOverAligned" {!cir.array<!s8i x 32>}> +!rec_UEmpty = !cir.union<"UEmpty" {}, padding = {!u8i}> +!rec_UNoRegs = !cir.union<"UNoRegs" {!s32i, !cir.float}> +!rec_SWithUnion = !cir.struct<"SWithUnion" {!rec_UIntFloat, !s32i}> + +module attributes { + cir.triple = "x86_64-unknown-linux-gnu", + cir.record_layouts = { + UNoRegs = #cir.record_layout< + arg_passing_kind = cannot_pass_in_regs, has_trivial_dtor = false, + record_align = 4>, + UBigOverAligned = #cir.record_layout< + arg_passing_kind = can_pass_in_regs, has_trivial_dtor = true, + record_align = 32>, + SOverAligned = #cir.record_layout< + arg_passing_kind = can_pass_in_regs, has_trivial_dtor = 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>>, + #dlti.dl_entry<i32, dense<32>: vector<2xi64>>, + #dlti.dl_entry<i64, dense<64>: vector<2xi64>>, + #dlti.dl_entry<f32, dense<32>: vector<2xi64>>, + #dlti.dl_entry<f64, dense<64>: vector<2xi64>>> +} { + + // Every union member sits at offset zero, so a 4-byte union of an int and a + // float classifies INTEGER on its single eightbyte and coerces to i32. + cir.func @take_int_float(%arg0: !rec_UIntFloat) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_int_float(%arg0: !s32i) + // CHECK: %[[SLOT:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!s32i> + // CHECK: cir.store %arg0, %[[SLOT]] : !s32i, !cir.ptr<!s32i> + // CHECK: %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!s32i> -> !cir.ptr<!rec_UIntFloat> + // CHECK: %{{.*}} = cir.load %[[CAST]] : !cir.ptr<!rec_UIntFloat>, !rec_UIntFloat + + // Same members as take_int_float in the opposite declaration order. The + // classifier merges eightbyte classes across every member (INTEGER beats + // SSE), so which member is listed first does not change the class: this + // still coerces to a 32-bit integer, not to the float that comes first. + // The coercion type is unsigned where take_int_float's is signed. On a tie + // for widest member the classifier's reduction keeps the first field (float + // here), and resolving an integer coercion from a float storage type falls + // through to a byte-size fallback that is always unsigned. That difference + // is confined to CIR: LLVM integers have no signedness, so both lower to + // i32, which the LLVM checks at the end of this file pin. + cir.func @take_float_int(%arg0: !rec_UFloatInt) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_float_int(%arg0: !u32i) + // CHECK: %[[CAST:.*]] = cir.cast bitcast %{{.*}} : !cir.ptr<!u32i> -> !cir.ptr<!rec_UFloatInt> + + // An 8-byte union fills its eightbyte and coerces to i64. + cir.func @take_long_double(%arg0: !rec_ULongDouble) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_long_double(%arg0: !s64i) + // CHECK: %[[SLOT:.*]] = cir.alloca "coerce" align(8) : !cir.ptr<!s64i> + // CHECK: %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!s64i> -> !cir.ptr<!rec_ULongDouble> + + // Same class-merge point at 8 bytes: double-first still coerces to a + // 64-bit integer, unsigned for the same reduction-tie reason as + // take_float_int above. + cir.func @take_double_long(%arg0: !rec_UDoubleLong) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_double_long(%arg0: !u64i) + // CHECK: %[[CAST:.*]] = cir.cast bitcast %{{.*}} : !cir.ptr<!u64i> -> !cir.ptr<!rec_UDoubleLong> + + // A union of floats classifies SSE, so the coercion is a float register + // rather than an integer one. + cir.func @take_floats(%arg0: !rec_UFloats) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_floats(%arg0: !cir.float) + // CHECK: %[[SLOT:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!cir.float> + // CHECK: %[[CAST:.*]] = cir.cast bitcast %[[SLOT]] : !cir.ptr<!cir.float> -> !cir.ptr<!rec_UFloats> + + // A 3-byte union coerces to the i24 that spans it. + cir.func @take_three(%arg0: !rec_UThree) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_three(%arg0: !cir.int<u, 24>) + // CHECK: %[[CAST:.*]] = cir.cast bitcast %{{.*}} : !cir.ptr<!cir.int<u, 24>> -> !cir.ptr<!rec_UThree> + + // The highest-aligned member (the int) is narrower than the union, whose + // 8-byte size comes from the char array. The eightbyte is sized from the + // union, not from that member, so this coerces to i64 rather than i32. + cir.func @take_narrow_storage(%arg0: !rec_UNarrowStorage) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_narrow_storage(%arg0: !u64i) + // CHECK: %[[CAST:.*]] = cir.cast bitcast %{{.*}} : !cir.ptr<!u64i> -> !cir.ptr<!rec_UNarrowStorage> + + // A 16-byte union is two INTEGER eightbytes, flattened into one argument per + // eightbyte. + cir.func @take_two_eightbytes(%arg0: !rec_UTwoEightbytes) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_two_eightbytes(%arg0: !s64i, %arg1: !u64i) + // CHECK: cir.alloca "coerce" align(8) : !cir.ptr<!rec_anon_struct> + // CHECK: %[[FLAT:.*]] = cir.alloca "coerce" align(8) : !cir.ptr<!rec_anon_struct> + // CHECK: %[[E0:.*]] = cir.get_member %[[FLAT]][0] {{.*}} : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!s64i> + // CHECK: cir.store %arg0, %[[E0]] : !s64i, !cir.ptr<!s64i> + // CHECK: %[[E1:.*]] = cir.get_member %[[FLAT]][1] {{.*}} : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i> + // CHECK: cir.store %arg1, %[[E1]] : !u64i, !cir.ptr<!u64i> + // CHECK: %{{.*}} = cir.cast bitcast %{{.*}} : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!rec_UTwoEightbytes> + + // A union too large for registers is passed byval. + cir.func @take_big(%arg0: !rec_UBig) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_big(%arg0: !cir.ptr<!rec_UBig> {llvm.align = 8 : i64, llvm.byval = !rec_UBig, llvm.noalias, llvm.noundef}) + // CHECK: %{{.*}} = cir.load %arg0 : !cir.ptr<!rec_UBig>, !rec_UBig + + // The byval alignment comes from the record's declared alignment, which the + // layout metadata carries because the members alone cannot express an + // alignment attribute. Same members as take_big, alignment 32 rather than 8. + cir.func @take_big_over_aligned(%arg0: !rec_UBigOverAligned) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_big_over_aligned(%arg0: !cir.ptr<!rec_UBigOverAligned> {llvm.align = 32 : i64, llvm.byval = !rec_UBigOverAligned, llvm.noalias, llvm.noundef}) + + // The same declared-alignment source feeds every accepted record, not just + // unions: an over-aligned STRUCT gets the same byval alignment fix, since + // mapCIRType's alignment lookup is on the shared record path. + cir.func @take_struct_over_aligned(%arg0: !rec_SOverAligned) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_struct_over_aligned(%arg0: !cir.ptr<!rec_SOverAligned> {llvm.align = 32 : i64, llvm.byval = !rec_SOverAligned, llvm.noalias, llvm.noundef}) + + // A union with no members classifies Ignore and is dropped from the + // signature. + cir.func @take_empty(%arg0: !rec_UEmpty) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_empty() + + // An empty-union return is dropped too: the function returns void while its + // local storage slot survives. + cir.func @ret_empty() -> !rec_UEmpty { + %0 = cir.alloca "u" align(1) : !cir.ptr<!rec_UEmpty> + %1 = cir.load %0 : !cir.ptr<!rec_UEmpty>, !rec_UEmpty + cir.return %1 : !rec_UEmpty + } + + // CHECK: cir.func{{.*}} @ret_empty() + // CHECK: cir.alloca "u" align(1) : !cir.ptr<!rec_UEmpty> + // CHECK: cir.return{{$}} + + // A union the record layout marks as unable to pass in registers goes + // indirect without byval, however small it is. + cir.func @take_no_regs(%arg0: !rec_UNoRegs) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_no_regs(%arg0: !cir.ptr<!rec_UNoRegs> {llvm.align = 4 : i64, llvm.byref = !rec_UNoRegs}) + + // A struct member that is itself a union is mapped through the same union + // handling, so the enclosing 8-byte struct coerces to one i64. + cir.func @take_struct_with_union(%arg0: !rec_SWithUnion) { + cir.return + } + + // CHECK: cir.func{{.*}} @take_struct_with_union(%arg0: !u64i) + // CHECK: %[[CAST:.*]] = cir.cast bitcast %{{.*}} : !cir.ptr<!u64i> -> !cir.ptr<!rec_SWithUnion> + + // A coerced union return round-trips through the coercion type. + cir.func @ret_int_float(%arg0: !rec_UIntFloat) -> !rec_UIntFloat { + %0 = cir.alloca "u" align(4) : !cir.ptr<!rec_UIntFloat> + cir.store %arg0, %0 : !rec_UIntFloat, !cir.ptr<!rec_UIntFloat> + %1 = cir.load %0 : !cir.ptr<!rec_UIntFloat>, !rec_UIntFloat + cir.return %1 : !rec_UIntFloat + } + + // CHECK: cir.func{{.*}} @ret_int_float(%arg0: !s32i) -> !s32i + // CHECK: %[[RETSLOT:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!rec_UIntFloat> + // CHECK: cir.store %{{.*}}, %[[RETSLOT]] : !rec_UIntFloat, !cir.ptr<!rec_UIntFloat> + // CHECK: %[[RETCAST:.*]] = cir.cast bitcast %[[RETSLOT]] : !cir.ptr<!rec_UIntFloat> -> !cir.ptr<!s32i> + // CHECK: %[[RET:.*]] = cir.load %[[RETCAST]] : !cir.ptr<!s32i>, !s32i + // CHECK: cir.return %[[RET]] : !s32i + + // A union return too large for registers uses the caller's sret slot. + cir.func @ret_big(%arg0: !rec_UBig) -> !rec_UBig { + %0 = cir.alloca "u" align(1) : !cir.ptr<!rec_UBig> + cir.store %arg0, %0 : !rec_UBig, !cir.ptr<!rec_UBig> + %1 = cir.load %0 : !cir.ptr<!rec_UBig>, !rec_UBig + cir.return %1 : !rec_UBig + } + + // CHECK: cir.func{{.*}} @ret_big(%arg0: !cir.ptr<!rec_UBig> {llvm.align = 1 : i64, llvm.dead_on_unwind, llvm.noalias, llvm.sret = !rec_UBig, llvm.writable}, %arg1: !cir.ptr<!rec_UBig> {llvm.align = 8 : i64, llvm.byval = !rec_UBig, llvm.noalias, llvm.noundef}) + // CHECK: %[[VAL:.*]] = cir.load %arg1 : !cir.ptr<!rec_UBig>, !rec_UBig + // CHECK: cir.store %[[VAL]], %arg0 : !rec_UBig, !cir.ptr<!rec_UBig> + + // The call site coerces the union argument the same way the callee expects + // it. + cir.func @call_int_float(%arg0: !rec_UIntFloat) { + cir.call @take_int_float(%arg0) : (!rec_UIntFloat) -> () + cir.return + } + + // CHECK: cir.func{{.*}} @call_int_float(%arg0: !s32i) + // CHECK: %[[ARGSLOT:.*]] = cir.alloca "coerce" align(4) : !cir.ptr<!rec_UIntFloat> + // CHECK: cir.store %{{.*}}, %[[ARGSLOT]] : !rec_UIntFloat, !cir.ptr<!rec_UIntFloat> + // CHECK: %[[ARGCAST:.*]] = cir.cast bitcast %[[ARGSLOT]] : !cir.ptr<!rec_UIntFloat> -> !cir.ptr<!s32i> + // CHECK: %[[ARG:.*]] = cir.load %[[ARGCAST]] : !cir.ptr<!s32i>, !s32i + // CHECK: cir.call @take_int_float(%[[ARG]]) : (!s32i) -> () + + // Both eightbytes are decomposed at the call site. + cir.func @call_two_eightbytes(%arg0: !rec_UTwoEightbytes) { + cir.call @take_two_eightbytes(%arg0) : (!rec_UTwoEightbytes) -> () + cir.return + } + + // CHECK: cir.func{{.*}} @call_two_eightbytes(%arg0: !s64i, %arg1: !u64i) + // CHECK: %[[CALLSLOT:.*]] = cir.alloca "coerce" align(8) : !cir.ptr<!rec_UTwoEightbytes> + // CHECK: %[[CALLCAST:.*]] = cir.cast bitcast %[[CALLSLOT]] : !cir.ptr<!rec_UTwoEightbytes> -> !cir.ptr<!rec_anon_struct> + // CHECK: %[[G0:.*]] = cir.get_member %[[CALLCAST]][0] {{.*}} : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!s64i> + // CHECK: %[[A0:.*]] = cir.load %[[G0]] : !cir.ptr<!s64i>, !s64i + // CHECK: %[[G1:.*]] = cir.get_member %[[CALLCAST]][1] {{.*}} : !cir.ptr<!rec_anon_struct> -> !cir.ptr<!u64i> + // CHECK: %[[A1:.*]] = cir.load %[[G1]] : !cir.ptr<!u64i>, !u64i + // CHECK: cir.call @take_two_eightbytes(%[[A0]], %[[A1]]) : (!s64i, !u64i) -> () +} + +// LLVM: define void @take_int_float(i32 %{{.+}}) +// Declaration order does not reach the lowered signature: the CIR-level +// signedness difference from the reduction tie disappears here, and both +// orders land on the same integer register the class merge picked. +// LLVM: define void @take_float_int(i32 %{{.+}}) +// LLVM: define void @take_long_double(i64 %{{.+}}) +// LLVM: define void @take_double_long(i64 %{{.+}}) +// LLVM: define void @take_floats(float %{{.+}}) +// LLVM: define void @take_three(i24 %{{.+}}) +// LLVM: define void @take_narrow_storage(i64 %{{.+}}) +// LLVM: define void @take_two_eightbytes(i64 %{{.+}}, i64 %{{.+}}) +// LLVM: define void @take_big(ptr noalias noundef byval(%union.UBig) align 8 %{{.+}}) +// LLVM: define void @take_big_over_aligned(ptr noalias noundef byval(%union.UBigOverAligned) align 32 %{{.+}}) +// LLVM: define void @take_struct_over_aligned(ptr noalias noundef byval(%struct.SOverAligned) align 32 %{{.+}}) +// LLVM: define void @take_empty() +// LLVM: define void @ret_empty() +// LLVM: define void @take_no_regs(ptr byref(%union.UNoRegs) align 4 %{{.+}}) +// LLVM: define void @take_struct_with_union(i64 %{{.+}}) +// LLVM: define i32 @ret_int_float(i32 %{{.+}}) +// LLVM: define void @ret_big(ptr dead_on_unwind noalias writable sret(%union.UBig) align 1 %{{.+}}, ptr noalias noundef byval(%union.UBig) align 8 %{{.+}}) +// LLVM: define void @call_int_float(i32 %{{.+}}) +// LLVM: call void @take_int_float(i32 %{{.+}}) +// LLVM: define void @call_two_eightbytes(i64 %{{.+}}, i64 %{{.+}}) +// LLVM: call void @take_two_eightbytes(i64 %{{.+}}, i64 %{{.+}}) _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
