Skip to content

[Clang][AST] Introduce ExplicitInstantiationDecl to preserve source info and fix diagnostic locations - #191658

Merged
mizvekov merged 33 commits into
llvm:mainfrom
16bit-ykiko:fix-explicit-instantiation
Apr 22, 2026
Merged

mizvekov merged 33 commits into
llvm:mainfrom
16bit-ykiko:fix-explicit-instantiation

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Apr 11, 2026 •

Copy link
Copy Markdown
Contributor

This is the initial fix of #191442. Following the discussion here #115418 (comment).

Description

This PR introduces a new AST node, ExplicitInstantiationDecl, to systematically fix the long-standing issue of missing or incorrect source location information for explicit template instantiations.

Background & The Problem

Historically, Clang's AST lacked a dedicated node to represent the lexical occurrence of an explicit instantiation statement. Instead, Sema tried to shoehorn this information into existing specialization nodes (e.g., FunctionDecl, VarTemplateSpecializationDecl) or simply returned nullptr.

This resulted in fragmented behavior across the seven instantiable entity types:

  • Function & Member Function Templates: Returned nullptr, completely losing SourceRange and NestedNameSpecifier information.
  • Member Functions & Static Data Members: Mutated existing nodes in-place. Consequently, multiple template or extern template declarations in the same file would overwrite each other's source locations.
  • Variable Templates: Suffered from dyn_cast bugs and dropped NNS information.

Design Trade-offs Evaluated

Before settling on the current design, I evaluated a mixed redeclaration-chain approach (similar to how explicit specializations are handled, creating new FunctionDecl nodes and stitching them into the redecl chain). However, this approach had significant flaws:

  1. Inconsistency: It couldn't be cleanly applied to member functions or static data members due to DeclContext constraints (e.g., a member function shouldn't lexically reside in a namespace DeclContext, but placing it in the class context would pollute member lookup).
  2. Fragility: It required bypassing standard FoldingSet mechanisms (setFunctionTemplateSpecialization).
  3. Lookup Pollution: Injecting new NamedDecl nodes purely for instantiations risked breaking downstream ASTMatchers and altering name lookup behavior.

To avoid these pitfalls, this PR introduces ExplicitInstantiationDecl as a purely lexical annotation node.

Key Design Characteristics:

  1. Inherits from Decl, not NamedDecl: This is the most crucial design choice. Much like StaticAssertDecl or FriendDecl, this node lives in a DeclContext (making it traversable by RecursiveASTVisitor and visible in AST dumps) but remains completely invisible to C++ name lookup. It does not interfere with overload resolution or lookup tables.
  2. Unified Representation: A single node type now covers all seven entity types. It holds a pointer (Specialization) to the underlying instantiated declaration, unifying how functions, variables, classes, and members are handled.
  3. Lexical Fidelity: The node resides in the enclosing namespace or Translation Unit where the explicit instantiation was actually written, perfectly preserving the SourceRange, NestedNameSpecifierLoc, and the exact locations of the template and extern keywords.

Assisted-by: Claude Code (Anthropic) — used for test writing and checking test results

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules clang:codegen IR generation bugs: mangling, exceptions, etc. clang:as-a-library libclang and C++ API ClangIR Anything related to the ClangIR project labels Apr 11, 2026
@llvmbot

llvmbot commented Apr 11, 2026 •

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-clang-tidy
@llvm/pr-subscribers-clang-tools-extra
@llvm/pr-subscribers-clang-modules
@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clangir

Author: ykiko (16bit-ykiko)

Changes

This is the initial fix of #191442. Following the discussion here #115418 (comment).

Description

This PR introduces a new AST node, ExplicitInstantiationDecl, to systematically fix the long-standing issue of missing or incorrect source location information for explicit template instantiations (Issue #191442). As a direct result of this new architecture, it also resolves Issue #21133 by ensuring that duplicate explicit instantiation diagnostics point to the actual lexical occurrence rather than the implicit point of instantiation.

Background & The Problem

Historically, Clang's AST lacked a dedicated node to represent the lexical occurrence of an explicit instantiation statement. Instead, Sema tried to shoehorn this information into existing specialization nodes (e.g., FunctionDecl, VarTemplateSpecializationDecl) or simply returned nullptr.

This resulted in fragmented behavior across the seven instantiable entity types:

  • Function & Member Function Templates: Returned nullptr, completely losing SourceRange and NestedNameSpecifier information.
  • Member Functions & Static Data Members: Mutated existing nodes in-place. Consequently, multiple template or extern template declarations in the same file would overwrite each other's source locations.
  • Variable Templates: Suffered from dyn_cast bugs and dropped NNS information.

Design Trade-offs Evaluated

Before settling on the current design, I evaluated a mixed redeclaration-chain approach (similar to how explicit specializations are handled, creating new FunctionDecl nodes and stitching them into the redecl chain). However, this approach had significant flaws:

  1. Inconsistency: It couldn't be cleanly applied to member functions or static data members due to DeclContext constraints (e.g., a member function shouldn't lexically reside in a namespace DeclContext, but placing it in the class context would pollute member lookup).
  2. Fragility: It required bypassing standard FoldingSet mechanisms (setFunctionTemplateSpecialization).
  3. Lookup Pollution: Injecting new NamedDecl nodes purely for instantiations risked breaking downstream ASTMatchers and altering name lookup behavior.

The Chosen Architecture: ExplicitInstantiationDecl

To avoid these pitfalls, this PR introduces ExplicitInstantiationDecl as a purely lexical annotation node.

Key Design Characteristics:

  1. Inherits from Decl, not NamedDecl: This is the most crucial design choice. Much like StaticAssertDecl or FriendDecl, this node lives in a DeclContext (making it traversable by RecursiveASTVisitor and visible in AST dumps) but remains completely invisible to C++ name lookup. It does not interfere with overload resolution or lookup tables.
  2. Unified Representation: A single node type now covers all seven entity types. It holds a pointer (Specialization) to the underlying instantiated declaration, unifying how functions, variables, classes, and members are handled.
  3. Lexical Fidelity: The node resides in the enclosing namespace or Translation Unit where the explicit instantiation was actually written, perfectly preserving the SourceRange, NestedNameSpecifierLoc, and the exact locations of the template and extern keywords.
  • Fix #21040
  • Fix #52659
  • Fix #115418
  • Fix #14230
  • Fix #21133 (Diagnostic Locations)
    > Because every explicit instantiation is now explicitly recorded in its DeclContext, DiagLocForExplicitInstantiation no longer relies on the often-inaccurate PointOfInstantiation (which frequently points to implicit instantiation sites). Instead, it searches the DeclContext hierarchy to find the corresponding ExplicitInstantiationDecl. As a result, the note: previous explicit instantiation is here diagnostic now correctly points to the exact lexical location.

Patch is 48.70 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/191658.diff

25 Files Affected:

  • (modified) clang/include/clang/AST/ASTNodeTraverser.h (+11)
  • (modified) clang/include/clang/AST/DeclTemplate.h (+116)
  • (modified) clang/include/clang/AST/JSONNodeDumper.h (+1)
  • (modified) clang/include/clang/AST/RecursiveASTVisitor.h (+14-2)
  • (modified) clang/include/clang/AST/TextNodeDumper.h (+1)
  • (modified) clang/include/clang/Basic/DeclNodes.td (+1)
  • (modified) clang/include/clang/Serialization/ASTBitCodes.h (+4-1)
  • (modified) clang/lib/AST/DeclTemplate.cpp (+20)
  • (modified) clang/lib/AST/JSONNodeDumper.cpp (+26)
  • (modified) clang/lib/AST/TextNodeDumper.cpp (+14)
  • (modified) clang/lib/CIR/CodeGen/CIRGenDecl.cpp (+1)
  • (modified) clang/lib/CIR/CodeGen/CIRGenModule.cpp (+1)
  • (modified) clang/lib/CodeGen/CGDecl.cpp (+1)
  • (modified) clang/lib/CodeGen/CodeGenModule.cpp (+1)
  • (modified) clang/lib/Sema/SemaTemplate.cpp (+87-15)
  • (modified) clang/lib/Sema/SemaTemplateInstantiateDecl.cpp (+8)
  • (modified) clang/lib/Serialization/ASTCommon.cpp (+1)
  • (modified) clang/lib/Serialization/ASTReaderDecl.cpp (+20)
  • (modified) clang/lib/Serialization/ASTWriterDecl.cpp (+18)
  • (modified) clang/lib/Tooling/Syntax/BuildTree.cpp (+7)
  • (modified) clang/test/AST/ast-dump-templates-pattern.cpp (+26-6)
  • (modified) clang/test/AST/ast-dump-templates.cpp (+50-12)
  • (added) clang/test/AST/explicit-instantiation-source-info.cpp (+166)
  • (added) clang/test/SemaTemplate/explicit-instantiation-diag-location.cpp (+31)
  • (modified) clang/tools/libclang/CIndex.cpp (+2)
diff --git a/clang/include/clang/AST/ASTNodeTraverser.h b/clang/include/clang/AST/ASTNodeTraverser.h
index 3be24ff868c2d..39a6864bf169e 100644
--- a/clang/include/clang/AST/ASTNodeTraverser.h
+++ b/clang/include/clang/AST/ASTNodeTraverser.h
@@ -683,6 +683,17 @@ class ASTNodeTraverser
     Visit(D->getMessage());
   }
 
+  void VisitExplicitInstantiationDecl(const ExplicitInstantiationDecl *D) {
+    // The specialization is already elsewhere in the AST; don't re-traverse it.
+    // Traverse source-location sub-nodes: template arguments and type-as-written.
+    if (const auto *ArgsAsWritten = D->getTemplateArgsAsWritten())
+      for (unsigned I = 0, E = ArgsAsWritten->NumTemplateArgs; I != E; ++I)
+        Visit((*ArgsAsWritten)[I].getArgument(),
+              (*ArgsAsWritten)[I].getSourceRange());
+    if (TypeSourceInfo *TSI = D->getTypeAsWritten())
+      Visit(TSI->getTypeLoc());
+  }
+
   void VisitFunctionTemplateDecl(const FunctionTemplateDecl *D) {
     dumpTemplateDecl(D);
   }
