https://github.com/mpusz created 
https://github.com/llvm/llvm-project/pull/232956

Depends on #232949. This branch is stacked on it, so only the last commit 
belongs to this PR.

A template parameter object is uniqued by its canonical type and value, but it 
stores the type of the use that created it. Printing that type exposed the 
spelling of whichever use came first in diagnostics about unrelated code:

```cpp
#include <type_traits>
struct metre {};
inline constexpr metre m;
template<typename U> struct kilo_ {};
template<auto U> constexpr kilo_<std::remove_cvref_t<decltype(U)>> kilo;
template<auto V> struct quantity {};
template<auto V> constexpr quantity<V> make() { return {}; }
void take(int);
void f() { take(make<kilo<m>>()); }
```

Before: `no known conversion from 
'quantity<kilo_<std::remove_cvref_t<decltype(metre{})>>{}>' to 'int'`
After: `no known conversion from 'quantity<kilo_<metre>{}>' to 'int'`

This patch prints the canonical type instead. The stored spelling belongs to 
whichever use created the object, which depends on instantiation order and is 
often not the code being diagnosed.

A spelling written directly in a template argument, such as `quantity<km{}>`, 
is still printed as written. When the object reaches the printer through 
deduction or substitution, the canonical type is printed even if the code being 
diagnosed created it. With `using km = kilo_<metre>;` in namespace `ns`, such a 
use now prints `ns::kilo_<ns::metre>{}` instead of `km{}`. I think that trade 
is right because otherwise the printed spelling depends on which use happened 
to come first. Outside diagnostics, the `-ast-print` output of the 
specialization changes the same way. DWARF names already used the canonical 
type, so they do not change, and the diagnostics now match them. The 
template-diff printer and `TemplateArgument::print` each had a copy of this 
printing code, and now call `printAsExpr`.

A canonical class type prints with its full qualification, so four expectations 
in two existing tests now read `Diags::A{1, 2}`, 
`GH175831::CannotResolve0::reference<char>{}`, and 
`GH175831::CannotResolve1::reference<int>{}`. A new test covers both the 
ordinary and the template-diff printer. `check-clang` passes.

This covers the case reported in #96638, where the `decltype` comes from a 
library's variable template rather than from the code being diagnosed.

This patch was developed with the help of an AI assistant (Claude). I have 
reviewed and tested every change.


>From 1d71ab1d42acd9b7cfcd9690f996b271d1a5ef57 Mon Sep 17 00:00:00 2001
From: Mateusz Pusz <[email protected]>
Date: Fri, 16 Oct 2026 21:56:57 +0200
Subject: [PATCH 1/2] [clang] Print a value of an empty class as `{}`

APValue::printPretty prints a class value as a braced list of its bases and
fields, recursively. So a value of an empty class printed one pair of braces
per level of empty bases, such as `metre{{}}` for a class with one empty
base. An empty class has only one value, so these braces tell the reader
nothing. Print `{}` for a class for which CXXRecordDecl::isEmpty() holds. A
class that is not empty keeps its braces because they show which subobject
each value belongs to.

This also changes the -ast-print output and the DW_AT_name of such a
specialization in DWARF, for example from `quantity<ns::metre{{}}>` to
`quantity<ns::metre{}>`.

Fixes #88502

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 clang/lib/AST/APValue.cpp                     |  8 ++++-
 .../AST/ast-nttp-template-tree-compares.cpp   | 29 +++++++++++++++++--
 2 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/clang/lib/AST/APValue.cpp b/clang/lib/AST/APValue.cpp
