[CIR] Add initial support for operator delete (#160574)

This adds basic operator delete handling in CIR. This does not yet
handle destroying delete or array delete, which will be added later. It
also does not insert non-null checks when not optimizing for size.
This commit is contained in:
Andy Kaylor
2025-09-29 10:29:41 -07:00
committed by GitHub
parent 782ab835dc
commit 38953f4d66
5 changed files with 314 additions and 0 deletions

View File

@@ -208,6 +208,7 @@ struct MissingFeatures {
static bool dataLayoutTypeAllocSize() { return false; }
static bool dataLayoutTypeStoreSize() { return false; }
static bool deferredCXXGlobalInit() { return false; }
static bool deleteArray() { return false; }
static bool devirtualizeMemberFunction() { return false; }
static bool ehCleanupFlags() { return false; }
static bool ehCleanupScope() { return false; }
@@ -219,6 +220,7 @@ struct MissingFeatures {
static bool emitCondLikelihoodViaExpectIntrinsic() { return false; }
static bool emitLifetimeMarkers() { return false; }
static bool emitLValueAlignmentAssumption() { return false; }
static bool emitNullCheckForDeleteCalls() { return false; }
static bool emitNullabilityCheck() { return false; }
static bool emitTypeCheck() { return false; }
static bool emitTypeMetadataCodeForVCall() { return false; }

View File

@@ -210,6 +210,60 @@ RValue CIRGenFunction::emitCXXMemberOrOperatorCall(
return emitCall(fnInfo, callee, returnValue, args, nullptr, loc);
}
namespace {
/// The parameters to pass to a usual operator delete.
struct UsualDeleteParams {
TypeAwareAllocationMode typeAwareDelete = TypeAwareAllocationMode::No;
bool destroyingDelete = false;
bool size = false;
AlignedAllocationMode alignment = AlignedAllocationMode::No;
};
} // namespace
// FIXME(cir): this should be shared with LLVM codegen
static UsualDeleteParams getUsualDeleteParams(const FunctionDecl *fd) {
UsualDeleteParams params;
const FunctionProtoType *fpt = fd->getType()->castAs<FunctionProtoType>();
auto ai = fpt->param_type_begin(), ae = fpt->param_type_end();
if (fd->isTypeAwareOperatorNewOrDelete()) {
params.typeAwareDelete = TypeAwareAllocationMode::Yes;
assert(ai != ae);
++ai;
}
// The first argument after the type-identity parameter (if any) is
// always a void* (or C* for a destroying operator delete for class
// type C).
++ai;
// The next parameter may be a std::destroying_delete_t.
if (fd->isDestroyingOperatorDelete()) {
params.destroyingDelete = true;
assert(ai != ae);
++ai;
}
// Figure out what other parameters we should be implicitly passing.
if (ai != ae && (*ai)->isIntegerType()) {
params.size = true;
++ai;
} else {
assert(!isTypeAwareAllocation(params.typeAwareDelete));
}
if (ai != ae && (*ai)->isAlignValT()) {
params.alignment = AlignedAllocationMode::Yes;
++ai;
} else {
assert(!isTypeAwareAllocation(params.typeAwareDelete));
}
assert(ai == ae && "unexpected usual deallocation function parameter");
return params;
}
static mlir::Value emitCXXNewAllocSize(CIRGenFunction &cgf, const CXXNewExpr *e,
unsigned minElements,
mlir::Value &numElements,
@@ -332,6 +386,117 @@ static RValue emitNewDeleteCall(CIRGenFunction &cgf,
return rv;
}
namespace {
/// Calls the given 'operator delete' on a single object.
struct CallObjectDelete final : EHScopeStack::Cleanup {
mlir::Value ptr;
const FunctionDecl *operatorDelete;
QualType elementType;
CallObjectDelete(mlir::Value ptr, const FunctionDecl *operatorDelete,
QualType elementType)
: ptr(ptr), operatorDelete(operatorDelete), elementType(elementType) {}
void emit(CIRGenFunction &cgf) override {
cgf.emitDeleteCall(operatorDelete, ptr, elementType);
}
// This is a placeholder until EHCleanupScope is implemented.
size_t getSize() const override {
assert(!cir::MissingFeatures::ehCleanupScope());
return sizeof(CallObjectDelete);
}
};
} // namespace
/// Emit the code for deleting a single object.
static void emitObjectDelete(CIRGenFunction &cgf, const CXXDeleteExpr *de,
Address ptr, QualType elementType) {
// C++11 [expr.delete]p3:
// If the static type of the object to be deleted is different from its
// dynamic type, the static type shall be a base class of the dynamic type
// of the object to be deleted and the static type shall have a virtual
// destructor or the behavior is undefined.
assert(!cir::MissingFeatures::emitTypeCheck());
const FunctionDecl *operatorDelete = de->getOperatorDelete();
assert(!operatorDelete->isDestroyingOperatorDelete());
// Find the destructor for the type, if applicable. If the
// destructor is virtual, we'll just emit the vcall and return.
const CXXDestructorDecl *dtor = nullptr;
if (const auto *rd = elementType->getAsCXXRecordDecl()) {
if (rd->hasDefinition() && !rd->hasTrivialDestructor()) {
dtor = rd->getDestructor();
if (dtor->isVirtual()) {
cgf.cgm.errorNYI(de->getSourceRange(),
"emitObjectDelete: virtual destructor");
}
}
}
// Make sure that we call delete even if the dtor throws.
// This doesn't have to a conditional cleanup because we're going
// to pop it off in a second.
cgf.ehStack.pushCleanup<CallObjectDelete>(
NormalAndEHCleanup, ptr.getPointer(), operatorDelete, elementType);
if (dtor) {
cgf.emitCXXDestructorCall(dtor, Dtor_Complete,
/*ForVirtualBase=*/false,
/*Delegating=*/false, ptr, elementType);
} else if (elementType.getObjCLifetime()) {
assert(!cir::MissingFeatures::objCLifetime());
cgf.cgm.errorNYI(de->getSourceRange(), "emitObjectDelete: ObjCLifetime");
}
// In traditional LLVM codegen null checks are emitted to save a delete call.
// In CIR we optimize for size by default, the null check should be added into
// this function callers.
assert(!cir::MissingFeatures::emitNullCheckForDeleteCalls());
cgf.popCleanupBlock();
}
void CIRGenFunction::emitCXXDeleteExpr(const CXXDeleteExpr *e) {
const Expr *arg = e->getArgument();
Address ptr = emitPointerWithAlignment(arg);
// Null check the pointer.
//
// We could avoid this null check if we can determine that the object
// destruction is trivial and doesn't require an array cookie; we can
// unconditionally perform the operator delete call in that case. For now, we
// assume that deleted pointers are null rarely enough that it's better to
// keep the branch. This might be worth revisiting for a -O0 code size win.
//
// CIR note: emit the code size friendly by default for now, such as mentioned
// in `emitObjectDelete`.
assert(!cir::MissingFeatures::emitNullCheckForDeleteCalls());
QualType deleteTy = e->getDestroyedType();
// A destroying operator delete overrides the entire operation of the
// delete expression.
if (e->getOperatorDelete()->isDestroyingOperatorDelete()) {
cgm.errorNYI(e->getSourceRange(),
"emitCXXDeleteExpr: destroying operator delete");
return;
}
// We might be deleting a pointer to array.
deleteTy = getContext().getBaseElementType(deleteTy);
ptr = ptr.withElementType(builder, convertTypeForMem(deleteTy));
if (e->isArrayForm()) {
assert(!cir::MissingFeatures::deleteArray());
cgm.errorNYI(e->getSourceRange(), "emitCXXDeleteExpr: array delete");
return;
} else {
emitObjectDelete(*this, e, ptr, deleteTy);
}
}
mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
// The element type being allocated.
QualType allocType = getContext().getBaseElementType(e->getAllocatedType());
@@ -443,3 +608,53 @@ mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
allocSizeWithoutCookie);
return result.getPointer();
}
void CIRGenFunction::emitDeleteCall(const FunctionDecl *deleteFD,
mlir::Value ptr, QualType deleteTy) {
assert(!cir::MissingFeatures::deleteArray());
const auto *deleteFTy = deleteFD->getType()->castAs<FunctionProtoType>();
CallArgList deleteArgs;
UsualDeleteParams params = getUsualDeleteParams(deleteFD);
auto paramTypeIt = deleteFTy->param_type_begin();
// Pass std::type_identity tag if present
if (isTypeAwareAllocation(params.typeAwareDelete))
cgm.errorNYI(deleteFD->getSourceRange(),
"emitDeleteCall: type aware delete");
// Pass the pointer itself.
QualType argTy = *paramTypeIt++;
mlir::Value deletePtr =
builder.createBitcast(ptr.getLoc(), ptr, convertType(argTy));
deleteArgs.add(RValue::get(deletePtr), argTy);
// Pass the std::destroying_delete tag if present.
if (params.destroyingDelete)
cgm.errorNYI(deleteFD->getSourceRange(),
"emitDeleteCall: destroying delete");
// Pass the size if the delete function has a size_t parameter.
if (params.size) {
QualType sizeType = *paramTypeIt++;
CharUnits deleteTypeSize = getContext().getTypeSizeInChars(deleteTy);
assert(mlir::isa<cir::IntType>(convertType(sizeType)) &&
"expected cir::IntType");
cir::ConstantOp size = builder.getConstInt(
*currSrcLoc, convertType(sizeType), deleteTypeSize.getQuantity());
deleteArgs.add(RValue::get(size), sizeType);
}
// Pass the alignment if the delete function has an align_val_t parameter.
if (isAlignedAllocation(params.alignment))
cgm.errorNYI(deleteFD->getSourceRange(),
"emitDeleteCall: aligned allocation");
assert(paramTypeIt == deleteFTy->param_type_end() &&
"unknown parameter to usual delete function");
// Emit the call to delete.
emitNewDeleteCall(*this, deleteFD, deleteFTy, deleteArgs);
}

View File

@@ -687,6 +687,10 @@ public:
mlir::Value VisitCXXNewExpr(const CXXNewExpr *e) {
return cgf.emitCXXNewExpr(e);
}
mlir::Value VisitCXXDeleteExpr(const CXXDeleteExpr *e) {
cgf.emitCXXDeleteExpr(e);
return {};
}
mlir::Value VisitCXXThrowExpr(const CXXThrowExpr *e) {
cgf.emitCXXThrowExpr(e);

View File

@@ -1197,6 +1197,8 @@ public:
bool delegating, Address thisAddr,
CallArgList &args, clang::SourceLocation loc);
void emitCXXDeleteExpr(const CXXDeleteExpr *e);
void emitCXXDestructorCall(const CXXDestructorDecl *dd, CXXDtorType type,
bool forVirtualBase, bool delegating,
Address thisAddr, QualType thisTy);
@@ -1244,6 +1246,9 @@ public:
void emitDelegatingCXXConstructorCall(const CXXConstructorDecl *ctor,
const FunctionArgList &args);
void emitDeleteCall(const FunctionDecl *deleteFD, mlir::Value ptr,
QualType deleteTy);
mlir::LogicalResult emitDoStmt(const clang::DoStmt &s);
/// Emit an expression as an initializer for an object (variable, field, etc.)

View File

@@ -0,0 +1,88 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++20 -fclangir -mconstructor-aliases -emit-cir %s -o %t.cir
// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++20 -fclangir -mconstructor-aliases -emit-llvm %s -o %t-cir.ll
// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++20 -mconstructor-aliases -emit-llvm %s -o %t.ll
// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
typedef __typeof(sizeof(int)) size_t;
struct SizedDelete {
void operator delete(void*, size_t);
int member;
};
void test_sized_delete(SizedDelete *x) {
delete x;
}
// SizedDelete::operator delete(void*, unsigned long)
// CIR: cir.func private @_ZN11SizedDeletedlEPvm(!cir.ptr<!void>, !u64i)
// LLVM: declare void @_ZN11SizedDeletedlEPvm(ptr, i64)
// CIR: cir.func dso_local @_Z17test_sized_deleteP11SizedDelete
// CIR: %[[X:.*]] = cir.load{{.*}} %{{.*}}
// CIR: %[[X_CAST:.*]] = cir.cast(bitcast, %[[X]] : !cir.ptr<!rec_SizedDelete>), !cir.ptr<!void>
// CIR: %[[OBJ_SIZE:.*]] = cir.const #cir.int<4> : !u64i
// CIR: cir.call @_ZN11SizedDeletedlEPvm(%[[X_CAST]], %[[OBJ_SIZE]]) nothrow : (!cir.ptr<!void>, !u64i) -> ()
// LLVM: define dso_local void @_Z17test_sized_deleteP11SizedDelete
// LLVM: %[[X:.*]] = load ptr, ptr %{{.*}}
// LLVM: call void @_ZN11SizedDeletedlEPvm(ptr %[[X]], i64 4)
// OGCG: define dso_local void @_Z17test_sized_deleteP11SizedDelete
// OGCG: %[[X:.*]] = load ptr, ptr %{{.*}}
// OGCG: %[[ISNULL:.*]] = icmp eq ptr %[[X]], null
// OGCG: br i1 %[[ISNULL]], label %{{.*}}, label %[[DELETE_NOTNULL:.*]]
// OGCG: [[DELETE_NOTNULL]]:
// OGCG: call void @_ZN11SizedDeletedlEPvm(ptr noundef %[[X]], i64 noundef 4)
// This function is declared below the call in OGCG.
// OGCG: declare void @_ZN11SizedDeletedlEPvm(ptr noundef, i64 noundef)
struct Contents {
~Contents() {}
};
struct Container {
Contents *contents;
~Container();
};
Container::~Container() { delete contents; }
// Contents::~Contents()
// CIR: cir.func comdat linkonce_odr @_ZN8ContentsD2Ev
// LLVM: define linkonce_odr void @_ZN8ContentsD2Ev
// operator delete(void*, unsigned long)
// CIR: cir.func private @_ZdlPvm(!cir.ptr<!void>, !u64i)
// LLVM: declare void @_ZdlPvm(ptr, i64)
// Container::~Container()
// CIR: cir.func dso_local @_ZN9ContainerD2Ev
// CIR: %[[THIS:.*]] = cir.load %{{.*}}
// CIR: %[[CONTENTS_PTR_ADDR:.*]] = cir.get_member %[[THIS]][0] {name = "contents"} : !cir.ptr<!rec_Container> -> !cir.ptr<!cir.ptr<!rec_Contents>>
// CIR: %[[CONTENTS_PTR:.*]] = cir.load{{.*}} %[[CONTENTS_PTR_ADDR]]
// CIR: cir.call @_ZN8ContentsD2Ev(%[[CONTENTS_PTR]]) nothrow : (!cir.ptr<!rec_Contents>) -> ()
// CIR: %[[CONTENTS_CAST:.*]] = cir.cast(bitcast, %[[CONTENTS_PTR]] : !cir.ptr<!rec_Contents>), !cir.ptr<!void>
// CIR: %[[OBJ_SIZE:.*]] = cir.const #cir.int<1> : !u64i
// CIR: cir.call @_ZdlPvm(%[[CONTENTS_CAST]], %[[OBJ_SIZE]]) nothrow : (!cir.ptr<!void>, !u64i) -> ()
// LLVM: define dso_local void @_ZN9ContainerD2Ev
// LLVM: %[[THIS:.*]] = load ptr, ptr %{{.*}}
// LLVM: %[[CONTENTS_PTR_ADDR:.*]] = getelementptr %struct.Container, ptr %[[THIS]], i32 0, i32 0
// LLVM: %[[CONTENTS_PTR:.*]] = load ptr, ptr %[[CONTENTS_PTR_ADDR]]
// LLVM: call void @_ZN8ContentsD2Ev(ptr %[[CONTENTS_PTR]])
// LLVM: call void @_ZdlPvm(ptr %[[CONTENTS_PTR]], i64 1)
// OGCG: define dso_local void @_ZN9ContainerD2Ev
// OGCG: %[[THIS:.*]] = load ptr, ptr %{{.*}}
// OGCG: %[[CONTENTS:.*]] = getelementptr inbounds nuw %struct.Container, ptr %[[THIS]], i32 0, i32 0
// OGCG: %[[CONTENTS_PTR:.*]] = load ptr, ptr %[[CONTENTS]]
// OGCG: %[[ISNULL:.*]] = icmp eq ptr %[[CONTENTS_PTR]], null
// OGCG: br i1 %[[ISNULL]], label %{{.*}}, label %[[DELETE_NOTNULL:.*]]
// OGCG: [[DELETE_NOTNULL]]:
// OGCG: call void @_ZN8ContentsD2Ev(ptr noundef nonnull align 1 dereferenceable(1) %[[CONTENTS_PTR]])
// OGCG: call void @_ZdlPvm(ptr noundef %[[CONTENTS_PTR]], i64 noundef 1)
// These functions are declared/defined below the calls in OGCG.
// OGCG: define linkonce_odr void @_ZN8ContentsD2Ev
// OGCG: declare void @_ZdlPvm(ptr noundef, i64 noundef)