[gcc r17-2951] match: fold two comparisons of a masked value

Kyrylo Tkachov via Gcc-cvs <[email protected]> Tue, 4 Aug 2026 15:17:33 +0000 (GMT)
Newsgroups gmane.comp.gcc.cvs
Message-ID <[email protected]>
https://gcc.gnu.org/g:1c71996e6e067ab3ee26ae581bef037a77324a1a

commit r17-2951-g1c71996e6e067ab3ee26ae581bef037a77324a1a
Author: Kyrylo Tkachov <[email protected]>
Date:   Wed Jul 29 21:26:55 2026 +0200

    match: fold two comparisons of a masked value
    
      ((X + C1) & LOWMASK) ==/!= C2 -> (X & LOWMASK) ==/!= C2 - C1
      (X & HIGHMASK) >/<= C -> X >/<= (C | ~HIGHMASK)
    
    Only the low bits of the addition survive the first mask, so the constant
    moves to the other side of the comparison and the addition goes away.  A
    comparison constant that does not fit the mask makes the result fixed.
    
    Clearing the low bits rounds the value down, and a rounded value passes a
    relational comparison exactly when the value itself passes it against the
    constant with those bits set, so the second mask goes away too.  The
    other two predicates are already canonicalised into these.
    
      int f (int x) { return (x & -8) > 16; }
    
    aarch64 -O2:
    
      before                          after
        and   w0, w0, -8                cmp   w0, 23
        cmp   w0, 16                    cset  w0, gt
        cset  w0, gt
    
    Two existing tests observe shapes this rewrite now folds.  bic-bitmask-19.c
    scans for "> 1" and the loop guard (n & -16) > 0 becomes n > 15, which the
    regex also matches, so it is anchored on the statement end.  pr68217.c
    relies on the comparison keeping the masked value live, so the value is
    returned instead.
    
    Keep signed additions that trap or carry sanitizer instrumentation.
    Also accept high-mask constants whose discarded low bits are already set.
    
    Bootstrapped and tested on aarch64-none-linux-gnu.
    
    gcc/ChangeLog:
    
            * match.pd (((X + C1) & LOWMASK) ==/!= C2): New simplification.
            ((X & HIGHMASK) >/<= C): Likewise.
    
    gcc/testsuite/ChangeLog:
    
            * gcc.dg/tree-ssa/maskcmp-1.c: New test.
            * gcc.dg/bic-bitmask-19.c: Anchor the comparison scan.
            * gcc.dg/pr68217.c: Return the masked value.
            * gcc.dg/tree-ssa/maskcmp-overflow-1.c: New test.
            * gcc.dg/tree-ssa/maskcmp-overflow-2.c: New test.
    
    Signed-off-by: Kyrylo Tkachov <[email protected]>

Diff:
---
 gcc/match.pd                                       | 28 ++++++++++++++++++++++
 gcc/testsuite/gcc.dg/bic-bitmask-19.c              |  4 +++-
 gcc/testsuite/gcc.dg/pr68217.c                     | 12 +++++++---
 gcc/testsuite/gcc.dg/tree-ssa/maskcmp-1.c          | 23 ++++++++++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/maskcmp-overflow-1.c | 10 ++++++++
 gcc/testsuite/gcc.dg/tree-ssa/maskcmp-overflow-2.c | 10 ++++++++
 6 files changed, 83 insertions(+), 4 deletions(-)

diff --git a/gcc/match.pd b/gcc/match.pd
index 623be4b1b805..344883d6ea4c 100644
--- a/gcc/match.pd
+++ b/gcc/match.pd
@@ -5319,6 +5319,34 @@ DEFINE_INT_AND_FLOAT_ROUND_FN (RINT)
 	 && wi::lshift (wi::to_wide (@0), cand) == wi::to_wide (@2))
      (cmp @1 { build_int_cst (TREE_TYPE (@1), cand); }))))))
 
