https://github.com/ChenMiaoi created 
https://github.com/llvm/llvm-project/pull/236740

Clang maps C23 `thread_local` to `_Thread_local` to retain C semantics. 
Diagnostics using the internal specifier therefore report `_Thread_local` when 
the source spells the keyword `thread_local`.

Track the C23 spelling in DeclSpec and use it for duplicate and invalid 
declaration diagnostics.

>From 67ecf76ffc09366e739ee14668af346057b2db94 Mon Sep 17 00:00:00 2001
From: Chen Miao <[email protected]>
Date: Thu, 29 Oct 2026 19:49:00 +0800
Subject: [PATCH] [clang][C23] Preserve `thread_local` spelling in diagnostics

Clang maps C23 `thread_local` to `_Thread_local` to retain C semantics.
Diagnostics using the internal specifier therefore report
`_Thread_local` when the source spells the keyword `thread_local`.

Track the C23 spelling in DeclSpec and use it for duplicate and invalid
declaration diagnostics.
---
 clang/include/clang/Sema/DeclSpec.h |  9 +++++++--
 clang/lib/Parse/ParseDecl.cpp       |  5 +----
 clang/lib/Sema/DeclSpec.cpp         | 25 ++++++++++++++++++-------
 clang/lib/Sema/SemaDecl.cpp         | 18 +++++++++---------
 clang/lib/Sema/SemaDeclObjC.cpp     |  4 ++--
 clang/test/Sema/constexpr.c         |  8 ++++----
 clang/test/Sema/thread_local.c      |  8 ++++++--
 7 files changed, 47 insertions(+), 30 deletions(-)

