[clang] [clang] Don't use `VarDecl` of local variables as `ManglingContextDecl` for lambdas (PR #179035)

Jan Kokemüller via cfe-commits cfe-commits at lists.llvm.org
Sat Feb 14 01:33:42 PST 2026


https://github.com/jiixyj updated https://github.com/llvm/llvm-project/pull/179035

>From 72d814fb6d05ae529538d5b673277e6c065bbbb5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sat, 31 Jan 2026 14:26:59 +0100
Subject: [PATCH 01/18] Don't use VarDecl of local variables as
 ManglingContextDecl for lambdas

---
 clang/lib/Sema/SemaLambda.cpp    |  8 ++++++++
 clang/test/Modules/pr178893.cppm | 29 +++++++++++++++++++++++++++++
 2 files changed, 37 insertions(+)
 create mode 100644 clang/test/Modules/pr178893.cppm

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index e74fe02bd0cf5..24426259bf01c 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -294,6 +294,14 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   bool IsInNonspecializedTemplate =
       inTemplateInstantiation() || CurContext->isDependentContext();
 
+  // If we must allocate mangling numbers but the `ManglingContextDecl`
+  // is a local variable, use the `DeclContext` containing the lambda expression
+  // instead.
+  if (ManglingContextDecl)
+    if (VarDecl *Var = dyn_cast<VarDecl>(ManglingContextDecl);
+        Var && Var->isLocalVarDecl())
+      ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
+
   // Default arguments of member function parameters that appear in a class
   // definition, as well as the initializers of data members, receive special
   // treatment. Identify them.
diff --git a/clang/test/Modules/pr178893.cppm b/clang/test/Modules/pr178893.cppm
new file mode 100644
index 0000000000000..6d2a599588667
--- /dev/null
+++ b/clang/test/Modules/pr178893.cppm
@@ -0,0 +1,29 @@
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-apple-macosx10.7.0 -fmodules -xc++ -emit-llvm -o - %s -w | FileCheck %s
+
+// CHECK-LABEL: define linkonce_odr noundef i32 @_ZZN8PR178893W3mod6format5parseEPiENKUlvE_clEv
+// CHECK-LABEL: define linkonce_odr noundef i32 @_ZZN8PR178893W3mod6format5parseEPiENKUlvE0_clEv
+
+export module mod;
+
+namespace PR178893 {
+  struct format {
+      static inline int parse(int* i)
+      {
+          int number;
+          number = [&]() -> int { return i[0]; }();
+
+          volatile bool b = true;
+          if (b) {
+              auto identifier = [&]() -> int { return i[1]; }();
+              return identifier;
+          }
+
+          return number;
+      }
+  };
+
+  int test_format() {
+      int n[2] = {1, 0};
+      return format::parse(n);
+  }
+}

>From d5dbb1738f1d2c90850d27ba5e3e7245e6f325ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sun, 1 Feb 2026 10:45:25 +0100
Subject: [PATCH 02/18] Use `dyn_cast_or_null` instead of null check plus
 `dyn_cast`

Co-authored-by: Corentin Jabot <corentinjabot at gmail.com>
---
 clang/lib/Sema/SemaLambda.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index 24426259bf01c..cdefd60508164 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -297,8 +297,7 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   // If we must allocate mangling numbers but the `ManglingContextDecl`
   // is a local variable, use the `DeclContext` containing the lambda expression
   // instead.
-  if (ManglingContextDecl)
-    if (VarDecl *Var = dyn_cast<VarDecl>(ManglingContextDecl);
+  if (VarDecl *Var = dyn_cast_or_null<VarDecl>(ManglingContextDecl);
         Var && Var->isLocalVarDecl())
       ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
 

>From 42bb683d0d44e008408f542b01e192a4d192e273 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sun, 1 Feb 2026 10:56:51 +0100
Subject: [PATCH 03/18] Autoformat

---
 clang/lib/Sema/SemaLambda.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index cdefd60508164..1066333722bd8 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -298,8 +298,8 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   // is a local variable, use the `DeclContext` containing the lambda expression
   // instead.
   if (VarDecl *Var = dyn_cast_or_null<VarDecl>(ManglingContextDecl);
-        Var && Var->isLocalVarDecl())
-      ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
+      Var && Var->isLocalVarDecl())
+    ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
 
   // Default arguments of member function parameters that appear in a class
   // definition, as well as the initializers of data members, receive special

>From 4be67cb8356bf53bfe074663dc47792cb66fecf5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Mon, 2 Feb 2026 13:10:49 +0100
Subject: [PATCH 04/18] Refactor so that fallback only applies to module case

