[clang] [CIR] Implement null check for CXXNewExpr (PR #192848)

via cfe-commits cfe-commits at lists.llvm.org
Sun Apr 19 06:56:06 PDT 2026


https://github.com/AbdallahRashed created https://github.com/llvm/llvm-project/pull/192848


When operator new is allowed to return null (e.g. nothrow new), the initialization needs to be guarded by a null check,Previously this was an errorNYI.
This wraps the initialization in a cir.if conditioned on the allocation being non null, then uses cir.select to pick between the allocation pointer and null as the result. Also removes MissingFeatures::exprNewNullCheck.
part of https://github.com/llvm/llvm-project/issues/192328


>From c5aad484ad4cbaa5f7302b85d91287799fe7dc83 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] [CIR] Implement null check for CXXNewExpr

---
 clang/include/clang/CIR/MissingFeatures.h |  3 -
 clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp   | 62 +++++++++++----
 clang/test/CIR/CodeGen/new-null.cpp       | 96 +++++++++++++++++++++++
 3 files changed, 144 insertions(+), 17 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..356198f04e645 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -1472,9 +1472,28 @@ 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");
+
+  // The null-check means that the initializer is conditionally
+  // evaluated.
+  ConditionalEvaluation conditional(*this);
+
+  mlir::Location loc = getLoc(e->getSourceRange());
+  cir::IfOp nullCheckOp;
+  cir::YieldOp nullCheckYield;
+  if (nullCheck) {
+    conditional.beginEvaluation();
+
+    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
@@ -1492,8 +1511,7 @@ mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
                           allocatorArgs);
     operatorDeleteCleanup = ehStack.stable_begin();
     cleanupDominator =
-        cir::UnreachableOp::create(builder, getLoc(e->getSourceRange()))
-            .getOperation();
+        cir::UnreachableOp::create(builder, loc).getOperation();
   }
 
   if (allocSize != allocSizeWithoutCookie) {
@@ -1510,17 +1528,15 @@ mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
   } else {
     elementTy = convertTypeForMem(allocType);
   }
-  Address result = builder.createElementBitCast(getLoc(e->getSourceRange()),
-                                                allocation, elementTy);
+  Address result = builder.createElementBitCast(loc, 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);
+                         loc, "__new_result");
+    builder.createStore(loc, result.getPointer(), resultPtr);
   }
 
   // Passing pointer through launder.invariant.group to avoid propagation of
@@ -1543,14 +1559,32 @@ mlir::Value CIRGenFunction::emitCXXNewExpr(const CXXNewExpr *e) {
     assert(resultPtr.isValid());
     deactivateCleanupBlock(operatorDeleteCleanup, cleanupDominator);
     cleanupDominator->erase();
-    cir::LoadOp loadResult =
-        builder.createLoad(getLoc(e->getSourceRange()), resultPtr);
+    cir::LoadOp loadResult = builder.createLoad(loc, resultPtr);
     result = result.withPointer(loadResult.getResult());
   }
 
-  assert(!cir::MissingFeatures::exprNewNullCheck());
+  mlir::Value resultValue = result.getPointer();
+
+  if (nullCheck) {
+    conditional.endEvaluation();
+
+    // Restore insertion point to after the IfOp.
+    builder.setInsertionPointAfter(nullCheckOp);
+
+    // The allocation pointer was checked for null before the IfOp, so we can
+    // use it directly. If it was null, return null; otherwise return the
+    // (possibly cookie-adjusted and bitcast) allocation pointer.
+    mlir::Value allocPtr = allocation.getPointer();
+    mlir::Type resultTy = resultValue.getType();
+    if (allocPtr.getType() != resultTy)
+      allocPtr = builder.createBitcast(allocPtr, resultTy);
+    mlir::Value nullPtr = builder.getNullPtr(resultTy, loc).getResult();
+    resultValue =
+        builder.createSelect(loc, nullCheckOp.getCondition(), allocPtr,
+                             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..91a2d6c9e93a9
--- /dev/null
+++ b/clang/test/CIR/CodeGen/new-null.cpp
@@ -0,0 +1,96 @@
+// 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();
+  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



More information about the cfe-commits mailing list