[clang] [clang][HLSL] Complete the pattern an instantiation is built from, not the primary template (PR #216388)

Deric C. via cfe-commits cfe-commits at lists.llvm.org
Fri Aug 14 18:01:51 PDT 2026


https://github.com/Icohedron updated https://github.com/llvm/llvm-project/pull/216388

>From 33222fd092d08933c6e87d02a25f3bb43f22e61d Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Fri, 14 Aug 2026 11:53:18 -0700
Subject: [PATCH 1/3] Fix lazy template partial specialization instantiation

---
 clang/lib/AST/DeclTemplate.cpp                |   8 --
 clang/lib/Sema/HLSLExternalSemaSource.cpp     |  27 +----
 clang/lib/Sema/SemaTemplateInstantiate.cpp    |   7 ++
 .../Resources/Textures-declaration-order.hlsl |  48 ++++++++
 clang/unittests/AST/ExternalASTSourceTest.cpp | 111 ++++++++++++++++--
 5 files changed, 160 insertions(+), 41 deletions(-)
 create mode 100644 clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl

diff --git a/clang/lib/AST/DeclTemplate.cpp b/clang/lib/AST/DeclTemplate.cpp
index 3ff6e3a3221fb..93514752e5a22 100644
--- a/clang/lib/AST/DeclTemplate.cpp
+++ b/clang/lib/AST/DeclTemplate.cpp
@@ -991,14 +991,6 @@ ClassTemplateSpecializationDecl *ClassTemplateSpecializationDecl::Create(
       Context, ClassTemplateSpecialization, TK, DC, StartLoc, IdLoc,
       SpecializedTemplate, Args, StrictPackMatch, PrevDecl);
 
-  // If the template decl is incomplete, copy the external lexical storage from
-  // the base template. This allows instantiations of incomplete types to
-  // complete using the external AST if the template's declaration came from an
-  // external AST.
-  if (!SpecializedTemplate->getTemplatedDecl()->isCompleteDefinition())
-    Result->setHasExternalLexicalStorage(
-      SpecializedTemplate->getTemplatedDecl()->hasExternalLexicalStorage());
-
   return Result;
 }
 
diff --git a/clang/lib/Sema/HLSLExternalSemaSource.cpp b/clang/lib/Sema/HLSLExternalSemaSource.cpp
index 61998e5653a00..8eb8f073ca1c9 100644
--- a/clang/lib/Sema/HLSLExternalSemaSource.cpp
+++ b/clang/lib/Sema/HLSLExternalSemaSource.cpp
@@ -24,7 +24,6 @@
 #include "clang/Sema/Lookup.h"
 #include "clang/Sema/Sema.h"
 #include "clang/Sema/SemaHLSL.h"
-#include "clang/Sema/TemplateDeduction.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SmallVector.h"
 
