Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No "weird" floats in const fn {from,to}_bits #95971

Merged
merged 4 commits into from
Apr 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 158 additions & 9 deletions library/core/src/num/f32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,8 @@ impl f32 {
#[inline]
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
pub(crate) const fn abs_private(self) -> f32 {
f32::from_bits(self.to_bits() & 0x7fff_ffff)
// SAFETY: This transmutation is fine. Probably. For the reasons std is using it.
unsafe { mem::transmute::<u32, f32>(mem::transmute::<f32, u32>(self) & 0x7fff_ffff) }
}

/// Returns `true` if this value is positive infinity or negative infinity, and
Expand All @@ -472,7 +473,10 @@ impl f32 {
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
#[inline]
pub const fn is_infinite(self) -> bool {
self.abs_private() == Self::INFINITY
// Getting clever with transmutation can result in incorrect answers on some FPUs
// FIXME: alter the Rust <-> Rust calling convention to prevent this problem.
// See https://github.com/rust-lang/rust/issues/72327
(self == f32::INFINITY) | (self == f32::NEG_INFINITY)
}

/// Returns `true` if this number is neither infinite nor `NaN`.
Expand Down Expand Up @@ -568,15 +572,76 @@ 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.
// Unfortunately, floating point numbers can be much worse than that.
// This also needs to not result in recursive evaluations of f64::to_bits.
//
// On some processors, in some cases, LLVM will "helpfully" lower floating point ops,
// in spite of a request for them using f32 and f64, to things like x87 operations.
// These have an f64's mantissa, but can have a larger than normal exponent.
// FIXME(jubilee): Using x87 operations is never necessary in order to function
// on x86 processors for Rust-to-Rust calls, so this issue should not happen.
// Code generation should be adjusted to use non-C calling conventions, avoiding this.
//
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: f32 to u32 is fine. Usually.
// If classify has gotten this far, the value is definitely in one of these categories.
unsafe { f32::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.
// 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.
//
// # 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.
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
const unsafe fn partial_classify(self) -> FpCategory {
const EXP_MASK: u32 = 0x7f800000;
const MAN_MASK: u32 = 0x007fffff;

let bits = self.to_bits();
match (bits & MAN_MASK, bits & EXP_MASK) {
// SAFETY: The caller is not asking questions for which this will tell lies.
let b = unsafe { mem::transmute::<f32, u32>(self) };
match (b & MAN_MASK, b & EXP_MASK) {
(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.
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
const fn classify_bits(b: u32) -> FpCategory {
const EXP_MASK: u32 = 0x7f800000;
const MAN_MASK: u32 = 0x007fffff;

match (b & MAN_MASK, b & EXP_MASK) {
(0, EXP_MASK) => FpCategory::Infinite,
(_, EXP_MASK) => FpCategory::Nan,
(0, 0) => FpCategory::Zero,
(_, 0) => FpCategory::Subnormal,
_ => FpCategory::Normal,
}
}
Expand Down Expand Up @@ -616,7 +681,8 @@ impl f32 {
pub const fn is_sign_negative(self) -> bool {
// IEEE754 says: isSignMinus(x) is true if and only if x has negative sign. isSignMinus
// applies to zeros and NaNs as well.
self.to_bits() & 0x8000_0000 != 0
// SAFETY: This is just transmuting to get the sign bit, it's fine.
unsafe { mem::transmute::<f32, u32>(self) & 0x8000_0000 != 0 }
}

/// Takes the reciprocal (inverse) of a number, `1/x`.
Expand Down Expand Up @@ -831,8 +897,49 @@ impl f32 {
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
#[inline]
pub const fn to_bits(self) -> u32 {
// SAFETY: `u32` is a plain old datatype so we can always transmute to it
unsafe { mem::transmute(self) }
// SAFETY: `u32` 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".
// This is not a problem per se, but at least one tier2 platform for Rust
// actually exhibits this behavior by default.
//
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
// i.e. not soft-float, the way Rust does parameter passing can actually alter
// a number that is "not infinity" to have the same exponent as infinity,
// in a slightly unpredictable manner.
//
// 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.
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
const fn ct_f32_to_u32(ct: f32) -> u32 {
match ct.classify() {
FpCategory::Nan => {
panic!("const-eval error: cannot use f32::to_bits on a NaN")
}
FpCategory::Subnormal => {
panic!("const-eval error: cannot use f32::to_bits on a subnormal number")
}
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
// SAFETY: We have a normal floating point number. Now we transmute, i.e. do a bitcopy.
unsafe { mem::transmute::<f32, u32>(ct) }
}
}
}
// SAFETY: `u32` is a plain old datatype so we can always... uh...
// ...look, just pretend you forgot what you just read.
// Stability concerns.
let rt_f32_to_u32 = |rt| unsafe { mem::transmute::<f32, u32>(rt) };
// SAFETY: We use internal implementations that either always work or fail at compile time.
unsafe { intrinsics::const_eval_select((self,), ct_f32_to_u32, rt_f32_to_u32) }
}

/// Raw transmutation from `u32`.
Expand Down Expand Up @@ -876,9 +983,51 @@ impl f32 {
#[must_use]
#[inline]
pub const fn from_bits(v: u32) -> Self {
// SAFETY: `u32` is a plain old datatype so we can always transmute from it
// It turns out the safety issues with sNaN were overblown! Hooray!
unsafe { mem::transmute(v) }
// SAFETY: `u32` 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"
//
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
// i.e. not soft-float, the way Rust does parameter passing can actually alter
// a number that is "not infinity" to have the same exponent as infinity,
// in a slightly unpredictable manner.
//
// 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.
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
const fn ct_u32_to_f32(ct: u32) -> f32 {
match f32::classify_bits(ct) {
FpCategory::Subnormal => {
panic!("const-eval error: cannot use f32::from_bits on a subnormal number")
}
FpCategory::Nan => {
panic!("const-eval error: cannot use f32::from_bits on NaN")
}
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
// SAFETY: It's not a frumious number
unsafe { mem::transmute::<u32, f32>(ct) }
}
}
}
// SAFETY: `u32` is a plain old datatype so we can always... uh...
// ...look, just pretend you forgot what you just read.
// Stability concerns.
let rt_u32_to_f32 = |rt| unsafe { mem::transmute::<u32, f32>(rt) };
// SAFETY: We use internal implementations that either always work or fail at compile time.
unsafe { intrinsics::const_eval_select((v,), ct_u32_to_f32, rt_u32_to_f32) }
}

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