diff --git a/clang/include/clang/Sema/DeclSpec.h 
b/clang/include/clang/Sema/DeclSpec.h
index 7b58bfcf53d98a..5bcf8f5f81c17f 100644
--- a/clang/include/clang/Sema/DeclSpec.h
+++ b/clang/include/clang/Sema/DeclSpec.h
@@ -342,6 +342,8 @@ class DeclSpec {
   LLVM_PREFERRED_TYPE(TSCS)
   unsigned ThreadStorageClassSpec : 2;
   LLVM_PREFERRED_TYPE(bool)
+  unsigned C23ThreadLocalSpelling : 1;
+  LLVM_PREFERRED_TYPE(bool)
   unsigned SCS_extern_in_linkage_spec : 1;
 
   // type-specifier
@@ -470,7 +472,7 @@ class DeclSpec {
 
   DeclSpec(AttributeFactory &attrFactory)
       : StorageClassSpec(SCS_unspecified),
-        ThreadStorageClassSpec(TSCS_unspecified),
+        ThreadStorageClassSpec(TSCS_unspecified), 
C23ThreadLocalSpelling(false),
         SCS_extern_in_linkage_spec(false),
         TypeSpecWidth(static_cast<unsigned>(TypeSpecifierWidth::Unspecified)),
         TypeSpecComplex(TSC_unspecified),
@@ -491,6 +493,7 @@ class DeclSpec {
   TSCS getThreadStorageClassSpec() const {
     return (TSCS)ThreadStorageClassSpec;
   }
+  const char *getThreadStorageClassSpecName() const;
   bool isExternInLinkageSpec() const { return SCS_extern_in_linkage_spec; }
   void setExternInLinkageSpec(bool Value) {
     SCS_extern_in_linkage_spec = Value;
@@ -504,6 +507,7 @@ class DeclSpec {
   void ClearStorageClassSpecs() {
     StorageClassSpec           = DeclSpec::SCS_unspecified;
     ThreadStorageClassSpec     = DeclSpec::TSCS_unspecified;
+    C23ThreadLocalSpelling = false;
     SCS_extern_in_linkage_spec = false;
     StorageClassSpecLoc        = SourceLocation();
     ThreadStorageClassSpecLoc  = SourceLocation();
@@ -733,7 +737,8 @@ class DeclSpec {
                            const char *&PrevSpec, unsigned &DiagID,
                            const PrintingPolicy &Policy);
   bool SetStorageClassSpecThread(TSCS TSC, SourceLocation Loc,
-                                 const char *&PrevSpec, unsigned &DiagID);
+                                 const char *&PrevSpec, unsigned &DiagID,
+                                 bool IsC23ThreadLocal = false);
   bool SetTypeSpecWidth(TypeSpecifierWidth W, SourceLocation Loc,
                         const char *&PrevSpec, unsigned &DiagID,
                         const PrintingPolicy &Policy);
diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp
index e452c312702491..4a453e72dcae40 100644
--- a/clang/lib/Parse/ParseDecl.cpp
+++ b/clang/lib/Parse/ParseDecl.cpp
@@ -4217,13 +4217,10 @@ void Parser::ParseDeclarationSpecifiers(
         Diag(Tok, diag::warn_c23_compat_keyword) << Tok.getName();
       // We map thread_local to _Thread_local in C23 mode so it retains the C
       // semantics rather than getting the C++ semantics.
-      // FIXME: diagnostics will show _Thread_local when the user wrote
-      // thread_local in source in C23 mode; we need some general way to
-      // identify which way the user spelled the keyword in source.
       isInvalid = DS.SetStorageClassSpecThread(
           getLangOpts().C23 ? DeclSpec::TSCS__Thread_local
                             : DeclSpec::TSCS_thread_local,
-          Loc, PrevSpec, DiagID);
+          Loc, PrevSpec, DiagID, getLangOpts().C23);
       isStorageClass = true;
       break;
     case tok::kw__Thread_local:
diff --git a/clang/lib/Sema/DeclSpec.cpp b/clang/lib/Sema/DeclSpec.cpp
index dfa7a79d1f92b2..3dc7e284ea8780 100644
--- a/clang/lib/Sema/DeclSpec.cpp
+++ b/clang/lib/Sema/DeclSpec.cpp
@@ -496,6 +496,11 @@ const char *DeclSpec::getSpecifierName(DeclSpec::TSCS S) {
   llvm_unreachable("Unknown typespec!");
 }
 
+const char *DeclSpec::getThreadStorageClassSpecName() const {
+  return C23ThreadLocalSpelling ? "thread_local"
+                                : 
getSpecifierName(getThreadStorageClassSpec());
+}
+
 const char *DeclSpec::getSpecifierName(TypeSpecifierWidth W) {
   switch (W) {
   case TypeSpecifierWidth::Unspecified:
@@ -698,11 +703,16 @@ bool DeclSpec::SetStorageClassSpec(Sema &S, SCS SC, 
SourceLocation Loc,
 
 bool DeclSpec::SetStorageClassSpecThread(TSCS TSC, SourceLocation Loc,
                                          const char *&PrevSpec,
-                                         unsigned &DiagID) {
-  if (ThreadStorageClassSpec != TSCS_unspecified)
-    return BadSpecifier(TSC, (TSCS)ThreadStorageClassSpec, PrevSpec, DiagID);
+                                         unsigned &DiagID,
+                                         bool IsC23ThreadLocal) {
+  if (ThreadStorageClassSpec != TSCS_unspecified) {
+    BadSpecifier(TSC, (TSCS)ThreadStorageClassSpec, PrevSpec, DiagID);
+    PrevSpec = getThreadStorageClassSpecName();
+    return true;
+  }
 
   ThreadStorageClassSpec = TSC;
+  C23ThreadLocalSpelling = IsC23ThreadLocal;
   ThreadStorageClassSpecLoc = Loc;
   return false;
 }
@@ -1390,9 +1400,9 @@ void DeclSpec::CheckTypeSpec(Sema &S, const 
PrintingPolicy &Policy) {
       if (S.getSourceManager().isBeforeInTranslationUnit(
             getThreadStorageClassSpecLoc(), getStorageClassSpecLoc()))
         S.Diag(getStorageClassSpecLoc(),
-             diag::err_invalid_decl_spec_combination)
-          << DeclSpec::getSpecifierName(getThreadStorageClassSpec())
-          << SourceRange(getThreadStorageClassSpecLoc());
+               diag::err_invalid_decl_spec_combination)
+            << getThreadStorageClassSpecName()
+            << SourceRange(getThreadStorageClassSpecLoc());
       else
         S.Diag(getThreadStorageClassSpecLoc(),
              diag::err_invalid_decl_spec_combination)
@@ -1400,12 +1410,13 @@ void DeclSpec::CheckTypeSpec(Sema &S, const 
PrintingPolicy &Policy) {
           << SourceRange(getStorageClassSpecLoc());
       // Discard the thread storage class specifier to recover.
       ThreadStorageClassSpec = TSCS_unspecified;
+      C23ThreadLocalSpelling = false;
       ThreadStorageClassSpecLoc = SourceLocation();
     }
     if (S.getLangOpts().C23 &&
         getConstexprSpecifier() == ConstexprSpecKind::Constexpr) {
       S.Diag(ConstexprLoc, diag::err_invalid_decl_spec_combination)
-          << DeclSpec::getSpecifierName(getThreadStorageClassSpec())
+          << getThreadStorageClassSpecName()
           << SourceRange(getThreadStorageClassSpecLoc());
     }
   }
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index f3d38a555a524a..446d9bab4ec448 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -5539,9 +5539,9 @@ Decl *Sema::ParsedFreeStandingDeclSpec(Scope *S, 
AccessSpecifier AS,
         << DeclSpec::getSpecifierName(SCS);
   }
 
-  if (DeclSpec::TSCS TSCS = DS.getThreadStorageClassSpec())
+  if (DS.getThreadStorageClassSpec())
     Diag(DS.getThreadStorageClassSpecLoc(), DiagID)
-      << DeclSpec::getSpecifierName(TSCS);
+        << DS.getThreadStorageClassSpecName();
   if (DS.getTypeQualifiers()) {
     if (DS.getTypeQualifiers() & DeclSpec::TQ_const)
       Diag(DS.getConstSpecLoc(), DiagID) << "const";
@@ -8220,7 +8220,7 @@ NamedDecl *Sema::ActOnVariableDeclarator(
          !DC->isFunctionOrMethod()))
       Diag(D.getDeclSpec().getThreadStorageClassSpecLoc(),
            diag::err_thread_non_global)
-        << DeclSpec::getSpecifierName(TSCS);
+          << D.getDeclSpec().getThreadStorageClassSpecName();
     else if (!Context.getTargetInfo().isTLSSupported()) {
       if (getLangOpts().CUDA || getLangOpts().isTargetDevice()) {
         // Postpone error emission until we've collected attributes required to
@@ -10297,10 +10297,10 @@ Sema::ActOnFunctionDeclarator(Scope *S, Declarator 
&D, DeclContext *DC,
   DeclarationName Name = NameInfo.getName();
   StorageClass SC = getFunctionStorageClass(*this, D);
 
-  if (DeclSpec::TSCS TSCS = D.getDeclSpec().getThreadStorageClassSpec())
+  if (D.getDeclSpec().getThreadStorageClassSpec())
     Diag(D.getDeclSpec().getThreadStorageClassSpecLoc(),
          diag::err_invalid_thread)
-      << DeclSpec::getSpecifierName(TSCS);
+        << D.getDeclSpec().getThreadStorageClassSpecName();
 
   if (D.isFirstDeclarationOfMember())
     adjustMemberFunctionCC(
@@ -15975,9 +15975,9 @@ Decl *Sema::ActOnParamDeclarator(Scope *S, Declarator 
&D,
     D.getMutableDeclSpec().ClearStorageClassSpecs();
   }
 
-  if (DeclSpec::TSCS TSCS = DS.getThreadStorageClassSpec())
+  if (DS.getThreadStorageClassSpec())
     Diag(DS.getThreadStorageClassSpecLoc(), diag::err_invalid_thread)
-      << DeclSpec::getSpecifierName(TSCS);
+        << DS.getThreadStorageClassSpecName();
   if (DS.isInlineSpecified())
     Diag(DS.getInlineSpecLoc(), diag::err_inline_non_function)
         << getLangOpts().CPlusPlus17;
@@ -19601,10 +19601,10 @@ FieldDecl *Sema::HandleField(Scope *S, RecordDecl 
*Record,
   if (D.getDeclSpec().isInlineSpecified())
     Diag(D.getDeclSpec().getInlineSpecLoc(), diag::err_inline_non_function)
         << getLangOpts().CPlusPlus17;
-  if (DeclSpec::TSCS TSCS = D.getDeclSpec().getThreadStorageClassSpec())
+  if (D.getDeclSpec().getThreadStorageClassSpec())
     Diag(D.getDeclSpec().getThreadStorageClassSpecLoc(),
          diag::err_invalid_thread)
-      << DeclSpec::getSpecifierName(TSCS);
+        << D.getDeclSpec().getThreadStorageClassSpecName();
 
   // Check to see if this name was declared as a member previously
   NamedDecl *PrevDecl = nullptr;
diff --git a/clang/lib/Sema/SemaDeclObjC.cpp b/clang/lib/Sema/SemaDeclObjC.cpp
index 4a6fc658575d9c..4e5855cb381257 100644
--- a/clang/lib/Sema/SemaDeclObjC.cpp
+++ b/clang/lib/Sema/SemaDeclObjC.cpp
@@ -5220,10 +5220,10 @@ Decl *SemaObjC::ActOnObjCExceptionDecl(Scope *S, 
Declarator &D) {
   if (DS.isInlineSpecified())
     Diag(DS.getInlineSpecLoc(), diag::err_inline_non_function)
         << getLangOpts().CPlusPlus17;
-  if (DeclSpec::TSCS TSCS = D.getDeclSpec().getThreadStorageClassSpec())
+  if (D.getDeclSpec().getThreadStorageClassSpec())
     Diag(D.getDeclSpec().getThreadStorageClassSpecLoc(),
          diag::err_invalid_thread)
-     << DeclSpec::getSpecifierName(TSCS);
+        << D.getDeclSpec().getThreadStorageClassSpecName();
   D.getMutableDeclSpec().ClearStorageClassSpecs();
 
   SemaRef.DiagnoseFunctionSpecifiers(D.getDeclSpec());
diff --git a/clang/test/Sema/constexpr.c b/clang/test/Sema/constexpr.c
index 3bb8cffe16ab21..21e15c84d30cd3 100644
--- a/clang/test/Sema/constexpr.c
+++ b/clang/test/Sema/constexpr.c
@@ -54,12 +54,12 @@ void f3(constexpr register int P1) { // expected-error 
{{function parameter cann
   constexpr register auto V10 = 0.0;
 }
 
-constexpr thread_local int V11 = 38; // expected-error {{cannot combine with 
previous '_Thread_local' declaration specifier}}
-constexpr static thread_local double V12 = 38; // expected-error {{cannot 
combine with previous '_Thread_local' declaration specifier}}
-constexpr extern thread_local char V13; // expected-error {{cannot combine 
with previous '_Thread_local' declaration specifier}}
+constexpr thread_local int V11 = 38; // expected-error {{cannot combine with 
previous 'thread_local' declaration specifier}}
+constexpr static thread_local double V12 = 38; // expected-error {{cannot 
combine with previous 'thread_local' declaration specifier}}
+constexpr extern thread_local char V13; // expected-error {{cannot combine 
with previous 'thread_local' declaration specifier}}
 // expected-error@-1 {{cannot combine with previous 'constexpr' declaration 
specifier}}
 // expected-error@-2 {{constexpr variable declaration must be a definition}}
-constexpr thread_local short V14 = 38; // expected-error {{cannot combine with 
previous '_Thread_local' declaration specifier}}
+constexpr thread_local short V14 = 38; // expected-error {{cannot combine with 
previous 'thread_local' declaration specifier}}
 
 // Check how constexpr works with qualifiers.
 constexpr _Atomic int V15 = 0; // expected-error {{constexpr variable cannot 
