[clang] [clang] Support DecompositionDecl in -ast-print (PR #221711)
Ran Regev via cfe-commits
cfe-commits at lists.llvm.org
Thu Sep 10 05:19:32 PDT 2026
https://github.com/regevran updated https://github.com/llvm/llvm-project/pull/221711
>From 7fb058545a2c275cc5bee88b3b41b35eb56c4fb6 Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Mon, 7 Sep 2026 12:57:15 +0300
Subject: [PATCH 01/14] [clang] Support DecompositionDecl in -ast-print
DeclPrinter had no VisitDecompositionDecl:
auto [a, b] = get();
printed as:
auto = get();
silently dropping the whole binding list.
At namespace scope this was worse:
struct Pair { int a, b; };
Pair get();
auto [gx, gy] = get();
printed as:
struct Pair {
int a;
int b;
};
Pair get();
;
;
auto = get();
Added VisitDecompositionDecl, sharing VisitVarDecl's specifier and
initializer logic via two extracted helpers: printVarDeclSpecifiers,
printVarInitializer, to keep the two in sync Skip BindingDecl in
VisitDeclContext.
Structured binding packs (`auto [...rest] = arr;`) are a separate,
pre-existing gap this doesn't address: a pack binding still prints its
name, just without the leading `...`, e.g.
auto [first, ...rest, last] = arr;
currently prints as
auto [first, rest, last] = arr;
---
clang/lib/AST/DeclPrinter.cpp | 41 +++++++++++++-
clang/test/AST/ast-print-decomposition.cpp | 66 ++++++++++++++++++++++
2 files changed, 106 insertions(+), 1 deletion(-)
create mode 100644 clang/test/AST/ast-print-decomposition.cpp
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index cdd6bb0a90a2b..6fa7c341ef9d5 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -52,6 +52,9 @@ namespace {
void PrintObjCTypeParams(ObjCTypeParamList *Params);
void PrintOpenACCRoutineOnLambda(Decl *D);
+ QualType printVarDeclSpecifiers(VarDecl *D);
+ void printVarInitializer(VarDecl *D);
+
public:
DeclPrinter(raw_ostream &Out, const PrintingPolicy &Policy,
const ASTContext &Context, unsigned Indentation = 0,
@@ -73,6 +76,7 @@ namespace {
void VisitFriendTemplateDecl(FriendTemplateDecl *D);
void VisitFieldDecl(FieldDecl *D);
void VisitVarDecl(VarDecl *D);
+ void VisitDecompositionDecl(DecompositionDecl *D);
void VisitLabelDecl(LabelDecl *D);
void VisitParmVarDecl(ParmVarDecl *D);
void VisitFileScopeAsmDecl(FileScopeAsmDecl *D);
@@ -461,6 +465,11 @@ void DeclPrinter::VisitDeclContext(DeclContext *DC, bool Indent) {
if (isa<ObjCIvarDecl>(*D))
continue;
+ // Don't print BindingDecls, as they are printed when visiting the
+ // containing DecompositionDecl.
+ if (isa<BindingDecl>(*D))
+ continue;
+
// Skip over implicit declarations in pretty-printing mode.
if (D->isImplicit())
continue;
@@ -963,7 +972,7 @@ void DeclPrinter::VisitLabelDecl(LabelDecl *D) {
Out << *D << ":";
}
-void DeclPrinter::VisitVarDecl(VarDecl *D) {
+QualType DeclPrinter::printVarDeclSpecifiers(VarDecl *D) {
prettyPrintPragmas(D);
if (std::optional<std::string> Attrs =
@@ -1006,6 +1015,12 @@ void DeclPrinter::VisitVarDecl(VarDecl *D) {
}
}
+ return T;
+}
+
+void DeclPrinter::VisitVarDecl(VarDecl *D) {
+ QualType T = printVarDeclSpecifiers(D);
+
printDeclType(T, (isa<ParmVarDecl>(D) && Policy.CleanUglifiedParameters &&
D->getIdentifier())
? D->getIdentifier()->deuglifiedName()
@@ -1015,6 +1030,10 @@ void DeclPrinter::VisitVarDecl(VarDecl *D) {
prettyPrintAttributes(D, AttrPosAsWritten::Right))
Out << ' ' << *Attrs;
+ printVarInitializer(D);
+}
+
+void DeclPrinter::printVarInitializer(VarDecl *D) {
Expr *Init = D->getInit();
if (!Policy.SuppressInitializers && Init) {
bool ImplicitInit = false;
@@ -1044,6 +1063,26 @@ void DeclPrinter::VisitVarDecl(VarDecl *D) {
}
}
+void DeclPrinter::VisitDecompositionDecl(DecompositionDecl *D) {
+ QualType T = printVarDeclSpecifiers(D);
+
+ // DecompositionDecl has no name of its own.
+ printDeclType(T, "");
+
+ Out << " [";
+ bool First = true;
+ for (BindingDecl *B : D->bindings()) {
+ if (!First)
+ Out << ", ";
+ First = false;
+ // FIXME: this drops the leading "..." for a pack binding.
+ Out << B->getName();
+ }
+ Out << "]";
+
+ printVarInitializer(D);
+}
+
void DeclPrinter::VisitParmVarDecl(ParmVarDecl *D) {
VisitVarDecl(D);
}
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
new file mode 100644
index 0000000000000..aabfcad111bc5
--- /dev/null
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -0,0 +1,66 @@
+// RUN: %clang_cc1 -std=c++20 -ast-print %s | FileCheck %s
+
+// The `[a, b]` binding list must survive -ast-print, not just the type.
+
+namespace std {
+using size_t = decltype(sizeof(0));
+template <typename> struct tuple_size;
+template <size_t, typename> struct tuple_element;
+} // namespace std
+
+namespace Aggregate {
+struct Pair { int a, b; };
+Pair get();
+Pair &getref();
+
+// CHECK-LABEL: void local() {
+void local() {
+ // CHECK-NEXT: auto [x, y] = get();
+ auto [x, y] = get();
+ // CHECK-NEXT: auto & [rx, ry] = getref();
+ auto &[rx, ry] = getref();
+ // CHECK-NEXT: const auto [cx, cy] = get();
+ const auto [cx, cy] = get();
+ // CHECK-NEXT: static auto [sx, sy] = get();
+ static auto [sx, sy] = get();
+}
+} // namespace Aggregate
+
+namespace Array {
+// CHECK-LABEL: void local() {
+void local() {
+ // CHECK-NEXT: int arr[3] = {1, 2, 3};
+ int arr[3] = {1, 2, 3};
+ // CHECK-NEXT: auto [a, b, c]
+ auto [a, b, c] = arr;
+}
+} // namespace Array
+
+namespace TupleLike {
+struct Two {};
+Two getTwo();
+} // namespace TupleLike
+
+template <> struct std::tuple_size<TupleLike::Two> { enum { value = 2 }; };
+template <> struct std::tuple_element<0, TupleLike::Two> { using type = int; };
+template <> struct std::tuple_element<1, TupleLike::Two> { using type = int; };
+
+namespace TupleLike {
+// get() must be found by ADL, so it needs to live here, not at global scope.
+template <std::size_t N> int get(Two);
+
+// CHECK-LABEL: void local() {
+void local() {
+ // CHECK-NEXT: auto [p, q] = getTwo();
+ auto [p, q] = getTwo();
+}
+} // namespace TupleLike
+
+namespace NamespaceScope {
+using Aggregate::Pair;
+using Aggregate::get;
+
+// CHECK: auto [gx, gy] = get();
+auto [gx, gy] = get();
+// CHECK-NOT: {{^;$}}
+} // namespace NamespaceScope
>From a6772d4da6fc0a404c3af1cc4da80ba5f97ec100 Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 12:02:32 +0300
Subject: [PATCH 02/14] [clang] Fold printDeclType into printVarDeclSpecifiers
Both VisitVarDecl and VisitDecompositionDecl called printDeclType(T,
...) immediately after printVarDeclSpecifiers(D) returned T, and the
name argument in both cases is computable from D alone:
DecompositionDecl has no identifier (Id is null), and
NamedDecl::getName() already returns "" -- exactly the declarator
VisitDecompositionDecl wants. So the existing ternary, unmodified,
already produces the right name for all three cases (VarDecl,
ParmVarDecl, DecompositionDecl) without a separate parameter.
Move the printDeclType call into printVarDeclSpecifiers so it no
longer needs to return QualType across the function boundary; both
callers now just call printVarDeclSpecifiers(D) with no local T
needed.
Assisted-by: AI
---
clang/lib/AST/DeclPrinter.cpp | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index 6fa7c341ef9d5..3d79b5eb241fe 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -52,7 +52,7 @@ namespace {
void PrintObjCTypeParams(ObjCTypeParamList *Params);
void PrintOpenACCRoutineOnLambda(Decl *D);
- QualType printVarDeclSpecifiers(VarDecl *D);
+ void printVarDeclSpecifiers(VarDecl *D);
void printVarInitializer(VarDecl *D);
public:
@@ -972,7 +972,7 @@ void DeclPrinter::VisitLabelDecl(LabelDecl *D) {
Out << *D << ":";
}
-QualType DeclPrinter::printVarDeclSpecifiers(VarDecl *D) {
+void DeclPrinter::printVarDeclSpecifiers(VarDecl *D) {
prettyPrintPragmas(D);
if (std::optional<std::string> Attrs =
@@ -1015,16 +1015,16 @@ QualType DeclPrinter::printVarDeclSpecifiers(VarDecl *D) {
}
}
- return T;
-}
-
-void DeclPrinter::VisitVarDecl(VarDecl *D) {
- QualType T = printVarDeclSpecifiers(D);
-
+ // D->getName() is "" for a DecompositionDecl (it has no name of its own),
+ // which is exactly the declarator we want for one: just the type.
printDeclType(T, (isa<ParmVarDecl>(D) && Policy.CleanUglifiedParameters &&
D->getIdentifier())
? D->getIdentifier()->deuglifiedName()
: D->getName());
+}
+
+void DeclPrinter::VisitVarDecl(VarDecl *D) {
+ printVarDeclSpecifiers(D);
if (std::optional<std::string> Attrs =
prettyPrintAttributes(D, AttrPosAsWritten::Right))
@@ -1064,10 +1064,7 @@ void DeclPrinter::printVarInitializer(VarDecl *D) {
}
void DeclPrinter::VisitDecompositionDecl(DecompositionDecl *D) {
- QualType T = printVarDeclSpecifiers(D);
-
- // DecompositionDecl has no name of its own.
- printDeclType(T, "");
+ printVarDeclSpecifiers(D);
Out << " [";
bool First = true;
>From 85ee278033b37ab3205df3b559931c6f922b0d2c Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 12:38:37 +0300
Subject: [PATCH 03/14] [clang] Use llvm::ListSeparator instead of a
hand-rolled bool flag
Using existing practice instead of reimplementing it: DeclPrinter.cpp
already uses this idiom elsewhere for the same purpose.
Assisted-by: AI
---
clang/lib/AST/DeclPrinter.cpp | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index 3d79b5eb241fe..4f36862e58d72 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -1067,11 +1067,9 @@ void DeclPrinter::VisitDecompositionDecl(DecompositionDecl *D) {
printVarDeclSpecifiers(D);
Out << " [";
- bool First = true;
+ llvm::ListSeparator LS;
for (BindingDecl *B : D->bindings()) {
- if (!First)
- Out << ", ";
- First = false;
+ Out << LS;
// FIXME: this drops the leading "..." for a pack binding.
Out << B->getName();
}
>From 57f1f22caab4d00ed19acf2d4c584258f335559d Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 12:58:30 +0300
Subject: [PATCH 04/14] [clang] Fix ast-print-decomposition.cpp's
namespace-scope regression check
The check was non-functional in both directions:
- CHECK-NOT only ran after the CHECK: auto [gx, gy]... match, so a
regression reintroducing the bogus ';' statements before that line
would still pass (CHECK doesn't require adjacency).
- Independently, {{^;$}} could never match at all: the printed line is
indented (' ;', inside namespace NamespaceScope), and ^ anchors to
true column 0.
Add a CHECK-NOT before the CHECK too, and use
{{^[[:space:]]*;[[:space:]]*$}} so it actually matches an
indented-only-a-semicolon line. Verified against a reverted
VisitDeclContext fix: fails as expected with the bug reintroduced,
passes with it fixed.
Assisted-by: AI
---
clang/test/AST/ast-print-decomposition.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index aabfcad111bc5..b65140061b7ad 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -60,7 +60,8 @@ namespace NamespaceScope {
using Aggregate::Pair;
using Aggregate::get;
+// CHECK-NOT: {{^[[:space:]]*;[[:space:]]*$}}
// CHECK: auto [gx, gy] = get();
auto [gx, gy] = get();
-// CHECK-NOT: {{^;$}}
+// CHECK-NOT: {{^[[:space:]]*;[[:space:]]*$}}
} // namespace NamespaceScope
>From 92424a507fe1eae896700f48ac24aeb8e25065c3 Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 13:08:21 +0300
Subject: [PATCH 05/14] [clang] Print the leading "..." for a structured
binding pack
D->bindings() already exposes the pack as a single BindingDecl with
isParameterPack() set, for both the primary template and each
instantiation, so this is a one-line addition, not a design change.
Assisted-by: AI
---
clang/lib/AST/DeclPrinter.cpp | 3 ++-
clang/test/AST/ast-print-decomposition.cpp | 13 ++++++++++++-
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index 4f36862e58d72..d7a62f490fd7d 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -1070,7 +1070,8 @@ void DeclPrinter::VisitDecompositionDecl(DecompositionDecl *D) {
llvm::ListSeparator LS;
for (BindingDecl *B : D->bindings()) {
Out << LS;
- // FIXME: this drops the leading "..." for a pack binding.
+ if (B->isParameterPack())
+ Out << "...";
Out << B->getName();
}
Out << "]";
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index b65140061b7ad..efbfa8a215263 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -std=c++20 -ast-print %s | FileCheck %s
+// RUN: %clang_cc1 -std=c++26 -ast-print %s | FileCheck %s
// The `[a, b]` binding list must survive -ast-print, not just the type.
@@ -65,3 +65,14 @@ using Aggregate::get;
auto [gx, gy] = get();
// CHECK-NOT: {{^[[:space:]]*;[[:space:]]*$}}
} // namespace NamespaceScope
+
+namespace Packs {
+// CHECK-LABEL: void local() {
+template <unsigned N> void local() {
+ // CHECK-NEXT: int arr[4] = {1, 2, 3, 4};
+ int arr[4] = {1, 2, 3, 4};
+ // CHECK-NEXT: auto [first, ...rest, last]
+ auto [first, ...rest, last] = arr;
+}
+void (*p)() = local<0>;
+} // namespace Packs
>From a6ff2a82565faa5416df4f15c8fe3492308889df Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 13:13:11 +0300
Subject: [PATCH 06/14] [clang] Print attributes on individual structured
bindings
prettyPrintAttributes already takes a generic const Decl*, and
BindingDecl is-a Decl, so this is the same one-liner VisitVarDecl
already uses for its own right-position attributes, just called on
each binding instead of on D.
Assisted-by: AI
---
clang/lib/AST/DeclPrinter.cpp | 3 +++
clang/test/AST/ast-print-decomposition.cpp | 11 +++++++++++
2 files changed, 14 insertions(+)
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index d7a62f490fd7d..812bfa3d84eee 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -1073,6 +1073,9 @@ void DeclPrinter::VisitDecompositionDecl(DecompositionDecl *D) {
if (B->isParameterPack())
Out << "...";
Out << B->getName();
+ if (std::optional<std::string> Attrs =
+ prettyPrintAttributes(B, AttrPosAsWritten::Right))
+ Out << ' ' << *Attrs;
}
Out << "]";
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index efbfa8a215263..37e53745115c4 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -76,3 +76,14 @@ template <unsigned N> void local() {
}
void (*p)() = local<0>;
} // namespace Packs
+
+namespace Attributes {
+using Aggregate::Pair;
+using Aggregate::get;
+
+// CHECK-LABEL: void local() {
+void local() {
+ // CHECK-NEXT: auto [x {{\[\[}}maybe_unused{{\]\]}}, y] = get();
+ auto [x [[maybe_unused]], y] = get();
+}
+} // namespace Attributes
>From d39b4c49cb42da0cc90077d2a1fd2a32ee93ae41 Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 17:59:15 +0300
Subject: [PATCH 07/14] [clang][test] Loosen two Analysis CHECK lines pinned to
DecompositionDecl's spelling
cfg.cpp and anonymous-decls.cpp each dump a decomposition's DeclStmt
as one element of a larger CFG-shape / DeclRefExpr-printing test, not
as a test of the declaration's own printed form. Both CHECK lines
hard-coded that text exactly. Adding the binding list -- the actual
target of this PR -- broke both, even though neither test cares about
that text: they were asserting against wrong output, just because
something had to be printed there.
This commit loosens both lines to a wildcard over the binding list.
That's safe because the wildcard only spans text these tests never
examine -- it doesn't hide the DeclStmt element itself disappearing,
or anything about the surrounding CFG elements the tests do check; a
real regression there would still fail the match. So future changes
to declaration printing don't require touching unrelated CFG tests
again.
Assisted-by: AI
---
clang/test/Analysis/anonymous-decls.cpp | 2 +-
clang/test/Analysis/cfg.cpp | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/clang/test/Analysis/anonymous-decls.cpp b/clang/test/Analysis/anonymous-decls.cpp
index 705273328e6cc..88aa569a1212b 100644
--- a/clang/test/Analysis/anonymous-decls.cpp
+++ b/clang/test/Analysis/anonymous-decls.cpp
@@ -72,7 +72,7 @@ int main() {
// CHECK-NEXT: 2: [B3.1] (ImplicitCastExpr, FunctionToPointerDecay, iterator_traits<pair<int, int> *>::reference (*)(void))
// CHECK-NEXT: 3: __begin1
// CHECK-NEXT: 4: * [B3.3] (OperatorCall)
-// CHECK-NEXT: 5: auto &;
+// CHECK-NEXT: 5: auto &{{.*}};
// CHECK-NEXT: 6: get<0UL>
// CHECK-NEXT: 7: [B3.6] (ImplicitCastExpr, FunctionToPointerDecay, tuple_element<0L, pair<int, int> >::type (*)(pair<int, int> &))
// CHECK-NEXT: 8: decomposition-a-b
diff --git a/clang/test/Analysis/cfg.cpp b/clang/test/Analysis/cfg.cpp
index 2a88b73d27756..478e457d5dcd4 100644
--- a/clang/test/Analysis/cfg.cpp
+++ b/clang/test/Analysis/cfg.cpp
@@ -659,7 +659,7 @@ int crash_with_thread_local(char *p, int *q) {
// CHECK-NEXT: 5: [B1.3]{{\[\[}}B1.4]]
// CHECK-NEXT: 6: [B1.5] (ImplicitCastExpr, LValueToRValue, int)
// CHECK-NEXT: 7: {{\{}}[B1.6]{{(\})}}
-// CHECK-NEXT: 8: auto = {{\{}}arr[*]{{(\})}};
+// CHECK-NEXT: 8: auto{{.*}} = {{\{}}arr[*]{{(\})}};
void DecompositionDecl() {
int arr[2];
>From 4aa3278fb291a5e8f2cbdb9959530c670fdc35c2 Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 18:19:19 +0300
Subject: [PATCH 08/14] [clang] Combine the ObjCIvarDecl/BindingDecl skip
checks in VisitDeclContext
Both checks exist for the same reason -- each is printed later as part
of its containing decl instead -- and isa<> already supports multiple
types in one call, so there's no need for two separate checks and two
separate comments.
Assisted-by: AI
---
clang/lib/AST/DeclPrinter.cpp | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index 812bfa3d84eee..22c7c295011c1 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -460,14 +460,9 @@ void DeclPrinter::VisitDeclContext(DeclContext *DC, bool Indent) {
for (DeclContext::decl_iterator D = DC->decls_begin(), DEnd = DC->decls_end();
D != DEnd; ++D) {
- // Don't print ObjCIvarDecls, as they are printed when visiting the
- // containing ObjCInterfaceDecl.
- if (isa<ObjCIvarDecl>(*D))
- continue;
-
- // Don't print BindingDecls, as they are printed when visiting the
- // containing DecompositionDecl.
- if (isa<BindingDecl>(*D))
+ // Don't print ObjCIvarDecls or BindingDecls, as they are printed when
+ // visiting the containing ObjCInterfaceDecl or DecompositionDecl.
+ if (isa<ObjCIvarDecl, BindingDecl>(*D))
continue;
// Skip over implicit declarations in pretty-printing mode.
>From 92372303f6190693577b2433617efbd3316d3c76 Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 18:19:52 +0300
Subject: [PATCH 09/14] [clang][test] Verify the initializer prints in
ast-print-decomposition.cpp's Array case
The Array namespace's CHECK-NEXT only checked the binding list, not
that '= ...' after it was still there -- so the initializer could
have silently gone missing without failing the test.
Assisted-by: AI
---
clang/test/AST/ast-print-decomposition.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index 37e53745115c4..429b3feb22076 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -31,7 +31,7 @@ namespace Array {
void local() {
// CHECK-NEXT: int arr[3] = {1, 2, 3};
int arr[3] = {1, 2, 3};
- // CHECK-NEXT: auto [a, b, c]
+ // CHECK-NEXT: auto [a, b, c] = {{\{}}arr[*]{{(\})}};
auto [a, b, c] = arr;
}
} // namespace Array
>From cb58ff331ca6eaa77d319c5ad1dae69f76a8b8ed Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 18:20:06 +0300
Subject: [PATCH 10/14] [clang][test] Verify the initializer prints in
ast-print-decomposition.cpp's Packs case
Same gap as the Array case: the CHECK-NEXT only checked the binding
list, not that '= ...' after it was still there.
Assisted-by: AI
---
clang/test/AST/ast-print-decomposition.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index 429b3feb22076..504e4c23416f5 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -71,7 +71,7 @@ namespace Packs {
template <unsigned N> void local() {
// CHECK-NEXT: int arr[4] = {1, 2, 3, 4};
int arr[4] = {1, 2, 3, 4};
- // CHECK-NEXT: auto [first, ...rest, last]
+ // CHECK-NEXT: auto [first, ...rest, last] = {{\{}}arr[*]{{(\})}};
auto [first, ...rest, last] = arr;
}
void (*p)() = local<0>;
>From 2620457bc83ad6276c8414e07b1bf58cae487f1f Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 18:36:23 +0300
Subject: [PATCH 11/14] [clang][test] Don't escape the closing ]] in the
attribute CHECK line
Only [[ is FileCheck's own special syntax (a variable reference); a
closing ]] on its own has no special meaning and needs no escaping.
Existing tests already rely on exactly this (e.g.
clang/test/Sema/format-attr-missing.c), so match that instead of
over-escaping both brackets symmetrically.
Assisted-by: AI
---
clang/test/AST/ast-print-decomposition.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index 504e4c23416f5..a849fcbca2e10 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -83,7 +83,7 @@ using Aggregate::get;
// CHECK-LABEL: void local() {
void local() {
- // CHECK-NEXT: auto [x {{\[\[}}maybe_unused{{\]\]}}, y] = get();
+ // CHECK-NEXT: auto [x {{\[\[}}maybe_unused]], y] = get();
auto [x [[maybe_unused]], y] = get();
}
} // namespace Attributes
>From 3a98dd1aa0868c9e861ab8f6548634172fa9ead8 Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 20:37:25 +0300
Subject: [PATCH 12/14] [clang][test] Drop the unnecessary regex around the
Array case's initializer
{ and } are not special to FileCheck (only the doubled {{/}} regex
delimiter is), so the literal braces match without escaping.
Assisted-by: AI
---
clang/test/AST/ast-print-decomposition.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index a849fcbca2e10..235759a1e7493 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -31,7 +31,7 @@ namespace Array {
void local() {
// CHECK-NEXT: int arr[3] = {1, 2, 3};
int arr[3] = {1, 2, 3};
- // CHECK-NEXT: auto [a, b, c] = {{\{}}arr[*]{{(\})}};
+ // CHECK-NEXT: auto [a, b, c] = {arr[*]};
auto [a, b, c] = arr;
}
} // namespace Array
>From f7741812f4d69a9920a7ab198b06bf0626a525aa Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Tue, 8 Sep 2026 20:37:33 +0300
Subject: [PATCH 13/14] [clang][test] Drop the unnecessary regex around the
Packs case's initializer
Same as the Array case: { and } are literal to FileCheck outside a
{{/}} regex block, so no escaping is needed.
Assisted-by: AI
---
clang/test/AST/ast-print-decomposition.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index 235759a1e7493..47267c732a445 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -71,7 +71,7 @@ namespace Packs {
template <unsigned N> void local() {
// CHECK-NEXT: int arr[4] = {1, 2, 3, 4};
int arr[4] = {1, 2, 3, 4};
- // CHECK-NEXT: auto [first, ...rest, last] = {{\{}}arr[*]{{(\})}};
+ // CHECK-NEXT: auto [first, ...rest, last] = {arr[*]};
auto [first, ...rest, last] = arr;
}
void (*p)() = local<0>;
>From 993407e60762f81c57dc1ce684ba98e7bcdf0ef1 Mon Sep 17 00:00:00 2001
From: Ran Regev <regev.ran at gmail.com>
Date: Thu, 10 Sep 2026 15:11:46 +0300
Subject: [PATCH 14/14] [clang] Don't merge a decomposition declaration into a
preceding tag decl
VisitDeclContext groups a non-free-standing tag declaration with the
declarators that follow it, matching on each declaration's type being the
owned TagType of that tag. A decomposition declaration's deduced type can
be exactly that owned tag type, so
struct S { int a, b; } obj;
auto [x, y] = obj;
printed as:
struct S {
int a;
int b;
} obj, auto [x, y] = obj;
A decomposition declaration is always a declaration of its own; it can
never be one declarator among several, so keep it out of the group.
Assisted-by: AI
---
clang/lib/AST/DeclPrinter.cpp | 8 ++++++--
clang/test/AST/ast-print-decomposition.cpp | 18 ++++++++++++++++++
2 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index 22c7c295011c1..77b0c017f6ceb 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -487,9 +487,13 @@ void DeclPrinter::VisitDeclContext(DeclContext *DC, bool Indent) {
// only merges declarations directly referring to the tag, not typedefs.
//
// Check whether the current declaration should be grouped with a previous
- // non-free-standing tag declaration.
+ // non-free-standing tag declaration. A decomposition declaration is always
+ // a declaration of its own -- it can never be one declarator among several
+ // -- but its deduced type can be the tag type owned by the preceding
+ // declaration, so exclude it explicitly.
QualType CurDeclType = getDeclType(*D);
- if (!Decls.empty() && !CurDeclType.isNull()) {
+ if (!Decls.empty() && !CurDeclType.isNull() &&
+ !isa<DecompositionDecl>(*D)) {
QualType BaseType = GetBaseType(CurDeclType);
if (const auto *TT = dyn_cast_or_null<TagType>(BaseType);
TT && TT->isTagOwned()) {
diff --git a/clang/test/AST/ast-print-decomposition.cpp b/clang/test/AST/ast-print-decomposition.cpp
index 47267c732a445..294b2a606ca4b 100644
--- a/clang/test/AST/ast-print-decomposition.cpp
+++ b/clang/test/AST/ast-print-decomposition.cpp
@@ -87,3 +87,21 @@ void local() {
auto [x [[maybe_unused]], y] = get();
}
} // namespace Attributes
+
+namespace OwnedTag {
+// At declaration-context scope the printer groups a tag declaration with the
+// declarators that follow it, so an owned tag type keeps `struct Owned { ... }
+// obj;` on one line. A decomposition declaration is a declaration of its own
+// and must never be pulled into that group, even though its deduced type is
+// precisely the tag type owned by `obj`'s declaration.
+// CHECK-LABEL: struct Owned {
+// CHECK: } obj;
+// CHECK-NEXT: auto [ox, oy] = obj;
+struct Owned { int a, b; } obj;
+auto [ox, oy] = obj;
+
+// A second declarator of an owned tag type still merges.
+// CHECK-NEXT: struct Merged {
+// CHECK: } m1, m2;
+struct Merged { int v; } m1, m2;
+} // namespace OwnedTag
More information about the cfe-commits
mailing list