From 2f00cfa8e12d6e3d4f608cd3e076ba39b74f05c6 Mon Sep 17 00:00:00 2001 From: Andrew Radcliffe Date: Mon, 23 Oct 2023 14:41:46 -0700 Subject: [PATCH 1/8] `TotalOrder` trait for floating point numbers Define an orthogonal trait which corresponds to the `totalOrder` predicate the IEEE 754 (2008 revision) floating point standard. In order to maintain coherence, the bounds on `TotalOrder` should most likely be `TotalOrder: Float` (or `TotalOrder: FloatCore`). Without type constraints, `TotalOrder` could be defined on, well, anything. Though slightly ugly, one way to deal with this is to define two traits, `TotalOrderCore: FloatCore` and `TotalOrder: Float`. On the other hand, `Inv` has no such constraints (due to the possibility of a meaningful implementation on rational numbers). --- src/float.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 115 insertions(+) diff --git a/src/float.rs b/src/float.rs index 87f8387..a549438 100644 --- a/src/float.rs +++ b/src/float.rs @@ -2210,6 +2210,74 @@ float_const_impl! { SQRT_2, } +/// Trait for floating point numbers that provide an implementation +/// of the `totalOrder` predicate as defined in the IEEE 754 (2008 revision) +/// floating point standard. +pub trait TotalOrder { + /// Return the ordering between `self` and `other`. + /// + /// Unlike the standard partial comparison between floating point numbers, + /// this comparison always produces an ordering in accordance to + /// the `totalOrder` predicate as defined in the IEEE 754 (2008 revision) + /// floating point standard. The values are ordered in the following sequence: + /// + /// - negative quiet NaN + /// - negative signaling NaN + /// - negative infinity + /// - negative numbers + /// - negative subnormal numbers + /// - negative zero + /// - positive zero + /// - positive subnormal numbers + /// - positive numbers + /// - positive infinity + /// - positive signaling NaN + /// - positive quiet NaN. + /// + /// The ordering established by this function does not always agree with the + /// [`PartialOrd`] and [`PartialEq`] implementations. For example, + /// they consider negative and positive zero equal, while `total_cmp` + /// doesn't. + /// + /// The interpretation of the signaling NaN bit follows the definition in + /// the IEEE 754 standard, which may not match the interpretation by some of + /// the older, non-conformant (e.g. MIPS) hardware implementations. + /// + /// # Examples + /// ``` + /// use num_traits::float::TotalOrder; + /// use std::{f32, f64}; + /// + /// fn check_eq(x: T, y: T) { + /// assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Equal); + /// } + /// + /// check_eq(f64::NAN, f64::NAN); + /// check_eq(f32::NAN, f32::NAN); + /// + /// fn check_lt(x: T, y: T) { + /// assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Less); + /// } + /// + /// check_lt(-f64::NAN, f64::NAN); + /// check_lt(f64::INFINITY, f64::NAN); + /// check_lt(-0.0_f64, 0.0_f64); + /// ``` + fn total_cmp(&self, other: &Self) -> std::cmp::Ordering; +} +macro_rules! totalorder_impl { + ($T:ident) => { + impl TotalOrder for $T { + #[inline] + fn total_cmp(&self, other: &Self) -> std::cmp::Ordering { + Self::total_cmp(&self, other) + } + } + }; +} +totalorder_impl!(f64); +totalorder_impl!(f32); + #[cfg(test)] mod tests { use core::f64::consts; @@ -2341,4 +2409,50 @@ mod tests { test_subnormal::(); test_subnormal::(); } + + #[test] + fn total_cmp() { + use crate::float::{Float, TotalOrder}; + fn check_eq(x: T, y: T) { + assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Equal); + } + fn check_lt(x: T, y: T) { + assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Less); + } + fn check_gt(x: T, y: T) { + assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Greater); + } + + check_eq(f64::NAN, f64::NAN); + check_eq(f32::NAN, f32::NAN); + + check_lt(-0.0_f64, 0.0_f64); + check_lt(-0.0_f32, 0.0_f32); + + let s_nan = unsafe { std::mem::transmute::(0x7ff4000000000000) }; + let q_nan = unsafe { std::mem::transmute::(0x7ff8000000000000) }; + check_lt(s_nan, q_nan); + + let neg_s_nan = unsafe { std::mem::transmute::(0xfff4000000000000) }; + let neg_q_nan = unsafe { std::mem::transmute::(0xfff8000000000000) }; + check_lt(neg_q_nan, neg_s_nan); + + let s_nan = unsafe { std::mem::transmute::(0x7fa00000) }; + let q_nan = unsafe { std::mem::transmute::(0x7fc00000) }; + check_lt(s_nan, q_nan); + + let neg_s_nan = unsafe { std::mem::transmute::(0xffa00000) }; + let neg_q_nan = unsafe { std::mem::transmute::(0xffc00000) }; + check_lt(neg_q_nan, neg_s_nan); + + check_lt(-f64::NAN, f64::NEG_INFINITY); + check_gt(1.0_f64, -f64::NAN); + check_lt(f64::INFINITY, f64::NAN); + check_gt(f64::NAN, 1.0_f64); + + check_lt(-f32::NAN, f32::NEG_INFINITY); + check_gt(1.0_f32, -f32::NAN); + check_lt(f32::INFINITY, f32::NAN); + check_gt(f32::NAN, 1.0_f32); + } } diff --git a/src/lib.rs b/src/lib.rs index 54dab6e..821908e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub use crate::bounds::Bounded; #[cfg(any(feature = "std", feature = "libm"))] pub use crate::float::Float; pub use crate::float::FloatConst; +pub use crate::float::TotalOrder; // pub use real::{FloatCore, Real}; // NOTE: Don't do this, it breaks `use num_traits::*;`. pub use crate::cast::{cast, AsPrimitive, FromPrimitive, NumCast, ToPrimitive}; pub use crate::identities::{one, zero, One, Zero}; From 2a76a8dd93d7c1abb4b0a0d0f3b43f78509b9f8b Mon Sep 17 00:00:00 2001 From: Andrew Radcliffe Date: Wed, 25 Oct 2023 18:00:53 -0700 Subject: [PATCH 2/8] Limit exposure --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 821908e..54dab6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,6 @@ pub use crate::bounds::Bounded; #[cfg(any(feature = "std", feature = "libm"))] pub use crate::float::Float; pub use crate::float::FloatConst; -pub use crate::float::TotalOrder; // pub use real::{FloatCore, Real}; // NOTE: Don't do this, it breaks `use num_traits::*;`. pub use crate::cast::{cast, AsPrimitive, FromPrimitive, NumCast, ToPrimitive}; pub use crate::identities::{one, zero, One, Zero}; From 621b1b79cfe13c11c529ba460f27640417288dc9 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Thu, 26 Oct 2023 10:52:08 -0700 Subject: [PATCH 3/8] Use Ordering from core where appropriate --- src/float.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/float.rs b/src/float.rs index a549438..cbdc768 100644 --- a/src/float.rs +++ b/src/float.rs @@ -1,3 +1,4 @@ +use core::cmp::Ordering; use core::num::FpCategory; use core::ops::{Add, Div, Neg}; @@ -2246,30 +2247,31 @@ pub trait TotalOrder { /// # Examples /// ``` /// use num_traits::float::TotalOrder; + /// use std::cmp::Ordering; /// use std::{f32, f64}; /// /// fn check_eq(x: T, y: T) { - /// assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Equal); + /// assert_eq!(x.total_cmp(&y), Ordering::Equal); /// } /// /// check_eq(f64::NAN, f64::NAN); /// check_eq(f32::NAN, f32::NAN); /// /// fn check_lt(x: T, y: T) { - /// assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Less); + /// assert_eq!(x.total_cmp(&y), Ordering::Less); /// } /// /// check_lt(-f64::NAN, f64::NAN); /// check_lt(f64::INFINITY, f64::NAN); /// check_lt(-0.0_f64, 0.0_f64); /// ``` - fn total_cmp(&self, other: &Self) -> std::cmp::Ordering; + fn total_cmp(&self, other: &Self) -> Ordering; } macro_rules! totalorder_impl { ($T:ident) => { impl TotalOrder for $T { #[inline] - fn total_cmp(&self, other: &Self) -> std::cmp::Ordering { + fn total_cmp(&self, other: &Self) -> Ordering { Self::total_cmp(&self, other) } } @@ -2413,14 +2415,17 @@ mod tests { #[test] fn total_cmp() { use crate::float::{Float, TotalOrder}; + use core::cmp::Ordering; + use core::{f32, f64}; + fn check_eq(x: T, y: T) { - assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Equal); + assert_eq!(x.total_cmp(&y), Ordering::Equal); } fn check_lt(x: T, y: T) { - assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Less); + assert_eq!(x.total_cmp(&y), Ordering::Less); } fn check_gt(x: T, y: T) { - assert_eq!(x.total_cmp(&y), std::cmp::Ordering::Greater); + assert_eq!(x.total_cmp(&y), Ordering::Greater); } check_eq(f64::NAN, f64::NAN); From ec24ea24f43e7794101974e1ddac368f05f5ab8e Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Thu, 26 Oct 2023 11:00:01 -0700 Subject: [PATCH 4/8] Use {float}::from_bits instead of mem::transmute --- src/float.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/float.rs b/src/float.rs index cbdc768..e34d7ed 100644 --- a/src/float.rs +++ b/src/float.rs @@ -2434,20 +2434,20 @@ mod tests { check_lt(-0.0_f64, 0.0_f64); check_lt(-0.0_f32, 0.0_f32); - let s_nan = unsafe { std::mem::transmute::(0x7ff4000000000000) }; - let q_nan = unsafe { std::mem::transmute::(0x7ff8000000000000) }; + let s_nan = f64::from_bits(0x7ff4000000000000); + let q_nan = f64::from_bits(0x7ff8000000000000); check_lt(s_nan, q_nan); - let neg_s_nan = unsafe { std::mem::transmute::(0xfff4000000000000) }; - let neg_q_nan = unsafe { std::mem::transmute::(0xfff8000000000000) }; + let neg_s_nan = f64::from_bits(0xfff4000000000000); + let neg_q_nan = f64::from_bits(0xfff8000000000000); check_lt(neg_q_nan, neg_s_nan); - let s_nan = unsafe { std::mem::transmute::(0x7fa00000) }; - let q_nan = unsafe { std::mem::transmute::(0x7fc00000) }; + let s_nan = f32::from_bits(0x7fa00000); + let q_nan = f32::from_bits(0x7fc00000); check_lt(s_nan, q_nan); - let neg_s_nan = unsafe { std::mem::transmute::(0xffa00000) }; - let neg_q_nan = unsafe { std::mem::transmute::(0xffc00000) }; + let neg_s_nan = f32::from_bits(0xffa00000); + let neg_q_nan = f32::from_bits(0xffc00000); check_lt(neg_q_nan, neg_s_nan); check_lt(-f64::NAN, f64::NEG_INFINITY); From a4c94352d30c75bf7b01aad1f173cf798f1f467e Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Thu, 26 Oct 2023 11:01:24 -0700 Subject: [PATCH 5/8] The total_cmp tests don't need Float --- src/float.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/float.rs b/src/float.rs index e34d7ed..442fa9b 100644 --- a/src/float.rs +++ b/src/float.rs @@ -2414,17 +2414,17 @@ mod tests { #[test] fn total_cmp() { - use crate::float::{Float, TotalOrder}; + use crate::float::TotalOrder; use core::cmp::Ordering; use core::{f32, f64}; - fn check_eq(x: T, y: T) { + fn check_eq(x: T, y: T) { assert_eq!(x.total_cmp(&y), Ordering::Equal); } - fn check_lt(x: T, y: T) { + fn check_lt(x: T, y: T) { assert_eq!(x.total_cmp(&y), Ordering::Less); } - fn check_gt(x: T, y: T) { + fn check_gt(x: T, y: T) { assert_eq!(x.total_cmp(&y), Ordering::Greater); } From d6647588dc23c8b986602d470b457cdf0cbab308 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Thu, 26 Oct 2023 10:53:29 -0700 Subject: [PATCH 6/8] Backport total_cmp for Rust <1.62 --- build.rs | 1 + src/float.rs | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/build.rs b/build.rs index bb78328..11bf090 100644 --- a/build.rs +++ b/build.rs @@ -16,6 +16,7 @@ fn main() { ac.emit_expression_cfg("1f64.copysign(-1f64)", "has_copysign"); } ac.emit_expression_cfg("1f64.is_subnormal()", "has_is_subnormal"); + ac.emit_expression_cfg("1f64.total_cmp(&2f64)", "has_total_cmp"); ac.emit_expression_cfg("1u32.to_ne_bytes()", "has_int_to_from_bytes"); ac.emit_expression_cfg("3.14f64.to_ne_bytes()", "has_float_to_from_bytes"); diff --git a/src/float.rs b/src/float.rs index 442fa9b..481d5e0 100644 --- a/src/float.rs +++ b/src/float.rs @@ -2268,17 +2268,31 @@ pub trait TotalOrder { fn total_cmp(&self, other: &Self) -> Ordering; } macro_rules! totalorder_impl { - ($T:ident) => { + ($T:ident, $I:ident, $U:ident, $bits:expr) => { impl TotalOrder for $T { #[inline] + #[cfg(has_total_cmp)] fn total_cmp(&self, other: &Self) -> Ordering { + // Forward to the core implementation Self::total_cmp(&self, other) } + #[inline] + #[cfg(not(has_total_cmp))] + fn total_cmp(&self, other: &Self) -> Ordering { + // Backport the core implementation (since 1.62) + let mut left = self.to_bits() as $I; + let mut right = other.to_bits() as $I; + + left ^= (((left >> ($bits - 1)) as $U) >> 1) as $I; + right ^= (((right >> ($bits - 1)) as $U) >> 1) as $I; + + left.cmp(&right) + } } }; } -totalorder_impl!(f64); -totalorder_impl!(f32); +totalorder_impl!(f64, i64, u64, 64); +totalorder_impl!(f32, i32, u32, 32); #[cfg(test)] mod tests { From 56210ef4602b7de266813cfeb764ba39029b0619 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Thu, 26 Oct 2023 10:54:30 -0700 Subject: [PATCH 7/8] Add CI for Rust 1.62 has_total_cmp --- .github/workflows/ci.yaml | 1 + bors.toml | 1 + ci/rustup.sh | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 95eb826..5f35692 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,7 @@ jobs: 1.44.0, # has_to_int_unchecked 1.46.0, # has_leading_trailing_ones 1.53.0, # has_is_subnormal + 1.62.0, # has_total_cmp stable, beta, nightly, diff --git a/bors.toml b/bors.toml index 7314c27..20713ee 100644 --- a/bors.toml +++ b/bors.toml @@ -6,6 +6,7 @@ status = [ "Test (1.44.0)", "Test (1.46.0)", "Test (1.53.0)", + "Test (1.62.0)", "Test (stable)", "Test (beta)", "Test (nightly)", diff --git a/ci/rustup.sh b/ci/rustup.sh index 56a8df1..26c8c3d 100755 --- a/ci/rustup.sh +++ b/ci/rustup.sh @@ -5,6 +5,6 @@ set -ex ci=$(dirname $0) -for version in 1.31.0 1.35.0 1.37.0 1.38.0 1.44.0 1.46.0 1.53.0 stable beta nightly; do +for version in 1.31.0 1.35.0 1.37.0 1.38.0 1.44.0 1.46.0 1.53.0 1.62.0 stable beta nightly; do rustup run "$version" "$ci/test_full.sh" done From aeee0382a0ffea3660d0efed4e0c65458998f9be Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 27 Oct 2023 12:03:43 -0700 Subject: [PATCH 8/8] Skip signaling NaN tests on x86 --- src/float.rs | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/float.rs b/src/float.rs index 481d5e0..844b89a 100644 --- a/src/float.rs +++ b/src/float.rs @@ -2448,21 +2448,26 @@ mod tests { check_lt(-0.0_f64, 0.0_f64); check_lt(-0.0_f32, 0.0_f32); - let s_nan = f64::from_bits(0x7ff4000000000000); - let q_nan = f64::from_bits(0x7ff8000000000000); - check_lt(s_nan, q_nan); - - let neg_s_nan = f64::from_bits(0xfff4000000000000); - let neg_q_nan = f64::from_bits(0xfff8000000000000); - check_lt(neg_q_nan, neg_s_nan); - - let s_nan = f32::from_bits(0x7fa00000); - let q_nan = f32::from_bits(0x7fc00000); - check_lt(s_nan, q_nan); - - let neg_s_nan = f32::from_bits(0xffa00000); - let neg_q_nan = f32::from_bits(0xffc00000); - check_lt(neg_q_nan, neg_s_nan); + // x87 registers don't preserve the exact value of signaling NaN: + // https://github.com/rust-lang/rust/issues/115567 + #[cfg(not(target_arch = "x86"))] + { + let s_nan = f64::from_bits(0x7ff4000000000000); + let q_nan = f64::from_bits(0x7ff8000000000000); + check_lt(s_nan, q_nan); + + let neg_s_nan = f64::from_bits(0xfff4000000000000); + let neg_q_nan = f64::from_bits(0xfff8000000000000); + check_lt(neg_q_nan, neg_s_nan); + + let s_nan = f32::from_bits(0x7fa00000); + let q_nan = f32::from_bits(0x7fc00000); + check_lt(s_nan, q_nan); + + let neg_s_nan = f32::from_bits(0xffa00000); + let neg_q_nan = f32::from_bits(0xffc00000); + check_lt(neg_q_nan, neg_s_nan); + } check_lt(-f64::NAN, f64::NEG_INFINITY); check_gt(1.0_f64, -f64::NAN);