float-to-float casts also have non-deterministic NaN results

This commit is contained in:
Ralf Jung 2023-10-09 07:38:00 +02:00
parent 615d738abe
commit 08deb0daed
6 changed files with 150 additions and 17 deletions

View File

@ -311,6 +311,21 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
F: Float + Into<Scalar<M::Provenance>> + FloatConvert<Single> + FloatConvert<Double>,
{
use rustc_type_ir::sty::TyKind::*;
fn adjust_nan<
'mir,
'tcx: 'mir,
M: Machine<'mir, 'tcx>,
F1: rustc_apfloat::Float + FloatConvert<F2>,
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),
}

View File

@ -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<F: Float>(_ecx: &InterpCx<'mir, 'tcx, Self>, _inputs: &[F]) -> F {
fn generate_nan<F1: Float + FloatConvert<F2>, 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`.

View File

@ -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<F: Float + Into<Scalar<M::Provenance>>>(
fn binary_float_op<F: Float + FloatConvert<F> + Into<Scalar<M::Provenance>>>(
&self,
bin_op: mir::BinOp,
layout: TyAndLayout<'tcx>,

View File

@ -1002,7 +1002,10 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
}
#[inline(always)]
fn generate_nan<F: rustc_apfloat::Float>(ecx: &InterpCx<'mir, 'tcx, Self>, inputs: &[F]) -> F {
fn generate_nan<F1: rustc_apfloat::Float + rustc_apfloat::FloatConvert<F2>, F2: rustc_apfloat::Float>(
ecx: &InterpCx<'mir, 'tcx, Self>,
inputs: &[F1],
) -> F2 {
ecx.generate_nan(inputs)
}

View File

@ -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<F: Float>(&self, inputs: &[F]) -> F {
fn generate_nan<F1: Float + FloatConvert<F2>, 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: Float>(f: F) -> Option<F> {
// 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();

View File

@ -311,6 +311,95 @@ fn test_f64() {
);
}
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 +410,5 @@ fn main() {
test_f32();
test_f64();
test_casts();
}