Skip to content

Commit 55543a5

Browse files
committed
float to/from bits and classify: update comments regarding non-conformant hardware
1 parent eefd2ea commit 55543a5

File tree

5 files changed

+43
-394
lines changed

5 files changed

+43
-394
lines changed

library/core/src/num/f32.rs

+11-113
Original file line numberDiff line numberDiff line change
@@ -654,18 +654,19 @@ impl f32 {
654654
pub const fn classify(self) -> FpCategory {
655655
// A previous implementation tried to only use bitmask-based checks,
656656
// using f32::to_bits to transmute the float to its bit repr and match on that.
657-
// Unfortunately, floating point numbers can be much worse than that.
658-
// This also needs to not result in recursive evaluations of f64::to_bits.
657+
// If we only cared about being "technically" correct, that's an entirely legit
658+
// implementation.
659+
//
660+
// Unfortunately, there is hardware out there that does not correctly implement the IEEE
661+
// float semantics Rust relies on: x87 uses a too large mantissa and exponent, and some
662+
// hardware flushes subnormals to zero. Rust will misbehave on such hardware, but we can at
663+
// least try to make things seem as sane as possible by being careful here.
659664
//
660-
// On some processors, in some cases, LLVM will "helpfully" lower floating point ops,
661-
// in spite of a request for them using f32 and f64, to things like x87 operations.
662-
// These have an f64's mantissa, but can have a larger than normal exponent.
663665
// FIXME(jubilee): Using x87 operations is never necessary in order to function
664666
// on x86 processors for Rust-to-Rust calls, so this issue should not happen.
665667
// Code generation should be adjusted to use non-C calling conventions, avoiding this.
666-
//
667668
if self.is_infinite() {
668-
// Thus, a value may compare unequal to infinity, despite having a "full" exponent mask.
669+
// A value may compare unequal to infinity, despite having a "full" exponent mask.
669670
FpCategory::Infinite
670671
} else if self.is_nan() {
671672
// And it may not be NaN, as it can simply be an "overextended" finite value.
@@ -706,20 +707,6 @@ impl f32 {
706707
}
707708
}
708709

709-
// This operates on bits, and only bits, so it can ignore concerns about weird FPUs.
710-
// FIXME(jubilee): In a just world, this would be the entire impl for classify,
711-
// plus a transmute. We do not live in a just world, but we can make it more so.
712-
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
713-
const fn classify_bits(b: u32) -> FpCategory {
714-
match (b & Self::MAN_MASK, b & Self::EXP_MASK) {
715-
(0, Self::EXP_MASK) => FpCategory::Infinite,
716-
(_, Self::EXP_MASK) => FpCategory::Nan,
717-
(0, 0) => FpCategory::Zero,
718-
(_, 0) => FpCategory::Subnormal,
719-
_ => FpCategory::Normal,
720-
}
721-
}
722-
723710
/// Returns `true` if `self` has a positive sign, including `+0.0`, NaNs with
724711
/// positive sign bit and positive infinity.
725712
///
@@ -1140,51 +1127,7 @@ impl f32 {
11401127
#[inline]
11411128
pub const fn to_bits(self) -> u32 {
11421129
// SAFETY: `u32` is a plain old datatype so we can always transmute to it.
1143-
// ...sorta.
1144-
//
1145-
// It turns out that at runtime, it is possible for a floating point number
1146-
// to be subject to a floating point mode that alters nonzero subnormal numbers
1147-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
1148-
// This is not a problem per se, but at least one tier2 platform for Rust
1149-
// actually exhibits this behavior by default.
1150-
//
1151-
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
1152-
// i.e. not soft-float, the way Rust does parameter passing can actually alter
1153-
// a number that is "not infinity" to have the same exponent as infinity,
1154-
// in a slightly unpredictable manner.
1155-
//
1156-
// And, of course evaluating to a NaN value is fairly nondeterministic.
1157-
// More precisely: when NaN should be returned is knowable, but which NaN?
1158-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
1159-
// This function, however, allows observing the bitstring of a NaN,
1160-
// thus introspection on CTFE.
1161-
//
1162-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
1163-
// we reject any of these possible situations from happening.
1164-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1165-
const fn ct_f32_to_u32(ct: f32) -> u32 {
1166-
match ct.classify() {
1167-
FpCategory::Nan => {
1168-
panic!("const-eval error: cannot use f32::to_bits on a NaN")
1169-
}
1170-
FpCategory::Subnormal => {
1171-
panic!("const-eval error: cannot use f32::to_bits on a subnormal number")
1172-
}
1173-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
1174-
// SAFETY: We have a normal floating point number. Now we transmute, i.e. do a bitcopy.
1175-
unsafe { mem::transmute::<f32, u32>(ct) }
1176-
}
1177-
}
1178-
}
1179-
1180-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
1181-
fn rt_f32_to_u32(x: f32) -> u32 {
1182-
// SAFETY: `u32` is a plain old datatype so we can always... uh...
1183-
// ...look, just pretend you forgot what you just read.
1184-
// Stability concerns.
1185-
unsafe { mem::transmute(x) }
1186-
}
1187-
intrinsics::const_eval_select((self,), ct_f32_to_u32, rt_f32_to_u32)
1130+
unsafe { mem::transmute(self) }
11881131
}
11891132

11901133
/// Raw transmutation from `u32`.
@@ -1229,53 +1172,8 @@ impl f32 {
12291172
#[inline]
12301173
pub const fn from_bits(v: u32) -> Self {
12311174
// It turns out the safety issues with sNaN were overblown! Hooray!
1232-
// SAFETY: `u32` is a plain old datatype so we can always transmute from it
1233-
// ...sorta.
1234-
//
1235-
// It turns out that at runtime, it is possible for a floating point number
1236-
// to be subject to floating point modes that alter nonzero subnormal numbers
1237-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
1238-
// This is not a problem usually, but at least one tier2 platform for Rust
1239-
// actually exhibits this behavior by default: thumbv7neon
1240-
// aka "the Neon FPU in AArch32 state"
1241-
//
1242-
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
1243-
// i.e. not soft-float, the way Rust does parameter passing can actually alter
1244-
// a number that is "not infinity" to have the same exponent as infinity,
1245-
// in a slightly unpredictable manner.
1246-
//
1247-
// And, of course evaluating to a NaN value is fairly nondeterministic.
1248-
// More precisely: when NaN should be returned is knowable, but which NaN?
1249-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
1250-
// This function, however, allows observing the bitstring of a NaN,
1251-
// thus introspection on CTFE.
1252-
//
1253-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
1254-
// reject any of these possible situations from happening.
1255-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1256-
const fn ct_u32_to_f32(ct: u32) -> f32 {
1257-
match f32::classify_bits(ct) {
1258-
FpCategory::Subnormal => {
1259-
panic!("const-eval error: cannot use f32::from_bits on a subnormal number")
1260-
}
1261-
FpCategory::Nan => {
1262-
panic!("const-eval error: cannot use f32::from_bits on NaN")
1263-
}
1264-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
1265-
// SAFETY: It's not a frumious number
1266-
unsafe { mem::transmute::<u32, f32>(ct) }
1267-
}
1268-
}
1269-
}
1270-
1271-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
1272-
fn rt_u32_to_f32(x: u32) -> f32 {
1273-
// SAFETY: `u32` is a plain old datatype so we can always... uh...
1274-
// ...look, just pretend you forgot what you just read.
1275-
// Stability concerns.
1276-
unsafe { mem::transmute(x) }
1277-
}
1278-
intrinsics::const_eval_select((v,), ct_u32_to_f32, rt_u32_to_f32)
1175+
// SAFETY: `u32` is a plain old datatype so we can always transmute from it.
1176+
unsafe { mem::transmute(v) }
12791177
}
12801178

12811179
/// Returns the memory representation of this floating point number as a byte array in

library/core/src/num/f64.rs

+10-98
Original file line numberDiff line numberDiff line change
@@ -653,12 +653,14 @@ impl f64 {
653653
pub const fn classify(self) -> FpCategory {
654654
// A previous implementation tried to only use bitmask-based checks,
655655
// using f64::to_bits to transmute the float to its bit repr and match on that.
656-
// Unfortunately, floating point numbers can be much worse than that.
657-
// This also needs to not result in recursive evaluations of f64::to_bits.
656+
// If we only cared about being "technically" correct, that's an entirely legit
657+
// implementation.
658+
//
659+
// Unfortunately, there is hardware out there that does not correctly implement the IEEE
660+
// float semantics Rust relies on: x87 uses a too large exponent, and some hardware flushes
661+
// subnormals to zero. Rust will misbehave on such hardware, but we can at least try to make
662+
// things seem as sane as possible by being careful here.
658663
//
659-
// On some processors, in some cases, LLVM will "helpfully" lower floating point ops,
660-
// in spite of a request for them using f32 and f64, to things like x87 operations.
661-
// These have an f64's mantissa, but can have a larger than normal exponent.
662664
// FIXME(jubilee): Using x87 operations is never necessary in order to function
663665
// on x86 processors for Rust-to-Rust calls, so this issue should not happen.
664666
// Code generation should be adjusted to use non-C calling conventions, avoiding this.
@@ -696,20 +698,6 @@ impl f64 {
696698
}
697699
}
698700

699-
// This operates on bits, and only bits, so it can ignore concerns about weird FPUs.
700-
// FIXME(jubilee): In a just world, this would be the entire impl for classify,
701-
// plus a transmute. We do not live in a just world, but we can make it more so.
702-
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
703-
const fn classify_bits(b: u64) -> FpCategory {
704-
match (b & Self::MAN_MASK, b & Self::EXP_MASK) {
705-
(0, Self::EXP_MASK) => FpCategory::Infinite,
706-
(_, Self::EXP_MASK) => FpCategory::Nan,
707-
(0, 0) => FpCategory::Zero,
708-
(_, 0) => FpCategory::Subnormal,
709-
_ => FpCategory::Normal,
710-
}
711-
}
712-
713701
/// Returns `true` if `self` has a positive sign, including `+0.0`, NaNs with
714702
/// positive sign bit and positive infinity.
715703
///
@@ -1131,33 +1119,7 @@ impl f64 {
11311119
#[inline]
11321120
pub const fn to_bits(self) -> u64 {
11331121
// SAFETY: `u64` is a plain old datatype so we can always transmute to it.
1134-
// ...sorta.
1135-
//
1136-
// See the SAFETY comment in f64::from_bits for more.
1137-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1138-
const fn ct_f64_to_u64(ct: f64) -> u64 {
1139-
match ct.classify() {
1140-
FpCategory::Nan => {
1141-
panic!("const-eval error: cannot use f64::to_bits on a NaN")
1142-
}
1143-
FpCategory::Subnormal => {
1144-
panic!("const-eval error: cannot use f64::to_bits on a subnormal number")
1145-
}
1146-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
1147-
// SAFETY: We have a normal floating point number. Now we transmute, i.e. do a bitcopy.
1148-
unsafe { mem::transmute::<f64, u64>(ct) }
1149-
}
1150-
}
1151-
}
1152-
1153-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
1154-
fn rt_f64_to_u64(rt: f64) -> u64 {
1155-
// SAFETY: `u64` is a plain old datatype so we can always... uh...
1156-
// ...look, just pretend you forgot what you just read.
1157-
// Stability concerns.
1158-
unsafe { mem::transmute::<f64, u64>(rt) }
1159-
}
1160-
intrinsics::const_eval_select((self,), ct_f64_to_u64, rt_f64_to_u64)
1122+
unsafe { mem::transmute(self) }
11611123
}
11621124

11631125
/// Raw transmutation from `u64`.
@@ -1202,58 +1164,8 @@ impl f64 {
12021164
#[inline]
12031165
pub const fn from_bits(v: u64) -> Self {
12041166
// It turns out the safety issues with sNaN were overblown! Hooray!
1205-
// SAFETY: `u64` is a plain old datatype so we can always transmute from it
1206-
// ...sorta.
1207-
//
1208-
// It turns out that at runtime, it is possible for a floating point number
1209-
// to be subject to floating point modes that alter nonzero subnormal numbers
1210-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
1211-
// This is not a problem usually, but at least one tier2 platform for Rust
1212-
// actually exhibits an FTZ behavior by default: thumbv7neon
1213-
// aka "the Neon FPU in AArch32 state"
1214-
//
1215-
// Even with this, not all instructions exhibit the FTZ behaviors on thumbv7neon,
1216-
// so this should load the same bits if LLVM emits the "correct" instructions,
1217-
// but LLVM sometimes makes interesting choices about float optimization,
1218-
// and other FPUs may do similar. Thus, it is wise to indulge luxuriously in caution.
1219-
//
1220-
// In addition, on x86 targets with SSE or SSE2 disabled and the x87 FPU enabled,
1221-
// i.e. not soft-float, the way Rust does parameter passing can actually alter
1222-
// a number that is "not infinity" to have the same exponent as infinity,
1223-
// in a slightly unpredictable manner.
1224-
//
1225-
// And, of course evaluating to a NaN value is fairly nondeterministic.
1226-
// More precisely: when NaN should be returned is knowable, but which NaN?
1227-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
1228-
// This function, however, allows observing the bitstring of a NaN,
1229-
// thus introspection on CTFE.
1230-
//
1231-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
1232-
// reject any of these possible situations from happening.
1233-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1234-
const fn ct_u64_to_f64(ct: u64) -> f64 {
1235-
match f64::classify_bits(ct) {
1236-
FpCategory::Subnormal => {
1237-
panic!("const-eval error: cannot use f64::from_bits on a subnormal number")
1238-
}
1239-
FpCategory::Nan => {
1240-
panic!("const-eval error: cannot use f64::from_bits on NaN")
1241-
}
1242-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
1243-
// SAFETY: It's not a frumious number
1244-
unsafe { mem::transmute::<u64, f64>(ct) }
1245-
}
1246-
}
1247-
}
1248-
1249-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
1250-
fn rt_u64_to_f64(rt: u64) -> f64 {
1251-
// SAFETY: `u64` is a plain old datatype so we can always... uh...
1252-
// ...look, just pretend you forgot what you just read.
1253-
// Stability concerns.
1254-
unsafe { mem::transmute::<u64, f64>(rt) }
1255-
}
1256-
intrinsics::const_eval_select((v,), ct_u64_to_f64, rt_u64_to_f64)
1167+
// SAFETY: `u64` is a plain old datatype so we can always transmute from it.
1168+
unsafe { mem::transmute(v) }
12571169
}
12581170

12591171
/// Returns the memory representation of this floating point number as a byte array in

tests/ui/consts/const-float-bits-conv.rs

+22
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ fn f32() {
3838
const_assert!(f32::from_bits(0x44a72000), 1337.0);
3939
const_assert!(f32::from_ne_bytes(0x44a72000u32.to_ne_bytes()), 1337.0);
4040
const_assert!(f32::from_bits(0xc1640000), -14.25);
41+
42+
// Check that NaNs roundtrip their bits regardless of signalingness
43+
// 0xA is 0b1010; 0x5 is 0b0101 -- so these two together clobbers all the mantissa bits
44+
// ...actually, let's just check that these break. :D
45+
const MASKED_NAN1: u32 = f32::NAN.to_bits() ^ 0x002A_AAAA;
46+
const MASKED_NAN2: u32 = f32::NAN.to_bits() ^ 0x0055_5555;
47+
48+
const_assert!(f32::from_bits(MASKED_NAN1).is_nan());
49+
const_assert!(f32::from_bits(MASKED_NAN1).is_nan());
50+
const_assert!(f32::from_bits(MASKED_NAN1).to_bits(), MASKED_NAN1);
51+
const_assert!(f32::from_bits(MASKED_NAN2).to_bits(), MASKED_NAN2);
4152
}
4253

4354
fn f64() {
@@ -55,6 +66,17 @@ fn f64() {
5566
const_assert!(f64::from_bits(0x4094e40000000000), 1337.0);
5667
const_assert!(f64::from_ne_bytes(0x4094e40000000000u64.to_ne_bytes()), 1337.0);
5768
const_assert!(f64::from_bits(0xc02c800000000000), -14.25);
69+
70+
// Check that NaNs roundtrip their bits regardless of signalingness
71+
// 0xA is 0b1010; 0x5 is 0b0101 -- so these two together clobbers all the mantissa bits
72+
// ...actually, let's just check that these break. :D
73+
const MASKED_NAN1: u64 = f64::NAN.to_bits() ^ 0x000A_AAAA_AAAA_AAAA;
74+
const MASKED_NAN2: u64 = f64::NAN.to_bits() ^ 0x0005_5555_5555_5555;
75+
76+
const_assert!(f64::from_bits(MASKED_NAN1).is_nan());
77+
const_assert!(f64::from_bits(MASKED_NAN1).is_nan());
78+
const_assert!(f64::from_bits(MASKED_NAN1).to_bits(), MASKED_NAN1);
79+
const_assert!(f64::from_bits(MASKED_NAN2).to_bits(), MASKED_NAN2);
5880
}
5981

6082
fn main() {

tests/ui/consts/const-float-bits-reject-conv.rs

-68
This file was deleted.

0 commit comments

Comments
 (0)