Skip to content

Conversation

andykaylor
Copy link
Contributor

This adds support for zero-initialization during delegating constructor processing.

Note, this also adds code to skip emitting constructors that are trivial and default to match the classic codegen behavior. The incubator does not skip these constructors, but I have found a case where this results in a call to a default constructor that is never defined.

This adds support for zero-initialization during delegating constructor
processing.

Note, this also adds code to skip emitting constructors that are
trivial and default to match the classic codegen behavior. The incubator
does not skip these constructors, but I have found a case where this
results in a call to a default constructor that is never defined.
@llvmbot llvmbot added clang Clang issues not falling into any other category ClangIR Anything related to the ClangIR project labels Sep 3, 2025
@llvmbot
Copy link
Member

llvmbot commented Sep 3, 2025

@llvm/pr-subscribers-clang

Author: Andy Kaylor (andykaylor)

Changes

This adds support for zero-initialization during delegating constructor processing.

Note, this also adds code to skip emitting constructors that are trivial and default to match the classic codegen behavior. The incubator does not skip these constructors, but I have found a case where this results in a call to a default constructor that is never defined.


Full diff: https://github.com/llvm/llvm-project/pull/156757.diff

5 Files Affected:

  • (modified) clang/lib/CIR/CodeGen/CIRGenExpr.cpp (+15-7)
  • (modified) clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp (+9)
  • (added) clang/test/CIR/CodeGen/delegating-ctor.cpp (+72)
  • (modified) clang/test/CIR/CodeGen/new.cpp (-4)
  • (modified) clang/test/CIR/CodeGen/vbase.cpp (-18)
