Skip to content

[Clang] Implement P2280R4 Using unknown pointers and references in constant expressions #95474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

shafik
Copy link
Collaborator

@shafik shafik commented Jun 13, 2024

P2280R4 allows the use of references in pointers of unknown origins in a constant expression context but only in specific cases that could be constant expressions.

We track whether a variable is a constexpr unknown in a constant expression by setting a flag in either APValue or LValue and using this flag to prevent using unknown values in places where it is not allowed.

Fixes: #63139 #63117

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Jun 13, 2024
@llvmbot
Copy link
Member

llvmbot commented Jun 13, 2024

@llvm/pr-subscribers-clang

Author: Shafik Yaghmour (shafik)

Changes

P2280R4 allows the use of references in pointers of unknown origins in a constant expression context but only in specific cases that could be constant expressions.

We track whether a variable is a constexpr unknown in a constant expression by setting a flag in either APValue or LValue and using this flag to prevent using unknown values in places where it is not allowed.

Fixes: #63139 #63117


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

6 Files Affected:

  • (modified) clang/include/clang/AST/APValue.h (+33-15)
  • (modified) clang/lib/AST/APValue.cpp (+10-2)
  • (modified) clang/lib/AST/ExprConstant.cpp (+76-9)
  • (modified) clang/test/SemaCXX/constant-expression-cxx11.cpp (+9-7)
  • (modified) clang/test/SemaCXX/constant-expression-cxx2a.cpp (+1-2)
  • (added) clang/test/SemaCXX/constant-expression-p2280r4.cpp (+54)
