https://github.com/ipopov created 
https://github.com/llvm/llvm-project/pull/202248

### Intro

Human writing: I encountered this problem in Clang, and used some LLM help to 
nail it down to a very simple reproducer (the regression test added in this 
PR), and an "idiomatic" (so I am told, but I cannot fully assess) fix. I would 
appreciate some help seeing if this is indeed reasonable. What follows after 
here was written with the help of AI.

### Bug Description
When instantiating a dependent template definition in a C++20 modules context, 
Clang may select a locally-parsed definition instead of the canonical one from 
an imported module PCM. However, if the template contains nested generic 
lambdas or local classes, their closure types and call operators are eagerly 
canonicalized and merged with the imported module definitions.

This mismatch causes compilation errors during template instantiation: the 
outer function template is instantiated from the local pattern (meaning local 
variables in scope are from the local pattern), but the lambda body is 
instantiated from the imported canonical pattern (which references the imported 
canonical variables). Because `LocalInstantiationScope` only contains the local 
variables from the locally-parsed pattern, references to the canonical 
variables fail to map, causing instantiation failures.

### Solution
1. **Structural Mapping:** Introduce a structural mapping helper in `Sema` 
(`getCanonicalLocalDecl`) to map local variables (`VarDecl` and structured 
bindings `BindingDecl`) from a non-canonical function definition to their 
canonical counterpart using their relative index in the declaration context.
2. **Local Instantiation Scope integration:** Update `LocalInstantiationScope` 
to use this canonical mapping when resolving local variables.
3. **Reduced BMI Preservation:** Prevent stripping the lexical block of 
dependent context template definitions under Reduced BMI, ensuring the 
canonical declarations are preserved and available for mapping.

### Test Coverage
Added a new regression test under 
`clang/test/Modules/modules-generic-lambda-local-capture.cpp` that exercises 
generic lambdas and structured bindings inside templates across module 
boundaries.


>From b2b8d4fae2b3674a04db48afe4daaf81b61975ea Mon Sep 17 00:00:00 2001
From: Ivo Popov <[email protected]>
Date: Mon, 8 Jun 2026 02:49:00 +0000
Subject: [PATCH] [Clang][Modules] Fix generic lambda local capture mapping
 mismatch in template instantiation

---
 clang/include/clang/Sema/Sema.h               |   8 +
 clang/lib/Sema/SemaTemplateInstantiate.cpp    |  21 ++-
 .../lib/Sema/SemaTemplateInstantiateDecl.cpp  |  64 ++++++++
 clang/lib/Serialization/ASTWriter.cpp         |   9 +-
 .../modules-generic-lambda-local-capture.cpp  | 137 ++++++++++++++++++
 5 files changed, 226 insertions(+), 13 deletions(-)
 create mode 100644 clang/test/Modules/modules-generic-lambda-local-capture.cpp

diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index ff474fdd99562..bcd4dfe0728e7 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -13100,6 +13100,14 @@ class Sema final : public SemaBase {
   /// variables.
   LocalInstantiationScope *CurrentInstantiationScope;
 
+  /// A mapping from canonical function definitions to maps of their local
+  /// declarations to canonical local declarations.
+  llvm::DenseMap<const DeclContext *,
+                 llvm::DenseMap<const Decl *, const Decl *>>
+      CanonicalLocalDecls;
+
+  const Decl *getCanonicalLocalDecl(const Decl *D);
+
   typedef llvm::DenseMap<ParmVarDecl *, llvm::TinyPtrVector<ParmVarDecl *>>
       UnparsedDefaultArgInstantiationsMap;
 
diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp 
b/clang/lib/Sema/SemaTemplateInstantiate.cpp
index 6df6d5505c61c..da5b8b412404d 100644
--- a/clang/lib/Sema/SemaTemplateInstantiate.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp
@@ -4399,26 +4399,23 @@ Sema::SubstTemplateName(SourceLocation TemplateKWLoc,
                                             NameLoc);
 }
 
