Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit ebb4eda

Browse files
enable non-determinism but with smaller error, 4ULP; conform to IEEE 754 and C Standards
1 parent d10109a commit ebb4eda

File tree

2 files changed

+215
-58
lines changed

2 files changed

+215
-58
lines changed

src/tools/miri/src/intrinsics/mod.rs

Lines changed: 202 additions & 58 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::{Double, IeeeFloat, Semantics, Single};
11+
use rustc_apfloat::{self, Category, Float, Round};
912
use rustc_middle::mir;
1013
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_ulp;
19+
use crate::math::{IeeeExt, apply_random_float_error_ulp};
1720
use crate::*;
1821

1922
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
@@ -235,30 +238,42 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
235238
=> {
236239
let [f] = check_intrinsic_arg_count(args)?;
237240
let f = this.read_scalar(f)?.to_f32()?;
238-
// Using host floats (but it's fine, these operations do not have
239-
// guaranteed precision).
240-
let host = f.to_host();
241-
let res = match intrinsic_name {
242-
"sinf32" => host.sin(),
243-
"cosf32" => host.cos(),
244-
"expf32" => host.exp(),
245-
"exp2f32" => host.exp2(),
246-
"logf32" => host.ln(),
247-
"log10f32" => host.log10(),
248-
"log2f32" => host.log2(),
249-
_ => bug!(),
250-
};
251-
let res = res.to_soft();
252-
// Apply a relative error of 16ULP to introduce some non-determinism
253-
// simulating imprecise implementations and optimizations.
254-
// FIXME: temporarily disabled as it breaks std tests.
255-
// let res = apply_random_float_error_ulp(
256-
// this,
257-
// res,
258-
// 4, // log2(16)
259-
// );
241+
242+
let res = fixed_float_value(intrinsic_name, f).unwrap_or_else(||{
243+
// Using host floats (but it's fine, these operations do not have
244+
// guaranteed precision).
245+
let host = f.to_host();
246+
let res = match intrinsic_name {
247+
"sinf32" => host.sin(),
248+
"cosf32" => host.cos(),
249+
"expf32" => host.exp(),
250+
"exp2f32" => host.exp2(),
251+
"logf32" => host.ln(),
252+
"log10f32" => host.log10(),
253+
"log2f32" => host.log2(),
254+
_ => bug!(),
255+
};
256+
let res = res.to_soft();
257+
258+
// Apply a relative error of 4ULP to introduce some non-determinism
259+
// simulating imprecise implementations and optimizations.
260+
let res = apply_random_float_error_ulp(
261+
this,
262+
res,
263+
2, // log2(4)
264+
);
265+
266+
match intrinsic_name {
267+
// sin and cos: [-1, 1]
268+
"sinf32" | "cosf32" => res.clamp(Single::one().neg(), Single::one()),
269+
// exp: [0, +INF]
270+
"expf32" | "exp2f32" => res.maximum(Single::ZERO),
271+
_ => res,
272+
}
273+
});
260274
let res = this.adjust_nan(res, &[f]);
261275
this.write_scalar(res, dest)?;
276+
262277
}
263278
#[rustfmt::skip]
264279
| "sinf64"
@@ -271,30 +286,43 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
271286
=> {
272287
let [f] = check_intrinsic_arg_count(args)?;
273288
let f = this.read_scalar(f)?.to_f64()?;
274-
// Using host floats (but it's fine, these operations do not have
275-
// guaranteed precision).
276-
let host = f.to_host();
277-
let res = match intrinsic_name {
278-
"sinf64" => host.sin(),
279-
"cosf64" => host.cos(),
280-
"expf64" => host.exp(),
281-
"exp2f64" => host.exp2(),
282-
"logf64" => host.ln(),
283-
"log10f64" => host.log10(),
284-
"log2f64" => host.log2(),
285-
_ => bug!(),
286-
};
287-
let res = res.to_soft();
288-
// Apply a relative error of 16ULP to introduce some non-determinism
289-
// simulating imprecise implementations and optimizations.
290-
// FIXME: temporarily disabled as it breaks std tests.
291-
// let res = apply_random_float_error_ulp(
292-
// this,
293-
// res,
294-
// 4, // log2(16)
295-
// );
289+
290+
let res = fixed_float_value(intrinsic_name, f).unwrap_or_else(||{
291+
// Using host floats (but it's fine, these operations do not have
292+
// guaranteed precision).
293+
let host = f.to_host();
294+
let res = match intrinsic_name {
295+
"sinf64" => host.sin(),
296+
"cosf64" => host.cos(),
297+
"expf64" => host.exp(),
298+
"exp2f64" => host.exp2(),
299+
"logf64" => host.ln(),
300+
"log10f64" => host.log10(),
301+
"log2f64" => host.log2(),
302+
_ => bug!(),
303+
};
304+
let res = res.to_soft();
305+
306+
// Apply a relative error of 4ULP to introduce some non-determinism
307+
// simulating imprecise implementations and optimizations.
308+
let res = apply_random_float_error_ulp(
309+
this,
310+
res,
311+
2, // log2(4)
312+
);
313+
314+
// Clamp values to the output range defined in IEEE 754 9.1.
315+
match intrinsic_name {
316+
// sin and cos: [-1, 1]
317+
"sinf64" | "cosf64" => res.clamp(Double::one().neg(), Double::one()),
318+
// exp: [0, +INF]
319+
"expf64" | "exp2f64" => res.maximum(Double::ZERO),
320+
_ => res,
321+
}
322+
});
296323
let res = this.adjust_nan(res, &[f]);
297324
this.write_scalar(res, dest)?;
325+
298326
}
299327

