[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