index 97023be49c715b..1e33ffd4da8b11 100644
--- a/clang/lib/AST/APValue.cpp
+++ b/clang/lib/AST/APValue.cpp
@@ -1026,9 +1026,15 @@ void APValue::printPretty(raw_ostream &Out, const 
PrintingPolicy &Policy,
     return;
   }
   case APValue::Struct: {
+    const auto *RD = Ty->castAsRecordDecl();
+    // Every value of an empty class is the same value, so the nested braces of
+    // its (empty) bases carry no information.
+    if (const auto *CD = dyn_cast<CXXRecordDecl>(RD); CD && CD->isEmpty()) {
+      Out << "{}";
+      return;
+    }
     Out << '{';
     llvm::ListSeparator Comma;
-    const auto *RD = Ty->castAsRecordDecl();
     if (unsigned N = getStructNumBases()) {
       const CXXRecordDecl *CD = cast<CXXRecordDecl>(RD);
       CXXRecordDecl::base_class_const_iterator BI = CD->bases_begin();
diff --git a/clang/test/AST/ast-nttp-template-tree-compares.cpp 
b/clang/test/AST/ast-nttp-template-tree-compares.cpp
index a767e385b8f0ab..3cbb950344736f 100644
--- a/clang/test/AST/ast-nttp-template-tree-compares.cpp
+++ b/clang/test/AST/ast-nttp-template-tree-compares.cpp
@@ -9,8 +9,31 @@ template<auto> struct quantity { }; // #QUANT
 void use() {
   quantity<divided_units<metre{}, second{}>{}> q{};
   quantity<metre{}> q2 = q;
-  // expected-error@-1 {{no viable conversion from 
'quantity<divided_units<metre{}, second{}>{{}}>' to 'quantity<metre{{}}>'}}
-  // expected-note@#QUANT {{candidate constructor (the implicit copy 
constructor) not viable: no known conversion from 
'quantity<divided_units<metre{}, second{}>{}>' to 'const quantity<metre{{}}> &' 
for 1st argument}}
-  // expected-note@#QUANT {{candidate constructor (the implicit move 
constructor) not viable: no known conversion from 
'quantity<divided_units<metre{}, second{}>{}>' to 'quantity<metre{{}}> &&' for 
1st argument}}
+  // expected-error@-1 {{no viable conversion from 
'quantity<divided_units<metre{}, second{}>{}>' to 'quantity<metre{}>'}}
+  // expected-note@#QUANT {{candidate constructor (the implicit copy 
constructor) not viable: no known conversion from 
'quantity<divided_units<metre{}, second{}>{}>' to 'const quantity<metre{}> &' 
for 1st argument}}
+  // expected-note@#QUANT {{candidate constructor (the implicit move 
constructor) not viable: no known conversion from 
'quantity<divided_units<metre{}, second{}>{}>' to 'quantity<metre{}> &&' for 
1st argument}}
 }
 
+
+// An empty class has a single value, so the braces of its empty bases are not
+// printed, however deep the hierarchy.
+struct kilometre : metre { };
+template<auto> struct quantity2 { }; // #QUANT2
+
+void use_deep() {
+  quantity2<kilometre{}> q{};
+  quantity2<second{}> q2 = q;
+  // expected-error@-1 {{no viable conversion from 'quantity2<kilometre{}>' to 
'quantity2<second{}>'}}
+  // expected-note@#QUANT2 2{{candidate constructor}}
+}
+
+// A class with data members keeps the braces that position each subobject.
+struct tagged : empty { int v; };
+template<auto> struct quantity3 { }; // #QUANT3
+
+void use_non_empty() {
+  quantity3<tagged{{}, 1}> q{};
+  quantity3<tagged{{}, 2}> q2 = q;
+  // expected-error@-1 {{{no viable conversion from 'quantity3<tagged{{}, 1}>' 
to 'quantity3<tagged{{}, 2}>'}}}
+  // expected-note@#QUANT3 2{{candidate constructor}}
+}

>From 8e3a7f52b2d08f1684d4841081d3824ff8deed5b Mon Sep 17 00:00:00 2001
From: Mateusz Pusz <[email protected]>
Date: Fri, 16 Oct 2026 21:56:58 +0200
Subject: [PATCH 2/2] [clang] Print the canonical type of a template parameter
 object

A template parameter object is uniqued by its canonical type and value, but
it stores the type of the use that created it. Printing that type exposed
the spelling of whichever use came first, such as
`kilo_<std::remove_cvref_t<decltype(U)>>` from the declaration of a variable
template, in diagnostics about unrelated code. Print the canonical type
instead. The template-diff printer and TemplateArgument::print each had a
copy of this printing code, and now call printAsExpr.

A spelling written directly in a template argument, such as `quantity<km{}>`,
is still printed as written. When the object reaches the printer through
deduction or substitution, the canonical type is printed even if the code
being diagnosed created it. With `using km = kilo_<metre>;` in namespace
`ns`, such a use now prints `ns::kilo_<ns::metre>{}` instead of `km{}`. The
-ast-print output of the specialization changes the same way. DWARF names
already used the canonical type and do not change.