diff --git a/clang/include/clang/AST/APValue.h b/clang/include/clang/AST/APValue.h
index c4206b73b1156..6352348107a64 100644
--- a/clang/include/clang/AST/APValue.h
+++ b/clang/include/clang/AST/APValue.h
@@ -249,6 +249,7 @@ class APValue {
   struct NoLValuePath {};
   struct UninitArray {};
   struct UninitStruct {};
+  struct ConstexprUnknown {};
 
   template <typename Impl> friend class clang::serialization::BasicReaderBase;
   friend class ASTImporter;
@@ -256,6 +257,7 @@ class APValue {
 
 private:
   ValueKind Kind;
+  bool AllowConstexprUnknown = false;
 
   struct ComplexAPSInt {
     APSInt Real, Imag;
@@ -314,53 +316,69 @@ class APValue {
   DataType Data;
 
 public:
-  APValue() : Kind(None) {}
-  explicit APValue(APSInt I) : Kind(None) {
+  bool allowConstexprUnknown() const { return AllowConstexprUnknown; }
+
+  void setConstexprUnknown() { AllowConstexprUnknown = true; }
+
+  APValue() : Kind(None), AllowConstexprUnknown(false) {}
+  explicit APValue(APSInt I) : Kind(None), AllowConstexprUnknown(false) {
     MakeInt(); setInt(std::move(I));
   }
-  explicit APValue(APFloat F) : Kind(None) {
+  explicit APValue(APFloat F) : Kind(None), AllowConstexprUnknown(false) {
     MakeFloat(); setFloat(std::move(F));
   }
-  explicit APValue(APFixedPoint FX) : Kind(None) {
+  explicit APValue(APFixedPoint FX) : Kind(None), AllowConstexprUnknown(false) {
     MakeFixedPoint(std::move(FX));
   }
-  explicit APValue(const APValue *E, unsigned N) : Kind(None) {
+  explicit APValue(const APValue *E, unsigned N)
+      : Kind(None), AllowConstexprUnknown(false) {
     MakeVector(); setVector(E, N);
   }
-  APValue(APSInt R, APSInt I) : Kind(None) {
+  APValue(APSInt R, APSInt I) : Kind(None), AllowConstexprUnknown(false) {
     MakeComplexInt(); setComplexInt(std::move(R), std::move(I));
   }
-  APValue(APFloat R, APFloat I) : Kind(None) {
+  APValue(APFloat R, APFloat I) : Kind(None), AllowConstexprUnknown(false) {
     MakeComplexFloat(); setComplexFloat(std::move(R), std::move(I));
   }
   APValue(const APValue &RHS);
   APValue(APValue &&RHS);
   APValue(LValueBase B, const CharUnits &O, NoLValuePath N,
           bool IsNullPtr = false)
-      : Kind(None) {
+      : Kind(None), AllowConstexprUnknown(false) {
     MakeLValue(); setLValue(B, O, N, IsNullPtr);
   }
   APValue(LValueBase B, const CharUnits &O, ArrayRef<LValuePathEntry> Path,
           bool OnePastTheEnd, bool IsNullPtr = false)
-      : Kind(None) {
+      : Kind(None), AllowConstexprUnknown(false) {
     MakeLValue(); setLValue(B, O, Path, OnePastTheEnd, IsNullPtr);
   }
-  APValue(UninitArray, unsigned InitElts, unsigned Size) : Kind(None) {
+
+  APValue(LValueBase B, ConstexprUnknown, const CharUnits &O,
+          bool IsNullPtr = false)
+      : Kind(None), AllowConstexprUnknown(true) {
+    MakeLValue();
+    setLValue(B, O, NoLValuePath{}, IsNullPtr);
+  }
+
+  APValue(UninitArray, unsigned InitElts, unsigned Size)
+      : Kind(None), AllowConstexprUnknown(false) {
     MakeArray(InitElts, Size);
   }
-  APValue(UninitStruct, unsigned B, unsigned M) : Kind(None) {
+  APValue(UninitStruct, unsigned B, unsigned M)
+      : Kind(None), AllowConstexprUnknown(false) {
     MakeStruct(B, M);
   }
   explicit APValue(const FieldDecl *D, const APValue &V = APValue())
-      : Kind(None) {
+      : Kind(None), AllowConstexprUnknown(false) {
     MakeUnion(); setUnion(D, V);
   }
   APValue(const ValueDecl *Member, bool IsDerivedMember,
-          ArrayRef<const CXXRecordDecl*> Path) : Kind(None) {
+          ArrayRef<const CXXRecordDecl *> Path)
+      : Kind(None), AllowConstexprUnknown(false) {
     MakeMemberPointer(Member, IsDerivedMember, Path);
   }
-  APValue(const AddrLabelExpr* LHSExpr, const AddrLabelExpr* RHSExpr)
-      : Kind(None) {
+  APValue(const AddrLabelExpr *LHSExpr, const AddrLabelExpr *RHSExpr)
+      : Kind(None), AllowConstexprUnknown(false) {
     MakeAddrLabelDiff(); setAddrLabelDiff(LHSExpr, RHSExpr);
   }
   static APValue IndeterminateValue() {
diff --git a/clang/lib/AST/APValue.cpp b/clang/lib/AST/APValue.cpp
index d8e33ff421c06..f7841cb6556de 100644
--- a/clang/lib/AST/APValue.cpp
+++ b/clang/lib/AST/APValue.cpp
@@ -308,7 +308,8 @@ APValue::UnionData::~UnionData () {
   delete Value;
 }
 
-APValue::APValue(const APValue &RHS) : Kind(None) {
+APValue::APValue(const APValue &RHS)
+    : Kind(None), AllowConstexprUnknown(RHS.AllowConstexprUnknown) {
   switch (RHS.getKind()) {
   case None:
   case Indeterminate:
@@ -379,13 +380,17 @@ APValue::APValue(const APValue &RHS) : Kind(None) {
   }
 }
 
-APValue::APValue(APValue &&RHS) : Kind(RHS.Kind), Data(RHS.Data) {
+APValue::APValue(APValue &&RHS)
+    : Kind(RHS.Kind), AllowConstexprUnknown(RHS.AllowConstexprUnknown),
+      Data(RHS.Data) {
   RHS.Kind = None;
 }
 
 APValue &APValue::operator=(const APValue &RHS) {
   if (this != &RHS)
     *this = APValue(RHS);
+
+  AllowConstexprUnknown = RHS.AllowConstexprUnknown;
   return *this;
 }
 
@@ -395,6 +400,7 @@ APValue &APValue::operator=(APValue &&RHS) {
       DestroyDataAndMakeUninit();
     Kind = RHS.Kind;
     Data = RHS.Data;
+    AllowConstexprUnknown = RHS.AllowConstexprUnknown;
     RHS.Kind = None;
   }
   return *this;
@@ -426,6 +432,7 @@ void APValue::DestroyDataAndMakeUninit() {
   else if (Kind == AddrLabelDiff)
     ((AddrLabelDiffData *)(char *)&Data)->~AddrLabelDiffData();
   Kind = None;
+  AllowConstexprUnknown = false;
 }
 
 bool APValue::needsCleanup() const {
@@ -468,6 +475,7 @@ bool APValue::needsCleanup() const {
 void APValue::swap(APValue &RHS) {
   std::swap(Kind, RHS.Kind);
   std::swap(Data, RHS.Data);
+  std::swap(AllowConstexprUnknown, RHS.AllowConstexprUnknown);
 }
 
 /// Profile the value of an APInt, excluding its bit-width.
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 7178f081d9cf3..9a427d908e4c9 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -1609,8 +1609,11 @@ namespace {
     SubobjectDesignator Designator;
     bool IsNullPtr : 1;
     bool InvalidBase : 1;
+    // P2280R4 track if we have an unknown reference or pointer.
+    bool AllowConstexprUnknown = false;
 
     const APValue::LValueBase getLValueBase() const { return Base; }
+    bool allowConstexprUnknown() const { return AllowConstexprUnknown; }
     CharUnits &getLValueOffset() { return Offset; }
     const CharUnits &getLValueOffset() const { return Offset; }
     SubobjectDesignator &getLValueDesignator() { return Designator; }
@@ -1628,6 +1631,8 @@ namespace {
         V = APValue(Base, Offset, Designator.Entries,
                     Designator.IsOnePastTheEnd, IsNullPtr);
       }
+      if (AllowConstexprUnknown)
+        V.setConstexprUnknown();
     }
     void setFrom(ASTContext &Ctx, const APValue &V) {
       assert(V.isLValue() && "Setting LValue from a non-LValue?");
@@ -1636,6 +1641,7 @@ namespace {
       InvalidBase = false;
       Designator = SubobjectDesignator(Ctx, V);
       IsNullPtr = V.isNullPointer();
+      AllowConstexprUnknown = V.allowConstexprUnknown();
     }
 
     void set(APValue::LValueBase B, bool BInvalid = false) {
@@ -1653,6 +1659,7 @@ namespace {
       InvalidBase = BInvalid;
       Designator = SubobjectDesignator(getType(B));
       IsNullPtr = false;
+      AllowConstexprUnknown = false;
     }
 
     void setNull(ASTContext &Ctx, QualType PointerTy) {
@@ -1662,6 +1669,7 @@ namespace {
       InvalidBase = false;
       Designator = SubobjectDesignator(PointerTy->getPointeeType());
       IsNullPtr = true;
+      AllowConstexprUnknown = false;
     }
 
     void setInvalid(APValue::LValueBase B, unsigned I = 0) {
@@ -3300,6 +3308,11 @@ static bool HandleLValueComplexElement(EvalInfo &Info, const Expr *E,
 static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
                                 const VarDecl *VD, CallStackFrame *Frame,
                                 unsigned Version, APValue *&Result) {
+  // P2280R4 If we have a reference type and we are in C++23 allow unknown
+  // references and pointers.
+  bool AllowConstexprUnknown =
+      Info.getLangOpts().CPlusPlus23 && VD->getType()->isReferenceType();
+
   APValue::LValueBase Base(VD, Frame ? Frame->Index : 0, Version);
 
   // If this is a local variable, dig out its value.
@@ -3334,7 +3347,9 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
     return true;
   }
 
-  if (isa<ParmVarDecl>(VD)) {
+  // P2280R4 struck the restriction that variable of referene type lifetime
+  // should begin within the evaluation of E
+  if (isa<ParmVarDecl>(VD) && !AllowConstexprUnknown) {
     // Assume parameters of a potential constant expression are usable in
     // constant expressions.
     if (!Info.checkingPotentialConstantExpression() ||
@@ -3358,7 +3373,9 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
   // FIXME: We should eventually check whether the variable has a reachable
   // initializing declaration.
   const Expr *Init = VD->getAnyInitializer(VD);
-  if (!Init) {
+  // P2280R4 struck the restriction that variable of referene type should have
+  // a preceding initialization.
+  if (!Init && !AllowConstexprUnknown) {
     // Don't diagnose during potential constant expression checking; an
     // initializer might be added later.
     if (!Info.checkingPotentialConstantExpression()) {
@@ -3369,7 +3386,9 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
     return false;
   }
 
-  if (Init->isValueDependent()) {
+  // P2280R4 struck the initialization requirement for variables of reference
+  // type so we can no longer assume we have an Init.
+  if (Init && Init->isValueDependent()) {
     // The DeclRefExpr is not value-dependent, but the variable it refers to
     // has a value-dependent initializer. This should only happen in
     // constant-folding cases, where the variable is not actually of a suitable
@@ -3388,7 +3407,9 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
 
   // Check that we can fold the initializer. In C++, we will have already done
   // this in the cases where it matters for conformance.
-  if (!VD->evaluateValue()) {
+  // P2280R4 struck the initialization requirement for variables of reference
+  // type so we can no longer assume we have an Init.
+  if (Init && !VD->evaluateValue()) {
     Info.FFDiag(E, diag::note_constexpr_var_init_non_constant, 1) << VD;
     NoteLValueLocation(Info, Base);
     return false;
@@ -3420,6 +3441,15 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
   }
 
   Result = VD->getEvaluatedValue();
+
+  // P2280R4 If we don't have a value because this is a reference that was not
+  // initialized or whose lifetime began within E then create a value with as
+  // a ConstexprUnknown status.
+  if (AllowConstexprUnknown) {
+    if (!Result) {
+      Result = new APValue(Base, APValue::ConstexprUnknown{}, CharUnits::One());
+    }
+  }
   return true;
 }
 
@@ -3700,6 +3730,11 @@ findSubobject(EvalInfo &Info, const Expr *E, const CompleteObject &Obj,
   const FieldDecl *LastField = nullptr;
   const FieldDecl *VolatileField = nullptr;
 
+  // P2280R4 If we have an unknown referene or pointer and we don't have a
+  // value then bail out.
+  if (O->allowConstexprUnknown() && !O->hasValue())
+    return false;
+
   // Walk the designator's path to find the subobject.
   for (unsigned I = 0, N = Sub.Entries.size(); /**/; ++I) {
     // Reading an indeterminate value is undefined, but assigning over one is OK.
@@ -5732,6 +5767,12 @@ struct CheckDynamicTypeHandler {
 /// dynamic type.
 static bool checkDynamicType(EvalInfo &Info, const Expr *E, const LValue &This,
                              AccessKinds AK, bool Polymorphic) {
+  // P2280R4 We are not allowed to invoke a virtual function whose dynamic type
+  // us constexpr-unknown, so stop early and let this fail later on if we
+  // attempt to do so.
+  if (This.allowConstexprUnknown())
+    return true;
+
   if (This.Designator.Invalid)
     return false;
 
@@ -5804,7 +5845,13 @@ static std::optional<DynamicType> ComputeDynamicType(EvalInfo &Info,
   // If we don't have an lvalue denoting an object of class type, there is no
   // meaningful dynamic type. (We consider objects of non-class type to have no
   // dynamic type.)
-  if (!checkDynamicType(Info, E, This, AK, true))
+  if (!checkDynamicType(Info, E, This, AK,
+                        (AK == AK_TypeId
+                             ? (E->getType()->isReferenceType() ? true : false)
+                             : true)))
+    return std::nullopt;
+
+  if (This.Designator.Invalid)
     return std::nullopt;
 
   // Refuse to compute a dynamic type in the presence of virtual bases. This
@@ -8539,7 +8586,8 @@ static bool HandleLambdaCapture(EvalInfo &Info, const Expr *E, LValue &Result,
     const ParmVarDecl *Self = MD->getParamDecl(0);
     if (Self->getType()->isReferenceType()) {
       APValue *RefValue = Info.getParamSlot(Info.CurrentCall->Arguments, Self);
-      Result.setFrom(Info.Ctx, *RefValue);
+      if (!RefValue->allowConstexprUnknown() || RefValue->hasValue())
+        Result.setFrom(Info.Ctx, *RefValue);
     } else {
       const ParmVarDecl *VD = Info.CurrentCall->Arguments.getOrigParam(Self);
       CallStackFrame *Frame =
@@ -8595,7 +8643,10 @@ bool LValueExprEvaluator::VisitDeclRefExpr(const DeclRefExpr *E) {
 
 
 bool LValueExprEvaluator::VisitVarDecl(const Expr *E, const VarDecl *VD) {
-
+  // P2280R4 if we are in C++23 track if we have an unknown reference or
+  // pointer.
+  bool AllowConstexprUnknown =
+      Info.getLangOpts().CPlusPlus23 && VD->getType()->isReferenceType();
   // If we are within a lambda's call operator, check whether the 'VD' referred
   // to within 'E' actually represents a lambda-capture that maps to a
   // data-member/field within the closure object, and if so, evaluate to the
@@ -8665,10 +8716,23 @@ bool LValueExprEvaluator::VisitVarDecl(const Expr *E, const VarDecl *VD) {
   if (!V->hasValue()) {
     // FIXME: Is it possible for V to be indeterminate here? If so, we should
     // adjust the diagnostic to say that.
-    if (!Info.checkingPotentialConstantExpression())
+    // P2280R4 If we are have a variable that is unknown reference or pointer
+    // it may not have a value but still be usable later on so do not diagnose.
+    if (!Info.checkingPotentialConstantExpression() && !AllowConstexprUnknown)
       Info.FFDiag(E, diag::note_constexpr_use_uninit_reference);
+
+    // P2280R4 If we are have a variable that is unknown reference or pointer
+    // try to recover it from the frame and set the result accordingly.
+    if (VD->getType()->isReferenceType() && AllowConstexprUnknown) {
+      if (Frame) {
+        Result.set({VD, Frame->Index, Version});
+        return true;
+      }
+      return Success(VD);
+    }
     return false;
   }
+
   return Success(*V, E);
 }
 
@@ -11486,7 +11550,10 @@ class IntExprEvaluator
   }
 
   bool Success(const APValue &V, const Expr *E) {
-    if (V.isLValue() || V.isAddrLabelDiff() || V.isIndeterminate()) {
+    // P2280R4 if we have an unknown reference or pointer allow further
+    // evaluation of the value.
+    if (V.isLValue() || V.isAddrLabelDiff() || V.isIndeterminate() ||
+        V.allowConstexprUnknown()) {
       Result = V;
       return true;
     }
diff --git a/clang/test/SemaCXX/constant-expression-cxx11.cpp b/clang/test/SemaCXX/constant-expression-cxx11.cpp
index efb391ba0922d..684a6a242bf71 100644
--- a/clang/test/SemaCXX/constant-expression-cxx11.cpp
+++ b/clang/test/SemaCXX/constant-expression-cxx11.cpp
@@ -1419,9 +1419,9 @@ namespace ConvertedConstantExpr {
   // useless note and instead just point to the non-constant subexpression.
   enum class E {
     em = m,
-    en = n, // expected-error {{not a constant expression}} expected-note {{initializer of 'n' is unknown}}
+    en = n, // cxx23-note {{initializer of 'n' is not a constant expression}} expected-error {{enumerator value is not a constant expression}} cxx11_20-note {{initializer of 'n' is unknown}}
     eo = (m + // expected-error {{not a constant expression}}
-          n // expected-note {{initializer of 'n' is unknown}}
+          n // cxx23-note {{initializer of 'n' is not a constant expression}} cxx11_20-note {{initializer of 'n' is unknown}}
           ),
     eq = reinterpret_cast<long>((int*)0) // expected-error {{not a constant expression}} expected-note {{reinterpret_cast}}
   };
@@ -1961,7 +1961,8 @@ namespace ConstexprConstructorRecovery {
 
 namespace Lifetime {
   void f() {
-    constexpr int &n = n; // expected-error {{constant expression}} expected-note {{use of reference outside its lifetime}} expected-warning {{not yet bound to a value}}
+    constexpr int &n = n; // expected-error {{constant expression}} cxx23-note {{reference to 'n' is not a constant expression}} cxx23-note {{address of non-static constexpr variable 'n' may differ}} expected-warning {{not yet bound to a value}}
+                          // cxx11_20-note@-1 {{use of reference outside its lifetime is not allowed in a constant expression}}
     constexpr int m = m; // expected-error {{constant expression}} expected-note {{read of object outside its lifetime}}
   }
 
@@ -2381,15 +2382,15 @@ namespace array_size {
   template<typename T> void f1(T t) {
     constexpr int k = t.size();
   }
-  template<typename T> void f2(const T &t) { // expected-note 2{{declared here}}
-    constexpr int k = t.size(); // expected-error 2{{constant}} expected-note 2{{function parameter 't' with unknown value cannot be used in a constant expression}}
+  template<typename T> void f2(const T &t) { // cxx11_20-note 2{{declared here}}
+    constexpr int k = t.size();  // cxx11_20-error 2{{constexpr variable 'k' must be initialized by a constant expression}} cxx11_20-note 2{{function parameter 't' with unknown value cannot be used in a constant expression}}
   }
   template<typename T> void f3(const T &t) {
     constexpr int k = T::size();
   }
   void g(array<3> a) {
     f1(a);
-    f2(a); // expected-note {{instantiation of}}
+    f2(a); // cxx11_20-note {{in instantiation of function template}}
     f3(a);
   }
 
@@ -2398,8 +2399,9 @@ namespace array_size {
   };
   void h(array_nonstatic<3> a) {
     f1(a);
-    f2(a); // expected-note {{instantiation of}}
+    f2(a); // cxx11_20-note {{instantiation of}}
   }
+  //static_assert(f2(array_size::array<3>{}));
 }
 
 namespace flexible_array {
diff --git a/clang/test/SemaCXX/constant-expression-cxx2a.cpp b/clang/test/SemaCXX/constant-expression-cxx2a.cpp
index e4d97dcb73562..3910886a36346 100644
--- a/clang/test/SemaCXX/constant-expression-cxx2a.cpp
+++ b/clang/test/SemaCXX/constant-expression-cxx2a.cpp
@@ -308,8 +308,7 @@ namespace TypeId {
   static_assert(&B2().ti1 == &typeid(B));
   static_assert(&B2().ti2 == &typeid(B2));
   extern B2 extern_b2;
-  // expected-note@+1 {{typeid applied to object 'extern_b2' whose dynamic type is not constant}}
-  static_assert(&typeid(extern_b2) == &typeid(B2)); // expected-error {{constant expression}}
+  static_assert(&typeid(extern_b2) == &typeid(B2));
 
   constexpr B2 b2;
   constexpr const B &b1 = b2;
diff --git a/clang/test/SemaCXX/constant-expression-p2280r4.cpp b/clang/test/SemaCXX/constant-expression-p2280r4.cpp
new file mode 100644
index 0000000000000..be41eed88c494
--- /dev/null
+++ b/clang/test/SemaCXX/constant-expression-p2280r4.cpp
@@ -0,0 +1,54 @@
+// RUN: %clang_cc1 -std=c++23 -verify %s
+
+using size_t = decltype(sizeof(0));
+
+namespace std {
+struct type_info {
+  const char* name() const noexcept(true);
+};
+}
+
+template <typename T, size_t N>
+constexpr size_t array_size(T (&)[N]) {
+  return N;
+}
+
+void use_array(int const (&gold_medal_mel)[2]) {
+  constexpr auto gold = array_size(gold_medal_mel); // ok
+}
+
+constexpr auto olympic_mile() {
+  const int ledecky = 1500;
+  return []{ return ledecky; };
+}
+static_assert(olympic_mile()() == 1500); // ok
+
+struct Swim {
+  constexpr int phelps() { return 28; }
+  virtual constexpr int lochte() { return 12; }
+  int coughlin = 12;
+};
+
+constexpr int how_many(Swim& swam) {
+  Swim* p = &swam;
+  return (p + 1 - 1)->phelps();
+}
+
+void splash(Swim& swam) {
+  static_assert(swam.phelps() == 28);     // ok
+  static_assert((&swam)->phelps() == 28); // ok
+  Swim* pswam = &swam;                    // expected-note {{declared here}}
+  static_assert(pswam->phelps() ==...
[truncated]

Comment on lines 3311 to 3314
// P2280R4 If we have a reference type and we are in C++23 allow unknown
// references and pointers.
bool AllowConstexprUnknown =
Info.getLangOpts().CPlusPlus23 && VD->getType()->isReferenceType();
Copy link
Contributor

@frederick-vs-ja frederick-vs-ja Jun 14, 2024

Choose a reason for hiding this comment

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

This is a DR per N4916 (ditto below).

Suggested change
// P2280R4 If we have a reference type and we are in C++23 allow unknown
// references and pointers.
bool AllowConstexprUnknown =
Info.getLangOpts().CPlusPlus23 && VD->getType()->isReferenceType();
// P2280R4 If we have a reference type allow unknown references and pointers.
bool AllowConstexprUnknown = VD->getType()->isReferenceType();

Copy link
Contributor

Choose a reason for hiding this comment

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

Additionally, I would prefer references to the c++ standard wording rather than the paper

Comment on lines -1964 to +1965
constexpr int &n = n; // expected-error {{constant expression}} expected-note {{use of reference outside its lifetime}} expected-warning {{not yet bound to a value}}
constexpr int &n = n; // expected-error {{constant expression}} cxx23-note {{reference to 'n' is not a constant expression}} cxx23-note {{address of non-static constexpr variable 'n' may differ}} expected-warning {{not yet bound to a value}}
// cxx11_20-note@-1 {{use of reference outside its lifetime is not allowed in a constant expression}}
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks strange. The old message looks better (see also CWG453) and the new one is possibly misleading as we perhaps shoudn't say the address of a reference variable.

Copy link
Contributor

Choose a reason for hiding this comment

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

@cor3ntin cor3ntin requested a review from tbaederr June 14, 2024 09:20
Copy link
Contributor

@cor3ntin cor3ntin left a comment

Choose a reason for hiding this comment

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

Thanks a lot Shafik!

I like the direction, it looks less complex than i thought it would be
I agree with @frederick-vs-ja that some diagnostics seem to have regressed.
Maybe a variable should not treated as constexpr-unknown if it's not within its lifetime

I'd like you to go over all the related issues we have (including the ones we closed for duplication), to make sure they work as expected. i think they make up for a good corpus of unit tests

We will need changelog / status page update

Just to point it out, there are a few unresolved core issues

https://cplusplus.github.io/CWG/issues/2740.html
https://cplusplus.github.io/CWG/issues/2633.html

Comment on lines 3311 to 3314
// P2280R4 If we have a reference type and we are in C++23 allow unknown
// references and pointers.
bool AllowConstexprUnknown =
Info.getLangOpts().CPlusPlus23 && VD->getType()->isReferenceType();
Copy link
Contributor

Choose a reason for hiding this comment

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

Additionally, I would prefer references to the c++ standard wording rather than the paper

@yronglin
Copy link
Contributor

yronglin commented Jul 1, 2024

Thank you work on this! I really like this feature. Will this PR also provide support for the new constant interpreter? The new constant interpreter seems to already support some unknown constexpr features (FYI https://godbolt.org/z/xTYhGEfxT). It has the concept of DummyValue.

@cor3ntin
Copy link
Contributor

cor3ntin commented Jul 6, 2024

Thank you work on this! I really like this feature. Will this PR also provide support for the new constant interpreter? The new constant interpreter seems to already support some unknown constexpr features (FYI https://godbolt.org/z/xTYhGEfxT). It has the concept of DummyValue.

I think that can be a separate PR @tbaederr

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

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

Generally seems like a reasonable direction, thank you!


template <typename Impl> friend class clang::serialization::BasicReaderBase;
friend class ASTImporter;
friend class ASTNodeImporter;

private:
ValueKind Kind;
bool AllowConstexprUnknown = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to use a bit-field here so we can pack this bit together with the Kind?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Initializing a bit-field inline is a C++20 extension and so I don't think I can use it.

I set AllowConstexprUnknown everyplace I thought it mattered in APValue but I obtain different results. It means I am missing someplace where APValue is being initialized. I can try again and see if I can hunt down the places I missed but it left me a bit spooked that it was not obvious.

…nstant expressions

P2280R4 allows the use of references in pointers of unknown origins in a
constant expression context but only in specific cases that could be constant
expressions.

We track whether a variable is a constexpr unknown in a constant expression by
setting a flag in either APValue or LValue and using this flag to prevent using
unknown values in places where it is not allowed.

In `evaluateVarDeclInit` we may need to create a new `APValue` to track the
unknown referene or pointer and we track that `APValue` in the
`CallStackFrame`.

Fixes: llvm#63139
llvm#63117
@shafik shafik force-pushed the impl_unknown_pointers_and_references_in_constant_expr branch from 7583d32 to 69b09ea Compare July 17, 2024 19:21
@Endilll
Copy link
Contributor

Endilll commented Jul 27, 2024

Shafik contacted me offline about a libc++ test failure this PR causes. The test in question is mem.res.eq/not_equal_pass.cpp.
I reduced it down to the following:

struct memory_resource {
  virtual ~memory_resource();
  bool is_equal(const memory_resource &) const noexcept;
};

inline bool operator==(const memory_resource &__lhs, const memory_resource &__rhs) {
  return &__lhs == &__rhs || __lhs.is_equal(__rhs);
  //     ^^^^^^^^^^^^^^^^ frontend built from this branch believes that this is never true         
}

void __assert_fail();

struct TestResourceImp : memory_resource {
  memory_resource do_is_equal_other;

  virtual bool do_is_equal() noexcept {
    return dynamic_cast<TestResourceImp *>(&do_is_equal_other);
  }
};

int main() {
  memory_resource mr1, mr2;
  mr1 != mr2 ? void() : __assert_fail();
}

Clang built from this branch produces the following IR for operator==

; Function Attrs: mustprogress noinline nounwind optnone uwtable
define linkonce_odr dso_local noundef zeroext i1 @_ZeqRK15memory_resourceS1_(ptr noundef nonnull align 8 dereferenceable(8) %__lhs, ptr noundef nonnull align 8 dereferenceable(8) %__rhs) #1 comdat {
entry:
  %__lhs.addr = alloca ptr, align 8
  %__rhs.addr = alloca ptr, align 8
  store ptr %__lhs, ptr %__lhs.addr, align 8
  store ptr %__rhs, ptr %__rhs.addr, align 8
  %0 = load ptr, ptr %__lhs.addr, align 8
  %1 = load ptr, ptr %__rhs.addr, align 8
  %call = call noundef zeroext i1 @_ZNK15memory_resource8is_equalERKS_(ptr noundef nonnull align 8 dereferenceable(8) %0, ptr noundef nonnull align 8 dereferenceable(8) %1) #4
  ret i1 %call
}

Whereas a recent nighly build (++20240704100628+b298e2d2d225-1~exp1~20240704220819.2190) properly lowers the LHS of the logical OR:

; Function Attrs: mustprogress noinline nounwind optnone uwtable
define linkonce_odr dso_local noundef zeroext i1 @_ZeqRK15memory_resourceS1_(ptr noundef nonnull align 8 dereferenceable(8) %__lhs, ptr noundef nonnull align 8 dereferenceable(8) %__rhs) #1 comdat {
entry:
  %__lhs.addr = alloca ptr, align 8
  %__rhs.addr = alloca ptr, align 8
  store ptr %__lhs, ptr %__lhs.addr, align 8
  store ptr %__rhs, ptr %__rhs.addr, align 8
  %0 = load ptr, ptr %__lhs.addr, align 8
  %1 = load ptr, ptr %__rhs.addr, align 8
  %cmp = icmp eq ptr %0, %1
  br i1 %cmp, label %lor.end, label %lor.rhs

lor.rhs:                                          ; preds = %entry
  %2 = load ptr, ptr %__lhs.addr, align 8
  %3 = load ptr, ptr %__rhs.addr, align 8
  %call = call noundef zeroext i1 @_ZNK15memory_resource8is_equalERKS_(ptr noundef nonnull align 8 dereferenceable(8) %2, ptr noundef nonnull align 8 dereferenceable(8) %3) #4
  br label %lor.end

lor.end:                                          ; preds = %lor.rhs, %entry
  %4 = phi i1 [ true, %entry ], [ %call, %lor.rhs ]
  ret i1 %4
}

… and pointers

- Fix handling for comparing unknown pointers
- Add more tests
Copy link

github-actions bot commented Jul 29, 2024

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

shafik added 2 commits July 29, 2024 10:55
…es and

pointers it did not have to handle this case before.
Copy link
Contributor

@cor3ntin cor3ntin left a comment

Choose a reason for hiding this comment

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

TBH, i think this looks good

  • We should probably fix up the comments to quote the standard rather than the paper
  • This should be applied in all language modes
  • Do we have sufficient test coverage (especially for things like explicit and implicit this pointers)?

updating comments to refer to the standard when possible
@tbaederr
Copy link
Contributor

Just a s/both/expected/ in those two lines should be enough

@cor3ntin
Copy link
Contributor

cor3ntin commented Jan 22, 2025

Fixed in 28c819c

@llvm-ci
Copy link
Collaborator

llvm-ci commented Jan 22, 2025

LLVM Buildbot has detected a new failure on builder clang-x86_64-debian-fast running on gribozavr4 while building clang at step 6 "test-build-unified-tree-check-all".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/56/builds/16819

Here is the relevant piece of the build log for the reference
Step 6 (test-build-unified-tree-check-all) failure: test (failure)
******************** TEST 'Clang :: AST/ByteCode/cxx2a.cpp' FAILED ********************
Exit Code: 1

Command Output (stderr):
--
RUN: at line 1: /b/1/clang-x86_64-debian-fast/llvm.obj/bin/clang -cc1 -internal-isystem /b/1/clang-x86_64-debian-fast/llvm.obj/lib/clang/20/include -nostdsysteminc -std=c++2a -fsyntax-only -fcxx-exceptions -verify=ref,both /b/1/clang-x86_64-debian-fast/llvm.src/clang/test/AST/ByteCode/cxx2a.cpp
+ /b/1/clang-x86_64-debian-fast/llvm.obj/bin/clang -cc1 -internal-isystem /b/1/clang-x86_64-debian-fast/llvm.obj/lib/clang/20/include -nostdsysteminc -std=c++2a -fsyntax-only -fcxx-exceptions -verify=ref,both /b/1/clang-x86_64-debian-fast/llvm.src/clang/test/AST/ByteCode/cxx2a.cpp
error: 'both-error' diagnostics expected but not seen: 
  File /b/1/clang-x86_64-debian-fast/llvm.src/clang/test/AST/ByteCode/cxx2a.cpp Line 142: constant expression
error: 'both-note' diagnostics expected but not seen: 
  File /b/1/clang-x86_64-debian-fast/llvm.src/clang/test/AST/ByteCode/cxx2a.cpp Line 142: typeid applied to object 'extern_b2' whose dynamic type is not constant
2 errors generated.

--

********************


@llvm-ci
Copy link
Collaborator

llvm-ci commented Jan 22, 2025

LLVM Buildbot has detected a new failure on builder llvm-x86_64-debian-dylib running on gribozavr4 while building clang at step 6 "test-build-unified-tree-check-clang".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/60/builds/17619

Here is the relevant piece of the build log for the reference
Step 6 (test-build-unified-tree-check-clang) failure: test (failure)
******************** TEST 'Clang :: AST/ByteCode/cxx2a.cpp' FAILED ********************
Exit Code: 1

Command Output (stderr):
--
RUN: at line 1: /b/1/llvm-x86_64-debian-dylib/build/bin/clang -cc1 -internal-isystem /b/1/llvm-x86_64-debian-dylib/build/lib/clang/20/include -nostdsysteminc -std=c++2a -fsyntax-only -fcxx-exceptions -verify=ref,both /b/1/llvm-x86_64-debian-dylib/llvm-project/clang/test/AST/ByteCode/cxx2a.cpp
+ /b/1/llvm-x86_64-debian-dylib/build/bin/clang -cc1 -internal-isystem /b/1/llvm-x86_64-debian-dylib/build/lib/clang/20/include -nostdsysteminc -std=c++2a -fsyntax-only -fcxx-exceptions -verify=ref,both /b/1/llvm-x86_64-debian-dylib/llvm-project/clang/test/AST/ByteCode/cxx2a.cpp
error: 'both-error' diagnostics expected but not seen: 
  File /b/1/llvm-x86_64-debian-dylib/llvm-project/clang/test/AST/ByteCode/cxx2a.cpp Line 142: constant expression
error: 'both-note' diagnostics expected but not seen: 
  File /b/1/llvm-x86_64-debian-dylib/llvm-project/clang/test/AST/ByteCode/cxx2a.cpp Line 142: typeid applied to object 'extern_b2' whose dynamic type is not constant
2 errors generated.

--

********************


@llvm-ci
Copy link
Collaborator

llvm-ci commented Jan 22, 2025

LLVM Buildbot has detected a new failure on builder premerge-monolithic-linux running on premerge-linux-1 while building clang at step 7 "test-build-unified-tree-check-all".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/153/builds/20475

Here is the relevant piece of the build log for the reference
Step 7 (test-build-unified-tree-check-all) failure: test (failure)
******************** TEST 'Clang :: AST/ByteCode/cxx2a.cpp' FAILED ********************
Exit Code: 1

Command Output (stderr):
--
RUN: at line 1: /build/buildbot/premerge-monolithic-linux/build/bin/clang -cc1 -internal-isystem /build/buildbot/premerge-monolithic-linux/build/lib/clang/20/include -nostdsysteminc -std=c++2a -fsyntax-only -fcxx-exceptions -verify=ref,both /build/buildbot/premerge-monolithic-linux/llvm-project/clang/test/AST/ByteCode/cxx2a.cpp
+ /build/buildbot/premerge-monolithic-linux/build/bin/clang -cc1 -internal-isystem /build/buildbot/premerge-monolithic-linux/build/lib/clang/20/include -nostdsysteminc -std=c++2a -fsyntax-only -fcxx-exceptions -verify=ref,both /build/buildbot/premerge-monolithic-linux/llvm-project/clang/test/AST/ByteCode/cxx2a.cpp
error: 'both-error' diagnostics expected but not seen: 
  File /build/buildbot/premerge-monolithic-linux/llvm-project/clang/test/AST/ByteCode/cxx2a.cpp Line 142: constant expression
error: 'both-note' diagnostics expected but not seen: 
  File /build/buildbot/premerge-monolithic-linux/llvm-project/clang/test/AST/ByteCode/cxx2a.cpp Line 142: typeid applied to object 'extern_b2' whose dynamic type is not constant
2 errors generated.

--

********************


shafik added a commit that referenced this pull request Jan 26, 2025
…ating APValue (#124478)

When implmenting P2280R4 here:
#95474

When creating the APValue to store and constexprUnknown value I used an
offset of CharUnits::One() but it should have been CharUnits::Zero().

This change just adjusts that value.
github-actions bot pushed a commit to arm/arm-toolchain that referenced this pull request Jan 26, 2025
…set when ceating APValue (#124478)

When implmenting P2280R4 here:
llvm/llvm-project#95474

When creating the APValue to store and constexprUnknown value I used an
offset of CharUnits::One() but it should have been CharUnits::Zero().

This change just adjusts that value.
@ahatanak
Copy link
Collaborator

ahatanak commented May 1, 2025

clang started issuing a warning after this PR was merged when compiling the following code:

typedef long unsigned int size_t;

extern "C" {
void *
  memcpy(void * __dst, const void * __src,
  size_t __n);
}

struct MyClass
{
    unsigned numberOfBuffers;
    unsigned long long buffers[1];
};
typedef struct MyClass MyClass;

MyClass a0;

void test() {
  const size_t byteSize = sizeof(MyClass) + sizeof(unsigned long long);
  MyClass &a1 = *(MyClass *)__builtin_alloca(byteSize);
  MyClass *a2 = (MyClass *)__builtin_alloca(byteSize);
  memcpy(&a1.buffers[0], &a0.buffers[0], sizeof(unsigned long long)); // no warnings.
  memcpy(&a1.buffers[1], &a0.buffers[0], sizeof(unsigned long long)); // warning: 'memcpy' will always overflow; destination buffer has size 0, but size argument is 8 [-Wfortify-source]
  memcpy(&a2->buffers[1], &a0.buffers[0], sizeof(unsigned long long)); // no warnings.
}

Is the warning valid? Is the change in clang's behavior intentional?

@zygoloid
Copy link
Collaborator

zygoloid commented May 1, 2025

Is the warning valid? Is the change in clang's behavior intentional?

What do you have -fstrict-flex-arrays set to? Per the documentation (which weirdly seems to be missing at HEAD...) if it's not set to 1 then we don't consider a [1] array bound to indicate a flexible array member, which would make the warning correct.

@ahatanak
Copy link
Collaborator

ahatanak commented May 1, 2025

Is passing -fstrict-flex-arrays=1 supposed to silence the warning? I still see the warning: https://godbolt.org/z/YEKhW19nK

@zygoloid
Copy link
Collaborator

zygoloid commented May 1, 2025

Looks like a bug to me. This warning presumably ought to be checking whether it's valid to read / write to the given location using something like isUserWritingOffTheEnd. (But I don't think that's a bug in this PR.)

@thevar1able
Copy link
Contributor

thevar1able commented May 11, 2025

Hey, I'm seeing a compiler crash after this PR.

https://pastila.nl/?000c2db4/648c000755344e7c43e24c25dd06a571#dyq1EG+tE1OVC+RrCFJoNw==
gtest_coordination_storage.tar.gz

Here is the commit I had to make to get it building again.

@kripken
Copy link
Member

kripken commented May 15, 2025

I am seeing a runtime error after this, when using C++23, which looks like a regression. Testcase:

// A.hpp
#include <iostream>
struct MyStruct {
    double m0{-9999.0};
    int m1{-12345};
};

constexpr MyStruct default_val;

auto compute_area(double l, const MyStruct &val = default_val) -> double {
    std::cout << "compute_area : val.m0 : " << val.m0 << "\n";
    std::cout << "compute_area : val.m1 : " << val.m1 << " \n";
    if (val.m1 != -12345) {
        return val.m0 * l;
    }
    return -9999;
}

// main.cpp
//#include "A.hpp"
#include <cassert>
#include <iostream>

auto main() -> int {
    MyStruct in_val{.m0 = 2.0, .m1 = 1};
    std::cout << "main : in_val.m0 : " << in_val.m0 << "\n";
    std::cout << "main : in_val.m1 : " << in_val.m1 << " \n\n";
    double val = compute_area(1.0, in_val);
    std::cout << "result = " << val << std::endl;
    assert(val == 2.0);
    return 0;
}

Before this landed, it runs ok:

$ build/bin/clang++ a.cpp -std=c++23 && ./a.out
main : in_val.m0 : 2
main : in_val.m1 : 1 

compute_area : val.m0 : 2
compute_area : val.m1 : 1 
result = 2

After this landed, the assert fails:

$ build/bin/clang++ a.cpp -std=c++23 && ./a.out                                                                                                               
main : in_val.m0 : 2                                                                                                                                                                          
main : in_val.m1 : 1                                                                                                                                                                          
                                                                                                                                                                                              
compute_area : val.m0 : 2                                                                                                                                                                     compute_area : val.m1 : -12345                                                                                                                                                                
result = -9999                                                                                                                                                                                
a.out: a.cpp:30: int main(): Assertion `val == 2.0' failed.                                                                                                                                   
Aborted (core dumped)                

(original report: emscripten-core/emscripten#24325)

@ahatanak
Copy link
Collaborator

evaluateVarDeclInit is using the default argument expression (default_val) to evaluate the initializer of parameter val.

// Dig out the initializer, and use the declaration which it's attached to.
// FIXME: We should eventually check whether the variable has a reachable
// initializing declaration.
const Expr *Init = VD->getAnyInitializer(VD);

@ahatanak
Copy link
Collaborator

Looks like a bug to me. This warning presumably ought to be checking whether it's valid to read / write to the given location using something like isUserWritingOffTheEnd. (But I don't think that's a bug in this PR.)

Should foo in the following code return 4 when -fstrict-flex-arrays is 2 or 3 (but not when it's 0 or 1) only when the target is -std=c++23 or newer? Or should it return 4 for older standards too?

struct S {
  int count;
  int array[1];
};

int foo(S &s) {
  return __builtin_object_size(s.array, 0);
}

And this function should return -1 regardless of what the standard is because s is a pointer?

int foo(S *s) {
  return __builtin_object_size(s->array, 0);
}

@zygoloid
Copy link
Collaborator

Should foo in the following code return 4 when -fstrict-flex-arrays is 2 or 3 (but not when it's 0 or 1) only when the target is -std=c++23 or newer? Or should it return 4 for older standards too?

We evaluate the operand of __builtin_object_size in EM_IgnoreSideEffects mode, which is documented as:

/// Evaluate in any way we know how. Don't worry about side-effects that
/// can't be modeled.

If that mode doesn't already permit using unknown pointers and references in all language modes, then we should change it so that it does. __builtin_object_size is best-effort, but we should produce a constant value for it that's not the -1 or 0 fallback value whenever we're able to do so.

@ahatanak
Copy link
Collaborator

If that mode doesn't already permit using unknown pointers and references in all language modes, then we should change it so that it does. __builtin_object_size is best-effort, but we should produce a constant value for it that's not the -1 or 0 fallback value whenever we're able to do so.

But there isn't much we can do with unknown pointers or references, is there?

We can't have foo return 4 unless we know for sure that s isn't embedded in a larger object. In the following case, wouldn't foo be expected to return 12?

int bar() {
  S a[2];
  return foo(a[0]);
}

@shafik
Copy link
Collaborator Author

shafik commented May 20, 2025

@kripken somewhat reduced:

struct MyStruct {
    double m0{-9999.0};
    int m1{-12345};
};

constexpr MyStruct default_val;

auto compute_area(double l, const MyStruct &val = default_val) -> double {
    if (val.m1 == 1)
      return 2.0;
    return 0;
}


#include <cassert>


auto main() -> int {
    MyStruct in_val{.m0 = 2.0, .m1 = 1};
    double val = compute_area(1.0, in_val);
    assert(val == 2.0);
}

@zygoloid
Copy link
Collaborator

If that mode doesn't already permit using unknown pointers and references in all language modes, then we should change it so that it does. __builtin_object_size is best-effort, but we should produce a constant value for it that's not the -1 or 0 fallback value whenever we're able to do so.

But there isn't much we can do with unknown pointers or references, is there?

We can't have foo return 4 unless we know for sure that s isn't embedded in a larger object. In the following case, wouldn't foo be expected to return 12?

int bar() {
  S a[2];
  return foo(a[0]);
}

It depends on the mode parameter in __builtin_object_size. For mode 1 and 3, we can use the access path and return 4 if -fstrict-flex-arrays is strict enough. For mode 0 or 2, or with lax flexible array rules, I agree that we can't conclude anything and would need to return 0 / -1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
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.

Implement P2280R4 Using unknown pointers and references in constant expressions