On Wed, Sep 9, 2026 at 2:32 PM Dominic P <[email protected]> wrote:
>
> 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)
You don't need TREE_SIDE_EFFECTS here as @0 is already unconditional
and the generic generation of genmatch already adds those checks.
> + && 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
You can just use wi::to_wide for the above 2. to_widest is expensive sometimes.
> + && 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
>