[clang] [Clang][AST/Sema] Fix lambda function pointer conversion operator immediate escalation (PR #235356)

Ferdinand Bachmann via cfe-commits cfe-commits at lists.llvm.org
Sat Oct 24 16:55:08 PDT 2026


https://github.com/Ferdi265 updated https://github.com/llvm/llvm-project/pull/235356

>From 195de4bf8ae46a8d9989803aaf0e229c8a2b1db9 Mon Sep 17 00:00:00 2001
From: Ferdinand Bachmann <ferdinand.bachmann at yrlf.at>
Date: Sat, 17 Oct 2026 11:26:25 +0200
Subject: [PATCH 1/4] [Clang][AST] Fix lambda conversion operators not being
 immediate-escalating

[expr.prim.lambda.closure]/p11 defines that the function F should be
immediate if the call operator is immediate (i.e. immediate-escalating),
but says nothing about whether the call operator should be. Since it
falls under none of the other definitions of [expr.const.imm]/p3 it
should not be immediate-escalating per the letter of the standard.

However, since the conversion operator for a consteval lambda would
otherwise be uncallable, consider this a defect in the standard and make
the conversion operator immediate-escalating.

This is also how other compilers, such as GCC, implement it.

Fixes #235353.

Signed-off-by: Ferdinand Bachmann <ferdinand.bachmann at yrlf.at>
---
 clang/include/clang/AST/ASTLambda.h | 12 ++++++------
 clang/lib/AST/Decl.cpp              |  4 ++++
 2 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/clang/include/clang/AST/ASTLambda.h b/clang/include/clang/AST/ASTLambda.h
index a1854b6a1a9491..eeb1fc6064d3ab 100644
--- a/clang/include/clang/AST/ASTLambda.h
+++ b/clang/include/clang/AST/ASTLambda.h
@@ -66,17 +66,17 @@ inline bool isGenericLambdaCallOperatorSpecialization(const CXXMethodDecl *MD) {
   return false;
 }
 
-inline bool isLambdaConversionOperator(CXXConversionDecl *C) {
+inline bool isLambdaConversionOperator(const CXXConversionDecl *C) {
   return C ? C->getParent()->isLambda() : false;
 }
 
-inline bool isLambdaConversionOperator(Decl *D) {
+inline bool isLambdaConversionOperator(const Decl *D) {
   if (!D) return false;
-  if (CXXConversionDecl *Conv = dyn_cast<CXXConversionDecl>(D))
+  if (const CXXConversionDecl *Conv = dyn_cast<CXXConversionDecl>(D))
     return isLambdaConversionOperator(Conv);
-  if (FunctionTemplateDecl *F = dyn_cast<FunctionTemplateDecl>(D))
-    if (CXXConversionDecl *Conv =
-        dyn_cast_or_null<CXXConversionDecl>(F->getTemplatedDecl()))
+  if (const FunctionTemplateDecl *F = dyn_cast<FunctionTemplateDecl>(D))
+    if (const CXXConversionDecl *Conv =
+            dyn_cast_or_null<CXXConversionDecl>(F->getTemplatedDecl()))
       return isLambdaConversionOperator(Conv);
   return false;
 }
diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp
index f920a1e9b8fd85..4431301ecf69a8 100644
--- a/clang/lib/AST/Decl.cpp
+++ b/clang/lib/AST/Decl.cpp
@@ -3370,6 +3370,10 @@ bool FunctionDecl::isImmediateEscalating() const {
   //  specifier,
   if (isLambdaCallOperator(this) && !isConsteval())
     return true;
+  // defect: [expr.prim.lambda.closure]/p11 should define that the conversion
+  // operator is immediate if the call operator is immediate (i.e. escalating)
+  if (isLambdaConversionOperator(this) && !isConsteval())
+    return true;
   // - a defaulted special member function that is not declared with the
   // consteval specifier,
   if (isDefaulted() && !isConsteval())

>From fbe9002d8b7928d84775cd95d53718bee8f97c1d Mon Sep 17 00:00:00 2001
From: Ferdinand Bachmann <ferdinand.bachmann at yrlf.at>
Date: Sat, 17 Oct 2026 21:09:04 +0200
Subject: [PATCH 2/4] [Clang][Sema] Fix SynthesizedFunctionScope not escalating
 immediate functions properly

CheckImmediateEscalatingFunctionDefinition() relies on
FSI->FoundImmediateEscalatingExpression already being set for the
function. This only happens in HandleImmediateInvocations() as part of
PopExpressionEvaluationContext(), which was only being called afterward.

Ensure we call PopExpressionEvaluationContext() before
CheckImmediateEscalatingFunctionDefinition().

This fixes immediate escalation of functions defined using
SynthesizedFunctionScope, such as lambda conversion operators.
---
 clang/include/clang/Sema/Sema.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index cd2227f2aa5c69..9062355b99b179 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -13809,11 +13809,11 @@ class Sema final : public SemaBase {
     ~SynthesizedFunctionScope() {
       if (PushedCodeSynthesisContext)
         S.popCodeSynthesisContext();
+      S.PopExpressionEvaluationContext();
       if (auto *FD = dyn_cast<FunctionDecl>(S.CurContext)) {
         FD->setWillHaveBody(false);
         S.CheckImmediateEscalatingFunctionDefinition(FD, S.getCurFunction());
       }
-      S.PopExpressionEvaluationContext();
       S.PopFunctionScopeInfo();
     }
 

>From d9d33989dc275221d3209d3a1f730193b4388c48 Mon Sep 17 00:00:00 2001
From: Ferdinand Bachmann <ferdinand.bachmann at yrlf.at>
Date: Sat, 17 Oct 2026 19:47:47 +0200
Subject: [PATCH 3/4] [Clang][Sema] Fix lambda methods inheriting immediate
 function context

Since lambda conversion operators are now immediate escalating, we don't
need this special case for lambda methods any more.

Additionally, the previous fix only worked in situations where an outer
immediate function context was allowed to "leak in", and didn't
correctly capture cases where such a lambda method was called from a
non-immediate scope.
---
 clang/lib/Sema/SemaExpr.cpp | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index 43b0c86132172b..7177d5faf46cf0 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -18598,8 +18598,6 @@ void Sema::PushExpressionEvaluationContextForFunction(
       FD && FD->isConsteval()
           ? ExpressionEvaluationContext::ImmediateFunctionContext
           : NewContext);
-  const Sema::ExpressionEvaluationContextRecord &Parent =
-      parentEvaluationContext();
   Sema::ExpressionEvaluationContextRecord &Current = currentEvaluationContext();
 
   Current.InDiscardedStatement = false;
@@ -18617,12 +18615,7 @@ void Sema::PushExpressionEvaluationContextForFunction(
     Current.InImmediateEscalatingFunctionContext =
         getLangOpts().CPlusPlus20 && FD->isImmediateEscalating();
 
-    if (isLambdaMethod(FD) && !isLambdaCallOperator(FD))
-      Current.InImmediateFunctionContext = FD->isConsteval() ||
-                                           Parent.isConstantEvaluated() ||
-                                           Parent.isImmediateFunctionContext();
-    else
-      Current.InImmediateFunctionContext = FD->isConsteval();
+    Current.InImmediateFunctionContext = FD->isConsteval();
   }
 }
 

>From bf84d64d4174e889835decfc7432f2810cec800c Mon Sep 17 00:00:00 2001
From: Ferdinand Bachmann <ferdinand.bachmann at yrlf.at>
Date: Sun, 18 Oct 2026 18:04:27 +0200
Subject: [PATCH 4/4] WIP: update consteval lambda fptr conversion testcases

---
 .../cxx23-static-callop-lambda-expression.cpp |  8 +++----
 clang/test/SemaCXX/cxx2a-consteval.cpp        | 23 ++++++++++++-------
 2 files changed, 19 insertions(+), 12 deletions(-)

diff --git a/clang/test/SemaCXX/cxx23-static-callop-lambda-expression.cpp b/clang/test/SemaCXX/cxx23-static-callop-lambda-expression.cpp
index 84fac9edad1027..2d7c1b25660e8c 100644
--- a/clang/test/SemaCXX/cxx23-static-callop-lambda-expression.cpp
+++ b/clang/test/SemaCXX/cxx23-static-callop-lambda-expression.cpp
@@ -9,12 +9,12 @@ namespace ns1 {
 
 namespace ns1_1 {
 
-  auto lstatic = []() static consteval  //expected-error{{cannot take address of consteval call}} \
-                                          expected-note {{declared here}}
+  auto lstatic = []() static consteval // expected-note {{operator int (*)()' is an immediate function because its body evaluates the address of a consteval function 'operator()'}} \
+                                       // expected-note {{declared here}}
   { return 3; };
 
-  // FIXME: the above error should indicate that it was triggered below.
-  int (*f2)(void) = lstatic;
+  int (*f2)(void) = lstatic; // expected-error {{call to immediate function 'ns1_1::(lambda)::operator int (*)()' is not a constant expression}} \
+                             // expected-note {{pointer to a consteval declaration is not a constant expression}}
 
 }
 
diff --git a/clang/test/SemaCXX/cxx2a-consteval.cpp b/clang/test/SemaCXX/cxx2a-consteval.cpp
index 7a017e513b2a3c..ade887e9941cf2 100644
--- a/clang/test/SemaCXX/cxx2a-consteval.cpp
+++ b/clang/test/SemaCXX/cxx2a-consteval.cpp
@@ -251,13 +251,14 @@ auto l0 = [](int i) consteval {
   return f(i);
 };
 
-auto l1 = [](int i) constexpr { // expected-error{{cannot take address of immediate call operator}} \
+auto l1 = [](int i) constexpr { // expected-note {{operator int (*)(int)' is an immediate function because its body evaluates the address of an immediate function '__invoke'}} \
                                 // expected-note {{declared here}}
   int t = f(i);
   return f(0);
 };
 
-int(*test)(int)  = l1;
+int(*test)(int)  = l1; // expected-error {{call to immediate function 'context::(lambda)::operator int (*)(int)' is not a constant expression}} \
++                      // expected-note {{pointer to a consteval declaration is not a constant expression}}
 
 }
 
@@ -1046,24 +1047,30 @@ int f() {
 
 namespace GH57682 {
 void test() {
-  constexpr auto l1 = []() consteval { // expected-error {{cannot take address of consteval call operator of '(lambda at}} \
+  constexpr auto l1 = []() consteval { // expected-note {{'operator int (*)()' is an immediate function because its body evaluates the address of a consteval function '__invoke'}} \
                                        // expected-note  2{{declared here}}
         return 3;
   };
-  constexpr int (*f1)(void) = l1; // expected-error {{constexpr variable 'f1' must be initialized by a constant expression}} \
+  constexpr int (*f1)(void) = l1; // expected-error {{call to immediate function 'GH57682::test()::(lambda)::operator int (*)()' is not a constant expression}} \
+                                  // expected-error {{constexpr variable 'f1' must be initialized by a constant expression}} \
+                                  // expected-note  {{pointer to a consteval declaration is not a constant expression}} \
                                   // expected-note  {{pointer to a consteval declaration is not a constant expression}}
 
 
-  constexpr auto lstatic = []() static consteval { // expected-error {{cannot take address of consteval call operator of '(lambda at}} \
+  constexpr auto lstatic = []() static consteval { // expected-note {{'operator int (*)()' is an immediate function because its body evaluates the address of a consteval function 'operator()'}} \
                                        // expected-note  2{{declared here}} \
                                        // expected-warning {{extension}}
         return 3;
   };
-  constexpr int (*f2)(void) = lstatic; // expected-error {{constexpr variable 'f2' must be initialized by a constant expression}} \
+  constexpr int (*f2)(void) = lstatic; // expected-error {{call to immediate function 'GH57682::test()::(lambda)::operator int (*)()' is not a constant expression}} \
+                                       // expected-error {{constexpr variable 'f2' must be initialized by a constant expression}} \
+                                       // expected-note  {{pointer to a consteval declaration is not a constant expression}} \
                                        // expected-note  {{pointer to a consteval declaration is not a constant expression}}
 
-  int (*f3)(void) = []() consteval { return 3; };  // expected-error {{cannot take address of consteval call operator of '(lambda at}} \
-                                                   // expected-note {{declared here}}
+  int (*f3)(void) = []() consteval { return 3; }; // expected-error {{call to immediate function 'GH57682::test()::(lambda)::operator int (*)()' is not a constant expression}} \
+                                                  // expected-note {{'operator int (*)()' is an immediate function because its body evaluates the address of a consteval function '__invoke'}} \
+                                                  // expected-note  {{pointer to a consteval declaration is not a constant expression}} \
+                                                  // expected-note {{declared here}}
 }
 
 consteval void consteval_test() {



More information about the cfe-commits mailing list