[lld] [llvm] [IPSCCP] Track returns of non-interposable definitions (PR #210374)
via llvm-commits
llvm-commits at lists.llvm.org
Fri Jul 17 21:33:23 PDT 2026
https://github.com/michaelselehov updated https://github.com/llvm/llvm-project/pull/210374
>From b1a96f0f4706d187dcd85c60938df9feddcd2e0d Mon Sep 17 00:00:00 2001
From: mselehov <mselehov at amd.com>
Date: Fri, 17 Jul 2026 11:13:38 -0500
Subject: [PATCH 1/3] [IPSCCP] Track returns of non-interposable definitions
canTrackReturnsInterprocedurally() gated interprocedural return-value
tracking on hasExactDefinition(), which excludes weak-for-linker ODR
linkages (linkonce_odr / weak_odr) because their bodies may be
derefined at link time. As a result IPSCCP could not attach inferred
return attributes (e.g. range) to such functions, even though ODR
guarantees every copy is semantically equivalent and the symbol is
not interposable.
Relax the gate to accept any non-interposable definition. A
return-value fact derived from the body (a constant, a lattice value,
or a range) holds for whichever ODR copy the linker keeps, so it is
sound to infer it and propagate it into callers. Interposable
definitions (linkonce_any / weak_any / common / extern_weak) remain
excluded, as do nobuiltin definitions, whose call sites may assume
builtin semantics instead of the visible body.
This lets range attributes propagate through linkonce_odr accessors
(such as HIP's thread-index accessors), which restores GEP inbounds
and offset folding that were lost in the device full-LTO pipeline.
---
llvm/lib/Analysis/ValueLatticeUtils.cpp | 9 ++-
llvm/test/Transforms/SCCP/comdat-ipo.ll | 33 ++++++--
.../SCCP/ipsccp-odr-return-range.ll | 79 +++++++++++++++++++
3 files changed, 115 insertions(+), 6 deletions(-)
create mode 100644 llvm/test/Transforms/SCCP/ipsccp-odr-return-range.ll
diff --git a/llvm/lib/Analysis/ValueLatticeUtils.cpp b/llvm/lib/Analysis/ValueLatticeUtils.cpp
index acc19667d7764..878768f9c4c62 100644
--- a/llvm/lib/Analysis/ValueLatticeUtils.cpp
+++ b/llvm/lib/Analysis/ValueLatticeUtils.cpp
@@ -22,7 +22,14 @@ bool llvm::canTrackArgumentsInterprocedurally(Function *F) {
}
bool llvm::canTrackReturnsInterprocedurally(Function *F) {
- return F->hasExactDefinition() && !F->hasFnAttribute(Attribute::Naked);
+ // A non-interposable definition (this includes linkonce_odr/weak_odr, not
+ // just strong definitions) is authoritative for its return value, so return
+ // attributes inferred from the body hold for whichever copy the linker keeps.
+ // nobuiltin definitions are excluded: call sites may assume builtin semantics
+ // rather than the visible body, so facts from the body must not be propagated.
+ return !F->isDeclaration() && !F->isInterposable() &&
+ !F->hasFnAttribute(Attribute::NoBuiltin) &&
+ !F->hasFnAttribute(Attribute::Naked);
}
bool llvm::canTrackGlobalVariableInterprocedurally(GlobalVariable *GV) {
diff --git a/llvm/test/Transforms/SCCP/comdat-ipo.ll b/llvm/test/Transforms/SCCP/comdat-ipo.ll
index 6f2ace6890fba..1198a6bfb5b48 100644
--- a/llvm/test/Transforms/SCCP/comdat-ipo.ll
+++ b/llvm/test/Transforms/SCCP/comdat-ipo.ll
@@ -10,15 +10,17 @@ define i32 @baz() {
ret i32 10
}
-; We can const-prop @baz's return value *into* @foo, but cannot
-; constprop @foo's return value into bar.
+; We can const-prop @baz's return value *into* @foo. @foo is linkonce_odr,
+; which is not interposable: ODR guarantees every copy is semantically
+; equivalent, so a return-value fact derived from this body is valid for
+; whichever copy the linker keeps. Therefore we may also const-prop @foo's
+; return value into its caller @bar.
define linkonce_odr i32 @foo() {
; CHECK-LABEL: @foo(
; CHECK-NEXT: [[VAL:%.*]] = call i32 @baz()
; CHECK-NEXT: ret i32 10
;
-
%val = call i32 @baz()
ret i32 %val
}
@@ -26,9 +28,30 @@ define linkonce_odr i32 @foo() {
define i32 @bar() {
; CHECK-LABEL: @bar(
; CHECK-NEXT: [[VAL:%.*]] = call i32 @foo()
-; CHECK-NEXT: ret i32 [[VAL]]
+; CHECK-NEXT: ret i32 10
;
-
%val = call i32 @foo()
ret i32 %val
}
+
+; @foo_any is linkonce_any, which *is* interposable: the linker may replace it
+; with an unrelated definition. We must not const-prop its return value into
+; @bar_any.
+
+define linkonce i32 @foo_any() {
+; CHECK-LABEL: @foo_any(
+; CHECK-NEXT: [[VAL:%.*]] = call i32 @baz()
+; CHECK-NEXT: ret i32 10
+;
+ %val = call i32 @baz()
+ ret i32 %val
+}
+
+define i32 @bar_any() {
+; CHECK-LABEL: @bar_any(
+; CHECK-NEXT: [[VAL:%.*]] = call i32 @foo_any()
+; CHECK-NEXT: ret i32 [[VAL]]
+;
+ %val = call i32 @foo_any()
+ ret i32 %val
+}
diff --git a/llvm/test/Transforms/SCCP/ipsccp-odr-return-range.ll b/llvm/test/Transforms/SCCP/ipsccp-odr-return-range.ll
new file mode 100644
index 0000000000000..0fd754701d107
--- /dev/null
+++ b/llvm/test/Transforms/SCCP/ipsccp-odr-return-range.ll
@@ -0,0 +1,79 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 4
+; RUN: opt -passes=ipsccp -S %s | FileCheck %s
+
+; IPSCCP can infer return-value attributes (here a range) for any
+; non-interposable definition, not only strong/local ones. ODR-linkage
+; definitions (linkonce_odr / weak_odr) are guaranteed to be token-identical
+; across translation units, so a fact derived from the body is valid for
+; whichever copy the linker keeps.
+
+define linkonce_odr i32 @linkonce_odr_ret(i16 %x) {
+; CHECK-LABEL: define linkonce_odr range(i32 0, 65536) i32 @linkonce_odr_ret(
+; CHECK-SAME: i16 [[X:%.*]]) {
+; CHECK-NEXT: [[Z:%.*]] = zext i16 [[X]] to i32
+; CHECK-NEXT: ret i32 [[Z]]
+;
+ %z = zext i16 %x to i32
+ ret i32 %z
+}
+
+define weak_odr i32 @weak_odr_ret(i16 %x) {
+; CHECK-LABEL: define weak_odr range(i32 0, 65536) i32 @weak_odr_ret(
+; CHECK-SAME: i16 [[X:%.*]]) {
+; CHECK-NEXT: [[Z:%.*]] = zext i16 [[X]] to i32
+; CHECK-NEXT: ret i32 [[Z]]
+;
+ %z = zext i16 %x to i32
+ ret i32 %z
+}
+
+; available_externally is also non-interposable (its body must match the
+; external definition), so a return fact can be inferred here too.
+
+define available_externally i32 @avail_ext_ret(i16 %x) {
+; CHECK-LABEL: define available_externally range(i32 0, 65536) i32 @avail_ext_ret(
+; CHECK-SAME: i16 [[X:%.*]]) {
+; CHECK-NEXT: [[Z:%.*]] = zext i16 [[X]] to i32
+; CHECK-NEXT: ret i32 [[Z]]
+;
+ %z = zext i16 %x to i32
+ ret i32 %z
+}
+
+; Negative cases: interposable definitions (linkonce_any / weak_any) may be
+; replaced at link time by an unrelated body, so no return fact can be inferred.
+
+define linkonce i32 @linkonce_any_ret(i16 %x) {
+; CHECK-LABEL: define linkonce i32 @linkonce_any_ret(
+; CHECK-SAME: i16 [[X:%.*]]) {
+; CHECK-NEXT: [[Z:%.*]] = zext i16 [[X]] to i32
+; CHECK-NEXT: ret i32 [[Z]]
+;
+ %z = zext i16 %x to i32
+ ret i32 %z
+}
+
+define weak i32 @weak_any_ret(i16 %x) {
+; CHECK-LABEL: define weak i32 @weak_any_ret(
+; CHECK-SAME: i16 [[X:%.*]]) {
+; CHECK-NEXT: [[Z:%.*]] = zext i16 [[X]] to i32
+; CHECK-NEXT: ret i32 [[Z]]
+;
+ %z = zext i16 %x to i32
+ ret i32 %z
+}
+
+; A nobuiltin definition is also excluded: call sites may assume builtin
+; semantics instead of the visible body, so no return fact may be inferred.
+
+define i32 @nobuiltin_ret(i16 %x) #0 {
+; CHECK-LABEL: define i32 @nobuiltin_ret(
+; CHECK-SAME: i16 [[X:%.*]]) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT: [[Z:%.*]] = zext i16 [[X]] to i32
+; CHECK-NEXT: ret i32 [[Z]]
+;
+ %z = zext i16 %x to i32
+ ret i32 %z
+}
+
+attributes #0 = { nobuiltin }
>From c512546ae8c15e472de1a270802f060d89cbcfb7 Mon Sep 17 00:00:00 2001
From: mselehov <mselehov at amd.com>
Date: Fri, 17 Jul 2026 12:03:06 -0500
Subject: [PATCH 2/3] [IPSCCP] clang-format: wrap comment to 80 columns
---
llvm/lib/Analysis/ValueLatticeUtils.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/llvm/lib/Analysis/ValueLatticeUtils.cpp b/llvm/lib/Analysis/ValueLatticeUtils.cpp
index 878768f9c4c62..3ea3a708f92b3 100644
--- a/llvm/lib/Analysis/ValueLatticeUtils.cpp
+++ b/llvm/lib/Analysis/ValueLatticeUtils.cpp
@@ -26,7 +26,8 @@ bool llvm::canTrackReturnsInterprocedurally(Function *F) {
// just strong definitions) is authoritative for its return value, so return
// attributes inferred from the body hold for whichever copy the linker keeps.
// nobuiltin definitions are excluded: call sites may assume builtin semantics
- // rather than the visible body, so facts from the body must not be propagated.
+ // rather than the visible body, so facts from the body must not be
+ // propagated.
return !F->isDeclaration() && !F->isInterposable() &&
!F->hasFnAttribute(Attribute::NoBuiltin) &&
!F->hasFnAttribute(Attribute::Naked);
>From c524d1ed61a96c5dc4fac8f806f1dcea01ed0a1c Mon Sep 17 00:00:00 2001
From: mselehov <mselehov at amd.com>
Date: Fri, 17 Jul 2026 23:30:46 -0500
Subject: [PATCH 3/3] [IPSCCP] Update WPD devirt tests for available_externally
return prop
---
lld/test/ELF/lto/devirt_vcall_vis_shared_def.ll | 2 +-
llvm/test/tools/gold/X86/devirt_vcall_vis_shared_def.ll | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/lld/test/ELF/lto/devirt_vcall_vis_shared_def.ll b/lld/test/ELF/lto/devirt_vcall_vis_shared_def.ll
index b77dde97a2c05..f7e2fc63458e8 100644
--- a/lld/test/ELF/lto/devirt_vcall_vis_shared_def.ll
+++ b/lld/test/ELF/lto/devirt_vcall_vis_shared_def.ll
@@ -57,7 +57,7 @@ target triple = "x86_64-grtev4-linux-gnu"
;; Prevent the vtables from being dead code eliminated.
@llvm.used = appending global [2 x ptr] [ ptr @_ZTV1A, ptr @_ZTV1B]
-; CHECK-IR-LABEL: define dso_local i32 @_start
+; CHECK-IR-LABEL: define dso_local {{.*}}i32 @_start
define i32 @_start(ptr %obj, i32 %a) {
entry:
%vtable = load ptr, ptr %obj
diff --git a/llvm/test/tools/gold/X86/devirt_vcall_vis_shared_def.ll b/llvm/test/tools/gold/X86/devirt_vcall_vis_shared_def.ll
index 6d05598e8b302..69e9443013cc6 100644
--- a/llvm/test/tools/gold/X86/devirt_vcall_vis_shared_def.ll
+++ b/llvm/test/tools/gold/X86/devirt_vcall_vis_shared_def.ll
@@ -77,7 +77,7 @@ target triple = "x86_64-grtev4-linux-gnu"
;; Prevent the vtables from being dead code eliminated.
@llvm.used = appending global [2 x ptr] [ ptr @_ZTV1A, ptr @_ZTV1B]
-; CHECK-IR-LABEL: define dso_local i32 @_start
+; CHECK-IR-LABEL: define dso_local {{.*}}i32 @_start
define i32 @_start(ptr %obj, i32 %a) {
entry:
%vtable = load ptr, ptr %obj
More information about the llvm-commits
mailing list