diff options
| -rw-r--r-- | src/tools/miri/src/intrinsics/mod.rs | 160 | ||||
| -rw-r--r-- | src/tools/miri/src/math.rs | 114 |
2 files changed, 140 insertions, 134 deletions
diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index 5e46768b0e6..8bcec4e7cbe 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -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, - } -} diff --git a/src/tools/miri/src/math.rs b/src/tools/miri/src/math.rs index 6427f3ca6e9..dc1541b43df 100644 --- a/src/tools/miri/src/math.rs +++ b/src/tools/miri/src/math.rs @@ -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 |
