[clang] [Clang] Keep the immediate-invocation wrapper on a reused CXXTemporaryObjectExpr during instantiation (PR #221816)

Stanislav Bardyuk via cfe-commits cfe-commits at lists.llvm.org
Fri Sep 11 04:40:24 PDT 2026


https://github.com/kodlan updated https://github.com/llvm/llvm-project/pull/221816

>From cc02deb7fd5be4c42121d4e8fe0bbdebbe5127cc Mon Sep 17 00:00:00 2001
From: Stanislav Bardyuk <sbardyuk at google.com>
Date: Mon, 7 Sep 2026 19:58:29 +0000
Subject: [PATCH 1/2] [Clang] Keep the immediate-invocation wrapper on a reused
 CXXTemporaryObjectExpr during instantiation

When a function template contains a temporary built with a consteval
constructor, like (void)S{1}, Sema wraps the CXXTemporaryObjectExpr in a
ConstantExpr marked as an immediate invocation and caches the value.
TreeTransform::TransformConstantExpr drops that wrapper on purpose and
expects the subexpression to be rebuilt through Sema, which re-creates
it. But when the type, constructor and arguments all come out of the
instantiation unchanged, TransformCXXTemporaryObjectExpr takes its reuse
shortcut and returns the bare node, so the instantiated function ends up
with an unwrapped call to the consteval constructor. CodeGen then emits
it (and the constructor body), which trips the "trying to emit a call to
an immediate function" assertion, or without assertions produces a real
call to a consteval function and a link error.

Run the reused node through CheckForImmediateInvocation before
MaybeBindToTemporary, the same two steps InitializationSequence::Perform
does when it creates the node, so it gets the same wrapper a rebuilt one
would get and the destructor cleanup of the temporary stays outside the
ConstantExpr.

Besides the braced one-argument form from the issue, the same shortcut
is reached for S(1, 2), S{}, and the same expressions inside generic
lambdas and class template members.

Fixes #219272
---
 clang/docs/ReleaseNotes.md                    |  3 ++
 clang/lib/Sema/TreeTransform.h                |  7 ++-
 .../test/CodeGenCXX/cxx20-consteval-crash.cpp | 47 +++++++++++++++++++
 3 files changed, 56 insertions(+), 1 deletion(-)

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index a49971adef86f..d223e5a924c5e 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -645,6 +645,9 @@ features cannot lower the translation-unit ABI level;
   (#GH214128)
 - Fixed a crash when a coroutine keyword appeared inside a mem-initializer on a
   function that is not a constructor. (#GH194298)
+- Fixed a crash (and, without assertions, a call to a consteval function being
+  emitted) when a temporary created with a `consteval` constructor, such as
+  `S{1}`, was instantiated from a function template. (#GH219272)
 
 #### Bug Fixes to AST Handling
 
diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h
index c8458fda58a88..75eb954163ab5 100644
--- a/clang/lib/Sema/TreeTransform.h
+++ b/clang/lib/Sema/TreeTransform.h
@@ -16183,7 +16183,12 @@ TreeTransform<Derived>::TransformCXXTemporaryObjectExpr(
       !ArgumentChanged) {
     // FIXME: Instantiation-specific
     SemaRef.MarkFunctionReferenced(E->getBeginLoc(), Constructor);
-    return SemaRef.MaybeBindToTemporary(E);
+    // The immediate-invocation wrapper was stripped by TransformConstantExpr;
+    // put it back before binding the temporary, as SemaInit does.
+    ExprResult Res = SemaRef.CheckForImmediateInvocation(E, Constructor);
+    if (Res.isInvalid())
+      return ExprError();
+    return SemaRef.MaybeBindToTemporary(Res.get());
   }
 
   SourceLocation LParenLoc = T->getTypeLoc().getEndLoc();
diff --git a/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp b/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
index 9c9324f428bec..f5c01729559ca 100644
--- a/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
+++ b/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
@@ -140,3 +140,50 @@ void b() {
 // CHECK-NOT: define {{.*}}foo{{.*}}()
 
 } // namespace GH61142
+
+namespace GH219272 {
+
+consteval void f() {}
+void g();
+
+struct S {
+  consteval S() { f(); }
+  consteval S(int) { f(); }
+  consteval S(int, int) { f(); }
+};
+
+struct D {
+  consteval D(int) { f(); }
+  constexpr ~D() {
+    if (!__builtin_is_constant_evaluated())
+      g();
+  }
+};
+
+template <typename T> void dtor(T) { (void)D{1}; }
+template <typename T> void braces(T) { (void)S{1}; }
+template <typename T> void parens(T) { (void)S(1, 2); }
+template <typename T> void empty_braces(T) { (void)S{}; }
+template <typename T> void lambda(T) { [](auto) { (void)S{1}; }(0); }
+template <typename T> struct C {
+  void m() { (void)S{1}; }
+};
+template <typename T> void member(T) { C<T>{}.m(); }
+
+template void dtor<int>(int);
+template void braces<int>(int);
+template void parens<int>(int);
+template void empty_braces<int>(int);
+template void lambda<int>(int);
+template void member<int>(int);
+
+// The temporary is constant-evaluated, but its destructor still runs.
+// CHECK: define {{.*}} @_ZN8GH2192724dtorIiEEvT_(
+// CHECK-NOT: call {{.*}}GH2192721DC
+// CHECK: call void @_ZN8GH2192721DD1Ev(
+
+// Make sure the consteval constructors are neither called nor emitted.
+// CHECK-NOT: call {{.*}}GH2192721{{S|D}}C
+// CHECK-NOT: define {{.*}}GH2192721{{S|D}}C
+
+} // namespace GH219272

>From 2d3fe86bcbcc912841a0d4ef4e00064dcbd79c85 Mon Sep 17 00:00:00 2001
From: Stanislav Bardyuk <sbardyuk at google.com>
Date: Fri, 11 Sep 2026 11:40:13 +0000
Subject: [PATCH 2/2] [Clang] Keep a reused immediate invocation's ConstantExpr
 in TreeTransform

TreeTransform drops the ConstantExpr that wraps an immediate invocation
and relies on Sema wrapping the rebuilt expression again. That does not
happen for the reuse shortcuts, which return the old node bare, and it
means a rebuilt one is constant evaluated a second time.

Keep the node instead: TransformConstantExpr returns the original
ConstantExpr, with its cached value, when the subexpression comes back
unchanged. The shortcuts return the reused node bound to a fresh
temporary, so a CXXBindTemporaryExpr around the same node counts as
unchanged; the binding is kept outside the ConstantExpr, where CodeGen
sees it (a binding underneath is not emitted once the value is cached).

For that to work the ConstantExpr has to be visited. The cast
transforms use getSubExprAsWritten(), and TransformInitializer strips
every FullExpr, so both skipped it. A small helper stops at an
immediate invocation when it wraps the operand as written, and
TransformInitializer transforms the ConstantExpr instead of the stripped
initializer on the paths that reuse the initializer as is. The nested
immediate invocation removal in Sema keeps the old behaviour so its
diagnostics do not change.

A reused subexpression is not rebuilt through Sema, so the references
to consteval functions inside it were recorded as if they were outside
an immediate invocation and diagnosed as escaping; drop them when the
node is kept.

This replaces the CheckForImmediateInvocation call added to the
CXXTemporaryObjectExpr shortcut in the previous commit and also covers a
reused consteval member call on a non-dependent object.
---
 clang/include/clang/Sema/Sema.h               |  4 +
 clang/lib/Sema/SemaExpr.cpp                   | 39 ++++++--
 clang/lib/Sema/TreeTransform.h                | 90 +++++++++++++++----
 .../test/CodeGenCXX/cxx20-consteval-crash.cpp | 22 +++++
 clang/test/SemaCXX/cxx2a-consteval.cpp        | 25 ++++++
 .../SemaCXX/cxx2b-consteval-propagate.cpp     | 10 +++
 6 files changed, 163 insertions(+), 27 deletions(-)

diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 4ff4c669a6b70..32ef4da35f478 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -7736,6 +7736,10 @@ class Sema final : public SemaBase {
   /// invocation.
   ExprResult CheckForImmediateInvocation(ExprResult E, FunctionDecl *Decl);
 
+  /// Forget the references to consteval functions inside \p E, an immediate
+  /// invocation that was reused as is rather than rebuilt.
+  void RemoveReferencesToConsteval(Expr *E);
+
   void MarkExpressionAsImmediateEscalating(Expr *E);
 
   // Check that the SME attributes for PSTATE.ZA and PSTATE.SM are compatible.
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index c93efeb928c56..27753237076fa 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -18617,6 +18617,12 @@ static void RemoveNestedImmediateInvocation(
     /// Base::TransformUserDefinedLiteral doesn't preserve the
     /// UserDefinedLiteral node.
     ExprResult TransformUserDefinedLiteral(UserDefinedLiteral *E) { return E; }
+    /// Keep skipping immediate invocations under implicit casts: they stay
+    /// separate candidates and are evaluated and diagnosed on their own, which
+    /// existing diagnostics depend on.
+    Expr *getCastOperandToTransform(CastExpr *E) {
+      return E->getSubExprAsWritten();
+    }
     /// Base::TransformInitializer skips ConstantExpr so we need to visit them
     /// here.
     ExprResult TransformInitializer(Expr *Init, bool NotCopyInit) {
@@ -18684,6 +18690,28 @@ static void RemoveNestedImmediateInvocation(
   }
 }
 
+/// Erase the DeclRefExprs within \p S, a subexpression of an immediate
+/// invocation, from the set of references to consteval functions.
+static void
+EraseReferencesToConsteval(llvm::SmallPtrSetImpl<DeclRefExpr *> &DRSet,
+                           Stmt *S) {
+  struct SimpleRemove : DynamicRecursiveASTVisitor {
+    llvm::SmallPtrSetImpl<DeclRefExpr *> &DRSet;
+    SimpleRemove(llvm::SmallPtrSetImpl<DeclRefExpr *> &S) : DRSet(S) {}
+    bool VisitDeclRefExpr(DeclRefExpr *E) override {
+      DRSet.erase(E);
+      return DRSet.size();
+    }
+  } Visitor(DRSet);
+  Visitor.TraverseStmt(S);
+}
+
+void Sema::RemoveReferencesToConsteval(Expr *E) {
+  auto &DRSet = currentEvaluationContext().ReferenceToConsteval;
+  if (!DRSet.empty())
+    EraseReferencesToConsteval(DRSet, E);
+}
+
 static void
 HandleImmediateInvocations(Sema &SemaRef,
                            Sema::ExpressionEvaluationContextRecord &Rec) {
@@ -18733,15 +18761,8 @@ HandleImmediateInvocations(Sema &SemaRef,
         RemoveNestedImmediateInvocation(SemaRef, Rec, It);
   } else if (Rec.ImmediateInvocationCandidates.size() == 1 &&
              Rec.ReferenceToConsteval.size()) {
-    struct SimpleRemove : DynamicRecursiveASTVisitor {
-      llvm::SmallPtrSetImpl<DeclRefExpr *> &DRSet;
-      SimpleRemove(llvm::SmallPtrSetImpl<DeclRefExpr *> &S) : DRSet(S) {}
-      bool VisitDeclRefExpr(DeclRefExpr *E) override {
-        DRSet.erase(E);
-        return DRSet.size();
-      }
-    } Visitor(Rec.ReferenceToConsteval);
-    Visitor.TraverseStmt(
+    EraseReferencesToConsteval(
+        Rec.ReferenceToConsteval,
         Rec.ImmediateInvocationCandidates.front().getPointer()->getSubExpr());
   }
   for (auto CE : Rec.ImmediateInvocationCandidates)
diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h
index 75eb954163ab5..d60aa98173380 100644
--- a/clang/lib/Sema/TreeTransform.h
+++ b/clang/lib/Sema/TreeTransform.h
@@ -23,6 +23,7 @@
 #include "clang/AST/ExprConcepts.h"
 #include "clang/AST/ExprObjC.h"
 #include "clang/AST/ExprOpenMP.h"
+#include "clang/AST/IgnoreExpr.h"
 #include "clang/AST/OpenMPClause.h"
 #include "clang/AST/Stmt.h"
 #include "clang/AST/StmtCXX.h"
@@ -487,6 +488,10 @@ class TreeTransform {
   /// \returns the transformed initializer.
   ExprResult TransformInitializer(Expr *Init, bool NotCopyInit);
 
+  /// Get the operand of a cast to transform: the operand as written, unless
+  /// that skips an immediate invocation, which TransformConstantExpr can keep.
+  Expr *getCastOperandToTransform(CastExpr *E);
+
   /// Transform the given list of expressions.
   ///
   /// This routine transforms a list of expressions by invoking
@@ -4487,8 +4492,15 @@ ExprResult TreeTransform<Derived>::TransformInitializer(Expr *Init,
   if (!Init)
     return Init;
 
-  if (auto *FE = dyn_cast<FullExpr>(Init))
+  // Remember an immediate invocation; TransformConstantExpr can keep it if the
+  // expression underneath is reused.
+  ConstantExpr *ImmediateInvocation = nullptr;
+  if (auto *FE = dyn_cast<FullExpr>(Init)) {
+    if (auto *CE = dyn_cast<ConstantExpr>(FE);
+        CE && CE->isImmediateInvocation())
+      ImmediateInvocation = CE;
     Init = FE->getSubExpr();
+  }
 
   if (auto *AIL = dyn_cast<ArrayInitLoopExpr>(Init)) {
     OpaqueValueExpr *OVE = AIL->getCommonExpr();
@@ -4502,18 +4514,27 @@ ExprResult TreeTransform<Derived>::TransformInitializer(Expr *Init,
     Init = Binder->getSubExpr();
 
   if (ImplicitCastExpr *ICE = dyn_cast<ImplicitCastExpr>(Init))
-    Init = ICE->getSubExprAsWritten();
+    Init = getDerived().getCastOperandToTransform(ICE);
 
   if (CXXStdInitializerListExpr *ILE =
           dyn_cast<CXXStdInitializerListExpr>(Init))
     return TransformInitializer(ILE->getSubExpr(), NotCopyInit);
 
+  // Where the initializer is transformed as is, transform the immediate
+  // invocation instead if it directly wraps the initializer, so that a reused
+  // initializer keeps it.
+  auto TransformAsIs = [&](Expr *E) {
+    if (ImmediateInvocation && ImmediateInvocation->getSubExpr() == E)
+      E = ImmediateInvocation;
+    return getDerived().TransformExpr(E);
+  };
+
   // If this is copy-initialization, we only need to reconstruct
   // InitListExprs. Other forms of copy-initialization will be a no-op if
   // the initializer is already the right type.
   CXXConstructExpr *Construct = dyn_cast<CXXConstructExpr>(Init);
   if (!NotCopyInit && !(Construct && Construct->isListInitialization()))
-    return getDerived().TransformExpr(Init);
+    return TransformAsIs(Init);
 
   // Revert value-initialization back to empty parens.
   if (CXXScalarValueInitExpr *VIE = dyn_cast<CXXScalarValueInitExpr>(Init)) {
@@ -4530,7 +4551,7 @@ ExprResult TreeTransform<Derived>::TransformInitializer(Expr *Init,
   // Revert initialization by constructor back to a parenthesized or braced list
   // of expressions. Any other form of initializer can just be reused directly.
   if (!Construct || isa<CXXTemporaryObjectExpr>(Construct))
-    return getDerived().TransformExpr(Init);
+    return TransformAsIs(Init);
 
   // If the initialization implicitly converted an initializer list to a
   // std::initializer_list object, unwrap the std::initializer_list too.
@@ -13547,7 +13568,45 @@ ExprResult TreeTransform<Derived>::TransformOpenACCAsteriskSizeExpr(
 template<typename Derived>
 ExprResult
 TreeTransform<Derived>::TransformConstantExpr(ConstantExpr *E) {
-  return TransformExpr(E->getSubExpr());
+  if (!E->isImmediateInvocation())
+    return TransformExpr(E->getSubExpr());
+
+  // Sema wraps a rebuilt immediate invocation again, but the reuse shortcuts
+  // return the old node bare (at most bound to a fresh temporary), so keep this
+  // node, and its cached result, for a reused subexpression.
+  ExprResult SubExpr = getDerived().TransformExpr(E->getSubExpr());
+  if (SubExpr.isInvalid())
+    return ExprError();
+  if (getDerived().AlwaysRebuild())
+    return SubExpr;
+  Expr *Old = E->getSubExpr();
+  auto *Bind = dyn_cast<CXXBindTemporaryExpr>(SubExpr.get());
+  if (SubExpr.get() != Old &&
+      !(Bind && Bind->getSubExpr() == Old->IgnoreImplicit()))
+    return SubExpr;
+  // The reused subexpression was not rebuilt through Sema, so its references
+  // to consteval functions were recorded as if outside an immediate invocation.
+  SemaRef.RemoveReferencesToConsteval(E);
+  if (!Bind)
+    return E;
+  // Keep the fresh binding outside the immediate invocation, where CodeGen
+  // sees it; a binding underneath is not emitted once the value is cached.
+  return CXXBindTemporaryExpr::Create(SemaRef.Context, Bind->getTemporary(), E);
+}
+
+template <typename Derived>
+Expr *TreeTransform<Derived>::getCastOperandToTransform(CastExpr *E) {
+  Expr *Written = E->getSubExprAsWritten();
+  for (Expr *Sub = E->getSubExpr(); Sub != Written;) {
+    if (auto *CE = dyn_cast<ConstantExpr>(Sub);
+        CE && CE->isImmediateInvocation() && CE->IgnoreImplicit() == Written)
+      return CE;
+    Expr *Next = IgnoreImplicitSingleStep(Sub);
+    if (Next == Sub)
+      break;
+    Sub = Next;
+  }
+  return Written;
 }
 
 template <typename Derived>
@@ -14444,7 +14503,7 @@ ExprResult
 TreeTransform<Derived>::TransformImplicitCastExpr(ImplicitCastExpr *E) {
   // Implicit casts are eliminated during transformation, since they
   // will be recomputed by semantic analysis after transformation.
-  return getDerived().TransformExpr(E->getSubExprAsWritten());
+  return getDerived().TransformExpr(getDerived().getCastOperandToTransform(E));
 }
 
 template<typename Derived>
@@ -14454,8 +14513,8 @@ TreeTransform<Derived>::TransformCStyleCastExpr(CStyleCastExpr *E) {
   if (!Type)
     return ExprError();
 
-  ExprResult SubExpr
-    = getDerived().TransformExpr(E->getSubExprAsWritten());
+  ExprResult SubExpr =
+      getDerived().TransformExpr(getDerived().getCastOperandToTransform(E));
   if (SubExpr.isInvalid())
     return ExprError();
 
@@ -14971,8 +15030,8 @@ TreeTransform<Derived>::TransformCXXNamedCastExpr(CXXNamedCastExpr *E) {
   if (!Type)
     return ExprError();
 
-  ExprResult SubExpr
-    = getDerived().TransformExpr(E->getSubExprAsWritten());
+  ExprResult SubExpr =
+      getDerived().TransformExpr(getDerived().getCastOperandToTransform(E));
   if (SubExpr.isInvalid())
     return ExprError();
 
@@ -15043,8 +15102,8 @@ TreeTransform<Derived>::TransformCXXFunctionalCastExpr(
   if (!Type)
     return ExprError();
 
-  ExprResult SubExpr
-    = getDerived().TransformExpr(E->getSubExprAsWritten());
+  ExprResult SubExpr =
+      getDerived().TransformExpr(getDerived().getCastOperandToTransform(E));
   if (SubExpr.isInvalid())
     return ExprError();
 
@@ -16183,12 +16242,7 @@ TreeTransform<Derived>::TransformCXXTemporaryObjectExpr(
       !ArgumentChanged) {
     // FIXME: Instantiation-specific
     SemaRef.MarkFunctionReferenced(E->getBeginLoc(), Constructor);
-    // The immediate-invocation wrapper was stripped by TransformConstantExpr;
-    // put it back before binding the temporary, as SemaInit does.
-    ExprResult Res = SemaRef.CheckForImmediateInvocation(E, Constructor);
-    if (Res.isInvalid())
-      return ExprError();
-    return SemaRef.MaybeBindToTemporary(Res.get());
+    return SemaRef.MaybeBindToTemporary(E);
   }
 
   SourceLocation LParenLoc = T->getTypeLoc().getEndLoc();
diff --git a/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp b/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
index f5c01729559ca..851de2ac58595 100644
--- a/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
+++ b/clang/test/CodeGenCXX/cxx20-consteval-crash.cpp
@@ -161,6 +161,7 @@ struct D {
 };
 
 template <typename T> void dtor(T) { (void)D{1}; }
+template <typename T> void dtor2(T) { (void)D{1}; (void)D{2}; }
 template <typename T> void braces(T) { (void)S{1}; }
 template <typename T> void parens(T) { (void)S(1, 2); }
 template <typename T> void empty_braces(T) { (void)S{}; }
@@ -170,20 +171,41 @@ template <typename T> struct C {
 };
 template <typename T> void member(T) { C<T>{}.m(); }
 
+struct M {
+  consteval int m() const { f(); return 1; }
+};
+constexpr M gm{};
+template <typename T> int memcall(T) { return gm.m(); }
+
+template int memcall<int>(int);
 template void dtor<int>(int);
+template void dtor2<int>(int);
 template void braces<int>(int);
 template void parens<int>(int);
 template void empty_braces<int>(int);
 template void lambda<int>(int);
 template void member<int>(int);
 
+// A consteval member call on a non-dependent object is reused as well.
+// CHECK: define {{.*}} @_ZN8GH2192727memcallIiEEiT_(
+// CHECK-NOT: call
+// CHECK: ret i32 1
+
 // The temporary is constant-evaluated, but its destructor still runs.
 // CHECK: define {{.*}} @_ZN8GH2192724dtorIiEEvT_(
 // CHECK-NOT: call {{.*}}GH2192721DC
 // CHECK: call void @_ZN8GH2192721DD1Ev(
 
+// Same with two immediate invocations in one body (Sema rewrites them then).
+// CHECK: define {{.*}} @_ZN8GH2192725dtor2IiEEvT_(
+// CHECK-NOT: call {{.*}}GH2192721DC
+// CHECK: call void @_ZN8GH2192721DD1Ev(
+// CHECK-NOT: call {{.*}}GH2192721DC
+// CHECK: call void @_ZN8GH2192721DD1Ev(
+
 // Make sure the consteval constructors are neither called nor emitted.
 // CHECK-NOT: call {{.*}}GH2192721{{S|D}}C
 // CHECK-NOT: define {{.*}}GH2192721{{S|D}}C
+// CHECK-NOT: define {{.*}}GH2192721M1m
 
 } // namespace GH219272
diff --git a/clang/test/SemaCXX/cxx2a-consteval.cpp b/clang/test/SemaCXX/cxx2a-consteval.cpp
index 7a017e513b2a3..0c8b041bc75c2 100644
--- a/clang/test/SemaCXX/cxx2a-consteval.cpp
+++ b/clang/test/SemaCXX/cxx2a-consteval.cpp
@@ -1363,3 +1363,28 @@ void bar() {
   __builtin_dump_struct(&g_c, F, s);
 }
 }  // namespace GH192846
+
+namespace GH219272 {
+consteval int f() { return 1; }
+struct S { consteval S(int (&p)()) { p(); } };
+struct M { consteval int m(int (&p)()) const { return p(); } };
+constexpr M gm{};
+int gi; // expected-note {{declared here}}
+struct Bad { consteval Bad(int *p) : v(*p) {} int v; }; // expected-note {{read of non-const variable 'gi' is not allowed in a constant expression}}
+
+// The instantiation reuses the immediate invocations of the template; the
+// references to consteval functions inside them are not stray references.
+template <typename T> void reused(T) {
+  (void)S{f};
+  gm.m(f);
+  int x = gm.m(f);
+}
+template void reused<int>(int);
+
+// A reused invocation that failed is diagnosed once, in the template.
+template <typename T> void failed(T) {
+  (void)Bad{&gi}; // expected-error {{call to consteval function 'GH219272::Bad::Bad' is not a constant expression}} \
+                  // expected-note {{in call to 'Bad(&gi)'}}
+}
+template void failed<int>(int);
+}
diff --git a/clang/test/SemaCXX/cxx2b-consteval-propagate.cpp b/clang/test/SemaCXX/cxx2b-consteval-propagate.cpp
index 39097d17441f7..a926833ee39fe 100644
--- a/clang/test/SemaCXX/cxx2b-consteval-propagate.cpp
+++ b/clang/test/SemaCXX/cxx2b-consteval-propagate.cpp
@@ -659,3 +659,13 @@ scope_exit guard( // expected-note {{in instantiation of member function}}
 );
 
 }
+
+namespace GH219272 {
+consteval int f() { return 1; }
+struct M { consteval int m(int (&p)()) const { return p(); } };
+constexpr M gm{};
+// The instantiation reuses the immediate invocation; the reference to f
+// inside it must not escalate cg<int>.
+template <typename T> constexpr int cg(T) { return gm.m(f); }
+int (*p)(int) = cg<int>;
+}



More information about the cfe-commits mailing list