[clang] Always emit complete debug info for types which appear as members of a standard-layout union. (PR #221615)
Clayton Knittel via cfe-commits
cfe-commits at lists.llvm.org
Sun Sep 6 17:02:40 PDT 2026
https://github.com/ClaytonKnittel updated https://github.com/llvm/llvm-project/pull/221615
>From 46dccb7a59674bda62e06c42063e1c5a8f2d59c8 Mon Sep 17 00:00:00 2001
From: Clayton Knittel <cknit1999 at gmail.com>
Date: Sun, 6 Sep 2026 23:45:05 +0000
Subject: [PATCH] Always emit complete debug info for types which appear as
members of a standard-layout union.
[[class.mem]](https://timsong-cpp.github.io/cppwp/n3337/class.mem#19)
states:
> "If a standard-layout union contains two or more standard-layout structs that share a common initial sequence, and if the standard-layout union object currently contains one of these standard-layout structs, it is permitted to inspect the common initial part of any of them. Two standard-layout structs share a common initial sequence if corresponding members have layout-compatible types and either neither member is a bit-field or both are bit-fields with the same width for a sequence of one or more initial members."
This makes it possible to obtain a reference to a type which was never
constructed, which violates the assumption made by constructor homing
that all types that may require debug info must be constructed.
This change takes the conservative approach of always emitting full
debug info for standard-layout types which appear as a member of a
standard-layout union somewhere in the TU. It could be made more
aggressive by only emitting full debug info if there is another type in
the union that shares a "common initial sequence", but that would add
complexity to this exclusion logic and likely isn't worth it.
Signed-off-by: Clayton Knittel <cknit1999 at gmail.com>
---
.../clang/AST/CXXRecordDeclDefinitionBits.def | 3 +
clang/lib/AST/DeclCXX.cpp | 16 +-
clang/lib/CodeGen/CGDebugInfo.cpp | 3 +-
clang/test/DebugInfo/CXX/limited-ctor.cpp | 20 +-
clang/unittests/AST/DeclTest.cpp | 256 ++++++++++++++++++
5 files changed, 295 insertions(+), 3 deletions(-)
diff --git a/clang/include/clang/AST/CXXRecordDeclDefinitionBits.def b/clang/include/clang/AST/CXXRecordDeclDefinitionBits.def
index 97e61aaec7d51..a45a57ecfff20 100644
--- a/clang/include/clang/AST/CXXRecordDeclDefinitionBits.def
+++ b/clang/include/clang/AST/CXXRecordDeclDefinitionBits.def
@@ -64,6 +64,9 @@ FIELD(Abstract, 1, NO_MERGE)
/// language rules (including DRs).
FIELD(IsStandardLayout, 1, NO_MERGE)
+/// True when this class appears as a member of some standard-layout union.
+FIELD(IsStandardLayoutUnionMember, 1, MERGE_OR)
+
/// True when this class was standard-layout under the C++11
/// definition.
///
diff --git a/clang/lib/AST/DeclCXX.cpp b/clang/lib/AST/DeclCXX.cpp
index f0da56542ae7e..476eb0bc2d5f3 100644
--- a/clang/lib/AST/DeclCXX.cpp
+++ b/clang/lib/AST/DeclCXX.cpp
@@ -76,7 +76,8 @@ void LazyASTUnresolvedSet::getFromExternalSource(ASTContext &C) const {
CXXRecordDecl::DefinitionData::DefinitionData(CXXRecordDecl *D)
: UserDeclaredConstructor(false), UserDeclaredSpecialMembers(0),
Aggregate(true), PlainOldData(true), Empty(true), Polymorphic(false),
- Abstract(false), IsStandardLayout(true), IsCXX11StandardLayout(true),
+ Abstract(false), IsStandardLayout(true),
+ IsStandardLayoutUnionMember(false), IsCXX11StandardLayout(true),
HasBasesWithFields(false), HasBasesWithNonStaticDataMembers(false),
HasPrivateFields(false), HasProtectedFields(false),
HasPublicFields(false), HasMutableFields(false), HasVariantMembers(false),
@@ -2321,6 +2322,19 @@ void CXXRecordDecl::completeDefinition(CXXFinalOverriderMap *FinalOverriders) {
data().IsStandardLayout = false;
data().IsCXX11StandardLayout = false;
}
+
+ if (isUnion() && isStandardLayout()) {
+ for (const FieldDecl *FD : fields()) {
+ if (const auto *RT =
+ FD->getType()->getBaseElementTypeUnsafe()->getAs<RecordType>()) {
+ if (auto *CRD =
+ dyn_cast<CXXRecordDecl>(RT->getDecl()->getDefinitionOrSelf())) {
+ if (CRD->hasDefinition() && CRD->isStandardLayout())
+ CRD->setIsStandardLayoutUnionMember(true);
+ }
+ }
+ }
+ }
}
bool CXXRecordDecl::mayBeAbstract() const {
diff --git a/clang/lib/CodeGen/CGDebugInfo.cpp b/clang/lib/CodeGen/CGDebugInfo.cpp
index 02864621d60a3..be6985f8eb4c9 100644
--- a/clang/lib/CodeGen/CGDebugInfo.cpp
+++ b/clang/lib/CodeGen/CGDebugInfo.cpp
@@ -3241,7 +3241,8 @@ static bool canUseCtorHoming(const CXXRecordDecl *RD) {
if (RD->isLambda() || RD->isAggregate() ||
RD->hasTrivialDefaultConstructor() ||
- RD->hasConstexprNonCopyMoveConstructor())
+ RD->hasConstexprNonCopyMoveConstructor() ||
+ RD->isStandardLayoutUnionMember())
return false;
for (const CXXConstructorDecl *Ctor : RD->ctors()) {
diff --git a/clang/test/DebugInfo/CXX/limited-ctor.cpp b/clang/test/DebugInfo/CXX/limited-ctor.cpp
index e820c0703df4f..3fe1682358af2 100644
--- a/clang/test/DebugInfo/CXX/limited-ctor.cpp
+++ b/clang/test/DebugInfo/CXX/limited-ctor.cpp
@@ -53,7 +53,7 @@ struct DeclaredConstexpr {
template <class A, class B> struct Aliased {
A first;
B second;
- constexpr Aliased(const A &a, const B &b) : first(a), second(b) {}
+ Aliased(const A &a, const B &b) : first(a), second(b) {}
};
union AliasedSlot {
Aliased<const int, int> value;
@@ -66,6 +66,24 @@ int ReadAliasedSlot() {
return TestAliasedSlot.value.first;
}
+// CHECK-DAG: !DICompositeType(tag: DW_TAG_structure_type, name: "ConstexprAliased<int, int>"{{.*}}DIFlagTypePassByValue
+// CHECK-DAG: !DICompositeType(tag: DW_TAG_structure_type, name: "ConstexprAliased<const int, int>"{{.*}}DIFlagTypePassByValue
+template <class A, class B> struct ConstexprAliased {
+ A first;
+ B second;
+ constexpr ConstexprAliased(const A &a, const B &b) : first(a), second(b) {}
+};
+union ConstexprAliasedSlot {
+ ConstexprAliased<const int, int> value;
+ ConstexprAliased<int, int> mutable_value;
+ ConstexprAliasedSlot() {}
+ ~ConstexprAliasedSlot() {}
+} TestConstexprAliasedSlot;
+int ReadConstexprAliasedSlot() {
+ TestConstexprAliasedSlot.mutable_value = ConstexprAliased<int, int>(1, 2);
+ return TestConstexprAliasedSlot.value.first;
+}
+
// Defined out-of-line constexpr constructor should emit full debug info.
// CHECK-DAG: !DICompositeType(tag: DW_TAG_structure_type, name: "OutOfLineConstexpr"{{.*}}DIFlagTypePassByValue
struct OutOfLineConstexpr {
diff --git a/clang/unittests/AST/DeclTest.cpp b/clang/unittests/AST/DeclTest.cpp
index 195b8ab4c4e66..6fa1da7f01f00 100644
--- a/clang/unittests/AST/DeclTest.cpp
+++ b/clang/unittests/AST/DeclTest.cpp
@@ -24,10 +24,12 @@
#include "clang/Basic/LLVM.h"
#include "clang/Basic/Linkage.h"
#include "clang/Basic/TargetInfo.h"
+#include "clang/Frontend/ASTUnit.h"
#include "clang/Lex/Lexer.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Testing/Annotations/Annotations.h"
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <cassert>
#include <memory>
@@ -36,6 +38,14 @@
using namespace clang::ast_matchers;
using namespace clang::tooling;
using namespace clang;
+using ::testing::AllOf;
+using ::testing::Not;
+using ::testing::Pointee;
+
+MATCHER(IsStandardLayout, "") { return arg.isStandardLayout(); }
+MATCHER(IsStandardLayoutUnionMember, "") {
+ return arg.isStandardLayoutUnionMember();
+}
TEST(Decl, CleansUpAPValues) {
MatchFinder Finder;
@@ -1029,3 +1039,249 @@ TEST(Decl, ObjCPropertyDeclNameForDiagnostic) {
/*Qualified=*/true);
EXPECT_EQ(ExtPQualifiedOS.str(), "-[MyClass extensionProp]");
}
+
+class StandardLayoutUnionMemberTest : public ::testing::Test {
+protected:
+ std::unique_ptr<ASTUnit> AST;
+ ASTContext *Ctx = nullptr;
+
+ void buildAST(StringRef Code, ArrayRef<std::string> Args = {"-std=c++11"}) {
+ AST = tooling::buildASTFromCodeWithArgs(Code, Args);
+ ASSERT_NE(AST, nullptr);
+ Ctx = &AST->getASTContext();
+ }
+
+ const CXXRecordDecl *findRecord(StringRef Name) const {
+ assert(Ctx && "ASTContext not initialized");
+ return selectFirst<CXXRecordDecl>(
+ "d",
+ match(cxxRecordDecl(hasName(Name), isDefinition()).bind("d"), *Ctx));
+ }
+
+ const CXXRecordDecl *findTemplate(StringRef Name,
+ StringRef TemplateArg) const {
+ assert(Ctx && "ASTContext not initialized");
+ return selectFirst<CXXRecordDecl>(
+ "d", match(classTemplateSpecializationDecl(
+ hasName(Name), isDefinition(),
+ hasTemplateArgument(
+ 0, refersToType(asString(TemplateArg.str()))))
+ .bind("d"),
+ *Ctx));
+ }
+
+ // Matches a standard-layout union member.
+ auto IsSLUnionMember() {
+ return Pointee(AllOf(IsStandardLayout(), IsStandardLayoutUnionMember()));
+ }
+
+ // Matches a standard-layout type that is not a member of a standard-layout
+ // union.
+ auto IsStandaloneSL() {
+ return Pointee(
+ AllOf(IsStandardLayout(), Not(IsStandardLayoutUnionMember())));
+ }
+
+ // Matches a non-standard-layout type.
+ auto IsNonSL() {
+ return Pointee(
+ AllOf(Not(IsStandardLayout()), Not(IsStandardLayoutUnionMember())));
+ }
+};
+
+TEST_F(StandardLayoutUnionMemberTest, StandaloneStruct) {
+ buildAST(R"cc(
+ struct StandaloneSL {
+ int x;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("StandaloneSL"), IsStandaloneSL());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, NonStandardLayoutStruct) {
+ buildAST(R"cc(
+ struct NonSL {
+ virtual void foo();
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("NonSL"), IsNonSL());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, StandardLayoutUnion) {
+ buildAST(R"cc(
+ struct SLInUnion {
+ int x;
+ };
+
+ union SLUnion {
+ SLInUnion u;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("SLUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("SLInUnion"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, TemplatedMember) {
+ buildAST(R"cc(
+ template <typename T>
+ struct TemplatedSL {
+ T x;
+ };
+
+ union TemplatedUnion {
+ TemplatedSL<int> a;
+ TemplatedSL<float> b;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("TemplatedUnion"), IsStandaloneSL());
+ EXPECT_THAT(findTemplate("TemplatedSL", "int"), IsSLUnionMember());
+ EXPECT_THAT(findTemplate("TemplatedSL", "float"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, NonStandardLayoutUnion) {
+ buildAST(R"cc(
+ struct NonSL {
+ virtual void foo();
+ };
+
+ struct SLInNonSLUnion {
+ int x;
+ };
+
+ union NonSLUnion {
+ SLInNonSLUnion s;
+ NonSL n;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("NonSLUnion"), Pointee(Not(IsStandardLayout())));
+ EXPECT_THAT(findRecord("NonSL"), IsNonSL());
+ EXPECT_THAT(findRecord("SLInNonSLUnion"), IsStandaloneSL());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, NestedStruct) {
+ buildAST(R"cc(
+ union NestedUnion {
+ struct NestedSL {
+ int a;
+ } n;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("NestedUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("NestedSL"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, Array) {
+ buildAST(R"cc(
+ struct SLInArray {
+ int x;
+ };
+ struct SLInMultiArray {
+ int y;
+ };
+ union ArrayUnion {
+ SLInArray arr[3];
+ SLInMultiArray multi_arr[2][4];
+ int raw;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("ArrayUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("SLInArray"), IsSLUnionMember());
+ EXPECT_THAT(findRecord("SLInMultiArray"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, CVQualified) {
+ buildAST(R"cc(
+ struct SLConst {
+ int x;
+ };
+ struct SLVolatile {
+ int y;
+ };
+ union CVUnion {
+ const SLConst c;
+ volatile SLVolatile v;
+ int raw;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("CVUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("SLConst"), IsSLUnionMember());
+ EXPECT_THAT(findRecord("SLVolatile"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, GenericUnionTemplate) {
+ buildAST(R"cc(
+ template <typename T>
+ union GenericUnion {
+ T val;
+ int raw;
+ };
+ struct SLInGenericUnion {
+ int x;
+ };
+ GenericUnion<SLInGenericUnion> u;
+ )cc");
+
+ EXPECT_THAT(findRecord("SLInGenericUnion"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, AnonymousUnion) {
+ buildAST(R"cc(
+ struct SLInAnonUnion {
+ int x;
+ };
+ struct EnclosingStruct {
+ union {
+ SLInAnonUnion a;
+ int b;
+ };
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("SLInAnonUnion"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, Inheritance) {
+ buildAST(R"cc(
+ struct EmptyBase {};
+ struct SLDerived : EmptyBase {
+ int x;
+ };
+ union DerivedUnion {
+ SLDerived d;
+ int raw;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("DerivedUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("SLDerived"), IsSLUnionMember());
+ EXPECT_THAT(findRecord("EmptyBase"), IsStandaloneSL());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, AnonymousTypedefStruct) {
+ buildAST(R"cc(
+ typedef struct {
+ int x;
+ } AnonTypedefSL;
+
+ union AnonTypedefUnion {
+ AnonTypedefSL s;
+ int raw;
+ };
+ )cc");
+
+ const auto *TD = selectFirst<TypedefDecl>(
+ "d", match(typedefDecl(hasName("AnonTypedefSL")).bind("d"), *Ctx));
+ ASSERT_NE(TD, nullptr);
+ const auto *AnonRecord = TD->getUnderlyingType()->getAsCXXRecordDecl();
+
+ EXPECT_THAT(findRecord("AnonTypedefUnion"), IsStandaloneSL());
+ EXPECT_THAT(AnonRecord, IsSLUnionMember());
+}
More information about the cfe-commits
mailing list