Add classify and related methods for f16 and f128

This commit is contained in:
Trevor Gross 2024-06-27 03:21:25 -04:00
parent cf2df68d1f
commit 3a2c0aedf1
4 changed files with 375 additions and 39 deletions

View File

@ -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,119 @@ 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::<f128, u128>(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

View File

@ -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,159 @@ 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`.
//
// Platforms without native support generally convert to `f32` to perform operations,
// and most of these platforms correctly round back to `f16` after each operation.
// However, some platforms have bugs where they keep the excess `f32` precision (e.g.
// WASM, see llvm/llvm-project#96437). This implementation makes a best-effort attempt
// to account for that excess precision.
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::<f16, u16>(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

View File

@ -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]

View File

@ -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]