Skip to content
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] Add __builtin_is_within_lifetime to implement P2641R4's std::is_within_lifetime #91895

Merged
merged 5 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ C++2c Feature Support

- Implemented `P2747R2 constexpr placement new <https://wg21.link/P2747R2>`_.

- Added the ``__builtin_is_within_lifetime`` builtin, which supports
`P2641R4 Checking if a union alternative is active <https://wg21.link/p2641r4>`_

C++23 Feature Support
^^^^^^^^^^^^^^^^^^^^^
- Removed the restriction to literal types in constexpr functions in C++23 mode.
Expand Down
6 changes: 6 additions & 0 deletions clang/include/clang/Basic/Builtins.td
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,12 @@ def IsConstantEvaluated : LangBuiltin<"CXX_LANG"> {
let Prototype = "bool()";
}

def IsWithinLifetime : LangBuiltin<"CXX_LANG"> {
let Spellings = ["__builtin_is_within_lifetime"];
let Attributes = [NoThrow, CustomTypeChecking, Consteval];
let Prototype = "bool(void*)";
}

// GCC exception builtins
def EHReturn : Builtin {
let Spellings = ["__builtin_eh_return"];
Expand Down
12 changes: 9 additions & 3 deletions clang/include/clang/Basic/DiagnosticASTKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,14 @@ def note_constexpr_this : Note<
def access_kind : TextSubstitution<
"%select{read of|read of|assignment to|increment of|decrement of|"
"member call on|dynamic_cast of|typeid applied to|construction of|"
"destruction of}0">;
"destruction of|read of}0">;
def access_kind_subobject : TextSubstitution<
"%select{read of|read of|assignment to|increment of|decrement of|"
"member call on|dynamic_cast of|typeid applied to|"
"construction of subobject of|destruction of}0">;
"construction of subobject of|destruction of|read of}0">;
Copy link
Contributor

Choose a reason for hiding this comment

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

read of is already there (0)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is for the messages shown in places like this:

constexpr struct { mutable int i } s;
static_assert(__builtin_is_within_lifetime(&s.i));
// read of mutable member 'i' is not allowed in a constant expression

This is the same message as AK_Read and AK_ReadObjectRepresentation because is_within_lifetime has basically the same restrictions as reading the value of an object (you "read" the value just to check if it is in lifetime).

Open to other suggestions to make it more clear

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh right. that's good enough then!

def access_kind_volatile : TextSubstitution<
"%select{read of|read of|assignment to|increment of|decrement of|"
"<ERROR>|<ERROR>|<ERROR>|<ERROR>|<ERROR>}0">;
"<ERROR>|<ERROR>|<ERROR>|<ERROR>|<ERROR>|<ERROR>}0">;
def note_constexpr_lifetime_ended : Note<
"%sub{access_kind}0 %select{temporary|variable}1 whose "
"%plural{8:storage duration|:lifetime}0 has ended">;
Expand Down Expand Up @@ -409,6 +409,12 @@ def warn_is_constant_evaluated_always_true_constexpr : Warning<
"'%0' will always evaluate to 'true' in a manifestly constant-evaluated expression">,
InGroup<DiagGroup<"constant-evaluated">>;

def err_invalid_is_within_lifetime : Note<
"'%0' cannot be called with "
"%select{a null pointer|a one-past-the-end pointer|"
"a pointer to an object whose lifetime has not yet begun}1"
>;

// inline asm related.
let CategoryName = "Inline Assembly Issue" in {
def err_asm_invalid_escape : Error<
Expand Down
4 changes: 4 additions & 0 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -12186,6 +12186,10 @@ def err_builtin_launder_invalid_arg : Error<
"%select{non-pointer|function pointer|void pointer}0 argument to "
"'__builtin_launder' is not allowed">;

def err_builtin_is_within_lifetime_invalid_arg : Error<
"%select{non-|function }0pointer argument to '__builtin_is_within_lifetime' "
"is not allowed">;

def err_builtin_invalid_arg_type: Error <
"%ordinal0 argument must be "
"%select{a vector, integer or floating point type|a matrix|"
Expand Down
1 change: 1 addition & 0 deletions clang/lib/AST/ByteCode/State.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum AccessKinds {
AK_TypeId,
AK_Construct,
AK_Destroy,
AK_IsWithinLifetime,
};

/// The order of this enum is important for diagnostics.
Expand Down
109 changes: 106 additions & 3 deletions clang/lib/AST/ExprConstant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,8 @@ CallStackFrame::~CallStackFrame() {
}

static bool isRead(AccessKinds AK) {
return AK == AK_Read || AK == AK_ReadObjectRepresentation;
return AK == AK_Read || AK == AK_ReadObjectRepresentation ||
AK == AK_IsWithinLifetime;
}

static bool isModification(AccessKinds AK) {
Expand All @@ -1532,6 +1533,7 @@ static bool isModification(AccessKinds AK) {
case AK_MemberCall:
case AK_DynamicCast:
case AK_TypeId:
case AK_IsWithinLifetime:
return false;
case AK_Assign:
case AK_Increment:
Expand All @@ -1549,7 +1551,8 @@ static bool isAnyAccess(AccessKinds AK) {

/// Is this an access per the C++ definition?
static bool isFormalAccess(AccessKinds AK) {
return isAnyAccess(AK) && AK != AK_Construct && AK != AK_Destroy;
return isAnyAccess(AK) && AK != AK_Construct && AK != AK_Destroy &&
AK != AK_IsWithinLifetime;
}

/// Is this kind of axcess valid on an indeterminate object value?
Expand All @@ -1561,6 +1564,7 @@ static bool isValidIndeterminateAccess(AccessKinds AK) {
// These need the object's value.
return false;

case AK_IsWithinLifetime:
case AK_ReadObjectRepresentation:
case AK_Assign:
case AK_Construct:
Expand Down Expand Up @@ -3707,7 +3711,8 @@ struct CompleteObject {
// In C++14 onwards, it is permitted to read a mutable member whose
// lifetime began within the evaluation.
// FIXME: Should we also allow this in C++11?
if (!Info.getLangOpts().CPlusPlus14)
if (!Info.getLangOpts().CPlusPlus14 &&
AK != AccessKinds::AK_IsWithinLifetime)
return false;
return lifetimeStartedInEvaluation(Info, Base, /*MutableSubobject*/true);
}
Expand Down Expand Up @@ -3760,6 +3765,12 @@ findSubobject(EvalInfo &Info, const Expr *E, const CompleteObject &Obj,
if ((O->isAbsent() && !(handler.AccessKind == AK_Construct && I == N)) ||
(O->isIndeterminate() &&
!isValidIndeterminateAccess(handler.AccessKind))) {
// Object has ended lifetime.
// If I is non-zero, some subobject (member or array element) of a
// complete object has ended its lifetime, so this is valid for
// IsWithinLifetime, resulting in false.
if (I != 0 && handler.AccessKind == AK_IsWithinLifetime)
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIK you can't use bool here, it will be problem when findSubobject will be used with non-compatible return type

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are already other return false;/return true; returns in findSubobject, so findSubobject::result_type must be constructible from bool

if (!Info.checkingPotentialConstantExpression())
Info.FFDiag(E, diag::note_constexpr_access_uninit)
<< handler.AccessKind << O->isIndeterminate()
Expand Down Expand Up @@ -3927,6 +3938,9 @@ findSubobject(EvalInfo &Info, const Expr *E, const CompleteObject &Obj,
// Placement new onto an inactive union member makes it active.
O->setUnion(Field, APValue());
} else {
// Pointer to/into inactive union member: Not within lifetime
if (handler.AccessKind == AK_IsWithinLifetime)
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

same here

// FIXME: If O->getUnionValue() is absent, report that there's no
// active union member rather than reporting the prior active union
// member. We'll need to fix nullptr_t to not use APValue() as its
Expand Down Expand Up @@ -11684,6 +11698,9 @@ class IntExprEvaluator

bool ZeroInitialization(const Expr *E) { return Success(0, E); }

friend std::optional<bool> EvaluateBuiltinIsWithinLifetime(IntExprEvaluator &,
const CallExpr *);

//===--------------------------------------------------------------------===//
// Visitor Methods
//===--------------------------------------------------------------------===//
Expand Down Expand Up @@ -12743,6 +12760,11 @@ bool IntExprEvaluator::VisitBuiltinCallExpr(const CallExpr *E,
return Success(Info.InConstantContext, E);
}

case Builtin::BI__builtin_is_within_lifetime:
if (auto result = EvaluateBuiltinIsWithinLifetime(*this, E))
return Success(*result, E);
return false;

case Builtin::BI__builtin_ctz:
case Builtin::BI__builtin_ctzl:
case Builtin::BI__builtin_ctzll:
Expand Down Expand Up @@ -17332,3 +17354,84 @@ bool Expr::tryEvaluateStrLen(uint64_t &Result, ASTContext &Ctx) const {
EvalInfo Info(Ctx, Status, EvalInfo::EM_ConstantFold);
return EvaluateBuiltinStrLen(this, Result, Info);
}

namespace {
struct IsWithinLifetimeHandler {
EvalInfo &Info;
static constexpr AccessKinds AccessKind = AccessKinds::AK_IsWithinLifetime;
using result_type = std::optional<bool>;
std::optional<bool> failed() { return std::nullopt; }
template <typename T>
std::optional<bool> found(T &Subobj, QualType SubobjType) {
return true;
}
};

std::optional<bool> EvaluateBuiltinIsWithinLifetime(IntExprEvaluator &IEE,
const CallExpr *E) {
EvalInfo &Info = IEE.Info;
// Sometimes this is called during some sorts of constant folding / early
// evaluation. These are meant for non-constant expressions and are not
// necessary since this consteval builtin will never be evaluated at runtime.
// Just fail to evaluate when not in a constant context.
if (!Info.InConstantContext)
return std::nullopt;
assert(E->getBuiltinCallee() == Builtin::BI__builtin_is_within_lifetime);
const Expr *Arg = E->getArg(0);
if (Arg->isValueDependent())
return std::nullopt;
LValue Val;
if (!EvaluatePointer(Arg, Val, Info))
return std::nullopt;

auto Error = [&](int Diag) {
bool CalledFromStd = false;
const auto *Callee = Info.CurrentCall->getCallee();
if (Callee && Callee->isInStdNamespace()) {
const IdentifierInfo *Identifier = Callee->getIdentifier();
CalledFromStd = Identifier && Identifier->isStr("is_within_lifetime");
}
Info.CCEDiag(CalledFromStd ? Info.CurrentCall->getCallRange().getBegin()
: E->getExprLoc(),
diag::err_invalid_is_within_lifetime)
<< (CalledFromStd ? "std::is_within_lifetime"
: "__builtin_is_within_lifetime")
<< Diag;
return std::nullopt;
};
// C++2c [meta.const.eval]p4:
// During the evaluation of an expression E as a core constant expression, a
// call to this function is ill-formed unless p points to an object that is
// usable in constant expressions or whose complete object's lifetime began
// within E.

// Make sure it points to an object
// nullptr does not point to an object
if (Val.isNullPointer() || Val.getLValueBase().isNull())
return Error(0);
QualType T = Val.getLValueBase().getType();
assert(!T->isFunctionType() &&
"Pointers to functions should have been typed as function pointers "
"which would have been rejected earlier");
assert(T->isObjectType());
// Hypothetical array element is not an object
if (Val.getLValueDesignator().isOnePastTheEnd())
return Error(1);
assert(Val.getLValueDesignator().isValidSubobject() &&
"Unchecked case for valid subobject");
// All other ill-formed values should have failed EvaluatePointer, so the
// object should be a pointer to an object that is usable in a constant
// expression or whose complete lifetime began within the expression
CompleteObject CO =
findCompleteObject(Info, E, AccessKinds::AK_IsWithinLifetime, Val, T);
// The lifetime hasn't begun yet if we are still evaluating the
// initializer ([basic.life]p(1.2))
if (Info.EvaluatingDeclValue && CO.Value == Info.EvaluatingDeclValue)
return Error(2);

if (!CO)
return false;
IsWithinLifetimeHandler handler{Info};
return findSubobject(Info, E, CO, Val.getLValueDesignator(), handler);
Copy link
Contributor

Choose a reason for hiding this comment

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

associated with previous comment ... you are returning false but .failure() will return std::nullopt, so be aware :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it's intentional: std::nullopt -> this call was ill-formed, false -> this is a well-formed call and results in false, true -> this is a well-formed call that results in true. I especially want the second one to return false instead of nullopt

}
} // namespace
3 changes: 3 additions & 0 deletions clang/lib/CodeGen/CGBuiltin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2538,6 +2538,9 @@ static RValue EmitHipStdParUnsupportedBuiltin(CodeGenFunction *CGF,
RValue CodeGenFunction::EmitBuiltinExpr(const GlobalDecl GD, unsigned BuiltinID,
const CallExpr *E,
ReturnValueSlot ReturnValue) {
assert(!getContext().BuiltinInfo.isImmediate(BuiltinID) &&
"Should not codegen for consteval builtins");

const FunctionDecl *FD = GD.getDecl()->getAsFunction();
// See if we can constant fold this builtin. If so, don't emit it at all.
// TODO: Extend this handling to all builtin calls that we can constant-fold.
Expand Down
40 changes: 40 additions & 0 deletions clang/lib/Sema/SemaChecking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,44 @@ static ExprResult BuiltinLaunder(Sema &S, CallExpr *TheCall) {
return TheCall;
}

static ExprResult BuiltinIsWithinLifetime(Sema &S, CallExpr *TheCall) {
if (S.checkArgCount(TheCall, 1))
return ExprError();

ExprResult Arg = S.DefaultFunctionArrayLvalueConversion(TheCall->getArg(0));
if (Arg.isInvalid())
return ExprError();
QualType ParamTy = Arg.get()->getType();
TheCall->setArg(0, Arg.get());
TheCall->setType(S.Context.BoolTy);

// Only accept pointers to objects as arguments, which should have object
// pointer or void pointer types.
if (const auto *PT = ParamTy->getAs<PointerType>()) {
// LWG4138: Function pointer types not allowed
if (PT->getPointeeType()->isFunctionType()) {
S.Diag(TheCall->getArg(0)->getExprLoc(),
diag::err_builtin_is_within_lifetime_invalid_arg)
<< 1;
return ExprError();
}
// Disallow VLAs too since those shouldn't be able to
// be a template parameter for `std::is_within_lifetime`
if (PT->getPointeeType()->isVariableArrayType()) {
S.Diag(TheCall->getArg(0)->getExprLoc(), diag::err_vla_unsupported)
<< 1 << "__builtin_is_within_lifetime";
return ExprError();
}
} else {
S.Diag(TheCall->getArg(0)->getExprLoc(),
diag::err_builtin_is_within_lifetime_invalid_arg)
<< 0;
return ExprError();
}

return TheCall;
}

// Emit an error and return true if the current object format type is in the
// list of unsupported types.
static bool CheckBuiltinTargetNotInUnsupported(
Expand Down Expand Up @@ -2276,6 +2314,8 @@ Sema::CheckBuiltinFunctionCall(FunctionDecl *FDecl, unsigned BuiltinID,
}
case Builtin::BI__builtin_launder:
return BuiltinLaunder(*this, TheCall);
case Builtin::BI__builtin_is_within_lifetime:
return BuiltinIsWithinLifetime(*this, TheCall);
case Builtin::BI__sync_fetch_and_add:
case Builtin::BI__sync_fetch_and_add_1:
case Builtin::BI__sync_fetch_and_add_2:
Expand Down
3 changes: 2 additions & 1 deletion clang/lib/Sema/SemaExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17622,7 +17622,8 @@ HandleImmediateInvocations(Sema &SemaRef,
(SemaRef.inTemplateInstantiation() && !ImmediateEscalating)) {
SemaRef.Diag(DR->getBeginLoc(), diag::err_invalid_consteval_take_address)
<< ND << isa<CXXRecordDecl>(ND) << FD->isConsteval();
SemaRef.Diag(ND->getLocation(), diag::note_declared_at);
if (!FD->getBuiltinID())
SemaRef.Diag(ND->getLocation(), diag::note_declared_at);
if (auto Context =
SemaRef.InnermostDeclarationWithDelayedImmediateInvocations()) {
SemaRef.Diag(Context->Loc, diag::note_invalid_consteval_initializer)
Expand Down
Loading
Loading