-static const Decl *getCanonicalParmVarDecl(const Decl *D) {
-  // When storing ParmVarDecls in the local instantiation scope, we always
-  // want to use the ParmVarDecl from the canonical function declaration,
-  // since the map is then valid for any redeclaration or definition of that
-  // function.
+static const Decl *getCanonicalLocalDecl(const Decl *D, Sema &SemaRef) {
   if (const ParmVarDecl *PV = dyn_cast<ParmVarDecl>(D)) {
     if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(PV->getDeclContext())) 
{
       unsigned i = PV->getFunctionScopeIndex();
-      // This parameter might be from a freestanding function type within the
-      // function and isn't necessarily referring to one of FD's parameters.
-      if (i < FD->getNumParams() && FD->getParamDecl(i) == PV)
+      if (i < FD->getNumParams() && FD->getParamDecl(i) == PV) {
         return FD->getCanonicalDecl()->getParamDecl(i);
     }
   }
+  } else if (isa<VarDecl>(D) || isa<BindingDecl>(D)) {
+    return SemaRef.getCanonicalLocalDecl(D);
+  }
   return D;
 }
 
 llvm::PointerUnion<Decl *, LocalInstantiationScope::DeclArgumentPack *> *
 LocalInstantiationScope::getInstantiationOfIfExists(const Decl *D) {
-  D = getCanonicalParmVarDecl(D);
+  D = getCanonicalLocalDecl(D, SemaRef);
   for (LocalInstantiationScope *Current = this; Current;
        Current = Current->Outer) {
 
@@ -4480,7 +4477,7 @@ LocalInstantiationScope::findInstantiationOf(const Decl 
*D) {
 }
 
 void LocalInstantiationScope::InstantiatedLocal(const Decl *D, Decl *Inst) {
-  D = getCanonicalParmVarDecl(D);
+  D = getCanonicalLocalDecl(D, SemaRef);
   llvm::PointerUnion<Decl *, DeclArgumentPack *> &Stored = LocalDecls[D];
   if (Stored.isNull()) {
 #ifndef NDEBUG
@@ -4502,7 +4499,7 @@ void LocalInstantiationScope::InstantiatedLocal(const 
Decl *D, Decl *Inst) {
 
 void LocalInstantiationScope::InstantiatedLocalPackArg(const Decl *D,
                                                        VarDecl *Inst) {
-  D = getCanonicalParmVarDecl(D);
+  D = getCanonicalLocalDecl(D, SemaRef);
   DeclArgumentPack *Pack = cast<DeclArgumentPack *>(LocalDecls[D]);
   Pack->push_back(Inst);
 }
@@ -4516,7 +4513,7 @@ void 
LocalInstantiationScope::MakeInstantiatedLocalArgPack(const Decl *D) {
            "Creating local pack after instantiation of local");
 #endif
 
-  D = getCanonicalParmVarDecl(D);
+  D = getCanonicalLocalDecl(D, SemaRef);
   llvm::PointerUnion<Decl *, DeclArgumentPack *> &Stored = LocalDecls[D];
   DeclArgumentPack *Pack = new DeclArgumentPack;
   Stored = Pack;
diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp 
b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
index aa381f09138de..1cf5497078cc3 100644
--- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
@@ -7421,3 +7421,67 @@ void Sema::PerformDependentDiagnostics(const DeclContext 
*Pattern,
     }
   }
 }
+
+static bool isMappedLocalDecl(const Decl *D) {
+  if (const auto *VD = dyn_cast<VarDecl>(D)) {
+    return VD->isLocalVarDeclOrParm() && !isa<ParmVarDecl>(VD);
+  }
+  return isa<BindingDecl>(D);
+}
+
+const Decl *Sema::getCanonicalLocalDecl(const Decl *D) {
+  if (isa<ParmVarDecl>(D)) {
+    return D;
+  }
+
+  const auto *VD = dyn_cast<VarDecl>(D);
+  const auto *BD = dyn_cast<BindingDecl>(D);
+  if (!VD && !BD) {
+    return D;
+  }
+
+  if (VD && !VD->isLocalVarDeclOrParm()) {
+    return D;
+  }
+
+  const DeclContext *DC = VD ? VD->getDeclContext() : BD->getDeclContext();
+  const auto *FD = dyn_cast<FunctionDecl>(DC);
+  if (!FD) {
+    return D;
+  }
+
+  const auto *CanonFD = FD->getCanonicalDecl();
+  if (FD == CanonFD) {
+    return D;
+  }
+
+  auto &Map = CanonicalLocalDecls[CanonFD];
+  if (Map.empty()) {
+    SmallVector<const Decl *, 8> CanonDecls;
+    for (const auto *De : CanonFD->decls()) {
+      if (isMappedLocalDecl(De)) {
+        CanonDecls.push_back(De);
+      }
+    }
+
+    SmallVector<const Decl *, 8> LocalDecls;
+    for (const auto *De : FD->decls()) {
+      if (isMappedLocalDecl(De)) {
+        LocalDecls.push_back(De);
+      }
+    }
+
+    if (CanonDecls.size() == LocalDecls.size()) {
+      for (size_t I = 0; I < LocalDecls.size(); ++I) {
+        Map[LocalDecls[I]] = CanonDecls[I];
+      }
+    }
+  }
+
+  const auto It = Map.find(D);
+  if (It != Map.end()) {
+    return It->second;
+  }
+
+  return D;
+}
diff --git a/clang/lib/Serialization/ASTWriter.cpp 
b/clang/lib/Serialization/ASTWriter.cpp
index 6497c22a762ae..8f0174d6f470c 100644
--- a/clang/lib/Serialization/ASTWriter.cpp
+++ b/clang/lib/Serialization/ASTWriter.cpp
@@ -3455,8 +3455,15 @@ uint64_t 
ASTWriter::WriteDeclContextLexicalBlock(ASTContext &Context,
     return 0;
 
   // In reduced BMI, we don't care the declarations in functions.
-  if (GeneratingReducedBMI && DC->isFunctionOrMethod())
+  if (GeneratingReducedBMI && DC->isFunctionOrMethod()) {
+    if (const auto *FD = dyn_cast<FunctionDecl>(DC)) {
+      if (!FD->isDependentContext() || !FD->isThisDeclarationADefinition()) {
     return 0;
+      }
+    } else {
+      return 0;
+    }
+  }
 
   uint64_t Offset = Stream.GetCurrentBitNo();
   SmallVector<DeclID, 128> KindDeclPairs;
diff --git a/clang/test/Modules/modules-generic-lambda-local-capture.cpp 
b/clang/test/Modules/modules-generic-lambda-local-capture.cpp
new file mode 100644
index 0000000000000..ca307ac796a90
--- /dev/null
+++ b/clang/test/Modules/modules-generic-lambda-local-capture.cpp
@@ -0,0 +1,137 @@
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+// RUN: cd %t
+//
+// RUN: %clang_cc1 -std=c++20 -fmodules -fno-implicit-modules \
+// RUN:            -fmodules-local-submodule-visibility \
+// RUN:            -fmodule-map-file=module.modulemap \
+// RUN:            -fmodule-name=repro_module_a -emit-module \
+// RUN:            -fmodules-embed-all-files -x c++ module.modulemap \
+// RUN:            -o repro_module_a.pcm
+//
+// RUN: %clang_cc1 -std=c++20 -fmodules -fno-implicit-modules \
+// RUN:            -fmodules-local-submodule-visibility \
+// RUN:            -fmodule-map-file=module.modulemap \
+// RUN:            -fmodule-name=repro_wrapper_mock \
+// RUN:            -fmodule-file=repro_module_a=repro_module_a.pcm \
+// RUN:            -emit-module -fmodules-embed-all-files -x c++ 
module.modulemap \
+// RUN:            -o repro_wrapper_mock.pcm
+//
+// RUN: %clang_cc1 -std=c++20 -fmodules -fno-implicit-modules \
+// RUN:            -fmodules-local-submodule-visibility \
+// RUN:            -fmodule-map-file=module.modulemap \
+// RUN:            -fmodule-name=repro \
+// RUN:            -fmodule-file=repro_module_a=repro_module_a.pcm \
+// RUN:            -fmodule-file=repro_wrapper_mock=repro_wrapper_mock.pcm \
+// RUN:            -fsyntax-only repro_main.cpp
+
+//--- module.modulemap
+module TemplateClassModule {
+  textual header "template_class.h"
+}
+module repro_module_a {
+  header "module_a.h"
+  export *
+  use TemplateClassModule
+}
+module repro_wrapper_mock {
+  header "repro_wrapper.h"
+  export *
+  use repro_module_a
+  use TemplateClassModule
+}
+module repro {
+  export *
+  use repro_wrapper_mock
+}
+
+//--- template_class.h
+#ifndef TEMPLATE_CLASS_H_
+#define TEMPLATE_CLASS_H_
+namespace std {
+template <typename T>
+T&& move(T& t) noexcept { return static_cast<T&&>(t); }
+}
+
+enum class MyEnum { kValue1, kValue2 };
+inline MyEnum GetLocalValue() { return MyEnum::kValue2; }
+
+struct Pair { MyEnum first; MyEnum second; };
+inline Pair GetLocalPair() { return Pair{MyEnum::kValue1, MyEnum::kValue2}; }
+
+template <typename T>
+struct Consumer {
+  template <typename U>
+  void operator()(const U& x) const {}
+};
+
+template <typename F>
+struct Holder {
+  F f;
+  explicit Holder(F f) : f(std::move(f)) {}
+  void call() const { f(0); }
+};
+
+template <typename F>
+auto make_holder(F f) {
+  return Holder<F>(std::move(f));
+}
+
+template <typename T>
+class TemplateClass {
+ public:
+  template <typename... Args>
+  explicit TemplateClass(Args&&... args) {
+    const auto local_val = GetLocalValue();
+    const auto [a, b] = GetLocalPair();
+    auto holder = make_holder([&](auto x) {
+      Consumer<decltype(x)>{}(local_val);
+      Consumer<decltype(x)>{}(a);
+    });
+    holder.call();
+  }
+};
+#endif
+
+//--- module_a.h
+#pragma once
+#include "template_class.h"
+
+//--- repro_wrapper.h
+#ifndef REPRO_WRAPPER_H_
+#define REPRO_WRAPPER_H_
+#include "template_class.h"
+#include "module_a.h"
+inline void TriggerInstantiation() {
+  TemplateClass<void> p;
+}
+#endif
+
+//--- repro_token.h
+#ifndef REPRO_TOKEN_H_
+#define REPRO_TOKEN_H_
+#include "template_class.h"
+#include "module_a.h"
+template <typename... Ts>
+struct MyOverload : Ts... {
+  constexpr MyOverload(Ts... ts) : Ts(std::move(ts))... {}
+};
+template <typename... Ts>
+MyOverload(Ts...) -> MyOverload<Ts...>;
+inline auto my_lambda = [](int x) { return x; };
+inline MyOverload visitor{my_lambda};
+
+struct Token {
+  void TriggerMethod() const {
+    TemplateClass<void> p(*this);
+  }
+};
+#endif
+
+//--- repro_main.cpp
+#include "repro_wrapper.h"
+#include "repro_token.h"
+int main() {
+  Token t;
+  t.TriggerMethod();
+}

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

Reply via email to