[clang] [clang][MS] Keep default-argument lambdas of function templates apart from the function's own lambdas (PR #232294)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Oct 15 05:11:23 PDT 2026
https://github.com/Nemi26 created https://github.com/llvm/llvm-project/pull/232294
Under the Microsoft ABI, a lambda in a default argument of a non-member function template can get the same mangled name as a lambda in the function's body. When that happens, calls to one call operator go to the other. It's a silent miscompile:
```cpp
template <typename F> struct Lazy { F f; int get() { int v = 0; f(v); return v; } };
template <typename B> int shoot(B &&b) { Lazy<B &> l{b}; return l.get(); }
template <int N, typename E = void (*)(int &)>
int track(E &&e = [](int &) {}) {
return shoot([&](int &v) { v = N; e(v); });
}
int main() { return track<1>() * 10 + track<2>(); } // 12; 10 for x86_64-pc-windows-msvc
```
The default argument's lambda has no mangling number and no context declaration, because `getCurrentMangleNumberContext` leaves default arguments of non-member function templates unnumbered. Its instantiation still lives in the function. The Microsoft mangler then names it `<lambda_N>`, taking N from the translation unit's count of unnumbered lambdas, which starts at 0. The function's own lambdas are numbered from 1 in the same scope. In `track<2>`, both closure types become `<lambda_1>@?0???$track@$01...`. Itanium is unaffected, because it spells unnumbered lambdas `$_N`.
This patch finds the parameter whose default argument spans such a lambda and mangles the lambda in the default-argument form `<lambda_K_N>`, which lambdas numbered in a default argument (class members) already use. The fix stays in the mangler. Numbering these lambdas in Sema would also change Itanium's mangling.
Tested with the new `mangle-ms-lambda-template-default-arg.cpp`, which fails without the patch. `clang/test/CodeGenCXX` and `clang/test/DebugInfo/CXX` pass, except 8 tests that need tools my build doesn't have (`llvm-dis`, `llvm-link`, `llvm-profdata`).
This patch was written with the assistance of an AI tool (Claude Code, Claude Opus); I reviewed it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
>From 7e2e949894c395bde268813e862bf20091261e58 Mon Sep 17 00:00:00 2001
From: Nemi26 <nemi.verploegh at icloud.com>
Date: Thu, 15 Oct 2026 14:10:44 +0200
Subject: [PATCH] [clang][MS] Keep default-argument lambdas of function
templates apart from the function's own lambdas
A lambda in a default argument of a non-member function template gets no
mangling number and no context declaration, but its instantiation lives in
the function. The Microsoft mangler named it <lambda_N> from the
translation unit's count of unnumbered lambdas, which starts at 0, while the
function's own lambdas are numbered from 1 in the same scope. When the two
numbers met, both closure types got the same mangled name, and calls to
one call operator went to the other, a silent miscompile.
Find the parameter whose default argument spans such a lambda and mangle it
in the default-argument form <lambda_K_N>, as lambdas numbered in a default
argument already are.
This patch was written with the assistance of an AI tool (Claude Code,
Claude Opus); the submitter reviewed it.
Assisted-by: Claude Code (Claude Opus)
---
clang/lib/AST/MicrosoftMangle.cpp | 29 ++++++++++++++++---
.../mangle-ms-lambda-template-default-arg.cpp | 18 ++++++++++++
2 files changed, 43 insertions(+), 4 deletions(-)
create mode 100644 clang/test/CodeGenCXX/mangle-ms-lambda-template-default-arg.cpp
diff --git a/clang/lib/AST/MicrosoftMangle.cpp b/clang/lib/AST/MicrosoftMangle.cpp
index 74edee531e48a0..5e9b8f26d365e0 100644
--- a/clang/lib/AST/MicrosoftMangle.cpp
+++ b/clang/lib/AST/MicrosoftMangle.cpp
@@ -86,6 +86,29 @@ struct msvc_hashing_ostream : public llvm::raw_svector_ostream {
}
};
+/// The parameter whose default argument holds the lambda, if any. A lambda in a
+/// default argument of a non-member function template has neither a mangling
+/// number nor a context declaration, yet its instantiation lives in the
+/// function, whose own lambdas it must not share a name with.
+static const ParmVarDecl *getLambdaDefaultArgParm(const CXXRecordDecl *Lambda) {
+ if (const auto *Parm =
+ dyn_cast_or_null<ParmVarDecl>(Lambda->getLambdaContextDecl()))
+ return Parm;
+ if (Lambda->getLambdaManglingNumber())
+ return nullptr;
+ const auto *Func = dyn_cast<FunctionDecl>(Lambda->getDeclContext());
+ if (!Func)
+ return nullptr;
+ const SourceManager &SM = Lambda->getASTContext().getSourceManager();
+ for (const ParmVarDecl *Parm : Func->parameters()) {
+ SourceRange Range = Parm->getDefaultArgRange();
+ if (Range.isValid() && SM.isPointWithin(Lambda->getLocation(),
+ Range.getBegin(), Range.getEnd()))
+ return Parm;
+ }
+ return nullptr;
+}
+
static const DeclContext *
getLambdaDefaultArgumentDeclContext(const Decl *D) {
if (const auto *RD = dyn_cast<CXXRecordDecl>(D))
@@ -250,10 +273,9 @@ class MicrosoftMangleContextImpl : public MicrosoftMangleContext {
assert(Lambda->isLambda() && "RD must be a lambda!");
std::string Name("<lambda_");
- Decl *LambdaContextDecl = Lambda->getLambdaContextDecl();
unsigned LambdaManglingNumber = Lambda->getLambdaManglingNumber();
unsigned LambdaId;
- const ParmVarDecl *Parm = dyn_cast_or_null<ParmVarDecl>(LambdaContextDecl);
+ const ParmVarDecl *Parm = getLambdaDefaultArgParm(Lambda);
const FunctionDecl *Func =
Parm ? dyn_cast<FunctionDecl>(Parm->getDeclContext()) : nullptr;
@@ -1245,8 +1267,7 @@ void MicrosoftCXXNameMangler::mangleUnqualifiedName(GlobalDecl GD,
Decl *LambdaContextDecl = Record->getLambdaContextDecl();
unsigned LambdaManglingNumber = Record->getLambdaManglingNumber();
unsigned LambdaId;
- const ParmVarDecl *Parm =
- dyn_cast_or_null<ParmVarDecl>(LambdaContextDecl);
+ const ParmVarDecl *Parm = getLambdaDefaultArgParm(Record);
const FunctionDecl *Func =
Parm ? dyn_cast<FunctionDecl>(Parm->getDeclContext()) : nullptr;
diff --git a/clang/test/CodeGenCXX/mangle-ms-lambda-template-default-arg.cpp b/clang/test/CodeGenCXX/mangle-ms-lambda-template-default-arg.cpp
new file mode 100644
index 00000000000000..7f02abd8b34f03
--- /dev/null
+++ b/clang/test/CodeGenCXX/mangle-ms-lambda-template-default-arg.cpp
@@ -0,0 +1,18 @@
+// RUN: %clang_cc1 -std=c++20 -emit-llvm %s -o - -triple=x86_64-pc-windows-msvc | FileCheck %s
+
+// A lambda in a default argument of a function template has no mangling
+// number, but its instantiation lives in the function. It must not take the
+// name of the function's own lambdas, else both call one call operator.
+
+template <typename F> int call(F f) { return f(); }
+
+template <int N> int g(int (*f)() = [] { return 10; }) {
+ return f() + call([] { return N; });
+}
+
+int use() { return g<1>() + g<2>(); }
+
+// CHECK-DAG: define {{.*}} @"??R<lambda_1>@?0???$g@$00@@YAHP6AHXZ at Z@QEBA?A?<auto>@@XZ"(
+// CHECK-DAG: define {{.*}} @"??R<lambda_1>@?0???$g@$01@@YAHP6AHXZ at Z@QEBA?A?<auto>@@XZ"(
+// CHECK-DAG: define internal {{.*}} @"??R<lambda_1_{{[0-9]+}}>@?0???$g@$00@@YAHP6AHXZ at Z@QEBA?A?<auto>@@XZ"(
+// CHECK-DAG: define internal {{.*}} @"??R<lambda_1_{{[0-9]+}}>@?0???$g@$01@@YAHP6AHXZ at Z@QEBA?A?<auto>@@XZ"(
More information about the cfe-commits
mailing list