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

Reply via email to