have type 'const _Atomic(int)'}}
diff --git a/clang/test/Sema/thread_local.c b/clang/test/Sema/thread_local.c
index b65f1119c73831..78d80740bceabe 100644
--- a/clang/test/Sema/thread_local.c
+++ b/clang/test/Sema/thread_local.c
@@ -4,8 +4,7 @@
 // Ensure that thread_local and _Thread_local are synonyms in C23 and both
 // restrict local variables to be explicitly static or extern.
 void func(void) {
-  // FIXME: it would be nice if the diagnostic said 'thread_local' in this 
case.
-  thread_local int i = 12;  // expected-error {{'_Thread_local' variables must 
have global storage}}
+  thread_local int i = 12; // expected-error {{'thread_local' variables must 
have global storage}}
   _Thread_local int j = 13; // expected-error {{'_Thread_local' variables must 
have global storage}}
 
   static thread_local int k = 14;
@@ -18,3 +17,8 @@ void func(void) {
 // This would previously fail because the tls models were different.
 extern thread_local unsigned a;
 _Thread_local unsigned a = 0;
+
+thread_local _Thread_local int duplicate1;
+// expected-warning@-1 {{duplicate 'thread_local' declaration specifier}}
+_Thread_local thread_local int duplicate2;
+// expected-warning@-1 {{duplicate '_Thread_local' declaration specifier}}

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

Reply via email to