diff --git a/clang/lib/CIR/CodeGen/CIRGenExpr.cpp b/clang/lib/CIR/CodeGen/CIRGenExpr.cpp
index aec60d01fc238..d8c7903a4888d 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExpr.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExpr.cpp
@@ -1966,15 +1966,23 @@ void CIRGenFunction::emitCXXConstructExpr(const CXXConstructExpr *e,
   // constructor, emit the zero initialization now, unless destination is
   // already zeroed.
   if (e->requiresZeroInitialization() && !dest.isZeroed()) {
-    cgm.errorNYI(e->getSourceRange(),
-                 "emitCXXConstructExpr: requires initialization");
-    return;
+    switch (e->getConstructionKind()) {
+    case CXXConstructionKind::Delegating:
+    case CXXConstructionKind::Complete:
+      emitNullInitialization(getLoc(e->getSourceRange()), dest.getAddress(),
+                             e->getType());
+      break;
+    case CXXConstructionKind::VirtualBase:
+    case CXXConstructionKind::NonVirtualBase:
+      cgm.errorNYI(e->getSourceRange(),
+                   "emitCXXConstructExpr: base requires initialization");
+      break;
+    }
   }
 
-  // If this is a call to a trivial default constructor:
-  // In LLVM: do nothing.
-  // In CIR: emit as a regular call, other later passes should lower the
-  // ctor call into trivial initialization.
+  // If this is a call to a trivial default constructor, do nothing.
+  if (cd->isTrivial() && cd->isDefaultConstructor())
+    return;
 
   // Elide the constructor if we're constructing from a temporary
   if (getLangOpts().ElideConstructors && e->isElidable()) {
diff --git a/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp b/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
index 3b76c0981fe80..ee9f58c829ca9 100644
--- a/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
+++ b/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
@@ -1368,6 +1368,15 @@ mlir::LogicalResult CIRToLLVMConstantOpLowering::matchAndRewrite(
     rewriter.replaceOp(op, lowerCirAttrAsValue(op, op.getValue(), rewriter,
                                                getTypeConverter()));
     return mlir::success();
+  } else if (auto recTy = mlir::dyn_cast<cir::RecordType>(op.getType())) {
+    if (mlir::isa<cir::ZeroAttr, cir::UndefAttr>(attr)) {
+      mlir::Value initVal =
+          lowerCirAttrAsValue(op, attr, rewriter, typeConverter);
+      rewriter.replaceOp(op, initVal);
+      return mlir::success();
+    }
+    return op.emitError() << "unsupported lowering for record constant type "
+                          << op.getType();
   } else if (auto complexTy = mlir::dyn_cast<cir::ComplexType>(op.getType())) {
     mlir::Type complexElemTy = complexTy.getElementType();
     mlir::Type complexElemLLVMTy = typeConverter->convertType(complexElemTy);
diff --git a/clang/test/CIR/CodeGen/delegating-ctor.cpp b/clang/test/CIR/CodeGen/delegating-ctor.cpp
new file mode 100644
index 0000000000000..a9cfc5d02173d
--- /dev/null
+++ b/clang/test/CIR/CodeGen/delegating-ctor.cpp
@@ -0,0 +1,72 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s --check-prefix=CIR
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --input-file=%t-cir.ll %s --check-prefix=LLVM
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --input-file=%t.ll %s --check-prefix=OGCG
+
+struct Delegating {
+  Delegating();
+  Delegating(int);
+};
+
+// Check that the constructor being delegated to is called with the correct
+// arguments.
+Delegating::Delegating() : Delegating(0) {}
+
+// CIR: cir.func {{.*}} @_ZN10DelegatingC2Ev(%[[THIS_ARG:.*]]: !cir.ptr<!rec_Delegating> {{.*}})
+// CIR:   %[[THIS_ADDR:.*]] = cir.alloca !cir.ptr<!rec_Delegating>, !cir.ptr<!cir.ptr<!rec_Delegating>>, ["this", init]
+// CIR:   cir.store{{.*}} %[[THIS_ARG]], %[[THIS_ADDR]]
+// CIR:   %[[THIS:.*]] = cir.load %[[THIS_ADDR]]
+// CIR:   %[[ZERO:.*]] = cir.const #cir.int<0> : !s32i
+// CIR:   cir.call @_ZN10DelegatingC2Ei(%[[THIS]], %[[ZERO]]) : (!cir.ptr<!rec_Delegating>, !s32i) -> ()
+
+// LLVM: define {{.*}} @_ZN10DelegatingC2Ev(ptr %[[THIS_ARG:.*]])
+// LLVM:   %[[THIS_ADDR:.*]] = alloca ptr
+// LLVM:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
+// LLVM:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
+// LLVM:   call void @_ZN10DelegatingC2Ei(ptr %[[THIS]], i32 0)
+
+// OGCG: define {{.*}} @_ZN10DelegatingC2Ev(ptr {{.*}} %[[THIS_ARG:.*]])
+// OGCG:   %[[THIS_ADDR:.*]] = alloca ptr
+// OGCG:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
+// OGCG:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
+// OGCG:   call void @_ZN10DelegatingC2Ei(ptr {{.*}} %[[THIS]], i32 {{.*}} 0)
+
+struct DelegatingWithZeroing {
+  int i;
+  DelegatingWithZeroing() = default;
+  DelegatingWithZeroing(int);
+};
+
+// Check that the delegating constructor performs zero-initialization here.
+// FIXME: we should either emit the trivial default constructor or remove the
+// call to it in a lowering pass.
+DelegatingWithZeroing::DelegatingWithZeroing(int) : DelegatingWithZeroing() {}
+
+// CIR: cir.func {{.*}} @_ZN21DelegatingWithZeroingC2Ei(%[[THIS_ARG:.*]]: !cir.ptr<!rec_DelegatingWithZeroing> {{.*}}, %[[I_ARG:.*]]: !s32i {{.*}})
+// CIR:   %[[THIS_ADDR:.*]] = cir.alloca !cir.ptr<!rec_DelegatingWithZeroing>, !cir.ptr<!cir.ptr<!rec_DelegatingWithZeroing>>, ["this", init]
+// CIR:   %[[I_ADDR:.*]] = cir.alloca !s32i, !cir.ptr<!s32i>, ["", init]
+// CIR:   cir.store{{.*}} %[[THIS_ARG]], %[[THIS_ADDR]]
+// CIR:   cir.store{{.*}} %[[I_ARG]], %[[I_ADDR]]
+// CIR:   %[[THIS:.*]] = cir.load %[[THIS_ADDR]]
+// CIR:   %[[ZERO:.*]] = cir.const #cir.zero : !rec_DelegatingWithZeroing
+// CIR:   cir.store{{.*}} %[[ZERO]], %[[THIS]] : !rec_DelegatingWithZeroing, !cir.ptr<!rec_DelegatingWithZeroing>
+
+// LLVM: define {{.*}} void @_ZN21DelegatingWithZeroingC2Ei(ptr %[[THIS_ARG:.*]], i32 %[[I_ARG:.*]])
+// LLVM:   %[[THIS_ADDR:.*]] = alloca ptr
+// LLVM:   %[[I_ADDR:.*]] = alloca i32
+// LLVM:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
+// LLVM:   store i32 %[[I_ARG]], ptr %[[I_ADDR]]
+// LLVM:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
+// LLVM:   store %struct.DelegatingWithZeroing zeroinitializer, ptr %[[THIS]]
+
+// Note: OGCG elides the call to the default constructor.
+
+// OGCG: define {{.*}} void @_ZN21DelegatingWithZeroingC2Ei(ptr {{.*}} %[[THIS_ARG:.*]], i32 {{.*}} %[[I_ARG:.*]])
+// OGCG:   %[[THIS_ADDR:.*]] = alloca ptr
+// OGCG:   %[[I_ADDR:.*]] = alloca i32
+// OGCG:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
+// OGCG:   store i32 %[[I_ARG]], ptr %[[I_ADDR]]
+// OGCG:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
+// OGCG:   call void @llvm.memset.p0.i64(ptr align 4 %[[THIS]], i8 0, i64 4, i1 false)
diff --git a/clang/test/CIR/CodeGen/new.cpp b/clang/test/CIR/CodeGen/new.cpp
index 4f88addc6116c..31adb9bf4859b 100644
--- a/clang/test/CIR/CodeGen/new.cpp
+++ b/clang/test/CIR/CodeGen/new.cpp
@@ -23,7 +23,6 @@ void test_basic_new() {
 // CHECK:   %[[EIGHT:.*]] = cir.const #cir.int<8>
 // CHECK:   %[[NEW_S:.*]] = cir.call @_Znwm(%[[EIGHT]])
 // CHECK:   %[[NEW_S_PTR:.*]] = cir.cast(bitcast, %[[NEW_S]]
-// CHECK:   cir.call @_ZN1SC1Ev(%[[NEW_S_PTR]])
 // CHECK:   cir.store{{.*}} %[[NEW_S_PTR]], %[[PS_ADDR]]
 // CHECK:   %[[FOUR:.*]] = cir.const #cir.int<4>
 // CHECK:   %[[NEW_INT:.*]] = cir.call @_Znwm(%[[FOUR]])
@@ -40,7 +39,6 @@ void test_basic_new() {
 // LLVM:   %[[PN_ADDR:.*]] = alloca ptr, i64 1, align 8
 // LLVM:   %[[PD_ADDR:.*]] = alloca ptr, i64 1, align 8
 // LLVM:   %[[NEW_S:.*]] = call{{.*}} ptr @_Znwm(i64 8)
-// LLVM:   call{{.*}} void @_ZN1SC1Ev(ptr %[[NEW_S]])
 // LLVM:   store ptr %[[NEW_S]], ptr %[[PS_ADDR]], align 8
 // LLVM:   %[[NEW_INT:.*]] = call{{.*}} ptr @_Znwm(i64 4)
 // LLVM:   store ptr %[[NEW_INT]], ptr %[[PN_ADDR]], align 8
@@ -48,8 +46,6 @@ void test_basic_new() {
 // LLVM:   store ptr %[[NEW_DOUBLE]], ptr %[[PD_ADDR]], align 8
 // LLVM:   ret void
 
-// NOTE: OGCG elides the constructor call here, but CIR does not.
-
 // OGCG: define{{.*}} void @_Z14test_basic_newv
 // OGCG:   %[[PS_ADDR:.*]] = alloca ptr, align 8
 // OGCG:   %[[PN_ADDR:.*]] = alloca ptr, align 8
diff --git a/clang/test/CIR/CodeGen/vbase.cpp b/clang/test/CIR/CodeGen/vbase.cpp
index 1d1b5e083bfc9..ee6471c944a42 100644
--- a/clang/test/CIR/CodeGen/vbase.cpp
+++ b/clang/test/CIR/CodeGen/vbase.cpp
@@ -24,28 +24,12 @@ void ppp() { B b; }
 
 // OGCG: @_ZTV1B = linkonce_odr unnamed_addr constant { [3 x ptr] } { [3 x ptr] [ptr inttoptr (i64 12 to ptr), ptr null, ptr @_ZTI1B] }, comdat, align 8
 
-// Constructor for A
-// CIR: cir.func comdat linkonce_odr @_ZN1AC2Ev(%arg0: !cir.ptr<!rec_A>
-// CIR:   %[[THIS_ADDR:.*]] = cir.alloca !cir.ptr<!rec_A>, !cir.ptr<!cir.ptr<!rec_A>>, ["this", init]
-// CIR:   cir.store %arg0, %[[THIS_ADDR]] : !cir.ptr<!rec_A>, !cir.ptr<!cir.ptr<!rec_A>>
-// CIR:   %[[THIS:.*]] = cir.load %[[THIS_ADDR]] : !cir.ptr<!cir.ptr<!rec_A>>, !cir.ptr<!rec_A>
-// CIR:   cir.return
-
-// LLVM: define{{.*}} void @_ZN1AC2Ev(ptr %[[THIS_ARG:.*]]) {
-// LLVM:   %[[THIS_ADDR:.*]] = alloca ptr
-// LLVM:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
-// LLVM:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
-// LLVM:   ret void
-
-// Note: OGCG elides the constructor for A. This is not yet implemented in CIR.
-
 // Constructor for B
 // CIR: cir.func comdat linkonce_odr @_ZN1BC1Ev(%arg0: !cir.ptr<!rec_B>
 // CIR:   %[[THIS_ADDR:.*]] = cir.alloca !cir.ptr<!rec_B>, !cir.ptr<!cir.ptr<!rec_B>>, ["this", init]
 // CIR:   cir.store %arg0, %[[THIS_ADDR]] : !cir.ptr<!rec_B>, !cir.ptr<!cir.ptr<!rec_B>>
 // CIR:   %[[THIS:.*]] = cir.load %[[THIS_ADDR]] : !cir.ptr<!cir.ptr<!rec_B>>, !cir.ptr<!rec_B>
 // CIR:   %[[BASE_A_ADDR:.*]] = cir.base_class_addr %[[THIS]] : !cir.ptr<!rec_B> nonnull [12] -> !cir.ptr<!rec_A>
-// CIR:   cir.call @_ZN1AC2Ev(%[[BASE_A_ADDR]]) nothrow : (!cir.ptr<!rec_A>) -> ()
 // CIR:   %[[VTABLE:.*]] = cir.vtable.address_point(@_ZTV1B, address_point = <index = 0, offset = 3>) : !cir.vptr
 // CIR:   %[[B_VPTR:.*]] = cir.vtable.get_vptr %[[THIS]] : !cir.ptr<!rec_B> -> !cir.ptr<!cir.vptr>
 // CIR:   cir.store align(8) %[[VTABLE]], %[[B_VPTR]] : !cir.vptr, !cir.ptr<!cir.vptr>
@@ -56,7 +40,6 @@ void ppp() { B b; }
 // LLVM:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
 // LLVM:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
 // LLVM:   %[[BASE_A_ADDR:.*]] = getelementptr i8, ptr %[[THIS]], i32 12
-// LLVM:   call void @_ZN1AC2Ev(ptr %[[BASE_A_ADDR]])
 // LLVM:   store ptr getelementptr inbounds nuw (i8, ptr @_ZTV1B, i64 24), ptr %[[THIS]]
 // LLVM:   ret void
 
@@ -67,4 +50,3 @@ void ppp() { B b; }
 // OGCG:   %[[BASE_A_ADDR:.*]] = getelementptr inbounds i8, ptr %[[THIS]], i64 12
 // OGCG:   store ptr getelementptr inbounds inrange(-24, 0) ({ [3 x ptr] }, ptr @_ZTV1B, i32 0, i32 0, i32 3), ptr %[[THIS]]
 // OGCG:   ret void
-

@llvmbot
Copy link
Member

llvmbot commented Sep 3, 2025

@llvm/pr-subscribers-clangir

Author: Andy Kaylor (andykaylor)

Changes

This adds support for zero-initialization during delegating constructor processing.

Note, this also adds code to skip emitting constructors that are trivial and default to match the classic codegen behavior. The incubator does not skip these constructors, but I have found a case where this results in a call to a default constructor that is never defined.


Full diff: https://github.com/llvm/llvm-project/pull/156757.diff

5 Files Affected:

  • (modified) clang/lib/CIR/CodeGen/CIRGenExpr.cpp (+15-7)
  • (modified) clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp (+9)
  • (added) clang/test/CIR/CodeGen/delegating-ctor.cpp (+72)
  • (modified) clang/test/CIR/CodeGen/new.cpp (-4)
  • (modified) clang/test/CIR/CodeGen/vbase.cpp (-18)
diff --git a/clang/lib/CIR/CodeGen/CIRGenExpr.cpp b/clang/lib/CIR/CodeGen/CIRGenExpr.cpp
index aec60d01fc238..d8c7903a4888d 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExpr.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExpr.cpp
@@ -1966,15 +1966,23 @@ void CIRGenFunction::emitCXXConstructExpr(const CXXConstructExpr *e,
   // constructor, emit the zero initialization now, unless destination is
   // already zeroed.
   if (e->requiresZeroInitialization() && !dest.isZeroed()) {
-    cgm.errorNYI(e->getSourceRange(),
-                 "emitCXXConstructExpr: requires initialization");
-    return;
+    switch (e->getConstructionKind()) {
+    case CXXConstructionKind::Delegating:
+    case CXXConstructionKind::Complete:
+      emitNullInitialization(getLoc(e->getSourceRange()), dest.getAddress(),
+                             e->getType());
+      break;
+    case CXXConstructionKind::VirtualBase:
+    case CXXConstructionKind::NonVirtualBase:
+      cgm.errorNYI(e->getSourceRange(),
+                   "emitCXXConstructExpr: base requires initialization");
+      break;
+    }
   }
 
-  // If this is a call to a trivial default constructor:
-  // In LLVM: do nothing.
-  // In CIR: emit as a regular call, other later passes should lower the
-  // ctor call into trivial initialization.
+  // If this is a call to a trivial default constructor, do nothing.
+  if (cd->isTrivial() && cd->isDefaultConstructor())
+    return;
 
   // Elide the constructor if we're constructing from a temporary
   if (getLangOpts().ElideConstructors && e->isElidable()) {
diff --git a/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp b/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
index 3b76c0981fe80..ee9f58c829ca9 100644
--- a/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
+++ b/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp
@@ -1368,6 +1368,15 @@ mlir::LogicalResult CIRToLLVMConstantOpLowering::matchAndRewrite(
     rewriter.replaceOp(op, lowerCirAttrAsValue(op, op.getValue(), rewriter,
                                                getTypeConverter()));
     return mlir::success();
+  } else if (auto recTy = mlir::dyn_cast<cir::RecordType>(op.getType())) {
+    if (mlir::isa<cir::ZeroAttr, cir::UndefAttr>(attr)) {
+      mlir::Value initVal =
+          lowerCirAttrAsValue(op, attr, rewriter, typeConverter);
+      rewriter.replaceOp(op, initVal);
+      return mlir::success();
+    }
+    return op.emitError() << "unsupported lowering for record constant type "
+                          << op.getType();
   } else if (auto complexTy = mlir::dyn_cast<cir::ComplexType>(op.getType())) {
     mlir::Type complexElemTy = complexTy.getElementType();
     mlir::Type complexElemLLVMTy = typeConverter->convertType(complexElemTy);
diff --git a/clang/test/CIR/CodeGen/delegating-ctor.cpp b/clang/test/CIR/CodeGen/delegating-ctor.cpp
new file mode 100644
index 0000000000000..a9cfc5d02173d
--- /dev/null
+++ b/clang/test/CIR/CodeGen/delegating-ctor.cpp
@@ -0,0 +1,72 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s --check-prefix=CIR
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --input-file=%t-cir.ll %s --check-prefix=LLVM
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --input-file=%t.ll %s --check-prefix=OGCG
+
+struct Delegating {
+  Delegating();
+  Delegating(int);
+};
+
+// Check that the constructor being delegated to is called with the correct
+// arguments.
+Delegating::Delegating() : Delegating(0) {}
+
+// CIR: cir.func {{.*}} @_ZN10DelegatingC2Ev(%[[THIS_ARG:.*]]: !cir.ptr<!rec_Delegating> {{.*}})
+// CIR:   %[[THIS_ADDR:.*]] = cir.alloca !cir.ptr<!rec_Delegating>, !cir.ptr<!cir.ptr<!rec_Delegating>>, ["this", init]
+// CIR:   cir.store{{.*}} %[[THIS_ARG]], %[[THIS_ADDR]]
+// CIR:   %[[THIS:.*]] = cir.load %[[THIS_ADDR]]
+// CIR:   %[[ZERO:.*]] = cir.const #cir.int<0> : !s32i
+// CIR:   cir.call @_ZN10DelegatingC2Ei(%[[THIS]], %[[ZERO]]) : (!cir.ptr<!rec_Delegating>, !s32i) -> ()
+
+// LLVM: define {{.*}} @_ZN10DelegatingC2Ev(ptr %[[THIS_ARG:.*]])
+// LLVM:   %[[THIS_ADDR:.*]] = alloca ptr
+// LLVM:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
+// LLVM:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
+// LLVM:   call void @_ZN10DelegatingC2Ei(ptr %[[THIS]], i32 0)
+
+// OGCG: define {{.*}} @_ZN10DelegatingC2Ev(ptr {{.*}} %[[THIS_ARG:.*]])
+// OGCG:   %[[THIS_ADDR:.*]] = alloca ptr
+// OGCG:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
+// OGCG:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
+// OGCG:   call void @_ZN10DelegatingC2Ei(ptr {{.*}} %[[THIS]], i32 {{.*}} 0)
+
+struct DelegatingWithZeroing {
+  int i;
+  DelegatingWithZeroing() = default;
+  DelegatingWithZeroing(int);
+};
+
+// Check that the delegating constructor performs zero-initialization here.
+// FIXME: we should either emit the trivial default constructor or remove the
+// call to it in a lowering pass.
+DelegatingWithZeroing::DelegatingWithZeroing(int) : DelegatingWithZeroing() {}
+
+// CIR: cir.func {{.*}} @_ZN21DelegatingWithZeroingC2Ei(%[[THIS_ARG:.*]]: !cir.ptr<!rec_DelegatingWithZeroing> {{.*}}, %[[I_ARG:.*]]: !s32i {{.*}})
+// CIR:   %[[THIS_ADDR:.*]] = cir.alloca !cir.ptr<!rec_DelegatingWithZeroing>, !cir.ptr<!cir.ptr<!rec_DelegatingWithZeroing>>, ["this", init]
+// CIR:   %[[I_ADDR:.*]] = cir.alloca !s32i, !cir.ptr<!s32i>, ["", init]
+// CIR:   cir.store{{.*}} %[[THIS_ARG]], %[[THIS_ADDR]]
+// CIR:   cir.store{{.*}} %[[I_ARG]], %[[I_ADDR]]
+// CIR:   %[[THIS:.*]] = cir.load %[[THIS_ADDR]]
+// CIR:   %[[ZERO:.*]] = cir.const #cir.zero : !rec_DelegatingWithZeroing
+// CIR:   cir.store{{.*}} %[[ZERO]], %[[THIS]] : !rec_DelegatingWithZeroing, !cir.ptr<!rec_DelegatingWithZeroing>
+
+// LLVM: define {{.*}} void @_ZN21DelegatingWithZeroingC2Ei(ptr %[[THIS_ARG:.*]], i32 %[[I_ARG:.*]])
+// LLVM:   %[[THIS_ADDR:.*]] = alloca ptr
+// LLVM:   %[[I_ADDR:.*]] = alloca i32
+// LLVM:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
+// LLVM:   store i32 %[[I_ARG]], ptr %[[I_ADDR]]
+// LLVM:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
+// LLVM:   store %struct.DelegatingWithZeroing zeroinitializer, ptr %[[THIS]]
+
+// Note: OGCG elides the call to the default constructor.
+
+// OGCG: define {{.*}} void @_ZN21DelegatingWithZeroingC2Ei(ptr {{.*}} %[[THIS_ARG:.*]], i32 {{.*}} %[[I_ARG:.*]])
+// OGCG:   %[[THIS_ADDR:.*]] = alloca ptr
+// OGCG:   %[[I_ADDR:.*]] = alloca i32
+// OGCG:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
+// OGCG:   store i32 %[[I_ARG]], ptr %[[I_ADDR]]
+// OGCG:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
+// OGCG:   call void @llvm.memset.p0.i64(ptr align 4 %[[THIS]], i8 0, i64 4, i1 false)
diff --git a/clang/test/CIR/CodeGen/new.cpp b/clang/test/CIR/CodeGen/new.cpp
index 4f88addc6116c..31adb9bf4859b 100644
--- a/clang/test/CIR/CodeGen/new.cpp
+++ b/clang/test/CIR/CodeGen/new.cpp
@@ -23,7 +23,6 @@ void test_basic_new() {
 // CHECK:   %[[EIGHT:.*]] = cir.const #cir.int<8>
 // CHECK:   %[[NEW_S:.*]] = cir.call @_Znwm(%[[EIGHT]])
 // CHECK:   %[[NEW_S_PTR:.*]] = cir.cast(bitcast, %[[NEW_S]]
-// CHECK:   cir.call @_ZN1SC1Ev(%[[NEW_S_PTR]])
 // CHECK:   cir.store{{.*}} %[[NEW_S_PTR]], %[[PS_ADDR]]
 // CHECK:   %[[FOUR:.*]] = cir.const #cir.int<4>
 // CHECK:   %[[NEW_INT:.*]] = cir.call @_Znwm(%[[FOUR]])
@@ -40,7 +39,6 @@ void test_basic_new() {
 // LLVM:   %[[PN_ADDR:.*]] = alloca ptr, i64 1, align 8
 // LLVM:   %[[PD_ADDR:.*]] = alloca ptr, i64 1, align 8
 // LLVM:   %[[NEW_S:.*]] = call{{.*}} ptr @_Znwm(i64 8)
-// LLVM:   call{{.*}} void @_ZN1SC1Ev(ptr %[[NEW_S]])
 // LLVM:   store ptr %[[NEW_S]], ptr %[[PS_ADDR]], align 8
 // LLVM:   %[[NEW_INT:.*]] = call{{.*}} ptr @_Znwm(i64 4)
 // LLVM:   store ptr %[[NEW_INT]], ptr %[[PN_ADDR]], align 8
@@ -48,8 +46,6 @@ void test_basic_new() {
 // LLVM:   store ptr %[[NEW_DOUBLE]], ptr %[[PD_ADDR]], align 8
 // LLVM:   ret void
 
-// NOTE: OGCG elides the constructor call here, but CIR does not.
-
 // OGCG: define{{.*}} void @_Z14test_basic_newv
 // OGCG:   %[[PS_ADDR:.*]] = alloca ptr, align 8
 // OGCG:   %[[PN_ADDR:.*]] = alloca ptr, align 8
diff --git a/clang/test/CIR/CodeGen/vbase.cpp b/clang/test/CIR/CodeGen/vbase.cpp
index 1d1b5e083bfc9..ee6471c944a42 100644
--- a/clang/test/CIR/CodeGen/vbase.cpp
+++ b/clang/test/CIR/CodeGen/vbase.cpp
@@ -24,28 +24,12 @@ void ppp() { B b; }
 
 // OGCG: @_ZTV1B = linkonce_odr unnamed_addr constant { [3 x ptr] } { [3 x ptr] [ptr inttoptr (i64 12 to ptr), ptr null, ptr @_ZTI1B] }, comdat, align 8
 
-// Constructor for A
-// CIR: cir.func comdat linkonce_odr @_ZN1AC2Ev(%arg0: !cir.ptr<!rec_A>
-// CIR:   %[[THIS_ADDR:.*]] = cir.alloca !cir.ptr<!rec_A>, !cir.ptr<!cir.ptr<!rec_A>>, ["this", init]
-// CIR:   cir.store %arg0, %[[THIS_ADDR]] : !cir.ptr<!rec_A>, !cir.ptr<!cir.ptr<!rec_A>>
-// CIR:   %[[THIS:.*]] = cir.load %[[THIS_ADDR]] : !cir.ptr<!cir.ptr<!rec_A>>, !cir.ptr<!rec_A>
-// CIR:   cir.return
-
-// LLVM: define{{.*}} void @_ZN1AC2Ev(ptr %[[THIS_ARG:.*]]) {
-// LLVM:   %[[THIS_ADDR:.*]] = alloca ptr
-// LLVM:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
-// LLVM:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
-// LLVM:   ret void
-
-// Note: OGCG elides the constructor for A. This is not yet implemented in CIR.
-
 // Constructor for B
 // CIR: cir.func comdat linkonce_odr @_ZN1BC1Ev(%arg0: !cir.ptr<!rec_B>
 // CIR:   %[[THIS_ADDR:.*]] = cir.alloca !cir.ptr<!rec_B>, !cir.ptr<!cir.ptr<!rec_B>>, ["this", init]
 // CIR:   cir.store %arg0, %[[THIS_ADDR]] : !cir.ptr<!rec_B>, !cir.ptr<!cir.ptr<!rec_B>>
 // CIR:   %[[THIS:.*]] = cir.load %[[THIS_ADDR]] : !cir.ptr<!cir.ptr<!rec_B>>, !cir.ptr<!rec_B>
 // CIR:   %[[BASE_A_ADDR:.*]] = cir.base_class_addr %[[THIS]] : !cir.ptr<!rec_B> nonnull [12] -> !cir.ptr<!rec_A>
-// CIR:   cir.call @_ZN1AC2Ev(%[[BASE_A_ADDR]]) nothrow : (!cir.ptr<!rec_A>) -> ()
 // CIR:   %[[VTABLE:.*]] = cir.vtable.address_point(@_ZTV1B, address_point = <index = 0, offset = 3>) : !cir.vptr
 // CIR:   %[[B_VPTR:.*]] = cir.vtable.get_vptr %[[THIS]] : !cir.ptr<!rec_B> -> !cir.ptr<!cir.vptr>
 // CIR:   cir.store align(8) %[[VTABLE]], %[[B_VPTR]] : !cir.vptr, !cir.ptr<!cir.vptr>
@@ -56,7 +40,6 @@ void ppp() { B b; }
 // LLVM:   store ptr %[[THIS_ARG]], ptr %[[THIS_ADDR]]
 // LLVM:   %[[THIS:.*]] = load ptr, ptr %[[THIS_ADDR]]
 // LLVM:   %[[BASE_A_ADDR:.*]] = getelementptr i8, ptr %[[THIS]], i32 12
-// LLVM:   call void @_ZN1AC2Ev(ptr %[[BASE_A_ADDR]])
 // LLVM:   store ptr getelementptr inbounds nuw (i8, ptr @_ZTV1B, i64 24), ptr %[[THIS]]
 // LLVM:   ret void
 
@@ -67,4 +50,3 @@ void ppp() { B b; }
 // OGCG:   %[[BASE_A_ADDR:.*]] = getelementptr inbounds i8, ptr %[[THIS]], i64 12
 // OGCG:   store ptr getelementptr inbounds inrange(-24, 0) ({ [3 x ptr] }, ptr @_ZTV1B, i32 0, i32 0, i32 3), ptr %[[THIS]]
 // OGCG:   ret void
-

@andykaylor
Copy link
Contributor Author

Here is an example where the incubator emits a declaration of a default constructor that is never defined.

https://godbolt.org/z/qM1Mf5xMc

A comment in the code claimed that a later pass would lower the ctor call to trivial initialization, but it never does.

Copy link
Contributor

@mmha mmha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Can you add a couple more test cases?

struct WithBraceInit {
  int i{};
  WithBraceInit() = default;
  WithBraceInit(int);
};

WithBraceInit:WithBraceInit(int) : WithBraceInit() {}

int main() {
  // Destinations are zeroed
  static DelegatingWithZeroing dwz(123);
  static WithBraceInit wbi(456);
}

@andykaylor
Copy link
Contributor Author

LGTM. Can you add a couple more test cases?

struct WithBraceInit {
  int i{};
  WithBraceInit() = default;
  WithBraceInit(int);
};

WithBraceInit:WithBraceInit(int) : WithBraceInit() {}

int main() {
  // Destinations are zeroed
  static DelegatingWithZeroing dwz(123);
  static WithBraceInit wbi(456);
}

Those cases run into other things that aren't yet implemented. For the default constructor of WithBraceInit, we hit scalar expression kind: : CXXDefaultInitExpr. For the static variable declarations, it's ConstExprEmitter::VisitCXXConstructExpr. I can add those in follow-up PRs.

@andykaylor andykaylor merged commit 83da177 into llvm:main Sep 4, 2025
12 checks passed
@andykaylor andykaylor deleted the cir-delegating-ctor branch September 4, 2025 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang Clang issues not falling into any other category ClangIR Anything related to the ClangIR project
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants