Skip to content

Commit 1aed419

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

File tree

7 files changed

+83
-654
lines changed

7 files changed

+83
-654
lines changed

library/core/src/num/f128.rs

+7-103
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
#![unstable(feature = "f128", issue = "116909")]
1313

1414
use crate::convert::FloatToInt;
15-
#[cfg(not(test))]
16-
use crate::intrinsics;
1715
use crate::mem;
1816
use crate::num::FpCategory;
1917

@@ -439,22 +437,12 @@ impl f128 {
439437
#[unstable(feature = "f128", issue = "116909")]
440438
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
441439
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.
445-
446-
// SAFETY: POD bitcast, same as in `to_bits`.
447-
let bits = unsafe { mem::transmute::<f128, u128>(self) };
448-
Self::classify_bits(bits)
449-
}
440+
// Other float types suffer from various platform bugs that violate the usual IEEE semantics
441+
// and also make bitwise classification not always work reliably. However, `f128` cannot fit
442+
// into any other float types so this is not a concern, and we can rely on bit patterns.
450443

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) {
444+
let bits = self.to_bits();
445+
match (bits & Self::MAN_MASK, bits & Self::EXP_MASK) {
458446
(0, Self::EXP_MASK) => FpCategory::Infinite,
459447
(_, Self::EXP_MASK) => FpCategory::Nan,
460448
(0, 0) => FpCategory::Zero,
@@ -746,48 +734,7 @@ impl f128 {
746734
#[must_use = "this returns the result of the operation, without modifying the original"]
747735
pub const fn to_bits(self) -> u128 {
748736
// SAFETY: `u128` is a plain old datatype so we can always transmute to it.
749-
// ...sorta.
750-
//
751-
// It turns out that at runtime, it is possible for a floating point number
752-
// to be subject to a floating point mode that alters nonzero subnormal numbers
753-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
754-
//
755-
// And, of course evaluating to a NaN value is fairly nondeterministic.
756-
// More precisely: when NaN should be returned is knowable, but which NaN?
757-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
758-
// This function, however, allows observing the bitstring of a NaN,
759-
// thus introspection on CTFE.
760-
//
761-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
762-
// we reject any of these possible situations from happening.
763-
#[inline]
764-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
765-
const fn ct_f128_to_u128(ct: f128) -> u128 {
766-
// FIXME(f16_f128): we should use `.classify()` like `f32` and `f64`, but that
767-
// is not available on all platforms (needs `netf2` and `unordtf2`). So classify
768-
// the bits instead.
769-
770-
// SAFETY: this is a POD transmutation
771-
let bits = unsafe { mem::transmute::<f128, u128>(ct) };
772-
match f128::classify_bits(bits) {
773-
FpCategory::Nan => {
774-
panic!("const-eval error: cannot use f128::to_bits on a NaN")
775-
}
776-
FpCategory::Subnormal => {
777-
panic!("const-eval error: cannot use f128::to_bits on a subnormal number")
778-
}
779-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => bits,
780-
}
781-
}
782-
783-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
784-
fn rt_f128_to_u128(x: f128) -> u128 {
785-
// SAFETY: `u128` is a plain old datatype so we can always... uh...
786-
// ...look, just pretend you forgot what you just read.
787-
// Stability concerns.
788-
unsafe { mem::transmute(x) }
789-
}
790-
intrinsics::const_eval_select((self,), ct_f128_to_u128, rt_f128_to_u128)
737+
unsafe { mem::transmute(self) }
791738
}
792739

793740
/// Raw transmutation from `u128`.
@@ -834,50 +781,7 @@ impl f128 {
834781
#[unstable(feature = "f128", issue = "116909")]
835782
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
836783
pub const fn from_bits(v: u128) -> Self {
837-
// It turns out the safety issues with sNaN were overblown! Hooray!
838-
// SAFETY: `u128` is a plain old datatype so we can always transmute from it
839-
// ...sorta.
840-
//
841-
// It turns out that at runtime, it is possible for a floating point number
842-
// to be subject to floating point modes that alter nonzero subnormal numbers
843-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
844-
// This is not a problem usually, but at least one tier2 platform for Rust
845-
// actually exhibits this behavior by default: thumbv7neon
846-
// aka "the Neon FPU in AArch32 state"
847-
//
848-
// And, of course evaluating to a NaN value is fairly nondeterministic.
849-
// More precisely: when NaN should be returned is knowable, but which NaN?
850-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
851-
// This function, however, allows observing the bitstring of a NaN,
852-
// thus introspection on CTFE.
853-
//
854-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
855-
// reject any of these possible situations from happening.
856-
#[inline]
857-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
858-
const fn ct_u128_to_f128(ct: u128) -> f128 {
859-
match f128::classify_bits(ct) {
860-
FpCategory::Subnormal => {
861-
panic!("const-eval error: cannot use f128::from_bits on a subnormal number")
862-
}
863-
FpCategory::Nan => {
864-
panic!("const-eval error: cannot use f128::from_bits on NaN")
865-
}
866-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
867-
// SAFETY: It's not a frumious number
868-
unsafe { mem::transmute::<u128, f128>(ct) }
869-
}
870-
}
871-
}
872-
873-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
874-
fn rt_u128_to_f128(x: u128) -> f128 {
875-
// SAFETY: `u128` is a plain old datatype so we can always... uh...
876-
// ...look, just pretend you forgot what you just read.
877-
// Stability concerns.
878-
unsafe { mem::transmute(x) }
879-
}
880-
intrinsics::const_eval_select((v,), ct_u128_to_f128, rt_u128_to_f128)
784+
unsafe { mem::transmute(v) }
881785
}
882786

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

library/core/src/num/f16.rs

+17-127
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
#![unstable(feature = "f16", issue = "116909")]
1313

1414
use crate::convert::FloatToInt;
15-
#[cfg(not(test))]
16-
use crate::intrinsics;
1715
use crate::mem;
1816
use crate::num::FpCategory;
1917

@@ -426,15 +424,15 @@ impl f16 {
426424
pub const fn classify(self) -> FpCategory {
427425
// A previous implementation for f32/f64 tried to only use bitmask-based checks,
428426
// 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`.
427+
// If we only cared about being "technically" correct, that's an entirely legit
428+
// implementation.
431429
//
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.
430+
// Unfortunately, there are platforms out there that do not correctly implement the IEEE
431+
// float semantics Rust relies on: some hardware flushes denormals to zero, and some
432+
// platforms convert to `f32` to perform operations without properly rounding back (e.g.
433+
// WASM, see llvm/llvm-project#96437). These are platforms bugs, and Rust will misbehave on
434+
// such platforms, but we can at least try to make things seem as sane as possible by being
435+
// careful here.
438436
if self.is_infinite() {
439437
// Thus, a value may compare unequal to infinity, despite having a "full" exponent mask.
440438
FpCategory::Infinite
@@ -447,45 +445,19 @@ impl f16 {
447445
// because it may be wrong under "denormals are zero" and "flush to zero" modes.
448446
// Most of std's targets don't use those, but they are used for thumbv7neon.
449447
// 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,
448+
f16::partial_classify(self)
477449
}
478450
}
479451

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.
452+
/// On x87, due do the incorrect float codegen on this hardware, this doesn't actually return a
453+
/// right answer for NaN because it cannot correctly discern between a floating point NaN, and
454+
/// some normal floating point numbers truncated from an x87 FPU.
483455
#[inline]
484456
#[rustc_const_unstable(feature = "const_float_classify", issue = "72505")]
485-
const fn classify_bits(b: u16) -> FpCategory {
457+
const fn partial_classify(self) -> FpCategory {
458+
let b = self.to_bits();
486459
match (b & Self::MAN_MASK, b & Self::EXP_MASK) {
487460
(0, Self::EXP_MASK) => FpCategory::Infinite,
488-
(_, Self::EXP_MASK) => FpCategory::Nan,
489461
(0, 0) => FpCategory::Zero,
490462
(_, 0) => FpCategory::Subnormal,
491463
_ => FpCategory::Normal,
@@ -781,48 +753,7 @@ impl f16 {
781753
#[must_use = "this returns the result of the operation, without modifying the original"]
782754
pub const fn to_bits(self) -> u16 {
783755
// SAFETY: `u16` is a plain old datatype so we can always transmute to it.
784-
// ...sorta.
785-
//
786-
// It turns out that at runtime, it is possible for a floating point number
787-
// to be subject to a floating point mode that alters nonzero subnormal numbers
788-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
789-
//
790-
// And, of course evaluating to a NaN value is fairly nondeterministic.
791-
// More precisely: when NaN should be returned is knowable, but which NaN?
792-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
793-
// This function, however, allows observing the bitstring of a NaN,
794-
// thus introspection on CTFE.
795-
//
796-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
797-
// we reject any of these possible situations from happening.
798-
#[inline]
799-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
800-
const fn ct_f16_to_u16(ct: f16) -> u16 {
801-
// FIXME(f16_f128): we should use `.classify()` like `f32` and `f64`, but we don't yet
802-
// want to rely on that on all platforms because it is nondeterministic (e.g. x86 has
803-
// convention discrepancies calling intrinsics). So just classify the bits instead.
804-
805-
// SAFETY: this is a POD transmutation
806-
let bits = unsafe { mem::transmute::<f16, u16>(ct) };
807-
match f16::classify_bits(bits) {
808-
FpCategory::Nan => {
809-
panic!("const-eval error: cannot use f16::to_bits on a NaN")
810-
}
811-
FpCategory::Subnormal => {
812-
panic!("const-eval error: cannot use f16::to_bits on a subnormal number")
813-
}
814-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => bits,
815-
}
816-
}
817-
818-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
819-
fn rt_f16_to_u16(x: f16) -> u16 {
820-
// SAFETY: `u16` is a plain old datatype so we can always... uh...
821-
// ...look, just pretend you forgot what you just read.
822-
// Stability concerns.
823-
unsafe { mem::transmute(x) }
824-
}
825-
intrinsics::const_eval_select((self,), ct_f16_to_u16, rt_f16_to_u16)
756+
unsafe { mem::transmute(self) }
826757
}
827758

828759
/// Raw transmutation from `u16`.
@@ -869,49 +800,8 @@ impl f16 {
869800
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
870801
pub const fn from_bits(v: u16) -> Self {
871802
// It turns out the safety issues with sNaN were overblown! Hooray!
872-
// SAFETY: `u16` is a plain old datatype so we can always transmute from it
873-
// ...sorta.
874-
//
875-
// It turns out that at runtime, it is possible for a floating point number
876-
// to be subject to floating point modes that alter nonzero subnormal numbers
877-
// to zero on reads and writes, aka "denormals are zero" and "flush to zero".
878-
// This is not a problem usually, but at least one tier2 platform for Rust
879-
// actually exhibits this behavior by default: thumbv7neon
880-
// aka "the Neon FPU in AArch32 state"
881-
//
882-
// And, of course evaluating to a NaN value is fairly nondeterministic.
883-
// More precisely: when NaN should be returned is knowable, but which NaN?
884-
// So far that's defined by a combination of LLVM and the CPU, not Rust.
885-
// This function, however, allows observing the bitstring of a NaN,
886-
// thus introspection on CTFE.
887-
//
888-
// In order to preserve, at least for the moment, const-to-runtime equivalence,
889-
// reject any of these possible situations from happening.
890-
#[inline]
891-
#[rustc_const_unstable(feature = "const_float_bits_conv", issue = "72447")]
892-
const fn ct_u16_to_f16(ct: u16) -> f16 {
893-
match f16::classify_bits(ct) {
894-
FpCategory::Subnormal => {
895-
panic!("const-eval error: cannot use f16::from_bits on a subnormal number")
896-
}
897-
FpCategory::Nan => {
898-
panic!("const-eval error: cannot use f16::from_bits on NaN")
899-
}
900-
FpCategory::Infinite | FpCategory::Normal | FpCategory::Zero => {
901-
// SAFETY: It's not a frumious number
902-
unsafe { mem::transmute::<u16, f16>(ct) }
903-
}
904-
}
905-
}
906-
907-
#[inline(always)] // See https://github.com/rust-lang/compiler-builtins/issues/491
908-
fn rt_u16_to_f16(x: u16) -> f16 {
909-
// SAFETY: `u16` is a plain old datatype so we can always... uh...
910-
// ...look, just pretend you forgot what you just read.
911-
// Stability concerns.
912-
unsafe { mem::transmute(x) }
913-
}
914-
intrinsics::const_eval_select((v,), ct_u16_to_f16, rt_u16_to_f16)
803+
// SAFETY: `u16` is a plain old datatype so we can always transmute from it.
804+
unsafe { mem::transmute(v) }
915805
}
916806

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

0 commit comments

Comments
 (0)