Skip to content

[Clang] Instantiate functions from constant evaluation. - #205557

Open
katzdm wants to merge 8 commits into
llvm:mainfrom
katzdm:third-time-is-the-charm
Open

[Clang] Instantiate functions from constant evaluation.#205557
katzdm wants to merge 8 commits into
llvm:mainfrom
katzdm:third-time-is-the-charm

Conversation

@katzdm

@katzdm katzdm commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

This PR is a rework of #173537, which was a rework of #115168; the sole goal remains to address #73232. I used #173537 as a starting point (for both code and tests), but attempted to follow the alternative design requested in the comments therein.

A quick design overview:

  • A SemaProxy interface is introduced to AST, which defines a set of (possibly mutable) semantic operations that can be triggered by the evaluation of certain expressions; this class finds its implementation in Sema/SemaEval.cpp. Sema creates a unique SemaProxy to itself at construction time, which is made available via Sema::getproxyForEval().
  • Ordinary evaluation (e.g., via Expr::EvaluateAsConstantExpr) has no access to SemaProxy, and therefore cannot act on the AST: instead, variants of these functions (e.g., Expr::EvaluateAsMandatedConstantExpr) are introduced, which accept a SemaProxy &. This makes clear from the call-side whether an operation can or cannot act on the AST (according to language semantic rules).
  • Within the constant evaluator(s), a pointer to SemaProxy is stored in interp::State; this is nullptr if evaluation began through a non-Mandated evaluation function (again, e.g., EvaluateAsConstantExpr). The proxy, if present, is leveraged during evaluation to call back through to Sema (e.g., to instantiate a specialization of a templated function).
  • Various callsites are updated to leverage the new Expr::EvaluateAsMandatedConstant* forms of evaluation. I'd appreciate help from reviewers in auditing whether I've made the correct choices here.

The tests are mostly from #173537, although I've added some to more explicitly cover certain kinds of manifestly constant-evaluated expressions (e.g., immediate invocations). I commented one test from #173537 that fails an assertion due to an unrelated issue (i.e., #199347). #17537 included some other changes to existing tests due to changes to diagnostic messages; I was able to avoid these changes by checking whether the template of a specialization has a definition, prior to trying to instantiate it (so the diagnostic continues to refer to "undefined function f<int>", rather than to an explicit specialization).

As with #173537, this PR neither addresses #59966 nor provides scaffolding for Reflection, though the scaffolding introduced here will help to address both use-cases. In particular, the availability of Sema through the constant evaluator will unblock the implementation of many of C++26 Reflection's metafunctions, which can explicitly act on the AST during evaluation.

It was brought to my attention that the Clang 23 branch is scheduled for a few weeks from now; seeing as this is a fairly significant architectural change, I have no intention to target that release.

Comment thread clang/lib/AST/ByteCode/Context.h Outdated
Comment thread clang/include/clang/AST/Decl.h Outdated
Comment thread clang/include/clang/AST/Decl.h Outdated
Comment thread clang/include/clang/AST/Decl.h Outdated
Comment thread clang/lib/AST/Decl.cpp Outdated
Comment on lines +2585 to +2588
isConstexpr() ?
Init->EvaluateAsMandatedConstantInitializer(EStatus, Ctx, *SP, this)
: Init->EvaluateAsInitializer(Ctx, this, EStatus,
IsConstantInitialization);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just passing a SemaProxy when there is one seems much easier...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wasn't happy how this particular line ended up either.

I pursued a design in which entrance to the constant evaluator is always via either

  • an evaluation function that does not affect the AST, which has no capacity to accept a SemaProxy, or
  • an evaluation function that can affect the AST, which must accept a SemaProxy.

Open to changing that design if others dislike how this ended up.