---
 clang/lib/Sema/SemaLambda.cpp | 27 ++++++++++++++++-----------
 1 file changed, 16 insertions(+), 11 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index 1066333722bd8..f7fd1d1831500 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -294,13 +294,6 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   bool IsInNonspecializedTemplate =
       inTemplateInstantiation() || CurContext->isDependentContext();
 
-  // If we must allocate mangling numbers but the `ManglingContextDecl`
-  // is a local variable, use the `DeclContext` containing the lambda expression
-  // instead.
-  if (VarDecl *Var = dyn_cast_or_null<VarDecl>(ManglingContextDecl);
-      Var && Var->isLocalVarDecl())
-    ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
-
   // Default arguments of member function parameters that appear in a class
   // definition, as well as the initializers of data members, receive special
   // treatment. Identify them.
@@ -311,12 +304,24 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     //    Yeah, I think the only cases left where lambdas don't need a
     //    mangling are when they have (effectively) internal linkage or appear
     //    in a non-inline function in a non-module translation unit.
-    if (auto *ND = dyn_cast<NamedDecl>(ManglingContextDecl ? ManglingContextDecl
-                                                           : cast<Decl>(DC));
+
+    Decl *ManglingContextDeclForModule = [&]() {
+      if (!ManglingContextDecl || [&]() {
+            // If we must allocate mangling numbers but the
+            // `ManglingContextDecl` is a local variable, use the `DeclContext`
+            // containing the lambda expression instead.
+            VarDecl *Var = dyn_cast<VarDecl>(ManglingContextDecl);
+            return Var && Var->isLocalVarDecl();
+          }())
+        return const_cast<Decl *>(cast<Decl>(DC));
+
+      return ManglingContextDecl;
+    }();
+
+    if (auto *ND = dyn_cast<NamedDecl>(ManglingContextDeclForModule);
         ND && (ND->isInNamedModule() || ND->isFromGlobalModule()) &&
         ND->isExternallyVisible()) {
-      if (!ManglingContextDecl)
-        ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
+      ManglingContextDecl = ND;
       return NonInlineInModulePurview;
     }
 

>From 8b7701621926ec2e1acbf50fed35973227523ada Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Tue, 3 Feb 2026 16:49:40 +0100
Subject: [PATCH 05/18] Remove unneeded '-fmodules' flag

---
 clang/test/Modules/pr178893.cppm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/test/Modules/pr178893.cppm b/clang/test/Modules/pr178893.cppm
index 6d2a599588667..eb4041c330cb3 100644
--- a/clang/test/Modules/pr178893.cppm
+++ b/clang/test/Modules/pr178893.cppm
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -std=c++20 -triple x86_64-apple-macosx10.7.0 -fmodules -xc++ -emit-llvm -o - %s -w | FileCheck %s
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-apple-macosx10.7.0 -xc++ -emit-llvm -o - %s -w | FileCheck %s
 
 // CHECK-LABEL: define linkonce_odr noundef i32 @_ZZN8PR178893W3mod6format5parseEPiENKUlvE_clEv
 // CHECK-LABEL: define linkonce_odr noundef i32 @_ZZN8PR178893W3mod6format5parseEPiENKUlvE0_clEv

>From 49338edb86222be0354114c4d4f19ff596c81750 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Tue, 3 Feb 2026 16:50:32 +0100
Subject: [PATCH 06/18] Make test a bit more robust

Co-authored-by: Chuanqi Xu <yedeng.yd at linux.alibaba.com>
---
 clang/test/Modules/pr178893.cppm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/test/Modules/pr178893.cppm b/clang/test/Modules/pr178893.cppm
index eb4041c330cb3..81f29ac087712 100644
--- a/clang/test/Modules/pr178893.cppm
+++ b/clang/test/Modules/pr178893.cppm
@@ -1,6 +1,6 @@
 // RUN: %clang_cc1 -std=c++20 -triple x86_64-apple-macosx10.7.0 -xc++ -emit-llvm -o - %s -w | FileCheck %s
 
-// CHECK-LABEL: define linkonce_odr noundef i32 @_ZZN8PR178893W3mod6format5parseEPiENKUlvE_clEv
+// CHECK-LABEL: define {{.*}}@_ZZN8PR178893W3mod6format5parseEPiENKUlvE_clEv
 // CHECK-LABEL: define linkonce_odr noundef i32 @_ZZN8PR178893W3mod6format5parseEPiENKUlvE0_clEv
 
 export module mod;

>From 691f4ed6ee9fa27c09d843206fc74b107277932a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Tue, 3 Feb 2026 16:51:20 +0100
Subject: [PATCH 07/18] Make test a bit more robust, part 2

---
 clang/test/Modules/pr178893.cppm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/test/Modules/pr178893.cppm b/clang/test/Modules/pr178893.cppm
index 81f29ac087712..70bb681f4235d 100644
--- a/clang/test/Modules/pr178893.cppm
+++ b/clang/test/Modules/pr178893.cppm
@@ -1,7 +1,7 @@
 // RUN: %clang_cc1 -std=c++20 -triple x86_64-apple-macosx10.7.0 -xc++ -emit-llvm -o - %s -w | FileCheck %s
 
 // CHECK-LABEL: define {{.*}}@_ZZN8PR178893W3mod6format5parseEPiENKUlvE_clEv
-// CHECK-LABEL: define linkonce_odr noundef i32 @_ZZN8PR178893W3mod6format5parseEPiENKUlvE0_clEv
+// CHECK-LABEL: define {{.*}}@_ZZN8PR178893W3mod6format5parseEPiENKUlvE0_clEv
 
 export module mod;
 

>From 4de014b1a13557e4296b1e0c7fe78733de241ce6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Tue, 3 Feb 2026 16:52:43 +0100
Subject: [PATCH 08/18] Use %itanium_abi_triple as triple for the test

---
 clang/test/Modules/pr178893.cppm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/test/Modules/pr178893.cppm b/clang/test/Modules/pr178893.cppm
index 70bb681f4235d..e58e183d82aba 100644
--- a/clang/test/Modules/pr178893.cppm
+++ b/clang/test/Modules/pr178893.cppm
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -std=c++20 -triple x86_64-apple-macosx10.7.0 -xc++ -emit-llvm -o - %s -w | FileCheck %s
+// RUN: %clang_cc1 -std=c++20 -triple %itanium_abi_triple -xc++ -emit-llvm -o - %s -w | FileCheck %s
 
 // CHECK-LABEL: define {{.*}}@_ZZN8PR178893W3mod6format5parseEPiENKUlvE_clEv
 // CHECK-LABEL: define {{.*}}@_ZZN8PR178893W3mod6format5parseEPiENKUlvE0_clEv

>From c09989925f901fbc9d7a073ad13e222e5b52f68b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Fri, 6 Feb 2026 18:22:24 +0100
Subject: [PATCH 09/18] Check for modules in 'Normal' Kind using helper
 function, removing 'NonInlineInModulePurview'

---
 clang/lib/Sema/SemaLambda.cpp | 46 ++++++++++++-----------------------
 1 file changed, 16 insertions(+), 30 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index f7fd1d1831500..772dc9f9d1287 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -275,6 +275,20 @@ static bool isInInlineFunction(const DeclContext *DC) {
   return false;
 }
 
+// See discussion in https://github.com/itanium-cxx-abi/cxx-abi/issues/186
+//
+// zygoloid:
+//    Yeah, I think the only cases left where lambdas don't need a
+//    mangling are when they have (effectively) internal linkage or
+//    appear in a non-inline function in a non-module translation unit.
+static bool isNonInlineInModulePurview(const Decl *ManglingContextDecl,
+                                       const DeclContext *DC) {
+  auto *ND = dyn_cast<NamedDecl>(ManglingContextDecl ? ManglingContextDecl
+                                                     : cast<Decl>(DC));
+  return ND && ((ND->isInNamedModule() || ND->isFromGlobalModule()) &&
+                ND->isExternallyVisible());
+}
+
 std::tuple<MangleNumberingContext *, Decl *>
 Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   // Compute the context for allocating mangling numbers in the current
@@ -288,7 +302,6 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     InlineVariable,
     TemplatedVariable,
     Concept,
-    NonInlineInModulePurview
   } Kind = Normal;
 
   bool IsInNonspecializedTemplate =
