From abe63f65a049866270a26f11ea7562ecea0e3264 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Wed, 11 Sep 2024 10:53:39 +0200 Subject: [PATCH] simplify float::classify logic --- core/src/num/f128.rs | 4 ---- core/src/num/f16.rs | 44 +++++++--------------------------------- core/src/num/f32.rs | 48 +++++++++++--------------------------------- core/src/num/f64.rs | 44 +++++++++++----------------------------- 4 files changed, 31 insertions(+), 109 deletions(-) diff --git a/core/src/num/f128.rs b/core/src/num/f128.rs index 1959628bd8f59..100271fa54c1f 100644 --- a/core/src/num/f128.rs +++ b/core/src/num/f128.rs @@ -439,10 +439,6 @@ impl f128 { #[unstable(feature = "f128", issue = "116909")] #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] pub const fn classify(self) -> FpCategory { - // Other float types suffer from various platform bugs that violate the usual IEEE semantics - // and also make bitwise classification not always work reliably. However, `f128` cannot fit - // into any other float types so this is not a concern, and we can rely on bit patterns. - let bits = self.to_bits(); match (bits & Self::MAN_MASK, bits & Self::EXP_MASK) { (0, Self::EXP_MASK) => FpCategory::Infinite, diff --git a/core/src/num/f16.rs b/core/src/num/f16.rs index da92da1086dab..6bdc569df28bd 100644 --- a/core/src/num/f16.rs +++ b/core/src/num/f16.rs @@ -424,43 +424,13 @@ impl f16 { #[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. - // If we only cared about being "technically" correct, that's an entirely legit - // implementation. - // - // Unfortunately, there are platforms out there that do not correctly implement the IEEE - // float semantics Rust relies on: some hardware flushes denormals to zero, and some - // platforms convert to `f32` to perform operations without properly rounding back (e.g. - // WASM, see llvm/llvm-project#96437). These are platforms bugs, and Rust will misbehave on - // such platforms, but we can at least try to make things seem as sane as possible by being - // careful here. - // see also https://github.com/rust-lang/rust/issues/114479 - 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. On x87, due to the incorrect - // float codegen on this hardware, this doesn't actually return a right answer for NaN - // because it cannot correctly discern between a floating point NaN, and some normal - // floating point numbers truncated from an x87 FPU -- but we took care of NaN above, so - // we are fine. - // FIXME(jubilee): This probably could at least answer things correctly for Infinity, - // like the f64 version does, but I need to run more checks on how things go on x86. - // I fear losing mantissa data that would have answered that differently. - let b = self.to_bits(); - match (b & Self::MAN_MASK, b & Self::EXP_MASK) { - (0, 0) => FpCategory::Zero, - (_, 0) => FpCategory::Subnormal, - _ => FpCategory::Normal, - } + let b = self.to_bits(); + 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, } } diff --git a/core/src/num/f32.rs b/core/src/num/f32.rs index 885f7608a337e..4c2a4ee3b3255 100644 --- a/core/src/num/f32.rs +++ b/core/src/num/f32.rs @@ -652,42 +652,18 @@ impl f32 { #[stable(feature = "rust1", since = "1.0.0")] #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] pub const fn classify(self) -> FpCategory { - // A previous implementation tried to only use bitmask-based checks, - // using f32::to_bits to transmute the float to its bit repr and match on that. - // If we only cared about being "technically" correct, that's an entirely legit - // implementation. - // - // Unfortunately, there is hardware out there that does not correctly implement the IEEE - // float semantics Rust relies on: x87 uses a too-large mantissa and exponent, and some - // hardware flushes subnormals to zero. These are platforms bugs, and Rust will misbehave on - // such hardware, but we can at least try to make things seem as sane as possible by being - // careful here. - // see also https://github.com/rust-lang/rust/issues/114479 - if self.is_infinite() { - // 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. On x87, due to the incorrect - // float codegen on this hardware, this doesn't actually return a right answer for NaN - // because it cannot correctly discern between a floating point NaN, and some normal - // floating point numbers truncated from an x87 FPU -- but we took care of NaN above, so - // we are fine. - // FIXME(jubilee): This probably could at least answer things correctly for Infinity, - // like the f64 version does, but I need to run more checks on how things go on x86. - // I fear losing mantissa data that would have answered that differently. - let b = self.to_bits(); - match (b & Self::MAN_MASK, b & Self::EXP_MASK) { - (0, 0) => FpCategory::Zero, - (_, 0) => FpCategory::Subnormal, - _ => FpCategory::Normal, - } + // We used to have complicated logic here that avoids the simple bit-based tests to work + // around buggy codegen for x87 targets (see + // https://github.com/rust-lang/rust/issues/114479). However, some LLVM versions later, none + // of our tests is able to find any difference between the complicated and the naive + // version, so now we are back to the naive version. + let b = self.to_bits(); + 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, } } diff --git a/core/src/num/f64.rs b/core/src/num/f64.rs index 28cc231ccc76d..87fb5fe7ebeea 100644 --- a/core/src/num/f64.rs +++ b/core/src/num/f64.rs @@ -651,38 +651,18 @@ impl f64 { #[stable(feature = "rust1", since = "1.0.0")] #[rustc_const_unstable(feature = "const_float_classify", issue = "72505")] pub const fn classify(self) -> FpCategory { - // A previous implementation tried to only use bitmask-based checks, - // using f64::to_bits to transmute the float to its bit repr and match on that. - // If we only cared about being "technically" correct, that's an entirely legit - // implementation. - // - // Unfortunately, there is hardware out there that does not correctly implement the IEEE - // float semantics Rust relies on: x87 uses a too-large exponent, and some hardware flushes - // subnormals to zero. These are platforms bugs, and Rust will misbehave on such hardware, - // but we can at least try to make things seem as sane as possible by being careful here. - // see also https://github.com/rust-lang/rust/issues/114479 - // - // Thus, a value may compare unequal to infinity, despite having a "full" exponent mask. - // And it may not be NaN, as it can simply be an "overextended" finite value. - if self.is_nan() { - 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. On x87, due to the incorrect - // float codegen on this hardware, this doesn't actually return a right answer for NaN - // because it cannot correctly discern between a floating point NaN, and some normal - // floating point numbers truncated from an x87 FPU -- but we took care of NaN above, so - // we are fine. - let b = self.to_bits(); - match (b & Self::MAN_MASK, b & Self::EXP_MASK) { - (0, Self::EXP_MASK) => FpCategory::Infinite, - (0, 0) => FpCategory::Zero, - (_, 0) => FpCategory::Subnormal, - _ => FpCategory::Normal, - } + // We used to have complicated logic here that avoids the simple bit-based tests to work + // around buggy codegen for x87 targets (see + // https://github.com/rust-lang/rust/issues/114479). However, some LLVM versions later, none + // of our tests is able to find any difference between the complicated and the naive + // version, so now we are back to the naive version. + let b = self.to_bits(); + 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, } }