Move float non determinism helpers to math.rs
This commit is contained in:
parent
71289c378d
commit
fd479ca23c
2 changed files with 140 additions and 134 deletions
|
|
@ -3,11 +3,8 @@
|
|||
mod atomic;
|
||||
mod simd;
|
||||
|
||||
use std::ops::Neg;
|
||||
|
||||
use rand::Rng;
|
||||
use rustc_abi::Size;
|
||||
use rustc_apfloat::ieee::{IeeeFloat, Semantics};
|
||||
use rustc_apfloat::{self, Float, Round};
|
||||
use rustc_middle::mir;
|
||||
use rustc_middle::ty::{self, FloatTy};
|
||||
|
|
@ -16,7 +13,7 @@ use rustc_span::{Symbol, sym};
|
|||
use self::atomic::EvalContextExt as _;
|
||||
use self::helpers::{ToHost, ToSoft};
|
||||
use self::simd::EvalContextExt as _;
|
||||
use crate::math::{IeeeExt, apply_random_float_error_ulp};
|
||||
use crate::math::apply_random_float_error_ulp;
|
||||
use crate::*;
|
||||
|
||||
/// Check that the number of args is what we expect.
|
||||
|
|
@ -209,7 +206,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let [f] = check_intrinsic_arg_count(args)?;
|
||||
let f = this.read_scalar(f)?.to_f32()?;
|
||||
|
||||
let res = fixed_float_value(this, intrinsic_name, &[f]).unwrap_or_else(|| {
|
||||
let res = math::fixed_float_value(this, intrinsic_name, &[f]).unwrap_or_else(|| {
|
||||
// Using host floats (but it's fine, these operations do not have
|
||||
// guaranteed precision).
|
||||
let host = f.to_host();
|
||||
|
|
@ -227,7 +224,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
|
||||
// Apply a relative error of 4ULP to introduce some non-determinism
|
||||
// simulating imprecise implementations and optimizations.
|
||||
let res = apply_random_float_error_ulp(
|
||||
let res = math::apply_random_float_error_ulp(
|
||||
this,
|
||||
res,
|
||||
4,
|
||||
|
|
@ -235,7 +232,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
|
||||
// Clamp the result to the guaranteed range of this function according to the C standard,
|
||||
// if any.
|
||||
clamp_float_value(intrinsic_name, res)
|
||||
math::clamp_float_value(intrinsic_name, res)
|
||||
});
|
||||
let res = this.adjust_nan(res, &[f]);
|
||||
this.write_scalar(res, dest)?;
|
||||
|
|
@ -253,7 +250,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let [f] = check_intrinsic_arg_count(args)?;
|
||||
let f = this.read_scalar(f)?.to_f64()?;
|
||||
|
||||
let res = fixed_float_value(this, intrinsic_name, &[f]).unwrap_or_else(|| {
|
||||
let res = math::fixed_float_value(this, intrinsic_name, &[f]).unwrap_or_else(|| {
|
||||
// Using host floats (but it's fine, these operations do not have
|
||||
// guaranteed precision).
|
||||
let host = f.to_host();
|
||||
|
|
@ -271,7 +268,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
|
||||
// Apply a relative error of 4ULP to introduce some non-determinism
|
||||
// simulating imprecise implementations and optimizations.
|
||||
let res = apply_random_float_error_ulp(
|
||||
let res = math::apply_random_float_error_ulp(
|
||||
this,
|
||||
res,
|
||||
4,
|
||||
|
|
@ -279,7 +276,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
|
||||
// Clamp the result to the guaranteed range of this function according to the C standard,
|
||||
// if any.
|
||||
clamp_float_value(intrinsic_name, res)
|
||||
math::clamp_float_value(intrinsic_name, res)
|
||||
});
|
||||
let res = this.adjust_nan(res, &[f]);
|
||||
this.write_scalar(res, dest)?;
|
||||
|
|
@ -330,14 +327,15 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let f1 = this.read_scalar(f1)?.to_f32()?;
|
||||
let f2 = this.read_scalar(f2)?.to_f32()?;
|
||||
|
||||
let res = fixed_float_value(this, intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
|
||||
// Using host floats (but it's fine, this operation does not have guaranteed precision).
|
||||
let res = f1.to_host().powf(f2.to_host()).to_soft();
|
||||
let res =
|
||||
math::fixed_float_value(this, intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
|
||||
// Using host floats (but it's fine, this operation does not have guaranteed precision).
|
||||
let res = f1.to_host().powf(f2.to_host()).to_soft();
|
||||
|
||||
// Apply a relative error of 4ULP to introduce some non-determinism
|
||||
// simulating imprecise implementations and optimizations.
|
||||
apply_random_float_error_ulp(this, res, 4)
|
||||
});
|
||||
// Apply a relative error of 4ULP to introduce some non-determinism
|
||||
// simulating imprecise implementations and optimizations.
|
||||
math::apply_random_float_error_ulp(this, res, 4)
|
||||
});
|
||||
let res = this.adjust_nan(res, &[f1, f2]);
|
||||
this.write_scalar(res, dest)?;
|
||||
}
|
||||
|
|
@ -346,14 +344,15 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let f1 = this.read_scalar(f1)?.to_f64()?;
|
||||
let f2 = this.read_scalar(f2)?.to_f64()?;
|
||||
|
||||
let res = fixed_float_value(this, intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
|
||||
// Using host floats (but it's fine, this operation does not have guaranteed precision).
|
||||
let res = f1.to_host().powf(f2.to_host()).to_soft();
|
||||
let res =
|
||||
math::fixed_float_value(this, intrinsic_name, &[f1, f2]).unwrap_or_else(|| {
|
||||
// Using host floats (but it's fine, this operation does not have guaranteed precision).
|
||||
let res = f1.to_host().powf(f2.to_host()).to_soft();
|
||||
|
||||
// Apply a relative error of 4ULP to introduce some non-determinism
|
||||
// simulating imprecise implementations and optimizations.
|
||||
apply_random_float_error_ulp(this, res, 4)
|
||||
});
|
||||
// Apply a relative error of 4ULP to introduce some non-determinism
|
||||
// simulating imprecise implementations and optimizations.
|
||||
math::apply_random_float_error_ulp(this, res, 4)
|
||||
});
|
||||
let res = this.adjust_nan(res, &[f1, f2]);
|
||||
this.write_scalar(res, dest)?;
|
||||
}
|
||||
|
|
@ -363,7 +362,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let f = this.read_scalar(f)?.to_f32()?;
|
||||
let i = this.read_scalar(i)?.to_i32()?;
|
||||
|
||||
let res = fixed_powi_float_value(this, f, i).unwrap_or_else(|| {
|
||||
let res = math::fixed_powi_float_value(this, f, i).unwrap_or_else(|| {
|
||||
// Using host floats (but it's fine, this operation does not have guaranteed precision).
|
||||
let res = f.to_host().powi(i).to_soft();
|
||||
|
||||
|
|
@ -379,13 +378,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
let f = this.read_scalar(f)?.to_f64()?;
|
||||
let i = this.read_scalar(i)?.to_i32()?;
|
||||
|
||||
let res = fixed_powi_float_value(this, f, i).unwrap_or_else(|| {
|
||||
let res = math::fixed_powi_float_value(this, f, i).unwrap_or_else(|| {
|
||||
// Using host floats (but it's fine, this operation does not have guaranteed precision).
|
||||
let res = f.to_host().powi(i).to_soft();
|
||||
|
||||
// Apply a relative error of 4ULP to introduce some non-determinism
|
||||
// simulating imprecise implementations and optimizations.
|
||||
apply_random_float_error_ulp(this, res, 4)
|
||||
math::apply_random_float_error_ulp(this, res, 4)
|
||||
});
|
||||
let res = this.adjust_nan(res, &[f]);
|
||||
this.write_scalar(res, dest)?;
|
||||
|
|
@ -440,7 +439,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
}
|
||||
// Apply a relative error of 4ULP to simulate non-deterministic precision loss
|
||||
// due to optimizations.
|
||||
let res = crate::math::apply_random_float_error_to_imm(this, res, 4)?;
|
||||
let res = math::apply_random_float_error_to_imm(this, res, 4)?;
|
||||
this.write_immediate(*res, dest)?;
|
||||
}
|
||||
|
||||
|
|
@ -477,108 +476,3 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
|
|||
interp_ok(EmulateItemResult::NeedsReturn)
|
||||
}
|
||||
}
|
||||
|
||||
/// For the intrinsics:
|
||||
/// - sinf32, sinf64
|
||||
/// - cosf32, cosf64
|
||||
/// - expf32, expf64, exp2f32, exp2f64
|
||||
/// - logf32, logf64, log2f32, log2f64, log10f32, log10f64
|
||||
/// - powf32, powf64
|
||||
///
|
||||
/// # Return
|
||||
///
|
||||
/// Returns `Some(output)` if the `intrinsic` results in a defined fixed `output` specified in the C standard
|
||||
/// (specifically, C23 annex F.10) when given `args` as arguments. Outputs that are unaffected by a relative error
|
||||
/// (such as INF and zero) are not handled here, they are assumed to be handled by the underlying
|
||||
/// implementation. Returns `None` if no specific value is guaranteed.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// For `powf*` operations of the form:
|
||||
///
|
||||
/// - `(SNaN)^(±0)`
|
||||
/// - `1^(SNaN)`
|
||||
///
|
||||
/// The result is implementation-defined:
|
||||
/// - musl returns for both `1.0`
|
||||
/// - glibc returns for both `NaN`
|
||||
///
|
||||
/// This discrepancy exists because SNaN handling is not consistently defined across platforms,
|
||||
/// and the C standard leaves behavior for SNaNs unspecified.
|
||||
///
|
||||
/// Miri chooses to adhere to both implementations and returns either one of them non-deterministically.
|
||||
fn fixed_float_value<S: Semantics>(
|
||||
ecx: &mut MiriInterpCx<'_>,
|
||||
intrinsic_name: &str,
|
||||
args: &[IeeeFloat<S>],
|
||||
) -> Option<IeeeFloat<S>> {
|
||||
let one = IeeeFloat::<S>::one();
|
||||
Some(match (intrinsic_name, args) {
|
||||
// cos(+- 0) = 1
|
||||
("cosf32" | "cosf64", [input]) if input.is_zero() => one,
|
||||
|
||||
// e^0 = 1
|
||||
("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => one,
|
||||
|
||||
// (-1)^(±INF) = 1
|
||||
("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => one,
|
||||
|
||||
// 1^y = 1 for any y, even a NaN
|
||||
("powf32" | "powf64", [base, exp]) if *base == one => {
|
||||
let rng = ecx.machine.rng.get_mut();
|
||||
// SNaN exponents get special treatment: they might return 1, or a NaN.
|
||||
let return_nan = exp.is_signaling() && ecx.machine.float_nondet && rng.random();
|
||||
// Handle both the musl and glibc cases non-deterministically.
|
||||
if return_nan { ecx.generate_nan(args) } else { one }
|
||||
}
|
||||
|
||||
// x^(±0) = 1 for any x, even a NaN
|
||||
("powf32" | "powf64", [base, exp]) if exp.is_zero() => {
|
||||
let rng = ecx.machine.rng.get_mut();
|
||||
// SNaN bases get special treatment: they might return 1, or a NaN.
|
||||
let return_nan = base.is_signaling() && ecx.machine.float_nondet && rng.random();
|
||||
// Handle both the musl and glibc cases non-deterministically.
|
||||
if return_nan { ecx.generate_nan(args) } else { one }
|
||||
}
|
||||
|
||||
// There are a lot of cases for fixed outputs according to the C Standard, but these are
|
||||
// mainly INF or zero which are not affected by the applied error.
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `Some(output)` if `powi` (called `pown` in C) results in a fixed value specified in the
|
||||
/// C standard (specifically, C23 annex F.10.4.6) when doing `base^exp`. Otherwise, returns `None`.
|
||||
fn fixed_powi_float_value<S: Semantics>(
|
||||
ecx: &mut MiriInterpCx<'_>,
|
||||
base: IeeeFloat<S>,
|
||||
exp: i32,
|
||||
) -> Option<IeeeFloat<S>> {
|
||||
Some(match exp {
|
||||
0 => {
|
||||
let one = IeeeFloat::<S>::one();
|
||||
let rng = ecx.machine.rng.get_mut();
|
||||
let return_nan = ecx.machine.float_nondet && rng.random() && base.is_signaling();
|
||||
// For SNaN treatment, we are consistent with `powf`above.
|
||||
// (We wouldn't have two, unlike powf all implementations seem to agree for powi,
|
||||
// but for now we are maximally conservative.)
|
||||
if return_nan { ecx.generate_nan(&[base]) } else { one }
|
||||
}
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Given an floating-point operation and a floating-point value, clamps the result to the output
|
||||
/// range of the given operation.
|
||||
fn clamp_float_value<S: Semantics>(intrinsic_name: &str, val: IeeeFloat<S>) -> IeeeFloat<S> {
|
||||
match intrinsic_name {
|
||||
// sin and cos: [-1, 1]
|
||||
"sinf32" | "cosf32" | "sinf64" | "cosf64" =>
|
||||
val.clamp(IeeeFloat::<S>::one().neg(), IeeeFloat::<S>::one()),
|
||||
// exp: [0, +INF]
|
||||
"expf32" | "exp2f32" | "expf64" | "exp2f64" =>
|
||||
IeeeFloat::<S>::maximum(val, IeeeFloat::<S>::ZERO),
|
||||
_ => val,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use std::ops::Neg;
|
||||
|
||||
use rand::Rng as _;
|
||||
use rustc_apfloat::Float as _;
|
||||
use rustc_apfloat::ieee::IeeeFloat;
|
||||
use rustc_apfloat::ieee::{IeeeFloat, Semantics};
|
||||
use rustc_middle::ty::{self, FloatTy, ScalarInt};
|
||||
|
||||
use crate::*;
|
||||
|
|
@ -105,6 +107,116 @@ pub(crate) fn apply_random_float_error_to_imm<'tcx>(
|
|||
interp_ok(ImmTy::from_scalar_int(res, val.layout))
|
||||
}
|
||||
|
||||
/// Given an floating-point operation and a floating-point value, clamps the result to the output
|
||||
/// range of the given operation.
|
||||
pub(crate) fn clamp_float_value<S: Semantics>(
|
||||
intrinsic_name: &str,
|
||||
val: IeeeFloat<S>,
|
||||
) -> IeeeFloat<S> {
|
||||
match intrinsic_name {
|
||||
// sin and cos: [-1, 1]
|
||||
"sinf32" | "cosf32" | "sinf64" | "cosf64" =>
|
||||
val.clamp(IeeeFloat::<S>::one().neg(), IeeeFloat::<S>::one()),
|
||||
// exp: [0, +INF]
|
||||
"expf32" | "exp2f32" | "expf64" | "exp2f64" =>
|
||||
IeeeFloat::<S>::maximum(val, IeeeFloat::<S>::ZERO),
|
||||
_ => val,
|
||||
}
|
||||
}
|
||||
|
||||
/// For the intrinsics:
|
||||
/// - sinf32, sinf64
|
||||
/// - cosf32, cosf64
|
||||
/// - expf32, expf64, exp2f32, exp2f64
|
||||
/// - logf32, logf64, log2f32, log2f64, log10f32, log10f64
|
||||
/// - powf32, powf64
|
||||
///
|
||||
/// # Return
|
||||
///
|
||||
/// Returns `Some(output)` if the `intrinsic` results in a defined fixed `output` specified in the C standard
|
||||
/// (specifically, C23 annex F.10) when given `args` as arguments. Outputs that are unaffected by a relative error
|
||||
/// (such as INF and zero) are not handled here, they are assumed to be handled by the underlying
|
||||
/// implementation. Returns `None` if no specific value is guaranteed.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// For `powf*` operations of the form:
|
||||
///
|
||||
/// - `(SNaN)^(±0)`
|
||||
/// - `1^(SNaN)`
|
||||
///
|
||||
/// The result is implementation-defined:
|
||||
/// - musl returns for both `1.0`
|
||||
/// - glibc returns for both `NaN`
|
||||
///
|
||||
/// This discrepancy exists because SNaN handling is not consistently defined across platforms,
|
||||
/// and the C standard leaves behavior for SNaNs unspecified.
|
||||
///
|
||||
/// Miri chooses to adhere to both implementations and returns either one of them non-deterministically.
|
||||
pub(crate) fn fixed_float_value<S: Semantics>(
|
||||
ecx: &mut MiriInterpCx<'_>,
|
||||
intrinsic_name: &str,
|
||||
args: &[IeeeFloat<S>],
|
||||
) -> Option<IeeeFloat<S>> {
|
||||
let this = ecx.eval_context_mut();
|
||||
let one = IeeeFloat::<S>::one();
|
||||
Some(match (intrinsic_name, args) {
|
||||
// cos(+- 0) = 1
|
||||
("cosf32" | "cosf64", [input]) if input.is_zero() => one,
|
||||
|
||||
// e^0 = 1
|
||||
("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => one,
|
||||
|
||||
// (-1)^(±INF) = 1
|
||||
("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => one,
|
||||
|
||||
// 1^y = 1 for any y, even a NaN
|
||||
("powf32" | "powf64", [base, exp]) if *base == one => {
|
||||
let rng = this.machine.rng.get_mut();
|
||||
// SNaN exponents get special treatment: they might return 1, or a NaN.
|
||||
let return_nan = exp.is_signaling() && this.machine.float_nondet && rng.random();
|
||||
// Handle both the musl and glibc cases non-deterministically.
|
||||
if return_nan { this.generate_nan(args) } else { one }
|
||||
}
|
||||
|
||||
// x^(±0) = 1 for any x, even a NaN
|
||||
("powf32" | "powf64", [base, exp]) if exp.is_zero() => {
|
||||
let rng = this.machine.rng.get_mut();
|
||||
// SNaN bases get special treatment: they might return 1, or a NaN.
|
||||
let return_nan = base.is_signaling() && this.machine.float_nondet && rng.random();
|
||||
// Handle both the musl and glibc cases non-deterministically.
|
||||
if return_nan { this.generate_nan(args) } else { one }
|
||||
}
|
||||
|
||||
// There are a lot of cases for fixed outputs according to the C Standard, but these are
|
||||
// mainly INF or zero which are not affected by the applied error.
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `Some(output)` if `powi` (called `pown` in C) results in a fixed value specified in the
|
||||
/// C standard (specifically, C23 annex F.10.4.6) when doing `base^exp`. Otherwise, returns `None`.
|
||||
pub(crate) fn fixed_powi_float_value<S: Semantics>(
|
||||
ecx: &mut MiriInterpCx<'_>,
|
||||
base: IeeeFloat<S>,
|
||||
exp: i32,
|
||||
) -> Option<IeeeFloat<S>> {
|
||||
let this = ecx.eval_context_mut();
|
||||
Some(match exp {
|
||||
0 => {
|
||||
let one = IeeeFloat::<S>::one();
|
||||
let rng = this.machine.rng.get_mut();
|
||||
let return_nan = this.machine.float_nondet && rng.random() && base.is_signaling();
|
||||
// For SNaN treatment, we are consistent with `powf`above.
|
||||
// (We wouldn't have two, unlike powf all implementations seem to agree for powi,
|
||||
// but for now we are maximally conservative.)
|
||||
if return_nan { this.generate_nan(&[base]) } else { one }
|
||||
}
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn sqrt<S: rustc_apfloat::ieee::Semantics>(x: IeeeFloat<S>) -> IeeeFloat<S> {
|
||||
match x.category() {
|
||||
// preserve zero sign
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue