Skip to content

Commit

Permalink
[C23] Implement N3018: The constexpr specifier for object definitions (
Browse files Browse the repository at this point in the history
…#73099)

The implementation mostly reuses C++ code paths where possible,
including narrowing check in order to provide diagnostic messages in
case initializer for constexpr variable is not exactly representable in
target type.

The following won't work due to lack of support for other features:
- Diagnosing of underspecified declarations involving constexpr
- Constexpr attached to compound literals

Also due to lack of support for char8_t some of examples with utf-8
strings don't work properly.

Fixes #64742
  • Loading branch information
Fznamznon committed Mar 6, 2024
1 parent 6cdf596 commit aced81c
Show file tree
Hide file tree
Showing 15 changed files with 766 additions and 39 deletions.
3 changes: 3 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ C23 Feature Support
macros typically exposed from ``<inttypes.h>``, such as ``PRIb8``.
(`#81896: <https://github.com/llvm/llvm-project/issues/81896>`_).

- Clang now supports `N3018 The constexpr specifier for object definitions`
<https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3018.htm>`_.

Non-comprehensive list of changes in this release
-------------------------------------------------

Expand Down
11 changes: 11 additions & 0 deletions clang/include/clang/AST/Expr.h
Original file line number Diff line number Diff line change
Expand Up @@ -1875,6 +1875,17 @@ class StringLiteral final
llvm_unreachable("Unsupported character width!");
}

// Get code unit but preserve sign info.
int64_t getCodeUnitS(size_t I, uint64_t BitWidth) const {
int64_t V = getCodeUnit(I);
if (isOrdinary() || isWide()) {
unsigned Width = getCharByteWidth() * BitWidth;
llvm::APInt AInt(Width, (uint64_t)V);
V = AInt.getSExtValue();
}
return V;
}

unsigned getByteLength() const { return getCharByteWidth() * getLength(); }
unsigned getLength() const { return *getTrailingObjects<unsigned>(); }
unsigned getCharByteWidth() const { return StringLiteralBits.CharByteWidth; }
Expand Down
12 changes: 12 additions & 0 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -2946,6 +2946,18 @@ def warn_private_extern : Warning<
def note_private_extern : Note<
"use __attribute__((visibility(\"hidden\"))) attribute instead">;

// C23 constexpr
def err_c23_constexpr_not_variable : Error<
"'constexpr' can only be used in variable declarations">;
def err_c23_constexpr_invalid_type : Error<
"constexpr variable cannot have type %0">;
def err_c23_constexpr_init_not_representable : Error<
"constexpr initializer evaluates to %0 which is not exactly representable in type %1">;
def err_c23_constexpr_init_type_mismatch : Error<
"constexpr initializer for type %0 is of type %1">;
def err_c23_constexpr_pointer_not_null : Error<
"constexpr pointer initializer is not null">;

// C++ Concepts
def err_concept_decls_may_only_appear_in_global_namespace_scope : Error<
"concept declarations may only appear in global or namespace scope">;
Expand Down
2 changes: 1 addition & 1 deletion clang/include/clang/Basic/TokenKinds.def
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ CXX11_KEYWORD(alignas , KEYC23)
CXX11_UNARY_EXPR_OR_TYPE_TRAIT(alignof, AlignOf, KEYC23)
CXX11_KEYWORD(char16_t , KEYNOMS18)
CXX11_KEYWORD(char32_t , KEYNOMS18)
CXX11_KEYWORD(constexpr , 0)
CXX11_KEYWORD(constexpr , KEYC23)
CXX11_KEYWORD(decltype , 0)
CXX11_KEYWORD(noexcept , 0)
CXX11_KEYWORD(nullptr , KEYC23)
Expand Down
25 changes: 16 additions & 9 deletions clang/lib/AST/Decl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2465,7 +2465,7 @@ bool VarDecl::mightBeUsableInConstantExpressions(const ASTContext &C) const {

// OpenCL permits const integral variables to be used in constant
// expressions, like in C++98.
if (!Lang.CPlusPlus && !Lang.OpenCL)
if (!Lang.CPlusPlus && !Lang.OpenCL && !Lang.C23)
return false;

// Function parameters are never usable in constant expressions.
Expand All @@ -2487,14 +2487,19 @@ bool VarDecl::mightBeUsableInConstantExpressions(const ASTContext &C) const {
if (!getType().isConstant(C) || getType().isVolatileQualified())
return false;

// In C++, const, non-volatile variables of integral or enumeration types
// can be used in constant expressions.
if (getType()->isIntegralOrEnumerationType())
// In C++, but not in C, const, non-volatile variables of integral or
// enumeration types can be used in constant expressions.
if (getType()->isIntegralOrEnumerationType() && !Lang.C23)
return true;

// C23 6.6p7: An identifier that is:
// ...
// - declared with storage-class specifier constexpr and has an object type,
// is a named constant, ... such a named constant is a constant expression
// with the type and value of the declared object.
// Additionally, in C++11, non-volatile constexpr variables can be used in
// constant expressions.
return Lang.CPlusPlus11 && isConstexpr();
return (Lang.CPlusPlus11 || Lang.C23) && isConstexpr();
}

bool VarDecl::isUsableInConstantExpressions(const ASTContext &Context) const {
Expand Down Expand Up @@ -2572,11 +2577,11 @@ APValue *VarDecl::evaluateValueImpl(SmallVectorImpl<PartialDiagnosticAt> &Notes,
bool Result = Init->EvaluateAsInitializer(Eval->Evaluated, Ctx, this, Notes,
IsConstantInitialization);

// In C++, this isn't a constant initializer if we produced notes. In that
// In C++/C23, this isn't a constant initializer if we produced notes. In that
// case, we can't keep the result, because it may only be correct under the
// assumption that the initializer is a constant context.
if (IsConstantInitialization && Ctx.getLangOpts().CPlusPlus &&
!Notes.empty())
if (IsConstantInitialization &&
(Ctx.getLangOpts().CPlusPlus || Ctx.getLangOpts().C23) && !Notes.empty())
Result = false;

// Ensure the computed APValue is cleaned up later if evaluation succeeded,
Expand Down Expand Up @@ -2634,7 +2639,9 @@ bool VarDecl::checkForConstantInitialization(
// std::is_constant_evaluated()).
assert(!Eval->WasEvaluated &&
"already evaluated var value before checking for constant init");
assert(getASTContext().getLangOpts().CPlusPlus && "only meaningful in C++");
assert((getASTContext().getLangOpts().CPlusPlus ||
getASTContext().getLangOpts().C23) &&
"only meaningful in C++/C23");

assert(!getInit()->isValueDependent());

Expand Down
10 changes: 9 additions & 1 deletion clang/lib/AST/ExprConstant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4133,6 +4133,10 @@ static CompleteObject findCompleteObject(EvalInfo &Info, const Expr *E,
}

bool IsConstant = BaseType.isConstant(Info.Ctx);
bool ConstexprVar = false;
if (const auto *VD = dyn_cast_if_present<VarDecl>(
Info.EvaluatingDecl.dyn_cast<const ValueDecl *>()))
ConstexprVar = VD->isConstexpr();

// Unless we're looking at a local variable or argument in a constexpr call,
// the variable we're reading must be const.
Expand All @@ -4152,6 +4156,9 @@ static CompleteObject findCompleteObject(EvalInfo &Info, const Expr *E,
return CompleteObject();
} else if (VD->isConstexpr()) {
// OK, we can read this variable.
} else if (Info.getLangOpts().C23 && ConstexprVar) {
Info.FFDiag(E);
return CompleteObject();
} else if (BaseType->isIntegralOrEnumerationType()) {
if (!IsConstant) {
if (!IsAccess)
Expand Down Expand Up @@ -15826,7 +15833,8 @@ bool Expr::EvaluateAsInitializer(APValue &Value, const ASTContext &Ctx,
EStatus.Diag = &Notes;

EvalInfo Info(Ctx, EStatus,
(IsConstantInitialization && Ctx.getLangOpts().CPlusPlus)
(IsConstantInitialization &&
(Ctx.getLangOpts().CPlusPlus || Ctx.getLangOpts().C23))
? EvalInfo::EM_ConstantExpression
: EvalInfo::EM_ConstantFold);
Info.setEvaluatingDecl(VD, Value);
Expand Down
2 changes: 2 additions & 0 deletions clang/lib/Parse/ParseDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4265,6 +4265,8 @@ void Parser::ParseDeclarationSpecifiers(

// constexpr, consteval, constinit specifiers
case tok::kw_constexpr:
if (getLangOpts().C23)
Diag(Tok, diag::warn_c23_compat_keyword) << Tok.getName();
isInvalid = DS.SetConstexprSpec(ConstexprSpecKind::Constexpr, Loc,
PrevSpec, DiagID);
break;
Expand Down
14 changes: 14 additions & 0 deletions clang/lib/Sema/DeclSpec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,20 @@ void DeclSpec::Finish(Sema &S, const PrintingPolicy &Policy) {
ThreadStorageClassSpec = TSCS_unspecified;
ThreadStorageClassSpecLoc = SourceLocation();
}
if (S.getLangOpts().C23 &&
getConstexprSpecifier() == ConstexprSpecKind::Constexpr) {
S.Diag(ConstexprLoc, diag::err_invalid_decl_spec_combination)
<< DeclSpec::getSpecifierName(getThreadStorageClassSpec())
<< SourceRange(getThreadStorageClassSpecLoc());
}
}

if (S.getLangOpts().C23 &&
getConstexprSpecifier() == ConstexprSpecKind::Constexpr &&
StorageClassSpec == SCS_extern) {
S.Diag(ConstexprLoc, diag::err_invalid_decl_spec_combination)
<< DeclSpec::getSpecifierName(getStorageClassSpec())
<< SourceRange(getStorageClassSpecLoc());
}

// If no type specifier was provided and we're parsing a language where
Expand Down
79 changes: 66 additions & 13 deletions clang/lib/Sema/SemaDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5147,6 +5147,8 @@ Decl *Sema::ParsedFreeStandingDeclSpec(Scope *S, AccessSpecifier AS,
Diag(DS.getConstexprSpecLoc(), diag::err_constexpr_tag)
<< GetDiagnosticTypeSpecifierID(DS)
<< static_cast<int>(DS.getConstexprSpecifier());
else if (getLangOpts().C23)
Diag(DS.getConstexprSpecLoc(), diag::err_c23_constexpr_not_variable);
else
Diag(DS.getConstexprSpecLoc(), diag::err_constexpr_wrong_decl_kind)
<< static_cast<int>(DS.getConstexprSpecifier());
Expand Down Expand Up @@ -8649,6 +8651,38 @@ static bool checkForConflictWithNonVisibleExternC(Sema &S, const T *ND,
return false;
}

static bool CheckC23ConstexprVarType(Sema &SemaRef, SourceLocation VarLoc,
QualType T) {
QualType CanonT = SemaRef.Context.getCanonicalType(T);
// C23 6.7.1p5: An object declared with storage-class specifier constexpr or
// any of its members, even recursively, shall not have an atomic type, or a
// variably modified type, or a type that is volatile or restrict qualified.
if (CanonT->isVariablyModifiedType()) {
SemaRef.Diag(VarLoc, diag::err_c23_constexpr_invalid_type) << T;
return true;
}

// Arrays are qualified by their element type, so get the base type (this
// works on non-arrays as well).
CanonT = SemaRef.Context.getBaseElementType(CanonT);

if (CanonT->isAtomicType() || CanonT.isVolatileQualified() ||
CanonT.isRestrictQualified()) {
SemaRef.Diag(VarLoc, diag::err_c23_constexpr_invalid_type) << T;
return true;
}

if (CanonT->isRecordType()) {
const RecordDecl *RD = CanonT->getAsRecordDecl();
if (llvm::any_of(RD->fields(), [&SemaRef, VarLoc](const FieldDecl *F) {
return CheckC23ConstexprVarType(SemaRef, VarLoc, F->getType());
}))
return true;
}

return false;
}

void Sema::CheckVariableDeclarationType(VarDecl *NewVD) {
// If the decl is already known invalid, don't check it.
if (NewVD->isInvalidDecl())
Expand Down Expand Up @@ -8899,6 +8933,12 @@ void Sema::CheckVariableDeclarationType(VarDecl *NewVD) {
return;
}

if (getLangOpts().C23 && NewVD->isConstexpr() &&
CheckC23ConstexprVarType(*this, NewVD->getLocation(), T)) {
NewVD->setInvalidDecl();
return;
}

if (NewVD->isConstexpr() && !T->isDependentType() &&
RequireLiteralType(NewVD->getLocation(), T,
diag::err_constexpr_var_non_literal)) {
Expand Down Expand Up @@ -9281,6 +9321,22 @@ static FunctionDecl *CreateNewFunctionDecl(Sema &SemaRef, Declarator &D,
FunctionDecl *NewFD = nullptr;
bool isInline = D.getDeclSpec().isInlineSpecified();

ConstexprSpecKind ConstexprKind = D.getDeclSpec().getConstexprSpecifier();
if (ConstexprKind == ConstexprSpecKind::Constinit ||
(SemaRef.getLangOpts().C23 &&
ConstexprKind == ConstexprSpecKind::Constexpr)) {

if (SemaRef.getLangOpts().C23)
SemaRef.Diag(D.getDeclSpec().getConstexprSpecLoc(),
diag::err_c23_constexpr_not_variable);
else
SemaRef.Diag(D.getDeclSpec().getConstexprSpecLoc(),
diag::err_constexpr_wrong_decl_kind)
<< static_cast<int>(ConstexprKind);
ConstexprKind = ConstexprSpecKind::Unspecified;
D.getMutableDeclSpec().ClearConstexprSpec();
}

if (!SemaRef.getLangOpts().CPlusPlus) {
// Determine whether the function was written with a prototype. This is
// true when:
Expand Down Expand Up @@ -9314,15 +9370,6 @@ static FunctionDecl *CreateNewFunctionDecl(Sema &SemaRef, Declarator &D,
}

ExplicitSpecifier ExplicitSpecifier = D.getDeclSpec().getExplicitSpecifier();

ConstexprSpecKind ConstexprKind = D.getDeclSpec().getConstexprSpecifier();
if (ConstexprKind == ConstexprSpecKind::Constinit) {
SemaRef.Diag(D.getDeclSpec().getConstexprSpecLoc(),
diag::err_constexpr_wrong_decl_kind)
<< static_cast<int>(ConstexprKind);
ConstexprKind = ConstexprSpecKind::Unspecified;
D.getMutableDeclSpec().ClearConstexprSpec();
}
Expr *TrailingRequiresClause = D.getTrailingRequiresClause();

SemaRef.CheckExplicitObjectMemberFunction(DC, D, Name, R);
Expand Down Expand Up @@ -13909,7 +13956,9 @@ void Sema::AddInitializerToDecl(Decl *RealDecl, Expr *Init, bool DirectInit) {
VDecl->setStorageClass(SC_Extern);

// C99 6.7.8p4. All file scoped initializers need to be constant.
if (!getLangOpts().CPlusPlus && !VDecl->isInvalidDecl())
// Avoid duplicate diagnostics for constexpr variables.
if (!getLangOpts().CPlusPlus && !VDecl->isInvalidDecl() &&
!VDecl->isConstexpr())
CheckForConstantInitializer(Init, DclT);
}

Expand Down Expand Up @@ -14520,17 +14569,21 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) {
QualType baseType = Context.getBaseElementType(type);
bool HasConstInit = true;

if (getLangOpts().C23 && var->isConstexpr() && !Init)
Diag(var->getLocation(), diag::err_constexpr_var_requires_const_init)
<< var;

// Check whether the initializer is sufficiently constant.
if (getLangOpts().CPlusPlus && !type->isDependentType() && Init &&
!Init->isValueDependent() &&
if ((getLangOpts().CPlusPlus || (getLangOpts().C23 && var->isConstexpr())) &&
!type->isDependentType() && Init && !Init->isValueDependent() &&
(GlobalStorage || var->isConstexpr() ||
var->mightBeUsableInConstantExpressions(Context))) {
// If this variable might have a constant initializer or might be usable in
// constant expressions, check whether or not it actually is now. We can't
// do this lazily, because the result might depend on things that change
// later, such as which constexpr functions happen to be defined.
SmallVector<PartialDiagnosticAt, 8> Notes;
if (!getLangOpts().CPlusPlus11) {
if (!getLangOpts().CPlusPlus11 && !getLangOpts().C23) {
// Prior to C++11, in contexts where a constant initializer is required,
// the set of valid constant initializers is described by syntactic rules
// in [expr.const]p2-6.
Expand Down

0 comments on commit aced81c

Please sign in to comment.