On Tue, Sep 22, 2026 at 12:22 PM Dominic P <[email protected]> wrote:
>
> (x ^ 2^(N-1)) - 2^(N-1) and "bit N-1 of x set ? x - 2^N : x" are
> sign_extend<N> (x) when x has no bit above bit N - 1 set.  The PR125405
> rule reads that range off an explicit (x & M), so neither spelling
> reaches it when the range comes from x's type instead.
>
> The xor constant must be positive in the xor's own type: as the sign bit
> of a signed N-bit type the conversion sign-extends and the fold loses a
> - 2^N.  MAX_FIXED_MODE_SIZE bounds N: _BitInt(256) at N = 128 folds and
> runs on a 64-bit host, but ICEs arm's handle_cast.

Maybe this should be only done before lowering of bitint ... Or you
not using bitint type for larger ones which is causing the ICE I
think.

>  The cond rule fires
> in phiopt through match_simplify_replacement and costs one convert.
> Vectors decline on INTEGRAL_TYPE_P (type); a ternary with an outer
> conversion on the arms and the unsigned-compare spelling are not
> handled.  All 25 relative-branch sites in PiTubeDirect's 6502
> interpreter fold: -46 branches and -600 B in M6502_run, LTO image.
>
> Bootstrapped and regression tested on x86_64-pc-linux-gnu.
>
> Assisted-by: Claude Opus 5 (Anthropic)
> Signed-off-by: Dominic P <[email protected]>
>
> gcc/ChangeLog:
>
>         * match.pd (`(x ^ CST) + CST2`): New pattern.
>         (`(signed:N) x < 0 ? (T) x + CST : (T) x`): New pattern.
>
> gcc/testsuite/ChangeLog:
>
>         * gcc.dg/tree-ssa/sext-xor-1.c: New test.
>         * gcc.dg/tree-ssa/sext-xor-2.c: New test.
>         * gcc.dg/tree-ssa/sext-cond-1.c: New test.
>         * gcc.dg/tree-ssa/sext-cond-2.c: New test.
>         * gcc.dg/tree-ssa/sext-bitint-1.c: New test.
>         * gcc.dg/tree-ssa/sext-vector-1.c: New test.
>         * gcc.dg/torture/sext-idiom-1.c: New test.
>         * gcc.target/arm/sext-idiom-1.c: New test.
> ---
>  gcc/match.pd                                  |  35 +++++
>  gcc/testsuite/gcc.dg/torture/sext-idiom-1.c   | 125 ++++++++++++++++++
>  gcc/testsuite/gcc.dg/tree-ssa/sext-bitint-1.c |  31 +++++
>  gcc/testsuite/gcc.dg/tree-ssa/sext-cond-1.c   |  31 +++++
>  gcc/testsuite/gcc.dg/tree-ssa/sext-cond-2.c   |  38 ++++++
>  gcc/testsuite/gcc.dg/tree-ssa/sext-vector-1.c |  39 ++++++
>  gcc/testsuite/gcc.dg/tree-ssa/sext-xor-1.c    |  33 +++++
>  gcc/testsuite/gcc.dg/tree-ssa/sext-xor-2.c    |  49 +++++++
>  gcc/testsuite/gcc.target/arm/sext-idiom-1.c   |  47 +++++++
>  9 files changed, 428 insertions(+)
>  create mode 100644 gcc/testsuite/gcc.dg/torture/sext-idiom-1.c
>  create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/sext-bitint-1.c
>  create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/sext-cond-1.c
>  create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/sext-cond-2.c
>  create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/sext-vector-1.c
>  create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/sext-xor-1.c
>  create mode 100644 gcc/testsuite/gcc.dg/tree-ssa/sext-xor-2.c
>  create mode 100644 gcc/testsuite/gcc.target/arm/sext-idiom-1.c
>
> diff --git a/gcc/match.pd b/gcc/match.pd
> index 268bf3f11e5..1290a2c8b54 100644
> --- a/gcc/match.pd
> +++ b/gcc/match.pd
> @@ -5472,6 +5472,41 @@ DEFINE_INT_AND_FLOAT_ROUND_FN (RINT)
>     (if (ntype && type_has_mode_precision_p (ntype))
>      (convert (convert:ntype @0))))))
>
> +/* (x ^ (1 << (N - 1))) - (1 << (N - 1)) -> sign_extend<N> (x), for an x
> +   with no bits above bit N - 1 possibly set.  */
Small comment nit here, let's use CST and then describe what CST is like:
```
(x ^ CST) - CST -> sign_extend<N> (x) where CST is (1 << (N - 1)), for an x
with no bits above bit N - 1 possibly set.
```
As I was originally looking for a shift in the pattern and noticed
there was now.


