Skip to content

Commit 1e9aa8a

Browse files
committed
Auto merge of #95971 - workingjubilee:no-weird-fp-in-const, r=oli-obk
No "weird" floats in const fn {from,to}_bits I suspect this code is subtly incorrect and that we don't even e.g. use x87-style floats in CTFE, so I don't have to guard against that case. A future PR will be hopefully removing them from concern entirely, anyways. But at the moment I wanted to get this rolling because small questions like that one seem best answered by review. r? `@oli-obk` cc `@eddyb` `@thomcc`
2 parents c212fc4 + 4da8682 commit 1e9aa8a

File tree

5 files changed

+479
-48
lines changed

5 files changed

+479
-48
lines changed

library/core/src/num/f32.rs

+158-9
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,76 @@ 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+
//
613+
// # Safety
614+
// This requires making sure you call this function for values it answers correctly on,
615+
// otherwise it returns a wrong answer. This is not important for memory safety per se,
616+
// but getting floats correct is important for not accidentally leaking const eval
617+
// runtime-deviating logic which may or may not be acceptable.
618+
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
619+
const unsafe fn partial_classify(self) -> FpCategory {
571620
const EXP_MASK: u32 = 0x7f800000;
572621
const MAN_MASK: u32 = 0x007fffff;
573622

574-
let bits = self.to_bits();
575-
match (bits & MAN_MASK, bits & EXP_MASK) {
623+
// SAFETY: The caller is not asking questions for which this will tell lies.
624+
let b = unsafe { mem::transmute::<f32, u32>(self) };
625+
match (b & MAN_MASK, b & EXP_MASK) {
576626
(0, 0) => FpCategory::Zero,
577627
(_, 0) => FpCategory::Subnormal,
628+
_ => FpCategory::Normal,
629+
}
630+
}
631+
632+
// This operates on bits, and only bits, so it can ignore concerns about weird FPUs.
633+
// FIXME(jubilee): In a just world, this would be the entire impl for classify,
634+
// plus a transmute. We do not live in a just world, but we can make it more so.
635+
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
636+
const fn classify_bits(b: u32) -> FpCategory {
637+
const EXP_MASK: u32 = 0x7f800000;
638+
const MAN_MASK: u32 = 0x007fffff;
639+
640+
match (b & MAN_MASK, b & EXP_MASK) {
578641
(0, EXP_MASK) => FpCategory::Infinite,
579642
(_, EXP_MASK) => FpCategory::Nan,
643+
(0, 0) => FpCategory::Zero,
644+
(_, 0) => FpCategory::Subnormal,
580645
_ => FpCategory::Normal,
581646
}
582647
}
@@ -616,7 +681,8 @@ impl f32 {
616681
pub const fn is_sign_negative(self) -> bool {
617682
// IEEE754 says: isSignMinus(x) is true if and only if x has negative sign. isSignMinus
618683
// applies to zeros and NaNs as well.
619-
self.to_bits() & 0x8000_0000 != 0
684+
// SAFETY: This is just transmuting to get the sign bit, it's fine.
685+
unsafe { mem::transmute::<f32, u32>(self) & 0x8000_0000 != 0 }
620686
}
621687

622688
/// Takes the reciprocal (inverse) of a number, `1/x`.
@@ -831,8 +897,49 @@ impl f32 {
831897
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
832898
#[inline]
833899
pub const fn to_bits(self) -> u32 {
834-
// SAFETY: `u32` is a plain old datatype so we can always transmute to it
835-
unsafe { mem::transmute(self) }
900+
// SAFETY: `u32` is a plain old datatype so we can always transmute to it.
901+
// ...sorta.
902+
//
903+
// It turns out that at runtime, it is possible for a floating point number
904+
// to be subject to a floating point mode that alters nonzero subnormal numbers
905+
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
906+
// This is not a problem per se, but at least one tier2 platform for Rust
907+
// actually exhibits this behavior by default.
908+
//
909+
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
910+
// i.e. not soft-float, the way Rust does parameter passing can actually alter
911+
// a number that is "not infinity" to have the same exponent as infinity,
912+
// in a slightly unpredictable manner.
913+
//
914+
// And, of course evaluating to a NaN value is fairly nondeterministic.
915+
// More precisely: when NaN should be returned is knowable, but which NaN?
916+
// So far that's defined by a combination of LLVM and the CPU, not Rust.
917+
// This function, however, allows observing the bitstring of a NaN,
918+
// thus introspection on CTFE.
919+
//
920+
// In order to preserve, at least for the moment, const-to-runtime equivalence,
921+
// we reject any of these possible situations from happening.
922+
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
923+
const fn ct_f32_to_u32(ct: f32) -> u32 {
924+
match ct.classify() {
925+
FpCategory::Nan => {
926+
panic!("const-eval error: cannot use f32::to_bits on a NaN")
927+
}
928+
FpCategory::Subnormal => {
929+
panic!("const-eval error: cannot use f32::to_bits on a subnormal number")
930+
}
931+
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
932+
// SAFETY: We have a normal floating point number. Now we transmute, i.e. do a bitcopy.
933+
unsafe { mem::transmute::<f32, u32>(ct) }
934+
}
935+
}
936+
}
937+
// SAFETY: `u32` is a plain old datatype so we can always... uh...
938+
// ...look, just pretend you forgot what you just read.
939+
// Stability concerns.
940+
let rt_f32_to_u32 = |rt| unsafe { mem::transmute::<f32, u32>(rt) };
941+
// SAFETY: We use internal implementations that either always work or fail at compile time.
942+
unsafe { intrinsics::const_eval_select((self,), ct_f32_to_u32, rt_f32_to_u32) }
836943
}
837944

838945
/// Raw transmutation from `u32`.
@@ -876,9 +983,51 @@ impl f32 {
876983
#[must_use]
877984
#[inline]
878985
pub const fn from_bits(v: u32) -> Self {
879-
// SAFETY: `u32` is a plain old datatype so we can always transmute from it
880986
// It turns out the safety issues with sNaN were overblown! Hooray!
881-
unsafe { mem::transmute(v) }
987+
// SAFETY: `u32` is a plain old datatype so we can always transmute from it
988+
// ...sorta.
989+
//
990+
// It turns out that at runtime, it is possible for a floating point number
991+
// to be subject to floating point modes that alter nonzero subnormal numbers
992+
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
993+
// This is not a problem usually, but at least one tier2 platform for Rust
994+
// actually exhibits this behavior by default: thumbv7neon
995+
// aka "the Neon FPU in AArch32 state"
996+
//
997+
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
998+
// i.e. not soft-float, the way Rust does parameter passing can actually alter
999+
// a number that is "not infinity" to have the same exponent as infinity,
1000+
// in a slightly unpredictable manner.
1001+
//
1002+
// And, of course evaluating to a NaN value is fairly nondeterministic.
1003+
// More precisely: when NaN should be returned is knowable, but which NaN?
1004+
// So far that's defined by a combination of LLVM and the CPU, not Rust.
1005+
// This function, however, allows observing the bitstring of a NaN,
1006+
// thus introspection on CTFE.
1007+
//
1008+
// In order to preserve, at least for the moment, const-to-runtime equivalence,
1009+
// reject any of these possible situations from happening.
1010+
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1011+
const fn ct_u32_to_f32(ct: u32) -> f32 {
1012+
match f32::classify_bits(ct) {
1013+
FpCategory::Subnormal => {
1014+
panic!("const-eval error: cannot use f32::from_bits on a subnormal number")
1015+
}
1016+
FpCategory::Nan => {
1017+
panic!("const-eval error: cannot use f32::from_bits on NaN")
1018+
}
1019+
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
1020+
// SAFETY: It's not a frumious number
1021+
unsafe { mem::transmute::<u32, f32>(ct) }
1022+
}
1023+
}
1024+
}
1025+
// SAFETY: `u32` is a plain old datatype so we can always... uh...
1026+
// ...look, just pretend you forgot what you just read.
1027+
// Stability concerns.
1028+
let rt_u32_to_f32 = |rt| unsafe { mem::transmute::<u32, f32>(rt) };
1029+
// SAFETY: We use internal implementations that either always work or fail at compile time.
1030+
unsafe { intrinsics::const_eval_select((v,), ct_u32_to_f32, rt_u32_to_f32) }
8821031
}
8831032

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

0 commit comments

Comments
 (0)