[clang] [C++20] [Modules] Merge lambda where the merge definition data is merged before the primary one (PR #218574)
Chuanqi Xu via cfe-commits
cfe-commits at lists.llvm.org
Mon Aug 24 19:45:57 PDT 2026
https://github.com/ChuanqiXu9 created https://github.com/llvm/llvm-project/pull/218574
Close https://github.com/llvm/llvm-project/issues/217858
The crash triggers on the assertion:
```C++
assert(!DD.IsLambda && !MergeDD.IsLambda && "faked up lambda definition?");
```
where DD.IsLambda is false and MergeDD.IsLambda is true. This is kinda surprising. But this turns out to be real for the example.
```C++
export template <class Callback> auto make_closure(Callback &cb) {
return [&cb](auto &arg) noexcept(noexcept(cb(arg))) { cb(arg); };
}
export template <class Callback, class Arg>
void for_each(Callback &&cb, Arg &arg)
noexcept(noexcept(make_closure(cb)(arg))) {}
export template <class It> struct iterator {
It cur;
void operator++() noexcept(noexcept(for_each([](auto &i) { ++i; },
cur))) {}
};
```
Here the chained use of lambda in noexcept inside the lambda makes the loading ordering pretty uncommon and odd. And it makes the first loading lambda definition doesn't get its definition data but the later loaded redeclaration get it.
As this falls into an assertion, the change itself is not expected to meet any regressions.
>From 9578297f12fbeb776734b491181001e384d29068 Mon Sep 17 00:00:00 2001
From: Chuanqi Xu <chuanqi.xcq at alibaba-inc.com>
Date: Tue, 25 Aug 2026 10:36:56 +0800
Subject: [PATCH] [C++20] [Modules] Merge lambda where the merge definition
data is merged before the primary one
Close https://github.com/llvm/llvm-project/issues/217858
The crash triggers on the assertion:
```C++
assert(!DD.IsLambda && !MergeDD.IsLambda && "faked up lambda definition?");
```
where DD.IsLambda is false and MergeDD.IsLambda is true. This is kinda
surprising. But this turns out to be real for the example.
```C++
export template <class Callback> auto make_closure(Callback &cb) {
return [&cb](auto &arg) noexcept(noexcept(cb(arg))) { cb(arg); };
}
export template <class Callback, class Arg>
void for_each(Callback &&cb, Arg &arg)
noexcept(noexcept(make_closure(cb)(arg))) {}
export template <class It> struct iterator {
It cur;
void operator++() noexcept(noexcept(for_each([](auto &i) { ++i; },
cur))) {}
};
```
Here the chained use of lambda in noexcept inside the lambda makes the
loading ordering pretty uncommon and odd. And it makes the first loading
lambda definition doesn't get its definition data but the later loaded
redeclaration get it.
As this falls into an assertion, the change itself is not expected to
meet any regressions.
---
clang/lib/Serialization/ASTReaderDecl.cpp | 22 ++++++++-
clang/test/Modules/pr217858.cppm | 56 +++++++++++++++++++++++
2 files changed, 77 insertions(+), 1 deletion(-)
create mode 100644 clang/test/Modules/pr217858.cppm
diff --git a/clang/lib/Serialization/ASTReaderDecl.cpp b/clang/lib/Serialization/ASTReaderDecl.cpp
index e973b7ae71954..74e0106520011 100644
--- a/clang/lib/Serialization/ASTReaderDecl.cpp
+++ b/clang/lib/Serialization/ASTReaderDecl.cpp
@@ -2103,7 +2103,27 @@ void ASTDeclMerger::MergeDefinitionData(
PFDI->second == ASTReader::PendingFakeDefinitionKind::Fake) {
// We faked up this definition data because we found a class for which we'd
// not yet loaded the definition. Replace it with the real thing now.
- assert(!DD.IsLambda && !MergeDD.IsLambda && "faked up lambda definition?");
+ assert(!DD.IsLambda && "faked up lambda definition?");
+
+ // This is possible for some special loading ordering. See
+ // clang/test/Modules/pr217858.cppm for an example.
+ //
+ // LambdaDefinitionData is larger than DefinitionData, so it cannot replace
+ // the fake DefinitionData object in place.
+ if (MergeDD.IsLambda) {
+ auto *Def = DD.Definition;
+ MergeDD.Definition = Def;
+ // Unlike an instantiated class definition,
+ // whose update-record reader removes the fake entry after loading its
+ // lexical declarations, a lambda's definition is part of its declaration
+ // record and is fully loaded here.
+ Reader.PendingFakeDefinitionData.erase(PFDI);
+ for (auto *R = Reader.getMostRecentExistingDecl(Def); R;
+ R = R->getPreviousDecl())
+ cast<CXXRecordDecl>(R)->DefinitionData = &MergeDD;
+ return;
+ }
+
PFDI->second = ASTReader::PendingFakeDefinitionKind::FakeLoaded;
// Don't change which declaration is the definition; that is required
diff --git a/clang/test/Modules/pr217858.cppm b/clang/test/Modules/pr217858.cppm
new file mode 100644
index 0000000000000..040518bfec8ef
--- /dev/null
+++ b/clang/test/Modules/pr217858.cppm
@@ -0,0 +1,56 @@
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+//
+// RUN: %clang_cc1 -std=c++23 -triple %itanium_abi_triple \
+// RUN: -emit-module-interface %t/lib.cppm -o %t/Lib.pcm
+// RUN: %clang_cc1 -std=c++23 -triple %itanium_abi_triple \
+// RUN: -emit-module-interface %t/mod.cppm -o %t/Mod.pcm \
+// RUN: -fmodule-file=Lib=%t/Lib.pcm
+// RUN: %clang_cc1 -std=c++23 -triple %itanium_abi_triple -emit-obj \
+// RUN: %t/main.cpp -o %t/main.o -fmodule-file=Lib=%t/Lib.pcm \
+// RUN: -fmodule-file=Mod=%t/Mod.pcm
+
+// Test again with reduced BMI.
+// RUN: %clang_cc1 -std=c++23 -triple %itanium_abi_triple \
+// RUN: -emit-reduced-module-interface %t/lib.cppm -o %t/Lib.pcm
+// RUN: %clang_cc1 -std=c++23 -triple %itanium_abi_triple \
+// RUN: -emit-reduced-module-interface %t/mod.cppm -o %t/Mod.pcm \
+// RUN: -fmodule-file=Lib=%t/Lib.pcm
+// RUN: %clang_cc1 -std=c++23 -triple %itanium_abi_triple -emit-obj \
+// RUN: %t/main.cpp -o %t/main.o -fmodule-file=Lib=%t/Lib.pcm \
+// RUN: -fmodule-file=Mod=%t/Mod.pcm
+
+//--- lib.cppm
+export module Lib;
+
+export template <class Callback> auto make_closure(Callback &callback) {
+ return [&callback](auto &arg) noexcept(noexcept(callback(arg))) {
+ callback(arg);
+ };
+}
+
+export template <class Callback, class Arg>
+void for_each(Callback &&callback, Arg &arg)
+ noexcept(noexcept(make_closure(callback)(arg))) {}
+
+export template <class It> struct iterator {
+ It current;
+ void operator++()
+ noexcept(noexcept(for_each([](auto &it) { ++it; }, current))) {}
+};
+
+//--- mod.cppm
+export module Mod;
+import Lib;
+
+export inline int test() {
+ int data[4]{};
+ iterator<int *> it{data};
+ ++it;
+ return static_cast<int>(it.current - data);
+}
+
+//--- main.cpp
+import Mod;
+int main() { return test(); }
More information about the cfe-commits
mailing list