Video codecs spell a clamp of a signed value to [0, M] as
(x & ~M) ? ((-x) >> (prec - 1)) & M : x, which the front end folds to
(unsigned) x > M ? ((-x) >> (prec - 1)) & M : x.  That is never turned
into MIN/MAX, so it if-converts into four vector operations where the
plain x < 0 ? 0 : x > M ? M : x spelling gives smax/smin.

The two forms agree except at x == INT_MIN, where -x is undefined, so
the rule is restricted to types with undefined signed overflow.

This is the neighbour of the conditional narrow clip added for PR127166:
that one narrows to a smaller type, this one clips at the width of x and
takes the mask the idiom applies to the shift.  Spelled on a sub-int type
the idiom does not match, as the negation is then performed on the
promoted int; spelled on a vector it is a VEC_COND_EXPR over VECTOR_CSTs,
which neither cond nor INTEGER_CST matches.

The pending "Saturating arithmetic matching improvements" series (Kyrylo
Tkachov, 2026-09-02) is disjoint: its clamp forms all narrow and end in a
.SAT_* call, and none of them matches the & M mask.  This rule works at
the width of x and feeds them, as 10/13 of that series recognizes the
MAX (MIN (X, M), 0) it leaves.

Bootstrapped and regression tested on x86_64-pc-linux-gnu.

Assisted-by: Claude Opus 5 (Anthropic)

gcc/ChangeLog:

        * match.pd ((unsigned) x > M ? ((-x) >> (prec - 1)) & M : x):
        New simplification.

gcc/testsuite/ChangeLog:

        * gcc.dg/tree-ssa/clip-canon-1.c: New test.
        * gcc.dg/tree-ssa/clip-canon-2.c: New test.
        * gcc.dg/tree-ssa/clip-canon-3.c: New test.
        * gcc.dg/tree-ssa/clip-canon-4.c: New test.

Signed-off-by: Dominic P <[email protected]>
---
v4, addressing the v3 review:

- As Andrew asked, the TREE_SIDE_EFFECTS test is gone: genmatch adds it for the 
conditional
  captures and @0 is unconditional here.
- As Andrew asked, no wi::to_widest is left.  The shift count uses wi::to_wide, 
and the
  threshold and the mask are compared with wi::to_wide at their common
  precision under tree_int_cst_sgn (@5) > 0, which rejects a mask with the
  top bit set -- MIN (x, M) would be wrong for such an M.
- types_match (type, TREE_TYPE (@0)) is gone: a COND_EXPR's arms already
  have its type.
- !TYPE_UNSIGNED (type) is gone: TYPE_OVERFLOW_UNDEFINED (type) implies it.
- wi::to_wide (@1) != 0 is gone: tree_int_cst_sgn (@5) > 0 implies it.
- The capture that only served the overflow tests is gone; both tests are
  now stated on type.
- As Naveen asked, the tests carry dg-require-effective-target int32 for
  the >> 31, and
  follow the directive order of gcc.dg/tree-ssa/pr127166.c.

The rule is otherwise unchanged from v3.

 gcc/match.pd                                 | 16 ++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c | 18 +++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c | 33 ++++++++++++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c | 21 +++++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c | 21 +++++++++++++
 5 files changed, 109 insertions(+)
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c

diff --git a/gcc/match.pd b/gcc/match.pd
index 032f29dcbc2..9f18dcab30f 100644
--- a/gcc/match.pd
+++ b/gcc/match.pd
@@ -13390,6 +13390,22 @@ and,
                 (max @0 { build_zero_cst (TREE_TYPE (@0)); }))
                @1)))))))
 
