Author: /bin/cat Date: 2026-10-29T09:03:32Z New Revision: dee6af34fe3f9c815273fd02664c1823d44c50bc
URL: https://github.com/llvm/llvm-project/commit/dee6af34fe3f9c815273fd02664c1823d44c50bc DIFF: https://github.com/llvm/llvm-project/commit/dee6af34fe3f9c815273fd02664c1823d44c50bc.diff LOG: [clang][modules] Allow merging module-local class members across modules (#236509) When the same variable template specialization is instantiated in two module units and a TU imports both, clang merges the lambda closure types (closure types are merged by identity), but not their `operator()` members: the instantiated `operator()` is module-local to the instantiating module, and `ASTContext::isSameEntity` refuses to merge module-local declarations from different modules. The merged closure type then has two `operator()` declarations that are not redeclarations of each other, which is diagnosed as a false ODR violation during deserialization. The change allows merging class members across modules even when they are marked module-local, provided that the (already checked) shared declaration context is a class: a class definition determines its members, so two members of the same class are the same entity. Two *distinct* module-local classes are still not merged — the module-local rule still applies to the classes themselves — so their members are never compared, which is what that check exists for. The regression test (`clang/test/Modules/merge-lambda-call-operator.cppm`) covers both reduced and full module interfaces with `expected-no-diagnostics`. Without the change both variants fail with the unexpected ODR diagnostics above; with it they pass. The test also keeps the `static_assert(__is_same(decltype(Trigger1()), decltype(Trigger2())))` from the issue, which states the expected semantics (the two closure types are the same type). This is the patch originally proposed by @ipopov in #206382. Fixes #206382. Co-authored-by: Ivo Popov <[email protected]> Assisted-by: Deepseek-V4.1-flash-1017 Added: clang/test/Modules/merge-lambda-call-operator.cppm Modified: clang/lib/AST/ASTContext.cpp Removed: ################################################################################ diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp index 8037085e6d5854..133227f677025f 100644 --- a/clang/lib/AST/ASTContext.cpp +++ b/clang/lib/AST/ASTContext.cpp @@ -7761,10 +7761,18 @@ bool ASTContext::isSameEntity(const NamedDecl *X, const NamedDecl *Y) const { return false; // If either X or Y are local to the owning module, they are only possible to - // be the same entity if they are in the same module. - if (X->isModuleLocal() || Y->isModuleLocal()) - if (!isInSameModule(X->getOwningModule(), Y->getOwningModule())) + // be the same entity if they are in the same module, or if they are members + // of a lambda closure type. Closure types are merged by identity, and a class + // definition determines its members, so the members of a merged closure type + // are the same entity even if they are module-local to diff erent modules. + // Two distinct closure types are still not merged, so their members are never + // compared. + if (X->isModuleLocal() || Y->isModuleLocal()) { + auto *RD = dyn_cast<CXXRecordDecl>(X->getDeclContext()); + if (!isInSameModule(X->getOwningModule(), Y->getOwningModule()) && + !(RD && RD->isLambda())) return false; + } // Two typedefs refer to the same entity if they have the same underlying // type. diff --git a/clang/test/Modules/merge-lambda-call-operator.cppm b/clang/test/Modules/merge-lambda-call-operator.cppm new file mode 100644 index 00000000000000..feeee93634a0b4 --- /dev/null +++ b/clang/test/Modules/merge-lambda-call-operator.cppm @@ -0,0 +1,51 @@ +// Test that a lambda closure type instantiated from a variable template in two +// modules is merged together with its members. The closure types are merged by +// identity, but the instantiated call operator is module-local to each +// instantiating module and used to be rejected by ASTContext::isSameEntity. +// That left the merged class with two distinct 'operator()' declarations, +// which was diagnosed as a false ODR violation. + +// RUN: rm -rf %t +// RUN: mkdir -p %t +// RUN: split-file %s %t +// +// RUN: %clang_cc1 -std=c++20 -I%t %t/m_a.cppm -emit-reduced-module-interface -o %t/m_a.pcm +// RUN: %clang_cc1 -std=c++20 -I%t %t/m_b.cppm -emit-reduced-module-interface -o %t/m_b.pcm +// RUN: %clang_cc1 -std=c++20 -I%t %t/main.cpp -fmodule-file=m_a=%t/m_a.pcm \ +// RUN: -fmodule-file=m_b=%t/m_b.pcm -fsyntax-only -verify +// +// Full module interfaces. +// RUN: %clang_cc1 -std=c++20 -I%t %t/m_a.cppm -emit-module-interface -o %t/m_a_full.pcm +// RUN: %clang_cc1 -std=c++20 -I%t %t/m_b.cppm -emit-module-interface -o %t/m_b_full.pcm +// RUN: %clang_cc1 -std=c++20 -I%t %t/main.cpp -fmodule-file=m_a=%t/m_a_full.pcm \ +// RUN: -fmodule-file=m_b=%t/m_b_full.pcm -fsyntax-only -verify + +//--- template.h +#pragma once +template <typename T> +inline auto var = [](T x) { return x; }; + +//--- m_a.cppm +module; +#include "template.h" +export module m_a; +export auto Trigger1() { + return var<int>; +} + +//--- m_b.cppm +module; +#include "template.h" +export module m_b; +export auto Trigger2() { + return var<int>; +} + +//--- main.cpp +// expected-no-diagnostics +import m_a; +import m_b; + +void use() { + static_assert(__is_same(decltype(Trigger1()), decltype(Trigger2()))); +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
