[clang] [clang][Interp] Implement dynamic memory allocation handling (PR #70306)
Timm Baeder via cfe-commits
cfe-commits at lists.llvm.org
Thu Oct 26 04:18:09 PDT 2023
Timm =?utf-8?q?Bäder?= <tbaeder at redhat.com>
Message-ID:
In-Reply-To: <llvm/llvm-project/pull/70306/clang at github.com>
https://github.com/tbaederr updated https://github.com/llvm/llvm-project/pull/70306
>From 33d24cd22642d2a6f9b00aaa3826472381e93d33 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Wed, 25 Oct 2023 08:33:30 +0200
Subject: [PATCH 1/2] [clang][Interp] Implement dynamic memory allocation
handling
---
clang/lib/AST/CMakeLists.txt | 1 +
clang/lib/AST/Interp/ByteCodeExprGen.cpp | 76 +++++++
clang/lib/AST/Interp/ByteCodeExprGen.h | 2 +
clang/lib/AST/Interp/Context.cpp | 2 +-
clang/lib/AST/Interp/DynamicAllocator.cpp | 91 ++++++++
clang/lib/AST/Interp/DynamicAllocator.h | 90 ++++++++
clang/lib/AST/Interp/EvalEmitter.cpp | 4 +-
clang/lib/AST/Interp/Interp.cpp | 37 ++++
clang/lib/AST/Interp/Interp.h | 94 ++++++++
clang/lib/AST/Interp/InterpState.cpp | 15 ++
clang/lib/AST/Interp/InterpState.h | 10 +
clang/lib/AST/Interp/Opcodes.td | 24 +++
clang/test/AST/Interp/new-delete.cpp | 247 ++++++++++++++++++++++
13 files changed, 690 insertions(+), 3 deletions(-)
create mode 100644 clang/lib/AST/Interp/DynamicAllocator.cpp
create mode 100644 clang/lib/AST/Interp/DynamicAllocator.h
create mode 100644 clang/test/AST/Interp/new-delete.cpp
diff --git a/clang/lib/AST/CMakeLists.txt b/clang/lib/AST/CMakeLists.txt
index fe3f8c485ec1c56..1423623fb038cab 100644
--- a/clang/lib/AST/CMakeLists.txt
+++ b/clang/lib/AST/CMakeLists.txt
@@ -75,6 +75,7 @@ add_clang_library(clangAST
Interp/Function.cpp
Interp/InterpBuiltin.cpp
Interp/Floating.cpp
+ Interp/DynamicAllocator.cpp
Interp/Interp.cpp
Interp/InterpBlock.cpp
Interp/InterpFrame.cpp
diff --git a/clang/lib/AST/Interp/ByteCodeExprGen.cpp b/clang/lib/AST/Interp/ByteCodeExprGen.cpp
index 1b33c69b93aa4b9..c42fcabe3b075ba 100644
--- a/clang/lib/AST/Interp/ByteCodeExprGen.cpp
+++ b/clang/lib/AST/Interp/ByteCodeExprGen.cpp
@@ -1617,6 +1617,82 @@ bool ByteCodeExprGen<Emitter>::VisitCXXScalarValueInitExpr(
return this->visitZeroInitializer(classifyPrim(Ty), Ty, E);
}
+template <class Emitter>
+bool ByteCodeExprGen<Emitter>::VisitCXXNewExpr(const CXXNewExpr *E) {
+ assert(classifyPrim(E->getType()) == PT_Ptr);
+ const Expr *Init = E->getInitializer();
+ QualType ElementType = E->getAllocatedType();
+ std::optional<PrimType> ElemT = classify(ElementType);
+
+ 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 (E->isArray()) {
+ assert(E->getArraySize());
+ PrimType SizeT = classifyPrim((*E->getArraySize())->getType());
+
+ if (!this->visit(*E->getArraySize()))
+ return false;
+
+ if (ElemT) {
+ // N primitive elements.
+ if (!this->emitAllocN(SizeT, *ElemT, E, E))
+ return false;
+ } else {
+ // N Composite elements.
+ if (!this->emitAllocCN(SizeT, Desc, E))
+ return false;
+ }
+
+ } else {
+ // Allocate just one element.
+ if (!this->emitAlloc(Desc, E))
+ return false;
+
+ if (Init) {
+ if (ElemT) {
+ if (!this->visit(Init))
+ return false;
+
+ if (!this->emitInit(*ElemT, E))
+ return false;
+ } else {
+ // Composite.
+ if (!this->visitInitializer(Init))
+ return false;
+ }
+ }
+ }
+
+ if (DiscardResult)
+ return this->emitPopPtr(E);
+
+ return true;
+}
+
+template <class Emitter>
+bool ByteCodeExprGen<Emitter>::VisitCXXDeleteExpr(const CXXDeleteExpr *E) {
+ const Expr *Arg = E->getArgument();
+
+ // Arg must be an lvalue.
+ if (!this->visit(Arg))
+ return false;
+
+ return this->emitFree(E->isArrayForm(), E);
+}
+
template <class Emitter> bool ByteCodeExprGen<Emitter>::discard(const Expr *E) {
if (E->containsErrors())
return false;
diff --git a/clang/lib/AST/Interp/ByteCodeExprGen.h b/clang/lib/AST/Interp/ByteCodeExprGen.h
index 83986d3dd579ed6..f65dc9494db36ef 100644
--- a/clang/lib/AST/Interp/ByteCodeExprGen.h
+++ b/clang/lib/AST/Interp/ByteCodeExprGen.h
@@ -107,6 +107,8 @@ class ByteCodeExprGen : public ConstStmtVisitor<ByteCodeExprGen<Emitter>, bool>,
bool VisitSourceLocExpr(const SourceLocExpr *E);
bool VisitOffsetOfExpr(const OffsetOfExpr *E);
bool VisitCXXScalarValueInitExpr(const CXXScalarValueInitExpr *E);
+ bool VisitCXXNewExpr(const CXXNewExpr *E);
+ bool VisitCXXDeleteExpr(const CXXDeleteExpr *E);
protected:
bool visitExpr(const Expr *E) override;
diff --git a/clang/lib/AST/Interp/Context.cpp b/clang/lib/AST/Interp/Context.cpp
index cb96e56fb5e1ad8..ede14de30419d0b 100644
--- a/clang/lib/AST/Interp/Context.cpp
+++ b/clang/lib/AST/Interp/Context.cpp
@@ -164,7 +164,7 @@ bool Context::Run(State &Parent, const Function *Func, APValue &Result) {
State.Current = new InterpFrame(State, Func, /*Caller=*/nullptr, {});
if (Interpret(State, Result)) {
assert(Stk.empty());
- return true;
+ return !State.maybeDiagnoseDanglingAllocations();
}
// State gets destroyed here, so the Stk.clear() below doesn't accidentally
diff --git a/clang/lib/AST/Interp/DynamicAllocator.cpp b/clang/lib/AST/Interp/DynamicAllocator.cpp
new file mode 100644
index 000000000000000..a353d09d1f960ea
--- /dev/null
+++ b/clang/lib/AST/Interp/DynamicAllocator.cpp
@@ -0,0 +1,91 @@
+//==---- DynamicAllocator.cpp - Types for the constexpr VM -------*- C++ -*-==//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "DynamicAllocator.h"
+#include "InterpBlock.h"
+#include "InterpState.h"
+
+using namespace clang;
+using namespace clang::interp;
+
+Block *DynamicAllocator::allocate(const Expr *Source, PrimType T,
+ size_t NumElements) {
+ assert(NumElements > 0);
+ // Create a new descriptor for an array of the specified size and
+ // element type.
+ const Descriptor *D = allocateDescriptor(
+ Source, T, Descriptor::InlineDescMD, NumElements, /*IsConst=*/false,
+ /*IsTemporary=*/false, /*IsMutable=*/false);
+ return allocate(D);
+}
+
+Block *DynamicAllocator::allocate(const Descriptor *ElementDesc,
+ size_t NumElements) {
+ assert(NumElements > 0);
+ // Create a new descriptor for an array of the specified size and
+ // element type.
+ const Descriptor *D = allocateDescriptor(
+ ElementDesc->asExpr(), ElementDesc, Descriptor::InlineDescMD, NumElements,
+ /*IsConst=*/false, /*IsTemporary=*/false, /*IsMutable=*/false);
+ return allocate(D);
+}
+
+Block *DynamicAllocator::allocate(const Descriptor *D) {
+ assert(D->asExpr());
+
+ auto Memory =
+ std::make_unique<std::byte[]>(sizeof(Block) + D->getAllocSize());
+ auto *B = new (Memory.get()) Block(D, /*isStatic=*/false);
+ B->invokeCtor();
+
+ InlineDescriptor *ID = reinterpret_cast<InlineDescriptor *>(B->rawData());
+ ID->Desc = const_cast<Descriptor *>(D);
+ ID->IsActive = true;
+ ID->Offset = sizeof(InlineDescriptor);
+ ID->IsBase = false;
+ ID->IsFieldMutable = false;
+ ID->IsConst = false;
+ ID->IsInitialized = false;
+ assert(ID->Desc);
+
+ if (auto It = AllocationSites.find(D->asExpr()); It != AllocationSites.end())
+ It->second.Allocations.emplace_back(std::move(Memory));
+ else
+ AllocationSites.insert(
+ {D->asExpr(), AllocationSite(std::move(Memory), D->isArray())});
+ return B;
+}
+
+void DynamicAllocator::deallocate(const Expr *Source,
+ const Block *BlockToDelete, InterpState &S) {
+ assert(AllocationSites.contains(Source));
+
+ auto It = AllocationSites.find(Source);
+ assert(It != AllocationSites.end());
+
+ auto &Site = It->second;
+ assert(Site.size() > 0);
+
+ // Find the Block to delete.
+ size_t I = 0;
+ [[maybe_unused]] bool Found = false;
+ for (auto &A : Site.Allocations) {
+ Block *B = reinterpret_cast<Block *>(A.Memory.get());
+ if (B == BlockToDelete) {
+ S.deallocate(B);
+ B->invokeDtor();
+ Site.Allocations.erase(Site.Allocations.begin() + I);
+ Found = true;
+ break;
+ }
+ }
+ assert(Found);
+
+ if (Site.size() == 0)
+ AllocationSites.erase(It);
+}
diff --git a/clang/lib/AST/Interp/DynamicAllocator.h b/clang/lib/AST/Interp/DynamicAllocator.h
new file mode 100644
index 000000000000000..d2a1d96232b9730
--- /dev/null
+++ b/clang/lib/AST/Interp/DynamicAllocator.h
@@ -0,0 +1,90 @@
+//==- DynamicAllocator.h - Bytecode allocator for the constexpr VM -*- C++
+//-*-=//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_AST_INTERP_DYNAMIC_ALLOCATOR_H
+#define LLVM_CLANG_AST_INTERP_DYNAMIC_ALLOCATOR_H
+
+#include "Descriptor.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/iterator_range.h"
+#include "llvm/Support/Allocator.h"
+
+namespace clang {
+class Expr;
+namespace interp {
+class Block;
+class InterpState;
+
+/// Manages dynamic memory allocations done during bytecode interpretation.
+///
+/// We manage allocations as a map from their new-expression to a list
+/// of allocations. This is called an AllocationSite. For each site, we
+/// record whether it was allocated using new or new[], the
+/// IsArrayAllocation flag.
+///
+/// For all array allocations, we need to allocat new Descriptor instances,
+/// so the DynamicAllocator has a llvm::BumpPtrAllocator similar to Program.
+class DynamicAllocator final {
+ struct Allocation {
+ std::unique_ptr<std::byte[]> Memory;
+ Allocation(std::unique_ptr<std::byte[]> Memory)
+ : Memory(std::move(Memory)) {}
+ };
+
+ struct AllocationSite {
+ llvm::SmallVector<Allocation> Allocations;
+ bool IsArrayAllocation = false;
+
+ AllocationSite(std::unique_ptr<std::byte[]> Memory, bool Array)
+ : IsArrayAllocation(Array) {
+ Allocations.push_back({std::move(Memory)});
+ }
+
+ size_t size() const { return Allocations.size(); }
+ };
+
+public:
+ DynamicAllocator() = default;
+
+ unsigned getNumAllocations() const { return AllocationSites.size(); }
+
+ /// Allocate ONE element of the given descriptor.
+ Block *allocate(const Descriptor *D);
+ /// Allocate \p NumElements primitive elements of the given type.
+ Block *allocate(const Expr *Source, PrimType T, size_t NumElements);
+ /// Allocate \p NumElements elements of the given descriptor.
+ Block *allocate(const Descriptor *D, size_t NumElements);
+
+ /// Deallocate the given source+block combination.
+ void deallocate(const Expr *Source, const Block *BlockToDelete,
+ InterpState &S);
+
+ /// Checks whether the allocation done at the given source is an array
+ /// allocation.
+ bool isArrayAllocation(const Expr *Source) const {
+ assert(AllocationSites.contains(Source));
+ return AllocationSites.at(Source).IsArrayAllocation;
+ }
+
+ // FIXME: Public because I'm not sure how to expose an iterator to it.
+ llvm::DenseMap<const Expr *, AllocationSite> AllocationSites;
+
+private:
+ using PoolAllocTy = llvm::BumpPtrAllocatorImpl<llvm::MallocAllocator>;
+ PoolAllocTy DescAllocator;
+
+ /// Allocates a new descriptor.
+ template <typename... Ts> Descriptor *allocateDescriptor(Ts &&...Args) {
+ return new (DescAllocator) Descriptor(std::forward<Ts>(Args)...);
+ }
+};
+
+} // namespace interp
+} // namespace clang
+#endif
diff --git a/clang/lib/AST/Interp/EvalEmitter.cpp b/clang/lib/AST/Interp/EvalEmitter.cpp
index f8942291b3b162d..a9b108717907b84 100644
--- a/clang/lib/AST/Interp/EvalEmitter.cpp
+++ b/clang/lib/AST/Interp/EvalEmitter.cpp
@@ -35,7 +35,7 @@ EvalEmitter::~EvalEmitter() {
llvm::Expected<bool> EvalEmitter::interpretExpr(const Expr *E) {
if (this->visitExpr(E))
- return true;
+ return S.maybeDiagnoseDanglingAllocations();
if (BailLocation)
return llvm::make_error<ByteCodeGenError>(*BailLocation);
return false;
@@ -43,7 +43,7 @@ llvm::Expected<bool> EvalEmitter::interpretExpr(const Expr *E) {
llvm::Expected<bool> EvalEmitter::interpretDecl(const VarDecl *VD) {
if (this->visitDecl(VD))
- return true;
+ return S.maybeDiagnoseDanglingAllocations();
if (BailLocation)
return llvm::make_error<ByteCodeGenError>(*BailLocation);
return false;
diff --git a/clang/lib/AST/Interp/Interp.cpp b/clang/lib/AST/Interp/Interp.cpp
index c87bb2fa6b02f16..e658fe310cd19c0 100644
--- a/clang/lib/AST/Interp/Interp.cpp
+++ b/clang/lib/AST/Interp/Interp.cpp
@@ -577,6 +577,43 @@ bool CheckFloatResult(InterpState &S, CodePtr OpPC, const Floating &Result,
return true;
}
+bool CheckDynamicMemoryAllocation(InterpState &S, CodePtr OpPC) {
+ if (S.getLangOpts().CPlusPlus20)
+ return true;
+
+ const SourceInfo &E = S.Current->getSource(OpPC);
+ S.FFDiag(E, diag::note_constexpr_new);
+ return false;
+}
+
+bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
+ bool DeleteIsArray, const Block *B,
+ const Expr *NewExpr) {
+ if (NewWasArray == DeleteIsArray)
+ return true;
+
+ const Descriptor *D = B->getDescriptor();
+
+ QualType TypeToDiagnose;
+ // We need to shuffle things around a bit here to get a better diagnostic,
+ // because the expression we allocated the block for was of type int*,
+ // but we want to get the array size right.
+ if (D->isArray()) {
+ QualType ElemQT = D->getType()->getPointeeType();
+ TypeToDiagnose = S.getCtx().getConstantArrayType(
+ ElemQT, APInt(64, D->getNumElems(), false), nullptr, ArrayType::Normal,
+ 0);
+ } else
+ TypeToDiagnose = D->getType()->getPointeeType();
+
+ const SourceInfo &E = S.Current->getSource(OpPC);
+ S.FFDiag(E, diag::note_constexpr_new_delete_mismatch)
+ << DeleteIsArray << 0 << TypeToDiagnose;
+ S.Note(NewExpr->getExprLoc(), diag::note_constexpr_dynamic_alloc_here)
+ << NewExpr->getSourceRange();
+ return false;
+}
+
/// We aleady know the given DeclRefExpr is invalid for some reason,
/// now figure out why and print appropriate diagnostics.
bool CheckDeclRef(InterpState &S, CodePtr OpPC, const DeclRefExpr *DR) {
diff --git a/clang/lib/AST/Interp/Interp.h b/clang/lib/AST/Interp/Interp.h
index 2132e8b0a8cfa29..03e12e9100a2c29 100644
--- a/clang/lib/AST/Interp/Interp.h
+++ b/clang/lib/AST/Interp/Interp.h
@@ -14,6 +14,7 @@
#define LLVM_CLANG_AST_INTERP_INTERP_H
#include "Boolean.h"
+#include "DynamicAllocator.h"
#include "Floating.h"
#include "Function.h"
#include "FunctionPointer.h"
@@ -112,6 +113,15 @@ bool CheckCtorCall(InterpState &S, CodePtr OpPC, const Pointer &This);
bool CheckPotentialReinterpretCast(InterpState &S, CodePtr OpPC,
const Pointer &Ptr);
+/// Checks if dynamic memory allocation is available in the current
+/// language mode.
+bool CheckDynamicMemoryAllocation(InterpState &S, CodePtr OpPC);
+
+/// Diagnose mismatched new[]/delete or new/delete[] pairs.
+bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
+ bool DeleteIsArray, const Block *B,
+ const Expr *NewExpr);
+
/// Sets the given integral value to the pointer, which is of
/// a std::{weak,partial,strong}_ordering type.
bool SetThreeWayComparisonField(InterpState &S, CodePtr OpPC,
@@ -1361,6 +1371,17 @@ bool InitPop(InterpState &S, CodePtr OpPC) {
return true;
}
+template <PrimType Name, class T = typename PrimConv<Name>::T>
+bool Init(InterpState &S, CodePtr OpPC) {
+ const T &Value = S.Stk.pop<T>();
+ const Pointer &Ptr = S.Stk.peek<Pointer>();
+ if (!CheckInit(S, OpPC, Ptr))
+ return false;
+ Ptr.initialize();
+ new (&Ptr.deref<T>()) T(Value);
+ return true;
+}
+
/// 1) Pops the value from the stack
/// 2) Peeks a pointer and gets its index \Idx
/// 3) Sets the value on the pointer, leaving the pointer on the stack.
@@ -1982,6 +2003,79 @@ inline bool OffsetOf(InterpState &S, CodePtr OpPC, const OffsetOfExpr *E) {
return true;
}
+inline bool Alloc(InterpState &S, CodePtr OpPC, const Descriptor *Desc) {
+ assert(Desc);
+
+ if (!CheckDynamicMemoryAllocation(S, OpPC))
+ return false;
+
+ DynamicAllocator &Allocator = S.getAllocator();
+ Block *B = Allocator.allocate(Desc);
+
+ S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
+
+ return true;
+}
+
+template <PrimType Name, class SizeT = typename PrimConv<Name>::T>
+inline bool AllocN(InterpState &S, CodePtr OpPC, PrimType T,
+ const Expr *Source) {
+ if (!CheckDynamicMemoryAllocation(S, OpPC))
+ return false;
+
+ SizeT NumElements = S.Stk.pop<SizeT>();
+
+ DynamicAllocator &Allocator = S.getAllocator();
+ Block *B = Allocator.allocate(Source, T, static_cast<size_t>(NumElements));
+
+ S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
+
+ return true;
+}
+
+template <PrimType Name, class SizeT = typename PrimConv<Name>::T>
+inline bool AllocCN(InterpState &S, CodePtr OpPC,
+ const Descriptor *ElementDesc) {
+ if (!CheckDynamicMemoryAllocation(S, OpPC))
+ return false;
+
+ SizeT NumElements = S.Stk.pop<SizeT>();
+
+ DynamicAllocator &Allocator = S.getAllocator();
+ Block *B = Allocator.allocate(ElementDesc, static_cast<size_t>(NumElements));
+
+ S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
+
+ return true;
+}
+
+static inline bool Free(InterpState &S, CodePtr OpPC, bool DeleteIsArrayForm) {
+
+ if (!CheckDynamicMemoryAllocation(S, OpPC))
+ return false;
+
+ const Expr *Source = nullptr;
+ const Block *BlockToDelete = nullptr;
+ {
+ // Extra scope for this so the block doesn't have this pointer
+ // pointing to it when we destroy it.
+ const Pointer &Ptr = S.Stk.pop<Pointer>();
+ Source = Ptr.getDeclDesc()->asExpr();
+ BlockToDelete = Ptr.block();
+ }
+
+ assert(Source);
+
+ DynamicAllocator &Allocator = S.getAllocator();
+ bool Result =
+ CheckNewDeleteForms(S, OpPC, Allocator.isArrayAllocation(Source),
+ DeleteIsArrayForm, BlockToDelete, Source);
+
+ Allocator.deallocate(Source, BlockToDelete, S);
+
+ return Result;
+}
+
//===----------------------------------------------------------------------===//
// Read opcode arguments
//===----------------------------------------------------------------------===//
diff --git a/clang/lib/AST/Interp/InterpState.cpp b/clang/lib/AST/Interp/InterpState.cpp
index 2cb87ef07fe5882..b231dd3e1aeae8a 100644
--- a/clang/lib/AST/Interp/InterpState.cpp
+++ b/clang/lib/AST/Interp/InterpState.cpp
@@ -71,3 +71,18 @@ void InterpState::deallocate(Block *B) {
B->invokeDtor();
}
}
+
+bool InterpState::maybeDiagnoseDanglingAllocations() {
+ bool NoAllocationsLeft = (Alloc.getNumAllocations() == 0);
+
+ if (!checkingPotentialConstantExpression()) {
+ for (const auto &It : Alloc.AllocationSites) {
+ assert(It.second.size() > 0);
+
+ const Expr *Source = It.first;
+ CCEDiag(Source->getExprLoc(), diag::note_constexpr_memory_leak)
+ << (It.second.size() - 1) << Source->getSourceRange();
+ }
+ }
+ return NoAllocationsLeft;
+}
diff --git a/clang/lib/AST/Interp/InterpState.h b/clang/lib/AST/Interp/InterpState.h
index 8f84bf6ed2eaffa..a2aa1a985e57c69 100644
--- a/clang/lib/AST/Interp/InterpState.h
+++ b/clang/lib/AST/Interp/InterpState.h
@@ -14,6 +14,7 @@
#define LLVM_CLANG_AST_INTERP_INTERPSTATE_H
#include "Context.h"
+#include "DynamicAllocator.h"
#include "Function.h"
#include "InterpFrame.h"
#include "InterpStack.h"
@@ -94,6 +95,13 @@ class InterpState final : public State, public SourceMapper {
Context &getContext() const { return Ctx; }
+ DynamicAllocator &getAllocator() { return Alloc; }
+
+ /// Diagnose any dynamic allocations that haven't been freed yet.
+ /// Will return \c false if there were any allocations to diagnose,
+ /// \c true otherwise.
+ bool maybeDiagnoseDanglingAllocations();
+
private:
/// AST Walker state.
State &Parent;
@@ -101,6 +109,8 @@ class InterpState final : public State, public SourceMapper {
DeadBlock *DeadBlocks = nullptr;
/// Reference to the offset-source mapping.
SourceMapper *M;
+ /// Allocator used for dynamic allocations performed via the program.
+ DynamicAllocator Alloc;
public:
/// Reference to the module containing all bytecode.
diff --git a/clang/lib/AST/Interp/Opcodes.td b/clang/lib/AST/Interp/Opcodes.td
index e1e7e5e2efbb059..1dfa3ab9e0a1d9a 100644
--- a/clang/lib/AST/Interp/Opcodes.td
+++ b/clang/lib/AST/Interp/Opcodes.td
@@ -55,9 +55,12 @@ def ArgRoundingMode : ArgType { let Name = "llvm::RoundingMode"; }
def ArgLETD: ArgType { let Name = "const LifetimeExtendedTemporaryDecl *"; }
def ArgCastKind : ArgType { let Name = "CastKind"; }
def ArgCallExpr : ArgType { let Name = "const CallExpr *"; }
+def ArgExpr : ArgType { let Name = "const Expr *"; }
def ArgOffsetOfExpr : ArgType { let Name = "const OffsetOfExpr *"; }
def ArgDeclRef : ArgType { let Name = "const DeclRefExpr *"; }
def ArgCCI : ArgType { let Name = "const ComparisonCategoryInfo *"; }
+def ArgDesc : ArgType { let Name = "const Descriptor *"; }
+def ArgPrimType : ArgType { let Name = "PrimType"; }
//===----------------------------------------------------------------------===//
// Classes of types instructions operate on.
@@ -460,6 +463,7 @@ def StoreBitFieldPop : StoreBitFieldOpcode {}
// [Pointer, Value] -> []
def InitPop : StoreOpcode {}
+def Init : StoreOpcode {}
// [Pointer, Value] -> [Pointer]
def InitElem : Opcode {
let Types = [AllTypeClass];
@@ -687,3 +691,23 @@ def InvalidCast : Opcode {
def InvalidDeclRef : Opcode {
let Args = [ArgDeclRef];
}
+
+def Alloc : Opcode {
+ let Args = [ArgDesc];
+}
+
+def AllocN : Opcode {
+ let Types = [IntegerTypeClass];
+ let Args = [ArgPrimType, ArgExpr];
+ let HasGroup = 1;
+}
+
+def AllocCN : Opcode {
+ let Types = [IntegerTypeClass];
+ let Args = [ArgDesc];
+ let HasGroup = 1;
+}
+
+def Free : Opcode {
+ let Args = [ArgBool];
+}
diff --git a/clang/test/AST/Interp/new-delete.cpp b/clang/test/AST/Interp/new-delete.cpp
new file mode 100644
index 000000000000000..0e50a17427c8020
--- /dev/null
+++ b/clang/test/AST/Interp/new-delete.cpp
@@ -0,0 +1,247 @@
+// RUN: %clang_cc1 -fexperimental-new-constant-interpreter -verify %s
+// RUN: %clang_cc1 -std=c++20 -fexperimental-new-constant-interpreter -verify %s
+// RUN: %clang_cc1 -verify=ref %s
+// RUN: %clang_cc1 -std=c++20 -verify=ref %s
+
+#if __cplusplus >= 202002L
+constexpr int a() {
+ new int(12); // expected-note {{allocation performed here was not deallocated}} \
+ // ref-note {{allocation performed here was not deallocated}}
+ return 1;
+}
+static_assert(a() == 1, ""); // expected-error {{not an integral constant expression}} \
+ // ref-error {{not an integral constant expression}}
+
+constexpr int b() {
+ int *i = new int(12);
+ int m = *i;
+ delete(i);
+ return m;
+}
+static_assert(b() == 12, "");
+
+
+struct S {
+ int a;
+ int b;
+
+ static constexpr S *create(int a, int b) {
+ return new S(a, b);
+ }
+};
+
+constexpr int c() {
+ S *s = new S(12, 13);
+
+ int i = s->a;
+ delete s;
+
+ return i;
+}
+static_assert(c() == 12, "");
+
+/// Dynamic allocation in function ::create(), freed in function d().
+constexpr int d() {
+ S* s = S::create(12, 14);
+
+ int sum = s->a + s->b;
+ delete s;
+ return sum;
+}
+static_assert(d() == 26);
+
+
+/// Test we emit the right diagnostic for several allocations done on
+/// the same site.
+constexpr int loop() {
+ for (int i = 0; i < 10; ++i) {
+ int *a = new int[10]; // ref-note {{not deallocated (along with 9 other memory leaks)}} \
+ // expected-note {{not deallocated (along with 9 other memory leaks)}}
+ }
+
+ return 1;
+}
+static_assert(loop() == 1, ""); // ref-error {{not an integral constant expression}} \
+ // expected-error {{not an integral constant expression}}
+
+
+/// No initializer.
+constexpr int noInit() {
+ int *i = new int;
+ delete i;
+ return 0;
+}
+static_assert(noInit() == 0, "");
+
+
+namespace Arrays {
+ constexpr int d() {
+ int *Arr = new int[12];
+
+ Arr[0] = 1;
+ Arr[1] = 5;
+
+ int sum = Arr[0] + Arr[1];
+ delete[] Arr;
+ return sum;
+ }
+ static_assert(d() == 6);
+
+
+ constexpr int mismatch1() { // ref-error {{never produces a constant expression}} \
+ // expected-error {{never produces a constant expression}}
+ int *i = new int(12); // ref-note {{allocated with 'new' here}} \
+ // ref-note 2{{heap allocation performed here}} \
+ // expected-note {{allocated with 'new' here}} \
+ // expected-note 2{{heap allocation performed here}}
+ delete[] i; // ref-warning {{'delete[]' applied to a pointer that was allocated with 'new'}} \
+ // ref-note 2{{array delete used to delete pointer to non-array object of type 'int'}} \
+ // expected-warning {{'delete[]' applied to a pointer that was allocated with 'new'}} \
+ // expected-note 2{{array delete used to delete pointer to non-array object of type 'int'}}
+ return 6;
+ }
+ static_assert(mismatch1() == 6); // ref-error {{not an integral constant expression}} \
+ // ref-note {{in call to 'mismatch1()'}} \
+ // expected-error {{not an integral constant expression}} \
+ // expected-note {{in call to 'mismatch1()'}}
+
+
+ constexpr int mismatch2() { // ref-error {{never produces a constant expression}} \
+ // expected-error {{never produces a constant expression}}
+ int *i = new int[12]; // ref-note {{allocated with 'new[]' here}} \
+ // ref-note 2{{heap allocation performed here}} \
+ // expected-note {{allocated with 'new[]' here}} \
+ // expected-note 2{{heap allocation performed here}}
+ delete i; // ref-warning {{'delete' applied to a pointer that was allocated with 'new[]'}} \
+ // ref-note 2{{non-array delete used to delete pointer to array object of type 'int[12]'}} \
+ // expected-warning {{'delete' applied to a pointer that was allocated with 'new[]'}} \
+ // expected-note 2{{non-array delete used to delete pointer to array object of type 'int[12]'}}
+ return 6;
+ }
+ static_assert(mismatch2() == 6); // ref-error {{not an integral constant expression}} \
+ // ref-note {{in call to 'mismatch2()'}} \
+ // expected-error {{not an integral constant expression}} \
+ // expected-note {{in call to 'mismatch2()'}}
+ /// Array of composite elements.
+ constexpr int foo() {
+ S *ss = new S[12];
+
+ ss[0].a = 12;
+
+ int m = ss[0].a;
+
+ delete[] ss;
+ return m;
+ }
+ static_assert(foo() == 12);
+}
+
+/// From test/SemaCXX/cxx2a-consteval.cpp
+
+namespace std {
+template <typename T> struct remove_reference { using type = T; };
+template <typename T> struct remove_reference<T &> { using type = T; };
+template <typename T> struct remove_reference<T &&> { using type = T; };
+template <typename T>
+constexpr typename std::remove_reference<T>::type&& move(T &&t) noexcept {
+ return static_cast<typename std::remove_reference<T>::type &&>(t);
+}
+}
+
+namespace cxx2a {
+struct A {
+ int* p = new int(42); // expected-note 7{{heap allocation performed here}} \
+ // ref-note 7{{heap allocation performed here}}
+ consteval int ret_i() const { return p ? *p : 0; }
+ consteval A ret_a() const { return A{}; }
+ constexpr ~A() { delete p; }
+};
+
+consteval int by_value_a(A a) { return a.ret_i(); }
+
+consteval int const_a_ref(const A &a) {
+ return a.ret_i();
+}
+
+consteval int rvalue_ref(const A &&a) {
+ return a.ret_i();
+}
+
+consteval const A &to_lvalue_ref(const A &&a) {
+ return a;
+}
+
+void test() {
+ constexpr A a{ nullptr };
+ { int k = A().ret_i(); }
+
+ { A k = A().ret_a(); } // expected-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // expected-note {{heap-allocated object is not a constant expression}} \
+ // ref-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // ref-note {{heap-allocated object is not a constant expression}}
+ { A k = to_lvalue_ref(A()); } // expected-error {{'cxx2a::to_lvalue_ref' is not a constant expression}} \
+ // expected-note {{reference to temporary is not a constant expression}} \
+ // expected-note {{temporary created here}} \
+ // ref-error {{'cxx2a::to_lvalue_ref' is not a constant expression}} \
+ // ref-note {{reference to temporary is not a constant expression}} \
+ // ref-note {{temporary created here}}
+ { A k = to_lvalue_ref(A().ret_a()); } // expected-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // expected-note {{heap-allocated object is not a constant expression}} \
+ // expected-error {{'cxx2a::to_lvalue_ref' is not a constant expression}} \
+ // expected-note {{reference to temporary is not a constant expression}} \
+ // expected-note {{temporary created here}} \
+ // ref-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // ref-note {{heap-allocated object is not a constant expression}} \
+ // ref-error {{'cxx2a::to_lvalue_ref' is not a constant expression}} \
+ // ref-note {{reference to temporary is not a constant expression}} \
+ // ref-note {{temporary created here}}
+ { int k = A().ret_a().ret_i(); } // expected-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // expected-note {{heap-allocated object is not a constant expression}} \
+ // ref-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // ref-note {{heap-allocated object is not a constant expression}}
+ { int k = by_value_a(A()); }
+ { int k = const_a_ref(A()); }
+ { int k = const_a_ref(a); }
+ { int k = rvalue_ref(A()); }
+ { int k = rvalue_ref(std::move(a)); }
+ { int k = const_a_ref(A().ret_a()); } // expected-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // expected-note {{is not a constant expression}} \
+ // ref-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // ref-note {{is not a constant expression}}
+
+
+ { int k = const_a_ref(to_lvalue_ref(A().ret_a())); } // expected-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // expected-note {{is not a constant expression}} \
+ // ref-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // ref-note {{is not a constant expression}}
+ { int k = const_a_ref(to_lvalue_ref(std::move(a))); }
+ { int k = by_value_a(A().ret_a()); }
+ { int k = by_value_a(to_lvalue_ref(static_cast<const A&&>(a))); }
+ { int k = (A().ret_a(), A().ret_i()); } // expected-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // expected-note {{is not a constant expression}} \
+ // ref-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // ref-note {{is not a constant expression}} \
+ // expected-warning {{left operand of comma operator has no effect}} \
+ // ref-warning {{left operand of comma operator has no effect}}
+ { int k = (const_a_ref(A().ret_a()), A().ret_i()); } // expected-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // expected-note {{is not a constant expression}} \
+ // ref-error {{'cxx2a::A::ret_a' is not a constant expression}} \
+ // ref-note {{is not a constant expression}} \
+ // expected-warning {{left operand of comma operator has no effect}} \
+ // ref-warning {{left operand of comma operator has no effect}}
+}
+}
+
+#else
+/// Make sure we reject this prior to C++20
+constexpr int a() { // ref-error {{never produces a constant expression}} \
+ // expected-error {{never produces a constant expression}}
+ delete new int(12); // ref-note 2{{dynamic memory allocation is not permitted in constant expressions until C++20}} \
+ // expected-note 2{{dynamic memory allocation is not permitted in constant expressions until C++20}}
+ return 1;
+}
+static_assert(a() == 1, ""); // ref-error {{not an integral constant expression}} \
+ // ref-note {{in call to 'a()'}} \
+ // expected-error {{not an integral constant expression}} \
+ // expected-note {{in call to 'a()'}}
+#endif
>From cfa053253aa3d82ad005cdb45c92b1e1392fb7da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Thu, 26 Oct 2023 12:56:47 +0200
Subject: [PATCH 2/2] Address review and add more tests
---
clang/lib/AST/Interp/DynamicAllocator.cpp | 33 ++++++++++-----------
clang/lib/AST/Interp/DynamicAllocator.h | 8 +++--
clang/lib/AST/Interp/Interp.cpp | 18 ++++++++++--
clang/lib/AST/Interp/Interp.h | 36 ++++++++++++++++++-----
clang/test/AST/Interp/new-delete.cpp | 35 ++++++++++++++++++++++
5 files changed, 99 insertions(+), 31 deletions(-)
diff --git a/clang/lib/AST/Interp/DynamicAllocator.cpp b/clang/lib/AST/Interp/DynamicAllocator.cpp
index a353d09d1f960ea..0b2f3141afc0ba5 100644
--- a/clang/lib/AST/Interp/DynamicAllocator.cpp
+++ b/clang/lib/AST/Interp/DynamicAllocator.cpp
@@ -61,31 +61,30 @@ Block *DynamicAllocator::allocate(const Descriptor *D) {
return B;
}
-void DynamicAllocator::deallocate(const Expr *Source,
+bool DynamicAllocator::deallocate(const Expr *Source,
const Block *BlockToDelete, InterpState &S) {
- assert(AllocationSites.contains(Source));
-
auto It = AllocationSites.find(Source);
- assert(It != AllocationSites.end());
+ if (It == AllocationSites.end())
+ return false;
auto &Site = It->second;
assert(Site.size() > 0);
// Find the Block to delete.
- size_t I = 0;
- [[maybe_unused]] bool Found = false;
- for (auto &A : Site.Allocations) {
- Block *B = reinterpret_cast<Block *>(A.Memory.get());
- if (B == BlockToDelete) {
- S.deallocate(B);
- B->invokeDtor();
- Site.Allocations.erase(Site.Allocations.begin() + I);
- Found = true;
- break;
- }
- }
- assert(Found);
+ auto AllocIt = llvm::find_if(Site.Allocations, [&](const Allocation &A) {
+ const Block *B = reinterpret_cast<const Block *>(A.Memory.get());
+ return BlockToDelete == B;
+ });
+
+ assert(AllocIt != Site.Allocations.end());
+
+ Block *B = reinterpret_cast<Block *>(AllocIt->Memory.get());
+ B->invokeDtor();
+ S.deallocate(B);
+ Site.Allocations.erase(AllocIt);
if (Site.size() == 0)
AllocationSites.erase(It);
+
+ return true;
}
diff --git a/clang/lib/AST/Interp/DynamicAllocator.h b/clang/lib/AST/Interp/DynamicAllocator.h
index d2a1d96232b9730..8b8701490aa4c5d 100644
--- a/clang/lib/AST/Interp/DynamicAllocator.h
+++ b/clang/lib/AST/Interp/DynamicAllocator.h
@@ -62,14 +62,16 @@ class DynamicAllocator final {
Block *allocate(const Descriptor *D, size_t NumElements);
/// Deallocate the given source+block combination.
- void deallocate(const Expr *Source, const Block *BlockToDelete,
+ /// Returns \c true if anything has been deallocatd, \c false otherwise.
+ bool deallocate(const Expr *Source, const Block *BlockToDelete,
InterpState &S);
/// Checks whether the allocation done at the given source is an array
/// allocation.
bool isArrayAllocation(const Expr *Source) const {
- assert(AllocationSites.contains(Source));
- return AllocationSites.at(Source).IsArrayAllocation;
+ if (auto It = AllocationSites.find(Source); It != AllocationSites.end())
+ return It->second.IsArrayAllocation;
+ return false;
}
// FIXME: Public because I'm not sure how to expose an iterator to it.
diff --git a/clang/lib/AST/Interp/Interp.cpp b/clang/lib/AST/Interp/Interp.cpp
index e658fe310cd19c0..9e4a4a8be61b649 100644
--- a/clang/lib/AST/Interp/Interp.cpp
+++ b/clang/lib/AST/Interp/Interp.cpp
@@ -587,13 +587,11 @@ bool CheckDynamicMemoryAllocation(InterpState &S, CodePtr OpPC) {
}
bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
- bool DeleteIsArray, const Block *B,
+ bool DeleteIsArray, const Descriptor *D,
const Expr *NewExpr) {
if (NewWasArray == DeleteIsArray)
return true;
- const Descriptor *D = B->getDescriptor();
-
QualType TypeToDiagnose;
// We need to shuffle things around a bit here to get a better diagnostic,
// because the expression we allocated the block for was of type int*,
@@ -614,6 +612,20 @@ bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
return false;
}
+bool CheckDeleteSource(InterpState &S, CodePtr OpPC, const Expr *Source,
+ const Pointer &Ptr) {
+ if (Source && isa<CXXNewExpr>(Source))
+ return true;
+
+ // Whatever this is, we didn't heap allocate it.
+ const SourceInfo &Loc = S.Current->getSource(OpPC);
+ // FIXME: There is a problem with pretty-printing the APValue we create
+ // for pointers (&local_primitive in this case).
+ S.FFDiag(Loc, diag::note_constexpr_delete_not_heap_alloc) << "";
+ S.Note(Ptr.getDeclLoc(), diag::note_declared_at);
+ return false;
+}
+
/// We aleady know the given DeclRefExpr is invalid for some reason,
/// now figure out why and print appropriate diagnostics.
bool CheckDeclRef(InterpState &S, CodePtr OpPC, const DeclRefExpr *DR) {
diff --git a/clang/lib/AST/Interp/Interp.h b/clang/lib/AST/Interp/Interp.h
index 03e12e9100a2c29..d5ef33783daaad3 100644
--- a/clang/lib/AST/Interp/Interp.h
+++ b/clang/lib/AST/Interp/Interp.h
@@ -119,9 +119,14 @@ bool CheckDynamicMemoryAllocation(InterpState &S, CodePtr OpPC);
/// Diagnose mismatched new[]/delete or new/delete[] pairs.
bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC, bool NewWasArray,
- bool DeleteIsArray, const Block *B,
+ bool DeleteIsArray, const Descriptor *D,
const Expr *NewExpr);
+/// Check the source of the pointer passed to delete/delete[] has actually
+/// been heap allocated by us.
+bool CheckDeleteSource(InterpState &S, CodePtr OpPC, const Expr *Source,
+ const Pointer &Ptr);
+
/// Sets the given integral value to the pointer, which is of
/// a std::{weak,partial,strong}_ordering type.
bool SetThreeWayComparisonField(InterpState &S, CodePtr OpPC,
@@ -2011,6 +2016,7 @@ inline bool Alloc(InterpState &S, CodePtr OpPC, const Descriptor *Desc) {
DynamicAllocator &Allocator = S.getAllocator();
Block *B = Allocator.allocate(Desc);
+ assert(B);
S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
@@ -2027,6 +2033,7 @@ inline bool AllocN(InterpState &S, CodePtr OpPC, PrimType T,
DynamicAllocator &Allocator = S.getAllocator();
Block *B = Allocator.allocate(Source, T, static_cast<size_t>(NumElements));
+ assert(B);
S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
@@ -2043,6 +2050,7 @@ inline bool AllocCN(InterpState &S, CodePtr OpPC,
DynamicAllocator &Allocator = S.getAllocator();
Block *B = Allocator.allocate(ElementDesc, static_cast<size_t>(NumElements));
+ assert(B);
S.Stk.push<Pointer>(B, sizeof(InlineDescriptor));
@@ -2060,20 +2068,32 @@ static inline bool Free(InterpState &S, CodePtr OpPC, bool DeleteIsArrayForm) {
// Extra scope for this so the block doesn't have this pointer
// pointing to it when we destroy it.
const Pointer &Ptr = S.Stk.pop<Pointer>();
+
+ // Deleteing nullptr is always fine.
+ if (Ptr.isZero())
+ return true;
+
Source = Ptr.getDeclDesc()->asExpr();
BlockToDelete = Ptr.block();
- }
+ if (!CheckDeleteSource(S, OpPC, Source, Ptr))
+ return false;
+ }
assert(Source);
+ assert(BlockToDelete);
DynamicAllocator &Allocator = S.getAllocator();
- bool Result =
- CheckNewDeleteForms(S, OpPC, Allocator.isArrayAllocation(Source),
- DeleteIsArrayForm, BlockToDelete, Source);
-
- Allocator.deallocate(Source, BlockToDelete, S);
+ bool WasArrayAlloc = Allocator.isArrayAllocation(Source);
+ const Descriptor *BlockDesc = BlockToDelete->getDescriptor();
- return Result;
+ if (!Allocator.deallocate(Source, BlockToDelete, S)) {
+ // Nothing has been deallocated, this must be a double-delete.
+ const SourceInfo &Loc = S.Current->getSource(OpPC);
+ S.FFDiag(Loc, diag::note_constexpr_double_delete);
+ return false;
+ }
+ return CheckNewDeleteForms(S, OpPC, WasArrayAlloc, DeleteIsArrayForm,
+ BlockDesc, Source);
}
//===----------------------------------------------------------------------===//
diff --git a/clang/test/AST/Interp/new-delete.cpp b/clang/test/AST/Interp/new-delete.cpp
index 0e50a17427c8020..552f506fc43adbc 100644
--- a/clang/test/AST/Interp/new-delete.cpp
+++ b/clang/test/AST/Interp/new-delete.cpp
@@ -73,6 +73,41 @@ constexpr int noInit() {
}
static_assert(noInit() == 0, "");
+/// Try to delete a pointer that hasn't been heap allocated.
+/// FIXME: pretty-printing the pointer is broken here.
+constexpr int notHeapAllocated() { // ref-error {{never produces a constant expression}} \
+ // expected-error {{never produces a constant expression}}
+ int A = 0; // ref-note 2{{declared here}} \
+ // expected-note 2{{declared here}}
+ delete &A; // ref-note 2{{delete of pointer '&A' that does not point to a heap-allocated object}} \
+ // expected-note 2{{delete of pointer '' that does not point to a heap-allocated object}}
+
+ return 1;
+}
+static_assert(notHeapAllocated() == 1, ""); // ref-error {{not an integral constant expression}} \
+ // ref-note {{in call to 'notHeapAllocated()'}} \
+ // expected-error {{not an integral constant expression}} \
+ // expected-note {{in call to 'notHeapAllocated()'}}
+
+consteval int deleteNull() {
+ int *A = nullptr;
+ delete A;
+ return 1;
+}
+static_assert(deleteNull() == 1, "");
+
+consteval int doubleDelete() { // ref-error {{never produces a constant expression}} \
+ // expected-error {{never produces a constant expression}}
+ int *A = new int;
+ delete A;
+ delete A; // ref-note 2{{delete of pointer that has already been deleted}} \
+ // expected-note 2{{delete of pointer that has already been deleted}}
+ return 1;
+}
+static_assert(doubleDelete() == 1); // ref-error {{not an integral constant expression}} \
+ // ref-note {{in call to 'doubleDelete()'}} \
+ // expected-error {{not an integral constant expression}} \
+ // expected-note {{in call to 'doubleDelete()'}}
namespace Arrays {
constexpr int d() {
More information about the cfe-commits
mailing list