[clang] [clang][bytecode] Implement placement-new (PR #107033)
Timm Baeder via cfe-commits
cfe-commits at lists.llvm.org
Mon Sep 2 23:11:29 PDT 2024
https://github.com/tbaederr updated https://github.com/llvm/llvm-project/pull/107033
>From be9096f14454cf1450915bd044aef1c037c3e935 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Tue, 3 Sep 2024 02:52:35 +0200
Subject: [PATCH] [clang][bytecode] Implement placement-new
---
clang/lib/AST/ByteCode/Compiler.cpp | 98 ++++++++----
clang/lib/AST/ByteCode/Interp.cpp | 75 +++++++++
clang/lib/AST/ByteCode/Interp.h | 11 ++
clang/lib/AST/ByteCode/Opcodes.td | 14 ++
clang/test/AST/ByteCode/new-delete.cpp | 35 +++--
clang/test/AST/ByteCode/placement-new.cpp | 177 ++++++++++++++++++++++
6 files changed, 365 insertions(+), 45 deletions(-)
create mode 100644 clang/test/AST/ByteCode/placement-new.cpp
diff --git a/clang/lib/AST/ByteCode/Compiler.cpp b/clang/lib/AST/ByteCode/Compiler.cpp
index 554e23e272e41c..0f23cc52e0804e 100644
--- a/clang/lib/AST/ByteCode/Compiler.cpp
+++ b/clang/lib/AST/ByteCode/Compiler.cpp
@@ -2821,12 +2821,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:
//
@@ -2835,27 +2834,43 @@ 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 (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);
+ }
- if (!this->discard(E->getPlacementArg(0)))
- return false;
- IsNoThrow = true;
+ } else {
+ return this->emitInvalid(E);
+ }
+ } else if (!OperatorNew->isReplaceableGlobalAllocationFunction()) {
+ return this->emitInvalidNewDeleteExpr(E, E);
}
const Descriptor *Desc;
- if (ElemT) {
- if (E->isArray())
- Desc = nullptr; // We're not going to use it in this case.
- else
- Desc = P.createDescriptor(E, *ElemT, Descriptor::InlineDescMD,
- /*IsConst=*/false, /*IsTemporary=*/false,
- /*IsMutable=*/false);
- } else {
- Desc = P.createDescriptor(
- E, ElementType.getTypePtr(),
- E->isArray() ? std::nullopt : Descriptor::InlineDescMD,
- /*IsConst=*/false, /*IsTemporary=*/false, /*IsMutable=*/false, Init);
+ if (!PlacementDest) {
+ if (ElemT) {
+ if (E->isArray())
+ Desc = nullptr; // We're not going to use it in this case.
+ else
+ Desc = P.createDescriptor(E, *ElemT, Descriptor::InlineDescMD,
+ /*IsConst=*/false, /*IsTemporary=*/false,
+ /*IsMutable=*/false);
+ } else {
+ Desc = P.createDescriptor(
+ E, ElementType.getTypePtr(),
+ E->isArray() ? std::nullopt : Descriptor::InlineDescMD,
+ /*IsConst=*/false, /*IsTemporary=*/false, /*IsMutable=*/false, Init);
+ }
}
if (E->isArray()) {
@@ -2872,26 +2887,42 @@ bool Compiler<Emitter>::VisitCXXNewExpr(const CXXNewExpr *E) {
PrimType SizeT = classifyPrim(Stripped->getType());
- if (!this->visit(Stripped))
- return false;
-
- if (ElemT) {
- // N primitive elements.
- if (!this->emitAllocN(SizeT, *ElemT, E, IsNoThrow, E))
+ if (PlacementDest) {
+ if (!this->visit(PlacementDest))
+ return false;
+ if (!this->visit(Stripped))
+ return false;
+ if (!this->emitCheckNewTypeMismatchArray(SizeT, E, E))
return false;
} else {
- // N Composite elements.
- if (!this->emitAllocCN(SizeT, Desc, IsNoThrow, E))
+ if (!this->visit(Stripped))
return false;
+
+ if (ElemT) {
+ // N primitive elements.
+ if (!this->emitAllocN(SizeT, *ElemT, E, IsNoThrow, E))
+ return false;
+ } else {
+ // N Composite elements.
+ if (!this->emitAllocCN(SizeT, Desc, IsNoThrow, E))
+ return false;
+ }
}
if (Init && !this->visitInitializer(Init))
return false;
} else {
- // Allocate just one element.
- if (!this->emitAlloc(Desc, E))
- return false;
+ 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) {
@@ -2918,6 +2949,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;
diff --git a/clang/lib/AST/ByteCode/Interp.cpp b/clang/lib/AST/ByteCode/Interp.cpp
index 30ccceb42eb374..26f46ddde52d22 100644
--- a/clang/lib/AST/ByteCode/Interp.cpp
+++ b/clang/lib/AST/ByteCode/Interp.cpp
@@ -986,6 +986,81 @@ void diagnoseEnumValue(InterpState &S, CodePtr OpPC, const EnumDecl *ED,
}
}
+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
diff --git a/clang/lib/AST/ByteCode/Interp.h b/clang/lib/AST/ByteCode/Interp.h
index c1423a060bcb97..04553b2b2ea3e1 100644
--- a/clang/lib/AST/ByteCode/Interp.h
+++ b/clang/lib/AST/ByteCode/Interp.h
@@ -3151,6 +3151,17 @@ inline bool CheckLiteralType(InterpState &S, CodePtr OpPC, const Type *T) {
return false;
}
+/// 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
//===----------------------------------------------------------------------===//
diff --git a/clang/lib/AST/ByteCode/Opcodes.td b/clang/lib/AST/ByteCode/Opcodes.td
index 46247688d4ef85..5588418826055a 100644
--- a/clang/lib/AST/ByteCode/Opcodes.td
+++ b/clang/lib/AST/ByteCode/Opcodes.td
@@ -786,4 +786,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;
diff --git a/clang/test/AST/ByteCode/new-delete.cpp b/clang/test/AST/ByteCode/new-delete.cpp
index 145bb366710f9b..1d5d9fac175ad9 100644
--- a/clang/test/AST/ByteCode/new-delete.cpp
+++ b/clang/test/AST/ByteCode/new-delete.cpp
@@ -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,6 +581,18 @@ 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}}
}
+/// 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}}
diff --git a/clang/test/AST/ByteCode/placement-new.cpp b/clang/test/AST/ByteCode/placement-new.cpp
new file mode 100644
index 00000000000000..1fcbdd706b18b6
--- /dev/null
+++ b/clang/test/AST/ByteCode/placement-new.cpp
@@ -0,0 +1,177 @@
+// RUN: %clang_cc1 -std=c++2c -fexperimental-new-constant-interpreter -verify=expected,both %s
+// RUN: %clang_cc1 -std=c++2c -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);
+
+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());
+}
+
+
More information about the cfe-commits
mailing list