+/* The clip of PR127166 at the width of X, with the mask spelled out:
+
+     (UT) X > M ? ((-X) >> (PREC - 1)) & M : X  ->  MAX (MIN (X, M), 0).  */
+(simplify
+ (cond (gt (nop_convert@2 @0) INTEGER_CST@1)
+       (bit_and:s (rshift (negate @0) INTEGER_CST@4) INTEGER_CST@5)
+       @0)
+ (if (INTEGRAL_TYPE_P (type)
+      && TYPE_UNSIGNED (TREE_TYPE (@2))
+      && TYPE_OVERFLOW_UNDEFINED (type)
+      && !TYPE_OVERFLOW_SANITIZED (type)
+      && wi::to_wide (@4) == TYPE_PRECISION (type) - 1
+      && tree_int_cst_sgn (@5) > 0
+      && wi::to_wide (@1) == wi::to_wide (@5))
+  (max (min @0 @5) { build_zero_cst (type); })))
+
 #if GIMPLE
 
 /* Include the saturation alu match patterns.  */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c
new file mode 100644
index 00000000000..b2edf7ad61f
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c
@@ -0,0 +1,18 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fdump-tree-optimized" } */
+
+int
+clip_255 (int x)
+{
+  return (x & ~255) ? ((-x) >> 31 & 255) : x;
+}
+
+int
+clip_1023 (int x)
+{
+  return (x & ~1023) ? ((-x) >> 31 & 1023) : x;
+}
+
+/* { dg-final { scan-tree-dump-times "MIN_EXPR" 2 "optimized" } } */
+/* { dg-final { scan-tree-dump-times "MAX_EXPR" 2 "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c
new file mode 100644
index 00000000000..bda43312562
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c
@@ -0,0 +1,33 @@
+/* { dg-do run } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2" } */
+
+#define M 255
+
+/* x == INT_MIN makes -x undefined, so it is not compared here.  */
+__attribute__ ((noipa)) int
+idiom (int x)
+{
+  return (x & ~M) ? ((-x) >> 31 & M) : x;
+}
+
+__attribute__ ((noipa)) int
+clamp (int x)
+{
+  return x < 0 ? 0 : (x > M ? M : x);
+}
+
+int
+main (void)
+{
+  for (int i = -1000; i <= 1000; i++)
+    if (idiom (i) != clamp (i))
+      __builtin_abort ();
+
+  int v[] = { __INT_MAX__, __INT_MAX__ - 1, -__INT_MAX__, -__INT_MAX__ + 1 };
+  for (unsigned i = 0; i < sizeof v / sizeof v[0]; i++)
+    if (idiom (v[i]) != clamp (v[i]))
+      __builtin_abort ();
+
+  return 0;
+}
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c
new file mode 100644
index 00000000000..20f278d5a7b
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c
@@ -0,0 +1,21 @@
+/* { dg-do run } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fwrapv -fdump-tree-optimized" } */
+
+/* With wrapping overflow -INT_MIN is INT_MIN, so the idiom yields 255 at
+   INT_MIN where a clamp yields 0: the fold must not fire.  */
+__attribute__ ((noipa)) int
+idiom (int x)
+{
+  return (x & ~255) ? ((-x) >> 31 & 255) : x;
+}
+
+int
+main (void)
+{
+  if (idiom (-__INT_MAX__ - 1) != 255)
+    __builtin_abort ();
+  return 0;
+}
+
+/* { dg-final { scan-tree-dump-not "MAX_EXPR|MIN_EXPR" "optimized" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c
new file mode 100644
index 00000000000..2db26fd17ad
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c
@@ -0,0 +1,21 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fsanitize=signed-integer-overflow" } */
+/* { dg-additional-options "-fdump-tree-optimized" } */
+
+/* Keep the negation conditional so that it is checked at runtime.  */
+int
+clip_sanitized (int x)
+{
+  return (x & ~255) ? ((-x) >> 31 & 255) : x;
+}
+
+/* An unsigned negation wraps, so this is not a clip.  */
+unsigned int
+clip_unsigned (unsigned int x)
+{
+  return (x & ~255u) ? ((-x) >> 31 & 255u) : x;
+}
+
+/* { dg-final { scan-tree-dump-not "MAX_EXPR|MIN_EXPR" "optimized" } } */
+/* { dg-final { scan-tree-dump {\.UBSAN_CHECK_SUB} "optimized" } } */
-- 
2.55.0

Reply via email to