From 4a06b7953da8e854a1438821b7d7519cf42e136a Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 9 Oct 2023 07:38:00 +0200 Subject: [PATCH] float-to-float casts also have non-deterministic NaN results --- .../rustc_const_eval/src/interpret/cast.rs | 23 ++++- .../rustc_const_eval/src/interpret/machine.rs | 9 +- .../src/interpret/operator.rs | 4 +- src/tools/miri/src/machine.rs | 5 +- src/tools/miri/src/operator.rs | 36 ++++++-- src/tools/miri/tests/pass/float_nan.rs | 91 +++++++++++++++++++ 6 files changed, 151 insertions(+), 17 deletions(-) diff --git a/compiler/rustc_const_eval/src/interpret/cast.rs b/compiler/rustc_const_eval/src/interpret/cast.rs index b9f88cf635271..b9557eaf6abbf 100644 --- a/compiler/rustc_const_eval/src/interpret/cast.rs +++ b/compiler/rustc_const_eval/src/interpret/cast.rs @@ -311,6 +311,21 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> { F: Float + Into> + FloatConvert + FloatConvert, { use rustc_type_ir::sty::TyKind::*; + + fn adjust_nan< + 'mir, + 'tcx: 'mir, + M: Machine<'mir, 'tcx>, + F1: rustc_apfloat::Float + FloatConvert, + F2: rustc_apfloat::Float, + >( + ecx: &InterpCx<'mir, 'tcx, M>, + f1: F1, + f2: F2, + ) -> F2 { + if f2.is_nan() { M::generate_nan(ecx, &[f1]) } else { f2 } + } + match *dest_ty.kind() { // float -> uint Uint(t) => { @@ -330,9 +345,13 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> { Scalar::from_int(v, size) } // float -> f32 - Float(FloatTy::F32) => Scalar::from_f32(f.convert(&mut false).value), + Float(FloatTy::F32) => { + Scalar::from_f32(adjust_nan(self, f, f.convert(&mut false).value)) + } // float -> f64 - Float(FloatTy::F64) => Scalar::from_f64(f.convert(&mut false).value), + Float(FloatTy::F64) => { + Scalar::from_f64(adjust_nan(self, f, f.convert(&mut false).value)) + } // That's it. _ => span_bug!(self.cur_span(), "invalid float to {} cast", dest_ty), } diff --git a/compiler/rustc_const_eval/src/interpret/machine.rs b/compiler/rustc_const_eval/src/interpret/machine.rs index b172fd9f51774..b615ced6c7690 100644 --- a/compiler/rustc_const_eval/src/interpret/machine.rs +++ b/compiler/rustc_const_eval/src/interpret/machine.rs @@ -6,7 +6,7 @@ use std::borrow::{Borrow, Cow}; use std::fmt::Debug; use std::hash::Hash; -use rustc_apfloat::Float; +use rustc_apfloat::{Float, FloatConvert}; use rustc_ast::{InlineAsmOptions, InlineAsmTemplatePiece}; use rustc_middle::mir; use rustc_middle::ty::layout::TyAndLayout; @@ -243,9 +243,12 @@ pub trait Machine<'mir, 'tcx: 'mir>: Sized { /// Generate the NaN returned by a float operation, given the list of inputs. /// (This is all inputs, not just NaN inputs!) - fn generate_nan(_ecx: &InterpCx<'mir, 'tcx, Self>, _inputs: &[F]) -> F { + fn generate_nan, F2: Float>( + _ecx: &InterpCx<'mir, 'tcx, Self>, + _inputs: &[F1], + ) -> F2 { // By default we always return the preferred NaN. - F::NAN + F2::NAN } /// Called before writing the specified `local` of the `frame`. diff --git a/compiler/rustc_const_eval/src/interpret/operator.rs b/compiler/rustc_const_eval/src/interpret/operator.rs index a685b499d0e9b..53e1756d897aa 100644 --- a/compiler/rustc_const_eval/src/interpret/operator.rs +++ b/compiler/rustc_const_eval/src/interpret/operator.rs @@ -1,4 +1,4 @@ -use rustc_apfloat::Float; +use rustc_apfloat::{Float, FloatConvert}; use rustc_middle::mir; use rustc_middle::mir::interpret::{InterpResult, Scalar}; use rustc_middle::ty::layout::TyAndLayout; @@ -104,7 +104,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> { (ImmTy::from_bool(res, *self.tcx), false) } - fn binary_float_op>>( + fn binary_float_op + Into>>( &self, bin_op: mir::BinOp, layout: TyAndLayout<'tcx>, diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs index d7177a4a1d202..3de27460860c9 100644 --- a/src/tools/miri/src/machine.rs +++ b/src/tools/miri/src/machine.rs @@ -1002,7 +1002,10 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> { } #[inline(always)] - fn generate_nan(ecx: &InterpCx<'mir, 'tcx, Self>, inputs: &[F]) -> F { + fn generate_nan, F2: rustc_apfloat::Float>( + ecx: &InterpCx<'mir, 'tcx, Self>, + inputs: &[F1], + ) -> F2 { ecx.generate_nan(inputs) } diff --git a/src/tools/miri/src/operator.rs b/src/tools/miri/src/operator.rs index d655d1b0465e1..e5a437f95f0ea 100644 --- a/src/tools/miri/src/operator.rs +++ b/src/tools/miri/src/operator.rs @@ -3,7 +3,7 @@ use std::iter; use log::trace; use rand::{seq::IteratorRandom, Rng}; -use rustc_apfloat::Float; +use rustc_apfloat::{Float, FloatConvert}; use rustc_middle::mir; use rustc_target::abi::Size; @@ -78,17 +78,35 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> { }) } - fn generate_nan(&self, inputs: &[F]) -> F { + fn generate_nan, F2: Float>(&self, inputs: &[F1]) -> F2 { + /// Make the given NaN a signaling NaN. + /// Returns `None` if this would not result in a NaN. + fn make_signaling(f: F) -> Option { + // The quiet/signaling bit is the leftmost bit in the mantissa. + // That's position `PRECISION-1`, since `PRECISION` includes the fixed leading 1 bit, + // and then we subtract 1 more since this is 0-indexed. + let quiet_bit_mask = 1 << (F::PRECISION - 2); + // Unset the bit. Double-check that this wasn't the last bit set in the payload. + // (which would turn the NaN into an infinity). + let f = F::from_bits(f.to_bits() & !quiet_bit_mask); + if f.is_nan() { Some(f) } else { None } + } + let this = self.eval_context_ref(); let mut rand = this.machine.rng.borrow_mut(); - // Assemble an iterator of possible NaNs: preferred, unchanged propagation, quieting propagation. - let preferred_nan = F::qnan(Some(0)); + // Assemble an iterator of possible NaNs: preferred, quieting propagation, unchanged propagation. + // On some targets there are more possibilities; for now we just generate those options that + // are possible everywhere. + let preferred_nan = F2::qnan(Some(0)); let nans = iter::once(preferred_nan) - .chain(inputs.iter().filter(|f| f.is_nan()).copied()) - .chain(inputs.iter().filter(|f| f.is_signaling()).map(|f| { - // Make it quiet, by setting the bit. We assume that `preferred_nan` - // only has bits set that all quiet NaNs need to have set. - F::from_bits(f.to_bits() | preferred_nan.to_bits()) + .chain(inputs.iter().filter(|f| f.is_nan()).map(|&f| { + // Regular apfloat cast is quieting. + f.convert(&mut false).value + })) + .chain(inputs.iter().filter(|f| f.is_signaling()).filter_map(|&f| { + let f: F2 = f.convert(&mut false).value; + // We have to de-quiet this again for unchanged propagation. + make_signaling(f) })); // Pick one of the NaNs. let nan = nans.choose(&mut *rand).unwrap(); diff --git a/src/tools/miri/tests/pass/float_nan.rs b/src/tools/miri/tests/pass/float_nan.rs index cb9cc42c60184..6badafe1e1239 100644 --- a/src/tools/miri/tests/pass/float_nan.rs +++ b/src/tools/miri/tests/pass/float_nan.rs @@ -311,6 +311,96 @@ fn test_f64() { ); } +#[allow(unused)] +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 @@ -321,4 +411,5 @@ fn main() { test_f32(); test_f64(); + test_casts(); }