blob: 9b0a40c41b9cba3d2329fa14bdbf51d98d8b9086 [file] [log] [blame]
use std::collections::HashSet;
use std::fmt;
use std::hash::Hash;
use std::hint::black_box;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Sign {
Neg = 1,
Pos = 0,
}
use Sign::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum NaNKind {
Quiet = 1,
Signaling = 0,
}
use NaNKind::*;
#[track_caller]
fn check_all_outcomes<T: Eq + Hash + fmt::Display>(expected: HashSet<T>, generate: impl Fn() -> T) {
let mut seen = HashSet::new();
// Let's give it 8x as many tries as we are expecting values.
let tries = expected.len() * 8;
for _ in 0..tries {
let val = generate();
assert!(expected.contains(&val), "got an unexpected value: {val}");
seen.insert(val);
}
// Let's see if we saw them all.
for val in expected {
if !seen.contains(&val) {
panic!("did not get value that should be possible: {val}");
}
}
}
// -- f32 support
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
struct F32(u32);
impl From<f32> for F32 {
fn from(x: f32) -> Self {
F32(x.to_bits())
}
}
/// Returns a value that is `ones` many 1-bits.
fn u32_ones(ones: u32) -> u32 {
assert!(ones <= 32);
if ones == 0 {
// `>>` by 32 doesn't actually shift. So inconsistent :(
return 0;
}
u32::MAX >> (32 - ones)
}
const F32_SIGN_BIT: u32 = 32 - 1; // position of the sign bit
const F32_EXP: u32 = 8; // 8 bits of exponent
const F32_MANTISSA: u32 = F32_SIGN_BIT - F32_EXP;
const F32_NAN_PAYLOAD: u32 = F32_MANTISSA - 1;
impl fmt::Display for F32 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Alaways show raw bits.
write!(f, "0x{:08x} ", self.0)?;
// Also show nice version.
let val = self.0;
let sign = val >> F32_SIGN_BIT;
let val = val & u32_ones(F32_SIGN_BIT); // mask away sign
let exp = val >> F32_MANTISSA;
let mantissa = val & u32_ones(F32_MANTISSA);
if exp == u32_ones(F32_EXP) {
// A NaN! Special printing.
let sign = if sign != 0 { Neg } else { Pos };
let quiet = if (mantissa >> F32_NAN_PAYLOAD) != 0 { Quiet } else { Signaling };
let payload = mantissa & u32_ones(F32_NAN_PAYLOAD);
write!(f, "(NaN: {:?}, {:?}, payload = {:#x})", sign, quiet, payload)
} else {
// Normal float value.
write!(f, "({})", f32::from_bits(self.0))
}
}
}
impl F32 {
fn nan(sign: Sign, kind: NaNKind, payload: u32) -> Self {
// Either the quiet bit must be set of the payload must be non-0;
// otherwise this is not a NaN but an infinity.
assert!(kind == Quiet || payload != 0);
// Payload must fit in 22 bits.
assert!(payload < (1 << F32_NAN_PAYLOAD));
// Concatenate the bits (with a 22bit payload).
// Pattern: [negative] ++ [1]^8 ++ [quiet] ++ [payload]
let val = ((sign as u32) << F32_SIGN_BIT)
| (u32_ones(F32_EXP) << F32_MANTISSA)
| ((kind as u32) << F32_NAN_PAYLOAD)
| payload;
// Sanity check.
assert!(f32::from_bits(val).is_nan());
// Done!
F32(val)
}
fn as_f32(self) -> f32 {
black_box(f32::from_bits(self.0))
}
}
// -- f64 support
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
struct F64(u64);
impl From<f64> for F64 {
fn from(x: f64) -> Self {
F64(x.to_bits())
}
}
/// Returns a value that is `ones` many 1-bits.
fn u64_ones(ones: u32) -> u64 {
assert!(ones <= 64);
if ones == 0 {
// `>>` by 32 doesn't actually shift. So inconsistent :(
return 0;
}
u64::MAX >> (64 - ones)
}
const F64_SIGN_BIT: u32 = 64 - 1; // position of the sign bit
const F64_EXP: u32 = 11; // 11 bits of exponent
const F64_MANTISSA: u32 = F64_SIGN_BIT - F64_EXP;
const F64_NAN_PAYLOAD: u32 = F64_MANTISSA - 1;
impl fmt::Display for F64 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Alaways show raw bits.
write!(f, "0x{:08x} ", self.0)?;
// Also show nice version.
let val = self.0;
let sign = val >> F64_SIGN_BIT;
let val = val & u64_ones(F64_SIGN_BIT); // mask away sign
let exp = val >> F64_MANTISSA;
let mantissa = val & u64_ones(F64_MANTISSA);
if exp == u64_ones(F64_EXP) {
// A NaN! Special printing.
let sign = if sign != 0 { Neg } else { Pos };
let quiet = if (mantissa >> F64_NAN_PAYLOAD) != 0 { Quiet } else { Signaling };
let payload = mantissa & u64_ones(F64_NAN_PAYLOAD);
write!(f, "(NaN: {:?}, {:?}, payload = {:#x})", sign, quiet, payload)
} else {
// Normal float value.
write!(f, "({})", f64::from_bits(self.0))
}
}
}
impl F64 {
fn nan(sign: Sign, kind: NaNKind, payload: u64) -> Self {
// Either the quiet bit must be set of the payload must be non-0;
// otherwise this is not a NaN but an infinity.
assert!(kind == Quiet || payload != 0);
// Payload must fit in 52 bits.
assert!(payload < (1 << F64_NAN_PAYLOAD));
// Concatenate the bits (with a 52bit payload).
// Pattern: [negative] ++ [1]^11 ++ [quiet] ++ [payload]
let val = ((sign as u64) << F64_SIGN_BIT)
| (u64_ones(F64_EXP) << F64_MANTISSA)
| ((kind as u64) << F64_NAN_PAYLOAD)
| payload;
// Sanity check.
assert!(f64::from_bits(val).is_nan());
// Done!
F64(val)
}
fn as_f64(self) -> f64 {
black_box(f64::from_bits(self.0))
}
}
// -- actual tests
fn test_f32() {
// Freshly generated NaNs can have either sign.
check_all_outcomes(
HashSet::from_iter([F32::nan(Pos, Quiet, 0), F32::nan(Neg, Quiet, 0)]),
|| F32::from(0.0 / black_box(0.0)),
);
// When there are NaN inputs, their payload can be propagated, with any sign.
let all1_payload = u32_ones(22);
let all1 = F32::nan(Pos, Quiet, all1_payload).as_f32();
check_all_outcomes(
HashSet::from_iter([
F32::nan(Pos, Quiet, 0),
F32::nan(Neg, Quiet, 0),
F32::nan(Pos, Quiet, all1_payload),
F32::nan(Neg, Quiet, all1_payload),
]),
|| F32::from(0.0 + all1),
);
// When there are two NaN inputs, the output can be either one, or the preferred NaN.
let just1 = F32::nan(Neg, Quiet, 1).as_f32();
check_all_outcomes(
HashSet::from_iter([
F32::nan(Pos, Quiet, 0),
F32::nan(Neg, Quiet, 0),
F32::nan(Pos, Quiet, 1),
F32::nan(Neg, Quiet, 1),
F32::nan(Pos, Quiet, all1_payload),
F32::nan(Neg, Quiet, all1_payload),
]),
|| F32::from(just1 - all1),
);
// When there are *signaling* NaN inputs, they might be quieted or not.
let all1_snan = F32::nan(Pos, Signaling, all1_payload).as_f32();
check_all_outcomes(
HashSet::from_iter([
F32::nan(Pos, Quiet, 0),
F32::nan(Neg, Quiet, 0),
F32::nan(Pos, Quiet, all1_payload),
F32::nan(Neg, Quiet, all1_payload),
F32::nan(Pos, Signaling, all1_payload),
F32::nan(Neg, Signaling, all1_payload),
]),
|| F32::from(0.0 * all1_snan),
);
// Mix signaling and non-signaling NaN.
check_all_outcomes(
HashSet::from_iter([
F32::nan(Pos, Quiet, 0),
F32::nan(Neg, Quiet, 0),
F32::nan(Pos, Quiet, 1),
F32::nan(Neg, Quiet, 1),
F32::nan(Pos, Quiet, all1_payload),
F32::nan(Neg, Quiet, all1_payload),
F32::nan(Pos, Signaling, all1_payload),
F32::nan(Neg, Signaling, all1_payload),
]),
|| F32::from(just1 % all1_snan),
);
// Unary `-` must preserve payloads exactly.
check_all_outcomes(HashSet::from_iter([F32::nan(Neg, Quiet, all1_payload)]), || {
F32::from(-all1)
});
check_all_outcomes(HashSet::from_iter([F32::nan(Neg, Signaling, all1_payload)]), || {
F32::from(-all1_snan)
});
}
fn test_f64() {
// Freshly generated NaNs can have either sign.
check_all_outcomes(
HashSet::from_iter([F64::nan(Pos, Quiet, 0), F64::nan(Neg, Quiet, 0)]),
|| F64::from(0.0 / black_box(0.0)),
);
// When there are NaN inputs, their payload can be propagated, with any sign.
let all1_payload = u64_ones(51);
let all1 = F64::nan(Pos, Quiet, all1_payload).as_f64();
check_all_outcomes(
HashSet::from_iter([
F64::nan(Pos, Quiet, 0),
F64::nan(Neg, Quiet, 0),
F64::nan(Pos, Quiet, all1_payload),
F64::nan(Neg, Quiet, all1_payload),
]),
|| F64::from(0.0 + all1),
);
// When there are two NaN inputs, the output can be either one, or the preferred NaN.
let just1 = F64::nan(Neg, Quiet, 1).as_f64();
check_all_outcomes(
HashSet::from_iter([
F64::nan(Pos, Quiet, 0),
F64::nan(Neg, Quiet, 0),
F64::nan(Pos, Quiet, 1),
F64::nan(Neg, Quiet, 1),
F64::nan(Pos, Quiet, all1_payload),
F64::nan(Neg, Quiet, all1_payload),
]),
|| F64::from(just1 - all1),
);
// When there are *signaling* NaN inputs, they might be quieted or not.
let all1_snan = F64::nan(Pos, Signaling, all1_payload).as_f64();
check_all_outcomes(
HashSet::from_iter([
F64::nan(Pos, Quiet, 0),
F64::nan(Neg, Quiet, 0),
F64::nan(Pos, Quiet, all1_payload),
F64::nan(Neg, Quiet, all1_payload),
F64::nan(Pos, Signaling, all1_payload),
F64::nan(Neg, Signaling, all1_payload),
]),
|| F64::from(0.0 * all1_snan),
);
// Mix signaling and non-signaling NaN.
check_all_outcomes(
HashSet::from_iter([
F64::nan(Pos, Quiet, 0),
F64::nan(Neg, Quiet, 0),
F64::nan(Pos, Quiet, 1),
F64::nan(Neg, Quiet, 1),
F64::nan(Pos, Quiet, all1_payload),
F64::nan(Neg, Quiet, all1_payload),
F64::nan(Pos, Signaling, all1_payload),
F64::nan(Neg, Signaling, all1_payload),
]),
|| F64::from(just1 % all1_snan),
);
}
fn test_casts() {
let all1_payload_32 = u32_ones(22);
let all1_payload_64 = u64_ones(51);
let left1_payload_64 = (all1_payload_32 as u64) << (51 - 22);
// 64-to-32
check_all_outcomes(
HashSet::from_iter([F32::nan(Pos, Quiet, 0), F32::nan(Neg, Quiet, 0)]),
|| F32::from(F64::nan(Pos, Quiet, 0).as_f64() as f32),
);
// The preferred payload is always a possibility.
check_all_outcomes(
HashSet::from_iter([
F32::nan(Pos, Quiet, 0),
F32::nan(Neg, Quiet, 0),
F32::nan(Pos, Quiet, all1_payload_32),
F32::nan(Neg, Quiet, all1_payload_32),
]),
|| F32::from(F64::nan(Pos, Quiet, all1_payload_64).as_f64() as f32),
);
// If the input is signaling, then the output *may* also be signaling.
check_all_outcomes(
HashSet::from_iter([
F32::nan(Pos, Quiet, 0),
F32::nan(Neg, Quiet, 0),
F32::nan(Pos, Quiet, all1_payload_32),
F32::nan(Neg, Quiet, all1_payload_32),
F32::nan(Pos, Signaling, all1_payload_32),
F32::nan(Neg, Signaling, all1_payload_32),
]),
|| F32::from(F64::nan(Pos, Signaling, all1_payload_64).as_f64() as f32),
);
// Check that the low bits are gone (not the high bits).
check_all_outcomes(
HashSet::from_iter([F32::nan(Pos, Quiet, 0), F32::nan(Neg, Quiet, 0)]),
|| F32::from(F64::nan(Pos, Quiet, 1).as_f64() as f32),
);
check_all_outcomes(
HashSet::from_iter([
F32::nan(Pos, Quiet, 0),
F32::nan(Neg, Quiet, 0),
F32::nan(Pos, Quiet, 1),
F32::nan(Neg, Quiet, 1),
]),
|| F32::from(F64::nan(Pos, Quiet, 1 << (51 - 22)).as_f64() as f32),
);
check_all_outcomes(
HashSet::from_iter([
F32::nan(Pos, Quiet, 0),
F32::nan(Neg, Quiet, 0),
// The `1` payload becomes `0`, and the `0` payload cannot be signaling,
// so these are the only options.
]),
|| F32::from(F64::nan(Pos, Signaling, 1).as_f64() as f32),
);
// 32-to-64
check_all_outcomes(
HashSet::from_iter([F64::nan(Pos, Quiet, 0), F64::nan(Neg, Quiet, 0)]),
|| F64::from(F32::nan(Pos, Quiet, 0).as_f32() as f64),
);
// The preferred payload is always a possibility.
// Also checks that 0s are added on the right.
check_all_outcomes(
HashSet::from_iter([
F64::nan(Pos, Quiet, 0),
F64::nan(Neg, Quiet, 0),
F64::nan(Pos, Quiet, left1_payload_64),
F64::nan(Neg, Quiet, left1_payload_64),
]),
|| F64::from(F32::nan(Pos, Quiet, all1_payload_32).as_f32() as f64),
);
// If the input is signaling, then the output *may* also be signaling.
check_all_outcomes(
HashSet::from_iter([
F64::nan(Pos, Quiet, 0),
F64::nan(Neg, Quiet, 0),
F64::nan(Pos, Quiet, left1_payload_64),
F64::nan(Neg, Quiet, left1_payload_64),
F64::nan(Pos, Signaling, left1_payload_64),
F64::nan(Neg, Signaling, left1_payload_64),
]),
|| F64::from(F32::nan(Pos, Signaling, all1_payload_32).as_f32() as f64),
);
}
fn main() {
// Check our constants against std, just to be sure.
// We add 1 since our numbers are the number of bits stored
// to represent the value, and std has the precision of the value,
// which is one more due to the implicit leading 1.
assert_eq!(F32_MANTISSA + 1, f32::MANTISSA_DIGITS);
assert_eq!(F64_MANTISSA + 1, f64::MANTISSA_DIGITS);
test_f32();
test_f64();
test_casts();
}