Skip to content

Conversation

andykaylor
Copy link
Contributor

This adds support for calling virtual destructors.

This adds support for calling virtual destructors.
@llvmbot llvmbot added clang Clang issues not falling into any other category ClangIR Anything related to the ClangIR project labels Oct 9, 2025
@llvmbot
Copy link
Member

llvmbot commented Oct 9, 2025

@llvm/pr-subscribers-clang

Author: Andy Kaylor (andykaylor)

Changes

This adds support for calling virtual destructors.


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

9 Files Affected:

  • (modified) clang/lib/CIR/CodeGen/Address.h (+7)
  • (modified) clang/lib/CIR/CodeGen/CIRGenCXXABI.h (+9)
  • (modified) clang/lib/CIR/CodeGen/CIRGenClass.cpp (+32-1)
  • (modified) clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp (+33-10)
  • (modified) clang/lib/CIR/CodeGen/CIRGenFunction.cpp (+7-1)
  • (modified) clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp (+30-1)
  • (modified) clang/lib/CIR/CodeGen/CIRGenTypes.cpp (+2-4)
  • (modified) clang/lib/CIR/CodeGen/CIRGenVTables.cpp (+3-7)
  • (added) clang/test/CIR/CodeGen/virtual-destructor-calls.cpp (+129)
diff --git a/clang/lib/CIR/CodeGen/Address.h b/clang/lib/CIR/CodeGen/Address.h
index fb74aa0f3bb00..683ff396eb5cb 100644
--- a/clang/lib/CIR/CodeGen/Address.h
+++ b/clang/lib/CIR/CodeGen/Address.h
@@ -90,6 +90,13 @@ class Address {
     return getPointer();
   }
 
