Skip to content

Commit 08deb0d

Browse files
committed
float-to-float casts also have non-deterministic NaN results
1 parent 615d738 commit 08deb0d

File tree

6 files changed

+150
-17
lines changed

6 files changed

+150
-17
lines changed

compiler/rustc_const_eval/src/interpret/cast.rs

+21-2
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,21 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
311311
F: Float + Into<Scalar<M::Provenance>> + FloatConvert<Single> + FloatConvert<Double>,
312312
{
313313
use rustc_type_ir::sty::TyKind::*;
314+
315+
fn adjust_nan<
316+
'mir,
317+
'tcx: 'mir,
318+
M: Machine<'mir, 'tcx>,
319+
F1: rustc_apfloat::Float + FloatConvert<F2>,
320+
F2: rustc_apfloat::Float,
321+
>(
322+
ecx: &InterpCx<'mir, 'tcx, M>,
323+
f1: F1,
324+
f2: F2,
325+
) -> F2 {
326+
if f2.is_nan() { M::generate_nan(ecx, &[f1]) } else { f2 }
327+
}
328+
314329
match *dest_ty.kind() {
315330
// float -> uint
316331
Uint(t) => {
@@ -330,9 +345,13 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
330345
Scalar::from_int(v, size)
331346
}
332347
// float -> f32
333-
Float(FloatTy::F32) => Scalar::from_f32(f.convert(&mut false).value),
348+
Float(FloatTy::F32) => {
349+
Scalar::from_f32(adjust_nan(self, f, f.convert(&mut false).value))
350+
}
334351
// float -> f64
335-
Float(FloatTy::F64) => Scalar::from_f64(f.convert(&mut false).value),
352+
Float(FloatTy::F64) => {
353+
Scalar::from_f64(adjust_nan(self, f, f.convert(&mut false).value))
354+
}
336355
// That's it.
337356
_ => span_bug!(self.cur_span(), "invalid float to {} cast", dest_ty),
338357
}

compiler/rustc_const_eval/src/interpret/machine.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::borrow::{Borrow, Cow};
66
use std::fmt::Debug;
77
use std::hash::Hash;
88

9-
use rustc_apfloat::Float;
9+
use rustc_apfloat::{Float, FloatConvert};
1010
use rustc_ast::{InlineAsmOptions, InlineAsmTemplatePiece};
1111
use rustc_middle::mir;
1212
use rustc_middle::ty::layout::TyAndLayout;
@@ -243,9 +243,12 @@ pub trait Machine<'mir, 'tcx: 'mir>: Sized {
243243

244244
/// Generate the NaN returned by a float operation, given the list of inputs.
245245
/// (This is all inputs, not just NaN inputs!)
246-
fn generate_nan<F: Float>(_ecx: &InterpCx<'mir, 'tcx, Self>, _inputs: &[F]) -> F {
246+
fn generate_nan<F1: Float + FloatConvert<F2>, F2: Float>(
247+
_ecx: &InterpCx<'mir, 'tcx, Self>,
248+
_inputs: &[F1],
249+
) -> F2 {
247250
// By default we always return the preferred NaN.
248-
F::NAN
251+
F2::NAN
249252
}
250253

251254
/// Called before writing the specified `local` of the `frame`.

compiler/rustc_const_eval/src/interpret/operator.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use rustc_apfloat::Float;
1+
use rustc_apfloat::{Float, FloatConvert};
22
use rustc_middle::mir;
33
use rustc_middle::mir::interpret::{InterpResult, Scalar};
44
use rustc_middle::ty::layout::TyAndLayout;
@@ -104,7 +104,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
104104
(ImmTy::from_bool(res, *self.tcx), false)
105105
}
106106

107-
fn binary_float_op<F: Float + Into<Scalar<M::Provenance>>>(
107+
fn binary_float_op<F: Float + FloatConvert<F> + Into<Scalar<M::Provenance>>>(
108108
&self,
109109
bin_op: mir::BinOp,
110110
layout: TyAndLayout<'tcx>,

src/tools/miri/src/machine.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,10 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
10021002
}
10031003

10041004
#[inline(always)]
1005-
fn generate_nan<F: rustc_apfloat::Float>(ecx: &InterpCx<'mir, 'tcx, Self>, inputs: &[F]) -> F {
1005+
fn generate_nan<F1: rustc_apfloat::Float + rustc_apfloat::FloatConvert<F2>, F2: rustc_apfloat::Float>(
1006+
ecx: &InterpCx<'mir, 'tcx, Self>,
1007+
inputs: &[F1],
1008+
) -> F2 {
10061009
ecx.generate_nan(inputs)
10071010
}
10081011

src/tools/miri/src/operator.rs

+27-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::iter;
33
use log::trace;
44

55
use rand::{seq::IteratorRandom, Rng};
6-
use rustc_apfloat::Float;
6+
use rustc_apfloat::{Float, FloatConvert};
77
use rustc_middle::mir;
88
use rustc_target::abi::Size;
99

@@ -78,17 +78,35 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
7878
})
7979
}
8080

