Skip to content

Commit 426a60a

Browse files
committed
Auto merge of rust-lang#128598 - RalfJung:float-comments, r=workingjubilee
float to/from bits and classify: update for float semantics RFC With rust-lang/rfcs#3514 having been accepted, it is clear that hardware which e.g. flushes subnormal to zero is just non-conformant from a Rust perspective -- this is a hardware bug, or maybe an LLVM backend bug (where LLVM doesn't lower floating-point ops in a way that they have the standardized behavior). So update the comments here to make it clear that we don't have to do any of this, we're just being nice. Also remove the subnormal/NaN checks from the (unstable) const-version of to/from-bits; they are not needed since we decided with the aforementioned RFC that it is okay to get a different result at const-time and at run-time. r? `@workingjubilee` since I think you wrote many of the comments I am editing here.
2 parents 54a50bd + 5f33085 commit 426a60a

File tree

7 files changed

+116
-683
lines changed

7 files changed

+116
-683
lines changed

library/core/src/num/f128.rs

+9-101
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ impl f128 {
290290
#[inline]
291291
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
292292
pub(crate) const fn abs_private(self) -> f128 {
293-
// SAFETY: This transmutation is fine. Probably. For the reasons std is using it.
293+
// SAFETY: This transmutation is fine just like in `to_bits`/`from_bits`.
294294
unsafe {
295295
mem::transmute::<u128, f128>(mem::transmute::<f128, u128>(self) & !Self::SIGN_MASK)
296296
}
@@ -439,22 +439,12 @@ impl f128 {
439439
#[unstable(feature = "f128", issue = "116909")]
440440
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
441441
pub const fn classify(self) -> FpCategory {
442-
// Other float types cannot use a bitwise classify because they may suffer a variety
443-
// of errors if the backend chooses to cast to different float types (x87). `f128` cannot
444-
// fit into any other float types so this is not a concern, and we rely on bit patterns.
442+
// Other float types suffer from various platform bugs that violate the usual IEEE semantics
443+
// and also make bitwise classification not always work reliably. However, `f128` cannot fit
444+
// into any other float types so this is not a concern, and we can rely on bit patterns.
445445

446-
// SAFETY: POD bitcast, same as in `to_bits`.
447-
let bits = unsafe { mem::transmute::<f128, u128>(self) };
448-
Self::classify_bits(bits)
449-
}
450-
451-
/// This operates on bits, and only bits, so it can ignore concerns about weird FPUs.
452-
/// FIXME(jubilee): In a just world, this would be the entire impl for classify,
453-
/// plus a transmute. We do not live in a just world, but we can make it more so.
454-
#[inline]
455-
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
456-
const fn classify_bits(b: u128) -> FpCategory {
457-
match (b & Self::MAN_MASK, b & Self::EXP_MASK) {
446+
let bits = self.to_bits();
447+
match (bits & Self::MAN_MASK, bits & Self::EXP_MASK) {
458448
(0, Self::EXP_MASK) => FpCategory::Infinite,
459449
(_, Self::EXP_MASK) => FpCategory::Nan,
460450
(0, 0) => FpCategory::Zero,
@@ -922,48 +912,7 @@ impl f128 {
922912
#[must_use = "this returns the result of the operation, without modifying the original"]
923913
pub const fn to_bits(self) -> u128 {
924914
// SAFETY: `u128` is a plain old datatype so we can always transmute to it.
925-
// ...sorta.
926-
//
927-
// It turns out that at runtime, it is possible for a floating point number
928-
// to be subject to a floating point mode that alters nonzero subnormal numbers
929-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
930-
//
931-
// And, of course evaluating to a NaN value is fairly nondeterministic.
932-
// More precisely: when NaN should be returned is knowable, but which NaN?
933-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
934-
// This function, however, allows observing the bitstring of a NaN,
935-
// thus introspection on CTFE.
936-
//
937-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
938-
// we reject any of these possible situations from happening.
939-
#[inline]
940-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
941-
const fn ct_f128_to_u128(ct: f128) -> u128 {
942-
// FIXME(f16_f128): we should use `.classify()` like `f32` and `f64`, but that
943-
// is not available on all platforms (needs `netf2` and `unordtf2`). So classify
944-
// the bits instead.
945-
946-
// SAFETY: this is a POD transmutation
947-
let bits = unsafe { mem::transmute::<f128, u128>(ct) };
948-
match f128::classify_bits(bits) {
949-
FpCategory::Nan => {
950-
panic!("const-eval error: cannot use f128::to_bits on a NaN")
951-
}
952-
FpCategory::Subnormal => {
953-
panic!("const-eval error: cannot use f128::to_bits on a subnormal number")
954-
}
955-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => bits,
956-
}
957-
}
958-
959-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
960-
fn rt_f128_to_u128(x: f128) -> u128 {
961-
// SAFETY: `u128` is a plain old datatype so we can always... uh...
962-
// ...look, just pretend you forgot what you just read.
963-
// Stability concerns.
964-
unsafe { mem::transmute(x) }
965-
}
966-
intrinsics::const_eval_select((self,), ct_f128_to_u128, rt_f128_to_u128)
915+
unsafe { mem::transmute(self) }
967916
}
968917

969918
/// Raw transmutation from `u128`.
@@ -1011,49 +960,8 @@ impl f128 {
1011960
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1012961
pub const fn from_bits(v: u128) -> Self {
1013962
// It turns out the safety issues with sNaN were overblown! Hooray!
1014-
// SAFETY: `u128` is a plain old datatype so we can always transmute from it
1015-
// ...sorta.
1016-
//
1017-
// It turns out that at runtime, it is possible for a floating point number
1018-
// to be subject to floating point modes that alter nonzero subnormal numbers
1019-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
1020-
// This is not a problem usually, but at least one tier2 platform for Rust
1021-
// actually exhibits this behavior by default: thumbv7neon
1022-
// aka "the Neon FPU in AArch32 state"
1023-
//
1024-
// And, of course evaluating to a NaN value is fairly nondeterministic.
1025-
// More precisely: when NaN should be returned is knowable, but which NaN?
1026-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
1027-
// This function, however, allows observing the bitstring of a NaN,
1028-
// thus introspection on CTFE.
1029-
//
1030-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
1031-
// reject any of these possible situations from happening.
1032-
#[inline]
1033-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1034-
const fn ct_u128_to_f128(ct: u128) -> f128 {
1035-
match f128::classify_bits(ct) {
1036-
FpCategory::Subnormal => {
1037-
panic!("const-eval error: cannot use f128::from_bits on a subnormal number")
1038-
}
1039-
FpCategory::Nan => {
1040-
panic!("const-eval error: cannot use f128::from_bits on NaN")
1041-
}
1042-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
1043-
// SAFETY: It's not a frumious number
1044-
unsafe { mem::transmute::<u128, f128>(ct) }
1045-
}
1046-
}
1047-
}
1048-
1049-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
1050-
fn rt_u128_to_f128(x: u128) -> f128 {
1051-
// SAFETY: `u128` is a plain old datatype so we can always... uh...
1052-
// ...look, just pretend you forgot what you just read.
1053-
// Stability concerns.
1054-
unsafe { mem::transmute(x) }
1055-
}
1056-
intrinsics::const_eval_select((v,), ct_u128_to_f128, rt_u128_to_f128)
963+
// SAFETY: `u128` is a plain old datatype so we can always transmute from it.
964+
unsafe { mem::transmute(v) }
1057965
}
1058966

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

library/core/src/num/f16.rs

+26-137
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ impl f16 {
284284
#[inline]
285285
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
286286
pub(crate) const fn abs_private(self) -> f16 {
287-
// SAFETY: This transmutation is fine. Probably. For the reasons std is using it.
287+
// SAFETY: This transmutation is fine just like in `to_bits`/`from_bits`.
288288
unsafe { mem::transmute::<u16, f16>(mem::transmute::<f16, u16>(self) & !Self::SIGN_MASK) }
289289
}
290290

@@ -426,15 +426,15 @@ impl f16 {
426426
pub const fn classify(self) -> FpCategory {
427427
// A previous implementation for f32/f64 tried to only use bitmask-based checks,
428428
// using `to_bits` to transmute the float to its bit repr and match on that.
429-
// Unfortunately, floating point numbers can be much worse than that.
430-
// This also needs to not result in recursive evaluations of `to_bits`.
429+
// If we only cared about being "technically" correct, that's an entirely legit
430+
// implementation.
431431
//
432-
433-
// Platforms without native support generally convert to `f32` to perform operations,
434-
// and most of these platforms correctly round back to `f16` after each operation.
435-
// However, some platforms have bugs where they keep the excess `f32` precision (e.g.
436-
// WASM, see llvm/llvm-project#96437). This implementation makes a best-effort attempt
437-
// to account for that excess precision.
432+
// Unfortunately, there are platforms out there that do not correctly implement the IEEE
433+
// float semantics Rust relies on: some hardware flushes denormals to zero, and some
434+
// platforms convert to `f32` to perform operations without properly rounding back (e.g.
435+
// WASM, see llvm/llvm-project#96437). These are platforms bugs, and Rust will misbehave on
436+
// such platforms, but we can at least try to make things seem as sane as possible by being
437+
// careful here.
438438
if self.is_infinite() {
439439
// Thus, a value may compare unequal to infinity, despite having a "full" exponent mask.
440440
FpCategory::Infinite
@@ -446,49 +446,20 @@ impl f16 {
446446
// as correctness requires avoiding equality tests that may be Subnormal == -0.0
447447
// because it may be wrong under "denormals are zero" and "flush to zero" modes.
448448
// Most of std's targets don't use those, but they are used for thumbv7neon.
449-
// So, this does use bitpattern matching for the rest.
450-
451-
// SAFETY: f16 to u16 is fine. Usually.
452-
// If classify has gotten this far, the value is definitely in one of these categories.
453-
unsafe { f16::partial_classify(self) }
454-
}
455-
}
456-
457-
/// This doesn't actually return a right answer for NaN on purpose,
458-
/// seeing as how it cannot correctly discern between a floating point NaN,
459-
/// and some normal floating point numbers truncated from an x87 FPU.
460-
///
461-
/// # Safety
462-
///
463-
/// This requires making sure you call this function for values it answers correctly on,
464-
/// otherwise it returns a wrong answer. This is not important for memory safety per se,
465-
/// but getting floats correct is important for not accidentally leaking const eval
466-
/// runtime-deviating logic which may or may not be acceptable.
467-
#[inline]
468-
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
469-
const unsafe fn partial_classify(self) -> FpCategory {
470-
// SAFETY: The caller is not asking questions for which this will tell lies.
471-
let b = unsafe { mem::transmute::<f16, u16>(self) };
472-
match (b & Self::MAN_MASK, b & Self::EXP_MASK) {
473-
(0, Self::EXP_MASK) => FpCategory::Infinite,
474-
(0, 0) => FpCategory::Zero,
475-
(_, 0) => FpCategory::Subnormal,
476-
_ => FpCategory::Normal,
477-
}
478-
}
479-
480-
/// This operates on bits, and only bits, so it can ignore concerns about weird FPUs.
481-
/// FIXME(jubilee): In a just world, this would be the entire impl for classify,
482-
/// plus a transmute. We do not live in a just world, but we can make it more so.
483-
#[inline]
484-
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
485-
const fn classify_bits(b: u16) -> FpCategory {
486-
match (b & Self::MAN_MASK, b & Self::EXP_MASK) {
487-
(0, Self::EXP_MASK) => FpCategory::Infinite,
488-
(_, Self::EXP_MASK) => FpCategory::Nan,
489-
(0, 0) => FpCategory::Zero,
490-
(_, 0) => FpCategory::Subnormal,
491-
_ => FpCategory::Normal,
449+
// So, this does use bitpattern matching for the rest. On x87, due to the incorrect
450+
// float codegen on this hardware, this doesn't actually return a right answer for NaN
451+
// because it cannot correctly discern between a floating point NaN, and some normal
452+
// floating point numbers truncated from an x87 FPU -- but we took care of NaN above, so
453+
// we are fine.
454+
// FIXME(jubilee): This probably could at least answer things correctly for Infinity,
455+
// like the f64 version does, but I need to run more checks on how things go on x86.
456+
// I fear losing mantissa data that would have answered that differently.
457+
let b = self.to_bits();
458+
match (b & Self::MAN_MASK, b & Self::EXP_MASK) {
459+
(0, 0) => FpCategory::Zero,
460+
(_, 0) => FpCategory::Subnormal,
461+
_ => FpCategory::Normal,
462+
}
492463
}
493464
}
494465

@@ -952,48 +923,7 @@ impl f16 {
952923
#[must_use = "this returns the result of the operation, without modifying the original"]
953924
pub const fn to_bits(self) -> u16 {
954925
// SAFETY: `u16` is a plain old datatype so we can always transmute to it.
955-
// ...sorta.
956-
//
957-
// It turns out that at runtime, it is possible for a floating point number
958-
// to be subject to a floating point mode that alters nonzero subnormal numbers
959-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
960-
//
961-
// And, of course evaluating to a NaN value is fairly nondeterministic.
962-
// More precisely: when NaN should be returned is knowable, but which NaN?
963-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
964-
// This function, however, allows observing the bitstring of a NaN,
965-
// thus introspection on CTFE.
966-
//
967-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
968-
// we reject any of these possible situations from happening.
969-
#[inline]
970-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
971-
const fn ct_f16_to_u16(ct: f16) -> u16 {
972-
// FIXME(f16_f128): we should use `.classify()` like `f32` and `f64`, but we don't yet
973-
// want to rely on that on all platforms because it is nondeterministic (e.g. x86 has
974-
// convention discrepancies calling intrinsics). So just classify the bits instead.
975-
976-
// SAFETY: this is a POD transmutation
977-
let bits = unsafe { mem::transmute::<f16, u16>(ct) };
978-
match f16::classify_bits(bits) {
979-
FpCategory::Nan => {
980-
panic!("const-eval error: cannot use f16::to_bits on a NaN")
981-
}
982-
FpCategory::Subnormal => {
983-
panic!("const-eval error: cannot use f16::to_bits on a subnormal number")
984-
}
985-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => bits,
986-
}
987-
}
988-
989-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
990-
fn rt_f16_to_u16(x: f16) -> u16 {
991-
// SAFETY: `u16` is a plain old datatype so we can always... uh...
992-
// ...look, just pretend you forgot what you just read.
993-
// Stability concerns.
994-
unsafe { mem::transmute(x) }
995-
}
996-
intrinsics::const_eval_select((self,), ct_f16_to_u16, rt_f16_to_u16)
926+
unsafe { mem::transmute(self) }
997927
}
998928

999929
/// Raw transmutation from `u16`.
@@ -1040,49 +970,8 @@ impl f16 {
1040970
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1041971
pub const fn from_bits(v: u16) -> Self {
1042972
// It turns out the safety issues with sNaN were overblown! Hooray!
1043-
// SAFETY: `u16` is a plain old datatype so we can always transmute from it
1044-
// ...sorta.
1045-
//
1046-
// It turns out that at runtime, it is possible for a floating point number
1047-
// to be subject to floating point modes that alter nonzero subnormal numbers
1048-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
1049-
// This is not a problem usually, but at least one tier2 platform for Rust
1050-
// actually exhibits this behavior by default: thumbv7neon
1051-
// aka "the Neon FPU in AArch32 state"
1052-
//
1053-
// And, of course evaluating to a NaN value is fairly nondeterministic.
1054-
// More precisely: when NaN should be returned is knowable, but which NaN?
1055-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
1056-
// This function, however, allows observing the bitstring of a NaN,
1057-
// thus introspection on CTFE.
1058-
//
1059-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
1060-
// reject any of these possible situations from happening.
1061-
#[inline]
1062-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
1063-
const fn ct_u16_to_f16(ct: u16) -> f16 {
1064-
match f16::classify_bits(ct) {
1065-
FpCategory::Subnormal => {
1066-
panic!("const-eval error: cannot use f16::from_bits on a subnormal number")
1067-
}
1068-
FpCategory::Nan => {
1069-
panic!("const-eval error: cannot use f16::from_bits on NaN")
1070-
}
1071-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
1072-
// SAFETY: It's not a frumious number
1073-
unsafe { mem::transmute::<u16, f16>(ct) }
1074-
}
1075-
}
1076-
}
1077-
1078-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
1079-
fn rt_u16_to_f16(x: u16) -> f16 {
1080-
// SAFETY: `u16` is a plain old datatype so we can always... uh...
1081-
// ...look, just pretend you forgot what you just read.
1082-
// Stability concerns.
1083-
unsafe { mem::transmute(x) }
1084-
}
1085-
intrinsics::const_eval_select((v,), ct_u16_to_f16, rt_u16_to_f16)
973+
// SAFETY: `u16` is a plain old datatype so we can always transmute from it.
974+
unsafe { mem::transmute(v) }
1086975
}
1087976

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

0 commit comments

Comments
 (0)