Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

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

Lines changed: 51 additions & 8 deletions
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

Lines changed: 52 additions & 8 deletions
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)