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() total order
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.
This is an internal-consistency fix and a prerequisite for upcoming
patches
which will make the sign of a zero endpoint load-bearing.
I've been away for a while and my mind is probably mush, so I could
appreciate a
second set of eyes before I push this.
From 9dd937d1df25405a6aaa1bd39129edaa276a583b Mon Sep 17 00:00:00 2001
From: Aldy Hernandez <[email protected]>
Date: Wed, 15 Jul 2026 09:16:10 +0000
Subject: [PATCH 1/5] [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() total order
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.
This is an internal-consistency fix and a prerequisite for upcoming patches
which will make the sign of a zero endpoint load-bearing.
I've been away for a while and my mind is probably mush, so I could appreciate a
second set of eyes before I push this.
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 | 92 +++++++++++++++++++++++++++++++++++-----------
gcc/value-range.h | 1 +
2 files changed, 71 insertions(+), 22 deletions(-)
diff --git a/gcc/value-range.cc b/gcc/value-range.cc
index 722eca9abca..7c7e34cf829 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 LO and HI 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 &lo, REAL_VALUE_TYPE &hi)
+{
+ if (!MODE_HAS_SIGNED_ZEROS (TYPE_MODE (m_type)))
+ {
+ if (real_iszero (&lo, 1))
+ lo.sign = 0;
+ if (real_iszero (&hi, 1))
+ hi.sign = 0;
+ }
+ else if (!HONOR_SIGNED_ZEROS (m_type))
+ {
+ if (real_iszero (&hi, 1))
+ hi.sign = 0;
+ if (real_iszero (&lo, 0))
+ lo.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.
@@ -3746,6 +3767,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 +3801,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 e6dc59eb706..5a2f8a0fcb9 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.53.0