[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