@@ -298,33 +311,6 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   // definition, as well as the initializers of data members, receive special
   // treatment. Identify them.
   Kind = [&]() {
-    // See discussion in https://github.com/itanium-cxx-abi/cxx-abi/issues/186
-    //
-    // zygoloid:
-    //    Yeah, I think the only cases left where lambdas don't need a
-    //    mangling are when they have (effectively) internal linkage or appear
-    //    in a non-inline function in a non-module translation unit.
-
-    Decl *ManglingContextDeclForModule = [&]() {
-      if (!ManglingContextDecl || [&]() {
-            // If we must allocate mangling numbers but the
-            // `ManglingContextDecl` is a local variable, use the `DeclContext`
-            // containing the lambda expression instead.
-            VarDecl *Var = dyn_cast<VarDecl>(ManglingContextDecl);
-            return Var && Var->isLocalVarDecl();
-          }())
-        return const_cast<Decl *>(cast<Decl>(DC));
-
-      return ManglingContextDecl;
-    }();
-
-    if (auto *ND = dyn_cast<NamedDecl>(ManglingContextDeclForModule);
-        ND && (ND->isInNamedModule() || ND->isFromGlobalModule()) &&
-        ND->isExternallyVisible()) {
-      ManglingContextDecl = ND;
-      return NonInlineInModulePurview;
-    }
-
     if (!ManglingContextDecl)
       return Normal;
 
@@ -364,7 +350,8 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     //  -- the bodies of inline or templated functions
     if ((IsInNonspecializedTemplate &&
          !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
-        isInInlineFunction(CurContext)) {
+        isInInlineFunction(CurContext) ||
+        isNonInlineInModulePurview(ManglingContextDecl, DC)) {
       while (auto *CD = dyn_cast<CapturedDecl>(DC))
         DC = CD->getParent();
       return std::make_tuple(&Context.getManglingNumberContext(DC), nullptr);
@@ -373,7 +360,6 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     return std::make_tuple(nullptr, nullptr);
   }
 
-  case NonInlineInModulePurview:
   case Concept:
     // Concept definitions aren't code generated and thus aren't mangled,
     // however the ManglingContextDecl is important for the purposes of

>From d73c3f874c4176af5ed8d7b7947146f27c98119c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sat, 7 Feb 2026 02:16:45 +0100
Subject: [PATCH 10/18] Add 'isNonInlineInModulePurview' check in 'VarDecl'
 case

---
 clang/lib/Sema/SemaLambda.cpp | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index 772dc9f9d1287..670642045f6ae 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -281,12 +281,15 @@ static bool isInInlineFunction(const DeclContext *DC) {
 //    Yeah, I think the only cases left where lambdas don't need a
 //    mangling are when they have (effectively) internal linkage or
 //    appear in a non-inline function in a non-module translation unit.
-static bool isNonInlineInModulePurview(const Decl *ManglingContextDecl,
-                                       const DeclContext *DC) {
-  auto *ND = dyn_cast<NamedDecl>(ManglingContextDecl ? ManglingContextDecl
-                                                     : cast<Decl>(DC));
-  return ND && ((ND->isInNamedModule() || ND->isFromGlobalModule()) &&
-                ND->isExternallyVisible());
+static bool isNonInlineInModulePurview(const NamedDecl *ND) {
+  return (ND->isInNamedModule() || ND->isFromGlobalModule()) &&
+         ND->isExternallyVisible();
+}
+
+static bool isNonInlineInModulePurview(const DeclContext *DC) {
+  if (auto *ND = dyn_cast<NamedDecl>(DC))
+    return isNonInlineInModulePurview(ND);
+  return false;
 }
 
 std::tuple<MangleNumberingContext *, Decl *>
@@ -323,6 +326,9 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
       if (Var->getMostRecentDecl()->isInline())
         return InlineVariable;
 
+      if (isNonInlineInModulePurview(Var))
+        return InlineVariable;
+
       if (Var->getDeclContext()->isRecord() && IsInNonspecializedTemplate)
         return TemplatedVariable;
 
@@ -351,7 +357,7 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     if ((IsInNonspecializedTemplate &&
          !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
         isInInlineFunction(CurContext) ||
-        isNonInlineInModulePurview(ManglingContextDecl, DC)) {
+        isNonInlineInModulePurview(CurContext)) {
       while (auto *CD = dyn_cast<CapturedDecl>(DC))
         DC = CD->getParent();
       return std::make_tuple(&Context.getManglingNumberContext(DC), nullptr);

>From 15f3b12e0ee063d99fb36d7317fc8c93d4978e96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sat, 7 Feb 2026 08:38:54 +0100
Subject: [PATCH 11/18] Try using 'DC' for 'ManglingContextDecl' in 'Normal'
 case when 'isNonInlineInModulePurview' is true

---
 clang/lib/Sema/SemaLambda.cpp | 22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index 670642045f6ae..a491d7531a623 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -353,17 +353,21 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   //   types in different translation units to "correspond":
   switch (Kind) {
   case Normal: {
-    //  -- the bodies of inline or templated functions
-    if ((IsInNonspecializedTemplate &&
-         !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
-        isInInlineFunction(CurContext) ||
-        isNonInlineInModulePurview(CurContext)) {
-      while (auto *CD = dyn_cast<CapturedDecl>(DC))
-        DC = CD->getParent();
-      return std::make_tuple(&Context.getManglingNumberContext(DC), nullptr);
+    if (!isNonInlineInModulePurview(CurContext)) {
+      //  -- the bodies of inline or templated functions
+      if ((IsInNonspecializedTemplate &&
+           !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
+          isInInlineFunction(CurContext)) {
+        while (auto *CD = dyn_cast<CapturedDecl>(DC))
+          DC = CD->getParent();
+        return std::make_tuple(&Context.getManglingNumberContext(DC), nullptr);
+      }
+
+      return std::make_tuple(nullptr, nullptr);
     }
 
-    return std::make_tuple(nullptr, nullptr);
+    ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
+    [[fallthrough]];
   }
 
   case Concept:

>From dfc40918f012ce6321649549db4bdcd845de6652 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sat, 7 Feb 2026 19:15:45 +0100
Subject: [PATCH 12/18] exp

---
 clang/lib/Sema/SemaLambda.cpp | 54 ++++++++++++++++++-----------------
 1 file changed, 28 insertions(+), 26 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index a491d7531a623..144b38fc13d95 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -281,14 +281,10 @@ static bool isInInlineFunction(const DeclContext *DC) {
 //    Yeah, I think the only cases left where lambdas don't need a
 //    mangling are when they have (effectively) internal linkage or
 //    appear in a non-inline function in a non-module translation unit.
-static bool isNonInlineInModulePurview(const NamedDecl *ND) {
-  return (ND->isInNamedModule() || ND->isFromGlobalModule()) &&
-         ND->isExternallyVisible();
-}
-
-static bool isNonInlineInModulePurview(const DeclContext *DC) {
-  if (auto *ND = dyn_cast<NamedDecl>(DC))
-    return isNonInlineInModulePurview(ND);
+static bool isNonInlineInModulePurview(const Decl *D) {
+  if (auto *ND = dyn_cast<NamedDecl>(D))
+    return (ND->isInNamedModule() || ND->isFromGlobalModule()) &&
+           ND->isExternallyVisible();
   return false;
 }
 
@@ -305,17 +301,27 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     InlineVariable,
     TemplatedVariable,
     Concept,
+    NonInlineInModulePurview,
   } Kind = Normal;
 
   bool IsInNonspecializedTemplate =
       inTemplateInstantiation() || CurContext->isDependentContext();
 
+  const auto NormalOrNonInlineInModulePurview = [&]() {
+    if (isNonInlineInModulePurview(cast<Decl>(DC))) {
+      ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
+      return NonInlineInModulePurview;
+    }
+
+    return Normal;
+  };
+
   // Default arguments of member function parameters that appear in a class
   // definition, as well as the initializers of data members, receive special
   // treatment. Identify them.
   Kind = [&]() {
     if (!ManglingContextDecl)
-      return Normal;
+      return NormalOrNonInlineInModulePurview();
 
     if (ParmVarDecl *Param = dyn_cast<ParmVarDecl>(ManglingContextDecl)) {
       if (const DeclContext *LexicalDC
@@ -326,9 +332,6 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
       if (Var->getMostRecentDecl()->isInline())
         return InlineVariable;
 
-      if (isNonInlineInModulePurview(Var))
-        return InlineVariable;
-
       if (Var->getDeclContext()->isRecord() && IsInNonspecializedTemplate)
         return TemplatedVariable;
 
@@ -339,13 +342,16 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
         if (!VTS->isExplicitSpecialization())
           return TemplatedVariable;
       }
+
+      if (isNonInlineInModulePurview(Var))
+        return NonInlineInModulePurview;
     } else if (isa<FieldDecl>(ManglingContextDecl)) {
       return DataMember;
     } else if (isa<ImplicitConceptSpecializationDecl>(ManglingContextDecl)) {
       return Concept;
     }
 
-    return Normal;
+    return NormalOrNonInlineInModulePurview();
   }();
 
   // Itanium ABI [5.1.7]:
@@ -353,23 +359,19 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   //   types in different translation units to "correspond":
   switch (Kind) {
   case Normal: {
-    if (!isNonInlineInModulePurview(CurContext)) {
-      //  -- the bodies of inline or templated functions
-      if ((IsInNonspecializedTemplate &&
-           !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
-          isInInlineFunction(CurContext)) {
-        while (auto *CD = dyn_cast<CapturedDecl>(DC))
-          DC = CD->getParent();
-        return std::make_tuple(&Context.getManglingNumberContext(DC), nullptr);
-      }
-
-      return std::make_tuple(nullptr, nullptr);
+    //  -- the bodies of inline or templated functions
+    if ((IsInNonspecializedTemplate &&
+         !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
+        isInInlineFunction(CurContext)) {
+      while (auto *CD = dyn_cast<CapturedDecl>(DC))
+        DC = CD->getParent();
+      return std::make_tuple(&Context.getManglingNumberContext(DC), nullptr);
     }
 
-    ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
-    [[fallthrough]];
+    return std::make_tuple(nullptr, nullptr);
   }
 
+  case NonInlineInModulePurview:
   case Concept:
     // Concept definitions aren't code generated and thus aren't mangled,
     // however the ManglingContextDecl is important for the purposes of

>From 5175951ca08be2078805f0ea06bc9f77a2b489e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sat, 7 Feb 2026 19:55:30 +0100
Subject: [PATCH 13/18] exp

---
 clang/lib/Sema/SemaLambda.cpp | 49 +++++++++++++----------------------
 1 file changed, 18 insertions(+), 31 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index 144b38fc13d95..f6e130ff05f50 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -261,20 +261,6 @@ Sema::createLambdaClosureType(SourceRange IntroducerRange, TypeSourceInfo *Info,
   return Class;
 }
 
-/// Determine whether the given context is or is enclosed in an inline
-/// function.
-static bool isInInlineFunction(const DeclContext *DC) {
-  while (!DC->isFileContext()) {
-    if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(DC))
-      if (FD->isInlined())
-        return true;
-
-    DC = DC->getLexicalParent();
-  }
-
-  return false;
-}
-
 // See discussion in https://github.com/itanium-cxx-abi/cxx-abi/issues/186
 //
 // zygoloid:
@@ -288,6 +274,21 @@ static bool isNonInlineInModulePurview(const Decl *D) {
   return false;
 }
 
+/// Determine whether the given context is or is enclosed in an inline
+/// function.
+static bool isInInlineFunction(const DeclContext *DC) {
+  while (!DC->isFileContext()) {
+    if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(DC)) {
+      if (FD->isInlined() || isNonInlineInModulePurview(FD))
+        return true;
+    }
+
+    DC = DC->getLexicalParent();
+  }
+
+  return false;
+}
+
 std::tuple<MangleNumberingContext *, Decl *>
 Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   // Compute the context for allocating mangling numbers in the current
@@ -301,27 +302,17 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     InlineVariable,
     TemplatedVariable,
     Concept,
-    NonInlineInModulePurview,
   } Kind = Normal;
 
   bool IsInNonspecializedTemplate =
       inTemplateInstantiation() || CurContext->isDependentContext();
 
-  const auto NormalOrNonInlineInModulePurview = [&]() {
-    if (isNonInlineInModulePurview(cast<Decl>(DC))) {
-      ManglingContextDecl = const_cast<Decl *>(cast<Decl>(DC));
-      return NonInlineInModulePurview;
-    }
-
-    return Normal;
-  };
-
   // Default arguments of member function parameters that appear in a class
   // definition, as well as the initializers of data members, receive special
   // treatment. Identify them.
   Kind = [&]() {
     if (!ManglingContextDecl)
-      return NormalOrNonInlineInModulePurview();
+      return Normal;
 
     if (ParmVarDecl *Param = dyn_cast<ParmVarDecl>(ManglingContextDecl)) {
       if (const DeclContext *LexicalDC
@@ -329,7 +320,7 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
         if (LexicalDC->isRecord())
           return DefaultArgument;
     } else if (VarDecl *Var = dyn_cast<VarDecl>(ManglingContextDecl)) {
-      if (Var->getMostRecentDecl()->isInline())
+      if (Var->getMostRecentDecl()->isInline() || isNonInlineInModulePurview(Var))
         return InlineVariable;
 
       if (Var->getDeclContext()->isRecord() && IsInNonspecializedTemplate)
@@ -342,16 +333,13 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
         if (!VTS->isExplicitSpecialization())
           return TemplatedVariable;
       }
-
-      if (isNonInlineInModulePurview(Var))
-        return NonInlineInModulePurview;
     } else if (isa<FieldDecl>(ManglingContextDecl)) {
       return DataMember;
     } else if (isa<ImplicitConceptSpecializationDecl>(ManglingContextDecl)) {
       return Concept;
     }
 
-    return NormalOrNonInlineInModulePurview();
+    return Normal;
   }();
 
   // Itanium ABI [5.1.7]:
@@ -371,7 +359,6 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     return std::make_tuple(nullptr, nullptr);
   }
 
-  case NonInlineInModulePurview:
   case Concept:
     // Concept definitions aren't code generated and thus aren't mangled,
     // however the ManglingContextDecl is important for the purposes of

>From 41082b0cdcac86c0369e1bb3ef508700a99ff441 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sat, 7 Feb 2026 20:00:37 +0100
Subject: [PATCH 14/18] Autoformat

---
 clang/lib/Sema/SemaLambda.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index f6e130ff05f50..5e8c2515fedb0 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -320,7 +320,8 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
         if (LexicalDC->isRecord())
           return DefaultArgument;
     } else if (VarDecl *Var = dyn_cast<VarDecl>(ManglingContextDecl)) {
-      if (Var->getMostRecentDecl()->isInline() || isNonInlineInModulePurview(Var))
+      if (Var->getMostRecentDecl()->isInline() ||
+          isNonInlineInModulePurview(Var))
         return InlineVariable;
 
       if (Var->getDeclContext()->isRecord() && IsInNonspecializedTemplate)

>From 726c8a25e782870633a7a8dfb13c55faed179c81 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sun, 8 Feb 2026 10:45:34 +0100
Subject: [PATCH 15/18] Remove braces

---
 clang/lib/Sema/SemaLambda.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index 5e8c2515fedb0..2a117f0458445 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -278,10 +278,9 @@ static bool isNonInlineInModulePurview(const Decl *D) {
 /// function.
 static bool isInInlineFunction(const DeclContext *DC) {
   while (!DC->isFileContext()) {
-    if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(DC)) {
+    if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(DC))
       if (FD->isInlined() || isNonInlineInModulePurview(FD))
         return true;
-    }
 
     DC = DC->getLexicalParent();
   }

>From 75e10eff045c3632190df84b0253ff19fb1418b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Mon, 9 Feb 2026 20:33:03 +0100
Subject: [PATCH 16/18] Move helpers lambdas into
 Sema::getCurrentMangleNumberContext

Also improve naming and add some more comments what these functions do.
---
 clang/lib/Sema/SemaLambda.cpp | 64 +++++++++++++++++++----------------
 1 file changed, 35 insertions(+), 29 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index 2a117f0458445..c2c8788c5e961 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -261,33 +261,6 @@ Sema::createLambdaClosureType(SourceRange IntroducerRange, TypeSourceInfo *Info,
   return Class;
 }
 
-// See discussion in https://github.com/itanium-cxx-abi/cxx-abi/issues/186
-//
-// zygoloid:
-//    Yeah, I think the only cases left where lambdas don't need a
-//    mangling are when they have (effectively) internal linkage or
-//    appear in a non-inline function in a non-module translation unit.
-static bool isNonInlineInModulePurview(const Decl *D) {
-  if (auto *ND = dyn_cast<NamedDecl>(D))
-    return (ND->isInNamedModule() || ND->isFromGlobalModule()) &&
-           ND->isExternallyVisible();
-  return false;
-}
-
-/// Determine whether the given context is or is enclosed in an inline
-/// function.
-static bool isInInlineFunction(const DeclContext *DC) {
-  while (!DC->isFileContext()) {
-    if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(DC))
-      if (FD->isInlined() || isNonInlineInModulePurview(FD))
-        return true;
-
-    DC = DC->getLexicalParent();
-  }
-
-  return false;
-}
-
 std::tuple<MangleNumberingContext *, Decl *>
 Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   // Compute the context for allocating mangling numbers in the current
@@ -306,6 +279,22 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   bool IsInNonspecializedTemplate =
       inTemplateInstantiation() || CurContext->isDependentContext();
 
+  // Checks if a VarDecl or FunctionDecl is from a module purview and externally
+  // visible. These Decls should be treated as "inline" for the purpose of
+  // mangling in the code below.
+  //
+  // See discussion in https://github.com/itanium-cxx-abi/cxx-abi/issues/186
+  //
+  // zygoloid:
+  //    Yeah, I think the only cases left where lambdas don't need a
+  //    mangling are when they have (effectively) internal linkage or
+  //    appear in a non-inline function in a non-module translation unit.
+  static constexpr auto IsExternallyVisibleInModulePurview =
+      [](const NamedDecl *ND) -> bool {
+    return (ND->isInNamedModule() || ND->isFromGlobalModule()) &&
+           ND->isExternallyVisible();
+  };
+
   // Default arguments of member function parameters that appear in a class
   // definition, as well as the initializers of data members, receive special
   // treatment. Identify them.
@@ -320,7 +309,7 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
           return DefaultArgument;
     } else if (VarDecl *Var = dyn_cast<VarDecl>(ManglingContextDecl)) {
       if (Var->getMostRecentDecl()->isInline() ||
-          isNonInlineInModulePurview(Var))
+          IsExternallyVisibleInModulePurview(Var))
         return InlineVariable;
 
       if (Var->getDeclContext()->isRecord() && IsInNonspecializedTemplate)
@@ -342,6 +331,23 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     return Normal;
   }();
 
+  // Determine whether the given context is or is enclosed in a function that
+  // must be mangled, either:
+  // - an inline function
+  // - or a function in a module purview that is externally visible
+  static constexpr auto IsInFunctionThatNeedsMangling =
+      [](const DeclContext *DC) -> bool {
+    while (!DC->isFileContext()) {
+      if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(DC))
+        if (FD->isInlined() || IsExternallyVisibleInModulePurview(FD))
+          return true;
+
+      DC = DC->getLexicalParent();
+    }
+
+    return false;
+  };
+
   // Itanium ABI [5.1.7]:
   //   In the following contexts [...] the one-definition rule requires closure
   //   types in different translation units to "correspond":
@@ -350,7 +356,7 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     //  -- the bodies of inline or templated functions
     if ((IsInNonspecializedTemplate &&
          !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
-        isInInlineFunction(CurContext)) {
+        IsInFunctionThatNeedsMangling(CurContext)) {
       while (auto *CD = dyn_cast<CapturedDecl>(DC))
         DC = CD->getParent();
       return std::make_tuple(&Context.getManglingNumberContext(DC), nullptr);

>From 9e866098ad1fdfa16f9d4bb851235daada42c50a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Mon, 9 Feb 2026 20:41:47 +0100
Subject: [PATCH 17/18] Be a bit more precise about what this helper does

---
 clang/lib/Sema/SemaLambda.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index c2c8788c5e961..d9ecf5303ce66 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -332,10 +332,10 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   }();
 
   // Determine whether the given context is or is enclosed in a function that
-  // must be mangled, either:
+  // requires Decl's inside to be mangled, so either:
   // - an inline function
   // - or a function in a module purview that is externally visible
-  static constexpr auto IsInFunctionThatNeedsMangling =
+  static constexpr auto IsInFunctionThatRequiresMangling =
       [](const DeclContext *DC) -> bool {
     while (!DC->isFileContext()) {
       if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(DC))
@@ -356,7 +356,7 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     //  -- the bodies of inline or templated functions
     if ((IsInNonspecializedTemplate &&
          !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
-        IsInFunctionThatNeedsMangling(CurContext)) {
+        IsInFunctionThatRequiresMangling(CurContext)) {
       while (auto *CD = dyn_cast<CapturedDecl>(DC))
         DC = CD->getParent();
       return std::make_tuple(&Context.getManglingNumberContext(DC), nullptr);

>From 457dadc858da65e3ba81910d35c3d8517bef8795 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Kokem=C3=BCller?= <jan.kokemueller at gmail.com>
Date: Sat, 14 Feb 2026 10:33:06 +0100
Subject: [PATCH 18/18] Split out ExternallyVisibleVariableInModulePurview
 case, and add some more comments

---
 clang/lib/Sema/SemaLambda.cpp | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp
index d9ecf5303ce66..be7f73dbc267f 100644
--- a/clang/lib/Sema/SemaLambda.cpp
+++ b/clang/lib/Sema/SemaLambda.cpp
@@ -273,6 +273,7 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     DataMember,
     InlineVariable,
     TemplatedVariable,
+    ExternallyVisibleVariableInModulePurview,
     Concept,
   } Kind = Normal;
 
@@ -308,10 +309,12 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
         if (LexicalDC->isRecord())
           return DefaultArgument;
     } else if (VarDecl *Var = dyn_cast<VarDecl>(ManglingContextDecl)) {
-      if (Var->getMostRecentDecl()->isInline() ||
-          IsExternallyVisibleInModulePurview(Var))
+      if (Var->getMostRecentDecl()->isInline())
         return InlineVariable;
 
+      if (IsExternallyVisibleInModulePurview(Var))
+        return ExternallyVisibleVariableInModulePurview;
+
       if (Var->getDeclContext()->isRecord() && IsInNonspecializedTemplate)
         return TemplatedVariable;
 
@@ -348,12 +351,15 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
     return false;
   };
 
-  // Itanium ABI [5.1.7]:
+  // Itanium ABI [5.1.8]:
   //   In the following contexts [...] the one-definition rule requires closure
   //   types in different translation units to "correspond":
   switch (Kind) {
   case Normal: {
     //  -- the bodies of inline or templated functions
+    //  -- the bodies of externally visible functions in a module purview
+    //     (note: this is not yet part of the Itanium ABI, see the linked Github
+    //     discussion above)
     if ((IsInNonspecializedTemplate &&
          !(ManglingContextDecl && isa<ParmVarDecl>(ManglingContextDecl))) ||
         IsInFunctionThatRequiresMangling(CurContext)) {
@@ -375,8 +381,12 @@ Sema::getCurrentMangleNumberContext(const DeclContext *DC) {
   case DefaultArgument:
     //  -- default arguments appearing in class definitions
   case InlineVariable:
+  case ExternallyVisibleVariableInModulePurview:
   case TemplatedVariable:
     //  -- the initializers of inline or templated variables
+    //  -- the initializers of externally visible variables in a module purview
+    //     (note: this is not yet part of the Itanium ABI, see the linked Github
+    //     discussion above)
     return std::make_tuple(
         &Context.getManglingNumberContext(ASTContext::NeedExtraManglingDecl,
                                           ManglingContextDecl),



More information about the cfe-commits mailing list