Comment thread clang/lib/Sema/Sema.cpp
Comment on lines 326 to +328
CurrentInstantiationScope(nullptr), NonInstantiationEntries(0),
ArgPackSubstIndex(std::nullopt), SatisfactionCache(Context) {
ArgPackSubstIndex(std::nullopt), SatisfactionCache(Context),
ProxyForEval(makeProxyForEval(*this)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would probably explode if the proxy is used before init - you probably rely on no one doing that/ Maybe that's fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - if you expand the diff upwards, there's precedent for passing *this to the initializers of other Sema subobjects. Seemed okay.

Comment on lines +7020 to +7025
SemaProxy *SP = Info.getSemaProxy();
if (SP && FunctionDefinitionCanBeLazilyInstantiated(FD) &&
Info.InConstantContext)
SP->instantiateFunctionDefinition(Loc, const_cast<FunctionDecl *>(FD));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, in your approach, do we still need Info.InConstantContext or is that true by construction and it should be an assert?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question - judging by the comment

Whether or not we're in a context where the front end requires a constant value.

we could probably get away with changing this to some State::inConstantContext() const that returns whether State::Sema is nullptr. I'd be fine either way.

What about external consumers of AST - we don't expect them to be able to pass a Sema, but maybe they still want to evaluate expressions in a constant context? Sort of at the edge of my clang knowledge here, so apologies if that's nonsense.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, Sema is a tool to construct the AST - once the AST is complete, Sema disappear.
Consumer of the AST get an immutable AST.

I do not think it will ever be possible to change that without very big redesign of the whole compiler.
i.e calling define_aggregate from lldb's expression evaluator will probably never work.

And I think that's perfectly fine.

But from there, it's not clear to me that State::Sema should imply inConstantContext() ( - Maybe it makes sense in your model. Does that mean we will never want to modify some sema state on overflow or constant folding? i don't know. I lack imagination either way

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to allow evaluation in a constant context without a Sema object. Instantiating function definitions is just the tip of the iceberg here -- reflection support adds a whole host of additional AST mutation operations that evaluation in a constant context can perform. The other option would be to add some kind of "evaluate in a constant context but without Sema" mode, that is not used by anything in-tree and would presumably bit-rot as a consequence. Speaking as a maintainer of an external project that uses Clang as a library (the Carbon toolchain) and does want to evaluate expressions, the burden of having a live Sema object to do this, for us, is just not a big deal.

The opposite direction, of allowing there to be a Sema proxy when not in a constant context, just seems fundamentally error-prone. Allowing that is precisely why I was concerned about prior approaches here -- it makes it too easy for code that just wants to perform a side-effect-free to result in leaking persistent side-effects into type-checking. I don't think there's no use cases that would benefit, but I do think the risk is likely to outweigh the benefit.

I think it would make sense to remove InConstantContext and instead have the invariant that we are in a constant context if and only if we have a proxy. I think we should also rename the proxy (SemaProxy is very vague) to something more like SemaConstantContextProxy to make it clearer that it encapsulates the things that Sema can be asked to do when evaluating in a constant context.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discuss removing InConstantContext, it would be helpful for me to understand how "stable" the API surface area of the constant evaluator must remain - what do we promise external users of these APIs? In particular:

  • Is it acceptable to remove theInConstantContext parameter from e.g., EvaluateAsRValue (which could break external users of that function)?
  • Is it acceptable to add to the set of overloads for a given name - for instance, would it be okay to add a new member function also named EvaluateAsRValue that accepts an additional SemaProxy argument (which could break external users that do &Expr::EvaluateAsRValue)?

Answers here will inform how the shape of these new constant evaluator "entry points" might look.

cc @cor3ntin @AaronBallman

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't promise the C++ API is stable; you can refactor it if you want.

// when the specialization is referenced in a context that requires a function
// definition to exist or if the existence of the definition affects the
// semantics of the program.
if (FunctionDefinitionCanBeLazilyInstantiated(Fn) && S.inConstantContext()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the same comment elsewhere, do we still need S.inConstantContext() ?

Comment thread clang/include/clang/Sema/Sema.h Outdated
Comment thread clang/lib/AST/ExprConstant.cpp
@cor3ntin cor3ntin added clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:bytecode Issues for the clang bytecode constexpr interpreter labels Jun 24, 2026
@llvmorg-github-actions llvmorg-github-actions Bot added the clang Clang issues not falling into any other category label Jun 24, 2026
@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-clang

Author: Daniel M. Katz (katzdm)

Changes

This PR is a rework of #173537, which was a rework of #115168; the sole goal remains to address #73232. I used #173537 as a starting point (for both code and tests), but attempted to follow the alternative design requested in the comments therein.

A quick design overview:

  • A SemaProxy interface is introduced to AST, which defines a set of (possibly mutable) semantic operations that can be triggered by the evaluation of certain expressions; this class finds its implementation in Sema/SemaEval.cpp. Sema creates a unique SemaProxy to itself at construction time, which is made available via Sema::getproxyForEval().
  • Ordinary evaluation (e.g., via Expr::EvaluateAsConstantExpr) has no access to SemaProxy, and therefore cannot act on the AST: instead, variants of these functions (e.g., Expr::EvaluateAsMandatedConstantExpr) are introduced, which accept a SemaProxy &amp;. This makes clear from the call-side whether an operation can or cannot act on the AST (according to language semantic rules).
  • Within the constant evaluator(s), a pointer to SemaProxy is stored in interp::State; this is nullptr if evaluation began through a non-Mandated evaluation function (again, e.g., EvaluateAsConstantExpr). The proxy, if present, is leveraged during evaluation to call back through to Sema (e.g., to instantiate a specialization of a templated function).
  • Various callsites are updated to leverage the new Expr::EvaluateAsMandatedConstant* forms of evaluation. I'd appreciate help from reviewers in auditing whether I've made the correct choices here.

The tests are mostly from #173537, although I've added some to more explicitly cover certain kinds of manifestly constant-evaluated expressions (e.g., immediate invocations). I commented one test from #173537 that fails an assertion due to an unrelated issue (i.e., #199347). #17537 included some other changes to existing tests due to changes to diagnostic messages; I was able to avoid these changes by checking whether the template of a specialization has a definition, prior to trying to instantiate it (so the diagnostic continues to refer to "undefined function f&lt;int&gt;", rather than to an explicit specialization).

As with #173537, this PR neither addresses #59966 nor provides scaffolding for Reflection, though the scaffolding introduced here will help to address both use-cases. In particular, the availability of Sema through the constant evaluator will unblock the implementation of many of C++26 Reflection's metafunctions, which can explicitly act on the AST during evaluation.

It was brought to my attention that the Clang 23 branch is scheduled for a few weeks from now; seeing as this is a fairly significant architectural change, I have no intention to target that release.


Patch is 65.01 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/205557.diff

27 Files Affected:

  • (modified) clang/docs/ReleaseNotes.rst (+1)
  • (modified) clang/include/clang/AST/ASTContext.h (+2-1)
  • (modified) clang/include/clang/AST/Decl.h (+12-1)
  • (modified) clang/include/clang/AST/Expr.h (+20)
  • (added) clang/include/clang/AST/SemaProxy.h (+40)
  • (modified) clang/include/clang/Sema/Sema.h (+26)
  • (modified) clang/lib/AST/ASTContext.cpp (+4-2)
  • (modified) clang/lib/AST/ByteCode/Context.cpp (+2-1)
  • (modified) clang/lib/AST/ByteCode/Context.h (+6-1)
  • (modified) clang/lib/AST/ByteCode/Interp.cpp (+23-9)
  • (modified) clang/lib/AST/ByteCode/InterpState.cpp (+6-6)
  • (modified) clang/lib/AST/ByteCode/State.h (+5-2)
  • (modified) clang/lib/AST/Decl.cpp (+10-4)
  • (modified) clang/lib/AST/ExprConstShared.h (+4)
  • (modified) clang/lib/AST/ExprConstant.cpp (+233-96)
  • (modified) clang/lib/Sema/CMakeLists.txt (+1)
  • (modified) clang/lib/Sema/Sema.cpp (+2-1)
  • (modified) clang/lib/Sema/SemaConcept.cpp (+2-2)
  • (modified) clang/lib/Sema/SemaDecl.cpp (+3-2)
  • (modified) clang/lib/Sema/SemaDeclCXX.cpp (+2-2)
  • (added) clang/lib/Sema/SemaEval.cpp (+48)
  • (modified) clang/lib/Sema/SemaExpr.cpp (+8-5)
  • (modified) clang/lib/Sema/SemaOverload.cpp (+2-1)
  • (modified) clang/test/SemaCXX/constexpr-late-instantiation.cpp (+225-2)
  • (modified) clang/unittests/AST/ByteCode/Descriptor.cpp (+1-1)
  • (modified) clang/unittests/AST/ByteCode/Pointer.cpp (+2-2)
  • (modified) clang/unittests/AST/ByteCode/toAPValue.cpp (+4-4)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 42c5dc16ea2e1..d27ba186a98df 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -804,6 +804,7 @@ Bug Fixes to C++ Support
 - Fixed a crash in constant evaluation using placement new on an array which was later initialized. (#GH196450)
 - Fixed an issue where Clang incorrectly accepted invalid unqualified uses of local nested class names outside their declaring scope. (#GH184622)
 - Fixed a crash when parsing invalid friend declaration with storage-class specifier. (#GH186569)
+- Instantiate constexpr functions as needed before they are evaluated. (#GH73232) (#GH35052) (#GH100897)
 
 Bug Fixes to AST Handling
 ^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/clang/include/clang/AST/ASTContext.h b/clang/include/clang/AST/ASTContext.h
index a4ed852d36442..6c157cc8755a5 100644
--- a/clang/include/clang/AST/ASTContext.h
+++ b/clang/include/clang/AST/ASTContext.h
@@ -129,6 +129,7 @@ class ParentMapContext;
 struct ParsedTargetAttr;
 class Preprocessor;
 class ProfileList;
+class SemaProxy;
 class StoredDeclsMap;
 class TargetAttr;
 class TargetInfo;
@@ -808,7 +809,7 @@ class ASTContext : public RefCountedBase<ASTContext> {
   ASTMutationListener *Listener = nullptr;
 
   /// Returns the clang bytecode interpreter context.
-  interp::Context &getInterpContext() const;
+  interp::Context &getInterpContext(SemaProxy *Sema) const;
 
   struct CUDAConstantEvalContext {
     /// Do not allow wrong-sided variables in constant expressions.
diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h
index e200b8f06ec4b..8c3effac13255 100644
--- a/clang/include/clang/AST/Decl.h
+++ b/clang/include/clang/AST/Decl.h
@@ -70,6 +70,7 @@ class Module;
 class NamespaceDecl;
 class ParmVarDecl;
 class RecordDecl;
+class SemaProxy;
 class Stmt;
 class StringLiteral;
 class TagDecl;
@@ -1435,6 +1436,7 @@ class VarDecl : public DeclaratorDecl, public Redeclarable<VarDecl> {
 
 private:
   APValue *evaluateValueImpl(SmallVectorImpl<PartialDiagnosticAt> *Notes,
+                             SemaProxy *SP,
                              bool IsConstantInitialization) const;
 
 public:
@@ -1451,6 +1453,15 @@ class VarDecl : public DeclaratorDecl, public Redeclarable<VarDecl> {
   ///         not.
   bool evaluateDestruction(SmallVectorImpl<PartialDiagnosticAt> &Notes) const;
 
+  /// Evaluate the destruction of this variable, the destruction of which is
+  /// required by language rules to be constant.
+  ///
+  /// \pre hasConstantInitialization()
+  /// \return \c true if this variable has constant destruction, \c false if
+  ///         not.
+  bool evaluateMandatedConstantDestruction(
+      SmallVectorImpl<PartialDiagnosticAt> &Notes, SemaProxy &SP) const;
+
   /// Determine whether this variable has constant initialization.
   ///
   /// This is only set in two cases: when the language semantics require
@@ -1468,7 +1479,7 @@ class VarDecl : public DeclaratorDecl, public Redeclarable<VarDecl> {
   /// constant initializer. Should only be called once, after completing the
   /// definition of the variable.
   bool checkForConstantInitialization(
-      SmallVectorImpl<PartialDiagnosticAt> &Notes) const;
+      SmallVectorImpl<PartialDiagnosticAt> &Notes, SemaProxy *SP) const;
 
   void setInitStyle(InitializationStyle Style) {
     VarDeclBits.InitStyle = Style;
diff --git a/clang/include/clang/AST/Expr.h b/clang/include/clang/AST/Expr.h
index 7c94c4d35641c..6ca1eb96b651a 100644
--- a/clang/include/clang/AST/Expr.h
+++ b/clang/include/clang/AST/Expr.h
@@ -55,6 +55,7 @@ namespace clang {
   class ObjCPropertyRefExpr;
   class OpaqueValueExpr;
   class ParmVarDecl;
+  class SemaProxy;
   class StringLiteral;
   class TargetInfo;
   class ValueDecl;
@@ -667,6 +668,13 @@ class Expr : public ValueStmt {
   bool EvaluateAsRValue(EvalResult &Result, const ASTContext &Ctx,
                         bool InConstantContext = false) const;
 
+  /// Evaluate an expression that is required by the language to be a constant
+  /// expression, and fold the resulting rvalue constant into Result. If the
+  /// expression is a glvalue, an lvalue-to-rvalue conversion will be applied.
+  bool EvaluateAsMandatedConstantRValue(EvalResult &Result,
+                                        const ASTContext &Ctx,
+                                        SemaProxy &SP) const;
+
   /// EvaluateAsBooleanCondition - Return true if this is a constant
   /// which we can fold and convert to a boolean condition using
   /// any crazy technique that we want to, even if the expression has
@@ -743,6 +751,12 @@ class Expr : public ValueStmt {
                              EvalResult &Result,
                              bool IsConstantInitializer) const;
 
+  /// Evaluate an expression that is required by the language to be a constant
+  /// expression.
+  bool EvaluateAsMandatedConstantInitializer(
+      EvalResult &Result, const ASTContext &Ctx, SemaProxy &SP,
+      const VarDecl *VD) const;
+
   /// EvaluateWithSubstitution - Evaluate an expression as if from the context
   /// of a call to the given function with the given arguments, inside an
   /// unevaluated context. Returns true if the expression could be folded to a
@@ -773,6 +787,12 @@ class Expr : public ValueStmt {
       EvalResult &Result, const ASTContext &Ctx,
       ConstantExprKind Kind = ConstantExprKind::Normal) const;
 
+  /// Evaluate an expression that is required by the language to be a constant
+  /// expression.
+  bool EvaluateAsMandatedConstantExpr(
+      EvalResult &Result, const ASTContext &Ctx, SemaProxy &SP,
+      ConstantExprKind Kind = ConstantExprKind::Normal) const;
+
   /// If the current Expr is a pointer, this will try to statically
   /// determine the number of bytes available where the pointer is pointing.
   /// Returns true if all of the above holds and we were able to figure out the
diff --git a/clang/include/clang/AST/SemaProxy.h b/clang/include/clang/AST/SemaProxy.h
new file mode 100644
index 0000000000000..ab6a87f1e32a5
--- /dev/null
+++ b/clang/include/clang/AST/SemaProxy.h
@@ -0,0 +1,40 @@
+//===--- SemaProxy.h - Interface to language semantics ---------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+//  This file defines the SemaProxy interface, used during language-mandated
+//  constant evaluation to act on, and query, the representation of the program
+//  according to language-defined semantics.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_AST_SEMA_PROXY_H
+#define LLVM_CLANG_AST_SEMA_PROXY_H
+
+#include "clang/Basic/SourceLocation.h"
+
+namespace clang {
+
+class FunctionDecl;
+
+/// Classes implementing SemaProxy present a restricted view of the (possibly
+/// mutating) actions and queries defined by language semantics against the
+/// representation of the program (i.e., the AST). Such a view is required in
+/// order to evaluate certain expressions (e.g., C++'s manifestly
+/// constant-evaluated expressions) according to language rules.
+class SemaProxy {
+public:
+  virtual ~SemaProxy() = default;
+
+  virtual void
+  instantiateFunctionDefinition(SourceLocation PointOfInstantiation,
+                                FunctionDecl *Function) = 0;
+};
+
+} // end namespace clang
+
+#endif // LLVM_CLANG_AST_SEMA_PROXY_H
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index b8d760e7e0975..97b951bf6d672 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -30,6 +30,7 @@
 #include "clang/AST/ExternalASTSource.h"
 #include "clang/AST/NestedNameSpecifier.h"
 #include "clang/AST/OperationKinds.h"
+#include "clang/AST/SemaProxy.h"
 #include "clang/AST/StmtCXX.h"
 #include "clang/AST/Type.h"
 #include "clang/AST/TypeLoc.h"
@@ -907,6 +908,7 @@ class Sema final : public SemaBase {
   // 33. Types (SemaType.cpp)
   // 34. FixIt Helpers (SemaFixItUtils.cpp)
   // 35. Function Effects (SemaFunctionEffects.cpp)
+  // 36. Language-Mandated Constant Evaluation (SemaEval.cpp)
 
   /// \name Semantic Analysis
   /// Implementations are in Sema.cpp
@@ -15811,6 +15813,30 @@ class Sema final : public SemaBase {
   void performFunctionEffectAnalysis(TranslationUnitDecl *TU);
 
   ///@}
+
+  //
+  //
+  // -------------------------------------------------------------------------
+  //
+  //
+
+  /// \name Language-Mandated Constant Evaluation
+  /// Implementations are in SemaEval.cpp
+  ///@{
+public:
+  SemaProxy &getProxyForEval() const {
+    assert(ProxyForEval);
+    return *ProxyForEval;
+  }
+
+private:
+  std::unique_ptr<SemaProxy> ProxyForEval;
+
+  static SemaProxy *makeProxyForEval(Sema &SemaRef);
+
+  ///@}
+public:
+
 };
 
 DeductionFailureInfo
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index abf0cd5e18c2b..f58e7c394ad18 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -40,6 +40,7 @@
 #include "clang/AST/ParentMapContext.h"
 #include "clang/AST/RawCommentList.h"
 #include "clang/AST/RecordLayout.h"
+#include "clang/AST/SemaProxy.h"
 #include "clang/AST/Stmt.h"
 #include "clang/AST/TemplateBase.h"
 #include "clang/AST/TemplateName.h"
@@ -901,9 +902,10 @@ CXXABI *ASTContext::createCXXABI(const TargetInfo &T) {
   llvm_unreachable("Invalid CXXABI type!");
 }
 
-interp::Context &ASTContext::getInterpContext() const {
+interp::Context &ASTContext::getInterpContext(SemaProxy *Sema) const {
   if (!InterpContext) {
-    InterpContext.reset(new interp::Context(const_cast<ASTContext &>(*this)));
+    InterpContext.reset(
+        new interp::Context(const_cast<ASTContext &>(*this), Sema));
   }
   return *InterpContext;
 }
diff --git a/clang/lib/AST/ByteCode/Context.cpp b/clang/lib/AST/ByteCode/Context.cpp
index 4beb35a9a7b43..eaf077a3eae92 100644
--- a/clang/lib/AST/ByteCode/Context.cpp
+++ b/clang/lib/AST/ByteCode/Context.cpp
@@ -26,7 +26,8 @@
 using namespace clang;
 using namespace clang::interp;
 
-Context::Context(ASTContext &Ctx) : Ctx(Ctx), P(new Program(*this)) {
+Context::Context(ASTContext &Ctx, SemaProxy *Sema)
+    : Ctx(Ctx), Sema(Sema), P(new Program(*this)) {
   this->ShortWidth = Ctx.getTargetInfo().getShortWidth();
   this->IntWidth = Ctx.getTargetInfo().getIntWidth();
   this->LongWidth = Ctx.getTargetInfo().getLongWidth();
diff --git a/clang/lib/AST/ByteCode/Context.h b/clang/lib/AST/ByteCode/Context.h
index 789f72ae34f73..085883b1d729d 100644
--- a/clang/lib/AST/ByteCode/Context.h
+++ b/clang/lib/AST/ByteCode/Context.h
@@ -25,6 +25,7 @@ class FunctionDecl;
 class VarDecl;
 class APValue;
 class BlockExpr;
+class SemaProxy;
 
 namespace interp {
 class Function;
@@ -47,7 +48,7 @@ class EvalIDScope;
 class Context final {
 public:
   /// Initialises the constexpr VM.
-  explicit Context(ASTContext &Ctx);
+  explicit Context(ASTContext &Ctx, SemaProxy *Sema);
 
   /// Cleans up the constexpr VM.
   ~Context();
@@ -99,6 +100,8 @@ class Context final {
 
   /// Returns the AST context.
   ASTContext &getASTContext() const { return Ctx; }
+  /// Returns the (possibly null) pointer to the language semantics proxy.
+  SemaProxy *getSemaProxy() const { return Sema; }
   /// Returns the language options.
   const LangOptions &getLangOpts() const;
   /// Returns CHAR_BIT.
@@ -191,6 +194,8 @@ class Context final {
 
   /// Current compilation context.
   ASTContext &Ctx;
+  /// Current proxy to language semantics.
+  SemaProxy *Sema;
   /// Interpreter stack, shared across invocations.
   InterpStack Stk;
   /// Constexpr program.
diff --git a/clang/lib/AST/ByteCode/Interp.cpp b/clang/lib/AST/ByteCode/Interp.cpp
index 3772def47408f..13de84a26eedc 100644
--- a/clang/lib/AST/ByteCode/Interp.cpp
+++ b/clang/lib/AST/ByteCode/Interp.cpp
@@ -21,6 +21,7 @@
 #include "clang/AST/DeclObjC.h"
 #include "clang/AST/Expr.h"
 #include "clang/AST/ExprCXX.h"
+#include "clang/AST/SemaProxy.h"
 #include "clang/Basic/DiagnosticSema.h"
 #include "clang/Basic/TargetInfo.h"
 #include "llvm/ADT/StringExtras.h"
@@ -1776,15 +1777,28 @@ bool CheckBitCast(InterpState &S, CodePtr OpPC, const Type *TargetType,
   return true;
 }
 
-static void compileFunction(InterpState &S, const Function *Func) {
-  const FunctionDecl *Definition;
-  if (!Func->getDecl()->getBody(Definition))
-    return;
-  if (!Definition)
+static void compileFunction(InterpState &S, const Function *Func,
+                            CodePtr OpPC) {
+  const FunctionDecl *Fn = Func->getDecl();
+
+  // [C++26] [temp.inst] p5
+  // [...] the function template specialization is implicitly instantiated
+  // when the specialization is referenced in a context that requires a function
+  // definition to exist or if the existence of the definition affects the
+  // semantics of the program.
+  if (FunctionDefinitionCanBeLazilyInstantiated(Fn) && S.inConstantContext()) {
+    SemaProxy *SP = S.getSemaProxy();
+    if (!SP)
+      return;
+    SP->instantiateFunctionDefinition(S.Current->getLocation(OpPC),
+                                      const_cast<FunctionDecl *>(Fn));
+  }
+  Fn = Fn->getDefinition();
+  if (!Fn)
     return;
 
   Compiler<ByteCodeEmitter>(S.getContext(), S.P)
-      .compileFunc(Definition, const_cast<Function *>(Func));
+      .compileFunc(Fn, const_cast<Function *>(Func));
 }
 
 bool CallVar(InterpState &S, CodePtr OpPC, const Function *Func,
@@ -1811,7 +1825,7 @@ bool CallVar(InterpState &S, CodePtr OpPC, const Function *Func,
   }
 
   if (!Func->isFullyCompiled())
-    compileFunction(S, Func);
+    compileFunction(S, Func, OpPC);
 
   if (!CheckCallable(S, OpPC, Func))
     return false;
@@ -1898,7 +1912,7 @@ bool Call(InterpState &S, CodePtr OpPC, const Function *Func,
   }
 
   if (!Func->isFullyCompiled())
-    compileFunction(S, Func);
+    compileFunction(S, Func, OpPC);
 
   if (!CheckCallable(S, OpPC, Func))
     return cleanup();
@@ -2316,7 +2330,7 @@ bool CallPtr(InterpState &S, CodePtr OpPC, uint32_t ArgSize,
   // because the Call/CallVirt below might access the instance pointer
   // but the Function's information about them is wrong.
   if (!F->isFullyCompiled())
-    compileFunction(S, F);
+    compileFunction(S, F, OpPC);
 
   if (!CheckCallable(S, OpPC, F))
     return false;
diff --git a/clang/lib/AST/ByteCode/InterpState.cpp b/clang/lib/AST/ByteCode/InterpState.cpp
index 2d6ed98e6b52c..ac6d45df2b9e6 100644
--- a/clang/lib/AST/ByteCode/InterpState.cpp
+++ b/clang/lib/AST/ByteCode/InterpState.cpp
@@ -19,9 +19,9 @@ using namespace clang::interp;
 
 InterpState::InterpState(const State &Parent, Program &P, InterpStack &Stk,
                          Context &Ctx, SourceMapper *M)
-    : State(Ctx.getASTContext(), Parent.getEvalStatus()), M(M), P(P), Stk(Stk),
-      Ctx(Ctx), BottomFrame(*this), Current(&BottomFrame),
-      StepsLeft(Ctx.getLangOpts().ConstexprStepLimit),
+    : State(Ctx.getASTContext(), Parent.getSemaProxy(), Parent.getEvalStatus()),
+      M(M), P(P), Stk(Stk), Ctx(Ctx), BottomFrame(*this),
+      Current(&BottomFrame), StepsLeft(Ctx.getLangOpts().ConstexprStepLimit),
       InfiniteSteps(StepsLeft == 0), EvalID(Ctx.getEvalID()) {
   InConstantContext = Parent.InConstantContext;
   CheckingPotentialConstantExpression =
@@ -32,9 +32,9 @@ InterpState::InterpState(const State &Parent, Program &P, InterpStack &Stk,
 
 InterpState::InterpState(const State &Parent, Program &P, InterpStack &Stk,
                          Context &Ctx, const Function *Func)
-    : State(Ctx.getASTContext(), Parent.getEvalStatus()), M(nullptr), P(P),
-      Stk(Stk), Ctx(Ctx), BottomFrame(*this), Current(&BottomFrame),
-      StepsLeft(Ctx.getLangOpts().ConstexprStepLimit),
+    : State(Ctx.getASTContext(), Parent.getSemaProxy(), Parent.getEvalStatus()),
+      M(nullptr), P(P), Stk(Stk), Ctx(Ctx), BottomFrame(*this),
+      Current(&BottomFrame), StepsLeft(Ctx.getLangOpts().ConstexprStepLimit),
       InfiniteSteps(StepsLeft == 0), EvalID(Ctx.getEvalID()) {
   InConstantContext = Parent.InConstantContext;
   CheckingPotentialConstantExpression =
diff --git a/clang/lib/AST/ByteCode/State.h b/clang/lib/AST/ByteCode/State.h
index df3afdf8cbc24..e9e94d4ffeb9b 100644
--- a/clang/lib/AST/ByteCode/State.h
+++ b/clang/lib/AST/ByteCode/State.h
@@ -20,6 +20,7 @@
 
 namespace clang {
 class OptionalDiagnostic;
+class SemaProxy;
 
 /// Kinds of access we can perform on an object, for diagnostics. Note that
 /// we consider a member function call to be a kind of access, even though
@@ -80,8 +81,8 @@ class SourceInfo;
 /// Interface for the VM to interact with the AST walker's context.
 class State {
 public:
-  State(ASTContext &ASTCtx, Expr::EvalStatus &EvalStatus)
-      : Ctx(ASTCtx), EvalStatus(EvalStatus) {}
+  State(ASTContext &ASTCtx, SemaProxy *Sema, Expr::EvalStatus &EvalStatus)
+      : Ctx(ASTCtx), Sema(Sema), EvalStatus(EvalStatus) {}
   virtual ~State();
 
   virtual const Frame *getCurrentFrame() = 0;
@@ -90,6 +91,7 @@ class State {
 
   Expr::EvalStatus &getEvalStatus() const { return EvalStatus; }
   ASTContext &getASTContext() const { return Ctx; }
+  SemaProxy *getSemaProxy() const { return Sema; }
   const LangOptions &getLangOpts() const { return Ctx.getLangOpts(); }
 
   /// Note that we have had a side-effect, and determine whether we should
@@ -188,6 +190,7 @@ class State {
 
   EvaluationMode EvalMode;
   ASTContext &Ctx;
+  SemaProxy *Sema;
   Expr::EvalStatus &EvalStatus;
 
 private:
diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp
index 7ab4235717dde..0382a26f0f692 100644
--- a/clang/lib/AST/Decl.cpp
+++ b/clang/lib/AST/Decl.cpp
@@ -32,6 +32,7 @@
 #include "clang/AST/Randstruct.h"
 #include "clang/AST/RecordLayout.h"
 #include "clang/AST/Redeclarable.h"
+#include "clang/AST/SemaProxy.h"
 #include "clang/AST/Stmt.h"
 #include "clang/AST/TemplateBase.h"
 #include "clang/AST/Type.h"
@@ -2552,10 +2553,12 @@ EvaluatedStmt *VarDecl::getEvaluatedStmt() const {
 }
 
 APValue *VarDecl::evaluateValue() const {
-  return evaluateValueImpl(/*Notes=*/nullptr, hasConstantInitialization());
+  return evaluateValueImpl(/*Notes=*/nullptr, /*Sema=*/nullptr,
+                           hasConstantInitialization());
 }
 
 APValue *VarDecl::evaluateValueImpl(SmallVectorImpl<PartialDiagnosticAt> *Notes,
+                                    SemaProxy *SP,
                                     bool IsConstantInitialization) const {
   EvaluatedStmt *Eval = ensureEvaluatedStmt();
 
@@ -2579,7 +2582,10 @@ APValue *VarDecl::evaluateValueImpl(SmallVectorImpl<PartialDiagnosticAt> *Notes,
   Expr::EvalResult EStatus;
   EStatus.Diag = Notes;
   bool Result =
-      Init->EvaluateAsInitializer(Ctx, this, EStatus, IsConstantInitialization);
+      isConstexpr() ?
+        Init->EvaluateAsMandatedConstantInitializer(EStatus, Ctx, *SP, this)
+      : Init->EvaluateAsInitializer(Ctx, this, EStatus,
+                                    IsConstantInitialization);
   Eval->Evaluated = std::move(EStatus.Val);
 
   // In C++, or in C23 if we're initialising a 'constexpr' variable, this isn't
@@ -2643,7 +2649,7 @@ bool VarDecl::hasConstantInitialization() const {
 }
 
 bool VarDecl::checkForConstantInitialization(
-    SmallVectorImpl<PartialDiagnosticAt> &Notes) const {
+    SmallVectorImpl<PartialDiagnosticAt> &Notes, SemaProxy *SP) const {
   EvaluatedStmt *Eval = ensureEvaluatedStmt();
   // If we ask for the value before we know whether we have a constant
   // initializer, we can compute the wrong value (for example, due to
@@ -2658,7 +2664,7 @@ bool VarDecl::checkForConstantInitialization(
 
   // Evaluate the initializer to check whether it's a constant expression.
   Eval->HasConstantInitialization =
-      evaluateValueImpl(&Notes, true) && Notes.empty();
+      evaluateValueImpl(&Notes, SP, true) && Notes.empty();
 
   // If evaluation as a constant initializer failed, allow re-evaluation as a
   // non-constant initializer if we later find we want the value.
diff --git a/clang/lib/AST/ExprConstShared.h b/clang/lib/AST/ExprConstShared.h
index 619c79a1408f3..45c7db846fc70 100644
--- a/clang/lib/AST/ExprConstShared.h
+++ b/clang/lib/AST/ExprConstShared.h
@@ -29,6 +29,7 @@ class LangOptions;
 class ASTContext;
 class CharUni...
[truncated]

@katzdm
katzdm force-pushed the third-time-is-the-charm branch from 2074fb5 to 67c2baf Compare June 24, 2026 20:51
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@katzdm
katzdm requested review from cor3ntin and tbaederr June 24, 2026 22:48
Comment thread clang/lib/AST/ByteCode/Interp.cpp Outdated
Comment thread clang/lib/Sema/SemaDecl.cpp
@katzdm
katzdm force-pushed the third-time-is-the-charm branch from 1e30611 to 2b5b4a1 Compare June 26, 2026 13:07
@katzdm

katzdm commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

A few open questions for reviewers:

First - Is there any sense for which version of C++ we would like to backport this to? I think at least '23, but have little sense as to whether we'd like it to apply back further. Per discussion with maintainers, we plan to just enable this for every version of C++.

Second - Now that the suggested API is in front of us - how do we feel about its shape? It achieves the "bright line visible at every call" that can mutate the AST (as asked for by @zygoloid). It's also nice that it avoids changes to the existing APIs (e.g., EvaluateAsConstantExpr), which would be obnoxiously breaking for external consumers of the AST library, but it does so by doubling the surface area of the constant evaluator API.

One other idea could be to pursue something like what @cor3ntin tried (i.e., shove the SemaProxy in the ASTContext), but set and unset the proxy via RAII object at the evaluation call-site, rather than leave it accessible throughout translation; something like:

// e.g., SemaDecl.cpp
bool HasConstInit;
SmallVector<PartialDiagnosticAt, 8> Notes;
{
  ManifestlyConstantEvaluatedContext mcce(Context, getProxyForEval());
  HasConstInit = var->checkForConstantInitialization(Notes);
}

What I like about this direction is that it ties the call-site more closely to the language reason for allowing side-effects: The expression is manifestly constant-evaluated, and it is clear from the call-site that we're evaluating it as such. My concern with the existing direction is that identifying which evaluations are side-effectual requires remembering which 8 or so evaluation-functions allow side-effects, and which don't - which feels like a high-ish cognitive load. Finding side-effectual evaluations could instead be a matter of greping for ManifestlyConstantEvaluatedContext; confirming whether a callsite permits side-effects is a matter of confirming the presence or absence of such a context.

Alternatively, we could tie this into the existing ExpressionEvaluationContext machinery in Sema; Sema could just set/restore the SemaProxy in the ASTContext whenever an ExpressionEvaluationContext::ManifestlyConstantEvaluated record is added/removed. So just:

// e.g., SemaDecl.cpp
bool HasConstInit;
SmallVector<PartialDiagnosticAt, 8> Notes;
{
  EnterExpressionEvaluationContext(*this, ExpressionEvaluationContext::ManifestlyConstant);
  HasConstInit = var->checkForConstantInitialization(Notes);
}

Something like this was admittedly also present in Corentin's design (which checked for Info.InConstantContext in ExprConstant.cpp before attempting instantiation), though perhaps this direction is more robust against error, since it keeps SemaProxy out of the evaluator entirely, unless opted into at the callsite.

Happy to stick with the existing direction or change course, whatever folks feel is best.

@zygoloid zygoloid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach here makes sense to me, and addresses my concerns with prior approaches for this issue. Thanks!

Comment on lines +7020 to +7025
SemaProxy *SP = Info.getSemaProxy();
if (SP && FunctionDefinitionCanBeLazilyInstantiated(FD) &&
Info.InConstantContext)
SP->instantiateFunctionDefinition(Loc, const_cast<FunctionDecl *>(FD));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to allow evaluation in a constant context without a Sema object. Instantiating function definitions is just the tip of the iceberg here -- reflection support adds a whole host of additional AST mutation operations that evaluation in a constant context can perform. The other option would be to add some kind of "evaluate in a constant context but without Sema" mode, that is not used by anything in-tree and would presumably bit-rot as a consequence. Speaking as a maintainer of an external project that uses Clang as a library (the Carbon toolchain) and does want to evaluate expressions, the burden of having a live Sema object to do this, for us, is just not a big deal.

The opposite direction, of allowing there to be a Sema proxy when not in a constant context, just seems fundamentally error-prone. Allowing that is precisely why I was concerned about prior approaches here -- it makes it too easy for code that just wants to perform a side-effect-free to result in leaking persistent side-effects into type-checking. I don't think there's no use cases that would benefit, but I do think the risk is likely to outweigh the benefit.

I think it would make sense to remove InConstantContext and instead have the invariant that we are in a constant context if and only if we have a proxy. I think we should also rename the proxy (SemaProxy is very vague) to something more like SemaConstantContextProxy to make it clearer that it encapsulates the things that Sema can be asked to do when evaluating in a constant context.

Comment thread clang/test/SemaCXX/constexpr-late-instantiation.cpp Outdated
zyn0217 added a commit that referenced this pull request Jul 15, 2026
…ates (#209693)

This fixes another case of member functions where we overlooked template
depths when only abbreviated template parameters are involved.

This mirrors previous fix cfb2520, but I don't intend to put it in
ParseTrailingRequiresClause because we might want the similar fix for
e.g. noexcept expressions, so let's keep it inline for future refactor.

The example comes from #205557.
pedroMVicente pushed a commit to pedroMVicente/llvm-project that referenced this pull request Jul 15, 2026
…ates (llvm#209693)

This fixes another case of member functions where we overlooked template
depths when only abbreviated template parameters are involved.

This mirrors previous fix cfb2520, but I don't intend to put it in
ParseTrailingRequiresClause because we might want the similar fix for
e.g. noexcept expressions, so let's keep it inline for future refactor.

The example comes from llvm#205557.
Comment thread clang/lib/AST/Decl.cpp
EStatus.Diag = Notes;
bool Result =
Init->EvaluateAsInitializer(Ctx, this, EStatus, IsConstantInitialization);
isConstexpr()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isConstexpr() doesn't seem right.

Consider a global variable definition like the following:

int a = f();

If f() is constexpr, we need to instantiate it: we need the instantiated function body to determine whether the variable is constant-initialized. even though it's not actually an error if the resulting expression isn't constant. At least, that's my understanding of the rules here.

dyung pushed a commit to llvmbot/llvm-project that referenced this pull request Jul 16, 2026
…ates (llvm#209693)

This fixes another case of member functions where we overlooked template
depths when only abbreviated template parameters are involved.

This mirrors previous fix cfb2520, but I don't intend to put it in
ParseTrailingRequiresClause because we might want the similar fix for
e.g. noexcept expressions, so let's keep it inline for future refactor.

The example comes from llvm#205557.

(cherry picked from commit 3485d85)
@jyknight

Copy link
Copy Markdown
Member

Since, with the current PR, you only can see issues in edge-cases (e.g. where the constexpr template is defined after it's mentioned by a constexpr function), in order to better test all the behaviors, I've deleted the eager instantiation via the following patch, and then run ninja check-clang.

--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -19152,11 +19152,6 @@ void Sema::MarkFunctionReferenced(SourceLocation Loc, FunctionDecl *Func,
               CodeSynthesisContexts.size())
             PendingLocalImplicitInstantiations.push_back(
                 std::make_pair(Func, PointOfInstantiation));
-          else if (Func->isConstexpr())
-            // Do not defer instantiations of constexpr functions, to avoid the
-            // expression evaluator needing to call back into Sema if it sees a
-            // call to such a function.
-            InstantiateFunctionDefinition(PointOfInstantiation, Func);
           else {
             Func->setInstantiationIsPending(true);
             PendingInstantiations.push_back(

IIUC, with a correct change here, lazy instantiation should be triggered in all cases where it's necessary, and so deleting the above should only change some of the notes around where instantiation was triggered, but not otherwise break any code.

However, that's not the case. There's a lot of errors reported. Looking at them, I see at least the following seemingly-distinct issues:

  • annotate attribute doesn't instantiate when evaluating its (required-constant) argument
  • enable_if attribute doesn't instantiate when evaluating its (required-constant) argument
  • non-constexpr globals don't instantiate (as mentioned in previous review comment)
  • There's some weird issue where a "friend" declaration with explicit template-args gets in the way of finding the actual definition of constexpr function to instantiate.
  • narrowing conversion in braces or initializer-list is not being permitted when it should be (only allowed for constants, and without instantiation we can't tell if we have a constant)
  • __builtin_matrix_column_major_load "must be a constant unsigned integer expression"
  • Some issues with "immediate function {} used before it is defined" related to consteval functions.
  • Also, some crashes in the -fexperimental-new-constant-interpreter mode.

@cor3ntin

Copy link
Copy Markdown
Contributor

@jyknight right, there are more work to be done to fix all of the issues - i do not think it needs to be in that PR - they are related but distinct issues

dyung pushed a commit to llvmbot/llvm-project that referenced this pull request Jul 22, 2026
…ates (llvm#209693)

This fixes another case of member functions where we overlooked template
depths when only abbreviated template parameters are involved.

This mirrors previous fix cfb2520, but I don't intend to put it in
ParseTrailingRequiresClause because we might want the similar fix for
e.g. noexcept expressions, so let's keep it inline for future refactor.

The example comes from llvm#205557.

(cherry picked from commit 3485d85)
@efriedma-quic

Copy link
Copy Markdown
Contributor

It shouldn't all be in the same PR, sure, but do we need to fix the issues before we enable this by default? It sounds like the errors could be triggered by code which previously compiled successfully.

@jyknight

Copy link
Copy Markdown
Member

I don't really agree they are distinct issues. (Or, at least, the ones I mentioned don't seem so. Certainly there could also be some independent issues in there).

While the diff disabling eager instantiation I used for testing doesn't necessarily need to be part of this PR, I believe the failures detected by testing with it are pointing out actual bugs which this PR has as-is. And that carefully crafting some slightly more-complex test-cases would also show those same failures, without that diff.

This PR's intent is to instantiate constexpr functions on-demand when it's required. It adds new APIs and updates all the appropriate callers to use those new APIs. Except: it's only updated some of the required callers, not all of them.

We could submit without addressing the bugs in this PR first, but ISTM it probably isn't that hard (by using this testing strategy), to find and update the rest of the identified callers that need to be able to do instantiation, to call the new API too?

Also, the "friend" failure might be pointing out that the instantiation code in TryInstantiateFunctionBeforeCall is buggy by trying to instantiate the friend-decl instead of the actual definition, or something like that. (Does it need to search the redecls chain to find the right thing to instantiate or something? I don't really know.)

@cor3ntin

Copy link
Copy Markdown
Contributor

We are trying to establish the mechanism to instantiate functions/mutate the ast from constant evaluation - which turn out to be more contentious than anyone hoped for.

I would prefer we focus on that, and then fix the code you commented. That code was always load bearing to a series of workaround. I agree it should go. But maybe we can make incremental progress?

it's the 3rd PR in 3 years trying to gain consensus here.

@efriedma-quic

Copy link
Copy Markdown
Contributor

Can we put this behind a flag for now? I don't want to block progress here, but it also seems likely to cause problems, and I don't want it bouncing in and out of the tree.

@AaronBallman

Copy link
Copy Markdown
Contributor

Can we put this behind a flag for now? I don't want to block progress here, but it also seems likely to cause problems, and I don't want it bouncing in and out of the tree.

I think a flag makes a fair amount of sense, particularly because I think we're still going to find corner cases that need adjusting. I think it'd make sense for the flag to start out as opt-in, then once we think we're ready, transition to opt-out, and hopefully we can eventually deprecate and remove the flag.

@jyknight

Copy link
Copy Markdown
Member

I'd really, really like to see the lazy-instantiation feature land, but it'll only be useful once it's actually working. Any method to most-efficiently get to that final working state is fine with me...whatever the maintainers would like.

I'm not sure this PR would actually break any compilations which work today, since currently we fail to build anything which would require instantiation during constant evaluation. With this PR, we will accept some such code that would've previously failed, but continue to reject (or potentially crash the compiler) on other such code. So, from that POV, it might be fine to submit this in the current half-way state, without a flag. Though such inconsistency in behavior doesn't really seem great: it'd be pretty difficult to explain to users why things seemingly-arbitrarily sometimes work and other times don't.

But, I do think it's also probably useful to at least do initial triage on the discovered errors, to see whether the assumptions made here are valid or not.

For example:
I've looked a bit into the narrowing conversion issue. That led me to the discussion in #142707, where I discovered that even though we have a requirement that the input to the narrowing conversion must be a constant expression which evaluates to particular values...apparently that doesn't count as evaluating the expression in a constant context (it's not "manifestly constant evaluation").

So, apparently we're supposed to validate that it is "a constant expression" but in some weird special way, pretending we're not doing constant evaluation? That feels REALLY darn sketchy to me, but OK...

Anyways, if we still buy the logic discussed on that issue, presumably for this oddball case (and are there any other cases like it?), we must be able to trigger template instantiation (and thus provide a SemaProxy) while simultaneously setting InConstantContext=false. In which case, we should not make the simplifications suggested in code reviews earlier.

@efriedma-quic

Copy link
Copy Markdown
Contributor

I'm not sure this PR would actually break any compilations which work today, since currently we fail to build anything which would require instantiation during constant evaluation

If you write something like the following:

int x = f();

This triggers constant evaluation of f(), but it's not an error if constant evaluation fails. So if the attempted constant evaluation triggers an error or crash, that's a problem. Not sure how likely that is in practice.

@cor3ntin

Copy link
Copy Markdown
Contributor

@AaronBallman @efriedma-quic can you expand on the set of breakages you expect?

I'm not sure this PR would actually break any compilations which work today, since currently we fail to build anything which would require instantiation during constant evaluation. With this PR, we will accept some such code that would've previously failed, but continue to reject (or potentially crash the compiler) on other such code.

Yes, that's the intent

If you write something like the following:
int x = f();
This triggers constant evaluation of f(),

This is not a constant context - and also we do not want that behavior in constant initialization, so in that case we should not instantiate f . The only case where we want to go through with instantiation is when we always need f.

consider for example https://compiler-explorer.com/z/sdv153zx8 - that's not something we want to break - even though the standard is not clear on that point.

(similarly, this PR does not instantiate on constant folding or any context where the standard does not require f() to have been instantiated)

@AaronBallman

Copy link
Copy Markdown
Contributor

@AaronBallman @efriedma-quic can you expand on the set of breakages you expect?

I don't have a code example at hand, just painful life experience that makes me conservative: if this can cause instantiations to happen at a different point in the TU, it's likely to break code somewhere because it seems like most times we modify point-of-instantiation, it has that kind of subtle fallout. So my concerns are likely more FUD at the moment.

@efriedma-quic

Copy link
Copy Markdown
Contributor

This is not a constant context

From the standard, "the initializer of a variable that is usable in constant expressions or has constant initialization" is manifestly constant-evaluated. (And a variable has constant initialization if it's constant-initialized, and it's constant-initialized if it's constant-initializable.) So it is a constant context as long as the evaluation succeeds, which means we need to perform instantiation to see whether the evaluation is successful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:bytecode Issues for the clang bytecode constexpr interpreter clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants