Skip to content

Commit 2764b3c

Browse files
committed
Auto merge of #138062 - LorrensP-2158466:miri-enable-float-nondet, r=RalfJung
Enable Non-determinism of float operations in Miri and change std tests Links to [#4208](#4208) and [#3555](#3555) in Miri. Non-determinism of floating point operations was disabled in rust-lang/rust#137594 because it breaks the tests and doc-tests in core/coretests and std. This PR enables some of them. This pr includes the following changes: - Enables the float non-determinism but with a lower relative error of 4ULP instead of 16ULP - These operations now have a fixed output based on the C23 standard, except the pow operations, this is tracked in [#4286](#4286 (comment)) - Changes tests that made incorrect assumptions about the operations, not to make that assumption anymore (from `assert_eq!` to `assert_approx_eq!`. - Changed the doctests of the stdlib of these operations to compare against fixed constants instead of `f*::EPSILON`, which now succeed with Miri and `-Zmiri-many-seeds` - Added a constant `APPROX_DELTA` in `std/tests/floats/f32.rs` which is used for approximation tests, but with a different value when run in Miri. This is to make these tests succeed. - Added tests in the float tests of Miri to test the C23 behaviour. Fixes #4208
2 parents 2455377 + 4b500ae commit 2764b3c

File tree

3 files changed

+329
-62
lines changed

3 files changed

+329
-62
lines changed

src/intrinsics/mod.rs

Lines changed: 203 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33
mod atomic;
44
mod simd;
55

6+
use std::ops::Neg;
7+
68
use rand::Rng;
79
use rustc_abi::Size;
8-
use rustc_apfloat::{Float, Round};
10+
use rustc_apfloat::ieee::{IeeeFloat, Semantics};
11+
use rustc_apfloat::{self, Float, Round};
912
use rustc_middle::mir;
10-
use rustc_middle::ty::{self, FloatTy};
13+
use rustc_middle::ty::{self, FloatTy, ScalarInt};
1114
use rustc_span::{Symbol, sym};
1215

1316
use self::atomic::EvalContextExt as _;
1417
use self::helpers::{ToHost, ToSoft, check_intrinsic_arg_count};
1518
use self::simd::EvalContextExt as _;
16-
use crate::math::apply_random_float_error_to_imm;
19+
use crate::math::{IeeeExt, apply_random_float_error_ulp};
1720
use crate::*;
1821

1922
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
@@ -187,31 +190,39 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
187190
=> {
188191
let [f] = check_intrinsic_arg_count(args)?;
189192
let f = this.read_scalar(f)?.to_f32()?;
190-
// Using host floats (but it's fine, these operations do not have
191-
// guaranteed precision).
192-
let host = f.to_host();
193-
let res = match intrinsic_name {
194-
"sinf32" => host.sin(),
195-
"cosf32" => host.cos(),
196-
"expf32" => host.exp(),
197-
"exp2f32" => host.exp2(),
198-
"logf32" => host.ln(),
199-
"log10f32" => host.log10(),
200-
"log2f32" => host.log2(),
201-
_ => bug!(),
202-
};
203-
let res = res.to_soft();
204-
// Apply a relative error of 16ULP to introduce some non-determinism
205-
// simulating imprecise implementations and optimizations.
206-
// FIXME: temporarily disabled as it breaks std tests.
207-
// let res = apply_random_float_error_ulp(
208-
// this,
209-
// res,
210-
// 4, // log2(16)
211-
// );
193+
194+
let res = fixed_float_value(intrinsic_name, &[f]).unwrap_or_else(||{
195+
// Using host floats (but it's fine, these operations do not have
196+
// guaranteed precision).
197+
let host = f.to_host();
198+
let res = match intrinsic_name {
199+
"sinf32" => host.sin(),
200+
"cosf32" => host.cos(),
201+
"expf32" => host.exp(),
202+
"exp2f32" => host.exp2(),
203+
"logf32" => host.ln(),
204+
"log10f32" => host.log10(),
205+
"log2f32" => host.log2(),
206+
_ => bug!(),
207+
};
208+
let res = res.to_soft();
209+
210+
// Apply a relative error of 4ULP to introduce some non-determinism
211+
// simulating imprecise implementations and optimizations.
212+
let res = apply_random_float_error_ulp(
213+
this,
214+
res,
215+
2, // log2(4)
216+
);
217+
218+
// Clamp the result to the guaranteed range of this function according to the C standard,
219+
// if any.
220+
clamp_float_value(intrinsic_name, res)
221+
});
212222
let res = this.adjust_nan(res, &[f]);
213223
this.write_scalar(res, dest)?;
214224
}
225+
215226
#[rustfmt::skip]
216227
| "sinf64"
217228
| "cosf64"
@@ -223,28 +234,35 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
223234
=> {
224235
let [f] = check_intrinsic_arg_count(args)?;
225236
let f = this.read_scalar(f)?.to_f64()?;
226-
// Using host floats (but it's fine, these operations do not have
227-
// guaranteed precision).
228-
let host = f.to_host();
229-
let res = match intrinsic_name {
230-
"sinf64" => host.sin(),
231-
"cosf64" => host.cos(),
232-
"expf64" => host.exp(),
233-
"exp2f64" => host.exp2(),
234-
"logf64" => host.ln(),
235-
"log10f64" => host.log10(),
236-
"log2f64" => host.log2(),
237-
_ => bug!(),
238-
};
239-
let res = res.to_soft();
240-
// Apply a relative error of 16ULP to introduce some non-determinism
241-
// simulating imprecise implementations and optimizations.
242-
// FIXME: temporarily disabled as it breaks std tests.
243-
// let res = apply_random_float_error_ulp(
244-
// this,
245-
// res,
246-
// 4, // log2(16)
247-
// );
237+
238+
let res = fixed_float_value(intrinsic_name, &[f]).unwrap_or_else(||{
239+
// Using host floats (but it's fine, these operations do not have
240+
// guaranteed precision).
241+
let host = f.to_host();
242+
let res = match intrinsic_name {
243+
"sinf64" => host.sin(),
244+
"cosf64" => host.cos(),
245+
"expf64" => host.exp(),
246+
"exp2f64" => host.exp2(),
247+
"logf64" => host.ln(),
248+
"log10f64" => host.log10(),
249+
"log2f64" => host.log2(),
250+
_ => bug!(),
251+
};
252+
let res = res.to_soft();
253+
254+
// Apply a relative error of 4ULP to introduce some non-determinism
255+
// simulating imprecise implementations and optimizations.
256+
let res = apply_random_float_error_ulp(
257+
this,
258+
res,
259+
2, // log2(4)
260+
);
261+
262+
// Clamp the result to the guaranteed range of this function according to the C standard,
263+
// if any.
264+
clamp_float_value(intrinsic_name, res)
265+
});
248266
let res = this.adjust_nan(res, &[f]);
249267
this.write_scalar(res, dest)?;
250268
}
@@ -302,43 +320,75 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
302320
}
303321

