[llvm] [InstCombine] Fold ashr((zext (xor X, C)) - 1, 31) to sext(X == C) (PR #228983)
via llvm-commits
llvm-commits at lists.llvm.org
Sun Oct 4 23:38:08 PDT 2026
=?utf-8?b?7Jik7IiY7JiB?= <ohsusu515 at gmail.com>
Message-ID:
In-Reply-To: <llvm.org/llvm/llvm-project/pull/228983 at github.com>
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-llvm-transforms
Author: Suyoung Oh (wimmings)
<details>
<summary>Changes</summary>
Fixes #<!-- -->227763
This adds an InstCombine fold for:
```text
ashr((zext (xor X, C)) - 1, 31)
-> sext(X == C)
```
`X xor C` is zero exactly when `X == C`. Zero-extension followed by
subtracting one and arithmetic shifting by the sign bit produces an all-ones
mask only for that equality case, which is equivalent to sign-extending the
comparison.
Alive2 proof: https://alive2.llvm.org/ce/z/Ff8C2q
Regression tests cover scalar, vector, narrow-width, multi-use, and
non-matching cases.
Tests:
```text
ninja -C build check-llvm-transforms-instcombine
```
Result:
```text
Total Discovered Tests: 1888
Unsupported: 140
Passed: 1748
```
Additional references:
- Godbolt: https://godbolt.org/z/4zM1d89n4
- Compiler Explorer: https://compiler-explorer.com/z/5sechMWMh
---
Full diff: https://github.com/llvm/llvm-project/pull/228983.diff
2 Files Affected:
- (modified) llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp (+4)
- (added) llvm/test/Transforms/InstCombine/ashr-xor-equality-mask.ll (+248)
``````````diff
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
index 97964cebb50b8..dd35bc9ff2970 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
@@ -1910,6 +1910,10 @@ Instruction *InstCombinerImpl::visitAShr(BinaryOperator &I) {
if (match(Op0, m_OneUse(m_c_And(m_Add(m_Value(X), m_AllOnes()),
m_Not(m_Deferred(X))))))
return new SExtInst(Builder.CreateIsNull(X), Ty);
+
+ // ashr (add (zext X), -1), BitWidth-1 --> sext (X == 0)
+ if (match(Op0, m_OneUse(m_Add(m_ZExt(m_Value(X)), m_AllOnes()))))
+ return new SExtInst(Builder.CreateIsNull(X), Ty);
}
const APInt *MulC;
diff --git a/llvm/test/Transforms/InstCombine/ashr-xor-equality-mask.ll b/llvm/test/Transforms/InstCombine/ashr-xor-equality-mask.ll
new file mode 100644
index 0000000000000..0979492a9a76d
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/ashr-xor-equality-mask.ll
@@ -0,0 +1,248 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt < %s -passes=instcombine -S | FileCheck %s
+
+declare void @use_i8(i8)
+declare void @use_i32(i32)
+
+; https://github.com/llvm/llvm-project/issues/227763
+define i32 @xor_eq_mask(i8 %x) {
+; CHECK-LABEL: define i32 @xor_eq_mask(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq i8 [[X]], 3
+; CHECK-NEXT: [[MASK:%.*]] = sext i1 [[TMP0]] to i32
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %xor = xor i8 %x, 3
+ %zext = zext i8 %xor to i32
+ %sub = add nsw i32 %zext, -1
+ %mask = ashr i32 %sub, 31
+ ret i32 %mask
+}
+
+; Check that the fold is not hard-coded specifically for C = 3.
+define i32 @xor_eq_mask_other_constant(i8 %x) {
+; CHECK-LABEL: define i32 @xor_eq_mask_other_constant(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq i8 [[X]], 42
+; CHECK-NEXT: [[MASK:%.*]] = sext i1 [[TMP0]] to i32
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %xor = xor i8 %x, 42
+ %zext = zext i8 %xor to i32
+ %sub = add nsw i32 %zext, -1
+ %mask = ashr i32 %sub, 31
+ ret i32 %mask
+}
+
+; The add cannot overflow because zext produces a value in [0, 255],
+; so the fold should not require an explicit nsw flag.
+define i32 @xor_eq_mask_no_nsw(i8 %x) {
+; CHECK-LABEL: define i32 @xor_eq_mask_no_nsw(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq i8 [[X]], 3
+; CHECK-NEXT: [[MASK:%.*]] = sext i1 [[TMP0]] to i32
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %xor = xor i8 %x, 3
+ %zext = zext i8 %xor to i32
+ %sub = add i32 %zext, -1
+ %mask = ashr i32 %sub, 31
+ ret i32 %mask
+}
+
+; The xor has another use, but folding the mask is still profitable.
+define i32 @xor_eq_mask_xor_multi_use(i8 %x) {
+; CHECK-LABEL: define i32 @xor_eq_mask_xor_multi_use(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[XOR:%.*]] = xor i8 [[X]], 3
+; CHECK-NEXT: call void @use_i8(i8 [[XOR]])
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq i8 [[X]], 3
+; CHECK-NEXT: [[MASK:%.*]] = sext i1 [[TMP0]] to i32
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %xor = xor i8 %x, 3
+ call void @use_i8(i8 %xor)
+ %zext = zext i8 %xor to i32
+ %sub = add nsw i32 %zext, -1
+ %mask = ashr i32 %sub, 31
+ ret i32 %mask
+}
+
+; The add has another use. Replacing only the ashr could increase the
+; instruction count, so this checks the profitability restriction.
+define i32 @xor_eq_mask_sub_multi_use(i8 %x) {
+; CHECK-LABEL: define i32 @xor_eq_mask_sub_multi_use(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[XOR:%.*]] = xor i8 [[X]], 3
+; CHECK-NEXT: [[ZEXT:%.*]] = zext i8 [[XOR]] to i32
+; CHECK-NEXT: [[SUB:%.*]] = add nsw i32 [[ZEXT]], -1
+; CHECK-NEXT: call void @use_i32(i32 [[SUB]])
+; CHECK-NEXT: [[MASK:%.*]] = ashr i32 [[SUB]], 31
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %xor = xor i8 %x, 3
+ %zext = zext i8 %xor to i32
+ %sub = add nsw i32 %zext, -1
+ call void @use_i32(i32 %sub)
+ %mask = ashr i32 %sub, 31
+ ret i32 %mask
+}
+
+; Negative test: shifting by 7 does not produce only 0 or -1.
+define i32 @xor_eq_mask_wrong_shift(i8 %x) {
+; CHECK-LABEL: define i32 @xor_eq_mask_wrong_shift(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[XOR:%.*]] = xor i8 [[X]], 3
+; CHECK-NEXT: [[ZEXT:%.*]] = zext i8 [[XOR]] to i32
+; CHECK-NEXT: [[SUB:%.*]] = add nsw i32 [[ZEXT]], -1
+; CHECK-NEXT: [[MASK:%.*]] = ashr i32 [[SUB]], 7
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %xor = xor i8 %x, 3
+ %zext = zext i8 %xor to i32
+ %sub = add nsw i32 %zext, -1
+ %mask = ashr i32 %sub, 7
+ ret i32 %mask
+}
+
+; Negative test: subtracting 2 detects more than just X == 3.
+define i32 @xor_eq_mask_wrong_addend(i8 %x) {
+; CHECK-LABEL: define i32 @xor_eq_mask_wrong_addend(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[XOR:%.*]] = xor i8 [[X]], 2
+; CHECK-NEXT: [[ZEXT:%.*]] = zext i8 [[XOR]] to i32
+; CHECK-NEXT: [[SUB:%.*]] = add nsw i32 [[ZEXT]], -2
+; CHECK-NEXT: [[MASK:%.*]] = ashr i32 [[SUB]], 31
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %xor = xor i8 %x, 3
+ %zext = zext i8 %xor to i32
+ %sub = add nsw i32 %zext, -2
+ %mask = ashr i32 %sub, 31
+ ret i32 %mask
+}
+
+; Record the behavior for a splat vector.
+define <2 x i32> @xor_eq_mask_vec_splat(<2 x i8> %x) {
+; CHECK-LABEL: define <2 x i32> @xor_eq_mask_vec_splat(
+; CHECK-SAME: <2 x i8> [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq <2 x i8> [[X]], splat (i8 3)
+; CHECK-NEXT: [[MASK:%.*]] = sext <2 x i1> [[TMP0]] to <2 x i32>
+; CHECK-NEXT: ret <2 x i32> [[MASK]]
+;
+entry:
+ %xor = xor <2 x i8> %x, <i8 3, i8 3>
+ %zext = zext <2 x i8> %xor to <2 x i32>
+ %sub = add nsw <2 x i32> %zext, <i32 -1, i32 -1>
+ %mask = ashr <2 x i32> %sub, <i32 31, i32 31>
+ ret <2 x i32> %mask
+}
+
+; Test the core fold without an xor.
+define i32 @zext_zero_mask(i8 %x) {
+; CHECK-LABEL: define i32 @zext_zero_mask(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq i8 [[X]], 0
+; CHECK-NEXT: [[MASK:%.*]] = sext i1 [[TMP0]] to i32
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %zext = zext i8 %x to i32
+ %sub = add i32 %zext, -1
+ %mask = ashr i32 %sub, 31
+ ret i32 %mask
+}
+
+define i8 @zext_zero_mask_i4_i8(i4 %x) {
+; CHECK-LABEL: define i8 @zext_zero_mask_i4_i8(
+; CHECK-SAME: i4 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq i4 [[X]], 0
+; CHECK-NEXT: [[MASK:%.*]] = sext i1 [[TMP0]] to i8
+; CHECK-NEXT: ret i8 [[MASK]]
+;
+entry:
+ %zext = zext i4 %x to i8
+ %sub = add i8 %zext, -1
+ %mask = ashr i8 %sub, 7
+ ret i8 %mask
+}
+
+define i8 @zext_zero_mask_i7_i8(i7 %x) {
+; CHECK-LABEL: define i8 @zext_zero_mask_i7_i8(
+; CHECK-SAME: i7 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq i7 [[X]], 0
+; CHECK-NEXT: [[MASK:%.*]] = sext i1 [[TMP0]] to i8
+; CHECK-NEXT: ret i8 [[MASK]]
+;
+entry:
+ %zext = zext i7 %x to i8
+ %sub = add i8 %zext, -1
+ %mask = ashr i8 %sub, 7
+ ret i8 %mask
+}
+
+define <2 x i32> @zext_zero_mask_vec(<2 x i8> %x) {
+; CHECK-LABEL: define <2 x i32> @zext_zero_mask_vec(
+; CHECK-SAME: <2 x i8> [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq <2 x i8> [[X]], zeroinitializer
+; CHECK-NEXT: [[MASK:%.*]] = sext <2 x i1> [[TMP0]] to <2 x i32>
+; CHECK-NEXT: ret <2 x i32> [[MASK]]
+;
+entry:
+ %zext = zext <2 x i8> %x to <2 x i32>
+ %sub = add <2 x i32> %zext, <i32 -1, i32 -1>
+ %mask = ashr <2 x i32> %sub, <i32 31, i32 31>
+ ret <2 x i32> %mask
+}
+
+define i8 @zext_zero_mask_i1_i8(i1 %x) {
+; CHECK-LABEL: define i8 @zext_zero_mask_i1_i8(
+; CHECK-SAME: i1 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[NOT_X:%.*]] = xor i1 [[X]], true
+; CHECK-NEXT: [[SUB:%.*]] = sext i1 [[NOT_X]] to i8
+; CHECK-NEXT: ret i8 [[SUB]]
+;
+entry:
+ %zext = zext i1 %x to i8
+ %sub = add i8 %zext, -1
+ %mask = ashr i8 %sub, 7
+ ret i8 %mask
+}
+
+define i32 @zext_zero_mask_zext_multi_use(i8 %x) {
+; CHECK-LABEL: define i32 @zext_zero_mask_zext_multi_use(
+; CHECK-SAME: i8 [[X:%.*]]) {
+; CHECK-NEXT: [[ENTRY:.*:]]
+; CHECK-NEXT: [[ZEXT:%.*]] = zext i8 [[X]] to i32
+; CHECK-NEXT: call void @use_i32(i32 [[ZEXT]])
+; CHECK-NEXT: [[TMP0:%.*]] = icmp eq i8 [[X]], 0
+; CHECK-NEXT: [[MASK:%.*]] = sext i1 [[TMP0]] to i32
+; CHECK-NEXT: ret i32 [[MASK]]
+;
+entry:
+ %zext = zext i8 %x to i32
+ call void @use_i32(i32 %zext)
+ %sub = add i32 %zext, -1
+ %mask = ashr i32 %sub, 31
+ ret i32 %mask
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/228983
More information about the llvm-commits
mailing list