From c764619ac54178697d606be1317d9e3d38d7d483 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Thu, 27 Jun 2024 03:21:25 -0400 Subject: [PATCH] Add `classify` and related methods for `f16` and `f128` --- library/core/src/num/f128.rs | 121 +++++++++++++++++++++++++ library/core/src/num/f16.rs | 160 +++++++++++++++++++++++++++++++++- library/std/src/f128/tests.rs | 66 ++++++++++---- library/std/src/f16/tests.rs | 66 ++++++++++---- 4 files changed, 374 insertions(+), 39 deletions(-) diff --git a/library/core/src/num/f128.rs b/library/core/src/num/f128.rs index 58ed98c888cc6..d5408316a98b9 100644 --- a/library/core/src/num/f128.rs +++ b/library/core/src/num/f128.rs @@ -13,6 +13,7 @@ use crate::convert::FloatToInt; use crate::mem; +use crate::num::FpCategory; /// Basic mathematical constants. #[unstable(feature = "f128", issue = "116909")] @@ -251,6 +252,12 @@ impl f128 { #[cfg(not(bootstrap))] pub(crate) const SIGN_MASK: u128 = 0x8000_0000_0000_0000_0000_0000_0000_0000; + /// Exponent mask + pub(crate) const EXP_MASK: u128 = 0x7fff_0000_0000_0000_0000_0000_0000_0000; + + /// Mantissa mask + pub(crate) const MAN_MASK: u128 = 0x0000_ffff_ffff_ffff_ffff_ffff_ffff_ffff; + /// Minimum representable positive value (min subnormal) #[cfg(not(bootstrap))] const TINY_BITS: u128 = 0x1; @@ -354,6 +361,120 @@ impl f128 { self.abs_private() < Self::INFINITY } + /// Returns `true` if the number is [subnormal]. + /// + /// ``` + /// #![feature(f128)] + /// # // FIXME(f16_f128): remove when `eqtf2` is available + /// # #[cfg(all(target_arch = "x86_64", target_os = "linux"))] { + /// + /// let min = f128::MIN_POSITIVE; // 3.362103143e-4932f128 + /// let max = f128::MAX; + /// let lower_than_min = 1.0e-4960_f128; + /// let zero = 0.0_f128; + /// + /// assert!(!min.is_subnormal()); + /// assert!(!max.is_subnormal()); + /// + /// assert!(!zero.is_subnormal()); + /// assert!(!f128::NAN.is_subnormal()); + /// assert!(!f128::INFINITY.is_subnormal()); + /// // Values between `0` and `min` are Subnormal. + /// assert!(lower_than_min.is_subnormal()); + /// # } + /// ``` + /// + /// [subnormal]: https://en.wikipedia.org/wiki/Denormal_number + #[inline] + #[must_use] + #[cfg(not(bootstrap))] + #[unstable(feature = "f128", issue = "116909")] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + pub const fn is_subnormal(self) -> bool { + matches!(self.classify(), FpCategory::Subnormal) + } + + /// Returns `true` if the number is neither zero, infinite, + /// [subnormal], or NaN. + /// + /// ``` + /// #![feature(f128)] + /// # // FIXME(f16_f128): remove when `eqtf2` is available + /// # #[cfg(all(target_arch = "x86_64", target_os = "linux"))] { + /// + /// let min = f128::MIN_POSITIVE; // 3.362103143e-4932f128 + /// let max = f128::MAX; + /// let lower_than_min = 1.0e-4960_f128; + /// let zero = 0.0_f128; + /// + /// assert!(min.is_normal()); + /// assert!(max.is_normal()); + /// + /// assert!(!zero.is_normal()); + /// assert!(!f128::NAN.is_normal()); + /// assert!(!f128::INFINITY.is_normal()); + /// // Values between `0` and `min` are Subnormal. + /// assert!(!lower_than_min.is_normal()); + /// # } + /// ``` + /// + /// [subnormal]: https://en.wikipedia.org/wiki/Denormal_number + #[inline] + #[must_use] + #[cfg(not(bootstrap))] + #[unstable(feature = "f128", issue = "116909")] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + pub const fn is_normal(self) -> bool { + matches!(self.classify(), FpCategory::Normal) + } + + /// Returns the floating point category of the number. If only one property + /// is going to be tested, it is generally faster to use the specific + /// predicate instead. + /// + /// ``` + /// #![feature(f128)] + /// # // FIXME(f16_f128): remove when `eqtf2` is available + /// # #[cfg(all(target_arch = "x86_64", target_os = "linux"))] { + /// + /// use std::num::FpCategory; + /// + /// let num = 12.4_f128; + /// let inf = f128::INFINITY; + /// + /// assert_eq!(num.classify(), FpCategory::Normal); + /// assert_eq!(inf.classify(), FpCategory::Infinite); + /// # } + /// ``` + #[inline] + #[cfg(not(bootstrap))] + #[unstable(feature = "f128", issue = "116909")] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + pub const fn classify(self) -> FpCategory { + // Other float types cannot use a bitwise classify because they may suffer a variety + // of errors if the backend chooses to cast to different float types (x87). `f128` cannot + // fit into any other float types so this is not a concern, and we rely on bit patterns. + + // SAFETY: POD bitcast, same as in `to_bits`. + let bits = unsafe { mem::transmute::(self) }; + Self::classify_bits(bits) + } + + /// This operates on bits, and only bits, so it can ignore concerns about weird FPUs. + /// FIXME(jubilee): In a just world, this would be the entire impl for classify, + /// plus a transmute. We do not live in a just world, but we can make it more so. + #[inline] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + const fn classify_bits(b: u128) -> FpCategory { + match (b & Self::MAN_MASK, b & Self::EXP_MASK) { + (0, Self::EXP_MASK) => FpCategory::Infinite, + (_, Self::EXP_MASK) => FpCategory::Nan, + (0, 0) => FpCategory::Zero, + (_, 0) => FpCategory::Subnormal, + _ => FpCategory::Normal, + } + } + /// Returns `true` if `self` has a positive sign, including `+0.0`, NaNs with /// positive sign bit and positive infinity. Note that IEEE 754 doesn't assign any /// meaning to the sign bit in case of a NaN, and as Rust doesn't guarantee that diff --git a/library/core/src/num/f16.rs b/library/core/src/num/f16.rs index 3c58b0af9c27e..b5cc2c4aa6b38 100644 --- a/library/core/src/num/f16.rs +++ b/library/core/src/num/f16.rs @@ -13,6 +13,7 @@ use crate::convert::FloatToInt; use crate::mem; +use crate::num::FpCategory; /// Basic mathematical constants. #[unstable(feature = "f16", issue = "116909")] @@ -244,7 +245,13 @@ impl f16 { /// Sign bit #[cfg(not(bootstrap))] - const SIGN_MASK: u16 = 0x8000; + pub(crate) const SIGN_MASK: u16 = 0x8000; + + /// Exponent mask + pub(crate) const EXP_MASK: u16 = 0x7c00; + + /// Mantissa mask + pub(crate) const MAN_MASK: u16 = 0x03ff; /// Minimum representable positive value (min subnormal) #[cfg(not(bootstrap))] @@ -344,6 +351,157 @@ impl f16 { self.abs_private() < Self::INFINITY } + /// Returns `true` if the number is [subnormal]. + /// + /// ``` + /// #![feature(f16)] + /// # #[cfg(target_arch = "aarch64")] { // FIXME(f16_F128): rust-lang/rust#123885 + /// + /// let min = f16::MIN_POSITIVE; // 6.1035e-5 + /// let max = f16::MAX; + /// let lower_than_min = 1.0e-7_f16; + /// let zero = 0.0_f16; + /// + /// assert!(!min.is_subnormal()); + /// assert!(!max.is_subnormal()); + /// + /// assert!(!zero.is_subnormal()); + /// assert!(!f16::NAN.is_subnormal()); + /// assert!(!f16::INFINITY.is_subnormal()); + /// // Values between `0` and `min` are Subnormal. + /// assert!(lower_than_min.is_subnormal()); + /// # } + /// ``` + /// [subnormal]: https://en.wikipedia.org/wiki/Denormal_number + #[inline] + #[must_use] + #[cfg(not(bootstrap))] + #[unstable(feature = "f16", issue = "116909")] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + pub const fn is_subnormal(self) -> bool { + matches!(self.classify(), FpCategory::Subnormal) + } + + /// Returns `true` if the number is neither zero, infinite, + /// [subnormal], or NaN. + /// + /// ``` + /// #![feature(f16)] + /// # #[cfg(target_arch = "aarch64")] { // FIXME(f16_F128): rust-lang/rust#123885 + /// + /// let min = f16::MIN_POSITIVE; // 6.1035e-5 + /// let max = f16::MAX; + /// let lower_than_min = 1.0e-7_f16; + /// let zero = 0.0_f16; + /// + /// assert!(min.is_normal()); + /// assert!(max.is_normal()); + /// + /// assert!(!zero.is_normal()); + /// assert!(!f16::NAN.is_normal()); + /// assert!(!f16::INFINITY.is_normal()); + /// // Values between `0` and `min` are Subnormal. + /// assert!(!lower_than_min.is_normal()); + /// # } + /// ``` + /// [subnormal]: https://en.wikipedia.org/wiki/Denormal_number + #[inline] + #[must_use] + #[cfg(not(bootstrap))] + #[unstable(feature = "f16", issue = "116909")] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + pub const fn is_normal(self) -> bool { + matches!(self.classify(), FpCategory::Normal) + } + + /// Returns the floating point category of the number. If only one property + /// is going to be tested, it is generally faster to use the specific + /// predicate instead. + /// + /// ``` + /// #![feature(f16)] + /// # #[cfg(target_arch = "aarch64")] { // FIXME(f16_F128): rust-lang/rust#123885 + /// + /// use std::num::FpCategory; + /// + /// let num = 12.4_f16; + /// let inf = f16::INFINITY; + /// + /// assert_eq!(num.classify(), FpCategory::Normal); + /// assert_eq!(inf.classify(), FpCategory::Infinite); + /// # } + /// ``` + #[inline] + #[cfg(not(bootstrap))] + #[unstable(feature = "f16", issue = "116909")] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + pub const fn classify(self) -> FpCategory { + // A previous implementation for f32/f64 tried to only use bitmask-based checks, + // using `to_bits` to transmute the float to its bit repr and match on that. + // Unfortunately, floating point numbers can be much worse than that. + // This also needs to not result in recursive evaluations of `to_bits`. + // + // `f16` casts correctly on most platforms, but there are some known bugs (e.g. + // WASM has rounding and quieting bugs). So keep the implementation consistent with + // `f32`/`f64`. + if self.is_infinite() { + // Thus, a value may compare unequal to infinity, despite having a "full" exponent mask. + FpCategory::Infinite + } else if self.is_nan() { + // And it may not be NaN, as it can simply be an "overextended" finite value. + FpCategory::Nan + } else { + // However, std can't simply compare to zero to check for zero, either, + // as correctness requires avoiding equality tests that may be Subnormal == -0.0 + // because it may be wrong under "denormals are zero" and "flush to zero" modes. + // Most of std's targets don't use those, but they are used for thumbv7neon. + // So, this does use bitpattern matching for the rest. + + // SAFETY: f16 to u16 is fine. Usually. + // If classify has gotten this far, the value is definitely in one of these categories. + unsafe { f16::partial_classify(self) } + } + } + + /// This doesn't actually return a right answer for NaN on purpose, + /// seeing as how it cannot correctly discern between a floating point NaN, + /// and some normal floating point numbers truncated from an x87 FPU. + /// + /// # Safety + /// + /// This requires making sure you call this function for values it answers correctly on, + /// otherwise it returns a wrong answer. This is not important for memory safety per se, + /// but getting floats correct is important for not accidentally leaking const eval + /// runtime-deviating logic which may or may not be acceptable. + #[inline] + #[cfg(not(bootstrap))] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + const unsafe fn partial_classify(self) -> FpCategory { + // SAFETY: The caller is not asking questions for which this will tell lies. + let b = unsafe { mem::transmute::(self) }; + match (b & Self::MAN_MASK, b & Self::EXP_MASK) { + (0, Self::EXP_MASK) => FpCategory::Infinite, + (0, 0) => FpCategory::Zero, + (_, 0) => FpCategory::Subnormal, + _ => FpCategory::Normal, + } + } + + /// This operates on bits, and only bits, so it can ignore concerns about weird FPUs. + /// FIXME(jubilee): In a just world, this would be the entire impl for classify, + /// plus a transmute. We do not live in a just world, but we can make it more so. + #[inline] + #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] + const fn classify_bits(b: u16) -> FpCategory { + match (b & Self::MAN_MASK, b & Self::EXP_MASK) { + (0, Self::EXP_MASK) => FpCategory::Infinite, + (_, Self::EXP_MASK) => FpCategory::Nan, + (0, 0) => FpCategory::Zero, + (_, 0) => FpCategory::Subnormal, + _ => FpCategory::Normal, + } + } + /// Returns `true` if `self` has a positive sign, including `+0.0`, NaNs with /// positive sign bit and positive infinity. Note that IEEE 754 doesn't assign any /// meaning to the sign bit in case of a NaN, and as Rust doesn't guarantee that diff --git a/library/std/src/f128/tests.rs b/library/std/src/f128/tests.rs index bd7a921c502a7..0b3e485b0e735 100644 --- a/library/std/src/f128/tests.rs +++ b/library/std/src/f128/tests.rs @@ -3,6 +3,7 @@ #![cfg(reliable_f128)] use crate::f128::consts; +use crate::num::FpCategory as Fp; use crate::num::*; /// Smallest number @@ -52,9 +53,8 @@ fn test_nan() { assert!(!nan.is_finite()); assert!(nan.is_sign_positive()); assert!(!nan.is_sign_negative()); - // FIXME(f16_f128): classify - // assert!(!nan.is_normal()); - // assert_eq!(Fp::Nan, nan.classify()); + assert!(!nan.is_normal()); + assert_eq!(Fp::Nan, nan.classify()); } #[test] @@ -65,9 +65,8 @@ fn test_infinity() { assert!(inf.is_sign_positive()); assert!(!inf.is_sign_negative()); assert!(!inf.is_nan()); - // FIXME(f16_f128): classify - // assert!(!inf.is_normal()); - // assert_eq!(Fp::Infinite, inf.classify()); + assert!(!inf.is_normal()); + assert_eq!(Fp::Infinite, inf.classify()); } #[test] @@ -78,9 +77,8 @@ fn test_neg_infinity() { assert!(!neg_inf.is_sign_positive()); assert!(neg_inf.is_sign_negative()); assert!(!neg_inf.is_nan()); - // FIXME(f16_f128): classify - // assert!(!neg_inf.is_normal()); - // assert_eq!(Fp::Infinite, neg_inf.classify()); + assert!(!neg_inf.is_normal()); + assert_eq!(Fp::Infinite, neg_inf.classify()); } #[test] @@ -92,9 +90,8 @@ fn test_zero() { assert!(zero.is_sign_positive()); assert!(!zero.is_sign_negative()); assert!(!zero.is_nan()); - // FIXME(f16_f128): classify - // assert!(!zero.is_normal()); - // assert_eq!(Fp::Zero, zero.classify()); + assert!(!zero.is_normal()); + assert_eq!(Fp::Zero, zero.classify()); } #[test] @@ -106,9 +103,8 @@ fn test_neg_zero() { assert!(!neg_zero.is_sign_positive()); assert!(neg_zero.is_sign_negative()); assert!(!neg_zero.is_nan()); - // FIXME(f16_f128): classify - // assert!(!neg_zero.is_normal()); - // assert_eq!(Fp::Zero, neg_zero.classify()); + assert!(!neg_zero.is_normal()); + assert_eq!(Fp::Zero, neg_zero.classify()); } #[test] @@ -120,9 +116,8 @@ fn test_one() { assert!(one.is_sign_positive()); assert!(!one.is_sign_negative()); assert!(!one.is_nan()); - // FIXME(f16_f128): classify - // assert!(one.is_normal()); - // assert_eq!(Fp::Normal, one.classify()); + assert!(one.is_normal()); + assert_eq!(Fp::Normal, one.classify()); } #[test] @@ -164,7 +159,40 @@ fn test_is_finite() { assert!((-109.2f128).is_finite()); } -// FIXME(f16_f128): add `test_is_normal` and `test_classify` when classify is working +#[test] +fn test_is_normal() { + let nan: f128 = f128::NAN; + let inf: f128 = f128::INFINITY; + let neg_inf: f128 = f128::NEG_INFINITY; + let zero: f128 = 0.0f128; + let neg_zero: f128 = -0.0; + assert!(!nan.is_normal()); + assert!(!inf.is_normal()); + assert!(!neg_inf.is_normal()); + assert!(!zero.is_normal()); + assert!(!neg_zero.is_normal()); + assert!(1f128.is_normal()); + assert!(1e-4931f128.is_normal()); + assert!(!1e-4932f128.is_normal()); +} + +#[test] +fn test_classify() { + let nan: f128 = f128::NAN; + let inf: f128 = f128::INFINITY; + let neg_inf: f128 = f128::NEG_INFINITY; + let zero: f128 = 0.0f128; + let neg_zero: f128 = -0.0; + assert_eq!(nan.classify(), Fp::Nan); + assert_eq!(inf.classify(), Fp::Infinite); + assert_eq!(neg_inf.classify(), Fp::Infinite); + assert_eq!(zero.classify(), Fp::Zero); + assert_eq!(neg_zero.classify(), Fp::Zero); + assert_eq!(1f128.classify(), Fp::Normal); + assert_eq!(1e-4931f128.classify(), Fp::Normal); + assert_eq!(1e-4932f128.classify(), Fp::Subnormal); +} + // FIXME(f16_f128): add missing math functions when available #[test] diff --git a/library/std/src/f16/tests.rs b/library/std/src/f16/tests.rs index bb6a811529e17..26658a0be87bc 100644 --- a/library/std/src/f16/tests.rs +++ b/library/std/src/f16/tests.rs @@ -3,6 +3,7 @@ #![cfg(reliable_f16)] use crate::f16::consts; +use crate::num::FpCategory as Fp; use crate::num::*; // We run out of precision pretty quickly with f16 @@ -58,9 +59,8 @@ fn test_nan() { assert!(!nan.is_finite()); assert!(nan.is_sign_positive()); assert!(!nan.is_sign_negative()); - // FIXME(f16_f128): classify - // assert!(!nan.is_normal()); - // assert_eq!(Fp::Nan, nan.classify()); + assert!(!nan.is_normal()); + assert_eq!(Fp::Nan, nan.classify()); } #[test] @@ -71,9 +71,8 @@ fn test_infinity() { assert!(inf.is_sign_positive()); assert!(!inf.is_sign_negative()); assert!(!inf.is_nan()); - // FIXME(f16_f128): classify - // assert!(!inf.is_normal()); - // assert_eq!(Fp::Infinite, inf.classify()); + assert!(!inf.is_normal()); + assert_eq!(Fp::Infinite, inf.classify()); } #[test] @@ -84,9 +83,8 @@ fn test_neg_infinity() { assert!(!neg_inf.is_sign_positive()); assert!(neg_inf.is_sign_negative()); assert!(!neg_inf.is_nan()); - // FIXME(f16_f128): classify - // assert!(!neg_inf.is_normal()); - // assert_eq!(Fp::Infinite, neg_inf.classify()); + assert!(!neg_inf.is_normal()); + assert_eq!(Fp::Infinite, neg_inf.classify()); } #[test] @@ -98,9 +96,8 @@ fn test_zero() { assert!(zero.is_sign_positive()); assert!(!zero.is_sign_negative()); assert!(!zero.is_nan()); - // FIXME(f16_f128): classify - // assert!(!zero.is_normal()); - // assert_eq!(Fp::Zero, zero.classify()); + assert!(!zero.is_normal()); + assert_eq!(Fp::Zero, zero.classify()); } #[test] @@ -112,9 +109,8 @@ fn test_neg_zero() { assert!(!neg_zero.is_sign_positive()); assert!(neg_zero.is_sign_negative()); assert!(!neg_zero.is_nan()); - // FIXME(f16_f128): classify - // assert!(!neg_zero.is_normal()); - // assert_eq!(Fp::Zero, neg_zero.classify()); + assert!(!neg_zero.is_normal()); + assert_eq!(Fp::Zero, neg_zero.classify()); } #[test] @@ -126,9 +122,8 @@ fn test_one() { assert!(one.is_sign_positive()); assert!(!one.is_sign_negative()); assert!(!one.is_nan()); - // FIXME(f16_f128): classify - // assert!(one.is_normal()); - // assert_eq!(Fp::Normal, one.classify()); + assert!(one.is_normal()); + assert_eq!(Fp::Normal, one.classify()); } #[test] @@ -170,7 +165,40 @@ fn test_is_finite() { assert!((-109.2f16).is_finite()); } -// FIXME(f16_f128): add `test_is_normal` and `test_classify` when classify is working +#[test] +fn test_is_normal() { + let nan: f16 = f16::NAN; + let inf: f16 = f16::INFINITY; + let neg_inf: f16 = f16::NEG_INFINITY; + let zero: f16 = 0.0f16; + let neg_zero: f16 = -0.0; + assert!(!nan.is_normal()); + assert!(!inf.is_normal()); + assert!(!neg_inf.is_normal()); + assert!(!zero.is_normal()); + assert!(!neg_zero.is_normal()); + assert!(1f16.is_normal()); + assert!(1e-4f16.is_normal()); + assert!(!1e-5f16.is_normal()); +} + +#[test] +fn test_classify() { + let nan: f16 = f16::NAN; + let inf: f16 = f16::INFINITY; + let neg_inf: f16 = f16::NEG_INFINITY; + let zero: f16 = 0.0f16; + let neg_zero: f16 = -0.0; + assert_eq!(nan.classify(), Fp::Nan); + assert_eq!(inf.classify(), Fp::Infinite); + assert_eq!(neg_inf.classify(), Fp::Infinite); + assert_eq!(zero.classify(), Fp::Zero); + assert_eq!(neg_zero.classify(), Fp::Zero); + assert_eq!(1f16.classify(), Fp::Normal); + assert_eq!(1e-4f16.classify(), Fp::Normal); + assert_eq!(1e-5f16.classify(), Fp::Subnormal); +} + // FIXME(f16_f128): add missing math functions when available #[test]