diff options
| author | Ralf Jung <post@ralfj.de> | 2023-10-08 12:03:01 +0200 |
|---|---|---|
| committer | Ralf Jung <post@ralfj.de> | 2023-10-08 20:35:46 +0200 |
| commit | 6796c5765d7b52271cd09ab977cb617ee3971a3a (patch) | |
| tree | 01c142e4e604df2dab1724493b5d230552d61c81 /src/tools/miri/tests/pass | |
| parent | d087c6fae215484bf47ba6dda1bc836ef4c07ac5 (diff) | |
| download | rust-6796c5765d7b52271cd09ab977cb617ee3971a3a.tar.gz rust-6796c5765d7b52271cd09ab977cb617ee3971a3a.zip | |
miri: make NaN generation non-deterministic
Diffstat (limited to 'src/tools/miri/tests/pass')
| -rw-r--r-- | src/tools/miri/tests/pass/float_nan.rs | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/src/tools/miri/tests/pass/float_nan.rs b/src/tools/miri/tests/pass/float_nan.rs new file mode 100644 index 00000000000..8fa567aa106 --- /dev/null +++ b/src/tools/miri/tests/pass/float_nan.rs @@ -0,0 +1,316 @@ +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), + ); +} + +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 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(); +} |