@@ -856,31 +855,7 @@ void HLSLExternalSemaSource::onCompletion(CXXRecordDecl *Record,
 void HLSLExternalSemaSource::CompleteType(TagDecl *Tag) {
   if (!isa<CXXRecordDecl>(Tag))
     return;
-  auto Record = cast<CXXRecordDecl>(Tag);
-
-  // If this is a specialization, we need to get the underlying templated
-  // declaration and complete that.
-  if (auto TDecl = dyn_cast<ClassTemplateSpecializationDecl>(Record)) {
-    if (!isa<ClassTemplatePartialSpecializationDecl>(TDecl)) {
-      ClassTemplateDecl *Template = TDecl->getSpecializedTemplate();
-      llvm::SmallVector<ClassTemplatePartialSpecializationDecl *, 4> Partials;
-      Template->getPartialSpecializations(Partials);
-      ClassTemplatePartialSpecializationDecl *MatchedPartial = nullptr;
-      for (auto *Partial : Partials) {
-        sema::TemplateDeductionInfo Info(TDecl->getLocation());
-        if (SemaPtr->DeduceTemplateArguments(Partial, TDecl->getTemplateArgs(),
-                                             Info) ==
-            TemplateDeductionResult::Success) {
-          MatchedPartial = Partial;
-          break;
-        }
-      }
-      if (MatchedPartial)
-        Record = MatchedPartial;
-      else
-        Record = Template->getTemplatedDecl();
-    }
-  }
+  auto *Record = cast<CXXRecordDecl>(Tag);
   Record = Record->getCanonicalDecl();
   auto It = Completions.find(Record);
   if (It == Completions.end())
diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp
index 21d68f765bdaf..11481eaeabd7c 100644
--- a/clang/lib/Sema/SemaTemplateInstantiate.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp
@@ -4184,6 +4184,13 @@ bool Sema::InstantiateClassTemplateSpecialization(
   if (!Pattern.isUsable())
     return Pattern.isInvalid();
 
+  // Deduction has picked the pattern this specialization will be instantiated
+  // from, which may be a declaration an external AST source has yet to define.
+  if (!Pattern.get()->isCompleteDefinition() &&
+      Pattern.get()->hasExternalLexicalStorage())
+    if (ExternalASTSource *Source = Context.getExternalSource())
+      Source->CompleteType(Pattern.get());
+
   bool Err = InstantiateClassImpl(
       PointOfInstantiation, ClassTemplateSpec, Pattern.get(),
       getTemplateInstantiationArgs(ClassTemplateSpec), TSK, Complain);
diff --git a/clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl b/clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl
new file mode 100644
index 0000000000000..ef4acc1aed590
--- /dev/null
+++ b/clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl
@@ -0,0 +1,48 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -fsyntax-only -finclude-default-header -verify -DSCALAR_FIRST %s
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -fsyntax-only -finclude-default-header -verify %s
+// RUN: %clang_cc1 -triple spirv-unknown-vulkan-library -x hlsl -fsyntax-only -finclude-default-header -verify -DSCALAR_FIRST %s
+// RUN: %clang_cc1 -triple spirv-unknown-vulkan-library -x hlsl -fsyntax-only -finclude-default-header -verify %s
+
+// Texture resource classes are declared as a primary class template, used for
+// scalar element types, plus a partial specialization used for vector element
+// types. Both patterns are only defined on demand by HLSLExternalSemaSource, so
+// completing one of them must not prevent the other one from being completed.
+// See https://github.com/llvm/llvm-project/issues/212575.
+
+// expected-no-diagnostics
+
+#ifdef SCALAR_FIRST
+Texture2D<float> Tex2D;
+Texture2D<float2> Tex2DVec;
+RWTexture2D<float> RWTex2D;
+RWTexture2D<float2> RWTex2DVec;
+Texture2DArray<float> Tex2DArray;
+Texture2DArray<float2> Tex2DArrayVec;
+RWTexture2DArray<float> RWTex2DArray;
+RWTexture2DArray<float2> RWTex2DArrayVec;
+#else
+Texture2D<float2> Tex2DVec;
+Texture2D<float> Tex2D;
+RWTexture2D<float2> RWTex2DVec;
+RWTexture2D<float> RWTex2D;
+Texture2DArray<float2> Tex2DArrayVec;
+Texture2DArray<float> Tex2DArray;
+RWTexture2DArray<float2> RWTex2DArrayVec;
+RWTexture2DArray<float> RWTex2DArray;
+#endif
+
+SamplerState Samp;
+
+// Use members of both the primary template and the partial specialization to
+// make sure both patterns really have been completed.
+export void useTextures(float2 UV, float3 UVW) {
+  float S = Tex2D.Sample(Samp, UV);
+  float2 V = Tex2DVec.Sample(Samp, UV);
+  RWTex2D[uint2(0, 0)] = S;
+  RWTex2DVec[uint2(0, 0)] = V;
+
+  float AS = Tex2DArray.Sample(Samp, UVW);
+  float2 AV = Tex2DArrayVec.Sample(Samp, UVW);
+  RWTex2DArray[uint3(0, 0, 0)] = AS;
+  RWTex2DArrayVec[uint3(0, 0, 0)] = AV;
+}
diff --git a/clang/unittests/AST/ExternalASTSourceTest.cpp b/clang/unittests/AST/ExternalASTSourceTest.cpp
index 15483ad250976..0616ac9fd092c 100644
--- a/clang/unittests/AST/ExternalASTSourceTest.cpp
+++ b/clang/unittests/AST/ExternalASTSourceTest.cpp
@@ -13,6 +13,8 @@
 #include "clang/AST/ExternalASTSource.h"
 #include "clang/AST/ASTConsumer.h"
 #include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/DeclTemplate.h"
 #include "clang/Frontend/CompilerInstance.h"
 #include "clang/Frontend/CompilerInvocation.h"
 #include "clang/Frontend/FrontendActions.h"
@@ -26,14 +28,18 @@ using namespace llvm;
 
 class TestFrontendAction : public ASTFrontendAction {
 public:
-  TestFrontendAction(IntrusiveRefCntPtr<ExternalASTSource> Source)
-      : Source(std::move(Source)) {}
+  TestFrontendAction(IntrusiveRefCntPtr<ExternalASTSource> Source,
+                     std::function<void(ASTContext &)> Inject = nullptr)
+      : Source(std::move(Source)), Inject(std::move(Inject)) {}
 
 private:
   void ExecuteAction() override {
-    getCompilerInstance().getASTContext().setExternalSource(Source);
-    getCompilerInstance().getASTContext().getTranslationUnitDecl()
-        ->setHasExternalVisibleStorage();
+    ASTContext &Ctx = getCompilerInstance().getASTContext();
+    Ctx.setExternalSource(Source);
+    if (Inject)
+      Inject(Ctx);
+    else
+      Ctx.getTranslationUnitDecl()->setHasExternalVisibleStorage();
     return ASTFrontendAction::ExecuteAction();
   }
 
@@ -43,10 +49,12 @@ class TestFrontendAction : public ASTFrontendAction {
   }
 
   IntrusiveRefCntPtr<ExternalASTSource> Source;
+  std::function<void(ASTContext &)> Inject;
 };
 
 bool testExternalASTSource(llvm::IntrusiveRefCntPtr<ExternalASTSource> Source,
-                           StringRef FileContents) {
+                           StringRef FileContents,
+                           std::function<void(ASTContext &)> Inject = nullptr) {
 
   auto Invocation = std::make_shared<CompilerInvocation>();
   Invocation->getPreprocessorOpts().addRemappedFile(
@@ -62,7 +70,7 @@ bool testExternalASTSource(llvm::IntrusiveRefCntPtr<ExternalASTSource> Source,
   Compiler.setVirtualFileSystem(llvm::vfs::getRealFileSystem());
   Compiler.createDiagnostics();
 
-  TestFrontendAction Action(Source);
+  TestFrontendAction Action(Source, std::move(Inject));
   return Compiler.ExecuteAction(Action);
 }
 
@@ -87,3 +95,92 @@ TEST(ExternalASTSourceTest, FailedLookupOccursOnce) {
       llvm::makeIntrusiveRefCnt<TestSource>(Calls), "int j, k = j;"));
   EXPECT_EQ(1u, Calls);
 }
+
+namespace {
+
+/// An external source which announces, without definitions,
+///
+///   template <typename T> struct A;      // primary pattern
+///   template <typename T> struct A<T *>; // partial specialization pattern
+///
+/// and supplies the definition of whichever pattern it is asked to complete.
+struct LazyTemplatePatterns : ExternalASTSource {
+  CXXRecordDecl *Primary = nullptr;
+  ClassTemplatePartialSpecializationDecl *Partial = nullptr;
+  unsigned PrimaryCompletions = 0;
+  unsigned PartialCompletions = 0;
+
+  void inject(ASTContext &Ctx) {
+    TranslationUnitDecl *TU = Ctx.getTranslationUnitDecl();
+    IdentifierInfo &AName = Ctx.Idents.get("A");
+
+    auto MakeParams = [&] {
+      auto *Param = TemplateTypeParmDecl::Create(
+          Ctx, TU, {}, {}, /*D=*/0, /*P=*/0, &Ctx.Idents.get("T"),
+          /*Typename=*/true, /*ParameterPack=*/false);
+      return TemplateParameterList::Create(Ctx, {}, {}, {Param}, {}, nullptr);
+    };
+
+    // template <typename T> struct A;
+    Primary = CXXRecordDecl::Create(Ctx, TagDecl::TagKind::Struct, TU, {}, {},
+                                    &AName);
+    auto *Template = ClassTemplateDecl::Create(
+        Ctx, TU, {}, DeclarationName(&AName), MakeParams(), Primary);
+    Primary->setDescribedClassTemplate(Template);
+    Primary->setHasExternalLexicalStorage();
+    TU->addDecl(Template);
+
+    // template <typename T> struct A<T *>;
+    TemplateParameterList *PartialParams = MakeParams();
+    TemplateArgument Arg(Ctx.getPointerType(Ctx.getTemplateTypeParmType(
+        /*Depth=*/0, /*Index=*/0, /*ParameterPack=*/false,
+        cast<TemplateTypeParmDecl>(PartialParams->getParam(0)))));
+    Partial = ClassTemplatePartialSpecializationDecl::Create(
+        Ctx, TagDecl::TagKind::Struct, TU, {}, {}, PartialParams, Template, Arg,
+        Ctx.getCanonicalType(Ctx.getTemplateSpecializationType(
+            ElaboratedTypeKeyword::Struct, TemplateName(Template), Arg, {})),
+        nullptr);
+    Partial->setHasExternalLexicalStorage();
+
+    // Deduction against a partial specialization reads its arguments as
+    // written, so they must be supplied even though nothing was written.
+    TemplateArgumentListInfo ArgsInfo;
+    ArgsInfo.addArgument(TemplateArgumentLoc(
+        Arg, Ctx.getTrivialTypeSourceInfo(Arg.getAsType())));
+    Partial->setTemplateArgsAsWritten(
+        ASTTemplateArgumentListInfo::Create(Ctx, ArgsInfo));
+
+    TU->addDecl(Partial);
+    Template->AddPartialSpecialization(Partial, nullptr);
+  }
+
+  void CompleteType(TagDecl *Tag) override {
+    auto *Record = dyn_cast<CXXRecordDecl>(Tag);
+    if (!Record || Record->isCompleteDefinition())
+      return;
+    if (Record == Primary)
+      ++PrimaryCompletions;
+    else if (Record == Partial)
+      ++PartialCompletions;
+    else
+      return;
+    Record->setHasExternalLexicalStorage(false);
+    Record->startDefinition();
+    Record->completeDefinition();
+  }
+};
+
+} // namespace
+
+// An instantiation must be able to complete the pattern it is actually built
+// from, whichever of the two that is, and in either order.
+TEST(ExternalASTSourceTest, CompletesPatternInEitherOrder) {
+  for (StringRef Code : {"A<int> a; A<int *> b;", "A<int *> b; A<int> a;"}) {
+    auto Source = llvm::makeIntrusiveRefCnt<LazyTemplatePatterns>();
+    ASSERT_TRUE(testExternalASTSource(Source, Code, [&](ASTContext &Ctx) {
+      Source->inject(Ctx);
+    })) << Code;
+    EXPECT_EQ(1u, Source->PrimaryCompletions) << Code;
+    EXPECT_EQ(1u, Source->PartialCompletions) << Code;
+  }
+}

>From 77bd8b956e7f7ed8fe0b40c8abb5b502555f24b6 Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Fri, 14 Aug 2026 17:58:39 -0700
Subject: [PATCH 2/3] Add -USCALAR_FIRST on the scalar-last run lines for
 clarity

---
 clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl b/clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl
index ef4acc1aed590..408c9dfe0451d 100644
--- a/clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl
+++ b/clang/test/SemaHLSL/Resources/Textures-declaration-order.hlsl
@@ -1,7 +1,7 @@
 // RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -fsyntax-only -finclude-default-header -verify -DSCALAR_FIRST %s
-// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -fsyntax-only -finclude-default-header -verify %s
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -x hlsl -fsyntax-only -finclude-default-header -verify -USCALAR_FIRST %s
 // RUN: %clang_cc1 -triple spirv-unknown-vulkan-library -x hlsl -fsyntax-only -finclude-default-header -verify -DSCALAR_FIRST %s
-// RUN: %clang_cc1 -triple spirv-unknown-vulkan-library -x hlsl -fsyntax-only -finclude-default-header -verify %s
+// RUN: %clang_cc1 -triple spirv-unknown-vulkan-library -x hlsl -fsyntax-only -finclude-default-header -verify -USCALAR_FIRST %s
 
 // Texture resource classes are declared as a primary class template, used for
 // scalar element types, plus a partial specialization used for vector element

>From 11bb9082e2a8f575b1b04117c34990cccbc1315f Mon Sep 17 00:00:00 2001
From: Deric Cheung <cheung.deric at gmail.com>
Date: Fri, 14 Aug 2026 18:01:07 -0700
Subject: [PATCH 3/3] Add TestExternalASTSource class for test refactoring

---
 clang/unittests/AST/ExternalASTSourceTest.cpp | 36 +++++++++----------
 1 file changed, 17 insertions(+), 19 deletions(-)

diff --git a/clang/unittests/AST/ExternalASTSourceTest.cpp b/clang/unittests/AST/ExternalASTSourceTest.cpp
index 0616ac9fd092c..19bc143ddb3d3 100644
--- a/clang/unittests/AST/ExternalASTSourceTest.cpp
+++ b/clang/unittests/AST/ExternalASTSourceTest.cpp
@@ -25,21 +25,22 @@
 using namespace clang;
 using namespace llvm;
 
+struct TestExternalASTSource : public ExternalASTSource {
+  virtual void setupTestAST(ASTContext &Ctx) {
+    Ctx.getTranslationUnitDecl()->setHasExternalVisibleStorage();
+  }
+};
 
 class TestFrontendAction : public ASTFrontendAction {
 public:
-  TestFrontendAction(IntrusiveRefCntPtr<ExternalASTSource> Source,
-                     std::function<void(ASTContext &)> Inject = nullptr)
-      : Source(std::move(Source)), Inject(std::move(Inject)) {}
+  TestFrontendAction(IntrusiveRefCntPtr<TestExternalASTSource> Source)
+      : Source(std::move(Source)) {}
 
 private:
   void ExecuteAction() override {
     ASTContext &Ctx = getCompilerInstance().getASTContext();
     Ctx.setExternalSource(Source);
-    if (Inject)
-      Inject(Ctx);
-    else
-      Ctx.getTranslationUnitDecl()->setHasExternalVisibleStorage();
+    Source->setupTestAST(Ctx);
     return ASTFrontendAction::ExecuteAction();
   }
 
@@ -48,13 +49,12 @@ class TestFrontendAction : public ASTFrontendAction {
     return std::make_unique<ASTConsumer>();
   }
 
-  IntrusiveRefCntPtr<ExternalASTSource> Source;
-  std::function<void(ASTContext &)> Inject;
+  IntrusiveRefCntPtr<TestExternalASTSource> Source;
 };
 
-bool testExternalASTSource(llvm::IntrusiveRefCntPtr<ExternalASTSource> Source,
-                           StringRef FileContents,
-                           std::function<void(ASTContext &)> Inject = nullptr) {
+bool testExternalASTSource(
+    llvm::IntrusiveRefCntPtr<TestExternalASTSource> Source,
+    StringRef FileContents) {
 
   auto Invocation = std::make_shared<CompilerInvocation>();
   Invocation->getPreprocessorOpts().addRemappedFile(
@@ -70,13 +70,13 @@ bool testExternalASTSource(llvm::IntrusiveRefCntPtr<ExternalASTSource> Source,
   Compiler.setVirtualFileSystem(llvm::vfs::getRealFileSystem());
   Compiler.createDiagnostics();
 
-  TestFrontendAction Action(Source, std::move(Inject));
+  TestFrontendAction Action(Source);
   return Compiler.ExecuteAction(Action);
 }
 
 // Ensure that a failed name lookup into an external source only occurs once.
 TEST(ExternalASTSourceTest, FailedLookupOccursOnce) {
-  struct TestSource : ExternalASTSource {
+  struct TestSource : TestExternalASTSource {
     TestSource(unsigned &Calls) : Calls(Calls) {}
 
     bool
@@ -104,13 +104,13 @@ namespace {
 ///   template <typename T> struct A<T *>; // partial specialization pattern
 ///
 /// and supplies the definition of whichever pattern it is asked to complete.
-struct LazyTemplatePatterns : ExternalASTSource {
+struct LazyTemplatePatterns : TestExternalASTSource {
   CXXRecordDecl *Primary = nullptr;
   ClassTemplatePartialSpecializationDecl *Partial = nullptr;
   unsigned PrimaryCompletions = 0;
   unsigned PartialCompletions = 0;
 
-  void inject(ASTContext &Ctx) {
+  void setupTestAST(ASTContext &Ctx) override {
     TranslationUnitDecl *TU = Ctx.getTranslationUnitDecl();
     IdentifierInfo &AName = Ctx.Idents.get("A");
 
@@ -177,9 +177,7 @@ struct LazyTemplatePatterns : ExternalASTSource {
 TEST(ExternalASTSourceTest, CompletesPatternInEitherOrder) {
   for (StringRef Code : {"A<int> a; A<int *> b;", "A<int *> b; A<int> a;"}) {
     auto Source = llvm::makeIntrusiveRefCnt<LazyTemplatePatterns>();
-    ASSERT_TRUE(testExternalASTSource(Source, Code, [&](ASTContext &Ctx) {
-      Source->inject(Ctx);
-    })) << Code;
+    ASSERT_TRUE(testExternalASTSource(Source, Code)) << Code;
     EXPECT_EQ(1u, Source->PrimaryCompletions) << Code;
     EXPECT_EQ(1u, Source->PartialCompletions) << Code;
   }



More information about the cfe-commits mailing list