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. It carries the same
guards, including no side effects on x. Spelled on a sub-int type the
idiom does not match, as the negation is then performed on the promoted
int.
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]>
---
gcc/match.pd | 19 ++++++++++++
gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c | 17 +++++++++++
gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c | 32 ++++++++++++++++++++
gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c | 20 ++++++++++++
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..641727e172a 100644
--- a/gcc/match.pd
+++ b/gcc/match.pd
@@ -13390,6 +13390,25 @@ 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@3 @0) INTEGER_CST@4) INTEGER_CST@5)
+ @0)
+ (if (!TREE_SIDE_EFFECTS (@0)
+ && INTEGRAL_TYPE_P (type)
+ && !TYPE_UNSIGNED (type)
+ && types_match (type, TREE_TYPE (@0))
+ && TYPE_UNSIGNED (TREE_TYPE (@2))
+ && TYPE_OVERFLOW_UNDEFINED (TREE_TYPE (@3))
+ && !TYPE_OVERFLOW_SANITIZED (TREE_TYPE (@3))
+ && wi::to_widest (@4) == TYPE_PRECISION (type) - 1
+ && wi::to_widest (@1) > 0
+ && wi::to_widest (@1) == wi::to_widest (@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..0d3e125da05
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-1.c
@@ -0,0 +1,17 @@
+/* { dg-do compile } */
+/* { 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..6e620a2a5d1
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-2.c
@@ -0,0 +1,32 @@
+/* { dg-do run } */
+/* { 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..1a94ae1f38c
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/tree-ssa/clip-canon-3.c
@@ -0,0 +1,20 @@
+/* { dg-do run } */
+/* { 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