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 <[email protected]> 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 <[email protected]> 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>; +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
