[llvm] [WPD] Remove type test assumes through PHI and select nodes (PR #224155)

via llvm-commits llvm-commits at lists.llvm.org
Wed Sep 16 15:45:58 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-transforms

Author: Michael Park (mpark)

<details>
<summary>Changes</summary>

Currently, `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, this change walks the type test's use graph through `phi` nodes
and `select` value operands and collect dependent `assume`s. These merged assumptions are kept
separate from the direct assumptions used as evidence for devirtualization because a merged condition
does not prove that each constituent test is `true`. The visited merge set is shared across unresolved
type tests so each merge is traversed at most once.

Added 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

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


2 Files Affected:

- (modified) llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp (+54) 
- (added) llvm/test/ThinLTO/X86/type_test_no_vtable.ll (+172) 


``````````diff
diff --git a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
index e9d2036e5fdbb..61be1a9be6067 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 cast<Value>(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"}

``````````

</details>


https://github.com/llvm/llvm-project/pull/224155


More information about the llvm-commits mailing list