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

Reply via email to