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
