Skip to content

Commit

Permalink
float to/from bits and classify: update comments regarding non-confor…
Browse files Browse the repository at this point in the history
…mant hardware
  • Loading branch information
RalfJung committed Aug 15, 2024
1 parent eefd2ea commit 29232ca
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 684 deletions.
110 changes: 7 additions & 103 deletions library/core/src/num/f128.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
#![unstable(feature = "f128", issue = "116909")]

use crate::convert::FloatToInt;
#[cfg(not(test))]
use crate::intrinsics;
use crate::mem;
use crate::num::FpCategory;

Expand Down Expand Up @@ -439,22 +437,12 @@ 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 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)
}
// 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.

/// 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) {
let bits = self.to_bits();
match (bits & Self::MAN_MASK, bits & Self::EXP_MASK) {
(0, Self::EXP_MASK) => FpCategory::Infinite,
(_, Self::EXP_MASK) => FpCategory::Nan,
(0, 0) => FpCategory::Zero,
Expand Down Expand Up @@ -746,48 +734,7 @@ impl f128 {
#[must_use = "this returns the result of the operation, without modifying the original"]
pub const fn to_bits(self) -> u128 {
// SAFETY: `u128` is a plain old datatype so we can always transmute to it.
// ...sorta.
//
// It turns out that at runtime, it is possible for a floating point number
// to be subject to a floating point mode that alters nonzero subnormal numbers
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
//
// And, of course evaluating to a NaN value is fairly nondeterministic.
// More precisely: when NaN should be returned is knowable, but which NaN?
// So far that's defined by a combination of LLVM and the CPU, not Rust.
// This function, however, allows observing the bitstring of a NaN,
// thus introspection on CTFE.
//
// In order to preserve, at least for the moment, const-to-runtime equivalence,
// we reject any of these possible situations from happening.
#[inline]
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
const fn ct_f128_to_u128(ct: f128) -> u128 {
// FIXME(f16_f128): we should use `.classify()` like `f32` and `f64`, but that
// is not available on all platforms (needs `netf2` and `unordtf2`). So classify
// the bits instead.

// SAFETY: this is a POD transmutation
let bits = unsafe { mem::transmute::<f128, u128>(ct) };
match f128::classify_bits(bits) {
FpCategory::Nan => {
panic!("const-eval error: cannot use f128::to_bits on a NaN")
}
FpCategory::Subnormal => {
panic!("const-eval error: cannot use f128::to_bits on a subnormal number")
}
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => bits,
}
}

#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
fn rt_f128_to_u128(x: f128) -> u128 {
// SAFETY: `u128` is a plain old datatype so we can always... uh...
// ...look, just pretend you forgot what you just read.
// Stability concerns.
unsafe { mem::transmute(x) }
}
intrinsics::const_eval_select((self,), ct_f128_to_u128, rt_f128_to_u128)
unsafe { mem::transmute(self) }
}

/// Raw transmutation from `u128`.
Expand Down Expand Up @@ -834,50 +781,7 @@ impl f128 {
#[unstable(feature = "f128", issue = "116909")]
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
pub const fn from_bits(v: u128) -> Self {
// It turns out the safety issues with sNaN were overblown! Hooray!
// SAFETY: `u128` is a plain old datatype so we can always transmute from it
// ...sorta.
//
// It turns out that at runtime, it is possible for a floating point number
// to be subject to floating point modes that alter nonzero subnormal numbers
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
// This is not a problem usually, but at least one tier2 platform for Rust
// actually exhibits this behavior by default: thumbv7neon
// aka "the Neon FPU in AArch32 state"
//
// And, of course evaluating to a NaN value is fairly nondeterministic.
// More precisely: when NaN should be returned is knowable, but which NaN?
// So far that's defined by a combination of LLVM and the CPU, not Rust.
// This function, however, allows observing the bitstring of a NaN,
// thus introspection on CTFE.
//
// In order to preserve, at least for the moment, const-to-runtime equivalence,
// reject any of these possible situations from happening.
#[inline]
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
const fn ct_u128_to_f128(ct: u128) -> f128 {
match f128::classify_bits(ct) {
FpCategory::Subnormal => {
panic!("const-eval error: cannot use f128::from_bits on a subnormal number")
}
FpCategory::Nan => {
panic!("const-eval error: cannot use f128::from_bits on NaN")
}
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
// SAFETY: It's not a frumious number
unsafe { mem::transmute::<u128, f128>(ct) }
}
}
}

#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
fn rt_u128_to_f128(x: u128) -> f128 {
// SAFETY: `u128` is a plain old datatype so we can always... uh...
// ...look, just pretend you forgot what you just read.
// Stability concerns.
unsafe { mem::transmute(x) }
}
intrinsics::const_eval_select((v,), ct_u128_to_f128, rt_u128_to_f128)
unsafe { mem::transmute(v) }
}

/// Returns the memory representation of this floating point number as a byte array in
Expand Down
161 changes: 23 additions & 138 deletions library/core/src/num/f16.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
#![unstable(feature = "f16", issue = "116909")]

use crate::convert::FloatToInt;
#[cfg(not(test))]
use crate::intrinsics;
use crate::mem;
use crate::num::FpCategory;

Expand Down Expand Up @@ -426,15 +424,15 @@ impl f16 {
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`.
// If we only cared about being "technically" correct, that's an entirely legit
// implementation.
//

// 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.
// 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.
if self.is_infinite() {
// Thus, a value may compare unequal to infinity, despite having a "full" exponent mask.
FpCategory::Infinite
Expand All @@ -446,49 +444,18 @@ impl f16 {
// 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]
#[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,
// So, this does use bitpattern matching for the rest. On x87, due do 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,
}
}
}

Expand Down Expand Up @@ -781,48 +748,7 @@ impl f16 {
#[must_use = "this returns the result of the operation, without modifying the original"]
pub const fn to_bits(self) -> u16 {
// SAFETY: `u16` is a plain old datatype so we can always transmute to it.
// ...sorta.
//
// It turns out that at runtime, it is possible for a floating point number
// to be subject to a floating point mode that alters nonzero subnormal numbers
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
//
// And, of course evaluating to a NaN value is fairly nondeterministic.
// More precisely: when NaN should be returned is knowable, but which NaN?
// So far that's defined by a combination of LLVM and the CPU, not Rust.
// This function, however, allows observing the bitstring of a NaN,
// thus introspection on CTFE.
//
// In order to preserve, at least for the moment, const-to-runtime equivalence,
// we reject any of these possible situations from happening.
#[inline]
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
const fn ct_f16_to_u16(ct: f16) -> u16 {
// FIXME(f16_f128): we should use `.classify()` like `f32` and `f64`, but we don't yet
// want to rely on that on all platforms because it is nondeterministic (e.g. x86 has
// convention discrepancies calling intrinsics). So just classify the bits instead.

// SAFETY: this is a POD transmutation
let bits = unsafe { mem::transmute::<f16, u16>(ct) };
match f16::classify_bits(bits) {
FpCategory::Nan => {
panic!("const-eval error: cannot use f16::to_bits on a NaN")
}
FpCategory::Subnormal => {
panic!("const-eval error: cannot use f16::to_bits on a subnormal number")
}
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => bits,
}
}

#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
fn rt_f16_to_u16(x: f16) -> u16 {
// SAFETY: `u16` is a plain old datatype so we can always... uh...
// ...look, just pretend you forgot what you just read.
// Stability concerns.
unsafe { mem::transmute(x) }
}
intrinsics::const_eval_select((self,), ct_f16_to_u16, rt_f16_to_u16)
unsafe { mem::transmute(self) }
}

/// Raw transmutation from `u16`.
Expand Down Expand Up @@ -869,49 +795,8 @@ impl f16 {
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
pub const fn from_bits(v: u16) -> Self {
// It turns out the safety issues with sNaN were overblown! Hooray!
// SAFETY: `u16` is a plain old datatype so we can always transmute from it
// ...sorta.
//
// It turns out that at runtime, it is possible for a floating point number
// to be subject to floating point modes that alter nonzero subnormal numbers
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
// This is not a problem usually, but at least one tier2 platform for Rust
// actually exhibits this behavior by default: thumbv7neon
// aka "the Neon FPU in AArch32 state"
//
// And, of course evaluating to a NaN value is fairly nondeterministic.
// More precisely: when NaN should be returned is knowable, but which NaN?
// So far that's defined by a combination of LLVM and the CPU, not Rust.
// This function, however, allows observing the bitstring of a NaN,
// thus introspection on CTFE.
//
// In order to preserve, at least for the moment, const-to-runtime equivalence,
// reject any of these possible situations from happening.
#[inline]
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
const fn ct_u16_to_f16(ct: u16) -> f16 {
match f16::classify_bits(ct) {
FpCategory::Subnormal => {
panic!("const-eval error: cannot use f16::from_bits on a subnormal number")
}
FpCategory::Nan => {
panic!("const-eval error: cannot use f16::from_bits on NaN")
}
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
// SAFETY: It's not a frumious number
unsafe { mem::transmute::<u16, f16>(ct) }
}
}
}

#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
fn rt_u16_to_f16(x: u16) -> f16 {
// SAFETY: `u16` is a plain old datatype so we can always... uh...
// ...look, just pretend you forgot what you just read.
// Stability concerns.
unsafe { mem::transmute(x) }
}
intrinsics::const_eval_select((v,), ct_u16_to_f16, rt_u16_to_f16)
// SAFETY: `u16` is a plain old datatype so we can always transmute from it.
unsafe { mem::transmute(v) }
}

/// Returns the memory representation of this floating point number as a byte array in
Expand Down
Loading

0 comments on commit 29232ca

Please sign in to comment.