> +(simplify
> + (plus (convert? (bit_xor @0 INTEGER_CST@1)) INTEGER_CST@2)

I think INTEGER_CST here should be integer_pow2p, otherwise ...

> + (if (INTEGRAL_TYPE_P (type))
> +  (with {
> +    unsigned int prec = TYPE_PRECISION (type);
> +    int width = wi::exact_log2 (wi::to_wide (@1)) + 1;

width will be 0 here so we can skip over those earlier. And it is
easier to notice we are looking for a power of 2 constant.

> +    tree ntype = NULL_TREE;
> +
> +    if (tree_int_cst_sgn (@1) > 0 && width > 0
Then you can move the `width > 0` also.

> +       && width <= MAX_FIXED_MODE_SIZE && width < (int) prec

Trying to understand why you special case MAX_FIXED_MODE_SIZE here
except for > MAX_FIXED_MODE_SIZE, we should use bitint type. And we
should still optimize large bitint types before lowering them anyways.

> +       && wi::to_wide (@2) == -wi::set_bit_in_zero (width - 1, prec)
I am trying to think of a better way of doing this.  than using - with
set_bit_in_zero.  I think we can use shifted_mask with a true operand.


> +       && wi::bit_and_not (wide_int::from (get_nonzero_bits (@0), prec,
> +                                           TYPE_SIGN (TREE_TYPE (@0))),
> +                           wi::mask (width, false, prec)) == 0)
> +      ntype = build_nonstandard_integer_type (width, SIGNED);
> +   }
> +   (if (ntype && (width == 1 || type_has_mode_precision_p (ntype)))
> +    (convert (convert:ntype @0))))))

Why are you special casing only type_has_mode_precision_p here?

