[clang] [CIR] Implement devirtualized member function calls (PR #195106)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Apr 30 08:18:43 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clangir
@llvm/pr-subscribers-clang
Author: AbdallahRashed (AbdallahRashed)
<details>
<summary>Changes</summary>
Implement the devirtualization logic in
emitCXXMemberOrOperatorMemberCallExpr that was previously guarded by MissingFeatures::devirtualizeMemberFunction(). This enables the compiler to emit direct calls instead of virtual dispatch when the dynamic type can be statically resolved (e.g., final classes, local variables of known type).
This fixes the 'devirtualized destructor call' errorNYI in CIRGenExprCXX.cpp, which is point 5 of #<!-- -->192330.
The implementation follows OGCG: use getDevirtualizedMethod() to check if devirtualization is possible, then getCorrespondingMethodInClass() to find the target method. For destructors, use getAddrOfFunction() instead of getAddrOfCXXStructor() since the devirtualized callee may differ from the original.
Fixes point 5 of #<!-- -->192330.
Supported by : Claude Opus4.6 High
---
Full diff: https://github.com/llvm/llvm-project/pull/195106.diff
6 Files Affected:
- (modified) clang/include/clang/CIR/MissingFeatures.h (-1)
- (modified) clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp (+36-3)
- (added) clang/test/CIR/CodeGen/devirtualize.cpp (+45)
- (modified) clang/test/CIR/CodeGen/vbase.cpp (+4-18)
- (modified) clang/test/CIR/CodeGen/virtual-fn-calls-eh.cpp (+3-15)
- (modified) clang/test/CIR/CodeGen/virtual-function-calls.cpp (+2-10)
``````````diff
diff --git a/clang/include/clang/CIR/MissingFeatures.h b/clang/include/clang/CIR/MissingFeatures.h
index 645fae3e4404d..c85fcf6556f7b 100644
--- a/clang/include/clang/CIR/MissingFeatures.h
+++ b/clang/include/clang/CIR/MissingFeatures.h
@@ -248,7 +248,6 @@ struct MissingFeatures {
static bool deferredCXXGlobalInit() { return false; }
static bool deleteArray() { return false; }
static bool devirtualizeDestructor() { return false; }
- static bool devirtualizeMemberFunction() { return false; }
static bool dtorCleanups() { return false; }
static bool ehCleanupScope() { return false; }
static bool ehScopeFilter() { return false; }
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
index 39a2068a7073f..2cff0a76401ef 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -123,6 +123,13 @@ CIRGenFunction::emitCXXMemberPointerCallExpr(const CXXMemberCallExpr *ce,
callee, returnValue, argsList, nullptr, loc);
}
+static const CXXRecordDecl *getCXXRecord(const Expr *e) {
+ QualType t = e->getType();
+ if (const PointerType *pty = t->getAs<PointerType>())
+ t = pty->getPointeeType();
+ return t->getAsCXXRecordDecl();
+}
+
RValue CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
const CallExpr *ce, const CXXMethodDecl *md, ReturnValueSlot returnValue,
bool hasQualifier, NestedNameSpecifier qualifier, bool isArrow,
@@ -132,7 +139,33 @@ RValue CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
// Compute the object pointer.
bool canUseVirtualCall = md->isVirtual() && !hasQualifier;
const CXXMethodDecl *devirtualizedMethod = nullptr;
- assert(!cir::MissingFeatures::devirtualizeMemberFunction());
+ if (canUseVirtualCall &&
+ md->getDevirtualizedMethod(base, getLangOpts().AppleKext)) {
+ const CXXRecordDecl *bestDynamicDecl = base->getBestDynamicClassType();
+ devirtualizedMethod = md->getCorrespondingMethodInClass(bestDynamicDecl);
+ assert(devirtualizedMethod);
+ const CXXRecordDecl *devirtualizedClass = devirtualizedMethod->getParent();
+ const Expr *inner = base->IgnoreParenBaseCasts();
+ if (devirtualizedMethod->getReturnType().getCanonicalType() !=
+ md->getReturnType().getCanonicalType())
+ // If the return types are not the same, this might be a case where more
+ // code needs to run to compensate for it. For example, the derived
+ // method might return a type that inherits from the return type of MD
+ // and has a prefix.
+ // For now we just avoid devirtualizing these covariant cases.
+ devirtualizedMethod = nullptr;
+ else if (getCXXRecord(inner) == devirtualizedClass)
+ // If the class of the Inner expression is where the dynamic method
+ // is defined, build the this pointer from it.
+ base = inner;
+ else if (getCXXRecord(base) != devirtualizedClass) {
+ // If the method is defined in a class that is not the best dynamic
+ // one or the one of the full expression, we would have to build
+ // a derived-to-base cast to compute the correct this pointer, but
+ // we don't have support for that yet, so do a virtual call.
+ devirtualizedMethod = nullptr;
+ }
+ }
// Note on trivial assignment
// --------------------------
@@ -214,8 +247,8 @@ RValue CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
callee = CIRGenCallee::forDirect(
cgm.getAddrOfCXXStructor(globalDecl, fInfo, ty), globalDecl);
} else {
- cgm.errorNYI(ce->getSourceRange(), "devirtualized destructor call");
- return RValue::get(nullptr);
+ callee = CIRGenCallee::forDirect(cgm.getAddrOfFunction(globalDecl, ty),
+ globalDecl);
}
QualType thisTy =
diff --git a/clang/test/CIR/CodeGen/devirtualize.cpp b/clang/test/CIR/CodeGen/devirtualize.cpp
new file mode 100644
index 0000000000000..6ce6f95db7478
--- /dev/null
+++ b/clang/test/CIR/CodeGen/devirtualize.cpp
@@ -0,0 +1,45 @@
+// RUN: %clang_cc1 -triple aarch64-none-linux-android21 -std=c++20 -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 -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 -O0 -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+
+struct Base {
+ virtual ~Base();
+ virtual int foo();
+};
+
+struct Final final : Base {
+ ~Final();
+ int foo() override;
+};
+
+// Devirtualized destructor call: calling a virtual destructor on a type
+// that the compiler can statically resolve (e.g. a final class).
+void test_devirtualized_dtor(Final *f) {
+ f->~Final();
+}
+
+// CIR-LABEL: @_Z23test_devirtualized_dtorP5Final
+// CIR: cir.call @_ZN5FinalD1Ev({{.*}}) nothrow
+
+// LLVM-LABEL: @_Z23test_devirtualized_dtorP5Final
+// LLVM: call void @_ZN5FinalD1Ev(
+
+// OGCG-LABEL: @_Z23test_devirtualized_dtorP5Final
+// OGCG: call void @_ZN5FinalD1Ev(
+
+// Devirtualized method call: calling a virtual method on a final class.
+int test_devirtualized_method(Final *f) {
+ return f->foo();
+}
+
+// CIR-LABEL: @_Z25test_devirtualized_methodP5Final
+// CIR: cir.call @_ZN5Final3fooEv(
+
+// LLVM-LABEL: @_Z25test_devirtualized_methodP5Final
+// LLVM: call noundef i32 @_ZN5Final3fooEv(
+
+// OGCG-LABEL: @_Z25test_devirtualized_methodP5Final
+// OGCG: call noundef i32 @_ZN5Final3fooEv(
diff --git a/clang/test/CIR/CodeGen/vbase.cpp b/clang/test/CIR/CodeGen/vbase.cpp
index 3ae8d192c798a..6a6e1eeb2fdb0 100644
--- a/clang/test/CIR/CodeGen/vbase.cpp
+++ b/clang/test/CIR/CodeGen/vbase.cpp
@@ -70,22 +70,14 @@ void ppp() { B b; }
// CIR: %[[ADJ_THIS_I8:.+]] = cir.ptr_stride %[[D_I8]], %[[OFFSET]] : (!cir.ptr<!u8i>, !s64i) -> !cir.ptr<!u8i>
// CIR: %[[ADJ_THIS_D:.+]] = cir.cast bitcast %[[ADJ_THIS_I8]] : !cir.ptr<!u8i> -> !cir.ptr<!rec_Derived>
// CIR: %[[BASE_THIS:.+]] = cir.cast bitcast %[[ADJ_THIS_D]] : !cir.ptr<!rec_Derived> -> !cir.ptr<!rec_Base>
-// CIR: %[[BASE_VPTR_PTR:.+]] = cir.vtable.get_vptr %[[BASE_THIS]] : !cir.ptr<!rec_Base> -> !cir.ptr<!cir.vptr>
-// CIR: %[[BASE_VPTR:.+]] = cir.load {{.*}} %[[BASE_VPTR_PTR]] : !cir.ptr<!cir.vptr>, !cir.vptr
-// CIR: %[[SLOT_PTR:.+]] = cir.vtable.get_virtual_fn_addr %[[BASE_VPTR]][0] : !cir.vptr -> !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_Base>)>>>
-// CIR: %[[FN:.+]] = cir.load {{.*}} %[[SLOT_PTR]] : !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_Base>)>>>, !cir.ptr<!cir.func<(!cir.ptr<!rec_Base>)>>
-// CIR: cir.call %[[FN]](%[[BASE_THIS]]) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_Base>)>>, !cir.ptr<!rec_Base> {{.*}}) -> ()
+// CIR: cir.call @_ZN4Base1fEv(%[[BASE_THIS]]) : (!cir.ptr<!rec_Base> {{.*}}) -> ()
// CIR: cir.return
// CIR: cir.func {{.*}}@_Z1gv()
// CIR: %[[DF:.+]] = cir.alloca !rec_DerivedFinal, !cir.ptr<!rec_DerivedFinal>, ["df", init]
// CIR: cir.call @_ZN12DerivedFinalC1Ev(%[[DF]]) nothrow : (!cir.ptr<!rec_DerivedFinal> {{.*}}) -> ()
// CIR: %[[BASE_THIS_2:.+]] = cir.base_class_addr %[[DF]] : !cir.ptr<!rec_DerivedFinal> nonnull [0] -> !cir.ptr<!rec_Base>
-// CIR: %[[BASE_VPTR_PTR_2:.+]] = cir.vtable.get_vptr %[[BASE_THIS_2]] : !cir.ptr<!rec_Base> -> !cir.ptr<!cir.vptr>
-// CIR: %[[BASE_VPTR_2:.+]] = cir.load {{.*}} %[[BASE_VPTR_PTR_2]] : !cir.ptr<!cir.vptr>, !cir.vptr
-// CIR: %[[SLOT_PTR_2:.+]] = cir.vtable.get_virtual_fn_addr %[[BASE_VPTR_2]][0] : !cir.vptr -> !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_Base>)>>>
-// CIR: %[[FN_2:.+]] = cir.load {{.*}} %[[SLOT_PTR_2]] : !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_Base>)>>>, !cir.ptr<!cir.func<(!cir.ptr<!rec_Base>)>>
-// CIR: cir.call %[[FN_2]](%[[BASE_THIS_2]]) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_Base>)>>, !cir.ptr<!rec_Base> {{.*}}) -> ()
+// CIR: cir.call @_ZN4Base1fEv(%[[BASE_THIS_2]]) : (!cir.ptr<!rec_Base> {{.*}}) -> ()
// CIR: cir.return
// LLVM: define {{.*}}void @_Z1fv(){{.*}}
@@ -95,19 +87,13 @@ void ppp() { B b; }
// LLVM: %[[NEG32_PTR:.+]] = getelementptr i8, ptr %[[VPTR_ADDR]], i64 -32
// LLVM: %[[OFF:.+]] = load i64, ptr %[[NEG32_PTR]]
// LLVM: %[[ADJ_THIS:.+]] = getelementptr i8, ptr %[[D]], i64 %[[OFF]]
-// LLVM: %[[VFN_TAB:.+]] = load ptr, ptr %[[ADJ_THIS]]
-// LLVM: %[[SLOT0:.+]] = getelementptr inbounds ptr, ptr %[[VFN_TAB]], i32 0
-// LLVM: %[[VFN:.+]] = load ptr, ptr %[[SLOT0]]
-// LLVM: call void %[[VFN]](ptr {{.*}}%[[ADJ_THIS]])
+// LLVM: call void @_ZN4Base1fEv(ptr {{.*}}%[[ADJ_THIS]])
// LLVM: ret void
// LLVM: define {{.*}}void @_Z1gv(){{.*}}
// LLVM: %[[DF:.+]] = alloca {{.*}}
// LLVM: call void @_ZN12DerivedFinalC1Ev(ptr {{.*}} %[[DF]])
-// LLVM: %[[VPTR2:.+]] = load ptr, ptr %[[DF]]
-// LLVM: %[[SLOT0_2:.+]] = getelementptr inbounds ptr, ptr %[[VPTR2]], i32 0
-// LLVM: %[[VFN2:.+]] = load ptr, ptr %[[SLOT0_2]]
-// LLVM: call void %[[VFN2]](ptr {{.*}}%[[DF]])
+// LLVM: call void @_ZN4Base1fEv(ptr {{.*}}%[[DF]])
// LLVM: ret void
// OGCG: define {{.*}}void @_Z1fv()
diff --git a/clang/test/CIR/CodeGen/virtual-fn-calls-eh.cpp b/clang/test/CIR/CodeGen/virtual-fn-calls-eh.cpp
index 5ae1b5745d711..71f749d53dc58 100644
--- a/clang/test/CIR/CodeGen/virtual-fn-calls-eh.cpp
+++ b/clang/test/CIR/CodeGen/virtual-fn-calls-eh.cpp
@@ -22,11 +22,7 @@ void call_virtual_fn_in_cleanup_scope() {
// CIR: cir.call @_ZN1BC2Ev(%[[B]])
// CIR: cir.cleanup.scope {
// CIR: %[[C_LITERAL:.*]] = cir.const #cir.int<99> : !s8i
-// CIR: %[[VPTR_ADDR:.*]] = cir.vtable.get_vptr %[[B]] : !cir.ptr<!rec_B> -> !cir.ptr<!cir.vptr>
-// CIR: %[[VPTR:.*]] = cir.load{{.*}} %[[VPTR_ADDR]]
-// CIR: %[[FN_PTR_ADDR:.*]] = cir.vtable.get_virtual_fn_addr %[[VPTR]][0] : !cir.vptr -> !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>>
-// CIR: %[[FN_PTR:.*]] = cir.load{{.*}} %[[FN_PTR_ADDR:.*]] : !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>>, !cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>
-// CIR: cir.call %[[FN_PTR]](%[[B]], %[[C_LITERAL]]) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>, !cir.ptr<!rec_B> {{.*}}, !s8i {{.*}}) -> ()
+// CIR: cir.call @_ZN1B1fEc(%[[B]], %[[C_LITERAL]]) : (!cir.ptr<!rec_B> {{.*}}, !s8i {{.*}}) -> ()
// CIR: cir.yield
// CIR: } cleanup all {
// CIR: cir.call @_ZN1BD1Ev(%[[B]]) nothrow : (!cir.ptr<!rec_B> {{.*}}) -> ()
@@ -39,11 +35,7 @@ void call_virtual_fn_in_cleanup_scope() {
// CIR-FLAT: cir.br ^[[CLEANUP_SCOPE:bb[0-9]+]]
// CIR-FLAT: ^[[CLEANUP_SCOPE]]:
// CIR-FLAT: %[[C_LITERAL:.*]] = cir.const #cir.int<99> : !s8i
-// CIR-FLAT: %[[VPTR_ADDR:.*]] = cir.vtable.get_vptr %[[B]] : !cir.ptr<!rec_B> -> !cir.ptr<!cir.vptr>
-// CIR-FLAT: %[[VPTR:.*]] = cir.load{{.*}} %[[VPTR_ADDR]]
-// CIR-FLAT: %[[FN_PTR_ADDR:.*]] = cir.vtable.get_virtual_fn_addr %[[VPTR]][0] : !cir.vptr -> !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>>
-// CIR-FLAT: %[[FN_PTR:.*]] = cir.load{{.*}} %[[FN_PTR_ADDR:.*]] : !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>>, !cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>
-// CIR-FLAT: cir.try_call %[[FN_PTR]](%[[B]], %[[C_LITERAL]]) ^[[NORMAL:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]] : (!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>, !cir.ptr<!rec_B> {{.*}}, !s8i {{.*}}) -> ()
+// CIR-FLAT: cir.try_call @_ZN1B1fEc(%[[B]], %[[C_LITERAL]]) ^[[NORMAL:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]] : (!cir.ptr<!rec_B> {{.*}}, !s8i {{.*}}) -> ()
// CIR-FLAT: ^[[NORMAL]]: // pred: ^bb1
// CIR-FLAT: cir.br ^[[NORMAL_CLEANUP:bb[0-9]+]]
// CIR-FLAT: ^[[NORMAL_CLEANUP]]:
@@ -67,10 +59,7 @@ void call_virtual_fn_in_cleanup_scope() {
// LLVM: call void @_ZN1BC2Ev(ptr {{.*}} %[[B]])
// LLVM: br label %[[CLEANUP_SCOPE:.*]]
// LLVM: [[CLEANUP_SCOPE]]:
-// LLVM: %[[B_VPTR:.*]] = load ptr, ptr %[[B]]
-// LLVM: %[[FN_PTR_ADDR:.*]] = getelementptr inbounds ptr, ptr %[[B_VPTR]], i32 0
-// LLVM: %[[FN_PTR:.*]] = load ptr, ptr %[[FN_PTR_ADDR]]
-// LLVM: invoke void %[[FN_PTR]](ptr {{.*}} %[[B]], i8 {{.*}} 99)
+// LLVM: invoke void @_ZN1B1fEc(ptr {{.*}} %[[B]], i8 noundef 99)
// LLVM: to label %[[NORMAL_CONTINUE:.*]] unwind label %[[UNWIND:.*]]
// LLVM: [[NORMAL_CONTINUE]]
// LLVM: br label %[[NORMAL_CLEANUP:.*]]
@@ -95,7 +84,6 @@ void call_virtual_fn_in_cleanup_scope() {
// LLVM: [[DONE]]:
// LLVM: ret void
-// Note: OGCG devirtualizes the call. We don't do that yet in CIR.
// OGCG: define {{.*}} void @_Z32call_virtual_fn_in_cleanup_scopev()
// OGCG: %[[B:.*]] = alloca %struct.B, align 8
// OGCG: %[[EXN_SLOT:.*]] = alloca ptr
diff --git a/clang/test/CIR/CodeGen/virtual-function-calls.cpp b/clang/test/CIR/CodeGen/virtual-function-calls.cpp
index b316aa2567b65..d683bd45d7f61 100644
--- a/clang/test/CIR/CodeGen/virtual-function-calls.cpp
+++ b/clang/test/CIR/CodeGen/virtual-function-calls.cpp
@@ -95,11 +95,7 @@ void call_virtual_fn_in_cleanup_scope() {
// CIR: cir.call @_ZN1BC2Ev(%[[B]])
// CIR: cir.cleanup.scope {
// CIR: %[[C_LITERAL:.*]] = cir.const #cir.int<99> : !s8i
-// CIR: %[[VPTR_ADDR:.*]] = cir.vtable.get_vptr %[[B]] : !cir.ptr<!rec_B> -> !cir.ptr<!cir.vptr>
-// CIR: %[[VPTR:.*]] = cir.load{{.*}} %[[VPTR_ADDR]]
-// CIR: %[[FN_PTR_ADDR:.*]] = cir.vtable.get_virtual_fn_addr %[[VPTR]][0] : !cir.vptr -> !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>>
-// CIR: %[[FN_PTR:.*]] = cir.load{{.*}} %[[FN_PTR_ADDR:.*]] : !cir.ptr<!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>>, !cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>
-// CIR: cir.call %[[FN_PTR]](%[[B]], %[[C_LITERAL]]) : (!cir.ptr<!cir.func<(!cir.ptr<!rec_B>, !s8i)>>, !cir.ptr<!rec_B> {{.*}}, !s8i {{.*}}) -> ()
+// CIR: cir.call @_ZN1B1fEc(%[[B]], %[[C_LITERAL]]) : (!cir.ptr<!rec_B> {{.*}}, !s8i {{.*}}) -> ()
// CIR: cir.yield
// CIR: } cleanup normal {
// CIR: cir.call @_ZN1BD1Ev(%[[B]]) nothrow : (!cir.ptr<!rec_B> {{.*}}) -> ()
@@ -111,15 +107,11 @@ void call_virtual_fn_in_cleanup_scope() {
// LLVM: call void @_ZN1BC2Ev(ptr {{.*}} %[[B]])
// LLVM: br label %[[CLEANUP_SCOPE:.*]]
// LLVM: [[CLEANUP_SCOPE]]:
-// LLVM: %[[B_VPTR:.*]] = load ptr, ptr %[[B]]
-// LLVM: %[[FN_PTR_ADDR:.*]] = getelementptr inbounds ptr, ptr %[[B_VPTR]], i32 0
-// LLVM: %[[FN_PTR:.*]] = load ptr, ptr %[[FN_PTR_ADDR]]
-// LLVM: call void %[[FN_PTR]](ptr {{.*}} %[[B]], i8 noundef 99)
+// LLVM: call void @_ZN1B1fEc(ptr {{.*}} %[[B]], i8 noundef 99)
// LLVM: br label %[[NORMAL_CLEANUP:.*]]
// LLVM: [[NORMAL_CLEANUP]]:
// LLVM: call void @_ZN1BD1Ev(ptr {{.*}} %[[B]])
-// Note: OGCG devirtualizes the call. We don't do that yet in CIR.
// OGCG: define {{.*}} void @_Z32call_virtual_fn_in_cleanup_scopev()
// OGCG: %[[B:.*]] = alloca %struct.B, align 8
// OGCG: call void @_ZN1BC2Ev(ptr {{.*}} %[[B]])
``````````
</details>
https://github.com/llvm/llvm-project/pull/195106
More information about the cfe-commits
mailing list