I need this patch for other non functional changes I need in order to continue on the frange + sub-ranges work. We can tackle any problems as follow-ups.
Also, I re-tested and included ppc64le-Linux in the mix, along with LAPACK tests, and making sure its Fortran preprocessed files show no changes in assembly output for -O2. Pushed to trunk. Attached is the latest version of the patch I pushed. Happy when hacking. Aldy
>From d284bf3a1349c1bb5e57a71b5d6b46de36f4db13 Mon Sep 17 00:00:00 2001 From: Aldy Hernandez <[email protected]> Date: Wed, 15 Jul 2026 09:16:10 +0000 Subject: [PATCH] [frange] Consolidate signed-zero canonicalization of endpoints The methods frange::set() and frange::flush_denormals_to_zero() canonicalize ranges containing floating point zero endpoints differently. Whereas set() includes both +0.0 and -0.0 in the range, so contains_p([+-]0.0) works, even under -ffast-math, flush_denormals_to_zero() always sets the lower bound of a positive denormal as +0.0, regardless of HONOR_SIGNED_ZEROS. This means that contains_p(-0.0) would theoretically fail under -ffast-math. In reality this last scenario doesn't happen because all users of zero endpoints in the code are special casing signed zeros. Also, building a range with -0.0 would cause the set() canonicalization to kick in, and set the range to [-0.0, +0.0] anyhow. Not only is this fragile at best, but in follow-up patches I'd like to do some cleanup in this area, and one representation across the board is in the spirit of what we've always tried to do with irange: a given range should be representable in only one way (ok, not exactly cause we have irange_bitmask's which complicate things, but we try ;-)). Ultimately I'd like to implement sub-ranges for frange so we can represent inequality with a constant, and tackle a couple of the signed zero PRs in this area. But initially I just want to provide an frange_cmp() comparison function that respects signed zeros, and lets users not have to worry about special casing anything when dealing with endpoints. For example, frange_cmp() will order -0.0 strictly below +0.0, so a membership test becomes just two endpoint comparisons: -0.0 lands outside [+0.0, x] but inside [-0.0, x], and nobody has to first ask "is this a zero, and if so which one?". But that only works if a range always spells its zero endpoints the same way. Today set() spells the lower zero of [0.0, 5.0] as -0.0 under -ffast-math, while flush_denormals_to_zero() spells it +0.0, so one range ends up with two bit patterns, and frange_cmp() would tell you [+0.0, 5.0] is a strict subset of [-0.0, 5.0] even though they are the same range. Once both producers run through canonicalize_zeros() the order is well defined, and when frange_cmp() gets implemented, contains_p(), and the eventual sub-range union/intersect code can just call it and drop their signed-zero special cases entirely. This patch moves the canonicalization into one function that both set() and flush_denormals_to_zero() can share. With it flush_denormals_to_zero() moves the denormal endpoint to the zero on its side and lets canonicalize_zeros() settle the sign, exactly as set() does. I've included a selftest to test this: under -ffast-math, flushing [+DENORM, 5.0] used to yield [+0.0, 5.0], while set() on the same range yields [-0.0, 5.0]. With this patch, flushing also yields [-0.0, 5.0], matching set(). There's no observable change with this patch-- every consumer that reads the sign of a zero endpoint already special-cases !HONOR_SIGNED_ZEROS, so nothing today can tell the two representations apart. Tested on x86-64 Linux with a full bootstrap and regtests. I also tested the LAPACK suite for regressions, and I ran f951 over all the preprocessed Fortran files in LAPACK and made sure there are no changes in the assembly with and without this patch. Also, tested on ppc64le Linux, including LAPACK, and making sure there are no changes in assembly with the preprocessed Fortran files. This is an internal-consistency fix and a prerequisite for upcoming patches which will make the sign of a zero endpoint load-bearing. gcc/ChangeLog: * value-range.cc (frange::flush_denormals_to_zero): Collapse a denormal endpoint to the zero of the same sign and defer the sign to canonicalize_zeros. (frange::canonicalize_zeros): New. (frange::set): Use it. (range_tests_flush_denormals): New. (range_tests_floats): Call it. * value-range.h (class frange): Declare canonicalize_zeros. --- gcc/value-range.cc | 99 +++++++++++++++++++++++++++++++++++----------- gcc/value-range.h | 1 + 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/gcc/value-range.cc b/gcc/value-range.cc index 722eca9abca..9e689ffbc0e 100644 --- a/gcc/value-range.cc +++ b/gcc/value-range.cc @@ -995,17 +995,51 @@ frange::flush_denormals_to_zero () return; machine_mode mode = TYPE_MODE (type ()); - // Flush [x, -DENORMAL] to [x, -0.0]. + + // Flush a denormal endpoint to a zero of the same sign: a +denormal lower + // bound to +0.0, and a -denormal upper bound to -0.0. Then call + // canonicalize_zeros to rewrite the sign to whatever the flags make + // canonical. For example, under !HONOR_SIGNED_ZEROS (-fno-signed-zeros) a + // range reaching zero must hold both signs of it, so: + // + // [ +DENORMAL, 5.0 ] flushes to [ -0.0, 5.0 ] + // + // keeping contains_p (-0.0) true; under HONOR_SIGNED_ZEROS the sign stands and + // it stays [ +0.0, 5.0 ]. if (real_isdenormal (&m_max, mode) && real_isneg (&m_max)) - { - if (HONOR_SIGNED_ZEROS (m_type)) - m_max = dconstm0; - else - m_max = dconst0; - } - // Flush [+DENORMAL, x] to [+0.0, x]. + m_max = dconstm0; if (real_isdenormal (&m_min, mode) && !real_isneg (&m_min)) m_min = dconst0; + canonicalize_zeros (m_min, m_max); +} + +// Canonicalize the signed zeros of the endpoints MIN and MAX according with what +// the target and flags want: +// +// !MODE_HAS_SIGNED_ZEROS: the mode has no signed zero, so any zero is +0.0. +// +// !HONOR_SIGNED_ZEROS: the two zeros are one value, so widen the range to +// include both signs of it. +// +// Otherwise the sign is a real distinction, and we keep it. + +void +frange::canonicalize_zeros (REAL_VALUE_TYPE &min, REAL_VALUE_TYPE &max) +{ + if (!MODE_HAS_SIGNED_ZEROS (TYPE_MODE (m_type))) + { + if (real_iszero (&min, 1)) + min.sign = 0; + if (real_iszero (&max, 1)) + max.sign = 0; + } + else if (!HONOR_SIGNED_ZEROS (m_type)) + { + if (real_iszero (&max, 1)) + max.sign = 0; + if (real_iszero (&min, 0)) + min.sign = 1; + } } // Setter for franges. @@ -1047,20 +1081,7 @@ frange::set (tree type, m_neg_nan = false; } - if (!MODE_HAS_SIGNED_ZEROS (TYPE_MODE (m_type))) - { - if (real_iszero (&m_min, 1)) - m_min.sign = 0; - if (real_iszero (&m_max, 1)) - m_max.sign = 0; - } - else if (!HONOR_SIGNED_ZEROS (m_type)) - { - if (real_iszero (&m_max, 1)) - m_max.sign = 0; - if (real_iszero (&m_min, 0)) - m_min.sign = 1; - } + canonicalize_zeros (m_min, m_max); // For -ffinite-math-only we can drop ranges outside the // representable numbers to min/max for the type. @@ -1507,6 +1528,13 @@ frange::verify_range () const // [ +0.0, -0.0 ] is nonsensical. gcc_checking_assert (!(real_iszero (&m_min, 0) && real_iszero (&m_max, 1))); + // A zero endpoint must carry its canonical sign. Every producer runs + // canonicalize_zeros, so a zero bound can only descend from a canonical one. + if (!MODE_HAS_SIGNED_ZEROS (TYPE_MODE (m_type))) + gcc_checking_assert (!real_iszero (&m_min, 1) && !real_iszero (&m_max, 1)); + else if (!HONOR_SIGNED_ZEROS (m_type)) + gcc_checking_assert (!real_iszero (&m_min, 0) && !real_iszero (&m_max, 1)); + // If all the properties are clear, we better not span the entire // domain, because that would make us varying. if (m_pos_nan && m_neg_nan) @@ -3746,6 +3774,32 @@ range_tests_signbit () ASSERT_TRUE (!r0.signbit_p (signbit)); } +static void +range_tests_flush_denormals () +{ + // We need -0.0 to exist for any of this to mean anything. + if (!MODE_HAS_SIGNED_ZEROS (TYPE_MODE (float_type_node))) + return; + + int save_flag = flag_signed_zeros; + flag_signed_zeros = 0; + + // Flushing a positive denormal lower bound to zero must canonicalize that + // zero to -0.0 to agree with set(). + frange flushed = frange_float ("1e-40", "5"); + flushed.clear_nan (); + flushed.flush_denormals_to_zero (); + + frange built = frange_float ("0", "5"); + built.clear_nan (); + + ASSERT_TRUE (flushed == built); + ASSERT_TRUE (flushed.contains_p (dconstm0)); + ASSERT_TRUE (flushed.contains_p (dconst0)); + + flag_signed_zeros = save_flag; +} + static void range_tests_floats () { @@ -3754,6 +3808,7 @@ range_tests_floats () if (HONOR_NANS (float_type_node)) range_tests_nan (); range_tests_signbit (); + range_tests_flush_denormals (); if (HONOR_SIGNED_ZEROS (float_type_node)) range_tests_signed_zeros (); diff --git a/gcc/value-range.h b/gcc/value-range.h index 307779b37ef..5d547bd7727 100644 --- a/gcc/value-range.h +++ b/gcc/value-range.h @@ -667,6 +667,7 @@ private: bool union_nans (const frange &); bool intersect_nans (const frange &); bool combine_zeros (const frange &, bool union_p); + void canonicalize_zeros (REAL_VALUE_TYPE &, REAL_VALUE_TYPE &); tree m_type; REAL_VALUE_TYPE m_min; -- 2.47.3