81-
fn generate_nan<F: Float>(&self, inputs: &[F]) -> F {
81+
fn generate_nan<F1: Float + FloatConvert<F2>, F2: Float>(&self, inputs: &[F1]) -> F2 {
82+
/// Make the given NaN a signaling NaN.
83+
/// Returns `None` if this would not result in a NaN.
84+
fn make_signaling<F: Float>(f: F) -> Option<F> {
85+
// The quiet/signaling bit is the leftmost bit in the mantissa.
86+
// That's position `PRECISION-1`, since `PRECISION` includes the fixed leading 1 bit,
87+
// and then we subtract 1 more since this is 0-indexed.
88+
let quiet_bit_mask = 1 << (F::PRECISION - 2);
89+
// Unset the bit. Double-check that this wasn't the last bit set in the payload.
90+
// (which would turn the NaN into an infinity).
91+
let f = F::from_bits(f.to_bits() & !quiet_bit_mask);
92+
if f.is_nan() { Some(f) } else { None }
93+
}
94+
8295
let this = self.eval_context_ref();
8396
let mut rand = this.machine.rng.borrow_mut();
84-
// Assemble an iterator of possible NaNs: preferred, unchanged propagation, quieting propagation.
85-
let preferred_nan = F::qnan(Some(0));
97+
// Assemble an iterator of possible NaNs: preferred, quieting propagation, unchanged propagation.
98+
// On some targets there are more possibilities; for now we just generate those options that
99+
// are possible everywhere.
100+
let preferred_nan = F2::qnan(Some(0));
86101
let nans = iter::once(preferred_nan)
87-
.chain(inputs.iter().filter(|f| f.is_nan()).copied())
88-
.chain(inputs.iter().filter(|f| f.is_signaling()).map(|f| {
89-
// Make it quiet, by setting the bit. We assume that `preferred_nan`
90-
// only has bits set that all quiet NaNs need to have set.
91-
F::from_bits(f.to_bits() | preferred_nan.to_bits())
102+
.chain(inputs.iter().filter(|f| f.is_nan()).map(|&f| {
103+
// Regular apfloat cast is quieting.
104+
f.convert(&mut false).value
105+
}))
106+
.chain(inputs.iter().filter(|f| f.is_signaling()).filter_map(|&f| {
107+
let f: F2 = f.convert(&mut false).value;
108+
// We have to de-quiet this again for unchanged propagation.
109+
make_signaling(f)
92110
}));
93111
// Pick one of the NaNs.
94112
let nan = nans.choose(&mut *rand).unwrap();

src/tools/miri/tests/pass/float_nan.rs

+90
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,95 @@ fn test_f64() {
311311
);
312312
}
313313

314+
fn test_casts() {
315+
let all1_payload_32 = u32_ones(22);
316+
let all1_payload_64 = u64_ones(51);
317+
let left1_payload_64 = (all1_payload_32 as u64) << (51 - 22);
318+
319+
// 64-to-32
320+
check_all_outcomes(
321+
HashSet::from_iter([F32::nan(Pos, Quiet, 0), F32::nan(Neg, Quiet, 0)]),
322+
|| F32::from(F64::nan(Pos, Quiet, 0).as_f64() as f32),
323+
);
324+
// The preferred payload is always a possibility.
325+
check_all_outcomes(
326+
HashSet::from_iter([
327+
F32::nan(Pos, Quiet, 0),
328+
F32::nan(Neg, Quiet, 0),
329+
F32::nan(Pos, Quiet, all1_payload_32),
330+
F32::nan(Neg, Quiet, all1_payload_32),
331+
]),
332+
|| F32::from(F64::nan(Pos, Quiet, all1_payload_64).as_f64() as f32),
333+
);
334+
// If the input is signaling, then the output *may* also be signaling.
335+
check_all_outcomes(
336+
HashSet::from_iter([
337+
F32::nan(Pos, Quiet, 0),
338+
F32::nan(Neg, Quiet, 0),
339+
F32::nan(Pos, Quiet, all1_payload_32),
340+
F32::nan(Neg, Quiet, all1_payload_32),
341+
F32::nan(Pos, Signaling, all1_payload_32),
342+
F32::nan(Neg, Signaling, all1_payload_32),
343+
]),
344+
|| F32::from(F64::nan(Pos, Signaling, all1_payload_64).as_f64() as f32),
345+
);
346+
// Check that the low bits are gone (not the high bits).
347+
check_all_outcomes(
348+
HashSet::from_iter([
349+
F32::nan(Pos, Quiet, 0),
350+
F32::nan(Neg, Quiet, 0),
351+
]),
352+
|| F32::from(F64::nan(Pos, Quiet, 1).as_f64() as f32),
353+
);
354+
check_all_outcomes(
355+
HashSet::from_iter([
356+
F32::nan(Pos, Quiet, 0),
357+
F32::nan(Neg, Quiet, 0),
358+
F32::nan(Pos, Quiet, 1),
359+
F32::nan(Neg, Quiet, 1),
360+
]),
361+
|| F32::from(F64::nan(Pos, Quiet, 1 << (51-22)).as_f64() as f32),
362+
);
363+
check_all_outcomes(
364+
HashSet::from_iter([
365+
F32::nan(Pos, Quiet, 0),
366+
F32::nan(Neg, Quiet, 0),
367+
// The `1` payload becomes `0`, and the `0` payload cannot be signaling,
368+
// so these are the only options.
369+
]),
370+
|| F32::from(F64::nan(Pos, Signaling, 1).as_f64() as f32),
371+
);
372+
373+
// 32-to-64
374+
check_all_outcomes(
375+
HashSet::from_iter([F64::nan(Pos, Quiet, 0), F64::nan(Neg, Quiet, 0)]),
376+
|| F64::from(F32::nan(Pos, Quiet, 0).as_f32() as f64),
377+
);
378+
// The preferred payload is always a possibility.
379+
// Also checks that 0s are added on the right.
380+
check_all_outcomes(
381+
HashSet::from_iter([
382+
F64::nan(Pos, Quiet, 0),
383+
F64::nan(Neg, Quiet, 0),
384+
F64::nan(Pos, Quiet, left1_payload_64),
385+
F64::nan(Neg, Quiet, left1_payload_64),
386+
]),
387+
|| F64::from(F32::nan(Pos, Quiet, all1_payload_32).as_f32() as f64),
388+
);
389+
// If the input is signaling, then the output *may* also be signaling.
390+
check_all_outcomes(
391+
HashSet::from_iter([
392+
F64::nan(Pos, Quiet, 0),
393+
F64::nan(Neg, Quiet, 0),
394+
F64::nan(Pos, Quiet, left1_payload_64),
395+
F64::nan(Neg, Quiet, left1_payload_64),
396+
F64::nan(Pos, Signaling, left1_payload_64),
397+
F64::nan(Neg, Signaling, left1_payload_64),
398+
]),
399+
|| F64::from(F32::nan(Pos, Signaling, all1_payload_32).as_f32() as f64),
400+
);
401+
}
402+
314403
fn main() {
315404
// Check our constants against std, just to be sure.
316405
// We add 1 since our numbers are the number of bits stored
@@ -321,4 +410,5 @@ fn main() {
321410

322411
test_f32();
323412
test_f64();
413+
test_casts();
324414
}

0 commit comments

Comments
 (0)