Skip to content

Commit

Permalink
float-to-float casts also have non-deterministic NaN results
Browse files Browse the repository at this point in the history
  • Loading branch information
RalfJung committed Oct 9, 2023
1 parent 615d738 commit a61dc2a
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 17 deletions.
25 changes: 23 additions & 2 deletions compiler/rustc_const_eval/src/interpret/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,23 @@ 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 {
// We use the *already casted* `f` as "inputs" here since the original `f` has the wrong
// type. We rely on apfloat preserving the NaN payload on casts.
if f2.is_nan() { M::generate_nan(ecx, &[f1]) } else { f2 }
}

match *dest_ty.kind() {
// float -> uint
Uint(t) => {
Expand All @@ -330,9 +347,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),
}
Expand Down
9 changes: 6 additions & 3 deletions compiler/rustc_const_eval/src/interpret/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions compiler/rustc_const_eval/src/interpret/operator.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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>,
Expand Down
5 changes: 4 additions & 1 deletion src/tools/miri/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
36 changes: 27 additions & 9 deletions src/tools/miri/src/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
91 changes: 91 additions & 0 deletions src/tools/miri/tests/pass/float_nan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -321,4 +411,5 @@ fn main() {

test_f32();
test_f64();
test_casts();
}

0 comments on commit a61dc2a

Please sign in to comment.