304322
"powf32" => {
305-
// FIXME: apply random relative error but without altering behaviour of powf
306323
let [f1, f2] = check_intrinsic_arg_count(args)?;
307324
let f1 = this.read_scalar(f1)?.to_f32()?;
308325
let f2 = this.read_scalar(f2)?.to_f32()?;
309-
// Using host floats (but it's fine, this operation does not have guaranteed precision).
310-
let res = f1.to_host().powf(f2.to_host()).to_soft();
326+
327+
let res = fixed_float_value(intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
328+
// Using host floats (but it's fine, this operation does not have guaranteed precision).
329+
let res = f1.to_host().powf(f2.to_host()).to_soft();
330+
331+
// Apply a relative error of 4ULP to introduce some non-determinism
332+
// simulating imprecise implementations and optimizations.
333+
apply_random_float_error_ulp(
334+
this, res, 2, // log2(4)
335+
)
336+
});
311337
let res = this.adjust_nan(res, &[f1, f2]);
312338
this.write_scalar(res, dest)?;
313339
}
314340
"powf64" => {
315-
// FIXME: apply random relative error but without altering behaviour of powf
316341
let [f1, f2] = check_intrinsic_arg_count(args)?;
317342
let f1 = this.read_scalar(f1)?.to_f64()?;
318343
let f2 = this.read_scalar(f2)?.to_f64()?;
319-
// Using host floats (but it's fine, this operation does not have guaranteed precision).
320-
let res = f1.to_host().powf(f2.to_host()).to_soft();
344+
345+
let res = fixed_float_value(intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
346+
// Using host floats (but it's fine, this operation does not have guaranteed precision).
347+
let res = f1.to_host().powf(f2.to_host()).to_soft();
348+
349+
// Apply a relative error of 4ULP to introduce some non-determinism
350+
// simulating imprecise implementations and optimizations.
351+
apply_random_float_error_ulp(
352+
this, res, 2, // log2(4)
353+
)
354+
});
321355
let res = this.adjust_nan(res, &[f1, f2]);
322356
this.write_scalar(res, dest)?;
323357
}
324358

