-
Notifications
You must be signed in to change notification settings - Fork 15.1k
[clang] fix constexpr-unknown handling of self-references. #132990
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
[clang] fix constexpr-unknown handling of self-references. #132990
Conversation
|
@llvm/pr-subscribers-clang Author: Eli Friedman (efriedma-quic) ChangesUsually, in constant evaluation, references which are local to the evaluation have to be initialized before they're accessed. However, there's one funny special case: the initializer of a reference can refer to itself. This generally ends up being undefined behavior if it's used in an evaluated context, but it isn't othewise forbidden. In constant evaluation, this splits into two cases: global variables, and local variables in constexpr functions. This patch handles both of those cases. (Local variables tends to trip other errors in most cases, but if you try hard enough, you can get an accepts-invalid.) I'm not sure what's correct for g5(): this patch and gcc accept, but -fexperimental-new-constant-interpreter rejects. Fixes #131330 . Full diff: https://github.com/llvm/llvm-project/pull/132990.diff 2 Files Affected:
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 0165a0a3b0df3..71c8b9a1a5f37 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -3485,11 +3485,27 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
APValue::LValueBase Base(VD, Frame ? Frame->Index : 0, Version);
+ auto CheckUninitReference = [&] {
+ if (!Result->hasValue() && VD->getType()->isReferenceType()) {
+ // We can't use an uninitialized reference directly.
+ if (!AllowConstexprUnknown) {
+ if (!Info.checkingPotentialConstantExpression())
+ Info.FFDiag(E, diag::note_constexpr_use_uninit_reference);
+ return false;
+ }
+ Result = &Info.CurrentCall->createConstexprUnknownAPValues(VD, Base);
+ }
+ return true;
+ };
+
// If this is a local variable, dig out its value.
if (Frame) {
Result = Frame->getTemporary(VD, Version);
- if (Result)
+ if (Result) {
+ if (!CheckUninitReference())
+ return false;
return true;
+ }
if (!isa<ParmVarDecl>(VD)) {
// Assume variables referenced within a lambda's call operator that were
@@ -3514,6 +3530,9 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
// in-flight value.
if (Info.EvaluatingDecl == Base) {
Result = Info.EvaluatingDeclValue;
+ if (!CheckUninitReference())
+ return false;
+
return true;
}
@@ -3587,11 +3606,7 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
// type so we can no longer assume we have an Init.
// Used to be C++20 [expr.const]p5.12:
// ... reference has a preceding initialization and either ...
- if (Init && !VD->evaluateValue()) {
- if (AllowConstexprUnknown) {
- Result = &Info.CurrentCall->createConstexprUnknownAPValues(VD, Base);
- return true;
- }
+ if (Init && !VD->evaluateValue() && !AllowConstexprUnknown) {
Info.FFDiag(E, diag::note_constexpr_var_init_non_constant, 1) << VD;
NoteLValueLocation(Info, Base);
return false;
@@ -3629,17 +3644,17 @@ static bool evaluateVarDeclInit(EvalInfo &Info, const Expr *E,
Result = VD->getEvaluatedValue();
- // C++23 [expr.const]p8
- // ... For such an object that is not usable in constant expressions, the
- // dynamic type of the object is constexpr-unknown. For such a reference that
- // is not usable in constant expressions, the reference is treated as binding
- // to an unspecified object of the referenced type whose lifetime and that of
- // all subobjects includes the entire constant evaluation and whose dynamic
- // type is constexpr-unknown.
- if (AllowConstexprUnknown) {
- if (!Result)
+ if (!Result) {
+ if (AllowConstexprUnknown) {
Result = &Info.CurrentCall->createConstexprUnknownAPValues(VD, Base);
+ } else {
+ return false;
+ }
}
+
+ if (!CheckUninitReference())
+ return false;
+
return true;
}
@@ -8931,10 +8946,6 @@ bool LValueExprEvaluator::VisitDeclRefExpr(const DeclRefExpr *E) {
bool LValueExprEvaluator::VisitVarDecl(const Expr *E, const VarDecl *VD) {
- // C++23 [expr.const]p8 If we have a reference type allow unknown references
- // and pointers.
- 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
@@ -9001,26 +9012,6 @@ bool LValueExprEvaluator::VisitVarDecl(const Expr *E, const VarDecl *VD) {
APValue *V;
if (!evaluateVarDeclInit(Info, E, VD, Frame, Version, V))
return false;
- if (!V->hasValue()) {
- // FIXME: Is it possible for V to be indeterminate here? If so, we should
- // adjust the diagnostic to say that.
- // C++23 [expr.const]p8 If we 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);
-
- // C++23 [expr.const]p8 If we 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);
}
diff --git a/clang/test/SemaCXX/constant-expression-p2280r4.cpp b/clang/test/SemaCXX/constant-expression-p2280r4.cpp
index f22430a0e88a2..544372ee4492d 100644
--- a/clang/test/SemaCXX/constant-expression-p2280r4.cpp
+++ b/clang/test/SemaCXX/constant-expression-p2280r4.cpp
@@ -1,5 +1,5 @@
// RUN: %clang_cc1 -std=c++23 -verify=expected,nointerpreter %s
-// RUN: %clang_cc1 -std=c++23 -verify %s -fexperimental-new-constant-interpreter
+// RUN: %clang_cc1 -std=c++23 -verify=expected,interpreter %s -fexperimental-new-constant-interpreter
using size_t = decltype(sizeof(0));
@@ -177,3 +177,50 @@ namespace extern_reference_used_as_unknown {
int y;
constinit int& g = (x,y); // expected-warning {{left operand of comma operator has no effect}}
}
+
+namespace uninit_reference_used {
+ int y;
+ constexpr int &r = r; // expected-error {{must be initialized by a constant expression}} \
+ // nointerpreter-note {{initializer of 'r' is not a constant expression}} \
+ // nointerpreter-note {{declared here}}
+ constexpr int &rr = (rr, y);
+ constexpr int &g() {
+ int &x = x; // expected-warning {{reference 'x' is not yet bound to a value when used within its own initialization}} \
+ // nointerpreter-note {{declared here}} \
+ // interpreter-note {{read of uninitialized object is not allowed in a constant expression}}
+ return x;
+ }
+ constexpr int &gg = g(); // expected-error {{must be initialized by a constant expression}} \
+ // nointerpreter-note {{reference to 'x' is not a constant expression}} \
+ // interpreter-note {{in call to 'g()'}}
+ constexpr int g2() {
+ int &x = x; // expected-warning {{reference 'x' is not yet bound to a value when used within its own initialization}} \
+ // interpreter-note {{read of uninitialized object is not allowed in a constant expression}}
+ return x;
+ }
+ constexpr int gg2 = g2(); // expected-error {{must be initialized by a constant expression}} \
+ // interpreter-note {{in call to 'g2()'}}
+ constexpr int &g3() {
+ int &x = (x,y); // expected-warning{{left operand of comma operator has no effect}} \
+ // expected-warning {{reference 'x' is not yet bound to a value when used within its own initialization}}
+ return x;
+ }
+ constexpr int &gg3 = g3();
+ typedef decltype(sizeof(1)) uintptr_t;
+ constexpr uintptr_t g4() {
+ uintptr_t * &x = x; // expected-warning {{reference 'x' is not yet bound to a value when used within its own initialization}} \
+ // interpreter-note {{read of uninitialized object is not allowed in a constant expression}}
+ *(uintptr_t*)x = 10;
+ return 3;
+ }
+ constexpr uintptr_t gg4 = g4(); // expected-error {{must be initialized by a constant expression}} \
+ // interpreter-note {{in call to 'g4()'}}
+ constexpr int g5() {
+ int &x = x; // expected-warning {{reference 'x' is not yet bound to a value when used within its own initialization}} \
+ // interpreter-note {{read of uninitialized object is not allowed in a constant expression}}
+ return 3;
+ }
+ constexpr uintptr_t gg5 = g5(); // interpreter-error {{must be initialized by a constant expression}} \
+ // interpreter-note {{in call to 'g5()'}}
+
+}
|
|
✅ With the latest revision this PR passed the C/C++ code formatter. |
f5195b8 to
2873bb1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks.
I think the direction makes sense
Do you intend to backport this change to 20?
clang/lib/AST/ExprConstant.cpp
Outdated
| if (!Result) { | ||
| if (AllowConstexprUnknown) { | ||
| Result = &Info.CurrentCall->createConstexprUnknownAPValues(VD, Base); | ||
| } else { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (!Result) { | |
| if (AllowConstexprUnknown) { | |
| Result = &Info.CurrentCall->createConstexprUnknownAPValues(VD, Base); | |
| } else { | |
| return false; | |
| } | |
| if (!Result) { | |
| if (AllowConstexprUnknown) | |
| Result = &Info.CurrentCall->createConstexprUnknownAPValues(VD, Base); | |
| else | |
| return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ping
clang/lib/AST/ExprConstant.cpp
Outdated
| if (!CheckUninitReference()) | ||
| return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return CheckUninitReference();
clang/lib/AST/ExprConstant.cpp
Outdated
| auto CheckUninitReference = [&] { | ||
| if (!Result->hasValue() && VD->getType()->isReferenceType()) { | ||
| // We can't use an uninitialized reference directly. | ||
| if (!AllowConstexprUnknown) { | ||
| if (!Info.checkingPotentialConstantExpression()) | ||
| Info.FFDiag(E, diag::note_constexpr_use_uninit_reference); | ||
| return false; | ||
| } | ||
| Result = &Info.CurrentCall->createConstexprUnknownAPValues(VD, Base); | ||
| } | ||
| return true; | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could use a comment
| constexpr uintptr_t gg5 = g5(); // interpreter-error {{must be initialized by a constant expression}} \ | ||
| // interpreter-note {{in call to 'g5()'}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
g5() looks valid to me - in the initializer x refer to a reference whose lifetime has not yet started, and therefore it is constexpr-unknown and should not be an error
@t3nsor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The relevant standard language is, I think, "all id-expressions and uses of *this that refer to an object or reference whose lifetime did not begin with the evaluation of E[...]". For a local variable inside a constexpr function, I think it does "begin with the evaluation of E", even if its lifetime hasn't yet started, because it's part of the constexpr call frame. Therefore it's not allowed. I might be missing something, though.
There's also a sort of practical argument: it's weird to say that int &x = x; in a constexpr function is well-defined during constant evaluation, but has undefined behavior at runtime.
That said, the compiler's state is internally consistent either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be extremely strange if the intent of the standard were to not allow this case to be diagnosed. Going by the literal wording, yes, you have an id-expression that refers to a reference whose lifetime didn't begin within the constant evaluation (because it hasn't begun at all), which makes it an unknown reference, but in reality we know exactly what's happening here. It seems like the standard may have a defect and the rule for references really should be that the reference is unknown only if its initialization didn't begin within the evaluation of E.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I mailed core)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
based on cwg453 this is UB and we should not accept it.
| // C++23 [expr.const]p8 | ||
| // ... For such an object that is not usable in constant expressions, the | ||
| // dynamic type of the object is constexpr-unknown. For such a reference that | ||
| // is not usable in constant expressions, the reference is treated as binding | ||
| // to an unspecified object of the referenced type whose lifetime and that of | ||
| // all subobjects includes the entire constant evaluation and whose dynamic | ||
| // type is constexpr-unknown. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove the comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment didn't make sense at that location in the code, given the refactoring. I could stick something into CheckUninitReference, I guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this up to CheckUninitReference
The weird behavior only shows up if you refer to a reference variable inside its own initializer, as far as I can tell. Maybe not worth backporting, even if it is a regression, because people shouldn't be writing code like that in the first place. |
Fair enough, in that case, can you add a changelog entry? Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed review comments. We now reject g3() and g5(), the cases that involved using a local variable in a constexpr function in its own initializer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
Ping |
|
No opinions about the code, but on the reflector we did agree that rejecting those initialization cases was the right thing to do. |
99df9ff to
51bd288
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM modulo nit
clang/lib/AST/ExprConstant.cpp
Outdated
| if (!Result) { | ||
| if (AllowConstexprUnknown) { | ||
| Result = &Info.CurrentCall->createConstexprUnknownAPValues(VD, Base); | ||
| } else { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ping
Usually, in constant evaluation, references which are local to the evaluation have to be initialized before they're accessed. However, there's one funny special case: the initializer of a reference can refer to itself. This generally ends up being undefined behavior if it's used in an evaluated context, but it isn't othewise forbidden. In constant evaluation, this splits into two cases: global variables, and local variables in constexpr functions. This patch handles both of those cases. (Local variables tends to trip other errors in most cases, but if you try hard enough, you can get an accepts-invalid.) Fixes llvm#131330 .
51bd288 to
3d02937
Compare
Usually, in constant evaluation, references which are local to the evaluation have to be initialized before they're accessed. However, there's one funny special case: the initializer of a reference can refer to itself. This generally ends up being undefined behavior if it's used in an evaluated context, but it isn't otherwise forbidden.
In constant evaluation, this splits into two cases: global variables, and local variables in constexpr functions. This patch handles both of those cases. (Local variables tends to trip other errors in most cases, but if you try hard enough, you can get an accepts-invalid.)
Fixes #131330 .