A canonical class type prints with its full qualification, so four
expectations in two existing tests now read `Diags::A{1, 2}`,
`GH175831::CannotResolve0::reference<char>{}`, and
`GH175831::CannotResolve1::reference<int>{}`.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 clang/lib/AST/ASTDiagnostic.cpp               |  3 +-
 clang/lib/AST/DeclTemplate.cpp                |  5 +++-
 clang/lib/AST/TemplateBase.cpp                |  3 +-
 clang/test/SemaTemplate/concepts.cpp          |  6 ++--
 .../SemaTemplate/temp_arg_nontype_cxx20.cpp   |  2 +-
 .../template-param-object-type-sugar.cpp      | 28 +++++++++++++++++++
 6 files changed, 38 insertions(+), 9 deletions(-)
 create mode 100644 clang/test/SemaTemplate/template-param-object-type-sugar.cpp

diff --git a/clang/lib/AST/ASTDiagnostic.cpp b/clang/lib/AST/ASTDiagnostic.cpp
index 0925fbdcbaeb7a..d67b376ec08597 100644
--- a/clang/lib/AST/ASTDiagnostic.cpp
+++ b/clang/lib/AST/ASTDiagnostic.cpp
@@ -1934,8 +1934,7 @@ class TemplateDiff {
         // FIXME: Diffing the APValue would be neat.
         // FIXME: Suppress this and use the full name of the declaration if the
         // parameter is a pointer or reference.
-        TPO->getType().getUnqualifiedType().print(OS, Policy);
-        TPO->printAsInit(OS, Policy);
+        TPO->printAsExpr(OS, Policy);
         return;
       }
       VD->printName(OS, Policy);
diff --git a/clang/lib/AST/DeclTemplate.cpp b/clang/lib/AST/DeclTemplate.cpp
index abcd0357409164..5ee1a878c62bb2 100644
--- a/clang/lib/AST/DeclTemplate.cpp
+++ b/clang/lib/AST/DeclTemplate.cpp
@@ -1675,7 +1675,10 @@ void 
TemplateParamObjectDecl::printAsExpr(llvm::raw_ostream &OS) const {
 
 void TemplateParamObjectDecl::printAsExpr(llvm::raw_ostream &OS,
                                           const PrintingPolicy &Policy) const {
-  getType().getUnqualifiedType().print(OS, Policy);
+  // Template parameter objects are uniqued by canonical type and value, so the
+  // sugar on getType() is whatever the first use happened to spell. It says
+  // nothing about the use being printed; print the canonical type instead.
+  getType().getCanonicalType().getUnqualifiedType().print(OS, Policy);
   printAsInit(OS, Policy);
 }
 
diff --git a/clang/lib/AST/TemplateBase.cpp b/clang/lib/AST/TemplateBase.cpp
index 9356bbadc7a48c..f2d087bc292391 100644
--- a/clang/lib/AST/TemplateBase.cpp
+++ b/clang/lib/AST/TemplateBase.cpp
@@ -555,8 +555,7 @@ void TemplateArgument::print(const PrintingPolicy &Policy, 
raw_ostream &Out,
     ValueDecl *VD = getAsDecl();
     if (getParamTypeForDecl()->isRecordType()) {
       if (auto *TPO = dyn_cast<TemplateParamObjectDecl>(VD)) {
-        TPO->getType().getUnqualifiedType().print(Out, Policy);
-        TPO->printAsInit(Out, Policy);
+        TPO->printAsExpr(Out, Policy);
         break;
       }
     }
diff --git a/clang/test/SemaTemplate/concepts.cpp 
b/clang/test/SemaTemplate/concepts.cpp
index 4e7001b5c519df..f45a4cad475290 100644
--- a/clang/test/SemaTemplate/concepts.cpp
+++ b/clang/test/SemaTemplate/concepts.cpp
@@ -2069,8 +2069,8 @@ template<auto V, representation_of<get_spec(V)>>
 struct quantity {};
 
 auto x = quantity<reference<char>{}, char>{};
-// expected-error@-1 {{constraints not satisfied for class template 'quantity' 
[with V = reference<char>{}, $1 = char]}}
-// expected-note@-5  {{because 'representation_of<char, 
get_spec(reference<char>{})>' evaluated to false}}
+// expected-error@-1 {{constraints not satisfied for class template 'quantity' 
[with V = GH175831::CannotResolve0::reference<char>{}, $1 = char]}}
+// expected-note@-5  {{because 'representation_of<char, 
get_spec(GH175831::CannotResolve0::reference<char>{})>' evaluated to false}}
 // expected-note-re@-7  {{because 'decltype({{.*}})' (aka 'char') does not 