325359
"powif32" => {
326-
// FIXME: apply random relative error but without altering behaviour of powi
327360
let [f, i] = check_intrinsic_arg_count(args)?;
328361
let f = this.read_scalar(f)?.to_f32()?;
329362
let i = this.read_scalar(i)?.to_i32()?;
330-
// Using host floats (but it's fine, this operation does not have guaranteed precision).
331-
let res = f.to_host().powi(i).to_soft();
363+
364+
let res = fixed_powi_float_value(f, i).unwrap_or_else(|| {
365+
// Using host floats (but it's fine, this operation does not have guaranteed precision).
366+
let res = f.to_host().powi(i).to_soft();
367+
368+
// Apply a relative error of 4ULP to introduce some non-determinism
369+
// simulating imprecise implementations and optimizations.
370+
apply_random_float_error_ulp(
371+
this, res, 2, // log2(4)
372+
)
373+
});
332374
let res = this.adjust_nan(res, &[f]);
333375
this.write_scalar(res, dest)?;
334376
}
335377
"powif64" => {
336-
// FIXME: apply random relative error but without altering behaviour of powi
337378
let [f, i] = check_intrinsic_arg_count(args)?;
338379
let f = this.read_scalar(f)?.to_f64()?;
339380
let i = this.read_scalar(i)?.to_i32()?;
340-
// Using host floats (but it's fine, this operation does not have guaranteed precision).
341-
let res = f.to_host().powi(i).to_soft();
381+
382+
let res = fixed_powi_float_value(f, i).unwrap_or_else(|| {
383+
// Using host floats (but it's fine, this operation does not have guaranteed precision).
384+
let res = f.to_host().powi(i).to_soft();
385+
386+
// Apply a relative error of 4ULP to introduce some non-determinism
387+
// simulating imprecise implementations and optimizations.
388+
apply_random_float_error_ulp(
389+
this, res, 2, // log2(4)
390+
)
391+
});
342392
let res = this.adjust_nan(res, &[f]);
343393
this.write_scalar(res, dest)?;
344394
}
@@ -425,3 +475,97 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
425475
interp_ok(EmulateItemResult::NeedsReturn)
426476
}
427477
}
478+
479+
/// Applies a random ULP floating point error to `val` and returns the new value.
480+
/// So if you want an X ULP error, `ulp_exponent` should be log2(X).
481+
///
482+
/// Will fail if `val` is not a floating point number.
483+
fn apply_random_float_error_to_imm<'tcx>(
484+
ecx: &mut MiriInterpCx<'tcx>,
485+
val: ImmTy<'tcx>,
486+
ulp_exponent: u32,
487+
) -> InterpResult<'tcx, ImmTy<'tcx>> {
488+
let scalar = val.to_scalar_int()?;
489+
let res: ScalarInt = match val.layout.ty.kind() {
490+
ty::Float(FloatTy::F16) =>
491+
apply_random_float_error_ulp(ecx, scalar.to_f16(), ulp_exponent).into(),
492+
ty::Float(FloatTy::F32) =>
493+
apply_random_float_error_ulp(ecx, scalar.to_f32(), ulp_exponent).into(),
494+
ty::Float(FloatTy::F64) =>
495+
apply_random_float_error_ulp(ecx, scalar.to_f64(), ulp_exponent).into(),
496+
ty::Float(FloatTy::F128) =>
497+
apply_random_float_error_ulp(ecx, scalar.to_f128(), ulp_exponent).into(),
498+
_ => bug!("intrinsic called with non-float input type"),
499+
};
500+
501+
interp_ok(ImmTy::from_scalar_int(res, val.layout))
502+
}
503+
504+
/// For the intrinsics:
505+
/// - sinf32, sinf64
506+
/// - cosf32, cosf64
507+
/// - expf32, expf64, exp2f32, exp2f64
508+
/// - logf32, logf64, log2f32, log2f64, log10f32, log10f64
509+
/// - powf32, powf64
510+
///
511+
/// Returns `Some(output)` if the `intrinsic` results in a defined fixed `output` specified in the C standard
512+
/// (specifically, C23 annex F.10) when given `args` as arguments. Outputs that are unaffected by a relative error
513+
/// (such as INF and zero) are not handled here, they are assumed to be handled by the underlying
514+
/// implementation. Returns `None` if no specific value is guaranteed.
515+
fn fixed_float_value<S: Semantics>(
516+
intrinsic_name: &str,
517+
args: &[IeeeFloat<S>],
518+
) -> Option<IeeeFloat<S>> {
519+
let one = IeeeFloat::<S>::one();
520+
match (intrinsic_name, args) {
521+
// cos(+- 0) = 1
522+
("cosf32" | "cosf64", [input]) if input.is_zero() => Some(one),
523+
524+
// e^0 = 1
525+
("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => Some(one),
526+
527+
// 1^y = 1 for any y, even a NaN.
528+
("powf32" | "powf64", [base, _]) if *base == one => Some(one),
529+
530+
// (-1)^(±INF) = 1
531+
("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => Some(one),
532+
533+
// FIXME(#4286): The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate
534+
// the NaN. We should return either 1 or the NaN non-deterministically here.
535+
// But for now, just handle them all the same.
536+
// x^(±0) = 1 for any x, even a NaN
537+
("powf32" | "powf64", [_, exp]) if exp.is_zero() => Some(one),
538+
539+
// There are a lot of cases for fixed outputs according to the C Standard, but these are mainly INF or zero
540+
// which are not affected by the applied error.
541+
_ => None,
542+
}
543+
}
544+
545+
/// Returns `Some(output)` if `powi` (called `pown` in C) results in a fixed value specified in the C standard
546+
/// (specifically, C23 annex F.10.4.6) when doing `base^exp`. Otherwise, returns `None`.
547+
fn fixed_powi_float_value<S: Semantics>(base: IeeeFloat<S>, exp: i32) -> Option<IeeeFloat<S>> {
548+
match (base.category(), exp) {
549+
// x^0 = 1, if x is not a Signaling NaN
550+
// FIXME(#4286): The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate
551+
// the NaN. We should return either 1 or the NaN non-deterministically here.
552+
// But for now, just handle them all the same.
553+
(_, 0) => Some(IeeeFloat::<S>::one()),
554+
555+
_ => None,
556+
}
557+
}
558+
559+
/// Given an floating-point operation and a floating-point value, clamps the result to the output
560+
/// range of the given operation.
561+
fn clamp_float_value<S: Semantics>(intrinsic_name: &str, val: IeeeFloat<S>) -> IeeeFloat<S> {
562+
match intrinsic_name {
563+
// sin and cos: [-1, 1]
564+
"sinf32" | "cosf32" | "sinf64" | "cosf64" =>
565+
val.clamp(IeeeFloat::<S>::one().neg(), IeeeFloat::<S>::one()),
566+
// exp: [0, +INF]
567+
"expf32" | "exp2f32" | "expf64" | "exp2f64" =>
568+
IeeeFloat::<S>::maximum(val, IeeeFloat::<S>::ZERO),
569+
_ => val,
570+
}
571+
}

src/math.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ pub(crate) fn sqrt<S: rustc_apfloat::ieee::Semantics>(x: IeeeFloat<S>) -> IeeeFl
151151
}
152152
}
153153

154+
/// Extend functionality of rustc_apfloat softfloats
155+
pub trait IeeeExt: rustc_apfloat::Float {
156+
#[inline]
157+
fn one() -> Self {
158+
Self::from_u128(1).value
159+
}
160+
161+
#[inline]
162+
fn clamp(self, min: Self, max: Self) -> Self {
163+
self.maximum(min).minimum(max)
164+
}
165+
}
166+
impl<S: rustc_apfloat::ieee::Semantics> IeeeExt for IeeeFloat<S> {}
167+
154168
#[cfg(test)]
155169
mod tests {
156170
use rustc_apfloat::ieee::{DoubleS, HalfS, IeeeFloat, QuadS, SingleS};

0 commit comments

Comments
 (0)