+  /// Return the pointer contained in this class after authenticating it and
+  /// adding offset to it if necessary.
+  mlir::Value emitRawPointer() const {
+    // TODO(cir): update this class with latest traditional LLVM codegen bits
+    return getBasePointer();
+  }
+
   mlir::Type getType() const {
     assert(mlir::cast<cir::PointerType>(
                pointerAndKnownNonNull.getPointer().getType())
diff --git a/clang/lib/CIR/CodeGen/CIRGenCXXABI.h b/clang/lib/CIR/CodeGen/CIRGenCXXABI.h
index 06f41cd8fcfdb..6d3741c417351 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCXXABI.h
+++ b/clang/lib/CIR/CodeGen/CIRGenCXXABI.h
@@ -191,6 +191,15 @@ class CIRGenCXXABI {
   virtual void emitVTableDefinitions(CIRGenVTables &cgvt,
                                      const CXXRecordDecl *rd) = 0;
 
+  using DeleteOrMemberCallExpr =
+      llvm::PointerUnion<const CXXDeleteExpr *, const CXXMemberCallExpr *>;
+
+  virtual mlir::Value emitVirtualDestructorCall(CIRGenFunction &cgf,
+                                                const CXXDestructorDecl *dtor,
+                                                CXXDtorType dtorType,
+                                                Address thisAddr,
+                                                DeleteOrMemberCallExpr e) = 0;
+
   /// Emit any tables needed to implement virtual inheritance.  For Itanium,
   /// this emits virtual table tables.
   virtual void emitVirtualInheritanceTables(const CXXRecordDecl *rd) = 0;
diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index 485b2c86cbc58..dd357ce69f1b3 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -895,6 +895,26 @@ void CIRGenFunction::destroyCXXObject(CIRGenFunction &cgf, Address addr,
 }
 
 namespace {
+mlir::Value loadThisForDtorDelete(CIRGenFunction &cgf,
+                                  const CXXDestructorDecl *dd) {
+  if (Expr *thisArg = dd->getOperatorDeleteThisArg())
+    return cgf.emitScalarExpr(thisArg);
+  return cgf.loadCXXThis();
+}
+
+/// Call the operator delete associated with the current destructor.
+struct CallDtorDelete final : EHScopeStack::Cleanup {
+  CallDtorDelete() {}
+
+  void emit(CIRGenFunction &cgf) override {
+    const CXXDestructorDecl *dtor = cast<CXXDestructorDecl>(cgf.curFuncDecl);
+    const CXXRecordDecl *classDecl = dtor->getParent();
+    cgf.emitDeleteCall(dtor->getOperatorDelete(),
+                       loadThisForDtorDelete(cgf, dtor),
+                       cgf.getContext().getCanonicalTagType(classDecl));
+  }
+};
+
 class DestroyField final : public EHScopeStack::Cleanup {
   const FieldDecl *field;
   CIRGenFunction::Destroyer *destroyer;
@@ -932,7 +952,18 @@ void CIRGenFunction::enterDtorCleanups(const CXXDestructorDecl *dd,
   // The deleting-destructor phase just needs to call the appropriate
   // operator delete that Sema picked up.
   if (dtorType == Dtor_Deleting) {
-    cgm.errorNYI(dd->getSourceRange(), "deleting destructor cleanups");
+    assert(dd->getOperatorDelete() &&
+           "operator delete missing - EnterDtorCleanups");
+    if (cxxStructorImplicitParamValue) {
+      cgm.errorNYI(dd->getSourceRange(), "deleting destructor with vtt");
+    } else {
+      if (dd->getOperatorDelete()->isDestroyingOperatorDelete()) {
+        cgm.errorNYI(dd->getSourceRange(),
+                     "deleting destructor with destroying operator delete");
+      } else {
+        ehStack.pushCleanup<CallDtorDelete>(NormalAndEHCleanup);
+      }
+    }
     return;
   }
 
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
index 97c0944fca336..2d11cee38b4c6 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -130,13 +130,11 @@ RValue CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
   const CXXMethodDecl *calleeDecl =
       devirtualizedMethod ? devirtualizedMethod : md;
   const CIRGenFunctionInfo *fInfo = nullptr;
-  if (isa<CXXDestructorDecl>(calleeDecl)) {
-    cgm.errorNYI(ce->getSourceRange(),
-                 "emitCXXMemberOrOperatorMemberCallExpr: destructor call");
-    return RValue::get(nullptr);
-  }
-
-  fInfo = &cgm.getTypes().arrangeCXXMethodDeclaration(calleeDecl);
+  if (const auto *dtor = dyn_cast<CXXDestructorDecl>(calleeDecl))
+    fInfo = &cgm.getTypes().arrangeCXXStructorDeclaration(
+        GlobalDecl(dtor, Dtor_Complete));
+  else
+    fInfo = &cgm.getTypes().arrangeCXXMethodDeclaration(calleeDecl);
 
   cir::FuncType ty = cgm.getTypes().getFunctionType(*fInfo);
 
@@ -151,9 +149,34 @@ RValue CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
   // because then we know what the type is.
   bool useVirtualCall = canUseVirtualCall && !devirtualizedMethod;
 
-  if (isa<CXXDestructorDecl>(calleeDecl)) {
-    cgm.errorNYI(ce->getSourceRange(),
-                 "emitCXXMemberOrOperatorMemberCallExpr: destructor call");
+  if (const auto *dtor = dyn_cast<CXXDestructorDecl>(calleeDecl)) {
+    assert(ce->arg_begin() == ce->arg_end() &&
+           "Destructor shouldn't have explicit parameters");
+    assert(returnValue.isNull() && "Destructor shouldn't have return value");
+    if (useVirtualCall) {
+      cgm.getCXXABI().emitVirtualDestructorCall(*this, dtor, Dtor_Complete,
+                                                thisPtr.getAddress(),
+                                                cast<CXXMemberCallExpr>(ce));
+    } else {
+      GlobalDecl globalDecl(dtor, Dtor_Complete);
+      CIRGenCallee callee;
+      assert(!cir::MissingFeatures::appleKext());
+      if (!devirtualizedMethod) {
+        callee = CIRGenCallee::forDirect(
+            cgm.getAddrOfCXXStructor(globalDecl, fInfo, ty), globalDecl);
+      } else {
+        cgm.errorNYI(ce->getSourceRange(), "devirtualized destructor call");
+        return RValue::get(nullptr);
+      }
+
+      QualType thisTy =
+          isArrow ? base->getType()->getPointeeType() : base->getType();
+      // CIRGen does not pass CallOrInvoke here (different from OG LLVM codegen)
+      // because in practice it always null even in OG.
+      emitCXXDestructorCall(globalDecl, callee, thisPtr.getPointer(), thisTy,
+                            /*ImplicitParam=*/nullptr,
+                            /*ImplicitParamTy=*/QualType(), ce);
+    }
     return RValue::get(nullptr);
   }
 
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
index 7a774e0441bbb..f7c2fb3a68bbb 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
@@ -678,7 +678,13 @@ void CIRGenFunction::emitDestructorBody(FunctionArgList &args) {
   // possible to delegate the destructor body to the complete
   // destructor.  Do so.
   if (dtorType == Dtor_Deleting) {
-    cgm.errorNYI(dtor->getSourceRange(), "deleting destructor");
+    RunCleanupsScope dtorEpilogue(*this);
+    enterDtorCleanups(dtor, Dtor_Deleting);
+    if (haveInsertPoint()) {
+      QualType thisTy = dtor->getFunctionObjectParameterType();
+      emitCXXDestructorCall(dtor, Dtor_Complete, /*ForVirtualBase=*/false,
+                            /*Delegating=*/false, loadCXXThisAddress(), thisTy);
+    }
     return;
   }
 
diff --git a/clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp b/clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp
index 9e490c6d054a4..4be03ca5f4dff 100644
--- a/clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp
@@ -95,7 +95,10 @@ class CIRGenItaniumCXXABI : public CIRGenCXXABI {
                                          clang::GlobalDecl gd, Address thisAddr,
                                          mlir::Type ty,
                                          SourceLocation loc) override;
-
+  mlir::Value emitVirtualDestructorCall(CIRGenFunction &cgf,
+                                        const CXXDestructorDecl *dtor,
+                                        CXXDtorType dtorType, Address thisAddr,
+                                        DeleteOrMemberCallExpr e) override;
   mlir::Value getVTableAddressPoint(BaseSubobject base,
                                     const CXXRecordDecl *vtableClass) override;
   mlir::Value getVTableAddressPointInStructorWithVTT(
@@ -465,6 +468,32 @@ void CIRGenItaniumCXXABI::emitVTableDefinitions(CIRGenVTables &cgvt,
   }
 }
 
+mlir::Value CIRGenItaniumCXXABI::emitVirtualDestructorCall(
+    CIRGenFunction &cgf, const CXXDestructorDecl *dtor, CXXDtorType dtorType,
+    Address thisAddr, DeleteOrMemberCallExpr expr) {
+  auto *callExpr = dyn_cast<const CXXMemberCallExpr *>(expr);
+  auto *delExpr = dyn_cast<const CXXDeleteExpr *>(expr);
+  assert((callExpr != nullptr) ^ (delExpr != nullptr));
+  assert(callExpr == nullptr || callExpr->arg_begin() == callExpr->arg_end());
+  assert(dtorType == Dtor_Deleting || dtorType == Dtor_Complete);
+
+  GlobalDecl globalDecl(dtor, dtorType);
+  const CIRGenFunctionInfo *fnInfo =
+      &cgm.getTypes().arrangeCXXStructorDeclaration(globalDecl);
+  const cir::FuncType &fnTy = cgm.getTypes().getFunctionType(*fnInfo);
+  auto callee = CIRGenCallee::forVirtual(callExpr, globalDecl, thisAddr, fnTy);
+
+  QualType thisTy;
+  if (callExpr)
+    thisTy = callExpr->getObjectType();
+  else
+    thisTy = delExpr->getDestroyedType();
+
+  cgf.emitCXXDestructorCall(globalDecl, callee, thisAddr.emitRawPointer(),
+                            thisTy, nullptr, QualType(), nullptr);
+  return nullptr;
+}
+
 void CIRGenItaniumCXXABI::emitVirtualInheritanceTables(
     const CXXRecordDecl *rd) {
   CIRGenVTables &vtables = cgm.getVTables();
diff --git a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
index e65896a9ff109..2ab1ea0c8ff8e 100644
--- a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
@@ -619,10 +619,8 @@ const CIRGenFunctionInfo &CIRGenTypes::arrangeGlobalDeclaration(GlobalDecl gd) {
   const auto *fd = cast<FunctionDecl>(gd.getDecl());
 
   if (isa<CXXConstructorDecl>(gd.getDecl()) ||
-      isa<CXXDestructorDecl>(gd.getDecl())) {
-    cgm.errorNYI(SourceLocation(),
-                 "arrangeGlobalDeclaration for C++ constructor or destructor");
-  }
+      isa<CXXDestructorDecl>(gd.getDecl()))
+    return arrangeCXXStructorDeclaration(gd);
 
   return arrangeFunctionDeclaration(fd);
 }
diff --git a/clang/lib/CIR/CodeGen/CIRGenVTables.cpp b/clang/lib/CIR/CodeGen/CIRGenVTables.cpp
index 84f59773757b5..36bab625c4dd2 100644
--- a/clang/lib/CIR/CodeGen/CIRGenVTables.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenVTables.cpp
@@ -120,12 +120,6 @@ mlir::Attribute CIRGenVTables::getVTableComponent(
   assert(!cir::MissingFeatures::vtableRelativeLayout());
 
   switch (component.getKind()) {
-  case VTableComponent::CK_CompleteDtorPointer:
-    cgm.errorNYI("getVTableComponent: CompleteDtorPointer");
-    return mlir::Attribute();
-  case VTableComponent::CK_DeletingDtorPointer:
-    cgm.errorNYI("getVTableComponent: DeletingDtorPointer");
-    return mlir::Attribute();
   case VTableComponent::CK_UnusedFunctionPointer:
     cgm.errorNYI("getVTableComponent: UnusedFunctionPointer");
     return mlir::Attribute();
@@ -148,7 +142,9 @@ mlir::Attribute CIRGenVTables::getVTableComponent(
            "expected GlobalViewAttr or ConstPtrAttr");
     return rtti;
 
-  case VTableComponent::CK_FunctionPointer: {
+  case VTableComponent::CK_FunctionPointer:
+  case VTableComponent::CK_CompleteDtorPointer:
+  case VTableComponent::CK_DeletingDtorPointer: {
     GlobalDecl gd = component.getGlobalDecl();
 
     assert(!cir::MissingFeatures::cudaSupport());
diff --git a/clang/test/CIR/CodeGen/virtual-destructor-calls.cpp b/clang/test/CIR/CodeGen/virtual-destructor-calls.cpp
new file mode 100644
index 0000000000000..08a6b21ca91d3
--- /dev/null
+++ b/clang/test/CIR/CodeGen/virtual-destructor-calls.cpp
@@ -0,0 +1,129 @@
+// RUN: %clang_cc1 -triple aarch64-none-linux-android21 -std=c++20 -mconstructor-aliases -O0 -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: %clang_cc1 -triple aarch64-none-linux-android21 -std=c++20 -mconstructor-aliases -O0 -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple aarch64-none-linux-android21 -std=c++20 -mconstructor-aliases -O0 -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+
+// TODO(cir): Try to emit base destructor as an alias at O1 or higher.
+
+// FIXME: LLVM IR dialect does not yet support function ptr globals, which precludes
+// a lot of the proper semantics for properly representing alias functions in LLVM
+// (see the note on LLVM_O1 below).
+
+struct Member {
+  ~Member();
+};
+
+struct A {
+  virtual ~A();
+};
+
+struct B : A {
+  Member m;
+  virtual ~B();
+};
+
+B::~B() { }
+
+// Aliases are inserted before the function definitions in LLVM IR
+// FIXME: These should have unnamed_addr set.
+// LLVM: @_ZN1BD1Ev = alias void (ptr), ptr @_ZN1BD2Ev
+// LLVM: @_ZN1CD1Ev = alias void (ptr), ptr @_ZN1CD2Ev
+
+// OGCG: @_ZN1BD1Ev = unnamed_addr alias void (ptr), ptr @_ZN1BD2Ev
+// OGCG: @_ZN1CD1Ev = unnamed_addr alias void (ptr), ptr @_ZN1CD2Ev
+
+
+// Base (D2) dtor for B: calls A's base dtor.
+
+// CIR: cir.func{{.*}} @_ZN1BD2Ev
+// CIR:   cir.call @_ZN6MemberD1Ev
+// CIR:   cir.call @_ZN1AD2Ev
+
+// LLVM: define{{.*}} void @_ZN1BD2Ev
+// LLVM:   call void @_ZN6MemberD1Ev
+// LLVM:   call void @_ZN1AD2Ev
+
+// OGCG: define{{.*}} @_ZN1BD2Ev
+// OGCG:   call void @_ZN6MemberD1Ev
+// OGCG:   call void @_ZN1AD2Ev
+
+// Complete (D1) dtor for B: just an alias because there are no virtual bases.
+
+// CIR: cir.func{{.*}} @_ZN1BD1Ev(!cir.ptr<!rec_B>) alias(@_ZN1BD2Ev)
+// This is defined above for LLVM and OGCG.
+
+// Deleting (D0) dtor for B: defers to the complete dtor but also calls operator delete.
+
+// CIR: cir.func{{.*}} @_ZN1BD0Ev
+// CIR:   cir.call @_ZN1BD1Ev(%[[THIS:.*]]) nothrow : (!cir.ptr<!rec_B>) -> ()
+// CIR:   %[[THIS_VOID:.*]] = cir.cast bitcast %[[THIS]] : !cir.ptr<!rec_B> -> !cir.ptr<!void>
+// CIR:   %[[SIZE:.*]] = cir.const #cir.int<16>
+// CIR:   cir.call @_ZdlPvm(%[[THIS_VOID]], %[[SIZE]])
+
+// LLVM: define{{.*}} void @_ZN1BD0Ev
+// LLVM:   call void @_ZN1BD1Ev(ptr %[[THIS:.*]])
+// LLVM:   call void @_ZdlPvm(ptr %[[THIS]], i64 16)
+
+// OGCG: define{{.*}} @_ZN1BD0Ev
+// OGCG:   call void @_ZN1BD1Ev(ptr{{.*}} %[[THIS:.*]])
+// OGCG:   call void @_ZdlPvm(ptr{{.*}} %[[THIS]], i64{{.*}} 16)
+
+struct C : B {
+  ~C();
+};
+
+C::~C() { }
+
+// Base (D2) dtor for C: calls B's base dtor.
+
+// CIR: cir.func{{.*}} @_ZN1CD2Ev
+// CIR:   %[[B:.*]] = cir.base_class_addr %[[THIS:.*]] : !cir.ptr<!rec_C> nonnull [0] -> !cir.ptr<!rec_B>
+// CIR:   cir.call @_ZN1BD2Ev(%[[B]])
+
+// LLVM: define{{.*}} void @_ZN1CD2Ev
+// LLVM:   call void @_ZN1BD2Ev
+
+// OGCG: define{{.*}} @_ZN1CD2Ev
+// OGCG:   call void @_ZN1BD2Ev
+
+// Complete (D1) dtor for C: just an alias because there are no virtual bases.
+
+// CIR: cir.func{{.*}} @_ZN1CD1Ev(!cir.ptr<!rec_C>) alias(@_ZN1CD2Ev)
+// This is defined above for LLVM and OGCG.
+
+
+// Deleting (D0) dtor for C: defers to the complete dtor but also calls operator delete.
+
+// CIR: cir.func{{.*}} @_ZN1CD0Ev
+// CIR:   cir.call @_ZN1CD1Ev(%[[THIS:.*]]) nothrow : (!cir.ptr<!rec_C>) -> ()
+// CIR:   %[[THIS_VOID:.*]] = cir.cast bitcast %[[THIS]] : !cir.ptr<!rec_C> -> !cir.ptr<!void>
+// CIR:   %[[SIZE:.*]] = cir.const #cir.int<16>
+// CIR:   cir.call @_ZdlPvm(%[[THIS_VOID]], %[[SIZE]])
+
+// LLVM: define{{.*}} void @_ZN1CD0Ev
+// LLVM:   call void @_ZN1CD1Ev(ptr %[[THIS:.*]])
+// LLVM:   call void @_ZdlPvm(ptr %[[THIS]], i64 16)
+
+// OGCG: define{{.*}} @_ZN1CD0Ev
+// OGCG:   call void @_ZN1CD1Ev(ptr{{.*}} %[[THIS:.*]])
+// OGCG:   call void @_ZdlPvm(ptr{{.*}} %[[THIS]], i64{{.*}} 16)
+
+namespace PR12798 {
+  // A qualified call to a base class destructor should not undergo virtual
+  // dispatch. Template instantiation used to lose the qualifier.
+  struct A { virtual ~A(); };
+  template<typename T> void f(T *p) { p->A::~A(); }
+
+  // CIR: cir.func{{.*}} @_ZN7PR127981fINS_1AEEEvPT_
+  // CIR:   cir.call @_ZN7PR127981AD1Ev
+
+  // LLVM: define{{.*}} @_ZN7PR127981fINS_1AEEEvPT_
+  // LLVM:   call void @_ZN7PR127981AD1Ev
+
+  // OGCG: define{{.*}} @_ZN7PR127981fINS_1AEEEvPT_
+  // OGCG:   call void @_ZN7PR127981AD1Ev
+
+  template void f(A*);
+}

@llvmbot
Copy link
Member

llvmbot commented Oct 9, 2025

@llvm/pr-subscribers-clangir

Author: Andy Kaylor (andykaylor)

Changes

This adds support for calling virtual destructors.


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

9 Files Affected:

  • (modified) clang/lib/CIR/CodeGen/Address.h (+7)
  • (modified) clang/lib/CIR/CodeGen/CIRGenCXXABI.h (+9)
  • (modified) clang/lib/CIR/CodeGen/CIRGenClass.cpp (+32-1)
  • (modified) clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp (+33-10)
  • (modified) clang/lib/CIR/CodeGen/CIRGenFunction.cpp (+7-1)
  • (modified) clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp (+30-1)
  • (modified) clang/lib/CIR/CodeGen/CIRGenTypes.cpp (+2-4)
  • (modified) clang/lib/CIR/CodeGen/CIRGenVTables.cpp (+3-7)
  • (added) clang/test/CIR/CodeGen/virtual-destructor-calls.cpp (+129)
diff --git a/clang/lib/CIR/CodeGen/Address.h b/clang/lib/CIR/CodeGen/Address.h
index fb74aa0f3bb00..683ff396eb5cb 100644
--- a/clang/lib/CIR/CodeGen/Address.h
+++ b/clang/lib/CIR/CodeGen/Address.h
@@ -90,6 +90,13 @@ class Address {
     return getPointer();
   }
 
+  /// Return the pointer contained in this class after authenticating it and
+  /// adding offset to it if necessary.
+  mlir::Value emitRawPointer() const {
+    // TODO(cir): update this class with latest traditional LLVM codegen bits
+    return getBasePointer();
+  }
+
   mlir::Type getType() const {
     assert(mlir::cast<cir::PointerType>(
                pointerAndKnownNonNull.getPointer().getType())
diff --git a/clang/lib/CIR/CodeGen/CIRGenCXXABI.h b/clang/lib/CIR/CodeGen/CIRGenCXXABI.h
index 06f41cd8fcfdb..6d3741c417351 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCXXABI.h
+++ b/clang/lib/CIR/CodeGen/CIRGenCXXABI.h
@@ -191,6 +191,15 @@ class CIRGenCXXABI {
   virtual void emitVTableDefinitions(CIRGenVTables &cgvt,
                                      const CXXRecordDecl *rd) = 0;
 
+  using DeleteOrMemberCallExpr =
+      llvm::PointerUnion<const CXXDeleteExpr *, const CXXMemberCallExpr *>;
+
+  virtual mlir::Value emitVirtualDestructorCall(CIRGenFunction &cgf,
+                                                const CXXDestructorDecl *dtor,
+                                                CXXDtorType dtorType,
+                                                Address thisAddr,
+                                                DeleteOrMemberCallExpr e) = 0;
+
   /// Emit any tables needed to implement virtual inheritance.  For Itanium,
   /// this emits virtual table tables.
   virtual void emitVirtualInheritanceTables(const CXXRecordDecl *rd) = 0;
diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index 485b2c86cbc58..dd357ce69f1b3 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -895,6 +895,26 @@ void CIRGenFunction::destroyCXXObject(CIRGenFunction &cgf, Address addr,
 }
 
 namespace {
+mlir::Value loadThisForDtorDelete(CIRGenFunction &cgf,
+                                  const CXXDestructorDecl *dd) {
+  if (Expr *thisArg = dd->getOperatorDeleteThisArg())
+    return cgf.emitScalarExpr(thisArg);
+  return cgf.loadCXXThis();
+}
+
+/// Call the operator delete associated with the current destructor.
+struct CallDtorDelete final : EHScopeStack::Cleanup {
+  CallDtorDelete() {}
+
+  void emit(CIRGenFunction &cgf) override {
+    const CXXDestructorDecl *dtor = cast<CXXDestructorDecl>(cgf.curFuncDecl);
+    const CXXRecordDecl *classDecl = dtor->getParent();
+    cgf.emitDeleteCall(dtor->getOperatorDelete(),
+                       loadThisForDtorDelete(cgf, dtor),
+                       cgf.getContext().getCanonicalTagType(classDecl));
+  }
+};
+
 class DestroyField final : public EHScopeStack::Cleanup {
   const FieldDecl *field;
   CIRGenFunction::Destroyer *destroyer;
@@ -932,7 +952,18 @@ void CIRGenFunction::enterDtorCleanups(const CXXDestructorDecl *dd,
   // The deleting-destructor phase just needs to call the appropriate
   // operator delete that Sema picked up.
   if (dtorType == Dtor_Deleting) {
-    cgm.errorNYI(dd->getSourceRange(), "deleting destructor cleanups");
+    assert(dd->getOperatorDelete() &&
+           "operator delete missing - EnterDtorCleanups");
+    if (cxxStructorImplicitParamValue) {
+      cgm.errorNYI(dd->getSourceRange(), "deleting destructor with vtt");
+    } else {
+      if (dd->getOperatorDelete()->isDestroyingOperatorDelete()) {
+        cgm.errorNYI(dd->getSourceRange(),
+                     "deleting destructor with destroying operator delete");
+      } else {
+        ehStack.pushCleanup<CallDtorDelete>(NormalAndEHCleanup);
+      }
+    }
     return;
   }
 
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
index 97c0944fca336..2d11cee38b4c6 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -130,13 +130,11 @@ RValue CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
   const CXXMethodDecl *calleeDecl =
       devirtualizedMethod ? devirtualizedMethod : md;
   const CIRGenFunctionInfo *fInfo = nullptr;
-  if (isa<CXXDestructorDecl>(calleeDecl)) {
-    cgm.errorNYI(ce->getSourceRange(),
-                 "emitCXXMemberOrOperatorMemberCallExpr: destructor call");
-    return RValue::get(nullptr);
-  }
-
-  fInfo = &cgm.getTypes().arrangeCXXMethodDeclaration(calleeDecl);
+  if (const auto *dtor = dyn_cast<CXXDestructorDecl>(calleeDecl))
+    fInfo = &cgm.getTypes().arrangeCXXStructorDeclaration(
+        GlobalDecl(dtor, Dtor_Complete));
+  else
+    fInfo = &cgm.getTypes().arrangeCXXMethodDeclaration(calleeDecl);
 
   cir::FuncType ty = cgm.getTypes().getFunctionType(*fInfo);
 
@@ -151,9 +149,34 @@ RValue CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
   // because then we know what the type is.
   bool useVirtualCall = canUseVirtualCall && !devirtualizedMethod;
 
-  if (isa<CXXDestructorDecl>(calleeDecl)) {
-    cgm.errorNYI(ce->getSourceRange(),
-                 "emitCXXMemberOrOperatorMemberCallExpr: destructor call");
+  if (const auto *dtor = dyn_cast<CXXDestructorDecl>(calleeDecl)) {
+    assert(ce->arg_begin() == ce->arg_end() &&
+           "Destructor shouldn't have explicit parameters");
+    assert(returnValue.isNull() && "Destructor shouldn't have return value");
+    if (useVirtualCall) {
+      cgm.getCXXABI().emitVirtualDestructorCall(*this, dtor, Dtor_Complete,
+                                                thisPtr.getAddress(),
+                                                cast<CXXMemberCallExpr>(ce));
+    } else {
+      GlobalDecl globalDecl(dtor, Dtor_Complete);
+      CIRGenCallee callee;
+      assert(!cir::MissingFeatures::appleKext());
+      if (!devirtualizedMethod) {
+        callee = CIRGenCallee::forDirect(
+            cgm.getAddrOfCXXStructor(globalDecl, fInfo, ty), globalDecl);
+      } else {
+        cgm.errorNYI(ce->getSourceRange(), "devirtualized destructor call");
+        return RValue::get(nullptr);
+      }
+
+      QualType thisTy =
+          isArrow ? base->getType()->getPointeeType() : base->getType();
+      // CIRGen does not pass CallOrInvoke here (different from OG LLVM codegen)
+      // because in practice it always null even in OG.
+      emitCXXDestructorCall(globalDecl, callee, thisPtr.getPointer(), thisTy,
+                            /*ImplicitParam=*/nullptr,
+                            /*ImplicitParamTy=*/QualType(), ce);
+    }
     return RValue::get(nullptr);
   }
 
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
index 7a774e0441bbb..f7c2fb3a68bbb 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
@@ -678,7 +678,13 @@ void CIRGenFunction::emitDestructorBody(FunctionArgList &args) {
   // possible to delegate the destructor body to the complete
   // destructor.  Do so.
   if (dtorType == Dtor_Deleting) {
-    cgm.errorNYI(dtor->getSourceRange(), "deleting destructor");
+    RunCleanupsScope dtorEpilogue(*this);
+    enterDtorCleanups(dtor, Dtor_Deleting);
+    if (haveInsertPoint()) {
+      QualType thisTy = dtor->getFunctionObjectParameterType();
+      emitCXXDestructorCall(dtor, Dtor_Complete, /*ForVirtualBase=*/false,
+                            /*Delegating=*/false, loadCXXThisAddress(), thisTy);
+    }
     return;
   }
 
diff --git a/clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp b/clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp
index 9e490c6d054a4..4be03ca5f4dff 100644
--- a/clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenItaniumCXXABI.cpp
@@ -95,7 +95,10 @@ class CIRGenItaniumCXXABI : public CIRGenCXXABI {
                                          clang::GlobalDecl gd, Address thisAddr,
                                          mlir::Type ty,
                                          SourceLocation loc) override;
-
+  mlir::Value emitVirtualDestructorCall(CIRGenFunction &cgf,
+                                        const CXXDestructorDecl *dtor,
+                                        CXXDtorType dtorType, Address thisAddr,
+                                        DeleteOrMemberCallExpr e) override;
   mlir::Value getVTableAddressPoint(BaseSubobject base,
                                     const CXXRecordDecl *vtableClass) override;
   mlir::Value getVTableAddressPointInStructorWithVTT(
@@ -465,6 +468,32 @@ void CIRGenItaniumCXXABI::emitVTableDefinitions(CIRGenVTables &cgvt,
   }
 }
 
+mlir::Value CIRGenItaniumCXXABI::emitVirtualDestructorCall(
+    CIRGenFunction &cgf, const CXXDestructorDecl *dtor, CXXDtorType dtorType,
+    Address thisAddr, DeleteOrMemberCallExpr expr) {
+  auto *callExpr = dyn_cast<const CXXMemberCallExpr *>(expr);
+  auto *delExpr = dyn_cast<const CXXDeleteExpr *>(expr);
+  assert((callExpr != nullptr) ^ (delExpr != nullptr));
+  assert(callExpr == nullptr || callExpr->arg_begin() == callExpr->arg_end());
+  assert(dtorType == Dtor_Deleting || dtorType == Dtor_Complete);
+
+  GlobalDecl globalDecl(dtor, dtorType);
+  const CIRGenFunctionInfo *fnInfo =
+      &cgm.getTypes().arrangeCXXStructorDeclaration(globalDecl);
+  const cir::FuncType &fnTy = cgm.getTypes().getFunctionType(*fnInfo);
+  auto callee = CIRGenCallee::forVirtual(callExpr, globalDecl, thisAddr, fnTy);
+
+  QualType thisTy;
+  if (callExpr)
+    thisTy = callExpr->getObjectType();
+  else
+    thisTy = delExpr->getDestroyedType();
+
+  cgf.emitCXXDestructorCall(globalDecl, callee, thisAddr.emitRawPointer(),
+                            thisTy, nullptr, QualType(), nullptr);
+  return nullptr;
+}
+
 void CIRGenItaniumCXXABI::emitVirtualInheritanceTables(
     const CXXRecordDecl *rd) {
   CIRGenVTables &vtables = cgm.getVTables();
diff --git a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
index e65896a9ff109..2ab1ea0c8ff8e 100644
--- a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
@@ -619,10 +619,8 @@ const CIRGenFunctionInfo &CIRGenTypes::arrangeGlobalDeclaration(GlobalDecl gd) {
   const auto *fd = cast<FunctionDecl>(gd.getDecl());
 
   if (isa<CXXConstructorDecl>(gd.getDecl()) ||
-      isa<CXXDestructorDecl>(gd.getDecl())) {
-    cgm.errorNYI(SourceLocation(),
-                 "arrangeGlobalDeclaration for C++ constructor or destructor");
-  }
+      isa<CXXDestructorDecl>(gd.getDecl()))
+    return arrangeCXXStructorDeclaration(gd);
 
   return arrangeFunctionDeclaration(fd);
 }
diff --git a/clang/lib/CIR/CodeGen/CIRGenVTables.cpp b/clang/lib/CIR/CodeGen/CIRGenVTables.cpp
index 84f59773757b5..36bab625c4dd2 100644
--- a/clang/lib/CIR/CodeGen/CIRGenVTables.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenVTables.cpp
@@ -120,12 +120,6 @@ mlir::Attribute CIRGenVTables::getVTableComponent(
   assert(!cir::MissingFeatures::vtableRelativeLayout());
 
   switch (component.getKind()) {
-  case VTableComponent::CK_CompleteDtorPointer:
-    cgm.errorNYI("getVTableComponent: CompleteDtorPointer");
-    return mlir::Attribute();
-  case VTableComponent::CK_DeletingDtorPointer:
-    cgm.errorNYI("getVTableComponent: DeletingDtorPointer");
-    return mlir::Attribute();
   case VTableComponent::CK_UnusedFunctionPointer:
     cgm.errorNYI("getVTableComponent: UnusedFunctionPointer");
     return mlir::Attribute();
@@ -148,7 +142,9 @@ mlir::Attribute CIRGenVTables::getVTableComponent(
            "expected GlobalViewAttr or ConstPtrAttr");
     return rtti;
 
-  case VTableComponent::CK_FunctionPointer: {
+  case VTableComponent::CK_FunctionPointer:
+  case VTableComponent::CK_CompleteDtorPointer:
+  case VTableComponent::CK_DeletingDtorPointer: {
     GlobalDecl gd = component.getGlobalDecl();
 
     assert(!cir::MissingFeatures::cudaSupport());
diff --git a/clang/test/CIR/CodeGen/virtual-destructor-calls.cpp b/clang/test/CIR/CodeGen/virtual-destructor-calls.cpp
new file mode 100644
index 0000000000000..08a6b21ca91d3
--- /dev/null
+++ b/clang/test/CIR/CodeGen/virtual-destructor-calls.cpp
@@ -0,0 +1,129 @@
+// RUN: %clang_cc1 -triple aarch64-none-linux-android21 -std=c++20 -mconstructor-aliases -O0 -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: %clang_cc1 -triple aarch64-none-linux-android21 -std=c++20 -mconstructor-aliases -O0 -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple aarch64-none-linux-android21 -std=c++20 -mconstructor-aliases -O0 -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+
+// TODO(cir): Try to emit base destructor as an alias at O1 or higher.
+
+// FIXME: LLVM IR dialect does not yet support function ptr globals, which precludes
+// a lot of the proper semantics for properly representing alias functions in LLVM
+// (see the note on LLVM_O1 below).
+
+struct Member {
+  ~Member();
+};
+
+struct A {
+  virtual ~A();
+};
+
+struct B : A {
+  Member m;
+  virtual ~B();
+};
+
+B::~B() { }
+
+// Aliases are inserted before the function definitions in LLVM IR
+// FIXME: These should have unnamed_addr set.
+// LLVM: @_ZN1BD1Ev = alias void (ptr), ptr @_ZN1BD2Ev
+// LLVM: @_ZN1CD1Ev = alias void (ptr), ptr @_ZN1CD2Ev
+
+// OGCG: @_ZN1BD1Ev = unnamed_addr alias void (ptr), ptr @_ZN1BD2Ev
+// OGCG: @_ZN1CD1Ev = unnamed_addr alias void (ptr), ptr @_ZN1CD2Ev
+
+
+// Base (D2) dtor for B: calls A's base dtor.
+
+// CIR: cir.func{{.*}} @_ZN1BD2Ev
+// CIR:   cir.call @_ZN6MemberD1Ev
+// CIR:   cir.call @_ZN1AD2Ev
+
+// LLVM: define{{.*}} void @_ZN1BD2Ev
+// LLVM:   call void @_ZN6MemberD1Ev
+// LLVM:   call void @_ZN1AD2Ev
+
+// OGCG: define{{.*}} @_ZN1BD2Ev
+// OGCG:   call void @_ZN6MemberD1Ev
+// OGCG:   call void @_ZN1AD2Ev
+
+// Complete (D1) dtor for B: just an alias because there are no virtual bases.
+
+// CIR: cir.func{{.*}} @_ZN1BD1Ev(!cir.ptr<!rec_B>) alias(@_ZN1BD2Ev)
+// This is defined above for LLVM and OGCG.
+
+// Deleting (D0) dtor for B: defers to the complete dtor but also calls operator delete.
+
+// CIR: cir.func{{.*}} @_ZN1BD0Ev
+// CIR:   cir.call @_ZN1BD1Ev(%[[THIS:.*]]) nothrow : (!cir.ptr<!rec_B>) -> ()
+// CIR:   %[[THIS_VOID:.*]] = cir.cast bitcast %[[THIS]] : !cir.ptr<!rec_B> -> !cir.ptr<!void>
+// CIR:   %[[SIZE:.*]] = cir.const #cir.int<16>
+// CIR:   cir.call @_ZdlPvm(%[[THIS_VOID]], %[[SIZE]])
+
+// LLVM: define{{.*}} void @_ZN1BD0Ev
+// LLVM:   call void @_ZN1BD1Ev(ptr %[[THIS:.*]])
+// LLVM:   call void @_ZdlPvm(ptr %[[THIS]], i64 16)
+
+// OGCG: define{{.*}} @_ZN1BD0Ev
+// OGCG:   call void @_ZN1BD1Ev(ptr{{.*}} %[[THIS:.*]])
+// OGCG:   call void @_ZdlPvm(ptr{{.*}} %[[THIS]], i64{{.*}} 16)
+
+struct C : B {
+  ~C();
+};
+
+C::~C() { }
+
+// Base (D2) dtor for C: calls B's base dtor.
+
+// CIR: cir.func{{.*}} @_ZN1CD2Ev
+// CIR:   %[[B:.*]] = cir.base_class_addr %[[THIS:.*]] : !cir.ptr<!rec_C> nonnull [0] -> !cir.ptr<!rec_B>
+// CIR:   cir.call @_ZN1BD2Ev(%[[B]])
+
+// LLVM: define{{.*}} void @_ZN1CD2Ev
+// LLVM:   call void @_ZN1BD2Ev
+
+// OGCG: define{{.*}} @_ZN1CD2Ev
+// OGCG:   call void @_ZN1BD2Ev
+
+// Complete (D1) dtor for C: just an alias because there are no virtual bases.
+
+// CIR: cir.func{{.*}} @_ZN1CD1Ev(!cir.ptr<!rec_C>) alias(@_ZN1CD2Ev)
+// This is defined above for LLVM and OGCG.
+
+
+// Deleting (D0) dtor for C: defers to the complete dtor but also calls operator delete.
+
+// CIR: cir.func{{.*}} @_ZN1CD0Ev
+// CIR:   cir.call @_ZN1CD1Ev(%[[THIS:.*]]) nothrow : (!cir.ptr<!rec_C>) -> ()
+// CIR:   %[[THIS_VOID:.*]] = cir.cast bitcast %[[THIS]] : !cir.ptr<!rec_C> -> !cir.ptr<!void>
+// CIR:   %[[SIZE:.*]] = cir.const #cir.int<16>
+// CIR:   cir.call @_ZdlPvm(%[[THIS_VOID]], %[[SIZE]])
+
+// LLVM: define{{.*}} void @_ZN1CD0Ev
+// LLVM:   call void @_ZN1CD1Ev(ptr %[[THIS:.*]])
+// LLVM:   call void @_ZdlPvm(ptr %[[THIS]], i64 16)
+
+// OGCG: define{{.*}} @_ZN1CD0Ev
+// OGCG:   call void @_ZN1CD1Ev(ptr{{.*}} %[[THIS:.*]])
+// OGCG:   call void @_ZdlPvm(ptr{{.*}} %[[THIS]], i64{{.*}} 16)
+
+namespace PR12798 {
+  // A qualified call to a base class destructor should not undergo virtual
+  // dispatch. Template instantiation used to lose the qualifier.
+  struct A { virtual ~A(); };
+  template<typename T> void f(T *p) { p->A::~A(); }
+
+  // CIR: cir.func{{.*}} @_ZN7PR127981fINS_1AEEEvPT_
+  // CIR:   cir.call @_ZN7PR127981AD1Ev
+
+  // LLVM: define{{.*}} @_ZN7PR127981fINS_1AEEEvPT_
+  // LLVM:   call void @_ZN7PR127981AD1Ev
+
+  // OGCG: define{{.*}} @_ZN7PR127981fINS_1AEEEvPT_
+  // OGCG:   call void @_ZN7PR127981AD1Ev
+
+  template void f(A*);
+}

Copy link
Member

@AmrDeveloper AmrDeveloper left a comment

Choose a reason for hiding this comment

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

LGTM, with some nits

Comment on lines 177 to 178
/*ImplicitParam=*/nullptr,
/*ImplicitParamTy=*/QualType(), ce);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/*ImplicitParam=*/nullptr,
/*ImplicitParamTy=*/QualType(), ce);
/*implicitParam=*/nullptr,
/*implicitParamTy=*/QualType(), ce);

Comment on lines 685 to 686
emitCXXDestructorCall(dtor, Dtor_Complete, /*ForVirtualBase=*/false,
/*Delegating=*/false, loadCXXThisAddress(), thisTy);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
emitCXXDestructorCall(dtor, Dtor_Complete, /*ForVirtualBase=*/false,
/*Delegating=*/false, loadCXXThisAddress(), thisTy);
emitCXXDestructorCall(dtor, Dtor_Complete, /*forVirtualBase=*/false,
/*delegating=*/false, loadCXXThisAddress(), thisTy);

Comment on lines 486 to 490
QualType thisTy;
if (callExpr)
thisTy = callExpr->getObjectType();
else
thisTy = delExpr->getDestroyedType();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
QualType thisTy;
if (callExpr)
thisTy = callExpr->getObjectType();
else
thisTy = delExpr->getDestroyedType();
QualType thisTy = callExpr ? callExpr->getObjectType() : delExpr->getDestroyedType();

Copy link
Member

@bcardosolopes bcardosolopes left a comment

Choose a reason for hiding this comment

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

LGTM after nit

/// Return the pointer contained in this class after authenticating it and
/// adding offset to it if necessary.
mlir::Value emitRawPointer() const {
// TODO(cir): update this class with latest traditional LLVM codegen bits
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth changing the comment to a more accurate assert on missing feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For sure. We even already have the MissingFeature I needed.

@andykaylor andykaylor merged commit d81e8c0 into llvm:main Oct 13, 2025
10 checks passed
@andykaylor andykaylor deleted the cir-virtual-dtor branch October 13, 2025 17:38
akadutta pushed a commit to akadutta/llvm-project that referenced this pull request Oct 14, 2025
This adds support for calling virtual destructors.
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.

4 participants