[clang][bytecode] Implement placement-new (#107033)

If we have a placement-new destination already, use that instead of
allocating a new one.
Tests are partially based on
`test/SemaCXX/cxx2c-constexpr-placement-new.cpp`.
This commit is contained in:
Timm Baeder 2024-09-23 13:26:49 +02:00 committed by GitHub
parent 26f272ebbd
commit c712ab829b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 406 additions and 46 deletions

View File

@ -3097,12 +3097,11 @@ bool Compiler<Emitter>::VisitCXXNewExpr(const CXXNewExpr *E) {
QualType ElementType = E->getAllocatedType();
std::optional<PrimType> ElemT = classify(ElementType);
unsigned PlacementArgs = E->getNumPlacementArgs();
const FunctionDecl *OperatorNew = E->getOperatorNew();
const Expr *PlacementDest = nullptr;
bool IsNoThrow = false;
// FIXME: Better diagnostic. diag::note_constexpr_new_placement
if (PlacementArgs != 0) {
// The only new-placement list we support is of the form (std::nothrow).
//
// FIXME: There is no restriction on this, but it's not clear that any
// other form makes any sense. We get here for cases such as:
//
@ -3111,15 +3110,30 @@ bool Compiler<Emitter>::VisitCXXNewExpr(const CXXNewExpr *E) {
// (which should presumably be valid only if N is a multiple of
// alignof(int), and in any case can't be deallocated unless N is
// alignof(X) and X has new-extended alignment).
if (PlacementArgs != 1 || !E->getPlacementArg(0)->getType()->isNothrowT())
return this->emitInvalid(E);
if (!this->discard(E->getPlacementArg(0)))
if (PlacementArgs == 1) {
const Expr *Arg1 = E->getPlacementArg(0);
if (Arg1->getType()->isNothrowT()) {
if (!this->discard(Arg1))
return false;
IsNoThrow = true;
} else if (Ctx.getLangOpts().CPlusPlus26 &&
OperatorNew->isReservedGlobalPlacementOperator()) {
// If we have a placement-new destination, we'll later use that instead
// of allocating.
PlacementDest = Arg1;
} else {
return this->emitInvalidNewDeleteExpr(E, E);
}
} else {
return this->emitInvalid(E);
}
} else if (!OperatorNew->isReplaceableGlobalAllocationFunction()) {
return this->emitInvalidNewDeleteExpr(E, E);
}
const Descriptor *Desc;
if (!PlacementDest) {
if (ElemT) {
if (E->isArray())
Desc = nullptr; // We're not going to use it in this case.
@ -3133,6 +3147,7 @@ bool Compiler<Emitter>::VisitCXXNewExpr(const CXXNewExpr *E) {
E->isArray() ? std::nullopt : Descriptor::InlineDescMD,
/*IsConst=*/false, /*IsTemporary=*/false, /*IsMutable=*/false, Init);
}
}
if (E->isArray()) {
std::optional<const Expr *> ArraySizeExpr = E->getArraySize();
@ -3148,6 +3163,14 @@ bool Compiler<Emitter>::VisitCXXNewExpr(const CXXNewExpr *E) {
PrimType SizeT = classifyPrim(Stripped->getType());
if (PlacementDest) {
if (!this->visit(PlacementDest))
return false;
if (!this->visit(Stripped))
return false;
if (!this->emitCheckNewTypeMismatchArray(SizeT, E, E))
return false;
} else {
if (!this->visit(Stripped))
return false;
@ -3160,14 +3183,22 @@ bool Compiler<Emitter>::VisitCXXNewExpr(const CXXNewExpr *E) {
if (!this->emitAllocCN(SizeT, Desc, IsNoThrow, E))
return false;
}
}
if (Init && !this->visitInitializer(Init))
return false;
} else {
if (PlacementDest) {
if (!this->visit(PlacementDest))
return false;
if (!this->emitCheckNewTypeMismatch(E, E))
return false;
} else {
// Allocate just one element.
if (!this->emitAlloc(Desc, E))
return false;
}
if (Init) {
if (ElemT) {
@ -3194,6 +3225,11 @@ template <class Emitter>
bool Compiler<Emitter>::VisitCXXDeleteExpr(const CXXDeleteExpr *E) {
const Expr *Arg = E->getArgument();
const FunctionDecl *OperatorDelete = E->getOperatorDelete();
if (!OperatorDelete->isReplaceableGlobalAllocationFunction())
return this->emitInvalidNewDeleteExpr(E, E);
// Arg must be an lvalue.
if (!this->visit(Arg))
return false;

View File

@ -1286,6 +1286,80 @@ bool CallPtr(InterpState &S, CodePtr OpPC, uint32_t ArgSize,
return Call(S, OpPC, F, VarArgSize);
}
bool CheckNewTypeMismatch(InterpState &S, CodePtr OpPC, const Expr *E,
std::optional<uint64_t> ArraySize) {
const Pointer &Ptr = S.Stk.peek<Pointer>();
if (!CheckStore(S, OpPC, Ptr))
return false;
const auto *NewExpr = cast<CXXNewExpr>(E);
QualType StorageType = Ptr.getType();
if (isa_and_nonnull<CXXNewExpr>(Ptr.getFieldDesc()->asExpr())) {
// FIXME: Are there other cases where this is a problem?
StorageType = StorageType->getPointeeType();
}
const ASTContext &ASTCtx = S.getASTContext();
QualType AllocType;
if (ArraySize) {
AllocType = ASTCtx.getConstantArrayType(
NewExpr->getAllocatedType(),
APInt(64, static_cast<uint64_t>(*ArraySize), false), nullptr,
ArraySizeModifier::Normal, 0);
} else {
AllocType = NewExpr->getAllocatedType();
}
unsigned StorageSize = 1;
unsigned AllocSize = 1;
if (const auto *CAT = dyn_cast<ConstantArrayType>(AllocType))
AllocSize = CAT->getZExtSize();
if (const auto *CAT = dyn_cast<ConstantArrayType>(StorageType))
StorageSize = CAT->getZExtSize();
if (AllocSize > StorageSize ||
!ASTCtx.hasSimilarType(ASTCtx.getBaseElementType(AllocType),
ASTCtx.getBaseElementType(StorageType))) {
S.FFDiag(S.Current->getLocation(OpPC),
diag::note_constexpr_placement_new_wrong_type)
<< StorageType << AllocType;
return false;
}
return true;
}
bool InvalidNewDeleteExpr(InterpState &S, CodePtr OpPC, const Expr *E) {
assert(E);
const auto &Loc = S.Current->getSource(OpPC);
if (const auto *NewExpr = dyn_cast<CXXNewExpr>(E)) {
const FunctionDecl *OperatorNew = NewExpr->getOperatorNew();
if (!S.getLangOpts().CPlusPlus26 && NewExpr->getNumPlacementArgs() > 0) {
S.FFDiag(Loc, diag::note_constexpr_new_placement)
<< /*C++26 feature*/ 1 << E->getSourceRange();
} else if (NewExpr->getNumPlacementArgs() == 1 &&
!OperatorNew->isReservedGlobalPlacementOperator()) {
S.FFDiag(Loc, diag::note_constexpr_new_placement)
<< /*Unsupported*/ 0 << E->getSourceRange();
} else if (!OperatorNew->isReplaceableGlobalAllocationFunction()) {
S.FFDiag(Loc, diag::note_constexpr_new_non_replaceable)
<< isa<CXXMethodDecl>(OperatorNew) << OperatorNew;
}
} else {
const auto *DeleteExpr = cast<CXXDeleteExpr>(E);
const FunctionDecl *OperatorDelete = DeleteExpr->getOperatorDelete();
if (!OperatorDelete->isReplaceableGlobalAllocationFunction()) {
S.FFDiag(Loc, diag::note_constexpr_new_non_replaceable)
<< isa<CXXMethodDecl>(OperatorDelete) << OperatorDelete;
}
}
return false;
}
bool Interpret(InterpState &S, APValue &Result) {
// The current stack frame when we started Interpret().
// This is being used by the ops to determine wheter

View File

@ -2947,6 +2947,17 @@ static inline bool IsConstantContext(InterpState &S, CodePtr OpPC) {
return true;
}
/// Check if the initializer and storage types of a placement-new expression
/// match.
bool CheckNewTypeMismatch(InterpState &S, CodePtr OpPC, const Expr *E,
std::optional<uint64_t> ArraySize = std::nullopt);
template <PrimType Name, class T = typename PrimConv<Name>::T>
bool CheckNewTypeMismatchArray(InterpState &S, CodePtr OpPC, const Expr *E) {
const auto &Size = S.Stk.pop<T>();
return CheckNewTypeMismatch(S, OpPC, E, static_cast<uint64_t>(Size));
}
bool InvalidNewDeleteExpr(InterpState &S, CodePtr OpPC, const Expr *E);
//===----------------------------------------------------------------------===//
// Read opcode arguments
//===----------------------------------------------------------------------===//

View File

@ -787,4 +787,18 @@ def Free : Opcode {
let Args = [ArgBool];
}
def CheckNewTypeMismatch : Opcode {
let Args = [ArgExpr];
}
def InvalidNewDeleteExpr : Opcode {
let Args = [ArgExpr];
}
def CheckNewTypeMismatchArray : Opcode {
let Types = [IntegerTypeClass];
let Args = [ArgExpr];
let HasGroup = 1;
}
def IsConstantContext: Opcode;

View File

@ -241,12 +241,10 @@ namespace std {
/// FIXME: The new interpreter produces the wrong diagnostic.
namespace PlacementNew {
constexpr int foo() { // both-error {{never produces a constant expression}}
char c[sizeof(int)];
new (c) int{12}; // ref-note {{this placement new expression is not supported in constant expressions before C++2c}} \
// expected-note {{subexpression not valid in a constant expression}}
new (c) int{12}; // both-note {{this placement new expression is not supported in constant expressions before C++2c}}
return 0;
}
}
@ -305,31 +303,28 @@ namespace placement_new_delete {
}
static_assert(ok());
/// FIXME: Diagnosting placement new.
constexpr bool bad(int which) {
switch (which) {
case 0:
delete new (placement_new_arg{}) int; // ref-note {{this placement new expression is not supported in constant expressions}} \
// expected-note {{subexpression not valid in a constant expression}}
delete new (placement_new_arg{}) int; // both-note {{this placement new expression is not supported in constant expressions}}
break;
case 1:
delete new ClassSpecificNew; // ref-note {{call to class-specific 'operator new'}}
delete new ClassSpecificNew; // both-note {{call to class-specific 'operator new'}}
break;
case 2:
delete new ClassSpecificDelete; // ref-note {{call to class-specific 'operator delete'}}
delete new ClassSpecificDelete; // both-note {{call to class-specific 'operator delete'}}
break;
case 3:
delete new DestroyingDelete; // ref-note {{call to class-specific 'operator delete'}}
delete new DestroyingDelete; // both-note {{call to class-specific 'operator delete'}}
break;
case 4:
// FIXME: This technically follows the standard's rules, but it seems
// unreasonable to expect implementations to support this.
delete new (std::align_val_t{64}) Overaligned; // ref-note {{this placement new expression is not supported in constant expressions}} \
// expected-note {{subexpression not valid in a constant expression}}
delete new (std::align_val_t{64}) Overaligned; // both-note {{this placement new expression is not supported in constant expressions}}
break;
}
@ -337,9 +332,9 @@ namespace placement_new_delete {
}
static_assert(bad(0)); // both-error {{constant expression}} \
// both-note {{in call}}
static_assert(bad(1)); // ref-error {{constant expression}} ref-note {{in call}}
static_assert(bad(2)); // ref-error {{constant expression}} ref-note {{in call}}
static_assert(bad(3)); // ref-error {{constant expression}} ref-note {{in call}}
static_assert(bad(1)); // both-error {{constant expression}} both-note {{in call}}
static_assert(bad(2)); // both-error {{constant expression}} both-note {{in call}}
static_assert(bad(3)); // both-error {{constant expression}} both-note {{in call}}
static_assert(bad(4)); // both-error {{constant expression}} \
// both-note {{in call}}
}
@ -586,7 +581,6 @@ constexpr void use_after_free_2() { // both-error {{never produces a constant ex
p->f(); // both-note {{member call on heap allocated object that has been deleted}}
}
/// std::allocator definition
namespace std {
using size_t = decltype(sizeof(0));
@ -758,6 +752,18 @@ namespace Limits {
#endif
}
/// Just test that we reject placement-new expressions before C++2c.
/// Tests for successful expressions are in placement-new.cpp
namespace Placement {
consteval auto ok1() { // both-error {{never produces a constant expression}}
bool b;
new (&b) bool(true); // both-note 2{{this placement new expression is not supported in constant expressions before C++2c}}
return b;
}
static_assert(ok1()); // both-error {{not an integral constant expression}} \
// both-note {{in call to}}
}
#else
/// Make sure we reject this prior to C++20
constexpr int a() { // both-error {{never produces a constant expression}}

View File

@ -0,0 +1,219 @@
// RUN: %clang_cc1 -std=c++2c -fcxx-exceptions -fexperimental-new-constant-interpreter -verify=expected,both %s
// RUN: %clang_cc1 -std=c++2c -fcxx-exceptions -verify=ref,both %s
namespace std {
using size_t = decltype(sizeof(0));
}
void *operator new(std::size_t, void *p) { return p; }
void* operator new[] (std::size_t, void* p) {return p;}
consteval auto ok1() {
bool b;
new (&b) bool(true);
return b;
}
static_assert(ok1());
consteval auto ok2() {
int b;
new (&b) int(12);
return b;
}
static_assert(ok2() == 12);
consteval auto ok3() {
float b;
new (&b) float(12.0);
return b;
}
static_assert(ok3() == 12.0);
consteval auto ok4() {
_BitInt(11) b;
new (&b) _BitInt(11)(37);
return b;
}
static_assert(ok4() == 37);
/// FIXME: Broken in both interpreters.
#if 0
consteval int ok5() {
int i;
new (&i) int[1]{1}; // expected-note {{assignment to dereferenced one-past-the-end pointer}}
return i;
}
static_assert(ok5() == 1); // expected-error {{not an integral constant expression}} \
// expected-note {{in call to}}
#endif
/// FIXME: Crashes the current interpreter.
#if 0
consteval int ok6() {
int i[2];
new (&i) int(100);
return i[0];
}
static_assert(ok6() == 100);
#endif
consteval int ok6() {
int i[2];
new (i) int(100);
new (i + 1) int(200);
return i[0] + i[1];
}
static_assert(ok6() == 300);
consteval auto fail1() {
int b;
new (&b) float(1.0); // both-note {{placement new would change type of storage from 'int' to 'float'}}
return b;
}
static_assert(fail1() == 0); // both-error {{not an integral constant expression}} \
// both-note {{in call to}}
consteval int fail2() {
int i;
new (static_cast<void*>(&i)) float(0); // both-note {{placement new would change type of storage from 'int' to 'float'}}
return 0;
}
static_assert(fail2() == 0); // both-error {{not an integral constant expression}} \
// both-note {{in call to}}
consteval int indeterminate() {
int * indeterminate;
new (indeterminate) int(0); // both-note {{read of uninitialized object is not allowed in a constant expression}}
return 0;
}
static_assert(indeterminate() == 0); // both-error {{not an integral constant expression}} \
// both-note {{in call to}}
consteval int array1() {
int i[2];
new (&i) int[]{1,2};
return i[0] + i[1];
}
static_assert(array1() == 3);
consteval int array2() {
int i[2];
new (static_cast<void*>(&i)) int[]{1,2};
return i[0] + i[1];
}
static_assert(array2() == 3);
consteval int array3() {
int i[1];
new (&i) int[2]; // both-note {{placement new would change type of storage from 'int[1]' to 'int[2]'}}
return 0;
}
static_assert(array3() == 0); // both-error {{not an integral constant expression}} \
// both-note {{in call to}}
consteval int array4() {
int i[2];
new (&i) int[]{12};
return i[0];
}
static_assert(array4() == 12);
constexpr int *intptr() {
return new int;
}
constexpr bool yay() {
int *ptr = new (intptr()) int(42);
bool ret = *ptr == 42;
delete ptr;
return ret;
}
static_assert(yay());
constexpr bool blah() {
int *ptr = new (intptr()) int[3]{ 1, 2, 3 }; // both-note {{placement new would change type of storage from 'int' to 'int[3]'}}
bool ret = ptr[0] == 1 && ptr[1] == 2 && ptr[2] == 3;
delete [] ptr;
return ret;
}
static_assert(blah()); // both-error {{not an integral constant expression}} \
// both-note {{in call to 'blah()'}}
constexpr int *get_indeterminate() {
int *evil;
return evil; // both-note {{read of uninitialized object is not allowed in a constant expression}}
}
constexpr bool bleh() {
int *ptr = new (get_indeterminate()) int; // both-note {{in call to 'get_indeterminate()'}}
return true;
}
static_assert(bleh()); // both-error {{not an integral constant expression}} \
// both-note {{in call to 'bleh()'}}
namespace records {
class S {
public:
float f;
};
constexpr bool record1() {
S s(13);
new (&s) S(42);
return s.f == 42;
}
static_assert(record1());
S GlobalS;
constexpr bool record2() {
new (&GlobalS) S(42); // both-note {{a constant expression cannot modify an object that is visible outside that expression}}
return GlobalS.f == 42;
}
static_assert(record2()); // both-error {{not an integral constant expression}} \
// both-note {{in call to}}
constexpr bool record3() {
S ss[3];
new (&ss) S[]{{1}, {2}, {3}};
return ss[0].f == 1 && ss[1].f == 2 && ss[2].f == 3;
}
static_assert(record3());
struct F {
float f;
};
struct R {
F f;
int a;
};
constexpr bool record4() {
R r;
new (&r.f) F{42.0};
new (&r.a) int(12);
return r.f.f == 42.0 && r.a == 12;
}
static_assert(record4());
/// Destructor is NOT called.
struct A {
bool b;
constexpr ~A() { if (b) throw; }
};
constexpr int foo() {
A a;
new (&a) A(true);
new (&a) A(false);
return 0;
}
static_assert(foo() == 0);
}