[clang] [CIR] Implement null check for CXXNewExpr (PR #192848)
via cfe-commits
cfe-commits at lists.llvm.org
Tue Apr 21 14:08:57 PDT 2026
https://github.com/AbdallahRashed updated https://github.com/llvm/llvm-project/pull/192848
>From ea26dad59c01ba3b19e2c0a095bab349e31a0e2c Mon Sep 17 00:00:00 2001
From: AbdallahRashed <abdallah.mrashed at gmail.com>
Date: Sun, 19 Apr 2026 15:52:35 +0200
Subject: [PATCH 1/2] [CIR] Implement null check for CXXNewExpr
---
clang/include/clang/CIR/MissingFeatures.h | 3 -
clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp | 48 +++++++++--
clang/test/CIR/CodeGen/new-null.cpp | 97 +++++++++++++++++++++++
3 files changed, 140 insertions(+), 8 deletions(-)
create mode 100644 clang/test/CIR/CodeGen/new-null.cpp
diff --git a/clang/include/clang/CIR/MissingFeatures.h b/clang/include/clang/CIR/MissingFeatures.h
index 92d052de4e8db..f52648b7c5476 100644
--- a/clang/include/clang/CIR/MissingFeatures.h
+++ b/clang/include/clang/CIR/MissingFeatures.h
@@ -114,9 +114,6 @@ struct MissingFeatures {
static bool opCallChain() { return false; }
static bool opCallExceptionAttr() { return false; }
- // CXXNewExpr
- static bool exprNewNullCheck() { return false; }
-
// FnInfoOpts -- This is used to track whether calls are chain calls or
// instance methods. Classic codegen uses chain call to track and extra free
// register for x86 and uses instance method as a condition for a thunk
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
index 30a833564ec2f..bb4f5291c2145 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -1472,9 +1472,22 @@ mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
// interesting initializer will be running sanitizers on the initialization.
bool nullCheck = e->shouldNullCheckAllocation() &&
(!allocType.isPODType(getContext()) || e->hasInitializer());
- assert(!cir::MissingFeatures::exprNewNullCheck());
- if (nullCheck)
- cgm.errorNYI(e->getSourceRange(), "emitCXXNewExpr: null check");
+
+ mlir::Location loc = getLoc(e->getSourceRange());
+ cir::IfOp nullCheckOp;
+ cir::YieldOp nullCheckYield;
+ if (nullCheck) {
+ mlir::Value isNotNull = builder.createPtrIsNotNull(allocation.getPointer());
+ nullCheckOp = cir::IfOp::create(builder, loc, isNotNull,
+ /*withElseRegion=*/false,
+ /*thenBuilder=*/
+ [&](mlir::OpBuilder &, mlir::Location loc) {
+ nullCheckYield = builder.createYield(loc);
+ });
+
+ // Emit initialization inside the if-op's then region.
+ builder.setInsertionPoint(nullCheckYield);
+ }
// If there's an operator delete, enter a cleanup to call it if an
// exception is thrown. If we do this, we'll be creating the result pointer
@@ -1548,9 +1561,34 @@ mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
result = result.withPointer(loadResult.getResult());
}
- assert(!cir::MissingFeatures::exprNewNullCheck());
+ mlir::Value resultValue = result.getPointer();
+
+ if (nullCheck) {
+ // Restore insertion point to after the IfOp.
+ builder.setInsertionPointAfter(nullCheckOp);
+
+ mlir::Type resultTy = resultValue.getType();
+
+ // If we needed a NewDeleteCleanup, allocation may have been modified
+ // inside the cir.if (e.g. by cookie adjustment). Use the result stored
+ // in the alloca instead, since the alloca dominates this point.
+ mlir::Value trueVal;
+ if (useNewDeleteCleanup) {
+ trueVal = builder.createLoad(getLoc(e->getSourceRange()), resultPtr)
+ .getResult();
+ } else {
+ trueVal = allocation.getPointer();
+ }
+ if (trueVal.getType() != resultTy)
+ trueVal = builder.createBitcast(trueVal, resultTy);
+ mlir::Value nullPtr =
+ builder.getNullPtr(resultTy, getLoc(e->getSourceRange())).getResult();
+ resultValue =
+ builder.createSelect(getLoc(e->getSourceRange()),
+ nullCheckOp.getCondition(), trueVal, nullPtr);
+ }
- return result.getPointer();
+ return resultValue;
}
void CIRGenFunction::emitDeleteCall(const FunctionDecl *deleteFD,
diff --git a/clang/test/CIR/CodeGen/new-null.cpp b/clang/test/CIR/CodeGen/new-null.cpp
new file mode 100644
index 0000000000000..50b69f5f9293f
--- /dev/null
+++ b/clang/test/CIR/CodeGen/new-null.cpp
@@ -0,0 +1,97 @@
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+
+typedef __typeof__(sizeof(int)) size_t;
+
+namespace std {
+ struct nothrow_t {};
+}
+std::nothrow_t nothrow;
+
+void *operator new(size_t, const std::nothrow_t &) throw();
+void operator delete(void *, const std::nothrow_t &) throw();
+
+struct S {
+ S();
+ ~S();
+ int a;
+};
+
+// nothrow new with non-POD type triggers null check
+S *test_nothrow_new() {
+ return new (nothrow) S;
+}
+
+// CHECK: cir.func {{.*}} @_Z16test_nothrow_newv()
+// CHECK: %[[ALLOC:.*]] = cir.call @_ZnwmRKSt9nothrow_t({{.*}}) nothrow
+// CHECK: %[[NULL:.*]] = cir.const #cir.ptr<null> : !cir.ptr<!void>
+// CHECK: %[[IS_NOT_NULL:.*]] = cir.cmp ne %[[ALLOC]], %[[NULL]] : !cir.ptr<!void>
+// CHECK: cir.if %[[IS_NOT_NULL]] {
+// CHECK: %[[CAST:.*]] = cir.cast bitcast %[[ALLOC]] : !cir.ptr<!void> -> !cir.ptr<!rec_S>
+// CHECK: cir.call @_ZN1SC1Ev(%[[CAST]])
+// CHECK: }
+// CHECK: %[[RESULT_CAST:.*]] = cir.cast bitcast %[[ALLOC]] : !cir.ptr<!void> -> !cir.ptr<!rec_S>
+// CHECK: %[[NULL_S:.*]] = cir.const #cir.ptr<null> : !cir.ptr<!rec_S>
+// CHECK: %[[RESULT:.*]] = cir.select if %[[IS_NOT_NULL]] then %[[RESULT_CAST]] else %[[NULL_S]]
+
+// LLVM: define {{.*}} ptr @_Z16test_nothrow_newv()
+// LLVM: %[[ALLOC:.*]] = call {{.*}} ptr @_ZnwmRKSt9nothrow_t(i64 noundef 4, {{.*}})
+// LLVM: %[[CMP:.*]] = icmp ne ptr %[[ALLOC]], null
+// LLVM: br i1 %[[CMP]], label %[[NOT_NULL:.*]], label %[[CONT:.*]]
+// LLVM: [[NOT_NULL]]:
+// LLVM: call void @_ZN1SC1Ev({{.*}} %[[ALLOC]])
+// LLVM: br label %[[CONT]]
+// LLVM: [[CONT]]:
+// LLVM: %[[RESULT:.*]] = select i1 %[[CMP]], ptr %[[ALLOC]], ptr null
+
+// OGCG: define {{.*}} ptr @_Z16test_nothrow_newv()
+// OGCG: %[[ALLOC:.*]] = call {{.*}} ptr @_ZnwmRKSt9nothrow_t(i64 noundef 4, {{.*}})
+// OGCG: %[[IS_NULL:.*]] = icmp eq ptr %[[ALLOC]], null
+// OGCG: br i1 %[[IS_NULL]], label %[[CONT:.*]], label %[[NOT_NULL:.*]]
+// OGCG: [[NOT_NULL]]:
+// OGCG: call void @_ZN1SC1Ev({{.*}} %[[ALLOC]])
+// OGCG: br label %[[CONT]]
+// OGCG: [[CONT]]:
+// OGCG: phi ptr
+
+// nothrow new with POD + initializer triggers null check
+int *test_nothrow_new_init() {
+ return new (nothrow) int(42);
+}
+
+// CHECK: cir.func {{.*}} @_Z21test_nothrow_new_initv()
+// CHECK: %[[ALLOC:.*]] = cir.call @_ZnwmRKSt9nothrow_t({{.*}}) nothrow
+// CHECK: %[[NULL:.*]] = cir.const #cir.ptr<null> : !cir.ptr<!void>
+// CHECK: %[[IS_NOT_NULL:.*]] = cir.cmp ne %[[ALLOC]], %[[NULL]] : !cir.ptr<!void>
+// CHECK: cir.if %[[IS_NOT_NULL]] {
+// CHECK: %[[CAST:.*]] = cir.cast bitcast %[[ALLOC]] : !cir.ptr<!void> -> !cir.ptr<!s32i>
+// CHECK: %[[FORTY_TWO:.*]] = cir.const #cir.int<42> : !s32i
+// CHECK: cir.store {{.*}} %[[FORTY_TWO]], %[[CAST]]
+// CHECK: }
+// CHECK: %[[RESULT_CAST:.*]] = cir.cast bitcast %[[ALLOC]] : !cir.ptr<!void> -> !cir.ptr<!s32i>
+// CHECK: %[[NULL_I:.*]] = cir.const #cir.ptr<null> : !cir.ptr<!s32i>
+// CHECK: %[[RESULT:.*]] = cir.select if %[[IS_NOT_NULL]] then %[[RESULT_CAST]] else %[[NULL_I]]
+
+// LLVM: define {{.*}} ptr @_Z21test_nothrow_new_initv()
+// LLVM: %[[ALLOC:.*]] = call {{.*}} ptr @_ZnwmRKSt9nothrow_t(i64 noundef 4, {{.*}})
+// LLVM: %[[CMP:.*]] = icmp ne ptr %[[ALLOC]], null
+// LLVM: br i1 %[[CMP]], label %[[NOT_NULL:.*]], label %[[CONT:.*]]
+// LLVM: [[NOT_NULL]]:
+// LLVM: store i32 42, ptr %[[ALLOC]], align 4
+// LLVM: br label %[[CONT]]
+// LLVM: [[CONT]]:
+// LLVM: %[[RESULT:.*]] = select i1 %[[CMP]], ptr %[[ALLOC]], ptr null
+
+// OGCG: define {{.*}} ptr @_Z21test_nothrow_new_initv()
+// OGCG: %[[ALLOC:.*]] = call {{.*}} ptr @_ZnwmRKSt9nothrow_t(i64 noundef 4, {{.*}})
+// OGCG: %[[IS_NULL:.*]] = icmp eq ptr %[[ALLOC]], null
+// OGCG: br i1 %[[IS_NULL]], label %[[CONT:.*]], label %[[NOT_NULL:.*]]
+// OGCG: [[NOT_NULL]]:
+// OGCG: store i32 42, ptr %[[ALLOC]], align 4
+// OGCG: br label %[[CONT]]
+// OGCG: [[CONT]]:
+// OGCG: phi ptr
>From 11cb8d1af4319023d3941c14779e173bd29e6e4e Mon Sep 17 00:00:00 2001
From: AbdallahRashed <abdallah.mrashed at gmail.com>
Date: Tue, 21 Apr 2026 23:08:42 +0200
Subject: [PATCH 2/2] [CIR][NFC] Factor new-expression init emission into
emitNewExprInit
---
clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp | 154 +++++++++++++-----------
1 file changed, 83 insertions(+), 71 deletions(-)
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
index bb4f5291c2145..b4ecb12d101e3 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -1366,6 +1366,85 @@ void CIRGenFunction::emitCXXDeleteExpr(const CXXDeleteExpr *e) {
}
}
+/// Emit the initialization of a new-expression result, including setting up
+/// a delete cleanup if needed, initializing array cookies, and calling the
+/// initializer. This is factored out to avoid manual insertion point
+/// manipulation at the call site.
+///
+/// \returns a tuple of (result pointer, resultPtr alloca, useNewDeleteCleanup).
+static std::tuple<Address, Address, bool>
+emitNewExprInit(CIRGenFunction &cgf, const CXXNewExpr *e,
+ QualType allocType, Address allocation, mlir::Value allocSize,
+ mlir::Value allocSizeWithoutCookie, CharUnits allocAlign,
+ const CallArgList &allocatorArgs, mlir::Value numElements,
+ const FunctionDecl *allocator) {
+ auto &builder = cgf.getBuilder();
+
+ bool useNewDeleteCleanup =
+ e->getOperatorDelete() &&
+ !e->getOperatorDelete()->isReservedGlobalPlacementOperator();
+ EHScopeStack::stable_iterator operatorDeleteCleanup;
+ mlir::Operation *cleanupDominator = nullptr;
+ if (useNewDeleteCleanup) {
+ assert(!cir::MissingFeatures::typeAwareAllocation());
+ enterNewDeleteCleanup(cgf, e, allocation, allocSize, allocAlign,
+ allocatorArgs);
+ operatorDeleteCleanup = cgf.ehStack.stable_begin();
+ cleanupDominator =
+ cir::UnreachableOp::create(builder, cgf.getLoc(e->getSourceRange()))
+ .getOperation();
+ }
+
+ if (allocSize != allocSizeWithoutCookie) {
+ assert(e->isArray());
+ allocation = cgf.cgm.getCXXABI().initializeArrayCookie(
+ cgf, allocation, numElements, e, allocType);
+ }
+
+ mlir::Type elementTy;
+ if (e->isArray())
+ elementTy = cgf.convertTypeForMem(e->getAllocatedType());
+ else
+ elementTy = cgf.convertTypeForMem(allocType);
+
+ Address result = builder.createElementBitCast(
+ cgf.getLoc(e->getSourceRange()), allocation, elementTy);
+
+ Address resultPtr = Address::invalid();
+ if (useNewDeleteCleanup) {
+ resultPtr = cgf.createTempAlloca(builder.getPointerTo(elementTy),
+ result.getAlignment(),
+ cgf.getLoc(e->getSourceRange()),
+ "__new_result");
+ builder.createStore(cgf.getLoc(e->getSourceRange()), result.getPointer(),
+ resultPtr);
+ }
+
+ // Passing pointer through launder.invariant.group to avoid propagation of
+ // vptrs information which may be included in previous type.
+ if (cgf.cgm.getCodeGenOpts().StrictVTablePointers &&
+ allocator->isReservedGlobalPlacementOperator())
+ cgf.cgm.errorNYI(e->getSourceRange(),
+ "emitCXXNewExpr: strict vtable pointers");
+
+ assert(!cir::MissingFeatures::sanitizers());
+
+ emitNewInitializer(cgf, e, allocType, elementTy, result, numElements,
+ allocSizeWithoutCookie);
+
+ if (useNewDeleteCleanup) {
+ assert(operatorDeleteCleanup.isValid());
+ assert(resultPtr.isValid());
+ cgf.deactivateCleanupBlock(operatorDeleteCleanup, cleanupDominator);
+ cleanupDominator->erase();
+ cir::LoadOp loadResult =
+ builder.createLoad(cgf.getLoc(e->getSourceRange()), resultPtr);
+ result = result.withPointer(loadResult.getResult());
+ }
+
+ return {result, resultPtr, useNewDeleteCleanup};
+}
+
mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
// The element type being allocated.
QualType allocType = getContext().getBaseElementType(e->getAllocatedType());
@@ -1489,77 +1568,10 @@ mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
builder.setInsertionPoint(nullCheckYield);
}
- // If there's an operator delete, enter a cleanup to call it if an
- // exception is thrown. If we do this, we'll be creating the result pointer
- // inside a cleanup scope, either with a bitcast or an offset based on the
- // array cookie size. However, we need to return that pointer from outside
- // the cleanup scope, so we need to store it in a temporary variable.
- bool useNewDeleteCleanup =
- e->getOperatorDelete() &&
- !e->getOperatorDelete()->isReservedGlobalPlacementOperator();
- EHScopeStack::stable_iterator operatorDeleteCleanup;
- mlir::Operation *cleanupDominator = nullptr;
- if (useNewDeleteCleanup) {
- assert(!cir::MissingFeatures::typeAwareAllocation());
- enterNewDeleteCleanup(*this, e, allocation, allocSize, allocAlign,
- allocatorArgs);
- operatorDeleteCleanup = ehStack.stable_begin();
- cleanupDominator =
- cir::UnreachableOp::create(builder, getLoc(e->getSourceRange()))
- .getOperation();
- }
-
- if (allocSize != allocSizeWithoutCookie) {
- assert(e->isArray());
- allocation = cgm.getCXXABI().initializeArrayCookie(
- *this, allocation, numElements, e, allocType);
- }
-
- mlir::Type elementTy;
- if (e->isArray()) {
- // For array new, use the allocated type to handle multidimensional arrays
- // correctly
- elementTy = convertTypeForMem(e->getAllocatedType());
- } else {
- elementTy = convertTypeForMem(allocType);
- }
- Address result = builder.createElementBitCast(getLoc(e->getSourceRange()),
- allocation, elementTy);
-
- // If we're inside a new delete cleanup, store the result pointer.
- Address resultPtr = Address::invalid();
- if (useNewDeleteCleanup) {
- resultPtr =
- createTempAlloca(builder.getPointerTo(elementTy), result.getAlignment(),
- getLoc(e->getSourceRange()), "__new_result");
- builder.createStore(getLoc(e->getSourceRange()), result.getPointer(),
- resultPtr);
- }
-
- // Passing pointer through launder.invariant.group to avoid propagation of
- // vptrs information which may be included in previous type.
- // To not break LTO with different optimizations levels, we do it regardless
- // of optimization level.
- if (cgm.getCodeGenOpts().StrictVTablePointers &&
- allocator->isReservedGlobalPlacementOperator())
- cgm.errorNYI(e->getSourceRange(), "emitCXXNewExpr: strict vtable pointers");
-
- assert(!cir::MissingFeatures::sanitizers());
-
- emitNewInitializer(*this, e, allocType, elementTy, result, numElements,
- allocSizeWithoutCookie);
-
- // Deactivate the 'operator delete' cleanup if we finished
- // initialization.
- if (useNewDeleteCleanup) {
- assert(operatorDeleteCleanup.isValid());
- assert(resultPtr.isValid());
- deactivateCleanupBlock(operatorDeleteCleanup, cleanupDominator);
- cleanupDominator->erase();
- cir::LoadOp loadResult =
- builder.createLoad(getLoc(e->getSourceRange()), resultPtr);
- result = result.withPointer(loadResult.getResult());
- }
+ auto [result, resultPtr, useNewDeleteCleanup] =
+ emitNewExprInit(*this, e, allocType, allocation, allocSize,
+ allocSizeWithoutCookie, allocAlign, allocatorArgs,
+ numElements, allocator);
mlir::Value resultValue = result.getPointer();
More information about the cfe-commits
mailing list