Skip to content

Commit b200483

Browse files
Rectify float classification impls for weird FPUs
Careful handling does its best to take care of both Armv7's "unenhanced" Neon as well as the x87 FPU.
1 parent dd38eea commit b200483

File tree

2 files changed

+103
-16
lines changed

2 files changed

+103
-16
lines changed

library/core/src/num/f32.rs

+51-8
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,8 @@ impl f32 {
449449
#[inline]
450450
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
451451
pub(crate) const fn abs_private(self) -> f32 {
452-
f32::from_bits(self.to_bits() & 0x7fff_ffff)
452+
// SAFETY: This transmutation is fine. Probably. For the reasons std is using it.
453+
unsafe { mem::transmute::<u32, f32>(mem::transmute::<f32, u32>(self) & 0x7fff_ffff) }
453454
}
454455

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

478482
/// Returns `true` if this number is neither infinite nor `NaN`.
@@ -568,15 +572,53 @@ impl f32 {
568572
#[stable(feature = "rust1", since = "1.0.0")]
569573
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
570574
pub const fn classify(self) -> FpCategory {
575+
// A previous implementation tried to only use bitmask-based checks,
576+
// using f32::to_bits to transmute the float to its bit repr and match on that.
577+
// Unfortunately, floating point numbers can be much worse than that.
578+
// This also needs to not result in recursive evaluations of f64::to_bits.
579+
//
580+
// On some processors, in some cases, LLVM will "helpfully" lower floating point ops,
581+
// in spite of a request for them using f32 and f64, to things like x87 operations.
582+
// These have an f64's mantissa, but can have a larger than normal exponent.
583+
// FIXME(jubilee): Using x87 operations is never necessary in order to function
584+
// on x86 processors for Rust-to-Rust calls, so this issue should not happen.
585+
// Code generation should be adjusted to use non-C calling conventions, avoiding this.
586+
//
587+
if self.is_infinite() {
588+
// Thus, a value may compare unequal to infinity, despite having a "full" exponent mask.
589+
FpCategory::Infinite
590+
} else if self.is_nan() {
591+
// And it may not be NaN, as it can simply be an "overextended" finite value.
592+
FpCategory::Nan
593+
} else {
594+
// However, std can't simply compare to zero to check for zero, either,
595+
// as correctness requires avoiding equality tests that may be Subnormal == -0.0
596+
// because it may be wrong under "denormals are zero" and "flush to zero" modes.
597+
// Most of std's targets don't use those, but they are used for thumbv7neon".
598+
// So, this does use bitpattern matching for the rest.
599+
600+
// SAFETY: f32 to u32 is fine. Usually.
601+
// If classify has gotten this far, the value is definitely in one of these categories.
602+
unsafe { f32::partial_classify(self) }
603+
}
604+
}
605+
606+
// This doesn't actually return a right answer for NaN on purpose,
607+
// seeing as how it cannot correctly discern between a floating point NaN,
608+
// and some normal floating point numbers truncated from an x87 FPU.
609+
// FIXME(jubilee): This probably could at least answer things correctly for Infinity,
610+
// like the f64 version does, but I need to run more checks on how things go on x86.
611+
// I fear losing mantissa data that would have answered that differently.
612+
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
613+
const unsafe fn partial_classify(self) -> FpCategory {
571614
const EXP_MASK: u32 = 0x7f800000;
572615
const MAN_MASK: u32 = 0x007fffff;
573616

574-
let bits = self.to_bits();
575-
match (bits & MAN_MASK, bits & EXP_MASK) {
617+
// SAFETY: The caller is not asking questions for which this will tell lies.
618+
let b = unsafe { mem::transmute::<f32, u32>(self) };
619+
match (b & MAN_MASK, b & EXP_MASK) {
576620
(0, 0) => FpCategory::Zero,
577621
(_, 0) => FpCategory::Subnormal,
578-
(0, EXP_MASK) => FpCategory::Infinite,
579-
(_, EXP_MASK) => FpCategory::Nan,
580622
_ => FpCategory::Normal,
581623
}
582624
}
@@ -616,7 +658,8 @@ impl f32 {
616658
pub const fn is_sign_negative(self) -> bool {
617659
// IEEE754 says: isSignMinus(x) is true if and only if x has negative sign. isSignMinus
618660
// applies to zeros and NaNs as well.
619-
self.to_bits() & 0x8000_0000 != 0
661+
// SAFETY: This is just transmuting to get the sign bit, it's fine.
662+
unsafe { mem::transmute::<f32, u32>(self) & 0x8000_0000 != 0 }
620663
}
621664

622665
/// Takes the reciprocal (inverse) of a number, `1/x`.
@@ -878,7 +921,7 @@ impl f32 {
878921
pub const fn from_bits(v: u32) -> Self {
879922
// SAFETY: `u32` is a plain old datatype so we can always transmute from it
880923
// It turns out the safety issues with sNaN were overblown! Hooray!
881-
unsafe { mem::transmute(v) }
924+
unsafe { mem::transmute::<u32, f32>(v) }
882925
}
883926

884927
/// Return the memory representation of this floating point number as a byte array in

library/core/src/num/f64.rs

+52-8
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,10 @@ impl f64 {
448448
#[inline]
449449
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
450450
pub(crate) const fn abs_private(self) -> f64 {
451-
f64::from_bits(self.to_bits() & 0x7fff_ffff_ffff_ffff)
451+
// SAFETY: This transmutation is fine. Probably. For the reasons std is using it.
452+
unsafe {
453+
mem::transmute::<u64, f64>(mem::transmute::<f64, u64>(self) & 0x7fff_ffff_ffff_ffff)
454+
}
452455
}
453456

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

477483
/// Returns `true` if this number is neither infinite nor `NaN`.
@@ -567,15 +573,50 @@ impl f64 {
567573
#[stable(feature = "rust1", since = "1.0.0")]
568574
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
569575
pub const fn classify(self) -> FpCategory {
576+
// A previous implementation tried to only use bitmask-based checks,
577+
// using f64::to_bits to transmute the float to its bit repr and match on that.
578+
// Unfortunately, floating point numbers can be much worse than that.
579+
// This also needs to not result in recursive evaluations of f64::to_bits.
580+
//
581+
// On some processors, in some cases, LLVM will "helpfully" lower floating point ops,
582+
// in spite of a request for them using f32 and f64, to things like x87 operations.
583+
// These have an f64's mantissa, but can have a larger than normal exponent.
584+
// FIXME(jubilee): Using x87 operations is never necessary in order to function
585+
// on x86 processors for Rust-to-Rust calls, so this issue should not happen.
586+
// Code generation should be adjusted to use non-C calling conventions, avoiding this.
587+
//
588+
// Thus, a value may compare unequal to infinity, despite having a "full" exponent mask.
589+
// And it may not be NaN, as it can simply be an "overextended" finite value.
590+
if self.is_nan() {
591+
FpCategory::Nan
592+
} else {
593+
// However, std can't simply compare to zero to check for zero, either,
594+
// as correctness requires avoiding equality tests that may be Subnormal == -0.0
595+
// because it may be wrong under "denormals are zero" and "flush to zero" modes.
596+
// Most of std's targets don't use those, but they are used for thumbv7neon".
597+
// So, this does use bitpattern matching for the rest.
598+
599+
// SAFETY: f64 to u64 is fine. Usually.
600+
// If control flow has gotten this far, the value is definitely in one of the categories
601+
// that f64::partial_classify can correctly analyze.
602+
unsafe { f64::partial_classify(self) }
603+
}
604+
}
605+
606+
// This doesn't actually return a right answer for NaN on purpose,
607+
// seeing as how it cannot correctly discern between a floating point NaN,
608+
// and some normal floating point numbers truncated from an x87 FPU.
609+
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
610+
const unsafe fn partial_classify(self) -> FpCategory {
570611
const EXP_MASK: u64 = 0x7ff0000000000000;
571612
const MAN_MASK: u64 = 0x000fffffffffffff;
572613

573-
let bits = self.to_bits();
574-
match (bits & MAN_MASK, bits & EXP_MASK) {
614+
// SAFETY: The caller is not asking questions for which this will tell lies.
615+
let b = unsafe { mem::transmute::<f64, u64>(self) };
616+
match (b & MAN_MASK, b & EXP_MASK) {
617+
(0, EXP_MASK) => FpCategory::Infinite,
575618
(0, 0) => FpCategory::Zero,
576619
(_, 0) => FpCategory::Subnormal,
577-
(0, EXP_MASK) => FpCategory::Infinite,
578-
(_, EXP_MASK) => FpCategory::Nan,
579620
_ => FpCategory::Normal,
580621
}
581622
}
@@ -622,7 +663,10 @@ impl f64 {
622663
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
623664
#[inline]
624665
pub const fn is_sign_negative(self) -> bool {
625-
self.to_bits() & 0x8000_0000_0000_0000 != 0
666+
// IEEE754 says: isSignMinus(x) is true if and only if x has negative sign. isSignMinus
667+
// applies to zeros and NaNs as well.
668+
// SAFETY: This is just transmuting to get the sign bit, it's fine.
669+
unsafe { mem::transmute::<f64, u64>(self) & 0x8000_0000_0000_0000 != 0 }
626670
}
627671

628672
#[must_use]
@@ -894,7 +938,7 @@ impl f64 {
894938
pub const fn from_bits(v: u64) -> Self {
895939
// SAFETY: `u64` is a plain old datatype so we can always transmute from it
896940
// It turns out the safety issues with sNaN were overblown! Hooray!
897-
unsafe { mem::transmute(v) }
941+
unsafe { mem::transmute::<u64, f64>(v) }
898942
}
899943

900944
/// Return the memory representation of this floating point number as a byte array in

0 commit comments

Comments
 (0)