[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ChuanqiXu9 approved this pull request. LGTM now. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ipopov edited https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
ipopov wrote: Done. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -498,6 +498,9 @@ bool serialization::needsAnonymousDeclarationNumber(const
NamedDecl *D) {
if (D->getLexicalDeclContext()->isFunctionOrMethod()) {
if (auto *VD = dyn_cast(D))
return VD->isStaticLocal();
+if (const auto *TD = dyn_cast(D))
+ if (TD->getLexicalDeclContext()->isDependentContext())
+return false;
ipopov wrote:
Done.
https://github.com/llvm/llvm-project/pull/202248
___
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ipopov updated
https://github.com/llvm/llvm-project/pull/202248
>From 5805a98439860b7ddbae7558ebd4ac6f0ee00f16 Mon Sep 17 00:00:00 2001
From: ipopov
Date: Fri, 26 Jun 2026 15:28:02 -0700
Subject: [PATCH 1/3] Serialization: Skip anonymous declaration numbering for
local tags in dependent contexts
Local tag declarations (classes, structs, enums, and lambdas) defined within
function template bodies or class templates do not require ODR merging at the
local declaration level across different modules. ODR consistency is already
guaranteed because the instantiator only walks the canonical template
definition body, which instantiates its own copy of the local class.
Merging them across different modules causes their member definitions (like
methods or call operators) to be canonicalized to one module, while the
instantiator walks the template body from another module. Since local variables
within template bodies are not merged, this mismatch leads to assertion crashes
during template instantiation in `LocalInstantiationScope::findInstantiationOf`.
---
clang/lib/Serialization/ASTCommon.cpp | 3 +
.../modules-lambda-dependent-crash.cppm | 73 +++
2 files changed, 76 insertions(+)
create mode 100644 clang/test/Modules/modules-lambda-dependent-crash.cppm
diff --git a/clang/lib/Serialization/ASTCommon.cpp
b/clang/lib/Serialization/ASTCommon.cpp
index 49e6fe8004cec..e3df196242919 100644
--- a/clang/lib/Serialization/ASTCommon.cpp
+++ b/clang/lib/Serialization/ASTCommon.cpp
@@ -498,6 +498,9 @@ bool serialization::needsAnonymousDeclarationNumber(const
NamedDecl *D) {
if (D->getLexicalDeclContext()->isFunctionOrMethod()) {
if (auto *VD = dyn_cast(D))
return VD->isStaticLocal();
+if (const auto *TD = dyn_cast(D))
+ if (TD->getLexicalDeclContext()->isDependentContext())
+return false;
// FIXME: What about CapturedDecls (and declarations nested within them)?
return isa(D);
}
diff --git a/clang/test/Modules/modules-lambda-dependent-crash.cppm
b/clang/test/Modules/modules-lambda-dependent-crash.cppm
new file mode 100644
index 0..a0701b54c1989
--- /dev/null
+++ b/clang/test/Modules/modules-lambda-dependent-crash.cppm
@@ -0,0 +1,73 @@
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+// RUN: cd %t
+//
+// RUN: %clang_cc1 -std=c++20 -I. m_template.cppm
-emit-reduced-module-interface \
+// RUN:-o m_template.pcm
+//
+// RUN: %clang_cc1 -std=c++20 -I. m_wrapper.cppm \
+// RUN:-emit-reduced-module-interface \
+// RUN:-fmodule-file=m_template=m_template.pcm \
+// RUN:-o m_wrapper.pcm
+//
+// RUN: %clang_cc1 -std=c++20 -I. main.cpp \
+// RUN:-fmodule-file=m_template=m_template.pcm \
+// RUN:-fmodule-file=m_wrapper=m_wrapper.pcm \
+// RUN:-fsyntax-only -verify
+
+//--- template_class.h
+#pragma once
+
+class TemplateClass {
+ public:
+ template
+ auto f1(U u);
+};
+
+template
+auto TemplateClass::f1(U u) {
+ U z = u;
+ return [=](auto) {
+(void)z;
+ };
+}
+
+template
+auto trigger_ast_deserialization(U u) {
+ return []() {};
+}
+
+//--- m_template.cppm
+module;
+#include "template_class.h"
+export module m_template;
+export using ::TemplateClass;
+export using ::trigger_ast_deserialization;
+
+//--- m_wrapper.cppm
+module;
+#include "template_class.h"
+export module m_wrapper;
+import m_template;
+
+export inline void TriggerInstantiation() {
+ TemplateClass tc;
+ tc.f1(0);
+ trigger_ast_deserialization(0);
+}
+
+//--- main.cpp
+// expected-no-diagnostics
+import m_template;
+import m_wrapper;
+#include "template_class.h"
+
+struct Token {};
+
+int main() {
+ TriggerInstantiation();
+ trigger_ast_deserialization(Token{});
+ TemplateClass tc;
+ tc.f1(Token{});
+}
>From 24c9297bc2db4cd0b8af7982c048b11c603d6e02 Mon Sep 17 00:00:00 2001
From: Ivo Popov
Date: Sat, 27 Jun 2026 18:47:23 -0400
Subject: [PATCH 2/3] Generalize the change, responding to review comments.
---
clang/lib/Serialization/ASTCommon.cpp | 11 ++-
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/clang/lib/Serialization/ASTCommon.cpp
b/clang/lib/Serialization/ASTCommon.cpp
index e3df196242919..9b454428457e2 100644
--- a/clang/lib/Serialization/ASTCommon.cpp
+++ b/clang/lib/Serialization/ASTCommon.cpp
@@ -493,14 +493,15 @@ bool serialization::needsAnonymousDeclarationNumber(const
NamedDecl *D) {
// At block scope, we number everything that we need to deduplicate, since we
// can't just use name matching to keep things lined up.
- // FIXME: This is only necessary for an inline function or a template or
- // similar.
+ // FIXME: This is only necessary for an inline function or
+ // a template specialization or similar.
if (D->getLexicalDeclContext()->isFunctionOrMethod()) {
+// An uninstantiated template pattern is never emitted; only its
+// instantiations are numbered, so its decls
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
zygoloid wrote: It makes sense that the lambda and the enclosing function would both be imported, and we'd pick definitions from different TUs. Looking at the ASTReader code now, I don't see any real protection against that happening, and it would definitely lead to a broken AST. In terms of how to fix this, perhaps: * When we try to import a closure type, if the context declaration is a function, then import a definition of that function. In this case, import *only* a declaration of the closure type. * When we import a lambda expression, import the definition of the closure type from the same source. * When we import a definition of a member of a closure type, use the definition from the same source that we used to import the definition of the closure type itself, rather than another declaration it's been merged with. That way, the definition of the closure type call operator and closure type captures that we import should come from the same source as the enclosing function, and hence should have the same declarations for local variables. In terms of how to do the above in practice, I'm not sure; I've not worked on the ASTReader code in quite a long time. But I think it should all be doable. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -3454,9 +3454,21 @@ uint64_t
ASTWriter::WriteDeclContextLexicalBlock(ASTContext &Context,
if (DC->decls_empty())
return 0;
- // In reduced BMI, we don't care the declarations in functions.
- if (GeneratingReducedBMI && DC->isFunctionOrMethod())
-return 0;
+ // In reduced BMI, we don't care the declarations in functions, unless they
+ // are templated functions, because their bodies may contain nested
class/lambda
+ // definitions that are canonicalized and need mapping of local declarations.
+ if (GeneratingReducedBMI && DC->isFunctionOrMethod()) {
ChuanqiXu9 wrote:
We have to understand **why** we need these function local declarations in
lexical lookup table to continue.
I don't feel bad with AI. But the contributor need to understand what their
patches did. I think this matches LLVM's guidelines.
https://github.com/llvm/llvm-project/pull/202248
___
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ChuanqiXu9 requested changes to this pull request. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -3454,9 +3454,21 @@ uint64_t
ASTWriter::WriteDeclContextLexicalBlock(ASTContext &Context,
if (DC->decls_empty())
return 0;
- // In reduced BMI, we don't care the declarations in functions.
- if (GeneratingReducedBMI && DC->isFunctionOrMethod())
-return 0;
+ // In reduced BMI, we don't care the declarations in functions, unless they
+ // are templated functions, because their bodies may contain nested
class/lambda
+ // definitions that are canonicalized and need mapping of local declarations.
+ if (GeneratingReducedBMI && DC->isFunctionOrMethod()) {
ChuanqiXu9 wrote:
It is possible to mix clang headers and named modules. My question is to
understand the underlying problem. The code snippet is to avoid something to
occur in the lexical lookup table. And my question is why we can't filter
declarations in lexical lookup table.
If we can't understand this, we are not going to fix the real problem.
https://github.com/llvm/llvm-project/pull/202248
___
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -3454,9 +3454,21 @@ uint64_t ASTWriter::WriteDeclContextLexicalBlock(ASTContext &Context, if (DC->decls_empty()) return 0; - // In reduced BMI, we don't care the declarations in functions. - if (GeneratingReducedBMI && DC->isFunctionOrMethod()) -return 0; + // In reduced BMI, we don't care the declarations in functions, unless they + // are templated functions, because their bodies may contain nested class/lambda ipopov wrote: I've tried to reduce this to the cases that seem to matter to Sema. Does this seem like less overhead? An alternative idea is to avoid fixing this bug for C++20 modules, but only do it for header-modules, in this PR; revert changes to ASTWriter.cpp here; and move them to another pull request. LMK what you would prefer! https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -0,0 +1,95 @@ +// RUN: rm -rf %t ipopov wrote: Done. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ipopov updated
https://github.com/llvm/llvm-project/pull/202248
>From b2b8d4fae2b3674a04db48afe4daaf81b61975ea Mon Sep 17 00:00:00 2001
From: Ivo Popov
Date: Mon, 8 Jun 2026 02:49:00 +
Subject: [PATCH 1/5] [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>
+ CanonicalLocalDecls;
+
+ const Decl *getCanonicalLocalDecl(const Decl *D);
+
typedef llvm::DenseMap>
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(D)) {
if (const FunctionDecl *FD = dyn_cast(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(D) || isa(D)) {
+return SemaRef.getCanonicalLocalDecl(D);
+ }
return D;
}
llvm::PointerUnion *
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 &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(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 &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(D)) {
+return VD->isLocalVarDeclOrParm() && !isa(VD);
+ }
+ return isa(D);
+}
+
+const Decl *Sema::getCanonicalLocalDecl(const Decl *D) {
+ if (isa(D)) {
+return D;
+ }
+
+ const auto *VD = dyn_cast(D);
+ const auto *BD = dyn_cast(D);
+ if (!VD && !BD) {
+return D;
+ }
+
+ if (VD && !VD->isLocalVarDeclOrParm()) {
+return D;
+ }
+
+ const DeclContext *DC = VD ? VD->g
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -3454,9 +3454,21 @@ uint64_t ASTWriter::WriteDeclContextLexicalBlock(ASTContext &Context, if (DC->decls_empty()) return 0; - // In reduced BMI, we don't care the declarations in functions. - if (GeneratingReducedBMI && DC->isFunctionOrMethod()) -return 0; + // In reduced BMI, we don't care the declarations in functions, unless they + // are templated functions, because their bodies may contain nested class/lambda ChuanqiXu9 wrote: I am not satisfying with this... this is too broad. Maybe we need to figure out in such cases what declarations is needed for the lexical lookup. I don't feel the fix here is correct. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -0,0 +1,95 @@ +// RUN: rm -rf %t ChuanqiXu9 wrote: Not a formal policy, but we want such test for C++20 modules to end with .cppm instead of .cpp. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ipopov edited https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -3454,9 +3454,21 @@ uint64_t
ASTWriter::WriteDeclContextLexicalBlock(ASTContext &Context,
if (DC->decls_empty())
return 0;
- // In reduced BMI, we don't care the declarations in functions.
- if (GeneratingReducedBMI && DC->isFunctionOrMethod())
-return 0;
+ // In reduced BMI, we don't care the declarations in functions, unless they
+ // are templated functions, because their bodies may contain nested
class/lambda
+ // definitions that are canonicalized and need mapping of local declarations.
+ if (GeneratingReducedBMI && DC->isFunctionOrMethod()) {
ipopov wrote:
You're right about the existing test - sorry... (as Reduced BMI is C++20
modules-specific.)
But I think that the underlying deserialization bug will recur when templates
containing generic lambdas are defined in C++20 named modules (compiled with
Reduced BMI) and then imported or mixed with header modules.
I added a separate C++20 named modules test
(`modules-generic-lambda-local-mapping-bmi.cpp`) that compiles using
`-emit-reduced-module-interface`. It crashes on the assertion without the
`ASTWriter` fix, and passes with it.
LMK if this kind of mixing of C++20 modules with Clang header modules is
unrealistic. I figured since it can trigger an alert, it's worth fixing.
Thanks for having a look!
https://github.com/llvm/llvm-project/pull/202248
___
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ipopov updated
https://github.com/llvm/llvm-project/pull/202248
>From b2b8d4fae2b3674a04db48afe4daaf81b61975ea Mon Sep 17 00:00:00 2001
From: Ivo Popov
Date: Mon, 8 Jun 2026 02:49:00 +
Subject: [PATCH 1/4] [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>
+ CanonicalLocalDecls;
+
+ const Decl *getCanonicalLocalDecl(const Decl *D);
+
typedef llvm::DenseMap>
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(D)) {
if (const FunctionDecl *FD = dyn_cast(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(D) || isa(D)) {
+return SemaRef.getCanonicalLocalDecl(D);
+ }
return D;
}
llvm::PointerUnion *
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 &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(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 &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(D)) {
+return VD->isLocalVarDeclOrParm() && !isa(VD);
+ }
+ return isa(D);
+}
+
+const Decl *Sema::getCanonicalLocalDecl(const Decl *D) {
+ if (isa(D)) {
+return D;
+ }
+
+ const auto *VD = dyn_cast(D);
+ const auto *BD = dyn_cast(D);
+ if (!VD && !BD) {
+return D;
+ }
+
+ if (VD && !VD->isLocalVarDeclOrParm()) {
+return D;
+ }
+
+ const DeclContext *DC = VD ? VD->g
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ChuanqiXu9 commented: Haven't looked into the details. But the description is not correct. The attached test is not C++20 modules. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
ipopov wrote: > BTW, it's nicer to not to force-push PR branches, and instead to just keep > adding new commits on top with each additional change. That way it's easier, > in the github UI, to see what the changes have been since last review. Sorry about that! I have never used Git or Github for pull requests like this before. Ugh. Let me try to untangle. I will do one more force-push, back to the original version, then; and from there on I will push deltas. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
jyknight wrote: > @ipopov ipopov force-pushed the fix-modules-generic-lambda-capture branch > from 23489db to 2672ae5 BTW, it's nicer to not to force-push PR branches, and instead to just keep adding new commits on top with each additional change. That way it's easier, in the github UI, to see what the changes have been since last review. https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -7421,3 +7421,67 @@ void Sema::PerformDependentDiagnostics(const DeclContext
*Pattern,
}
}
}
+
+static bool isMappedLocalDecl(const Decl *D) {
+ if (const auto *VD = dyn_cast(D)) {
+return VD->isLocalVarDeclOrParm() && !isa(VD);
+ }
+ return isa(D);
+}
+
+const Decl *Sema::getCanonicalLocalDecl(const Decl *D) {
+ if (isa(D)) {
+return D;
+ }
+
+ const auto *VD = dyn_cast(D);
+ const auto *BD = dyn_cast(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(DC);
+ if (!FD) {
+return D;
+ }
+
+ const auto *CanonFD = FD->getCanonicalDecl();
jyknight wrote:
We need the canonical _definition_, not just a decl, in order to be able to
iterate `decls()` on it. Probably `FD->getDefinition()`? I think that's
guaranteed to use return a canonical definition.
That'll end up with this using a different decl than the ParamVar mapping,
which uses the `getCanonicalDecl`. But maybe that's fine?
https://github.com/llvm/llvm-project/pull/202248
___
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/jyknight commented: FTR, running the provided test with current clang results in the following assertion error: ``` clang: clang/lib/Sema/SemaTemplateInstantiate.cpp:4772: llvm::PointerUnion *clang::LocalInstantiationScope::findInstantiationOf(const Decl *): Assertion `isa(D) && "declaration not instantiated in this scope"' failed. ``` I'm not really knowledgeable enough about Clang AST to appropriately review this change, but I commented on one problem in it. I also note that this depends on the imported functions always having exactly the same local decls within the function body, so that mapping by index can work. I _think_ that is correct to assume, since we do ODR hashing of the ASTs on imports and raise an error if they mismatch. But seemed worth pointing out explicitly, in case there's some edge case where that isn't true I don't know about. (And the existing mapping only happens on params, which I think are _more_ guaranteed-equal.) https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
@@ -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
+T&& move(T& t) noexcept { return static_cast(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
+struct Consumer {
+ template
+ void operator()(const U& x) const {}
+};
+
+template
+struct Holder {
+ F f;
+ explicit Holder(F f) : f(std::move(f)) {}
+ void call() const { f(0); }
+};
+
+template
+auto make_holder(F f) {
+ return Holder(std::move(f));
+}
+
+template
+class TemplateClass {
+ public:
+ template
+ explicit TemplateClass(Args&&... args) {
jyknight wrote:
Replacing this template fn with a decl, `explicit TemplateClass(Args&&...
args);` and moving the definition out-of-line below the class,
```
template
template
TemplateClass::TemplateClass(Args&&... args) {
[...snip...]
}
```
demonstrates the issue mentioned above.
https://github.com/llvm/llvm-project/pull/202248
___
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/jyknight edited https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
https://github.com/ipopov edited https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
llvmorg-github-actions[bot] wrote:
@llvm/pr-subscribers-clang-modules
Author: Ivo Popov (ipopov)
Changes
### 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.
---
Full diff: https://github.com/llvm/llvm-project/pull/202248.diff
5 Files Affected:
- (modified) clang/include/clang/Sema/Sema.h (+8)
- (modified) clang/lib/Sema/SemaTemplateInstantiate.cpp (+9-12)
- (modified) clang/lib/Sema/SemaTemplateInstantiateDecl.cpp (+64)
- (modified) clang/lib/Serialization/ASTWriter.cpp (+8-1)
- (added) clang/test/Modules/modules-generic-lambda-local-capture.cpp (+137)
``diff
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>
+ CanonicalLocalDecls;
+
+ const Decl *getCanonicalLocalDecl(const Decl *D);
+
typedef llvm::DenseMap>
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(D)) {
if (const FunctionDecl *FD = dyn_cast(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(D) || isa(D)) {
+return SemaRef.getCanonicalLocalDecl(D);
+ }
return D;
}
llvm::PointerUnion *
LocalInstantiationScope::getInstantiationOfIfExists(const Decl *D) {
- D = getCanonicalParmVarDecl(D);
+ D = getCanonicalLocalDecl(D, SemaRef);
for (LocalInstantiationScope *Current = this; Current;
Current = Current->Outer) {
@@ -448
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
github-actions[bot] wrote: Hello @ipopov :wave: Thank you for submitting a Pull Request (PR) to the LLVM Project. Since this is your first PR, here are a few useful links covering our main contribution policies and review practices. * All contributions to LLVM must follow our [LLVM AI Tool Use Policy](https://llvm.org/docs/AIToolPolicy.html). In particular, if you used AI while working on this PR, remember to add a note to the PR description. * The [LLVM Code-Review Policy and Practices](https://llvm.org/docs/CodeReview.html) document contains practical information about the PR process, including how patches are reviewed and accepted, and who can review a PR. * Our [LLVM Developer Policy](https://llvm.org/docs/DeveloperPolicy.html) describes our expectations for code quality, commit summaries and contains notes on our CI system. Please reply to this message to confirm that you have read these policies, especially the LLVM AI Tool Use Policy, and that any AI tool usage has been noted in the PR description. --- ### Frequently asked questions **How do I add reviewers?** This PR will be automatically labeled, and the relevant teams will be notified. For some parts of the project, reviewers may also be added automatically. You can also add reviewers manually using the **Reviewers** section on this page. If you cannot use that section, it is probably because you do not have write permissions for the repository. In that case, you can request a review by tagging reviewers in a comment using `@` followed by their GitHub username. **What if there are no comments?** If you have not received any comments on your PR after a week, you can request a review by pinging the PR with a comment such as “Ping”. The common courtesy ping rate is once a week. Please remember that you are asking for volunteer time from other developers. **Are any special GitHub settings required to contribute to LLVM?** We only require contributors to have a public email address associated with their GitHub commits, see this [section](https://llvm.org/docs/DeveloperPolicy.html#email-addresses) of LLVM Developer Policy for details. --- If you have questions, feel free to leave a comment on this PR, or ask on [LLVM Discord](https://discord.com/invite/xS7Z362) or [LLVM Discourse](https://discourse.llvm.org/). Thank you, The LLVM Community https://github.com/llvm/llvm-project/pull/202248 ___ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
[clang] [Clang][Modules] Fix generic lambda local capture mapping mismatch in template instantiation (PR #202248)
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
Date: Mon, 8 Jun 2026 02:49:00 +
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>
+ CanonicalLocalDecls;
+
+ const Decl *getCanonicalLocalDecl(const Decl *D);
+
typedef llvm::DenseMap>
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(D)) {
if (const FunctionDecl *FD = dyn_cast(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(D) || isa(D)) {
+return SemaRef.getCanonicalLocalDecl(D);
+ }
return D;
}
llvm::PointerUnion *
LocalInstantiationScope::g