diff --git a/clang/include/clang/AST/DeclTemplate.h b/clang/include/clang/AST/DeclTemplate.h
index a4a1bb9c13c79..b470b520531f1 100644
--- a/clang/include/clang/AST/DeclTemplate.h
+++ b/clang/include/clang/AST/DeclTemplate.h
@@ -3405,6 +3405,122 @@ getReplacedTemplateParameter(Decl *D, unsigned Index);
 /// If we have an implicit instantiation, adjust 'D' to refer to template.
 const Decl &adjustDeclToTemplate(const Decl &D);
 
+/// Represents an explicit instantiation of a template entity in source code.
+///
+/// This node records source location information for an explicit instantiation
+/// statement. It does not participate in name lookup (inherits from Decl, not
+/// NamedDecl), and does not affect code generation. The underlying
+/// specialization decl (FunctionDecl, VarDecl, CXXRecordDecl, etc.) continues
+/// to handle all semantic and codegen responsibilities.
+///
+/// \code
+///   template void ns::foo<int>(int);        // function template
+///   extern template struct ns::S<int>;      // class template (extern)
+///   template int ns::bar<int>;              // variable template
+///   template void ns::S<int>::method(int);  // member function
+/// \endcode
+class ExplicitInstantiationDecl : public Decl {
+  /// The underlying specialization being explicitly instantiated.
+  NamedDecl *Specialization = nullptr;
+
+  /// The source range of the entire explicit instantiation statement.
+  SourceRange Range;
+
+  /// Location of the 'extern' keyword (invalid if not extern template).
+  SourceLocation ExternLoc;
+
+  /// Location of the struct/class/union keyword (for class template and
+  /// nested class instantiations; invalid otherwise).
+  SourceLocation TagKWLoc;
+
+  /// Nested name specifier with source locations (e.g., ns::S<int>::).
+  NestedNameSpecifierLoc QualifierLoc;
+
+  /// Template arguments as written (e.g., <int>). Null if the template
+  /// arguments were deduced.
+  const ASTTemplateArgumentListInfo *TemplateArgsAsWritten = nullptr;
+
+  /// Location of the entity name (e.g., 'foo' in 'template void ns::foo<int>(int)').
+  SourceLocation NameLoc;
+
+  /// Type source info for the declaration type:
+  ///   - Function templates / member functions: FunctionProtoTypeLoc with
+  ///     return type location and parameter type locations.
+  ///   - Variable templates / static data members: the declared type.
+  ///   - Class templates / nested classes: null.
+  TypeSourceInfo *TypeAsWritten = nullptr;
+
+  /// Whether this is a declaration (extern template) or definition (template).
+  LLVM_PREFERRED_TYPE(TemplateSpecializationKind)
+  unsigned TSK : 3;
+
+  ExplicitInstantiationDecl(DeclContext *DC, SourceRange Range,
+                            NamedDecl *Specialization,
+                            SourceLocation ExternLoc,
+                            SourceLocation TemplateLoc,
+                            SourceLocation TagKWLoc,
+                            NestedNameSpecifierLoc QualifierLoc,
+                            const ASTTemplateArgumentListInfo *ArgsAsWritten,
+                            SourceLocation NameLoc,
+                            TypeSourceInfo *TypeAsWritten,
+                            TemplateSpecializationKind TSK)
+      : Decl(ExplicitInstantiation, DC, TemplateLoc),
+        Specialization(Specialization), Range(Range), ExternLoc(ExternLoc),
+        TagKWLoc(TagKWLoc), QualifierLoc(QualifierLoc),
+        TemplateArgsAsWritten(ArgsAsWritten), NameLoc(NameLoc),
+        TypeAsWritten(TypeAsWritten), TSK(TSK) {
+    assert((TSK == TSK_ExplicitInstantiationDeclaration) == ExternLoc.isValid()
+           && "ExternLoc should be valid iff TSK is a declaration");
+  }
+
+  ExplicitInstantiationDecl(EmptyShell Empty)
+      : Decl(ExplicitInstantiation, Empty), TSK(TSK_Undeclared) {}
+
+  virtual void anchor();
+
+public:
+  friend class ASTDeclReader;
+  friend class ASTDeclWriter;
+
+  static ExplicitInstantiationDecl *
+  Create(ASTContext &C, DeclContext *DC, SourceRange Range,
+         NamedDecl *Specialization, SourceLocation ExternLoc,
+         SourceLocation TemplateLoc, SourceLocation TagKWLoc,
+         NestedNameSpecifierLoc QualifierLoc,
+         const ASTTemplateArgumentListInfo *ArgsAsWritten,
+         SourceLocation NameLoc, TypeSourceInfo *TypeAsWritten,
+         TemplateSpecializationKind TSK);
+
+  static ExplicitInstantiationDecl *CreateDeserialized(ASTContext &C,
+                                                       GlobalDeclID ID);
+
+  NamedDecl *getSpecialization() const { return Specialization; }
+
+  SourceRange getSourceRange() const override LLVM_READONLY { return Range; }
+
+  SourceLocation getExternLoc() const { return ExternLoc; }
+  SourceLocation getTemplateLoc() const { return getLocation(); }
+  SourceLocation getTagKWLoc() const { return TagKWLoc; }
+  SourceLocation getNameLoc() const { return NameLoc; }
+
+  NestedNameSpecifierLoc getQualifierLoc() const { return QualifierLoc; }
+
+  const ASTTemplateArgumentListInfo *getTemplateArgsAsWritten() const {
+    return TemplateArgsAsWritten;
+  }
+
+  TypeSourceInfo *getTypeAsWritten() const { return TypeAsWritten; }
+
+  TemplateSpecializationKind getTemplateSpecializationKind() const {
+    return static_cast<TemplateSpecializationKind>(TSK);
+  }
+
+  bool isExternTemplate() const { return ExternLoc.isValid(); }
+
+  static bool classof(const Decl *D) { return classofKind(D->getKind()); }
+  static bool classofKind(Kind K) { return K == ExplicitInstantiation; }
+};
+
 } // namespace clang
 
 #endif // LLVM_CLANG_AST_DECLTEMPLATE_H
diff --git a/clang/include/clang/AST/JSONNodeDumper.h b/clang/include/clang/AST/JSONNodeDumper.h
index 69dbdbbdb3ecd..4e8d1649bbf8b 100644
--- a/clang/include/clang/AST/JSONNodeDumper.h
+++ b/clang/include/clang/AST/JSONNodeDumper.h
@@ -268,6 +268,7 @@ class JSONNodeDumper
   void VisitLinkageSpecDecl(const LinkageSpecDecl *LSD);
   void VisitAccessSpecDecl(const AccessSpecDecl *ASD);
   void VisitFriendDecl(const FriendDecl *FD);
+  void VisitExplicitInstantiationDecl(const ExplicitInstantiationDecl *D);
 
   void VisitObjCIvarDecl(const ObjCIvarDecl *D);
   void VisitObjCMethodDecl(const ObjCMethodDecl *D);
diff --git a/clang/include/clang/AST/RecursiveASTVisitor.h b/clang/include/clang/AST/RecursiveASTVisitor.h
index ce6ad723191e0..37bcfb5b2ed76 100644
--- a/clang/include/clang/AST/RecursiveASTVisitor.h
+++ b/clang/include/clang/AST/RecursiveASTVisitor.h
@@ -1748,6 +1748,16 @@ DEF_TRAVERSE_DECL(StaticAssertDecl, {
   TRY_TO(TraverseStmt(D->getMessage()));
 })
 