300328
"fmaf32" => {
@@ -350,43 +378,126 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
350378
}
351379

352380
"powf32" => {
353-
// FIXME: apply random relative error but without altering behaviour of powf
354381
let [f1, f2] = check_intrinsic_arg_count(args)?;
355382
let f1 = this.read_scalar(f1)?.to_f32()?;
356383
let f2 = this.read_scalar(f2)?.to_f32()?;
357-
// Using host floats (but it's fine, this operation does not have guaranteed precision).
358-
let res = f1.to_host().powf(f2.to_host()).to_soft();
384+
385+
let fixed_res = match (f1.category(), f2.category()) {
386+
// 1^y = 1 for any y even a NaN.
387+
// TODO: C Standard says any NaN, IEEE says not a Signaling NaN
388+
(Category::Normal, _) if f1 == 1.0f32.to_soft() => Some(1.0f32.to_soft()),
389+
390+
// (-1)^(±INF) = 1
391+
(Category::Normal, Category::Infinity) if f1 == (-1.0f32).to_soft() =>
392+
Some(1.0f32.to_soft()),
393+
394+
// x^(±0) = 1 for any x, even a NaN
395+
(_, Category::Zero) => Some(1.0f32.to_soft()),
396+
397+
_ => None,
398+
};
399+
let res = fixed_res.unwrap_or_else(|| {
400+
// Using host floats (but it's fine, this operation does not have guaranteed precision).
401+
let res = f1.to_host().powf(f2.to_host()).to_soft();
402+
// Apply a relative error of 4ULP to introduce some non-determinism
403+
// simulating imprecise implementations and optimizations.
404+
apply_random_float_error_ulp(
405+
this, res, 2, // log2(4)
406+
)
407+
});
359408
let res = this.adjust_nan(res, &[f1, f2]);
360409
this.write_scalar(res, dest)?;
361410
}
362411
"powf64" => {
363-
// FIXME: apply random relative error but without altering behaviour of powf
364412
let [f1, f2] = check_intrinsic_arg_count(args)?;
365413
let f1 = this.read_scalar(f1)?.to_f64()?;
366414
let f2 = this.read_scalar(f2)?.to_f64()?;
367-
// Using host floats (but it's fine, this operation does not have guaranteed precision).
368-
let res = f1.to_host().powf(f2.to_host()).to_soft();
415+
416+
let fixed_res = match (f1.category(), f2.category()) {
417+
// 1^y = 1 for any y even a NaN.
418+
// TODO: C says any NaN, IEEE says no a Sign NaN
419+
(Category::Normal, _) if f1 == 1.0f64.to_soft() => Some(1.0f64.to_soft()),
420+
421+
// (-1)^(±INF) = 1
422+
(Category::Normal, Category::Infinity) if f1 == (-1.0f64).to_soft() =>
423+
Some(1.0f64.to_soft()),
424+
425+
// x^(±0) = 1 for any x, even a NaN
426+
(_, Category::Zero) => Some(1.0f64.to_soft()),
427+
428+
// TODO: pow has a lot of "edge" cases which mostly result in ±0 or ±INF
429+
// do we have to catch them all?
430+
_ => None,
431+
};
432+
let res = fixed_res.unwrap_or_else(|| {
433+
// Using host floats (but it's fine, this operation does not have guaranteed precision).
434+
let res = f1.to_host().powf(f2.to_host()).to_soft();
435+
// Apply a relative error of 4ULP to introduce some non-determinism
436+
// simulating imprecise implementations and optimizations.
437+
apply_random_float_error_ulp(
438+
this, res, 2, // log2(4)
439+
)
440+
});
369441
let res = this.adjust_nan(res, &[f1, f2]);
370442
this.write_scalar(res, dest)?;
371443
}
372444