> +
> +/* (signed:N) x < 0 ? (T) x - (1 << N) : (T) x -> (T) (signed:N) x, for an
Same comment about the CST here.
> +   x with no bits above bit N - 1 possibly set.  */
> +(simplify
> + (cond (lt (convert@2 @0) integer_zerop) (plus (convert@1 @0) INTEGER_CST@3) 
> @1)
> + (if (INTEGRAL_TYPE_P (type) && INTEGRAL_TYPE_P (TREE_TYPE (@2)))
> +  (with { unsigned int prec = TYPE_PRECISION (type);
> +         unsigned int width = TYPE_PRECISION (TREE_TYPE (@2)); }
> +   (if (width < prec
> +       && wi::to_wide (@3) == -wi::set_bit_in_zero (width, prec)
> +       && wi::bit_and_not (wide_int::from (get_nonzero_bits (@0), prec,
> +                                           TYPE_SIGN (TREE_TYPE (@0))),
> +                           wi::mask (width, false, prec)) == 0)

Similar comments here about set_bit_in_zero .
Also even though you say signed in the comment, you don't check if you
have signed for the lt (yes <0 will be false for unsigned but it is
safer to have it).

> +    (convert @2)))))
> +
>  /* Optimize x >> x into 0 */
>  (simplify
>   (rshift @0 @0)
> diff --git a/gcc/testsuite/gcc.dg/torture/sext-idiom-1.c 
> b/gcc/testsuite/gcc.dg/torture/sext-idiom-1.c
> new file mode 100644
> index 00000000000..9ef27bd37f4
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/torture/sext-idiom-1.c
> @@ -0,0 +1,125 @@
> +/* { dg-do run } */
> +/* { dg-require-effective-target int32 } */
> +
> +typedef unsigned char u8;
> +typedef unsigned short u16;
> +typedef signed char s8;
> +
> +__attribute__((noipa)) static long
> +sext_ref (unsigned long x, volatile int *pn)
> +{
> +  int n = *pn;
> +  volatile unsigned long b = 1ul << (n - 1);
> +  volatile unsigned long v = x & (b + b - 1);
> +  return (v & b) ? (long) v - (long) (b + b) : (long) v;
> +}
> +
> +__attribute__((noipa)) static int
> +xor_sub (int x, volatile int *pm)
> +{
> +  return (x ^ *pm) - *pm;
> +}
> +
> +__attribute__((noipa)) static int
> +xa8 (u8 x)
> +{
> +  return (int) ((x ^ 0x80u) - 0x80u);
> +}
> +
> +__attribute__((noipa)) static int
> +xa16 (u16 x)
> +{
> +  return (int) ((x ^ 0x8000u) - 0x8000u);
> +}
> +
> +/* N = 1: the narrow type is a 1-bit signed type.  */
> +__attribute__((noipa)) static int
> +xa1 (u8 c)
> +{
> +  int x = c >> 7;
> +  return (x ^ 1) - 1;
> +}
> +
> +__attribute__((noipa)) static int
> +xa_signed (s8 x)
> +{
> +  return (int) (s8) (x ^ (s8) 0x80) - 128;
> +}
> +
> +/* The xor is done in a signed type of precision 8 whose operand does fit
> +   8 bits, so only the sign of the xor constant refuses the fold.  */
> +__attribute__((noipa)) static int
> +xa_signed_ranged (int a)
> +{
> +  s8 x = (s8) (a & 0x3f);
> +  s8 t = x ^ (s8) 0x80;
> +  return (int) t - 128;
> +}
> +
> +__attribute__((noipa)) static int
> +xa_unknown (int x)
> +{
> +  return (x ^ 0x80) - 0x80;
> +}
> +
> +__attribute__((noipa)) static int
> +cb8 (u8 x)
> +{
> +  unsigned e = x;
> +  if (e & 0x80)
> +    e -= 0x100;
> +  return (int) e;
> +}
> +
> +__attribute__((noipa)) static int
> +cb16 (u16 x)
> +{
> +  unsigned e = x;
> +  if (e & 0x8000)
> +    e -= 0x10000;
> +  return (int) e;
> +}
> +
> +__attribute__((noipa)) static int
> +cb_wrong_const (u8 x)
> +{
> +  int e = x;
> +  if ((s8) x < 0)
> +    e -= 0x80;
> +  return e;
> +}
> +
> +int
> +main (void)
> +{
> +  volatile int n8 = 8, n16 = 16, m80 = 0x80;
> +  int i;
> +
> +  for (i = 0; i < 256; i++)
> +    {
> +      u8 x = (u8) i;
> +      if (xa8 (x) != sext_ref (x, &n8))
> +       __builtin_abort ();
> +      if (cb8 (x) != sext_ref (x, &n8))
> +       __builtin_abort ();
> +      if (xa1 (x) != -(x >> 7))
> +       __builtin_abort ();
> +      if (xa_signed ((s8) i) != (int) (s8) ((s8) i ^ (s8) 0x80) - 128)
> +       __builtin_abort ();
> +      if (xa_signed_ranged (i) != (i & 0x3f) - 256)
> +       __builtin_abort ();
> +      if (xa_unknown (i - 128) != xor_sub (i - 128, &m80))
> +       __builtin_abort ();
> +      if (cb_wrong_const (x) != (((s8) x < 0) ? (int) x - 0x80 : (int) x))
> +       __builtin_abort ();
> +    }
> +  for (i = 0; i < 65536; i++)
> +    {
> +      u16 x = (u16) i;
> +      if (xa16 (x) != sext_ref (x, &n16))
> +       __builtin_abort ();
> +      if (cb16 (x) != sext_ref (x, &n16))
> +       __builtin_abort ();
> +    }
> +  return 0;
> +}
> diff --git a/gcc/testsuite/gcc.dg/tree-ssa/sext-bitint-1.c 
> b/gcc/testsuite/gcc.dg/tree-ssa/sext-bitint-1.c
> new file mode 100644
> index 00000000000..7bf527ce52a
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/tree-ssa/sext-bitint-1.c
> @@ -0,0 +1,31 @@
> +/* { dg-do compile } */
> +/* { dg-require-effective-target bitint575 } */
> +/* { dg-options "-O2 -fdump-tree-optimized" } */
> +
> +typedef unsigned _BitInt(256) u256;
> +
> +/* N = 8 has an integer mode everywhere, so this folds.  */
> +u256
> +sext8 (u256 x)
> +{
> +  return ((x & 0xff) ^ 0x80) - 0x80;
> +}
> +
> +/* N = 191 has no mode at all.  */
> +u256
> +no_sext_191 (u256 x)
> +{
> +  u256 m = ((u256) 1) << 190;
> +  return ((x & (m + m - 1)) ^ m) - m;
> +}
> +
> +/* N = 128 has a mode only where MAX_FIXED_MODE_SIZE allows it; both ways
> +   round must compile.  */
> +u256
> +sext128 (u256 x)
> +{
> +  u256 m = ((u256) 1) << 127;
> +  return ((x & (m + m - 1)) ^ m) - m;
> +}
> +
> +/* { dg-final { scan-tree-dump-times "\\(signed char\\)" 1 "optimized" } } */
> diff --git a/gcc/testsuite/gcc.dg/tree-ssa/sext-cond-1.c 
> b/gcc/testsuite/gcc.dg/tree-ssa/sext-cond-1.c
> new file mode 100644
> index 00000000000..6e32f7f2f43
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/tree-ssa/sext-cond-1.c
> @@ -0,0 +1,31 @@
> +/* { dg-do compile } */
> +/* { dg-require-effective-target int32 } */
> +/* { dg-options "-O2 -fdump-tree-phiopt2 -fdump-tree-optimized" } */
> +
> +typedef unsigned char u8;
> +typedef unsigned short u16;
> +
> +/* if (x & 0x80) x -= 0x100; is (signed char) x, and the sign test is
> +   already materialised for the branch.  */
> +int
> +sext8 (u8 x)
> +{
> +  unsigned e = x;
> +  if (e & 0x80)
> +    e -= 0x100;
> +  return (int) e;
> +}
> +
> +int
> +sext16 (u16 x)
> +{
> +  unsigned e = x;
> +  if (e & 0x8000)
> +    e -= 0x10000;
> +  return (int) e;
> +}
> +
> +/* { dg-final { scan-tree-dump-not "if \\(" "phiopt2" } } */
> +/* { dg-final { scan-tree-dump-not "PHI <" "phiopt2" } } */
> +/* { dg-final { scan-tree-dump-not "if \\(" "optimized" } } */
> +/* { dg-final { scan-tree-dump-not "PHI <" "optimized" } } */
> diff --git a/gcc/testsuite/gcc.dg/tree-ssa/sext-cond-2.c 
> b/gcc/testsuite/gcc.dg/tree-ssa/sext-cond-2.c
> new file mode 100644
> index 00000000000..17354658f39
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/tree-ssa/sext-cond-2.c
> @@ -0,0 +1,38 @@
> +/* { dg-do compile } */
> +/* { dg-require-effective-target int32 } */
> +/* { dg-options "-O2 -fdump-tree-optimized" } */
> +
> +typedef unsigned char u8;
> +typedef signed char s8;
> +
> +/* An unknown x: 0x100 is not the weight of x's bit 7.  */
> +int
> +no_sext_unknown (int x)
> +{
> +  int e = x;
> +  if (e & 0x80)
> +    e -= 0x100;
> +  return e;
> +}
> +
> +/* The wrong constant: 0x80 is not 2^8.  */
> +int
> +no_sext_wrong_const (u8 x)
> +{
> +  int e = x;
> +  if ((s8) x < 0)
> +    e -= 0x80;
> +  return e;
> +}
> +
> +/* The arms are not the tested value.  */
> +int
> +no_sext_other_value (u8 x, u8 y)
> +{
> +  int e = y;
> +  if ((s8) x < 0)
> +    e -= 0x100;
> +  return e;
> +}
> +
> +/* { dg-final { scan-tree-dump-times "if \\(" 3 "optimized" } } */
> diff --git a/gcc/testsuite/gcc.dg/tree-ssa/sext-vector-1.c 
> b/gcc/testsuite/gcc.dg/tree-ssa/sext-vector-1.c
> new file mode 100644
> index 00000000000..149422e27bd
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/tree-ssa/sext-vector-1.c
> @@ -0,0 +1,39 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O2 -Wno-psabi -fdump-tree-optimized" } */
> +
> +typedef unsigned char v8qi __attribute__((vector_size (8)));
> +typedef unsigned int v4si __attribute__((vector_size (16)));
> +typedef int s4si __attribute__((vector_size (16)));
> +
> +/* Both rules want a scalar integral type and an INTEGER_CST, so a vector
> +   must decline rather than reach get_nonzero_bits (PR127379).  */
> +v4si
> +vx (v4si x)
> +{
> +  v4si m = { 0x80, 0x80, 0x80, 0x80 };
> +  return ((x & (v4si) { 0xff, 0xff, 0xff, 0xff }) ^ m) - m;
> +}
> +
> +s4si
> +vxs (s4si x)
> +{
> +  s4si m = { 0x80, 0x80, 0x80, 0x80 };
> +  return ((x & (s4si) { 0xff, 0xff, 0xff, 0xff }) ^ m) - m;
> +}
> +
> +v8qi
> +vq (v8qi x)
> +{
> +  v8qi m = { 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40 };
> +  v8qi l = { 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f };
> +  return ((x & l) ^ m) - m;
> +}
> +
> +v4si
> +vnu (v4si x)
> +{
> +  v4si m = { 0x80, 0x8000, 0x80, 0x80 };
> +  return (x ^ m) - m;
> +}
> +
> +/* { dg-final { scan-tree-dump-times " \\^ " 4 "optimized" } } */
> diff --git a/gcc/testsuite/gcc.dg/tree-ssa/sext-xor-1.c 
> b/gcc/testsuite/gcc.dg/tree-ssa/sext-xor-1.c
> new file mode 100644
> index 00000000000..06fd081147e
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/tree-ssa/sext-xor-1.c
> @@ -0,0 +1,33 @@
> +/* { dg-do compile } */
> +/* { dg-require-effective-target int32 } */
> +/* { dg-options "-O2 -fdump-tree-forwprop1 -fdump-tree-optimized" } */
> +
> +typedef unsigned char u8;
> +typedef unsigned short u16;
> +
> +/* The narrow type supplies the range, so nonzero_bits has to be consulted:
> +   the PR125405 rule needs both masks in the IL.  */
> +int
> +sext8 (u8 x)
> +{
> +  return (int) ((x ^ 0x80u) - 0x80u);
> +}
> +
> +int
> +sext16 (u16 x)
> +{
> +  return (int) ((x ^ 0x8000u) - 0x8000u);
> +}
> +
> +long
> +sext8_from_mask (unsigned long x)
> +{
> +  unsigned long e = x & 0xff;
> +  return (long) ((e ^ 0x80UL) - 0x80UL);
> +}
> +
> +/* { dg-final { scan-tree-dump-times "\\(signed char\\)" 2 "forwprop1" } } */
> +/* { dg-final { scan-tree-dump-times "\\(signed short\\)" 1 "forwprop1" } } 
> */
> +/* { dg-final { scan-tree-dump-times "\\(signed char\\)" 2 "optimized" } } */
> +/* { dg-final { scan-tree-dump-times "\\(signed short\\)" 1 "optimized" } } 
> */
> +/* { dg-final { scan-tree-dump-not " \\^ " "optimized" } } */
> diff --git a/gcc/testsuite/gcc.dg/tree-ssa/sext-xor-2.c 
> b/gcc/testsuite/gcc.dg/tree-ssa/sext-xor-2.c
> new file mode 100644
> index 00000000000..385ebbe8186
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/tree-ssa/sext-xor-2.c
> @@ -0,0 +1,49 @@
> +/* { dg-do compile } */
> +/* { dg-require-effective-target int32 } */
> +/* { dg-options "-O2 -fdump-tree-optimized" } */
> +
> +typedef unsigned char u8;
> +typedef signed char s8;
> +
> +/* An unknown x: (x ^ 128) - 128 is not a sign-extension.  */
> +int
> +no_sext_unknown (int x)
> +{
> +  return (x ^ 0x80) - 0x80;
> +}
> +
> +/* The xor is done in a signed type of precision 8, so the conversion
> +   sign-extends it and the result is not (signed char) x:  x = 1 gives
> +   -255.  */
> +int
> +no_sext_signed_xor (s8 x)
> +{
> +  return (int) (s8) (x ^ (s8) 0x80) - 128;
> +}
> +
> +/* The same, with a range that does fit 8 bits: here nonzero_bits does not
> +   refuse, only the sign of the xor constant does.  For x in [0, 63] the
> +   value is x - 256.  */
> +int
> +no_sext_signed_xor_ranged (int a)
> +{
> +  s8 x = (s8) (a & 0x3f);
> +  s8 t = x ^ (s8) 0x80;
> +  return (int) t - 128;
> +}
> +
> +/* The two constants do not pair as 2^(N-1) and -2^(N-1).  */
> +int
> +no_sext_bad_pair (u8 x)
> +{
> +  return (int) ((x ^ 0x80u) - 0x40u);
> +}
> +
> +/* Not a narrowing: the result type is 8 bits wide itself.  */
> +int
> +no_sext_not_narrowing (u8 x)
> +{
> +  return (int) (u8) ((x ^ 0x80u) - 0x80u);
> +}
> +
> +/* { dg-final { scan-tree-dump-times " \\^ " 5 "optimized" } } */
> diff --git a/gcc/testsuite/gcc.target/arm/sext-idiom-1.c 
> b/gcc/testsuite/gcc.target/arm/sext-idiom-1.c
> new file mode 100644
> index 00000000000..ccbc1a6a146
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/arm/sext-idiom-1.c
> @@ -0,0 +1,47 @@
> +/* { dg-do compile } */
> +/* { dg-require-effective-target arm_arch_v6_ok } */
> +/* { dg-options "-O2" } */
> +/* { dg-add-options arm_arch_v6 } */
> +/* { dg-final { check-function-bodies "**" "" } } */
> +
> +typedef unsigned short word;
> +
> +/*
> +** cond_spelling:
> +**     sxtb    r0, r0
> +**     bx      lr
> +*/
> +int
> +cond_spelling (unsigned char c)
> +{
> +  unsigned e = c;
> +  if (e & 0x80)
> +    e -= 0x100;
> +  return (int) e;
> +}
> +
> +/*
> +** xor_spelling:
> +**     sxtb    r0, r0
> +**     bx      lr
> +*/
> +int
> +xor_spelling (unsigned char x)
> +{
> +  return (int) ((x ^ 0x80u) - 0x80u);
> +}
> +
> +/* The 6502 relative-branch shape: the sign extension folds into the load,
> +   so neither the mask nor the subtract survives.  */
> +word
> +relative (unsigned char *mem, word pc)
> +{
> +  unsigned ea = mem[pc++];
> +  if (ea & 0x80)
> +    ea -= 0x100;
> +  return (word) (pc + ea);
> +}
> +
> +/* { dg-final { scan-assembler-times "ldrsb" 1 } } */
> +/* { dg-final { scan-assembler-not "#256" } } */
> +/* { dg-final { scan-assembler-not "eor" } } */
> --
> 2.55.0
>

Reply via email to