satisfy 'repr_impl'}}
 // expected-note@-10  {{because 'sizeof(char) > sizeof(char)' (1 > 1) 
evaluated to false}}
 
@@ -2091,7 +2091,7 @@ template<auto V, representation_of<get_spec(V)>>
 struct quantity {};
 
 auto x = quantity<reference<int>{}, int>{};
-// expected-error@-1 {{constraints not satisfied for class template 'quantity' 
[with V = reference<int>{}, $1 = int]}}
+// expected-error@-1 {{constraints not satisfied for class template 'quantity' 
[with V = GH175831::CannotResolve1::reference<int>{}, $1 = int]}}
 // expected-note@-5 {{because 'representation_of<type-parameter-0-1, 
get_spec(V)>' evaluated to false}}
 // expected-note@-7 {{because 'decltype(V)' does not satisfy 'repr_impl'}}
 // expected-note@-7 {{because substituted constraint expression is ill-formed: 
non-type template argument is not a constant expression}}
diff --git a/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp 
b/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
index 924e27a7155a7b..7e37826bdea935 100644
--- a/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
+++ b/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
@@ -197,7 +197,7 @@ namespace Diags {
   struct A { int n, m; };
   template<A a> struct X { static_assert(a.n == a.m); }; // expected-error 
{{static assertion failed due to requirement 'Diags::A{1, 2}.n == Diags::A{1, 
2}.m'}} \
                                                          // expected-note 
{{evaluates to '1 == 2'}}
-  template struct X<A{1, 2}>; // expected-note {{in instantiation of template 
class 'Diags::X<A{1, 2}>' requested here}}
+  template struct X<A{1, 2}>; // expected-note {{in instantiation of template 
class 'Diags::X<Diags::A{1, 2}>' requested here}}
 }
 
 namespace CTADPartialOrder {
diff --git a/clang/test/SemaTemplate/template-param-object-type-sugar.cpp 
b/clang/test/SemaTemplate/template-param-object-type-sugar.cpp
new file mode 100644
index 00000000000000..4b22f0d71f6972
--- /dev/null
+++ b/clang/test/SemaTemplate/template-param-object-type-sugar.cpp
@@ -0,0 +1,28 @@
+// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
+
+// A template parameter object is shared by every use of the same value, so the
+// type it was created with carries the spelling of whichever use came first.
+// Diagnostics print its canonical type rather than that unrelated spelling.
+
+namespace lib {
+struct metre {};
+inline constexpr metre m;
+template <typename U> struct kilo_ {};
+template <typename T> struct remove_ref { using type = T; };
+template <auto U> constexpr kilo_<typename remove_ref<decltype(U)>::type> kilo;
+template <auto V> struct quantity {}; // #quantity
+template <auto V> constexpr quantity<V> make() { return {}; }
+} // namespace lib
+
+void take_int(int); // #take_int
+void take_metre(lib::quantity<lib::m>); // #take_metre
+
+void test() {
+  take_int(lib::make<lib::kilo<lib::m>>());
+  // expected-error@-1 {{no matching function for call to 'take_int'}}
+  // expected-note@#take_int {{candidate function not viable: no known 
conversion from 'quantity<lib::kilo_<lib::metre>{}>' to 'int' for 1st argument}}
+
+  take_metre(lib::make<lib::kilo<lib::m>>());
+  // expected-error@-1 {{no matching function for call to 'take_metre'}}
+  // expected-note@#take_metre {{candidate function not viable: no known 
conversion from 'quantity<lib::kilo_<lib::metre>{}>' to 
'quantity<lib::metre{}>' for 1st argument}}
+}

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to