+DEF_TRAVERSE_DECL(ExplicitInstantiationDecl, {
+  if (D->getQualifierLoc())
+    TRY_TO(TraverseNestedNameSpecifierLoc(D->getQualifierLoc()));
+  if (const auto *ArgsAsWritten = D->getTemplateArgsAsWritten())
+    for (unsigned I = 0, E = ArgsAsWritten->NumTemplateArgs; I != E; ++I)
+      TRY_TO(TraverseTemplateArgumentLoc((*ArgsAsWritten)[I]));
+  if (TypeSourceInfo *TSI = D->getTypeAsWritten())
+    TRY_TO(TraverseTypeLoc(TSI->getTypeLoc()));
+})
+
 DEF_TRAVERSE_DECL(TranslationUnitDecl, {
   // Code in an unnamed namespace shows up automatically in
   // decls_begin()/decls_end().  Thus we don't need to recurse on
@@ -2027,8 +2037,10 @@ bool RecursiveASTVisitor<Derived>::TraverseTemplateInstantiations(
         TRY_TO(TraverseDecl(RD));
         break;
 
-      // FIXME: For now traverse explicit instantiations here. Change that
-      // once they are represented as dedicated nodes in the AST.
+      // Unlike class/variable template specializations, function template
+      // specializations are not independent children of the DeclContext —
+      // they are only reachable via FunctionTemplateDecl::specializations().
+      // We must traverse them here so visitors can see the instantiated body.
       case TSK_ExplicitInstantiationDeclaration:
       case TSK_ExplicitInstantiationDefinition:
         TRY_TO(TraverseDecl(RD));
diff --git a/clang/include/clang/AST/TextNodeDumper.h b/clang/include/clang/AST/TextNodeDumper.h
index 32e83ebb5c8eb..6b7ac6fac8111 100644
--- a/clang/include/clang/AST/TextNodeDumper.h
+++ b/clang/include/clang/AST/TextNodeDumper.h
@@ -395,6 +395,7 @@ class TextNodeDumper
   void VisitLinkageSpecDecl(const LinkageSpecDecl *D);
   void VisitAccessSpecDecl(const AccessSpecDecl *D);
   void VisitFriendDecl(const FriendDecl *D);
+  void VisitExplicitInstantiationDecl(const ExplicitInstantiationDecl *D);
   void VisitObjCIvarDecl(const ObjCIvarDecl *D);
   void VisitObjCMethodDecl(const ObjCMethodDecl *D);
   void VisitObjCTypeParamDecl(const ObjCTypeParamDecl *D);
diff --git a/clang/include/clang/Basic/DeclNodes.td b/clang/include/clang/Basic/DeclNodes.td
index 04311055bb600..ffb58b43812dc 100644
--- a/clang/include/clang/Basic/DeclNodes.td
+++ b/clang/include/clang/Basic/DeclNodes.td
@@ -101,6 +101,7 @@ def AccessSpec : DeclNode<Decl>;
 def Friend : DeclNode<Decl>;
 def FriendTemplate : DeclNode<Decl>;
 def StaticAssert : DeclNode<Decl>;
+def ExplicitInstantiation : DeclNode<Decl>;
 def Block : DeclNode<Decl, "blocks">, DeclContext;
 def OutlinedFunction : DeclNode<Decl>, DeclContext;
 def Captured : DeclNode<Decl>, DeclContext;
diff --git a/clang/include/clang/Serialization/ASTBitCodes.h b/clang/include/clang/Serialization/ASTBitCodes.h
index 783cd82895a90..499bd772ae126 100644
--- a/clang/include/clang/Serialization/ASTBitCodes.h
+++ b/clang/include/clang/Serialization/ASTBitCodes.h
@@ -1543,7 +1543,10 @@ enum DeclCode {
   // An OpenACCRoutineDecl record.
   DECL_OPENACC_ROUTINE,
 
-  DECL_LAST = DECL_OPENACC_ROUTINE
+  /// An ExplicitInstantiationDecl record.
+  DECL_EXPLICIT_INSTANTIATION,
+
+  DECL_LAST = DECL_EXPLICIT_INSTANTIATION
 };
 
 /// Record codes for each kind of statement or expression.
diff --git a/clang/lib/AST/DeclTemplate.cpp b/clang/lib/AST/DeclTemplate.cpp
index 99d02fdc99e92..76e3de5394d86 100644
--- a/clang/lib/AST/DeclTemplate.cpp
+++ b/clang/lib/AST/DeclTemplate.cpp
@@ -1784,3 +1784,23 @@ const Decl &clang::adjustDeclToTemplate(const Decl &D) {
   // FIXME: Adjust alias templates?
   return D;
 }
+
+void ExplicitInstantiationDecl::anchor() {}
+
+ExplicitInstantiationDecl *ExplicitInstantiationDecl::Create(
+    ASTContext &C, DeclContext *DC, SourceRange Range,
+    NamedDecl *Specialization, SourceLocation ExternLoc,
+    SourceLocation TemplateLoc, SourceLocation TagKWLoc,
+    NestedNameSpecifierLoc QualifierLoc,
+    const ASTTemplateArgumentListInfo *ArgsAsWritten,
+    SourceLocation NameLoc, TypeSourceInfo *TypeAsWritten,
+    TemplateSpecializationKind TSK) {
+  return new (C, DC) ExplicitInstantiationDecl(
+      DC, Range, Specialization, ExternLoc, TemplateLoc, TagKWLoc,
+      QualifierLoc, ArgsAsWritten, NameLoc, TypeAsWritten, TSK);
+}
+
+ExplicitInstantiationDecl *
+ExplicitInstantiationDecl::CreateDeserialized(ASTContext &C, GlobalDeclID ID) {
+  return new (C, ID) ExplicitInstantiationDecl(EmptyShell());
+}
diff --git a/clang/lib/AST/JSONNodeDumper.cpp b/clang/lib/AST/JSONNodeDumper.cpp
index 3138f95e6a83b..8373dd8e373e0 100644
--- a/clang/lib/AST/JSONNodeDumper.cpp
+++ b/clang/lib/AST/JSONNodeDumper.cpp
@@ -1119,6 +1119,32 @@ void JSONNodeDumper::VisitAccessSpecDecl(const AccessSpecDecl *ASD) {
   JOS.attribute("access", createAccessSpecifier(ASD->getAccess()));
 }
 
+void JSONNodeDumper::VisitExplicitInstantiationDecl(
+    const ExplicitInstantiationDecl *D) {
+  attributeOnlyIfTrue("isExternTemplate", D->isExternTemplate());
+  if (D->getSpecialization())
+    JOS.attribute("specializationDeclId",
+                  createPointerRepresentation(D->getSpecialization()));
+  switch (D->getTemplateSpecializationKind()) {
+  case TSK_Undeclared:
+    break;
+  case TSK_ImplicitInstantiation:
+    JOS.attribute("templateSpecializationKind", "implicit_instantiation");
+    break;
+  case TSK_ExplicitSpecialization:
+    JOS.attribute("templateSpecializationKind", "explicit_specialization");
+    break;
+  case TSK_ExplicitInstantiationDeclaration:
+    JOS.attribute("templateSpecializationKind",
+                  "explicit_instantiation_declaration");
+    break;
+  case TSK_ExplicitInstantiationDefinition:
+    JOS.attribute("templateSpecializationKind",
+                  "explicit_instantiation_definition");
+    break;
+  }
+}
+
 void JSONNodeDumper::VisitFriendDecl(const FriendDecl *FD) {
   if (const TypeSourceInfo *T = FD->getFriendType())
     JOS.attribute("type", createQualType(T->getType()));
diff --git a/clang/lib/AST/TextNodeDumper.cpp b/clang/lib/AST/TextNodeDumper.cpp
index 250ec8b666e05..108d77527f276 100644
--- a/clang/lib/AST/TextNodeDumper.cpp
+++ b/clang/lib/AST/TextNodeDumper.cpp
@@ -2936,6 +2936,20 @@ void TextNodeDumper::VisitAccessSpecDecl(const AccessSpecDecl *D) {
   dumpAccessSpecifier(D->getAccess());
 }
 
+void TextNodeDumper::VisitExplicitInstantiationDecl(
+    const ExplicitInstantiationDecl *D) {
+  dumpTemplateSpecializationKind(D->getTemplateSpecializationKind());
+  if (D->isExternTemplate())
+    OS << " extern";
+  OS << " template";
+  if (D->getQualifierLoc())
+    dumpNestedNameSpecifier(D->getQualifierLoc().getNestedNameSpecifier());
+  if (const NamedDecl *Spec = D->getSpecialization()) {
+    OS << " '" << Spec->getDeclName() << "'";
+    dumpDeclRef(Spec);
+  }
+}
+
 void TextNodeDumper::VisitFriendDecl(const FriendDecl *D) {
   if (TypeSourceInfo *T = D->getFriendType())
     dumpType(T->getType());
diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
index b96b822609c10..3e2d4035fef15 100644
--- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
@@ -831,6 +831,7 @@ void CIRGenFunction::emitDecl(const Decl &d, bool evaluateConditionDecl) {
 
   case Decl::Function:     // void X();
   case Decl::EnumConstant: // enum ? { X = ? }
+  case Decl::ExplicitInstantiation:
   case Decl::StaticAssert: // static_assert(X, ""); [C++0x]
   case Decl::Label:        // __label__ x;
   case Decl::Import:
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.cpp b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
index 2037b92e2b5d1..abe5834af46ee 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
@@ -2031,6 +2031,7 @@ void CIRGenModule::emitTopLevelDecl(Decl *decl) {
   case Decl::Concept:
   case Decl::CXXDeductionGuide:
   case Decl::Empty:
+  case Decl::ExplicitInstantiation:
   case Decl::FunctionTemplate:
   case Decl::StaticAssert:
   case Decl::TypeAliasTemplate:
diff --git a/clang/lib/CodeGen/CGDecl.cpp b/clang/lib/CodeGen/CGDecl.cpp
index 748362105cb02..419b3c477e7b2 100644
--- a/clang/lib/CodeGen/CGDecl.cpp
+++ b/clang/lib/CodeGen/CGDecl.cpp
@@ -125,6 +125,7 @@ void CodeGenFunction::EmitDecl(const Decl &D, bool EvaluateConditionDecl) {
   case Decl::Function:     // void X();
   case Decl::EnumConstant: // enum ? { X = ? }
   case Decl::StaticAssert: // static_assert(X, ""); [C++0x]
+  case Decl::ExplicitInstantiation:
   case Decl::Label:        // __label__ x;
   case Decl::Import:
   case Decl::MSGuid:    // __declspec(uuid("..."))
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 68a403e02968d..6a1dcfd55f4f4 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -7777,6 +7777,7 @@ void CodeGenModule::EmitTopLevelDecl(Decl *D) {
     break;
 
   case Decl::StaticAssert:
+  case Decl::ExplicitInstantiation:
     // Nothing to do.
     break;
 
diff --git a/clang/lib/Sema/SemaTemplate.cpp b/clang/lib/Sema/SemaTemplate.cpp
index c436b7018a2bd..66fc2b6fc81e3 100644
--- a/clang/lib/Sema/SemaTemplate.cpp
+++ b/clang/lib/Sema/SemaTemplate.cpp
@@ -9314,10 +9314,41 @@ static void StripImplicitInstantiation(NamedDecl *D, bool MinGW) {
     FD->setInlineSpecified(false);
 }
 
+/// Create an ExplicitInstantiationDecl to record source-location info for an
+/// explicit template instantiation statement, and add it to \p CurContext.
+static void addExplicitInstantiationDecl(
+    ASTContext &Context, DeclContext *CurContext, const CXXScopeSpec &SS,
+    SourceLocation EndLoc, NamedDecl *Spec, SourceLocation ExternLoc,
+    SourceLocation TemplateLoc, SourceLocation TagKWLoc,
+    const ASTTemplateArgumentListInfo *ArgsAsWritten,
+    SourceLocation NameLoc, TypeSourceInfo *TypeAsWritten,
+    TemplateSpecializationKind TSK) {
+  NestedNameSpecifierLoc QualifierLoc;
+  if (SS.isNotEmpty())
+    QualifierLoc = SS.getWithLocInContext(Context);
+  SourceLocation BeginLoc = ExternLoc.isValid() ? ExternLoc : TemplateLoc;
+  auto *EID = ExplicitInstantiationDecl::Create(
+      Context, CurContext, SourceRange(BeginLoc, EndLoc), Spec, ExternLoc,
+      TemplateLoc, TagKWLoc, QualifierLoc, ArgsAsWritten, NameLoc,
+      TypeAsWritten, TSK);
+  CurContext->addDecl(EID);
+}
+
 /// Compute the diagnostic location for an explicit instantiation
 //  declaration or definition.
 static SourceLocation DiagLocForExplicitInstantiation(
     NamedDecl* D, SourceLocation PointOfInstantiation) {
+  // Search for an ExplicitInstantiationDecl that references D. Per
+  // [temp.explicit], explicit instantiations must appear in an enclosing
+  // namespace of the template, so the EID is in D's DeclContext or an ancestor.
+  for (DeclContext *DC = D->getDeclContext(); DC; DC = DC->getParent()) {
+    for (auto *Decl : DC->decls()) {
+      if (auto *EID = dyn_cast<ExplicitInstantiationDecl>(Decl))
+        if (EID->getSpecialization() == D)
+          return EID->getTemplateLoc();
+    }
+  }
+
   // Explicit instantiations following a specialization have no effect and
   // hence no PointOfInstantiation. In that case, walk decl backwards
   // until a valid name loc is found.
@@ -10406,6 +10437,11 @@ DeclResult Sema::ActOnExplicitInstantiation(
   if (HasNoEffect) {
     // Set the template specialization kind.
     Specialization->setTemplateSpecializationKind(TSK);
+
+    addExplicitInstantiationDecl(
+        Context, CurContext, SS, RAngleLoc, Specialization, ExternLoc,
+        TemplateLoc, KWLoc, Specialization->getTemplateArgsAsWritten(),
+        TemplateNameLoc, nullptr, TSK);
     return Specialization;
   }
 
@@ -10495,6 +10531,10 @@ DeclResult Sema::ActOnExplicitInstantiation(
     Specialization->setTemplateSpecializationKind(TSK);
   }
 
+  addExplicitInstantiationDecl(
+      Context, CurContext, SS, RAngleLoc, Specialization, ExternLoc,
+      TemplateLoc, KWLoc, Specialization->getTemplateArgsAsWritten(),
+      TemplateNameLoc, nullptr, TSK);
   return Specialization;
 }
 
@@ -10569,8 +10609,12 @@ Sema::ActOnExplicitInstantiation(Scope *S, SourceLocation ExternLoc,
                                              MSInfo->getPointOfInstantiation(),
                                                HasNoEffect))
       return true;
-    if (HasNoEffect)
+    if (HasNoEffect) {
+      addExplicitInstantiationDecl(Context, CurContext, SS, NameLoc, Record,
+                                   ExternLoc, TemplateLoc, KWLoc, nullptr,
+                                   NameLoc, nullptr, TSK);
       return TagD;
+    }
   }
 
   CXXRecordDecl *RecordDef
@@ -10606,10 +10650,9 @@ Sema::ActOnExplicitInstantiation(Scope *S, SourceLocation ExternLoc,
   if (TSK == TSK_ExplicitInstantiationDefinition)
     MarkVTableUsed(NameLoc, RecordDef, true);
 
-  // FIXME: We don't have any representation for explicit instantiations of
-  // member classes. Such a representation is not needed for compilation, but it
-  // ...
[truncated]

@llvmbot

llvmbot commented Apr 11, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-clang-codegen

Author: ykiko (16bit-ykiko)

Changes

This is the initial fix of #191442. Following the discussion here #115418 (comment).

Description

This PR introduces a new AST node, ExplicitInstantiationDecl, to systematically fix the long-standing issue of missing or incorrect source location information for explicit template instantiations (Issue #191442). As a direct result of this new architecture, it also resolves Issue #21133 by ensuring that duplicate explicit instantiation diagnostics point to the actual lexical occurrence rather than the implicit point of instantiation.

Background & The Problem

Historically, Clang's AST lacked a dedicated node to represent the lexical occurrence of an explicit instantiation statement. Instead, Sema tried to shoehorn this information into existing specialization nodes (e.g., FunctionDecl, VarTemplateSpecializationDecl) or simply returned nullptr.

This resulted in fragmented behavior across the seven instantiable entity types:

  • Function & Member Function Templates: Returned nullptr, completely losing SourceRange and NestedNameSpecifier information.
  • Member Functions & Static Data Members: Mutated existing nodes in-place. Consequently, multiple template or extern template declarations in the same file would overwrite each other's source locations.
  • Variable Templates: Suffered from dyn_cast bugs and dropped NNS information.

Design Trade-offs Evaluated

Before settling on the current design, I evaluated a mixed redeclaration-chain approach (similar to how explicit specializations are handled, creating new FunctionDecl nodes and stitching them into the redecl chain). However, this approach had significant flaws:

  1. Inconsistency: It couldn't be cleanly applied to member functions or static data members due to DeclContext constraints (e.g., a member function shouldn't lexically reside in a namespace DeclContext, but placing it in the class context would pollute member lookup).
  2. Fragility: It required bypassing standard FoldingSet mechanisms (setFunctionTemplateSpecialization).
  3. Lookup Pollution: Injecting new NamedDecl nodes purely for instantiations risked breaking downstream ASTMatchers and altering name lookup behavior.

The Chosen Architecture: ExplicitInstantiationDecl

To avoid these pitfalls, this PR introduces ExplicitInstantiationDecl as a purely lexical annotation node.

Key Design Characteristics:

  1. Inherits from Decl, not NamedDecl: This is the most crucial design choice. Much like StaticAssertDecl or FriendDecl, this node lives in a DeclContext (making it traversable by RecursiveASTVisitor and visible in AST dumps) but remains completely invisible to C++ name lookup. It does not interfere with overload resolution or lookup tables.
  2. Unified Representation: A single node type now covers all seven entity types. It holds a pointer (Specialization) to the underlying instantiated declaration, unifying how functions, variables, classes, and members are handled.
  3. Lexical Fidelity: The node resides in the enclosing namespace or Translation Unit where the explicit instantiation was actually written, perfectly preserving the SourceRange, NestedNameSpecifierLoc, and the exact locations of the template and extern keywords.
  • Fix #21040
  • Fix #52659
  • Fix #115418
  • Fix #14230
  • Fix #21133 (Diagnostic Locations)
    > Because every explicit instantiation is now explicitly recorded in its DeclContext, DiagLocForExplicitInstantiation no longer relies on the often-inaccurate PointOfInstantiation (which frequently points to implicit instantiation sites). Instead, it searches the DeclContext hierarchy to find the corresponding ExplicitInstantiationDecl. As a result, the note: previous explicit instantiation is here diagnostic now correctly points to the exact lexical location.

Patch is 48.70 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/191658.diff

25 Files Affected:

  • (modified) clang/include/clang/AST/ASTNodeTraverser.h (+11)
  • (modified) clang/include/clang/AST/DeclTemplate.h (+116)
  • (modified) clang/include/clang/AST/JSONNodeDumper.h (+1)
  • (modified) clang/include/clang/AST/RecursiveASTVisitor.h (+14-2)
  • (modified) clang/include/clang/AST/TextNodeDumper.h (+1)
  • (modified) clang/include/clang/Basic/DeclNodes.td (+1)
  • (modified) clang/include/clang/Serialization/ASTBitCodes.h (+4-1)
  • (modified) clang/lib/AST/DeclTemplate.cpp (+20)
  • (modified) clang/lib/AST/JSONNodeDumper.cpp (+26)
  • (modified) clang/lib/AST/TextNodeDumper.cpp (+14)
  • (modified) clang/lib/CIR/CodeGen/CIRGenDecl.cpp (+1)
  • (modified) clang/lib/CIR/CodeGen/CIRGenModule.cpp (+1)
  • (modified) clang/lib/CodeGen/CGDecl.cpp (+1)
  • (modified) clang/lib/CodeGen/CodeGenModule.cpp (+1)
  • (modified) clang/lib/Sema/SemaTemplate.cpp (+87-15)
  • (modified) clang/lib/Sema/SemaTemplateInstantiateDecl.cpp (+8)
  • (modified) clang/lib/Serialization/ASTCommon.cpp (+1)
  • (modified) clang/lib/Serialization/ASTReaderDecl.cpp (+20)
  • (modified) clang/lib/Serialization/ASTWriterDecl.cpp (+18)
  • (modified) clang/lib/Tooling/Syntax/BuildTree.cpp (+7)
  • (modified) clang/test/AST/ast-dump-templates-pattern.cpp (+26-6)
  • (modified) clang/test/AST/ast-dump-templates.cpp (+50-12)
  • (added) clang/test/AST/explicit-instantiation-source-info.cpp (+166)
  • (added) clang/test/SemaTemplate/explicit-instantiation-diag-location.cpp (+31)
  • (modified) clang/tools/libclang/CIndex.cpp (+2)
diff --git a/clang/include/clang/AST/ASTNodeTraverser.h b/clang/include/clang/AST/ASTNodeTraverser.h
index 3be24ff868c2d..39a6864bf169e 100644
--- a/clang/include/clang/AST/ASTNodeTraverser.h
+++ b/clang/include/clang/AST/ASTNodeTraverser.h
@@ -683,6 +683,17 @@ class ASTNodeTraverser
     Visit(D->getMessage());
   }
 
+  void VisitExplicitInstantiationDecl(const ExplicitInstantiationDecl *D) {
+    // The specialization is already elsewhere in the AST; don't re-traverse it.
+    // Traverse source-location sub-nodes: template arguments and type-as-written.
+    if (const auto *ArgsAsWritten = D->getTemplateArgsAsWritten())
+      for (unsigned I = 0, E = ArgsAsWritten->NumTemplateArgs; I != E; ++I)
+        Visit((*ArgsAsWritten)[I].getArgument(),
+              (*ArgsAsWritten)[I].getSourceRange());
+    if (TypeSourceInfo *TSI = D->getTypeAsWritten())
+      Visit(TSI->getTypeLoc());
+  }
+
   void VisitFunctionTemplateDecl(const FunctionTemplateDecl *D) {
     dumpTemplateDecl(D);
   }
diff --git a/clang/include/clang/AST/DeclTemplate.h b/clang/include/clang/AST/DeclTemplate.h
index a4a1bb9c13c79..b470b520531f1 100644
--- a/clang/include/clang/AST/DeclTemplate.h
+++ b/clang/include/clang/AST/DeclTemplate.h
@@ -3405,6 +3405,122 @@ getReplacedTemplateParameter(Decl *D, unsigned Index);
 /// If we have an implicit instantiation, adjust 'D' to refer to template.
 const Decl &adjustDeclToTemplate(const Decl &D);
 
+/// Represents an explicit instantiation of a template entity in source code.
+///
+/// This node records source location information for an explicit instantiation
+/// statement. It does not participate in name lookup (inherits from Decl, not
+/// NamedDecl), and does not affect code generation. The underlying
+/// specialization decl (FunctionDecl, VarDecl, CXXRecordDecl, etc.) continues
+/// to handle all semantic and codegen responsibilities.
+///
+/// \code
+///   template void ns::foo<int>(int);        // function template
+///   extern template struct ns::S<int>;      // class template (extern)
+///   template int ns::bar<int>;              // variable template
+///   template void ns::S<int>::method(int);  // member function
+/// \endcode
+class ExplicitInstantiationDecl : public Decl {
+  /// The underlying specialization being explicitly instantiated.
+  NamedDecl *Specialization = nullptr;
+
+  /// The source range of the entire explicit instantiation statement.
+  SourceRange Range;
+
+  /// Location of the 'extern' keyword (invalid if not extern template).
+  SourceLocation ExternLoc;
+
+  /// Location of the struct/class/union keyword (for class template and
+  /// nested class instantiations; invalid otherwise).
+  SourceLocation TagKWLoc;
+
+  /// Nested name specifier with source locations (e.g., ns::S<int>::).
+  NestedNameSpecifierLoc QualifierLoc;
+
+  /// Template arguments as written (e.g., <int>). Null if the template
+  /// arguments were deduced.
+  const ASTTemplateArgumentListInfo *TemplateArgsAsWritten = nullptr;
+
+  /// Location of the entity name (e.g., 'foo' in 'template void ns::foo<int>(int)').
+  SourceLocation NameLoc;
+
+  /// Type source info for the declaration type:
+  ///   - Function templates / member functions: FunctionProtoTypeLoc with
+  ///     return type location and parameter type locations.
+  ///   - Variable templates / static data members: the declared type.
+  ///   - Class templates / nested classes: null.
+  TypeSourceInfo *TypeAsWritten = nullptr;
+
+  /// Whether this is a declaration (extern template) or definition (template).
+  LLVM_PREFERRED_TYPE(TemplateSpecializationKind)
+  unsigned TSK : 3;
+
+  ExplicitInstantiationDecl(DeclContext *DC, SourceRange Range,
+                            NamedDecl *Specialization,
+                            SourceLocation ExternLoc,
+                            SourceLocation TemplateLoc,
+                            SourceLocation TagKWLoc,
+                            NestedNameSpecifierLoc QualifierLoc,
+                            const ASTTemplateArgumentListInfo *ArgsAsWritten,
+                            SourceLocation NameLoc,
+                            TypeSourceInfo *TypeAsWritten,
+                            TemplateSpecializationKind TSK)
+      : Decl(ExplicitInstantiation, DC, TemplateLoc),
+        Specialization(Specialization), Range(Range), ExternLoc(ExternLoc),
+        TagKWLoc(TagKWLoc), QualifierLoc(QualifierLoc),
+        TemplateArgsAsWritten(ArgsAsWritten), NameLoc(NameLoc),
+        TypeAsWritten(TypeAsWritten), TSK(TSK) {
+    assert((TSK == TSK_ExplicitInstantiationDeclaration) == ExternLoc.isValid()
+           && "ExternLoc should be valid iff TSK is a declaration");
+  }
+
+  ExplicitInstantiationDecl(EmptyShell Empty)
+      : Decl(ExplicitInstantiation, Empty), TSK(TSK_Undeclared) {}
+
+  virtual void anchor();
+
+public:
+  friend class ASTDeclReader;
+  friend class ASTDeclWriter;
+
+  static ExplicitInstantiationDecl *
+  Create(ASTContext &C, DeclContext *DC, SourceRange Range,
+         NamedDecl *Specialization, SourceLocation ExternLoc,
+         SourceLocation TemplateLoc, SourceLocation TagKWLoc,
+         NestedNameSpecifierLoc QualifierLoc,
+         const ASTTemplateArgumentListInfo *ArgsAsWritten,
+         SourceLocation NameLoc, TypeSourceInfo *TypeAsWritten,
+         TemplateSpecializationKind TSK);
+
+  static ExplicitInstantiationDecl *CreateDeserialized(ASTContext &C,
+                                                       GlobalDeclID ID);
+
+  NamedDecl *getSpecialization() const { return Specialization; }
+
+  SourceRange getSourceRange() const override LLVM_READONLY { return Range; }
+
+  SourceLocation getExternLoc() const { return ExternLoc; }
+  SourceLocation getTemplateLoc() const { return getLocation(); }
+  SourceLocation getTagKWLoc() const { return TagKWLoc; }
+  SourceLocation getNameLoc() const { return NameLoc; }
+
+  NestedNameSpecifierLoc getQualifierLoc() const { return QualifierLoc; }
+
+  const ASTTemplateArgumentListInfo *getTemplateArgsAsWritten() const {
+    return TemplateArgsAsWritten;
+  }
+
+  TypeSourceInfo *getTypeAsWritten() const { return TypeAsWritten; }
+
+  TemplateSpecializationKind getTemplateSpecializationKind() const {
+    return static_cast<TemplateSpecializationKind>(TSK);
+  }
+
+  bool isExternTemplate() const { return ExternLoc.isValid(); }
+
+  static bool classof(const Decl *D) { return classofKind(D->getKind()); }
+  static bool classofKind(Kind K) { return K == ExplicitInstantiation; }
+};
+
 } // namespace clang
 
 #endif // LLVM_CLANG_AST_DECLTEMPLATE_H
diff --git a/clang/include/clang/AST/JSONNodeDumper.h b/clang/include/clang/AST/JSONNodeDumper.h
index 69dbdbbdb3ecd..4e8d1649bbf8b 100644
--- a/clang/include/clang/AST/JSONNodeDumper.h
+++ b/clang/include/clang/AST/JSONNodeDumper.h
@@ -268,6 +268,7 @@ class JSONNodeDumper
   void VisitLinkageSpecDecl(const LinkageSpecDecl *LSD);
   void VisitAccessSpecDecl(const AccessSpecDecl *ASD);
   void VisitFriendDecl(const FriendDecl *FD);
+  void VisitExplicitInstantiationDecl(const ExplicitInstantiationDecl *D);
 
   void VisitObjCIvarDecl(const ObjCIvarDecl *D);
   void VisitObjCMethodDecl(const ObjCMethodDecl *D);
diff --git a/clang/include/clang/AST/RecursiveASTVisitor.h b/clang/include/clang/AST/RecursiveASTVisitor.h
index ce6ad723191e0..37bcfb5b2ed76 100644
--- a/clang/include/clang/AST/RecursiveASTVisitor.h
+++ b/clang/include/clang/AST/RecursiveASTVisitor.h
@@ -1748,6 +1748,16 @@ DEF_TRAVERSE_DECL(StaticAssertDecl, {
   TRY_TO(TraverseStmt(D->getMessage()));
 })
 
+DEF_TRAVERSE_DECL(ExplicitInstantiationDecl, {
+  if (D->getQualifierLoc())
+    TRY_TO(TraverseNestedNameSpecifierLoc(D->getQualifierLoc()));
+  if (const auto *ArgsAsWritten = D->getTemplateArgsAsWritten())
+    for (unsigned I = 0, E = ArgsAsWritten->NumTemplateArgs; I != E; ++I)
+      TRY_TO(TraverseTemplateArgumentLoc((*ArgsAsWritten)[I]));
+  if (TypeSourceInfo *TSI = D->getTypeAsWritten())
+    TRY_TO(TraverseTypeLoc(TSI->getTypeLoc()));
+})
+
 DEF_TRAVERSE_DECL(TranslationUnitDecl, {
   // Code in an unnamed namespace shows up automatically in
   // decls_begin()/decls_end().  Thus we don't need to recurse on
@@ -2027,8 +2037,10 @@ bool RecursiveASTVisitor<Derived>::TraverseTemplateInstantiations(
         TRY_TO(TraverseDecl(RD));
         break;
 
-      // FIXME: For now traverse explicit instantiations here. Change that
-      // once they are represented as dedicated nodes in the AST.
+      // Unlike class/variable template specializations, function template
+      // specializations are not independent children of the DeclContext —
+      // they are only reachable via FunctionTemplateDecl::specializations().
+      // We must traverse them here so visitors can see the instantiated body.
       case TSK_ExplicitInstantiationDeclaration:
       case TSK_ExplicitInstantiationDefinition:
         TRY_TO(TraverseDecl(RD));
diff --git a/clang/include/clang/AST/TextNodeDumper.h b/clang/include/clang/AST/TextNodeDumper.h
index 32e83ebb5c8eb..6b7ac6fac8111 100644
--- a/clang/include/clang/AST/TextNodeDumper.h
+++ b/clang/include/clang/AST/TextNodeDumper.h
@@ -395,6 +395,7 @@ class TextNodeDumper
   void VisitLinkageSpecDecl(const LinkageSpecDecl *D);
   void VisitAccessSpecDecl(const AccessSpecDecl *D);
   void VisitFriendDecl(const FriendDecl *D);
+  void VisitExplicitInstantiationDecl(const ExplicitInstantiationDecl *D);
   void VisitObjCIvarDecl(const ObjCIvarDecl *D);
   void VisitObjCMethodDecl(const ObjCMethodDecl *D);
   void VisitObjCTypeParamDecl(const ObjCTypeParamDecl *D);
diff --git a/clang/include/clang/Basic/DeclNodes.td b/clang/include/clang/Basic/DeclNodes.td
index 04311055bb600..ffb58b43812dc 100644
--- a/clang/include/clang/Basic/DeclNodes.td
+++ b/clang/include/clang/Basic/DeclNodes.td
@@ -101,6 +101,7 @@ def AccessSpec : DeclNode<Decl>;
 def Friend : DeclNode<Decl>;
 def FriendTemplate : DeclNode<Decl>;
 def StaticAssert : DeclNode<Decl>;
+def ExplicitInstantiation : DeclNode<Decl>;
 def Block : DeclNode<Decl, "blocks">, DeclContext;
 def OutlinedFunction : DeclNode<Decl>, DeclContext;
 def Captured : DeclNode<Decl>, DeclContext;
diff --git a/clang/include/clang/Serialization/ASTBitCodes.h b/clang/include/clang/Serialization/ASTBitCodes.h
index 783cd82895a90..499bd772ae126 100644
--- a/clang/include/clang/Serialization/ASTBitCodes.h
+++ b/clang/include/clang/Serialization/ASTBitCodes.h
@@ -1543,7 +1543,10 @@ enum DeclCode {
   // An OpenACCRoutineDecl record.
   DECL_OPENACC_ROUTINE,
 
-  DECL_LAST = DECL_OPENACC_ROUTINE
+  /// An ExplicitInstantiationDecl record.
+  DECL_EXPLICIT_INSTANTIATION,
+
+  DECL_LAST = DECL_EXPLICIT_INSTANTIATION
 };
 
 /// Record codes for each kind of statement or expression.
diff --git a/clang/lib/AST/DeclTemplate.cpp b/clang/lib/AST/DeclTemplate.cpp
index 99d02fdc99e92..76e3de5394d86 100644
--- a/clang/lib/AST/DeclTemplate.cpp
+++ b/clang/lib/AST/DeclTemplate.cpp
@@ -1784,3 +1784,23 @@ const Decl &clang::adjustDeclToTemplate(const Decl &D) {
   // FIXME: Adjust alias templates?
   return D;
 }
+
+void ExplicitInstantiationDecl::anchor() {}
+
+ExplicitInstantiationDecl *ExplicitInstantiationDecl::Create(
+    ASTContext &C, DeclContext *DC, SourceRange Range,
+    NamedDecl *Specialization, SourceLocation ExternLoc,
+    SourceLocation TemplateLoc, SourceLocation TagKWLoc,
+    NestedNameSpecifierLoc QualifierLoc,
+    const ASTTemplateArgumentListInfo *ArgsAsWritten,
+    SourceLocation NameLoc, TypeSourceInfo *TypeAsWritten,
+    TemplateSpecializationKind TSK) {
+  return new (C, DC) ExplicitInstantiationDecl(
+      DC, Range, Specialization, ExternLoc, TemplateLoc, TagKWLoc,
+      QualifierLoc, ArgsAsWritten, NameLoc, TypeAsWritten, TSK);
+}
+
+ExplicitInstantiationDecl *
+ExplicitInstantiationDecl::CreateDeserialized(ASTContext &C, GlobalDeclID ID) {
+  return new (C, ID) ExplicitInstantiationDecl(EmptyShell());
+}
diff --git a/clang/lib/AST/JSONNodeDumper.cpp b/clang/lib/AST/JSONNodeDumper.cpp
index 3138f95e6a83b..8373dd8e373e0 100644
--- a/clang/lib/AST/JSONNodeDumper.cpp
+++ b/clang/lib/AST/JSONNodeDumper.cpp
@@ -1119,6 +1119,32 @@ void JSONNodeDumper::VisitAccessSpecDecl(const AccessSpecDecl *ASD) {
   JOS.attribute("access", createAccessSpecifier(ASD->getAccess()));
 }
 
+void JSONNodeDumper::VisitExplicitInstantiationDecl(
+    const ExplicitInstantiationDecl *D) {
+  attributeOnlyIfTrue("isExternTemplate", D->isExternTemplate());
+  if (D->getSpecialization())
+    JOS.attribute("specializationDeclId",
+                  createPointerRepresentation(D->getSpecialization()));
+  switch (D->getTemplateSpecializationKind()) {
+  case TSK_Undeclared:
+    break;
+  case TSK_ImplicitInstantiation:
+    JOS.attribute("templateSpecializationKind", "implicit_instantiation");
+    break;
+  case TSK_ExplicitSpecialization:
+    JOS.attribute("templateSpecializationKind", "explicit_specialization");
+    break;
+  case TSK_ExplicitInstantiationDeclaration:
+    JOS.attribute("templateSpecializationKind",
+                  "explicit_instantiation_declaration");
+    break;
+  case TSK_ExplicitInstantiationDefinition:
+    JOS.attribute("templateSpecializationKind",
+                  "explicit_instantiation_definition");
+    break;
+  }
+}
+
 void JSONNodeDumper::VisitFriendDecl(const FriendDecl *FD) {
   if (const TypeSourceInfo *T = FD->getFriendType())
     JOS.attribute("type", createQualType(T->getType()));
diff --git a/clang/lib/AST/TextNodeDumper.cpp b/clang/lib/AST/TextNodeDumper.cpp
index 250ec8b666e05..108d77527f276 100644
--- a/clang/lib/AST/TextNodeDumper.cpp
+++ b/clang/lib/AST/TextNodeDumper.cpp
@@ -2936,6 +2936,20 @@ void TextNodeDumper::VisitAccessSpecDecl(const AccessSpecDecl *D) {
   dumpAccessSpecifier(D->getAccess());
 }
 
+void TextNodeDumper::VisitExplicitInstantiationDecl(
+    const ExplicitInstantiationDecl *D) {
+  dumpTemplateSpecializationKind(D->getTemplateSpecializationKind());
+  if (D->isExternTemplate())
+    OS << " extern";
+  OS << " template";
+  if (D->getQualifierLoc())
+    dumpNestedNameSpecifier(D->getQualifierLoc().getNestedNameSpecifier());
+  if (const NamedDecl *Spec = D->getSpecialization()) {
+    OS << " '" << Spec->getDeclName() << "'";
+    dumpDeclRef(Spec);
+  }
+}
+
 void TextNodeDumper::VisitFriendDecl(const FriendDecl *D) {
   if (TypeSourceInfo *T = D->getFriendType())
     dumpType(T->getType());
diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
index b96b822609c10..3e2d4035fef15 100644
--- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
@@ -831,6 +831,7 @@ void CIRGenFunction::emitDecl(const Decl &d, bool evaluateConditionDecl) {
 
   case Decl::Function:     // void X();
   case Decl::EnumConstant: // enum ? { X = ? }
+  case Decl::ExplicitInstantiation:
   case Decl::StaticAssert: // static_assert(X, ""); [C++0x]
   case Decl::Label:        // __label__ x;
   case Decl::Import:
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.cpp b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
index 2037b92e2b5d1..abe5834af46ee 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
@@ -2031,6 +2031,7 @@ void CIRGenModule::emitTopLevelDecl(Decl *decl) {
   case Decl::Concept:
   case Decl::CXXDeductionGuide:
   case Decl::Empty:
+  case Decl::ExplicitInstantiation:
   case Decl::FunctionTemplate:
   case Decl::StaticAssert:
   case Decl::TypeAliasTemplate:
diff --git a/clang/lib/CodeGen/CGDecl.cpp b/clang/lib/CodeGen/CGDecl.cpp
index 748362105cb02..419b3c477e7b2 100644
--- a/clang/lib/CodeGen/CGDecl.cpp
+++ b/clang/lib/CodeGen/CGDecl.cpp
@@ -125,6 +125,7 @@ void CodeGenFunction::EmitDecl(const Decl &D, bool EvaluateConditionDecl) {
   case Decl::Function:     // void X();
   case Decl::EnumConstant: // enum ? { X = ? }
   case Decl::StaticAssert: // static_assert(X, ""); [C++0x]
+  case Decl::ExplicitInstantiation:
   case Decl::Label:        // __label__ x;
   case Decl::Import:
   case Decl::MSGuid:    // __declspec(uuid("..."))
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 68a403e02968d..6a1dcfd55f4f4 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -7777,6 +7777,7 @@ void CodeGenModule::EmitTopLevelDecl(Decl *D) {
     break;
 
   case Decl::StaticAssert:
+  case Decl::ExplicitInstantiation:
     // Nothing to do.
     break;
 
diff --git a/clang/lib/Sema/SemaTemplate.cpp b/clang/lib/Sema/SemaTemplate.cpp
index c436b7018a2bd..66fc2b6fc81e3 100644
--- a/clang/lib/Sema/SemaTemplate.cpp
+++ b/clang/lib/Sema/SemaTemplate.cpp
@@ -9314,10 +9314,41 @@ static void StripImplicitInstantiation(NamedDecl *D, bool MinGW) {
     FD->setInlineSpecified(false);
 }
 
+/// Create an ExplicitInstantiationDecl to record source-location info for an
+/// explicit template instantiation statement, and add it to \p CurContext.
+static void addExplicitInstantiationDecl(
+    ASTContext &Context, DeclContext *CurContext, const CXXScopeSpec &SS,
+    SourceLocation EndLoc, NamedDecl *Spec, SourceLocation ExternLoc,
+    SourceLocation TemplateLoc, SourceLocation TagKWLoc,
+    const ASTTemplateArgumentListInfo *ArgsAsWritten,
+    SourceLocation NameLoc, TypeSourceInfo *TypeAsWritten,
+    TemplateSpecializationKind TSK) {
+  NestedNameSpecifierLoc QualifierLoc;
+  if (SS.isNotEmpty())
+    QualifierLoc = SS.getWithLocInContext(Context);
+  SourceLocation BeginLoc = ExternLoc.isValid() ? ExternLoc : TemplateLoc;
+  auto *EID = ExplicitInstantiationDecl::Create(
+      Context, CurContext, SourceRange(BeginLoc, EndLoc), Spec, ExternLoc,
+      TemplateLoc, TagKWLoc, QualifierLoc, ArgsAsWritten, NameLoc,
+      TypeAsWritten, TSK);
+  CurContext->addDecl(EID);
+}
+
 /// Compute the diagnostic location for an explicit instantiation
 //  declaration or definition.
 static SourceLocation DiagLocForExplicitInstantiation(
     NamedDecl* D, SourceLocation PointOfInstantiation) {
+  // Search for an ExplicitInstantiationDecl that references D. Per
+  // [temp.explicit], explicit instantiations must appear in an enclosing
+  // namespace of the template, so the EID is in D's DeclContext or an ancestor.
+  for (DeclContext *DC = D->getDeclContext(); DC; DC = DC->getParent()) {
+    for (auto *Decl : DC->decls()) {
+      if (auto *EID = dyn_cast<ExplicitInstantiationDecl>(Decl))
+        if (EID->getSpecialization() == D)
+          return EID->getTemplateLoc();
+    }
+  }
+
   // Explicit instantiations following a specialization have no effect and
   // hence no PointOfInstantiation. In that case, walk decl backwards
   // until a valid name loc is found.
@@ -10406,6 +10437,11 @@ DeclResult Sema::ActOnExplicitInstantiation(
   if (HasNoEffect) {
     // Set the template specialization kind.
     Specialization->setTemplateSpecializationKind(TSK);
+
+    addExplicitInstantiationDecl(
+        Context, CurContext, SS, RAngleLoc, Specialization, ExternLoc,
+        TemplateLoc, KWLoc, Specialization->getTemplateArgsAsWritten(),
+        TemplateNameLoc, nullptr, TSK);
     return Specialization;
   }
 
@@ -10495,6 +10531,10 @@ DeclResult Sema::ActOnExplicitInstantiation(
     Specialization->setTemplateSpecializationKind(TSK);
   }
 
+  addExplicitInstantiationDecl(
+      Context, CurContext, SS, RAngleLoc, Specialization, ExternLoc,
+      TemplateLoc, KWLoc, Specialization->getTemplateArgsAsWritten(),
+      TemplateNameLoc, nullptr, TSK);
   return Specialization;
 }
 
@@ -10569,8 +10609,12 @@ Sema::ActOnExplicitInstantiation(Scope *S, SourceLocation ExternLoc,
                                              MSInfo->getPointOfInstantiation(),
                                                HasNoEffect))
       return true;
-    if (HasNoEffect)
+    if (HasNoEffect) {
+      addExplicitInstantiationDecl(Context, CurContext, SS, NameLoc, Record,
+                                   ExternLoc, TemplateLoc, KWLoc, nullptr,
+                                   NameLoc, nullptr, TSK);
       return TagD;
+    }
   }
 
   CXXRecordDecl *RecordDef
@@ -10606,10 +10650,9 @@ Sema::ActOnExplicitInstantiation(Scope *S, SourceLocation ExternLoc,
   if (TSK == TSK_ExplicitInstantiationDefinition)
     MarkVTableUsed(NameLoc, RecordDef, true);
 
-  // FIXME: We don't have any representation for explicit instantiations of
-  // member classes. Such a representation is not needed for compilation, but it
-  // ...
[truncated]

@github-actions

github-actions Bot commented Apr 11, 2026 •

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@github-actions

github-actions Bot commented Apr 11, 2026 •

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 55323 tests passed
  • 3115 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented Apr 11, 2026 •

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 117092 tests passed
  • 3997 tests skipped

✅ The build succeeded and all tests passed.

@mizvekov mizvekov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done, thanks!

Just a few nits here, but great work, I appreciate.

Comment thread clang/lib/Sema/SemaTemplateInstantiateDecl.cpp Outdated
Comment thread clang/include/clang/AST/DeclTemplate.h Outdated
Comment thread clang/include/clang/AST/DeclTemplate.h Outdated
Comment thread clang/include/clang/AST/DeclTemplate.h Outdated
Comment thread clang/lib/Sema/SemaTemplate.cpp Outdated
Comment thread clang/lib/Sema/SemaTemplate.cpp Outdated
Comment thread clang/lib/Sema/SemaTemplate.cpp Outdated

@mizvekov mizvekov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be worth adding support for ast-print with a few tests, that looks fairly simple for this declaration.

Comment thread clang/lib/Sema/SemaTemplate.cpp Outdated
Comment thread clang/lib/Sema/SemaTemplate.cpp Outdated
Comment thread clang/test/AST/ast-print-explicit-instantiation.cpp Outdated
Comment thread clang/test/SemaTemplate/explicit-instantiation-diag-location.cpp Outdated
Comment thread clang/lib/Sema/SemaTemplate.cpp Outdated
Comment thread clang/include/clang/AST/RecursiveASTVisitor.h
@zyn0217
zyn0217 requested a review from cor3ntin April 12, 2026 03:05
@16bit-ykiko

Copy link
Copy Markdown
Contributor Author

I think this is a good direction, we should probably alert our downstreams @llvm/clang-vendors to make sure they are comfortable with the new node/having to deal with the new node in AST searches.

We should probably make sure our AST-matchers has a way of seeing these?

Also: I'm a bit concerned about the additional memory pressure, the ExplicitInstantiationDecl is pretty huge. Can we measure the difference in memory usage in some sort of major project here to see?

I'm also interested in if this has a sizable negative effect on compile times, we have the tracker, but I don't recall how to start it (@Endilll ?).

After optimization, this class is now quite small. In my experience, explicit instantiations are rarely used in codebases anyway, meaning very few of these nodes will actually be created. Therefore, I believe the impact on compile time will likely be negligible.

@erichkeane

Copy link
Copy Markdown
Contributor

I think this is a good direction, we should probably alert our downstreams @llvm/clang-vendors to make sure they are comfortable with the new node/having to deal with the new node in AST searches.
We should probably make sure our AST-matchers has a way of seeing these?
Also: I'm a bit concerned about the additional memory pressure, the ExplicitInstantiationDecl is pretty huge. Can we measure the difference in memory usage in some sort of major project here to see?
I'm also interested in if this has a sizable negative effect on compile times, we have the tracker, but I don't recall how to start it (@Endilll ?).

After optimization, this class is now quite small. In my experience, explicit instantiations are rarely used in codebases anyway, meaning very few of these nodes will actually be created. Therefore, I believe the impact on compile time will likely be negligible.

Still worth running the check benchmark though. I hope few of these nodes will be created, but we don't know what we don't know! There could likely be a pathological case here we're not thinking about.

@16bit-ykiko

16bit-ykiko commented Apr 18, 2026 •

Copy link
Copy Markdown
Contributor Author

I think this is a good direction, we should probably alert our downstreams @llvm/clang-vendors to make sure they are comfortable with the new node/having to deal with the new node in AST searches.
We should probably make sure our AST-matchers has a way of seeing these?
Also: I'm a bit concerned about the additional memory pressure, the ExplicitInstantiationDecl is pretty huge. Can we measure the difference in memory usage in some sort of major project here to see?
I'm also interested in if this has a sizable negative effect on compile times, we have the tracker, but I don't recall how to start it (@Endilll ?).

After optimization, this class is now quite small. In my experience, explicit instantiations are rarely used in codebases anyway, meaning very few of these nodes will actually be created. Therefore, I believe the impact on compile time will likely be negligible.

Still worth running the check benchmark though. I hope few of these nodes will be created, but we don't know what we don't know! There could likely be a pathological case here we're not thinking about.

Hi, here are the test results. It seems nikic has been busy recently, so I asked my friend to help run the tests. It looks like the impact on compile time is negligible—only a few hundredths of a percent (around 0.0x%).

https://llvm-compile-time-tracker.com/compare.php?from=4fb21a1d226bff588a66b51452a159b41c5373d5&to=ee2fc2ee0c6d96457339ddd073dde107e7369aff&stat=instructions:u

@erichkeane

Copy link
Copy Markdown
Contributor

I think this is a good direction, we should probably alert our downstreams @llvm/clang-vendors to make sure they are comfortable with the new node/having to deal with the new node in AST searches.
We should probably make sure our AST-matchers has a way of seeing these?
Also: I'm a bit concerned about the additional memory pressure, the ExplicitInstantiationDecl is pretty huge. Can we measure the difference in memory usage in some sort of major project here to see?
I'm also interested in if this has a sizable negative effect on compile times, we have the tracker, but I don't recall how to start it (@Endilll ?).

After optimization, this class is now quite small. In my experience, explicit instantiations are rarely used in codebases anyway, meaning very few of these nodes will actually be created. Therefore, I believe the impact on compile time will likely be negligible.

Still worth running the check benchmark though. I hope few of these nodes will be created, but we don't know what we don't know! There could likely be a pathological case here we're not thinking about.

Hi, here are the test results. It seems nikic has been busy recently, so I asked my friend to help run the tests. It looks like the impact on compile time is negligible—only a few hundredths of a percent (around 0.0x%).

https://llvm-compile-time-tracker.com/compare.php?from=4fb21a1d226bff588a66b51452a159b41c5373d5&to=ee2fc2ee0c6d96457339ddd073dde107e7369aff&stat=instructions:u

Thanks for that, I appreciate you doing that!

I think I'm happy here, but would like to see @zyn0217 and @mizvekov do the approval, since they did a more thorough review than I did.

@zyn0217 zyn0217 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs a release note, otherwise LGTM

@mizvekov for the final approval.

@16bit-ykiko
16bit-ykiko force-pushed the fix-explicit-instantiation branch from b48a910 to 28ad5ef Compare April 22, 2026 16:22

@mizvekov mizvekov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few nits, but LGTM, Thanks!

Comment thread clang/include/clang/AST/ASTContext.h Outdated
Comment thread clang/include/clang/AST/ASTNodeTraverser.h
Comment thread clang/include/clang/AST/RecursiveASTVisitor.h
Comment thread clang/lib/AST/DeclTemplate.cpp Outdated
@16bit-ykiko

Copy link
Copy Markdown
Contributor Author

All tests have passed, but I don't have merge permissions. @mizvekov, could you help merge this? Thanks!

@mizvekov
mizvekov merged commit fb02433 into llvm:main Apr 22, 2026
13 checks passed
yingopq pushed a commit to yingopq/llvm-project that referenced this pull request Apr 29, 2026
…e info and fix diagnostic locations (llvm#191658)

This is the initial fix of
llvm#191442. Following the
discussion here
llvm#115418 (comment).

- Fix llvm#21040
- Fix llvm#52659
- Fix llvm#115418
- Fix llvm#14230
- Fix llvm#21133

### Description

This PR introduces a new AST node, `ExplicitInstantiationDecl`, to
systematically fix the long-standing issue of missing or incorrect
source location information for explicit template instantiations.

#### Background & The Problem
Historically, Clang's AST lacked a dedicated node to represent the
lexical occurrence of an explicit instantiation statement. Instead,
`Sema` tried to shoehorn this information into existing specialization
nodes (e.g., `FunctionDecl`, `VarTemplateSpecializationDecl`) or simply
returned `nullptr`.

This resulted in fragmented behavior across the seven instantiable
entity types:
* Function & Member Function Templates: Returned `nullptr`, completely
losing `SourceRange` and `NestedNameSpecifier` information.
* Member Functions & Static Data Members: Mutated existing nodes
in-place. Consequently, multiple `template` or `extern template`
declarations in the same file would overwrite each other's source
locations.
* Variable Templates: Suffered from `dyn_cast` bugs and dropped NNS
information.

#### Design Trade-offs Evaluated

Before settling on the current design, I evaluated a mixed
redeclaration-chain approach (similar to how explicit *specializations*
are handled, creating new `FunctionDecl` nodes and stitching them into
the redecl chain). However, this approach had significant flaws:
1. Inconsistency: It couldn't be cleanly applied to member functions or
static data members due to `DeclContext` constraints (e.g., a member
function shouldn't lexically reside in a namespace `DeclContext`, but
placing it in the class context would pollute member lookup).
2. Fragility: It required bypassing standard `FoldingSet` mechanisms
(`setFunctionTemplateSpecialization`).
3. Lookup Pollution: Injecting new `NamedDecl` nodes purely for
instantiations risked breaking downstream `ASTMatcher`s and altering
name lookup behavior.

To avoid these pitfalls, this PR introduces `ExplicitInstantiationDecl`
as a **purely lexical annotation node**.

**Key Design Characteristics:**
1. Inherits from `Decl`, not `NamedDecl`: This is the most crucial
design choice. Much like `StaticAssertDecl` or `FriendDecl`, this node
lives in a `DeclContext` (making it traversable by `RecursiveASTVisitor`
and visible in AST dumps) but remains completely invisible to C++ name
lookup. It does not interfere with overload resolution or lookup tables.
2. Unified Representation: A single node type now covers all seven
entity types. It holds a pointer (`Specialization`) to the underlying
instantiated declaration, unifying how functions, variables, classes,
and members are handled.
3. Lexical Fidelity: The node resides in the enclosing namespace or
Translation Unit where the explicit instantiation was actually written,
perfectly preserving the `SourceRange`, `NestedNameSpecifierLoc`, and
the exact locations of the `template` and `extern` keywords.

Assisted-by: Claude Code (Anthropic) — used for test writing and
checking test results
KHicketts pushed a commit to KHicketts/llvm-project that referenced this pull request Apr 30, 2026
…e info and fix diagnostic locations (llvm#191658)

This is the initial fix of
llvm#191442. Following the
discussion here
llvm#115418 (comment).

- Fix llvm#21040
- Fix llvm#52659
- Fix llvm#115418
- Fix llvm#14230
- Fix llvm#21133

### Description

This PR introduces a new AST node, `ExplicitInstantiationDecl`, to
systematically fix the long-standing issue of missing or incorrect
source location information for explicit template instantiations.

#### Background & The Problem
Historically, Clang's AST lacked a dedicated node to represent the
lexical occurrence of an explicit instantiation statement. Instead,
`Sema` tried to shoehorn this information into existing specialization
nodes (e.g., `FunctionDecl`, `VarTemplateSpecializationDecl`) or simply
returned `nullptr`.

This resulted in fragmented behavior across the seven instantiable
entity types:
* Function & Member Function Templates: Returned `nullptr`, completely
losing `SourceRange` and `NestedNameSpecifier` information.
* Member Functions & Static Data Members: Mutated existing nodes
in-place. Consequently, multiple `template` or `extern template`
declarations in the same file would overwrite each other's source
locations.
* Variable Templates: Suffered from `dyn_cast` bugs and dropped NNS
information.

#### Design Trade-offs Evaluated

Before settling on the current design, I evaluated a mixed
redeclaration-chain approach (similar to how explicit *specializations*
are handled, creating new `FunctionDecl` nodes and stitching them into
the redecl chain). However, this approach had significant flaws:
1. Inconsistency: It couldn't be cleanly applied to member functions or
static data members due to `DeclContext` constraints (e.g., a member
function shouldn't lexically reside in a namespace `DeclContext`, but
placing it in the class context would pollute member lookup).
2. Fragility: It required bypassing standard `FoldingSet` mechanisms
(`setFunctionTemplateSpecialization`).
3. Lookup Pollution: Injecting new `NamedDecl` nodes purely for
instantiations risked breaking downstream `ASTMatcher`s and altering
name lookup behavior.

To avoid these pitfalls, this PR introduces `ExplicitInstantiationDecl`
as a **purely lexical annotation node**.

**Key Design Characteristics:**
1. Inherits from `Decl`, not `NamedDecl`: This is the most crucial
design choice. Much like `StaticAssertDecl` or `FriendDecl`, this node
lives in a `DeclContext` (making it traversable by `RecursiveASTVisitor`
and visible in AST dumps) but remains completely invisible to C++ name
lookup. It does not interfere with overload resolution or lookup tables.
2. Unified Representation: A single node type now covers all seven
entity types. It holds a pointer (`Specialization`) to the underlying
instantiated declaration, unifying how functions, variables, classes,
and members are handled.
3. Lexical Fidelity: The node resides in the enclosing namespace or
Translation Unit where the explicit instantiation was actually written,
perfectly preserving the `SourceRange`, `NestedNameSpecifierLoc`, and
the exact locations of the `template` and `extern` keywords.

Assisted-by: Claude Code (Anthropic) — used for test writing and
checking test results
delcypher added a commit to swiftlang/llvm-project that referenced this pull request Apr 30, 2026
…ly_mode.cpp`

This test seems to have primarily broken due to

```
commit fb02433
Author: ykiko <ykikoykikoykiko@gmail.com>
Date:   Thu Apr 23 03:21:27 2026 +0800

     [Clang][AST] Introduce `ExplicitInstantiationDecl` to preserve source info and fix diagnostic locations  (llvm#191658)
```

Explicit template instantiations now appear in the AST so the test case also needed to be updated to account for that.
The word `referenced` also started appearing on other AST nodes too so those lines needed updating too.

rdar://175193249
delcypher added a commit to swiftlang/llvm-project that referenced this pull request Apr 30, 2026
The FunctionTemplateDecl node isn't the last node anymore so the
FileCheck line stopped matching. The last top level node is now `ExplicitInstantiationDecl`.

This was likely caused by:

```
commit fb02433
Author: ykiko <ykikoykikoykiko@gmail.com>
Date:   Thu Apr 23 03:21:27 2026 +0800

     [Clang][AST] Introduce `ExplicitInstantiationDecl` to preserve source info and fix diagnostic locations  (llvm#191658)
```

rdar://175912866
@shafik

shafik commented May 14, 2026

Copy link
Copy Markdown
Contributor

We have a regression linked to this change: #197797 (comment)

16bit-ykiko added a commit to clice-io/clice that referenced this pull request Aug 1, 2026
…nt (#571)

## What

The per-feature migrations onto the Semantics node table left each
feature re-deriving "is this decl a template instantiation" on its own,
and one feature (inlay hints) not deriving it at all. This PR makes the
builder the single write point for that fact and cleans up the
duplication it left behind.

### Instantiation flag (single write point)

- The Semantics builder now sets `in_instantiation` on every node
recorded inside a template-instantiation subtree. The decl created by an
explicit instantiation directive is itself written and stays unflagged;
an implicit instantiation head is not written, so the flag covers it
too. The head predicate lives in the shared `decls::is_instantiation`.
- **Inlay hints** previously walked instantiated subtrees unguarded; the
end-of-pipeline dedup masked it for a single instantiation, but two
explicit instantiations stacked contradictory type hints (`: char` and
`: int`) on the same dependent `auto`. Fixed and pinned by a new
fixture. A side effect consciously accepted: a dependent `auto` local no
longer picks up its type from a lone instantiated body (that only ever
worked by accident); deducing it properly is tracked by the fixture's
`partial` status (clangd#2275).
- **Document symbols** and **folding ranges** drop their local TSK
re-derivations for the shared predicate (byte-identical snapshots).
- The **occurrence layer** (semantic tokens, TU index) deliberately does
NOT skip instantiated bodies: clice treats a template as a duck-typed
interface and each instantiation as an implementation of it, so a
dependent name in the pattern classifies as its actual resolutions — and
as a conflict token when instantiations disagree. Pinned by a dedicated
fixture and written down in the template resolver design doc
("Instantiations as Implementations"; go-to-implementation over these
relations is planned, index-side modeling stays open). The
`in_instantiation` flag stays truthful for the members an explicit
instantiation delivers as top-level decls.

### Hover comment lookup reuse

`decl_for_comment` hand-rolled a weaker version of
`decls::instantiated_from` (its `TSK_Undeclared` fallback always chose
the primary template). It now chases `instantiated_from` to a fixed
point, so an uninstantiated specialization like `Foo<int*>` documents
itself with the matching partial specialization's comment. Pinned for
both class and variable templates.

### Comment scanning

Semantic tokens' `has_logical_newline` re-parsed `/*...*/` syntax by
hand to decide where directive context ends; it now consults the comment
ranges the Lexer scan already collects, so comment syntax is parsed in
one place.

### Explicit instantiation directives (known limitation, now pinned)

Function and variable explicit instantiation directives (`template void
f<int>(int);`) are invisible today — no semantic token on the name, no
outline entry, no occurrence — because clang mislocates them at the
pattern. This is fixed upstream by llvm/llvm-project#191658
(`ExplicitInstantiationDecl`, clang 23); until the toolchain pin catches
up, every workaround site is tagged `FIXME(explicit-instantiation)` and
the current behavior is pinned by `partial` fixtures in the semantic
tokens and document symbol corpora. The class form (childless outline
node, painted reference) keeps working and is pinned alongside.

## Tests

- New fixtures: `inlay_hint/type_conflicting_instantiations`,
`semantic_tokens/explicit_instantiation_directives`,
`document_symbol/kinds_explicit_instantiations`, plus hover pins for
class and variable template comment fallback.
- Full local gate: unit (RelWithDebInfo + Debug/ASan), snap (both,
standalone + wire), integration, smoke, `npm run check`, docs check —
all green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:as-a-library libclang and C++ API clang:codegen IR generation bugs: mangling, exceptions, etc. clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules clang Clang issues not falling into any other category clang-tidy clang-tools-extra ClangIR Anything related to the ClangIR project

Projects

None yet

10 participants