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.

The threshold may be M or M - 1.  The masked arm is M for every x > 0,
so at x == M it is M, as the clamp is; (unsigned) x > M - 1 therefore
selects the same values as (unsigned) x > M.  M - 1 is how the
(unsigned) x >= M spelling arrives, since >= and < against a positive
constant are canonicalized to > and <= against the constant less one.
The <= spelling with the arms exchanged reaches this pattern as the >
form: fold_ternary_loc swaps the arms of a COND_EXPR on GENERIC and
phiopt retries every match with the inverted comparison on GIMPLE.

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 performed on the
promoted int, and a mask of 1 does not reach it either, since
((-x) >> (prec - 1)) & 1 is rewritten to (unsigned) x >> (prec - 1)
first.  Spelled on a vector the masked arm is rewritten by the
(-x >> C) fold to a compare mask before any cond pattern runs, so only
vector types wider than the target supports would reach this rule;
those are left alone.  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; this rule works at the width
of x.

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.
        * gcc.dg/tree-ssa/clip-canon-5.c: New test.
        * gcc.dg/tree-ssa/clip-canon-6.c: New test.

Signed-off-by: Dominic P <[email protected]>
---
Andrea -- apologies for getting your name wrong in the v4 notes.

v5, addressing the v4 review:

- Long-form tests: gcc.dg/tree-ssa/clip-canon-5.c has the gt, ge, le and
  lt spellings of the test and the (x & ~255) != 0 spelling, mask
  computed before the branch, and scans phiopt1 for 5 MIN_EXPR, 5 MAX_EXPR
  and no remaining PHI.  All five fold there with this patch; the ge and
  lt ones did not fold with v4.

- The rule now also accepts a threshold of M - 1, which is what ge and lt
  are canonicalized to before any pattern sees them (the mask is M at
  x == M, so M - 1 selects the same values; M - 2 does not).  That is
  the whole change to the rule.

- I did not add a separate le pattern because I could not make one fire:
  fold_ternary_loc swaps the arms on GENERIC and phiopt retries the
  inverted comparison on GIMPLE, so the le form reaches the gt pattern
  on both paths.  A cc1 with an explicit le pattern gives dumps
  identical to one without, for every spelling in the new tests.  If you
  would rather have it written out anyway, I will add it.

- I took your three functions as the gt, ge/lt and (x & ~255) != 0
  spellings and wrote them out in clip-canon-5.c: clip_ge and clip_lt are
  the (unsigned) x >= 255 ? mask : x reading, which is exactly the M - 1
  threshold v5 adds, and clip_bit_and is the (x & ~255) != 0 one.  The
  two literal spellings are kept in clip-canon-6.c as must-not-fold cases,
  since x & 255 and the reversed arms each compute a different function
  (x & 255 returns 256 at x == 256).  If you meant either literally, say
  so and I will follow.

- Two pending series touch the same ground and neither pre-empts this:
  Kyrylo's saturating-arithmetic v3 (5/9 uses the same M or M - 1
  threshold reading for its widened clamps; 9/9 extends the narrow clip
  this rule sits next to) and wangjue's "vect: Look through promotions
  for SAT_TRUNC" v2, whose testcase is the x & ~255 ? (-x) >> 31 : x
  spelling without the & M arm.

- clip-canon-2.c, the execution test, gains the >= spelling.

Bootstrapped and regression tested on x86_64-pc-linux-gnu; aarch64.exp
and vect.exp under qemu on aarch64-none-linux-gnu: no PASS lost, no new
FAIL.

 gcc/match.pd                                 | 20 ++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c | 18 +++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c | 40 +++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c | 21 ++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-4.c | 21 ++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c | 70 ++++++++++++++++++++
 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c | 33 +++++++++
 7 files changed, 223 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
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c
 create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c

diff --git a/gcc/match.pd b/gcc/match.pd
index 032f29dcbc2..9fc74fb095f 100644
--- a/gcc/match.pd
+++ b/gcc/match.pd
@@ -13390,6 +13390,26 @@ 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).
+
+   The mask is M at X == M, so a threshold of M - 1, which is how
+   (UT) X >= M is canonicalized, selects the same values.  */
+(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)
+         || wi::to_wide (@1) + 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..00edccdf7f3
--- /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..233fde07267
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c
@@ -0,0 +1,40 @@
+/* { 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;
+}
+
+/* The threshold M - 1 the (unsigned) x >= M spelling is folded to.  */
+__attribute__ ((noipa)) int
+idiom_ge (int x)
+{
+  return (unsigned) 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) || idiom_ge (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]) || idiom_ge (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..c1876653eaf
--- /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..19116ed568f
--- /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" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c
new file mode 100644
index 00000000000..3b9e76b0463
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-5.c
@@ -0,0 +1,70 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fdump-tree-phiopt1" } */
+
+/* The long form of the idiom: the mask is computed before the branch, so
+   the clip has to be recognized while the compare is still a branch.  */
+
+int
+clip_gt (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t > 255)
+    return tt;
+  return x;
+}
+
+int
+clip_ge (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t >= 255)
+    return tt;
+  return x;
+}
+
+int
+clip_le (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t <= 255)
+    return x;
+  return tt;
+}
+
+int
+clip_lt (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t < 255)
+    return x;
+  return tt;
+}
+
+int
+clip_bit_and (int x)
+{
+  int t = x & ~255;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t)
+    return tt;
+  return x;
+}
+
+/* { dg-final { scan-tree-dump-times "MIN_EXPR " 5 "phiopt1" } } */
+/* { dg-final { scan-tree-dump-times "MAX_EXPR " 5 "phiopt1" } } */
+/* { dg-final { scan-tree-dump-not "PHI <" "phiopt1" } } */
diff --git a/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c 
b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c
new file mode 100644
index 00000000000..44dcd604f8c
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-6.c
@@ -0,0 +1,33 @@
+/* { dg-do compile } */
+/* { dg-require-effective-target int32 } */
+/* { dg-options "-O2 -fdump-tree-phiopt1" } */
+
+/* X & 255 tests the bits the idiom keeps, not the ones it saturates on:
+   at X == 256 it is zero, so this returns 256 where a clip returns 255.  */
+int
+not_a_clip_bit_and (int x)
+{
+  int t = x & 255;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t)
+    return tt;
+  return x;
+}
+
+/* The arms the other way round: this returns 255 for X == 100.  */
+int
+not_a_clip_inverted (int x)
+{
+  unsigned t = x;
+  int tt = -x;
+  tt = tt >> 31;
+  tt &= 255;
+  if (t < 255)
+    return tt;
+  return x;
+}
+
+/* { dg-final { scan-tree-dump-not "MIN_EXPR |MAX_EXPR " "phiopt1" } } */
+/* { dg-final { scan-tree-dump-times "PHI <" 2 "phiopt1" } } */
-- 
2.55.0

Reply via email to