On Thu, Sep 10, 2026 at 2:17 AM 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. 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.
I am now known as Andrea. Please use my correct name.
> - 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); })))
Can you add a le version too?
And a few long form testcases. That is something like, these should
all happen at phiopt1 (we don't sink expressions until late in the
optimization pipeline):
```
int
clip_255_lt (int x)
{
int const = 255;
unsigned t = x;
int tt = -x;
tt = tt >> 31;
tt &= const;
if (t < const)
return tt;
return x;
}
int
clip_255_ge (int x)
{
int const = 255;
unsigned t = x;
int tt = -x;
tt = tt >> 31;
tt &= const;
if (t >= const)
return x;
return tt;
}
int
clip_255_and (int x)
{
int const = 255;
int t = x & const;
int tt = -x;
tt = tt >> 31;
tt &= const;
if (t)
return tt;
return x;
}
```
> +
> #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
>