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?= <tbae...@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()); +} + + _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits