[clang] [CIR] Fix record layout for a union with no storage type (PR #213591)
Adam Smith via cfe-commits
cfe-commits at lists.llvm.org
Sun Aug 2 21:53:40 PDT 2026
https://github.com/adams381 created https://github.com/llvm/llvm-project/pull/213591
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.
`UnionType::getABIAlignment` keeps its early return. Union padding is always a
char or an array of char, so folding it in cannot change the alignment of
anything CIRGen emits.
>From eedb82e864b1d5375ce954b77174a58b1472845e Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Sun, 2 Aug 2026 21:27:43 -0700
Subject: [PATCH] [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
More information about the cfe-commits
mailing list