https://github.com/adams381 updated https://github.com/llvm/llvm-project/pull/213591
>From eedb82e864b1d5375ce954b77174a58b1472845e Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Sun, 2 Aug 2026 21:27:43 -0700 Subject: [PATCH 1/3] [CIR] Fix record layout for a union with no storage type A union whose CIR type ends up with no members keeps its whole size in its padding field, and `UnionType::getTypeSizeInBits` returned early in exactly that case, before reaching the padding. A union need not look empty in the source to land there: a lone zero-length bitfield is dropped during lowering, leaving the same no-storage state. A record embedding such a union was then laid out wrong. In an unpacked record `insertPadding` pads whenever the end of the members placed so far, rounded up to the next member's alignment, falls short of that member's offset, so a union measuring zero earns a pad the AST layout does not have. In C++, `struct { union {} e; int x; }` loaded `x` from byte 8 rather than 4, and an array of that struct had a 12-byte stride, not 8. With the union `alignas(16)`, the load came from byte 32 rather than 16. The zero also reached `lowerUnion`, which sizes a union's padding as its layout size less its storage member's, so `union { union {} e; }` emitted a two-byte type for a one-byte union. Sum the storage and padding contributions instead of returning early. The has-storage path is unchanged, and a C empty union stays at size zero because it has no padding field to add. --- clang/lib/CIR/Dialect/IR/CIRTypes.cpp | 13 +- .../CIR/CodeGen/empty-union-record-layout.cpp | 134 ++++++++++++++++++ 2 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 clang/test/CIR/CodeGen/empty-union-record-layout.cpp diff --git a/clang/lib/CIR/Dialect/IR/CIRTypes.cpp b/clang/lib/CIR/Dialect/IR/CIRTypes.cpp index c1f3d3dc6cca5..fba7bf6ac0fda 100644 --- a/clang/lib/CIR/Dialect/IR/CIRTypes.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRTypes.cpp @@ -727,15 +727,16 @@ StructType::getABIAlignment(const ::mlir::DataLayout &dataLayout, llvm::TypeSize UnionType::getTypeSizeInBits(const mlir::DataLayout &dataLayout, mlir::DataLayoutEntryListRef params) const { - mlir::Type storage = getUnionStorageType(dataLayout); - if (!storage) - return llvm::TypeSize::getFixed(0); + // A union whose member list came out empty has no storage type, so whatever + // size it has lives entirely in the padding field below. Sum both. + llvm::TypeSize size = llvm::TypeSize::getFixed(0); + if (mlir::Type storage = getUnionStorageType(dataLayout)) + size += dataLayout.getTypeSizeInBits(storage); // The padding field holds enough bytes to bring the total up to the AST // layout size (set by lowerUnion from the ASTRecordLayout). Include it so // getTypeSize agrees with the {storage, padding} LLVM struct that - // LowerToLLVM emits; without it a containing record adds spurious tail - // padding via insertPadding, making sizeof and array GEPs wrong. - llvm::TypeSize size = dataLayout.getTypeSizeInBits(storage); + // LowerToLLVM emits. Without it a containing record adds spurious padding + // via insertPadding, making the emitted record's size and its GEPs wrong. if (mlir::Type pad = getPadding()) size += dataLayout.getTypeSizeInBits(pad); return size; diff --git a/clang/test/CIR/CodeGen/empty-union-record-layout.cpp b/clang/test/CIR/CodeGen/empty-union-record-layout.cpp new file mode 100644 index 0000000000000..4fca4a063e387 --- /dev/null +++ b/clang/test/CIR/CodeGen/empty-union-record-layout.cpp @@ -0,0 +1,134 @@ +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o - | FileCheck %s --check-prefix=CIR +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o - | FileCheck %s --check-prefixes=LLVM,LLVMCIR +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefixes=LLVM,OGCG + +union Memberless {}; + +union alignas(16) MemberlessOver {}; + +// A zero-length bitfield is dropped during lowering, so this union reaches the +// same no-storage state despite declaring a member. +union OnlyZeroBitfield { + int : 0; +}; + +struct Leading { + Memberless e; + int x; +}; + +struct Trailing { + int x; + Memberless e; +}; + +// A union whose only member is itself storage-less. This one HAS a storage +// type, so it is the storage member's reported size that must be right, and a +// wrapping record cannot expose the error because the trailing field is +// realigned regardless. +union OnlyMemberless { + Memberless e; +}; + +struct Middle { + int a; + Memberless e; + int b; +}; + +struct LeadingOver { + MemberlessOver e; + int x; +}; + +struct LeadingZeroBitfield { + OnlyZeroBitfield e; + int x; +}; + +OnlyMemberless onlyMemberless; +Leading lead; +Trailing trail; +Middle mid; +LeadingOver leadOver; +LeadingZeroBitfield leadZero; +Leading leadArr[2]; + +// CIR-DAG: !rec_Memberless = !cir.union<"Memberless" {}, padding = {!u8i}> +// CIR-DAG: !rec_MemberlessOver = !cir.union<"MemberlessOver" {}, padding = {!cir.array<!u8i x 16>}> +// CIR-DAG: !rec_OnlyMemberless = !cir.union<"OnlyMemberless" {!rec_Memberless}> +// CIR-DAG: !rec_Leading = !cir.struct<"Leading" {!rec_Memberless, !s32i}> +// CIR-DAG: !rec_Trailing = !cir.struct<"Trailing" {!s32i, !rec_Memberless}> +// CIR-DAG: !rec_Middle = !cir.struct<"Middle" {!s32i, !rec_Memberless, !s32i}> +// CIR-DAG: !rec_LeadingOver = !cir.struct<"LeadingOver" padded {!rec_MemberlessOver, !s32i, !cir.array<!u8i x 12>}> +// CIR-DAG: !rec_OnlyZeroBitfield = !cir.union<"OnlyZeroBitfield" {}, padding = {!u8i}> +// CIR-DAG: !rec_LeadingZeroBitfield = !cir.struct<"LeadingZeroBitfield" {!rec_OnlyZeroBitfield, !s32i}> + +// Neither path carries a pad for the union's own bytes, though they spell those +// bytes differently. +// LLVMCIR-DAG: %struct.Leading = type { %union.Memberless, i32 } +// LLVMCIR-DAG: %struct.Trailing = type { i32, %union.Memberless } +// LLVMCIR-DAG: %struct.Middle = type { i32, %union.Memberless, i32 } +// LLVMCIR-DAG: %struct.LeadingZeroBitfield = type { %union.OnlyZeroBitfield, i32 } +// LLVMCIR-DAG: %struct.LeadingOver = type { %union.MemberlessOver, i32, [12 x i8] } +// OGCG-DAG: %struct.Leading = type { [4 x i8], i32 } +// OGCG-DAG: %struct.Trailing = type { i32, [4 x i8] } +// OGCG-DAG: %struct.Middle = type { i32, [4 x i8], i32 } +// OGCG-DAG: %struct.LeadingZeroBitfield = type { [4 x i8], i32 } +// OGCG-DAG: %struct.LeadingOver = type { [16 x i8], i32, [12 x i8] } +// LLVM-DAG: %union.OnlyMemberless = type { %union.Memberless } +// LLVM-DAG: @lead = global %struct.Leading zeroinitializer, align 4 +// LLVM-DAG: @leadOver = global %struct.LeadingOver zeroinitializer, align 16 + +// The union occupies one byte, so the int follows at offset 4. +int getLeading() { return lead.x; } + +// CIR: cir.func{{.*}} @_Z10getLeadingv() +// CIR: %[[L:.*]] = cir.get_global @lead : !cir.ptr<!rec_Leading> +// CIR: %{{.*}} = cir.get_member %[[L]][1] {name = "x"} : !cir.ptr<!rec_Leading> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z10getLeadingv() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @lead, i64 4), align 4 + +// With the union last, the size it contributes lands in the record's tail. +int getTrailing() { return trail.x; } + +// CIR: cir.func{{.*}} @_Z11getTrailingv() +// CIR: %[[T:.*]] = cir.get_global @trail : !cir.ptr<!rec_Trailing> +// CIR: %{{.*}} = cir.get_member %[[T]][0] {name = "x"} : !cir.ptr<!rec_Trailing> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z11getTrailingv() +// LLVM: load i32, ptr @trail, align 4 + +// The union sits between two fields, so only the field AFTER it moves. +int getMiddle() { return mid.b; } + +// CIR: cir.func{{.*}} @_Z9getMiddlev() +// CIR: %[[M:.*]] = cir.get_global @mid : !cir.ptr<!rec_Middle> +// CIR: %{{.*}} = cir.get_member %[[M]][2] {name = "b"} : !cir.ptr<!rec_Middle> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z9getMiddlev() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @mid, i64 8), align 4 + +// An over-aligned union spells its size as an array of char rather than a +// single char, and the record embedding it has real tail padding of its own. +int getLeadingOver() { return leadOver.x; } + +// CIR: cir.func{{.*}} @_Z14getLeadingOverv() +// CIR: %[[O:.*]] = cir.get_global @leadOver : !cir.ptr<!rec_LeadingOver> +// CIR: %{{.*}} = cir.get_member %[[O]][1] {name = "x"} : !cir.ptr<!rec_LeadingOver> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z14getLeadingOverv() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadOver, i64 16), align 16 + +// The dropped bitfield leaves no storage member, so this behaves like Leading. +int getLeadingZeroBitfield() { return leadZero.x; } + +// CIR: cir.func{{.*}} @_Z22getLeadingZeroBitfieldv() +// CIR: %[[Z:.*]] = cir.get_global @leadZero : !cir.ptr<!rec_LeadingZeroBitfield> +// CIR: %{{.*}} = cir.get_member %[[Z]][1] {name = "x"} : !cir.ptr<!rec_LeadingZeroBitfield> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z22getLeadingZeroBitfieldv() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadZero, i64 4), align 4 + +// The element stride is 8, so the second element's int is at offset 12. +int getArray() { return leadArr[1].x; } + +// CIR: cir.func{{.*}} @_Z8getArrayv() +// LLVM: define dso_local noundef i32 @_Z8getArrayv() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadArr, i64 12), align 4 >From afc1312ae4ddcfe288485f95a4bf1590d4a994f0 Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Mon, 3 Aug 2026 13:38:41 -0700 Subject: [PATCH 2/3] [CIR] Fold empty-union layout tests into empty-union.cpp These went into a new file that re-declared two unions empty-union.cpp already had. Moving them there meant making the type-alias checks -DAG, since the aliases stop coming out in declaration order once seven more types are added. I also collapsed the LLVM and OGCG prefixes where the two agree. OnlyZeroBitfield had no check on its own lowered type. Classic never names it, since LeadingZeroBitfield covers the union with a char array, so useZeroBitfield() gives it a use and one LLVM check now covers both. --- .../CIR/CodeGen/empty-union-record-layout.cpp | 134 -------------- clang/test/CIR/CodeGen/empty-union.cpp | 173 ++++++++++++++++-- 2 files changed, 154 insertions(+), 153 deletions(-) delete mode 100644 clang/test/CIR/CodeGen/empty-union-record-layout.cpp diff --git a/clang/test/CIR/CodeGen/empty-union-record-layout.cpp b/clang/test/CIR/CodeGen/empty-union-record-layout.cpp deleted file mode 100644 index 4fca4a063e387..0000000000000 --- a/clang/test/CIR/CodeGen/empty-union-record-layout.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o - | FileCheck %s --check-prefix=CIR -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o - | FileCheck %s --check-prefixes=LLVM,LLVMCIR -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefixes=LLVM,OGCG - -union Memberless {}; - -union alignas(16) MemberlessOver {}; - -// A zero-length bitfield is dropped during lowering, so this union reaches the -// same no-storage state despite declaring a member. -union OnlyZeroBitfield { - int : 0; -}; - -struct Leading { - Memberless e; - int x; -}; - -struct Trailing { - int x; - Memberless e; -}; - -// A union whose only member is itself storage-less. This one HAS a storage -// type, so it is the storage member's reported size that must be right, and a -// wrapping record cannot expose the error because the trailing field is -// realigned regardless. -union OnlyMemberless { - Memberless e; -}; - -struct Middle { - int a; - Memberless e; - int b; -}; - -struct LeadingOver { - MemberlessOver e; - int x; -}; - -struct LeadingZeroBitfield { - OnlyZeroBitfield e; - int x; -}; - -OnlyMemberless onlyMemberless; -Leading lead; -Trailing trail; -Middle mid; -LeadingOver leadOver; -LeadingZeroBitfield leadZero; -Leading leadArr[2]; - -// CIR-DAG: !rec_Memberless = !cir.union<"Memberless" {}, padding = {!u8i}> -// CIR-DAG: !rec_MemberlessOver = !cir.union<"MemberlessOver" {}, padding = {!cir.array<!u8i x 16>}> -// CIR-DAG: !rec_OnlyMemberless = !cir.union<"OnlyMemberless" {!rec_Memberless}> -// CIR-DAG: !rec_Leading = !cir.struct<"Leading" {!rec_Memberless, !s32i}> -// CIR-DAG: !rec_Trailing = !cir.struct<"Trailing" {!s32i, !rec_Memberless}> -// CIR-DAG: !rec_Middle = !cir.struct<"Middle" {!s32i, !rec_Memberless, !s32i}> -// CIR-DAG: !rec_LeadingOver = !cir.struct<"LeadingOver" padded {!rec_MemberlessOver, !s32i, !cir.array<!u8i x 12>}> -// CIR-DAG: !rec_OnlyZeroBitfield = !cir.union<"OnlyZeroBitfield" {}, padding = {!u8i}> -// CIR-DAG: !rec_LeadingZeroBitfield = !cir.struct<"LeadingZeroBitfield" {!rec_OnlyZeroBitfield, !s32i}> - -// Neither path carries a pad for the union's own bytes, though they spell those -// bytes differently. -// LLVMCIR-DAG: %struct.Leading = type { %union.Memberless, i32 } -// LLVMCIR-DAG: %struct.Trailing = type { i32, %union.Memberless } -// LLVMCIR-DAG: %struct.Middle = type { i32, %union.Memberless, i32 } -// LLVMCIR-DAG: %struct.LeadingZeroBitfield = type { %union.OnlyZeroBitfield, i32 } -// LLVMCIR-DAG: %struct.LeadingOver = type { %union.MemberlessOver, i32, [12 x i8] } -// OGCG-DAG: %struct.Leading = type { [4 x i8], i32 } -// OGCG-DAG: %struct.Trailing = type { i32, [4 x i8] } -// OGCG-DAG: %struct.Middle = type { i32, [4 x i8], i32 } -// OGCG-DAG: %struct.LeadingZeroBitfield = type { [4 x i8], i32 } -// OGCG-DAG: %struct.LeadingOver = type { [16 x i8], i32, [12 x i8] } -// LLVM-DAG: %union.OnlyMemberless = type { %union.Memberless } -// LLVM-DAG: @lead = global %struct.Leading zeroinitializer, align 4 -// LLVM-DAG: @leadOver = global %struct.LeadingOver zeroinitializer, align 16 - -// The union occupies one byte, so the int follows at offset 4. -int getLeading() { return lead.x; } - -// CIR: cir.func{{.*}} @_Z10getLeadingv() -// CIR: %[[L:.*]] = cir.get_global @lead : !cir.ptr<!rec_Leading> -// CIR: %{{.*}} = cir.get_member %[[L]][1] {name = "x"} : !cir.ptr<!rec_Leading> -> !cir.ptr<!s32i> -// LLVM: define dso_local noundef i32 @_Z10getLeadingv() -// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @lead, i64 4), align 4 - -// With the union last, the size it contributes lands in the record's tail. -int getTrailing() { return trail.x; } - -// CIR: cir.func{{.*}} @_Z11getTrailingv() -// CIR: %[[T:.*]] = cir.get_global @trail : !cir.ptr<!rec_Trailing> -// CIR: %{{.*}} = cir.get_member %[[T]][0] {name = "x"} : !cir.ptr<!rec_Trailing> -> !cir.ptr<!s32i> -// LLVM: define dso_local noundef i32 @_Z11getTrailingv() -// LLVM: load i32, ptr @trail, align 4 - -// The union sits between two fields, so only the field AFTER it moves. -int getMiddle() { return mid.b; } - -// CIR: cir.func{{.*}} @_Z9getMiddlev() -// CIR: %[[M:.*]] = cir.get_global @mid : !cir.ptr<!rec_Middle> -// CIR: %{{.*}} = cir.get_member %[[M]][2] {name = "b"} : !cir.ptr<!rec_Middle> -> !cir.ptr<!s32i> -// LLVM: define dso_local noundef i32 @_Z9getMiddlev() -// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @mid, i64 8), align 4 - -// An over-aligned union spells its size as an array of char rather than a -// single char, and the record embedding it has real tail padding of its own. -int getLeadingOver() { return leadOver.x; } - -// CIR: cir.func{{.*}} @_Z14getLeadingOverv() -// CIR: %[[O:.*]] = cir.get_global @leadOver : !cir.ptr<!rec_LeadingOver> -// CIR: %{{.*}} = cir.get_member %[[O]][1] {name = "x"} : !cir.ptr<!rec_LeadingOver> -> !cir.ptr<!s32i> -// LLVM: define dso_local noundef i32 @_Z14getLeadingOverv() -// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadOver, i64 16), align 16 - -// The dropped bitfield leaves no storage member, so this behaves like Leading. -int getLeadingZeroBitfield() { return leadZero.x; } - -// CIR: cir.func{{.*}} @_Z22getLeadingZeroBitfieldv() -// CIR: %[[Z:.*]] = cir.get_global @leadZero : !cir.ptr<!rec_LeadingZeroBitfield> -// CIR: %{{.*}} = cir.get_member %[[Z]][1] {name = "x"} : !cir.ptr<!rec_LeadingZeroBitfield> -> !cir.ptr<!s32i> -// LLVM: define dso_local noundef i32 @_Z22getLeadingZeroBitfieldv() -// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadZero, i64 4), align 4 - -// The element stride is 8, so the second element's int is at offset 12. -int getArray() { return leadArr[1].x; } - -// CIR: cir.func{{.*}} @_Z8getArrayv() -// LLVM: define dso_local noundef i32 @_Z8getArrayv() -// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadArr, i64 12), align 4 diff --git a/clang/test/CIR/CodeGen/empty-union.cpp b/clang/test/CIR/CodeGen/empty-union.cpp index ffe3ad26b8b62..26a00959ffe5e 100644 --- a/clang/test/CIR/CodeGen/empty-union.cpp +++ b/clang/test/CIR/CodeGen/empty-union.cpp @@ -1,19 +1,25 @@ // RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o - | FileCheck %s --check-prefix=CIR -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o - | FileCheck %s --check-prefix=LLVM -// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefix=OGCG +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o - | FileCheck %s --check-prefixes=LLVM,LLVMCIR +// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o - | FileCheck %s --check-prefixes=LLVM,OGCG // Padding-only union: CIR has no storage member and stores size in padding // field, so getUnionStorageType() is null and getABIAlignment returns 1. union Empty {}; -// CIR: !rec_Empty = !cir.union<"Empty" {}, padding = {!u8i}> +// CIR-DAG: !rec_Empty = !cir.union<"Empty" {}, padding = {!u8i}> // LLVM-DAG: %union.Empty = type { i8 } -// OGCG-DAG: %union.Empty = type { i8 } // Aligned empty union (should have aligned integer member in CIR) union alignas(16) EmptyAligned {}; -// CIR: !rec_EmptyAligned = !cir.union<"EmptyAligned" {}, padding = {!cir.array<!u8i x 16>}> +// CIR-DAG: !rec_EmptyAligned = !cir.union<"EmptyAligned" {}, padding = {!cir.array<!u8i x 16>}> // LLVM-DAG: %union.EmptyAligned = type { [16 x i8] } -// OGCG-DAG: %union.EmptyAligned = type { [16 x i8] } + +// A zero-length bitfield is dropped during lowering, so this union reaches the +// same no-storage state despite declaring a member. +union OnlyZeroBitfield { + int : 0; +}; +// CIR-DAG: !rec_OnlyZeroBitfield = !cir.union<"OnlyZeroBitfield" {}, padding = {!u8i}> +// LLVM-DAG: %union.OnlyZeroBitfield = type { i8 } // Struct holding a padding-only union member: layout queries !rec_Empty // alignment (null largest), not OuterWithEmpty's int x. @@ -26,15 +32,78 @@ struct WrapEmpty { int s; }; WrapEmpty w; -// CIR: !rec_OuterWithEmpty = !cir.union<"OuterWithEmpty" {!rec_Empty, !s32i}> -// CIR: !rec_WrapEmpty = !cir.struct<"WrapEmpty" {!rec_OuterWithEmpty, !s32i}> -// CIR: cir.global external @w = #cir.zero : !rec_WrapEmpty {alignment = 4 : i64} +// CIR-DAG: !rec_OuterWithEmpty = !cir.union<"OuterWithEmpty" {!rec_Empty, !s32i}> +// CIR-DAG: !rec_WrapEmpty = !cir.struct<"WrapEmpty" {!rec_OuterWithEmpty, !s32i}> +// CIR-DAG: cir.global external @w = #cir.zero : !rec_WrapEmpty {alignment = 4 : i64} // LLVM-DAG: %struct.WrapEmpty = type { %union.OuterWithEmpty, i32 } // LLVM-DAG: %union.OuterWithEmpty = type { i32 } -// LLVM: @w = global %struct.WrapEmpty zeroinitializer, align 4 -// OGCG-DAG: %struct.WrapEmpty = type { %union.OuterWithEmpty, i32 } -// OGCG-DAG: %union.OuterWithEmpty = type { i32 } -// OGCG: @w = global %struct.WrapEmpty zeroinitializer, align 4 +// LLVM-DAG: @w = global %struct.WrapEmpty zeroinitializer, align 4 + +// A union whose only member is itself storage-less. This one HAS a storage +// type, so it is the storage member's reported size that must be right. +union OnlyEmpty { + Empty e; +}; +// CIR-DAG: !rec_OnlyEmpty = !cir.union<"OnlyEmpty" {!rec_Empty}> +// LLVM-DAG: %union.OnlyEmpty = type { %union.Empty } + +// A storage-less union still occupies its own bytes inside a record, so the +// fields after it must not be pushed past them. +struct Leading { + Empty e; + int x; +}; + +struct Trailing { + int x; + Empty e; +}; + +struct Middle { + int a; + Empty e; + int b; +}; + +struct LeadingOver { + EmptyAligned e; + int x; +}; + +struct LeadingZeroBitfield { + OnlyZeroBitfield e; + int x; +}; + +OnlyEmpty onlyEmpty; +Leading lead; +Trailing trail; +Middle mid; +LeadingOver leadOver; +LeadingZeroBitfield leadZero; +Leading leadArr[2]; + +// CIR-DAG: !rec_Leading = !cir.struct<"Leading" {!rec_Empty, !s32i}> +// CIR-DAG: !rec_Trailing = !cir.struct<"Trailing" {!s32i, !rec_Empty}> +// CIR-DAG: !rec_Middle = !cir.struct<"Middle" {!s32i, !rec_Empty, !s32i}> +// CIR-DAG: !rec_LeadingOver = !cir.struct<"LeadingOver" padded {!rec_EmptyAligned, !s32i, !cir.array<!u8i x 12>}> +// CIR-DAG: !rec_LeadingZeroBitfield = !cir.struct<"LeadingZeroBitfield" {!rec_OnlyZeroBitfield, !s32i}> + +// CIR keeps the union's own named type as the record's field and leaves the +// bytes after it to the LLVM struct layout. Classic covers the union together +// with those bytes in one char array. +// LLVMCIR-DAG: %struct.Leading = type { %union.Empty, i32 } +// LLVMCIR-DAG: %struct.Trailing = type { i32, %union.Empty } +// LLVMCIR-DAG: %struct.Middle = type { i32, %union.Empty, i32 } +// LLVMCIR-DAG: %struct.LeadingZeroBitfield = type { %union.OnlyZeroBitfield, i32 } +// LLVMCIR-DAG: %struct.LeadingOver = type { %union.EmptyAligned, i32, [12 x i8] } +// OGCG-DAG: %struct.Leading = type { [4 x i8], i32 } +// OGCG-DAG: %struct.Trailing = type { i32, [4 x i8] } +// OGCG-DAG: %struct.Middle = type { i32, [4 x i8], i32 } +// OGCG-DAG: %struct.LeadingZeroBitfield = type { [4 x i8], i32 } +// OGCG-DAG: %struct.LeadingOver = type { [16 x i8], i32, [12 x i8] } +// LLVM-DAG: @lead = global %struct.Leading zeroinitializer, align 4 +// LLVM-DAG: @leadOver = global %struct.LeadingOver zeroinitializer, align 16 void useEmpty() { Empty e; @@ -42,9 +111,8 @@ void useEmpty() { // CIR: cir.func {{.*}}@_Z8useEmptyv() // CIR: cir.alloca "e" align(1) : !cir.ptr<!rec_Empty> // LLVM: define {{.*}} void @_Z8useEmptyv() -// LLVM: alloca %union.Empty, i64 1, align 1 -// OGCG: define {{.*}} void @_Z8useEmptyv() -// OGCG: alloca %union.Empty, align 1 +// LLVMCIR: alloca %union.Empty, i64 1, align 1 +// OGCG: alloca %union.Empty, align 1 void useEmptyAligned() { EmptyAligned e; @@ -52,6 +120,73 @@ void useEmptyAligned() { // CIR: cir.func {{.*}}@_Z15useEmptyAlignedv() // CIR: cir.alloca "e" align(16) : !cir.ptr<!rec_EmptyAligned> // LLVM: define {{.*}} void @_Z15useEmptyAlignedv() -// LLVM: alloca %union.EmptyAligned, i64 1, align 16 -// OGCG: define {{.*}} void @_Z15useEmptyAlignedv() -// OGCG: alloca %union.EmptyAligned, align 16 +// LLVMCIR: alloca %union.EmptyAligned, i64 1, align 16 +// OGCG: alloca %union.EmptyAligned, align 16 + +// Classic never refers to this union's type from inside LeadingZeroBitfield, so +// a variable of the type is what puts it in both modules. +void useZeroBitfield() { + OnlyZeroBitfield e; +} +// CIR: cir.func {{.*}}@_Z15useZeroBitfieldv() +// CIR: cir.alloca "e" align(1) : !cir.ptr<!rec_OnlyZeroBitfield> +// LLVM: define {{.*}} void @_Z15useZeroBitfieldv() +// LLVMCIR: alloca %union.OnlyZeroBitfield, i64 1, align 1 +// OGCG: alloca %union.OnlyZeroBitfield, align 1 + +// The union occupies one byte, so the int follows at offset 4. +int getLeading() { return lead.x; } + +// CIR: cir.func{{.*}} @_Z10getLeadingv() +// CIR: %[[L:.*]] = cir.get_global @lead : !cir.ptr<!rec_Leading> +// CIR: %{{.*}} = cir.get_member %[[L]][1] {name = "x"} : !cir.ptr<!rec_Leading> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z10getLeadingv() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @lead, i64 4), align 4 + +// With the union last, `x` stays at offset 0 whatever the union measures. The +// record's own type above is what pins the tail. +int getTrailing() { return trail.x; } + +// CIR: cir.func{{.*}} @_Z11getTrailingv() +// CIR: %[[T:.*]] = cir.get_global @trail : !cir.ptr<!rec_Trailing> +// CIR: %{{.*}} = cir.get_member %[[T]][0] {name = "x"} : !cir.ptr<!rec_Trailing> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z11getTrailingv() +// LLVM: load i32, ptr @trail, align 4 + +// The union sits between two fields, so only the field after it moves. +int getMiddle() { return mid.b; } + +// CIR: cir.func{{.*}} @_Z9getMiddlev() +// CIR: %[[M:.*]] = cir.get_global @mid : !cir.ptr<!rec_Middle> +// CIR: %{{.*}} = cir.get_member %[[M]][2] {name = "b"} : !cir.ptr<!rec_Middle> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z9getMiddlev() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @mid, i64 8), align 4 + +// An over-aligned union spells its size as an array of char rather than a +// single char, and the record embedding it has real tail padding of its own. +int getLeadingOver() { return leadOver.x; } + +// CIR: cir.func{{.*}} @_Z14getLeadingOverv() +// CIR: %[[O:.*]] = cir.get_global @leadOver : !cir.ptr<!rec_LeadingOver> +// CIR: %{{.*}} = cir.get_member %[[O]][1] {name = "x"} : !cir.ptr<!rec_LeadingOver> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z14getLeadingOverv() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadOver, i64 16), align 16 + +// The dropped bitfield leaves no storage member, so this behaves like Leading. +int getLeadingZeroBitfield() { return leadZero.x; } + +// CIR: cir.func{{.*}} @_Z22getLeadingZeroBitfieldv() +// CIR: %[[Z:.*]] = cir.get_global @leadZero : !cir.ptr<!rec_LeadingZeroBitfield> +// CIR: %{{.*}} = cir.get_member %[[Z]][1] {name = "x"} : !cir.ptr<!rec_LeadingZeroBitfield> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z22getLeadingZeroBitfieldv() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadZero, i64 4), align 4 + +// The element stride is 8, so the second element's int is at offset 12. +int getArray() { return leadArr[1].x; } + +// CIR: cir.func{{.*}} @_Z8getArrayv() +// CIR: %[[A:.*]] = cir.get_global @leadArr : !cir.ptr<!cir.array<!rec_Leading x 2>> +// CIR: %[[E:.*]] = cir.get_element %[[A]][%{{.*}} : !s64i] : !cir.ptr<!cir.array<!rec_Leading x 2>> -> !cir.ptr<!rec_Leading> +// CIR: %{{.*}} = cir.get_member %[[E]][1] {name = "x"} : !cir.ptr<!rec_Leading> -> !cir.ptr<!s32i> +// LLVM: define dso_local noundef i32 @_Z8getArrayv() +// LLVM: load i32, ptr getelementptr inbounds nuw (i8, ptr @leadArr, i64 12), align 4 >From 29252ce9efdc4c9f39f230012c049685eb3c90cd Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Mon, 3 Aug 2026 14:45:42 -0700 Subject: [PATCH 3/3] [CIR] Tighten empty-union tests and union-size comment Simplify the comments on `UnionType::getTypeSizeInBits`. This wording keeps the important information without running on. Use wildcards to combine the test checks where irrelevantly different into a single `LLVM` check. Assisted-by: Cursor / claude-opus-5 --- clang/lib/CIR/Dialect/IR/CIRTypes.cpp | 10 +++------- clang/test/CIR/CodeGen/empty-union.cpp | 9 +++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/clang/lib/CIR/Dialect/IR/CIRTypes.cpp b/clang/lib/CIR/Dialect/IR/CIRTypes.cpp index fba7bf6ac0fda..c2a3b3cb17b9e 100644 --- a/clang/lib/CIR/Dialect/IR/CIRTypes.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRTypes.cpp @@ -724,19 +724,15 @@ StructType::getABIAlignment(const ::mlir::DataLayout &dataLayout, return computeStructAlignment(dataLayout); } +// Sums the storage member (if present) with the padding field (if present). +// A union whose member list came out empty has no storage type, so its whole +// size lives in the padding, which lowerUnion sizes from the ASTRecordLayout. llvm::TypeSize UnionType::getTypeSizeInBits(const mlir::DataLayout &dataLayout, mlir::DataLayoutEntryListRef params) const { - // A union whose member list came out empty has no storage type, so whatever - // size it has lives entirely in the padding field below. Sum both. llvm::TypeSize size = llvm::TypeSize::getFixed(0); if (mlir::Type storage = getUnionStorageType(dataLayout)) size += dataLayout.getTypeSizeInBits(storage); - // The padding field holds enough bytes to bring the total up to the AST - // layout size (set by lowerUnion from the ASTRecordLayout). Include it so - // getTypeSize agrees with the {storage, padding} LLVM struct that - // LowerToLLVM emits. Without it a containing record adds spurious padding - // via insertPadding, making the emitted record's size and its GEPs wrong. if (mlir::Type pad = getPadding()) size += dataLayout.getTypeSizeInBits(pad); return size; diff --git a/clang/test/CIR/CodeGen/empty-union.cpp b/clang/test/CIR/CodeGen/empty-union.cpp index 26a00959ffe5e..7922bb853f936 100644 --- a/clang/test/CIR/CodeGen/empty-union.cpp +++ b/clang/test/CIR/CodeGen/empty-union.cpp @@ -111,8 +111,7 @@ void useEmpty() { // CIR: cir.func {{.*}}@_Z8useEmptyv() // CIR: cir.alloca "e" align(1) : !cir.ptr<!rec_Empty> // LLVM: define {{.*}} void @_Z8useEmptyv() -// LLVMCIR: alloca %union.Empty, i64 1, align 1 -// OGCG: alloca %union.Empty, align 1 +// LLVM: alloca %union.Empty{{.*}}, align 1 void useEmptyAligned() { EmptyAligned e; @@ -120,8 +119,7 @@ void useEmptyAligned() { // CIR: cir.func {{.*}}@_Z15useEmptyAlignedv() // CIR: cir.alloca "e" align(16) : !cir.ptr<!rec_EmptyAligned> // LLVM: define {{.*}} void @_Z15useEmptyAlignedv() -// LLVMCIR: alloca %union.EmptyAligned, i64 1, align 16 -// OGCG: alloca %union.EmptyAligned, align 16 +// LLVM: alloca %union.EmptyAligned{{.*}}, align 16 // Classic never refers to this union's type from inside LeadingZeroBitfield, so // a variable of the type is what puts it in both modules. @@ -131,8 +129,7 @@ void useZeroBitfield() { // CIR: cir.func {{.*}}@_Z15useZeroBitfieldv() // CIR: cir.alloca "e" align(1) : !cir.ptr<!rec_OnlyZeroBitfield> // LLVM: define {{.*}} void @_Z15useZeroBitfieldv() -// LLVMCIR: alloca %union.OnlyZeroBitfield, i64 1, align 1 -// OGCG: alloca %union.OnlyZeroBitfield, align 1 +// LLVM: alloca %union.OnlyZeroBitfield{{.*}}, align 1 // The union occupies one byte, so the int follows at offset 4. int getLeading() { return lead.x; } _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
