[llvm] [GVN] Skip instructions without value numbers in performScalarPRE (PR #219851)
Nikhil Ludder via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 10 06:14:29 PDT 2026
https://github.com/badnikhil updated https://github.com/llvm/llvm-project/pull/219851
>From 6f432a5267d74a50e7ba55a7302a112ceeaf9d06 Mon Sep 17 00:00:00 2001
From: badnikhil <nikhilljatt at gmail.com>
Date: Mon, 31 Aug 2026 02:24:25 +0530
Subject: [PATCH 1/6] [GVN] Skip instructions without value numbers in
performScalarPRE
GVN may insert coercion instructions (MaterializeAdjustedValue) without
assigning them value numbers. If one becomes a GEP index of a PRE'd
load in the same processBlock sweep, the GEP-index scalar PRE in
processNonLocalLoad reaches it before the next re-numbering iteration
and the verifying VN.lookup() asserts "Value not numbered?".
Bail out instead: a just-inserted instruction has no leaders, so PRE
cannot transform it this iteration; the next GVN iteration numbers it
and handles it normally.
Fixes #216265.
---
llvm/lib/Transforms/Scalar/GVN.cpp | 8 +++-
.../GVN/pre-coerced-value-not-numbered.ll | 41 +++++++++++++++++++
2 files changed, 48 insertions(+), 1 deletion(-)
create mode 100644 llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
diff --git a/llvm/lib/Transforms/Scalar/GVN.cpp b/llvm/lib/Transforms/Scalar/GVN.cpp
index bd3fceb1ff3d2..64e3aaadc71cc 100644
--- a/llvm/lib/Transforms/Scalar/GVN.cpp
+++ b/llvm/lib/Transforms/Scalar/GVN.cpp
@@ -3643,7 +3643,13 @@ bool GVNPass::performScalarPRE(Instruction *CurInst) {
return false;
}
- uint32_t ValNo = VN.lookup(CurInst);
+ // Instructions inserted by GVN itself in the current iteration, e.g. when
+ // coercing an available load value to the load type, have not been assigned
+ // a value number yet. They have no leaders either, so PRE cannot do anything
+ // with them until they are numbered on the next iteration; skip them.
+ uint32_t ValNo = VN.lookup(CurInst, /*Verify=*/false);
+ if (!ValNo)
+ return false;
// Look for the predecessors for PRE opportunities. We're
// only trying to solve the basic diamond case, where
diff --git a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
new file mode 100644
index 0000000000000..1ca08c5709142
--- /dev/null
+++ b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
@@ -0,0 +1,41 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt -passes=gvn -S < %s | FileCheck %s
+
+; Test for #216265. When GVN forwards the vector store to the i64 load, it
+; inserts a coercion bitcast that has no value number yet. Processing the
+; PRE'd load of %gep afterwards runs scalar PRE on the GEP index (now the
+; bitcast) and used to assert with "Value not numbered?".
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+
+ at g14 = external global i64
+ at g23 = external global <2 x i32>
+
+define void @f2() {
+; CHECK-LABEL: define void @f2() {
+; CHECK-NEXT: [[ENTRY:.*]]:
+; CHECK-NEXT: [[ARR:%.*]] = alloca [9 x i8], align 1
+; CHECK-NEXT: [[V:%.*]] = load <2 x i32>, ptr @g14, align 8
+; CHECK-NEXT: br label %[[LOOP:.*]]
+; CHECK: [[LOOP]]:
+; CHECK-NEXT: [[IV:%.*]] = phi i64 [ 0, %[[ENTRY]] ], [ [[TMP0:%.*]], %[[LOOP]] ]
+; CHECK-NEXT: [[GEP:%.*]] = getelementptr inbounds i8, ptr [[ARR]], i64 [[IV]]
+; CHECK-NEXT: store <2 x i32> [[V]], ptr @g23, align 8
+; CHECK-NEXT: [[TMP0]] = bitcast <2 x i32> [[V]] to i64
+; CHECK-NEXT: [[GEP_PHI_TRANS_INSERT:%.*]] = getelementptr inbounds i8, ptr [[ARR]], i64 [[TMP0]]
+; CHECK-NEXT: br label %[[LOOP]]
+;
+entry:
+ %arr = alloca [9 x i8], align 1
+ %v = load <2 x i32>, ptr @g14, align 8
+ br label %loop
+
+loop:
+ %iv = phi i64 [ 0, %entry ], [ %conv, %loop ]
+ %gep = getelementptr inbounds i8, ptr %arr, i64 %iv
+ %load1 = load i8, ptr %gep, align 1
+ %t = trunc nuw i8 %load1 to i1
+ store <2 x i32> %v, ptr @g23, align 8
+ %conv = load i64, ptr @g23, align 8
+ br label %loop
+}
>From 936e7f134789d1cc5742b105f93588bd39ccc53c Mon Sep 17 00:00:00 2001
From: badnikhil <nikhilljatt at gmail.com>
Date: Tue, 8 Sep 2026 22:19:31 +0530
Subject: [PATCH 2/6] [GVN] Use a UB-free reproducer for the coerced-value PRE
test
The previous test indexed a 9-byte alloca with an unconstrained i64, so the
inbounds GEP was poison and the load past it was immediate UB. Index a
256-byte alloca from its midpoint with an i8 instead: GEP indices are sign
extended, so every offset stays in [0, 255] for any input. The crash sequence
(load PRE, then a coercing trunc RAUW'd without a value number) is unchanged.
---
.../GVN/pre-coerced-value-not-numbered.ll | 60 +++++++++++--------
1 file changed, 35 insertions(+), 25 deletions(-)
diff --git a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
index 1ca08c5709142..53e6e9454ad56 100644
--- a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
+++ b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
@@ -1,41 +1,51 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -passes=gvn -S < %s | FileCheck %s
-; Test for #216265. When GVN forwards the vector store to the i64 load, it
-; inserts a coercion bitcast that has no value number yet. Processing the
-; PRE'd load of %gep afterwards runs scalar PRE on the GEP index (now the
-; bitcast) and used to assert with "Value not numbered?".
-
-target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+; Test for #216265.
+;
+; GVN load-PREs %load1 across the backedge, inserting a phi-translated GEP whose
+; index is %conv. It then forwards the i64 store to the i8 load %conv, so
+; MaterializeAdjustedValue creates a coercing `trunc i64 %x to i8` and RAUWs it
+; over %conv without giving it a value number. Reaching the inserted .pre load
+; later in the same sweep runs scalar PRE on its GEP's indices, which used to
+; look up the value number of that trunc and assert with "Value not numbered?".
+;
+; No undefined behaviour is involved: a narrower GEP index is sign extended, so
+; the i8 index covers exactly [-128, 127], and %base points 128 bytes into a
+; 256-byte alloca. Every accessed offset is therefore in [0, 255] and the
+; inbounds GEP is never poison, whatever %x is.
- at g14 = external global i64
- at g23 = external global <2 x i32>
+ at g = external global i64
-define void @f2() {
-; CHECK-LABEL: define void @f2() {
+define void @f(i64 noundef %x) {
+; CHECK-LABEL: define void @f(
+; CHECK-SAME: i64 noundef [[X:%.*]]) {
; CHECK-NEXT: [[ENTRY:.*]]:
-; CHECK-NEXT: [[ARR:%.*]] = alloca [9 x i8], align 1
-; CHECK-NEXT: [[V:%.*]] = load <2 x i32>, ptr @g14, align 8
+; CHECK-NEXT: [[ARR:%.*]] = alloca [256 x i8], align 1
+; CHECK-NEXT: [[BASE:%.*]] = getelementptr inbounds i8, ptr [[ARR]], i64 128
; CHECK-NEXT: br label %[[LOOP:.*]]
; CHECK: [[LOOP]]:
-; CHECK-NEXT: [[IV:%.*]] = phi i64 [ 0, %[[ENTRY]] ], [ [[TMP0:%.*]], %[[LOOP]] ]
-; CHECK-NEXT: [[GEP:%.*]] = getelementptr inbounds i8, ptr [[ARR]], i64 [[IV]]
-; CHECK-NEXT: store <2 x i32> [[V]], ptr @g23, align 8
-; CHECK-NEXT: [[TMP0]] = bitcast <2 x i32> [[V]] to i64
-; CHECK-NEXT: [[GEP_PHI_TRANS_INSERT:%.*]] = getelementptr inbounds i8, ptr [[ARR]], i64 [[TMP0]]
+; CHECK-NEXT: [[IV:%.*]] = phi i8 [ 0, %[[ENTRY]] ], [ [[TMP0:%.*]], %[[LOOP]] ]
+; CHECK-NEXT: [[GEP:%.*]] = getelementptr inbounds i8, ptr [[BASE]], i8 [[IV]]
+; CHECK-NEXT: call void @use(i8 undef)
+; CHECK-NEXT: store i64 [[X]], ptr @g, align 4
+; CHECK-NEXT: [[TMP0]] = trunc i64 [[X]] to i8
+; CHECK-NEXT: [[GEP_PHI_TRANS_INSERT:%.*]] = getelementptr inbounds i8, ptr [[BASE]], i8 [[TMP0]]
; CHECK-NEXT: br label %[[LOOP]]
;
entry:
- %arr = alloca [9 x i8], align 1
- %v = load <2 x i32>, ptr @g14, align 8
+ %arr = alloca [256 x i8]
+ %base = getelementptr inbounds i8, ptr %arr, i64 128
br label %loop
loop:
- %iv = phi i64 [ 0, %entry ], [ %conv, %loop ]
- %gep = getelementptr inbounds i8, ptr %arr, i64 %iv
- %load1 = load i8, ptr %gep, align 1
- %t = trunc nuw i8 %load1 to i1
- store <2 x i32> %v, ptr @g23, align 8
- %conv = load i64, ptr @g23, align 8
+ %iv = phi i8 [ 0, %entry ], [ %conv, %loop ]
+ %gep = getelementptr inbounds i8, ptr %base, i8 %iv
+ %load1 = load i8, ptr %gep
+ call void @use(i8 %load1)
+ store i64 %x, ptr @g
+ %conv = load i8, ptr @g
br label %loop
}
+
+declare void @use(i8) memory(none)
>From c8715126877c70c37773c31f70e861f605ab4fed Mon Sep 17 00:00:00 2001
From: badnikhil <nikhilljatt at gmail.com>
Date: Thu, 10 Sep 2026 01:44:27 +0530
Subject: [PATCH 3/6] [GVN] Move the guard to the caller, per review
---
llvm/lib/Transforms/Scalar/GVN.cpp | 14 ++++++--------
.../GVN/pre-coerced-value-not-numbered.ll | 19 +++++--------------
2 files changed, 11 insertions(+), 22 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/GVN.cpp b/llvm/lib/Transforms/Scalar/GVN.cpp
index 64e3aaadc71cc..0da5764f5b44d 100644
--- a/llvm/lib/Transforms/Scalar/GVN.cpp
+++ b/llvm/lib/Transforms/Scalar/GVN.cpp
@@ -2116,7 +2116,11 @@ bool GVNPass::processNonLocalLoad(LoadInst *Load,
if (GetElementPtrInst *GEP =
dyn_cast<GetElementPtrInst>(Load->getOperand(0))) {
for (Use &U : GEP->indices())
- if (Instruction *I = dyn_cast<Instruction>(U.get()))
+ // Skip instructions GVN inserted in this iteration, e.g. when coercing
+ // an available load value: they have no value number yet, and no
+ // leaders either, so PRE cannot do anything with them until the next
+ // iteration numbers them.
+ if (Instruction *I = dyn_cast<Instruction>(U.get()); I && VN.exists(I))
Changed |= performScalarPRE(I);
}
}
@@ -3643,13 +3647,7 @@ bool GVNPass::performScalarPRE(Instruction *CurInst) {
return false;
}
- // Instructions inserted by GVN itself in the current iteration, e.g. when
- // coercing an available load value to the load type, have not been assigned
- // a value number yet. They have no leaders either, so PRE cannot do anything
- // with them until they are numbered on the next iteration; skip them.
- uint32_t ValNo = VN.lookup(CurInst, /*Verify=*/false);
- if (!ValNo)
- return false;
+ uint32_t ValNo = VN.lookup(CurInst);
// Look for the predecessors for PRE opportunities. We're
// only trying to solve the basic diamond case, where
diff --git a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
index 53e6e9454ad56..f0d81763a3533 100644
--- a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
+++ b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
@@ -1,19 +1,10 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -passes=gvn -S < %s | FileCheck %s
-; Test for #216265.
-;
-; GVN load-PREs %load1 across the backedge, inserting a phi-translated GEP whose
-; index is %conv. It then forwards the i64 store to the i8 load %conv, so
-; MaterializeAdjustedValue creates a coercing `trunc i64 %x to i8` and RAUWs it
-; over %conv without giving it a value number. Reaching the inserted .pre load
-; later in the same sweep runs scalar PRE on its GEP's indices, which used to
-; look up the value number of that trunc and assert with "Value not numbered?".
-;
-; No undefined behaviour is involved: a narrower GEP index is sign extended, so
-; the i8 index covers exactly [-128, 127], and %base points 128 bytes into a
-; 256-byte alloca. Every accessed offset is therefore in [0, 255] and the
-; inbounds GEP is never poison, whatever %x is.
+; Test for #216265: GVN load-PREs %load1, then forwards the i64 store to %conv,
+; so MaterializeAdjustedValue RAUWs a coercing trunc over it without a value
+; number. Scalar PRE on the inserted .pre load's GEP index then looked that
+; trunc up and asserted with "Value not numbered?".
@g = external global i64
@@ -48,4 +39,4 @@ loop:
br label %loop
}
-declare void @use(i8) memory(none)
+declare void @use(i8)
>From f722d1f5fba0620c6d124aaf03e5105524feef81 Mon Sep 17 00:00:00 2001
From: Nikhil Ludder <72620320+badnikhil at users.noreply.github.com>
Date: Thu, 10 Sep 2026 13:20:21 +0530
Subject: [PATCH 4/6] Update llvm/lib/Transforms/Scalar/GVN.cpp
Co-authored-by: Antonio Frighetto <me at antoniofrighetto.com>
---
llvm/lib/Transforms/Scalar/GVN.cpp | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/llvm/lib/Transforms/Scalar/GVN.cpp b/llvm/lib/Transforms/Scalar/GVN.cpp
index 0da5764f5b44d..1ee78dc6cbef8 100644
--- a/llvm/lib/Transforms/Scalar/GVN.cpp
+++ b/llvm/lib/Transforms/Scalar/GVN.cpp
@@ -2116,10 +2116,8 @@ bool GVNPass::processNonLocalLoad(LoadInst *Load,
if (GetElementPtrInst *GEP =
dyn_cast<GetElementPtrInst>(Load->getOperand(0))) {
for (Use &U : GEP->indices())
- // Skip instructions GVN inserted in this iteration, e.g. when coercing
- // an available load value: they have no value number yet, and no
- // leaders either, so PRE cannot do anything with them until the next
- // iteration numbers them.
+ // Instructions inserted by GVN during this iteration (e.g. coercion casts
+ // from MaterializeAdjustedValue) may not have value numbers yet are skipped.
if (Instruction *I = dyn_cast<Instruction>(U.get()); I && VN.exists(I))
Changed |= performScalarPRE(I);
}
>From 2961d17cc5e7bce85c390bda888a5f80ac5ca140 Mon Sep 17 00:00:00 2001
From: Nikhil Ludder <72620320+badnikhil at users.noreply.github.com>
Date: Thu, 10 Sep 2026 13:20:34 +0530
Subject: [PATCH 5/6] Update
llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
Co-authored-by: Antonio Frighetto <me at antoniofrighetto.com>
---
llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
index f0d81763a3533..74bd8e7af98da 100644
--- a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
+++ b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
@@ -8,7 +8,7 @@
@g = external global i64
-define void @f(i64 noundef %x) {
+define void @test_coerced_value_not_numbered(i64 noundef %x) {
; CHECK-LABEL: define void @f(
; CHECK-SAME: i64 noundef [[X:%.*]]) {
; CHECK-NEXT: [[ENTRY:.*]]:
>From 8348f7e2aa4362f18df93dd6bcf804699315371a Mon Sep 17 00:00:00 2001
From: Nikhil Ludder <72620320+badnikhil at users.noreply.github.com>
Date: Thu, 10 Sep 2026 18:44:16 +0530
Subject: [PATCH 6/6] Update
llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
Co-authored-by: Antonio Frighetto <me at antoniofrighetto.com>
---
.../Transforms/GVN/pre-coerced-value-not-numbered.ll | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
index 74bd8e7af98da..6dd68140d5fca 100644
--- a/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
+++ b/llvm/test/Transforms/GVN/pre-coerced-value-not-numbered.ll
@@ -1,10 +1,11 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
; RUN: opt -passes=gvn -S < %s | FileCheck %s
-; Test for #216265: GVN load-PREs %load1, then forwards the i64 store to %conv,
-; so MaterializeAdjustedValue RAUWs a coercing trunc over it without a value
-; number. Scalar PRE on the inserted .pre load's GEP index then looked that
-; trunc up and asserted with "Value not numbered?".
+; Test for #216265. GVN forwards the i64 store to the i8 load (%conv),
+; inserting a coercing `trunc i64 %x to i8` via MaterializeAdjustedValue
+; without assigning it a value number. When scalar PRE later runs on the GEP
+; indices of the PRE'd load, it encounters the unnumbered trunc and asserts
+; in VN.lookup().
@g = external global i64
More information about the llvm-commits
mailing list