[llvm] [IPSCCP] Track returns of non-interposable definitions (PR #210374)

via llvm-commits llvm-commits at lists.llvm.org
Fri Jul 17 09:51:32 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-analysis

Author: michaelselehov

<details>
<summary>Changes</summary>

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.

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


3 Files Affected:

- (modified) llvm/lib/Analysis/ValueLatticeUtils.cpp (+8-1) 
- (modified) llvm/test/Transforms/SCCP/comdat-ipo.ll (+28-5) 
- (added) llvm/test/Transforms/SCCP/ipsccp-odr-return-range.ll (+79) 


``````````diff
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 }

``````````

</details>


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


More information about the llvm-commits mailing list