[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