+/* Fold ((X + C1) & LOWMASK) ==/!= C2 into (X & LOWMASK) ==/!= C2 - C1.
+   Only the low bits of the addition survive the mask, so the constant can
+   move to the other side of the comparison and the addition goes away.  */
+(for cmp (eq ne)
+ (simplify
+  (cmp (bit_and:s (plus:s @0 INTEGER_CST@1) INTEGER_CST@2) INTEGER_CST@3)
+  (if (INTEGRAL_TYPE_P (TREE_TYPE (@0))
+       && !TYPE_OVERFLOW_TRAPS (TREE_TYPE (@0))
+       && !TYPE_OVERFLOW_SANITIZED (TREE_TYPE (@0))
+       && wi::popcount (wi::to_wide (@2) + 1) == 1)
+   (with { wide_int mask = wi::to_wide (@2);
+	   wide_int rhs = (wi::to_wide (@3) - wi::to_wide (@1)) & mask; }
+    (if ((wi::to_wide (@3) & ~mask) == 0)
+     (cmp (bit_and @0 @2)
+	  { wide_int_to_tree (TREE_TYPE (@0), rhs); })
+     { constant_boolean_node (cmp == NE_EXPR, type); })))))
+
+/* Fold (X & HIGHMASK) >/<= C into X >/<= (C | ~HIGHMASK).  Clearing the
+   low bits of X rounds it down, and a rounded value passes the comparison
+   exactly when X passes it against the constant with those bits set.  */
+(for cmp (gt le)
+ (simplify
+  (cmp (bit_and:s @0 INTEGER_CST@1) INTEGER_CST@2)
+  (if (INTEGRAL_TYPE_P (TREE_TYPE (@0))
+       && bitmask_inv_cst_vector_p (@1))
+   (with { wide_int c = wi::to_wide (@2) | wi::bit_not (wi::to_wide (@1)); }
+    (cmp @0 { wide_int_to_tree (TREE_TYPE (@0), c); })))))
+
 /* Fold ((X << C1) & C2) cmp C3 into (X & (C2 >> C1)) cmp (C3 >> C1)
 	((X >> C1) & C2) cmp C3 into (X & (C2 << C1)) cmp (C3 << C1).  */
 (for cmp (ne eq)
diff --git a/gcc/testsuite/gcc.dg/bic-bitmask-19.c b/gcc/testsuite/gcc.dg/bic-bitmask-19.c
index aa139da5c1ed..d1574fbff809 100644
--- a/gcc/testsuite/gcc.dg/bic-bitmask-19.c
+++ b/gcc/testsuite/gcc.dg/bic-bitmask-19.c
@@ -19,6 +19,8 @@ void fun2(uint32_t *x, int n)
 
 #include "bic-bitmask.h"
 
-/* { dg-final { scan-tree-dump-times {>\s* 1} 1 dce7 { target vect_int } } } */
+/* The loop guard (n & -16) > 0 now folds to n > 15, so anchor the scan on
+   the statement end to keep it matching only the comparison under test.  */
+/* { dg-final { scan-tree-dump-times {>\s* 1;} 1 dce7 { target vect_int } } } */
 /* { dg-final { scan-tree-dump-not {&\s* 4294967294} dce7 { target vect_int } } } */
 
diff --git a/gcc/testsuite/gcc.dg/pr68217.c b/gcc/testsuite/gcc.dg/pr68217.c
index 60c80106760d..279fedd86f45 100644
--- a/gcc/testsuite/gcc.dg/pr68217.c
+++ b/gcc/testsuite/gcc.dg/pr68217.c
@@ -1,13 +1,19 @@
 /* { dg-do compile } */
 /* { dg-options "-O2 -fdisable-tree-evrp -fdump-tree-vrp1 -fno-tree-ccp" } */
 
-int foo (void)
+#include <limits.h>
+
+/* Return x so that its range is still exported after the comparison folds
+   away.  (x & LLONG_MIN) < 1 is true for both values x can take, so the
+   test of it no longer keeps x live by itself.  */
+
+long long foo (void)
 {
     volatile int a = -1;
-    long long b = (1LL << (sizeof (b) * 8 - 1)); // LLONG_MIN
+    long long b = LLONG_MIN;
     long long x = (a & b); // x == 0x8000000000000000
     if (x < 1LL) { ; } else { __builtin_abort(); }
-    return 0;
+    return x;
 }
 
 /* { dg-final { scan-tree-dump "\\\[-INF, -INF\\\]\\\[0, 0\\\]" "vrp1" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-1.c b/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-1.c
new file mode 100644
index 000000000000..97025c45b1d5
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-1.c
@@ -0,0 +1,23 @@
+/* { dg-do compile } */
+/* { dg-options "-O2 -fdump-tree-optimized" } */
+
+/* Only the low bits of the addition survive the mask, so the constant
+   moves to the other side of the comparison.  */
+int f1 (unsigned int x) { return ((x + 3u) & 7u) == 5u; }
+int f2 (unsigned int x) { return ((x + 300u) & 255u) != 7u; }
+
+/* The comparison constant does not fit the mask, the result is fixed.  */
+int f3 (unsigned int x) { return ((x + 3u) & 7u) == 9u; }
+
+/* Clearing the low bits rounds down, so the comparison constant can absorb
+   them and the mask goes away.  */
+int f4 (int x) { return (x & -8) > 16; }
+int f5 (int x) { return (x & -8) <= -16; }
+int f6 (unsigned int x) { return (x & 0xfffffff0u) > 100u; }
+
+/* { dg-final { scan-tree-dump-not " \\+ " "optimized" } } */
+/* { dg-final { scan-tree-dump-times " & 7;" 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times " & 255" 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times " > 23" 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times " < -8" 1 "optimized" } } */
+/* { dg-final { scan-tree-dump-times " > 111" 1 "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-overflow-1.c b/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-overflow-1.c
new file mode 100644
index 000000000000..02e6d04bfff3
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-overflow-1.c
@@ -0,0 +1,10 @@
+/* { dg-do compile } */
+/* { dg-options "-O2 -ftrapv -fdump-tree-optimized" } */
+
+int
+f (int x)
+{
+  return ((x + 1) & 7) == 0;
+}
+
+/* { dg-final { scan-tree-dump "\\+ 1" "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-overflow-2.c b/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-overflow-2.c
new file mode 100644
index 000000000000..bacec863f28d
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/maskcmp-overflow-2.c
@@ -0,0 +1,10 @@
+/* { dg-do compile } */
+/* { dg-options "-O2 -fsanitize=signed-integer-overflow -fdump-tree-optimized" } */
+
+int
+f (int x)
+{
+  return ((x + 1) & 7) == 0;
+}
+
+/* { dg-final { scan-tree-dump "UBSAN_CHECK_ADD" "optimized" } } */