[llvm] [WPD] Remove type test assumes through PHI and select nodes (PR #224155)
Michael Park via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 17 11:12:25 PDT 2026
https://github.com/mpark updated https://github.com/llvm/llvm-project/pull/224155
>From 1532c1544c9e0173ca1e60f2e544ab169dbcf07b Mon Sep 17 00:00:00 2001
From: Michael Park <mcypark at gmail.com>
Date: Wed, 16 Sep 2026 14:28:07 -0700
Subject: [PATCH] [WPD] Remove type test assumes through PHI and select nodes
scanTypeTestUsers finds devirtualization candidates and removes type test
assumes when type information is missing. Without this cleanup,
LowerTypeTests treats missing type IDs as Unsat and lowers their tests to
false, potentially introducing assume(false).
The existing matcher only finds an assume that directly uses a type test.
SimplifyCFG can merge type test results through PHI or select nodes before
the assume. This limitation was described when llvm.public.type.test was
introduced in 2eade1dba4a8d6e1c6867e9127bcd88cf4e55976 (D128955), and is
also reported in issue #187049.
When assumptions need to be removed, walk the type test's use graph through
PHI nodes and select value operands and collect dependent assumes. Keep these
merged assumptions separate from the direct assumptions used as evidence for
devirtualization because a merged condition does not prove that each
constituent test is true. Share the visited merge set across unresolved type
tests so each merge is traversed at most once.
In practice, jump-table-to-switch optimization exposed this issue in
polymorphic std::variant dispatch. Converting the indirect dispatch to a
switch enabled inlining and CFG simplification to produce the merged type test
assumptions.
Add ThinLTO coverage for selects, PHIs, an ordinary if-chain that produces
nested selects, and a merge with both known and missing type information.
Fixes #187049.
Assisted by: GPT-5.6 Sol
---
.../lib/Transforms/IPO/WholeProgramDevirt.cpp | 54 ++++++
llvm/test/ThinLTO/X86/type_test_no_vtable.ll | 172 ++++++++++++++++++
2 files changed, 226 insertions(+)
create mode 100644 llvm/test/ThinLTO/X86/type_test_no_vtable.ll
diff --git a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
index e9d2036e5fdbb..daacf82fdb197 100644
--- a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
+++ b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
@@ -65,6 +65,7 @@
#include "llvm/ADT/DenseMapInfo.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/MapVector.h"
+#include "llvm/ADT/SmallPtrSet.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/Analysis/AssumptionCache.h"
@@ -2084,9 +2085,56 @@ bool DevirtModule::areRemarksEnabled() {
return false;
}
+/// Find assumes whose conditions depend on this type test through phi or select
+/// nodes. SimplifyCFG can produce these patterns by merging type test + assume
+/// sequences from different predecessors.
+static void
+findAssumesThroughMergesForTypeTest(SmallVectorImpl<CallInst *> &Assumes,
+ CallInst &TypeTest,
+ SmallPtrSetImpl<Value *> &VisitedMerges) {
+ SmallVector<Value *, 4> Worklist;
+
+ auto GetMergeUser = [](User *U, Value *V) -> Value * {
+ if (isa<PHINode>(U))
+ return U;
+ if (auto *Select = dyn_cast<SelectInst>(U);
+ Select && (Select->getTrueValue() == V || Select->getFalseValue() == V))
+ return Select;
+ return nullptr;
+ };
+
+ // Direct assume users were already collected by
+ // findDevirtualizableCallsForTypeTest. Start from merge users so this search
+ // finds only assumptions that depend on the type test through merges.
+ for (User *U : TypeTest.users())
+ if (Value *Merge = GetMergeUser(U, &TypeTest))
+ Worklist.push_back(Merge);
+
+ while (!Worklist.empty()) {
+ Value *V = Worklist.pop_back_val();
+ if (!VisitedMerges.insert(V).second)
+ continue;
+
+ for (User *U : V->users()) {
+ if (auto *Assume = dyn_cast<AssumeInst>(U)) {
+ if (Assume->getArgOperand(0) == V)
+ Assumes.push_back(Assume);
+ continue;
+ }
+
+ if (Value *Merge = GetMergeUser(U, V))
+ Worklist.push_back(Merge);
+ }
+ }
+}
+
void DevirtModule::scanTypeTestUsers(
Function *TypeTestFunc,
DenseMap<Metadata *, std::set<TypeMemberInfo>> &TypeIdMap) {
+ // Cleanup removes every assume reachable through a merge, so each merge only
+ // needs to be processed once even if multiple unresolved type tests reach it.
+ SmallPtrSet<Value *, 8> VisitedMerges;
+
// Find all virtual calls via a virtual table pointer %p under an assumption
// of the form llvm.assume(llvm.type.test(%p, %md)) or
// llvm.assume(llvm.public.type.test(%p, %md)).
@@ -2113,6 +2161,12 @@ void DevirtModule::scanTypeTestUsers(
}
auto RemoveTypeTestAssumes = [&]() {
+ // A merge of type test results does not imply that any individual type
+ // test can be assumed, so don't use these assumes to identify
+ // devirtualizable calls. They still need to be removed when type
+ // information is missing for any value contributing to the merge.
+ findAssumesThroughMergesForTypeTest(Assumes, *CI, VisitedMerges);
+
// We no longer need the assumes or the type test.
for (auto *Assume : Assumes)
Assume->eraseFromParent();
diff --git a/llvm/test/ThinLTO/X86/type_test_no_vtable.ll b/llvm/test/ThinLTO/X86/type_test_no_vtable.ll
new file mode 100644
index 0000000000000..56c96a01f30f6
--- /dev/null
+++ b/llvm/test/ThinLTO/X86/type_test_no_vtable.ll
@@ -0,0 +1,172 @@
+; Regression tests for https://github.com/llvm/llvm-project/issues/187049.
+;
+; Test that WPD removes type test assumes that are merged through a select or
+; phi when there are no corresponding vtables in the ThinLTO index. If an
+; assume is left behind, LowerTypeTests resolves the type tests to false,
+; producing assume(false) and incorrectly making the virtual call unreachable.
+;
+; REQUIRES: x86-registered-target
+;
+; RUN: opt -thinlto-bc -thinlto-split-lto-unit=false -o %t.o %s
+; RUN: llvm-lto2 run %t.o -save-temps -whole-program-visibility \
+; RUN: -r=%t.o,test_select,plx \
+; RUN: -r=%t.o,test_select_one_missing,plx \
+; RUN: -r=%t.o,test_select_chain,plx \
+; RUN: -r=%t.o,test_phi,plx \
+; RUN: -r=%t.o,seed_known,plx \
+; RUN: -r=%t.o,get_base, \
+; RUN: -r=%t.o,get_derived, \
+; RUN: -r=%t.o,observe, \
+; RUN: -r=%t.o,puts, \
+; RUN: -o %t2
+; RUN: llvm-dis %t2.1.4.opt.bc -o - | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+ at message = private unnamed_addr constant [6 x i8] c"hello\00"
+ at known_vtable = internal constant [1 x ptr] [ptr @known_target], !type !0
+ at llvm.compiler.used = appending global [1 x ptr] [ptr @known_vtable],
+ section "llvm.metadata"
+
+define void @test_select(ptr %object, i1 %derived) {
+entry:
+ %vtable = load ptr, ptr %object, align 8
+ %base.test = call i1 @llvm.public.type.test(
+ ptr %vtable, metadata !"_ZTS4Base")
+ %derived.test = call i1 @llvm.public.type.test(
+ ptr %vtable, metadata !"_ZTS7Derived")
+ %type.test = select i1 %derived, i1 %derived.test, i1 %base.test
+ call void @llvm.assume(i1 %type.test)
+ %target = load ptr, ptr %vtable, align 8
+ call void %target(ptr %object)
+ call i32 @puts(ptr @message)
+ ret void
+}
+
+; CHECK-LABEL: @test_select(
+; CHECK-NOT: @llvm.type.test
+; CHECK-NOT: @llvm.assume
+; CHECK: call void %{{.*}}(ptr {{.*}}%object)
+; CHECK: call i32 @puts(ptr {{.*}})
+; CHECK: ret void
+
+; Removing the assumption is necessary if even one of its type tests has no
+; type information. The other type test has a vtable entry in this module.
+define void @test_select_one_missing(ptr %object, i1 %use.missing) {
+entry:
+ %vtable = load ptr, ptr %object, align 8
+ %known.test = call i1 @llvm.public.type.test(
+ ptr %vtable, metadata !"_ZTS5Known")
+ %missing.test = call i1 @llvm.public.type.test(
+ ptr %vtable, metadata !"_ZTS7Missing")
+ %type.test = select i1 %use.missing, i1 %missing.test, i1 %known.test
+ call void @llvm.assume(i1 %type.test)
+ %target = load ptr, ptr %vtable, align 8
+ call void %target(ptr %object)
+ call i32 @puts(ptr @message)
+ ret void
+}
+
+; CHECK-LABEL: @test_select_one_missing(
+; CHECK-NOT: @llvm.type.test
+; CHECK-NOT: @llvm.assume
+; CHECK: call void %{{.*}}(ptr {{.*}}%object)
+; CHECK: call i32 @puts(ptr {{.*}})
+; CHECK: ret void
+
+; An if/else-if/else chain can produce nested selects when its values are
+; available in the entry block. The outer type test has a valid summary, while
+; the two tests nested inside the inner select do not. The latter must still
+; cause the dependent assume to be removed.
+define void @test_select_chain(ptr %object, i1 %first, i1 %second) {
+entry:
+ %vtable = load ptr, ptr %object, align 8
+ %known.test = call i1 @llvm.public.type.test(
+ ptr %vtable, metadata !"_ZTS5Known")
+ %missing.0 = call i1 @llvm.public.type.test(
+ ptr %vtable, metadata !"_ZTS8Missing0")
+ %missing.1 = call i1 @llvm.public.type.test(
+ ptr %vtable, metadata !"_ZTS8Missing1")
+ %inner = select i1 %second, i1 %missing.0, i1 %missing.1
+ %type.test = select i1 %first, i1 %known.test, i1 %inner
+ call void @llvm.assume(i1 %type.test)
+ %target = load ptr, ptr %vtable, align 8
+ call void %target(ptr %object)
+ call void @observe(i1 %first)
+ call i32 @puts(ptr @message)
+ ret void
+}
+
+; CHECK-LABEL: @test_select_chain(
+; CHECK-NOT: @llvm.type.test
+; CHECK-NOT: @llvm.assume
+; CHECK: call void %{{.*}}(ptr {{.*}}%object)
+; CHECK: call void @observe(i1 %first)
+; CHECK: call i32 @puts(ptr {{.*}})
+; CHECK: ret void
+
+; Separate dispatch arms can be merged into a phi instead of a select.
+define void @test_phi(i1 %derived) {
+entry:
+ br i1 %derived, label %derived.block, label %base.block
+
+base.block:
+ %base.object = call ptr @get_base()
+ %base.vtable = load ptr, ptr %base.object, align 8
+ %base.test = call i1 @llvm.public.type.test(
+ ptr %base.vtable, metadata !"_ZTS4Base")
+ br label %merge
+
+derived.block:
+ %derived.object = call ptr @get_derived()
+ %derived.vtable = load ptr, ptr %derived.object, align 8
+ %derived.test = call i1 @llvm.public.type.test(
+ ptr %derived.vtable, metadata !"_ZTS7Derived")
+ br label %merge
+
+merge:
+ %object = phi ptr [ %base.object, %base.block ],
+ [ %derived.object, %derived.block ]
+ %vtable = phi ptr [ %base.vtable, %base.block ],
+ [ %derived.vtable, %derived.block ]
+ %type.test = phi i1 [ %base.test, %base.block ],
+ [ %derived.test, %derived.block ]
+ call void @llvm.assume(i1 %type.test)
+ %target = load ptr, ptr %vtable, align 8
+ call void %target(ptr %object)
+ call i32 @puts(ptr @message)
+ ret void
+}
+
+; CHECK-LABEL: @test_phi(
+; CHECK-NOT: @llvm.type.test
+; CHECK-NOT: @llvm.assume
+; CHECK: call void %{{.*}}(ptr {{.*}})
+; CHECK: call i32 @puts(ptr {{.*}})
+; CHECK: ret void
+
+declare i1 @llvm.public.type.test(ptr, metadata)
+declare void @llvm.assume(i1)
+declare i32 @puts(ptr)
+declare ptr @get_base()
+declare ptr @get_derived()
+declare void @observe(i1)
+
+; Ensure that _ZTS5Known gets a TypeIdSummary, so only the nested leaves are
+; responsible for removing the assumption in test_select_chain.
+define void @seed_known(ptr %object) {
+ %vtable = load ptr, ptr %object, align 8
+ %type.test = call i1 @llvm.public.type.test(
+ ptr %vtable, metadata !"_ZTS5Known")
+ call void @llvm.assume(i1 %type.test)
+ %target = load ptr, ptr %vtable, align 8
+ call void %target(ptr %object)
+ ret void
+}
+
+define internal void @known_target(ptr %object) {
+ ret void
+}
+
+!0 = !{i64 0, !"_ZTS5Known"}
More information about the llvm-commits
mailing list