[llvm] [WPD] Remove type test assumes through PHI and select nodes (PR #224155)
Michael Park via llvm-commits
llvm-commits at lists.llvm.org
Tue Sep 29 01:04:59 PDT 2026
https://github.com/mpark updated https://github.com/llvm/llvm-project/pull/224155
>From 30bf84954eab3d953498a762327b8adccdd3f0b8 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 1/2] [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 8a35980003c27..f2959da3ec396 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"}
>From ac2d8634414c6811ff28b8726b607514f5650085 Mon Sep 17 00:00:00 2001
From: Michael Park <mcypark at gmail.com>
Date: Tue, 29 Sep 2026 01:04:18 -0700
Subject: [PATCH 2/2] Address comments from teresajohnson.
---
llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp | 8 +++++++-
llvm/test/ThinLTO/X86/type_test_no_vtable.ll | 12 +++---------
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
index f2959da3ec396..88c57d2195bbd 100644
--- a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
+++ b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
@@ -2093,6 +2093,9 @@ findAssumesThroughMergesForTypeTest(SmallVectorImpl<CallInst *> &Assumes,
CallInst &TypeTest,
SmallPtrSetImpl<Value *> &VisitedMerges) {
SmallVector<Value *, 4> Worklist;
+#ifndef NDEBUG
+ SmallPtrSet<CallInst *, 4> DirectAssumes(Assumes.begin(), Assumes.end());
+#endif
auto GetMergeUser = [](User *U, Value *V) -> Value * {
if (isa<PHINode>(U))
@@ -2117,8 +2120,11 @@ findAssumesThroughMergesForTypeTest(SmallVectorImpl<CallInst *> &Assumes,
for (User *U : V->users()) {
if (auto *Assume = dyn_cast<AssumeInst>(U)) {
- if (Assume->getArgOperand(0) == V)
+ if (Assume->getArgOperand(0) == V) {
+ assert(!DirectAssumes.contains(Assume) &&
+ "assume must not be both direct and merged");
Assumes.push_back(Assume);
+ }
continue;
}
diff --git a/llvm/test/ThinLTO/X86/type_test_no_vtable.ll b/llvm/test/ThinLTO/X86/type_test_no_vtable.ll
index 56c96a01f30f6..4f99af12ffbac 100644
--- a/llvm/test/ThinLTO/X86/type_test_no_vtable.ll
+++ b/llvm/test/ThinLTO/X86/type_test_no_vtable.ll
@@ -19,7 +19,9 @@
; RUN: -r=%t.o,observe, \
; RUN: -r=%t.o,puts, \
; RUN: -o %t2
-; RUN: llvm-dis %t2.1.4.opt.bc -o - | FileCheck %s
+; RUN: llvm-dis %t2.1.4.opt.bc -o - | FileCheck %s \
+; RUN: --implicit-check-not='@llvm{{(\.public)?}}.type.test' \
+; RUN: --implicit-check-not='@llvm.assume'
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"
@@ -45,8 +47,6 @@ entry:
}
; 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
@@ -69,8 +69,6 @@ entry:
}
; 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
@@ -99,8 +97,6 @@ entry:
}
; 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 {{.*}})
@@ -140,8 +136,6 @@ merge:
}
; 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
More information about the llvm-commits
mailing list