373445
"powif32" => {
374-
// FIXME: apply random relative error but without altering behaviour of powi
375446
let [f, i] = check_intrinsic_arg_count(args)?;
376447
let f = this.read_scalar(f)?.to_f32()?;
377448
let i = this.read_scalar(i)?.to_i32()?;
378-
// Using host floats (but it's fine, this operation does not have guaranteed precision).
379-
let res = f.to_host().powi(i).to_soft();
449+
450+
let fixed_res = match (f.category(), i) {
451+
// Standard specifies we can only fix to 1 if input is is not a Signaling NaN.
452+
(_, 0) if !f.is_signaling() => Some(1.0f32.to_soft()),
453+
454+
// TODO: Isn't this done by the implementation? And ULP error on 0.0 doesn't have an effect
455+
(Category::Zero, x) if x % 2 == 0 => Some(0.0f32.to_soft()),
456+
457+
// ±0^x = ±0 with x an odd integer.
458+
(Category::Zero, x) if x % 2 != 0 => Some(f), // preserve sign of zero.
459+
_ => None,
460+
};
461+
let res = fixed_res.unwrap_or_else(|| {
462+
// Using host floats (but it's fine, this operation does not have guaranteed precision).
463+
let res = f.to_host().powi(i).to_soft();
464+
465+
// Apply a relative error of 4ULP to introduce some non-determinism
466+
// simulating imprecise implementations and optimizations.
467+
apply_random_float_error_ulp(
468+
this, res, 2, // log2(4)
469+
)
470+
});
380471
let res = this.adjust_nan(res, &[f]);
381472
this.write_scalar(res, dest)?;
382473
}
383474
"powif64" => {
384-
// FIXME: apply random relative error but without altering behaviour of powi
385475
let [f, i] = check_intrinsic_arg_count(args)?;
386476
let f = this.read_scalar(f)?.to_f64()?;
387477
let i = this.read_scalar(i)?.to_i32()?;
388-
// Using host floats (but it's fine, this operation does not have guaranteed precision).
389-
let res = f.to_host().powi(i).to_soft();
478+
479+
let fixed_res = match (f.category(), i) {
480+
// Standard specifies we can only fix to 1 if input is is not a Signaling NaN.
481+
(_, 0) if !f.is_signaling() => Some(1.0f64.to_soft()),
482+
483+
// ±0^x = 0 with x an even integer.
484+
// TODO: Isn't this done by the implementation itself?
485+
(Category::Zero, x) if x % 2 == 0 => Some(0.0f64.to_soft()),
486+
487+
// ±0^x = ±0 with x an odd integer.
488+
(Category::Zero, x) if x % 2 != 0 => Some(f), // preserve sign of zero.
489+
_ => None,
490+
};
491+
let res = fixed_res.unwrap_or_else(|| {
492+
// Using host floats (but it's fine, this operation does not have guaranteed precision).
493+
let res = f.to_host().powi(i).to_soft();
494+
495+
// Apply a relative error of 4ULP to introduce some non-determinism
496+
// simulating imprecise implementations and optimizations.
497+
apply_random_float_error_ulp(
498+
this, res, 2, // log2(4)
499+
)
500+
});
390501
let res = this.adjust_nan(res, &[f]);
391502
this.write_scalar(res, dest)?;
392503
}
@@ -522,3 +633,36 @@ fn apply_random_float_error_to_imm<'tcx>(
522633

523634
interp_ok(ImmTy::from_scalar_int(res, val.layout))
524635
}
636+
637+
/// For the operations:
638+
/// - sinf32, sinf64
639+
/// - cosf32, cosf64
640+
/// - expf32, expf64, exp2f32, exp2f64
641+
/// - logf32, logf64, log2f32, log2f64, log10f32, log10f64
642+
///
643+
/// Returns Some(`output`) if the operation results in a defined fixed `output` when given `input`
644+
/// as an input, else None.
645+
fn fixed_float_value<S: Semantics>(
646+
intrinsic_name: &str,
647+
input: IeeeFloat<S>,
648+
) -> Option<IeeeFloat<S>>
649+
where
650+
IeeeFloat<S>: std::cmp::PartialEq,
651+
{
652+
// TODO: Should we really fix to ±0? Applying an error has no effect.
653+
let one = IeeeFloat::<S>::one();
654+
match intrinsic_name {
655+
"sinf32" | "sinf64" if input.is_pos_zero() => Some(IeeeFloat::<S>::ZERO),
656+
"sinf32" | "sinf64" if input.is_neg_zero() => Some(-IeeeFloat::<S>::ZERO),
657+
"cosf32" | "cosf64" if input.is_zero() => Some(one),
658+
"expf32" | "expf64" | "exp2f32" | "exp2f64" if input.is_zero() => Some(one),
659+
#[rustfmt::skip]
660+
"logf32"
661+
| "logf64"
662+
| "log10f32"
663+
| "log10f64"
664+
| "log2f32"
665+
| "log2f64" if input == one => Some(IeeeFloat::<S>::ZERO),
666+
_ => None,
667+
}
668+
}

src/tools/miri/src/math.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ pub(crate) fn sqrt<S: rustc_apfloat::ieee::Semantics>(x: IeeeFloat<S>) -> IeeeFl
125125
}
126126
}
127127

128+
impl<S: rustc_apfloat::ieee::Semantics> IeeeExt for IeeeFloat<S> {}
129+
/// Extend functionality of rustc_apfloat softfloats
130+
pub trait IeeeExt: rustc_apfloat::Float {
131+
fn one() -> Self {
132+
Self::from_u128(1).value
133+
}
134+
135+
#[inline]
136+
fn clamp(self, min: Self, max: Self) -> Self {
137+
self.maximum(min).minimum(max)
138+
}
139+
}
140+
128141
#[cfg(test)]
129142
mod tests {
130143
use rustc_apfloat::ieee::{DoubleS, HalfS, IeeeFloat, QuadS, SingleS};

0 commit comments

Comments
 (0)