From 69a5eee8ce50cdfe21af3efd22e7a10b1848e98b Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Wed, 5 Mar 2025 15:00:24 +0100 Subject: [PATCH 01/22] add tests to check that the operations behave like the Ieee and C standard specify --- src/tools/miri/tests/pass/float.rs | 62 +++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index cd60b6bd563d7..9f8879129822a 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1007,17 +1007,52 @@ pub fn libm() { assert_approx_eq!(25f32.powf(-2f32), 0.0016f32); assert_approx_eq!(400f64.powf(0.5f64), 20f64); + // Some inputs to powf and powi result in fixed outputs + // and thus must be exactly equal to that value + // TODO: How to test NaN inputs? f*::NAN is not guaranteed + // to be any specific bit pattern (in std). + assert_eq!(1f32.powf(10.0), 1f32); + assert_eq!(1f64.powf(100.0), 1f64); + assert_eq!(1f32.powf(f32::INFINITY), 1f32); + assert_eq!(1f64.powf(f64::INFINITY), 1f64); + + assert_eq!((-1f32).powf(f32::INFINITY), 1f32); + assert_eq!((-1f32).powf(f32::NEG_INFINITY), 1f32); + assert_eq!((-1f64).powf(f64::INFINITY), 1f64); + assert_eq!((-1f64).powf(f64::NEG_INFINITY), 1f64); + + assert_eq!(42f32.powf(0.0), 1f32); + assert_eq!(42f32.powf(-0.0), 1f32); + assert_eq!(42f64.powf(0.0), 1f64); + assert_eq!(42f64.powf(-0.0), 1f64); + + assert_eq!(0f32.powi(10), 0f32); + assert_eq!(0f64.powi(100), 0f64); + assert_eq!(0f32.powi(9), 0f32); + assert_eq!(0f64.powi(99), 0f64); + + assert_eq!((-0f32).powi(10), 0f32); + assert_eq!((-0f64).powi(100), 0f64); + assert_eq!((-0f32).powi(9), -0f32); + assert_eq!((-0f64).powi(99), -0f64); + assert_approx_eq!(1f32.exp(), f32::consts::E); assert_approx_eq!(1f64.exp(), f64::consts::E); + assert_eq!(0f32.exp(), 1f32); + assert_eq!(0f64.exp(), 1f64); assert_approx_eq!(1f32.exp_m1(), f32::consts::E - 1.0); assert_approx_eq!(1f64.exp_m1(), f64::consts::E - 1.0); assert_approx_eq!(10f32.exp2(), 1024f32); assert_approx_eq!(50f64.exp2(), 1125899906842624f64); + assert_eq!(0f32.exp2(), 1f32); + assert_eq!(0f64.exp2(), 1f64); assert_approx_eq!(f32::consts::E.ln(), 1f32); - assert_approx_eq!(1f64.ln(), 0f64); + assert_approx_eq!(f64::consts::E.ln(), 1f64); + assert_eq!(1f32.ln(), 0f32); + assert_eq!(1f64.ln(), 0f64); assert_approx_eq!(0f32.ln_1p(), 0f32); assert_approx_eq!(0f64.ln_1p(), 0f64); @@ -1046,7 +1081,8 @@ pub fn libm() { // Trigonometric functions. - assert_approx_eq!(0f32.sin(), 0f32); + assert_eq!(0f32.sin(), 0f32); + assert_eq!(0f64.sin(), 0f64); assert_approx_eq!((f64::consts::PI / 2f64).sin(), 1f64); assert_approx_eq!(f32::consts::FRAC_PI_6.sin(), 0.5); assert_approx_eq!(f64::consts::FRAC_PI_6.sin(), 0.5); @@ -1058,7 +1094,23 @@ pub fn libm() { assert_approx_eq!(2.0f32.asinh(), 1.443635475178810342493276740273105f32); assert_approx_eq!((-2.0f64).asinh(), -1.443635475178810342493276740273105f64); - assert_approx_eq!(0f32.cos(), 1f32); + // from #4207 + // TODO: should this be the behaviour? I haven't found anything in the IEEE Standard + let halve_pi_single = std::f32::consts::FRAC_PI_2; + let halve_pi_double = std::f64::consts::FRAC_PI_2; + let pi_single = std::f32::consts::PI; + let pi_double = std::f64::consts::PI; + for _ in 0..64 { + // sin() should be clamped to [-1, 1] so asin() can never return NaN + assert!(!halve_pi_single.sin().asin().is_nan()); + assert!(!halve_pi_double.sin().asin().is_nan()); + // sin() should be clamped to [-1, 1] so acos() can never return NaN + assert!(!pi_single.cos().acos().is_nan()); + assert!(!pi_double.cos().acos().is_nan()); + } + + assert_eq!(0f32.cos(), 1f32); + assert_eq!(0f64.cos(), 1f64); assert_approx_eq!((f64::consts::PI * 2f64).cos(), 1f64); assert_approx_eq!(f32::consts::FRAC_PI_3.cos(), 0.5); assert_approx_eq!(f64::consts::FRAC_PI_3.cos(), 0.5); @@ -1328,7 +1380,7 @@ fn test_non_determinism() { ensure_nondet(|| 27.0f32.cbrt()); ensure_nondet(|| 3.0f32.hypot(4.0f32)); ensure_nondet(|| 1f32.sin()); - ensure_nondet(|| 0f32.cos()); + ensure_nondet(|| 3.1f32.cos()); // On i686-pc-windows-msvc , these functions are implemented by calling the `f64` version, // which means the little rounding errors Miri introduces are discard by the cast down to `f32`. // Just skip the test for them. @@ -1362,7 +1414,7 @@ fn test_non_determinism() { ensure_nondet(|| 27.0f64.cbrt()); ensure_nondet(|| 3.0f64.hypot(4.0f64)); ensure_nondet(|| 1f64.sin()); - ensure_nondet(|| 0f64.cos()); + ensure_nondet(|| 3.1f64.cos()); ensure_nondet(|| 1.0f64.tan()); ensure_nondet(|| 1.0f64.asin()); ensure_nondet(|| 5.0f64.acos()); From 058699e92a0ebb8b5fd7a4ec1c88926bddc17bb0 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Wed, 5 Mar 2025 15:16:38 +0100 Subject: [PATCH 02/22] enable non-determinism but with smaller error, 4ULP; conform to IEEE 754 and C Standards --- src/tools/miri/src/intrinsics/mod.rs | 260 +++++++++++++++++++++------ src/tools/miri/src/math.rs | 13 ++ 2 files changed, 215 insertions(+), 58 deletions(-) diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index a3525dcc77ae6..ee1fa2bc3046e 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -3,9 +3,12 @@ mod atomic; mod simd; +use std::ops::Neg; + use rand::Rng; use rustc_abi::Size; -use rustc_apfloat::{Float, Round}; +use rustc_apfloat::ieee::{Double, IeeeFloat, Semantics, Single}; +use rustc_apfloat::{self, Category, Float, Round}; use rustc_middle::mir; use rustc_middle::ty::{self, FloatTy, ScalarInt}; use rustc_span::{Symbol, sym}; @@ -13,7 +16,7 @@ use rustc_span::{Symbol, sym}; use self::atomic::EvalContextExt as _; use self::helpers::{ToHost, ToSoft, check_intrinsic_arg_count}; use self::simd::EvalContextExt as _; -use crate::math::apply_random_float_error_ulp; +use crate::math::{IeeeExt, apply_random_float_error_ulp}; use crate::*; impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} @@ -235,30 +238,42 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { => { let [f] = check_intrinsic_arg_count(args)?; let f = this.read_scalar(f)?.to_f32()?; - // Using host floats (but it's fine, these operations do not have - // guaranteed precision). - let host = f.to_host(); - let res = match intrinsic_name { - "sinf32" => host.sin(), - "cosf32" => host.cos(), - "expf32" => host.exp(), - "exp2f32" => host.exp2(), - "logf32" => host.ln(), - "log10f32" => host.log10(), - "log2f32" => host.log2(), - _ => bug!(), - }; - let res = res.to_soft(); - // Apply a relative error of 16ULP to introduce some non-determinism - // simulating imprecise implementations and optimizations. - // FIXME: temporarily disabled as it breaks std tests. - // let res = apply_random_float_error_ulp( - // this, - // res, - // 4, // log2(16) - // ); + + let res = fixed_float_value(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(); + let res = match intrinsic_name { + "sinf32" => host.sin(), + "cosf32" => host.cos(), + "expf32" => host.exp(), + "exp2f32" => host.exp2(), + "logf32" => host.ln(), + "log10f32" => host.log10(), + "log2f32" => host.log2(), + _ => bug!(), + }; + let res = res.to_soft(); + + // Apply a relative error of 4ULP to introduce some non-determinism + // simulating imprecise implementations and optimizations. + let res = apply_random_float_error_ulp( + this, + res, + 2, // log2(4) + ); + + match intrinsic_name { + // sin and cos: [-1, 1] + "sinf32" | "cosf32" => res.clamp(Single::one().neg(), Single::one()), + // exp: [0, +INF] + "expf32" | "exp2f32" => res.maximum(Single::ZERO), + _ => res, + } + }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; + } #[rustfmt::skip] | "sinf64" @@ -271,30 +286,43 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { => { let [f] = check_intrinsic_arg_count(args)?; let f = this.read_scalar(f)?.to_f64()?; - // Using host floats (but it's fine, these operations do not have - // guaranteed precision). - let host = f.to_host(); - let res = match intrinsic_name { - "sinf64" => host.sin(), - "cosf64" => host.cos(), - "expf64" => host.exp(), - "exp2f64" => host.exp2(), - "logf64" => host.ln(), - "log10f64" => host.log10(), - "log2f64" => host.log2(), - _ => bug!(), - }; - let res = res.to_soft(); - // Apply a relative error of 16ULP to introduce some non-determinism - // simulating imprecise implementations and optimizations. - // FIXME: temporarily disabled as it breaks std tests. - // let res = apply_random_float_error_ulp( - // this, - // res, - // 4, // log2(16) - // ); + + let res = fixed_float_value(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(); + let res = match intrinsic_name { + "sinf64" => host.sin(), + "cosf64" => host.cos(), + "expf64" => host.exp(), + "exp2f64" => host.exp2(), + "logf64" => host.ln(), + "log10f64" => host.log10(), + "log2f64" => host.log2(), + _ => bug!(), + }; + let res = res.to_soft(); + + // Apply a relative error of 4ULP to introduce some non-determinism + // simulating imprecise implementations and optimizations. + let res = apply_random_float_error_ulp( + this, + res, + 2, // log2(4) + ); + + // Clamp values to the output range defined in IEEE 754 9.1. + match intrinsic_name { + // sin and cos: [-1, 1] + "sinf64" | "cosf64" => res.clamp(Double::one().neg(), Double::one()), + // exp: [0, +INF] + "expf64" | "exp2f64" => res.maximum(Double::ZERO), + _ => res, + } + }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; + } "fmaf32" => { @@ -350,43 +378,126 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } "powf32" => { - // FIXME: apply random relative error but without altering behaviour of powf let [f1, f2] = check_intrinsic_arg_count(args)?; let f1 = this.read_scalar(f1)?.to_f32()?; let f2 = this.read_scalar(f2)?.to_f32()?; - // 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 fixed_res = match (f1.category(), f2.category()) { + // 1^y = 1 for any y even a NaN. + // TODO: C Standard says any NaN, IEEE says not a Signaling NaN + (Category::Normal, _) if f1 == 1.0f32.to_soft() => Some(1.0f32.to_soft()), + + // (-1)^(±INF) = 1 + (Category::Normal, Category::Infinity) if f1 == (-1.0f32).to_soft() => + Some(1.0f32.to_soft()), + + // x^(±0) = 1 for any x, even a NaN + (_, Category::Zero) => Some(1.0f32.to_soft()), + + _ => None, + }; + let res = fixed_res.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, 2, // log2(4) + ) + }); let res = this.adjust_nan(res, &[f1, f2]); this.write_scalar(res, dest)?; } "powf64" => { - // FIXME: apply random relative error but without altering behaviour of powf let [f1, f2] = check_intrinsic_arg_count(args)?; let f1 = this.read_scalar(f1)?.to_f64()?; let f2 = this.read_scalar(f2)?.to_f64()?; - // 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 fixed_res = match (f1.category(), f2.category()) { + // 1^y = 1 for any y even a NaN. + // TODO: C says any NaN, IEEE says no a Sign NaN + (Category::Normal, _) if f1 == 1.0f64.to_soft() => Some(1.0f64.to_soft()), + + // (-1)^(±INF) = 1 + (Category::Normal, Category::Infinity) if f1 == (-1.0f64).to_soft() => + Some(1.0f64.to_soft()), + + // x^(±0) = 1 for any x, even a NaN + (_, Category::Zero) => Some(1.0f64.to_soft()), + + // TODO: pow has a lot of "edge" cases which mostly result in ±0 or ±INF + // do we have to catch them all? + _ => None, + }; + let res = fixed_res.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, 2, // log2(4) + ) + }); let res = this.adjust_nan(res, &[f1, f2]); this.write_scalar(res, dest)?; } "powif32" => { - // FIXME: apply random relative error but without altering behaviour of powi let [f, i] = check_intrinsic_arg_count(args)?; let f = this.read_scalar(f)?.to_f32()?; let i = this.read_scalar(i)?.to_i32()?; - // Using host floats (but it's fine, this operation does not have guaranteed precision). - let res = f.to_host().powi(i).to_soft(); + + let fixed_res = match (f.category(), i) { + // Standard specifies we can only fix to 1 if input is is not a Signaling NaN. + (_, 0) if !f.is_signaling() => Some(1.0f32.to_soft()), + + // TODO: Isn't this done by the implementation? And ULP error on 0.0 doesn't have an effect + (Category::Zero, x) if x % 2 == 0 => Some(0.0f32.to_soft()), + + // ±0^x = ±0 with x an odd integer. + (Category::Zero, x) if x % 2 != 0 => Some(f), // preserve sign of zero. + _ => None, + }; + let res = fixed_res.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, 2, // log2(4) + ) + }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; } "powif64" => { - // FIXME: apply random relative error but without altering behaviour of powi let [f, i] = check_intrinsic_arg_count(args)?; let f = this.read_scalar(f)?.to_f64()?; let i = this.read_scalar(i)?.to_i32()?; - // Using host floats (but it's fine, this operation does not have guaranteed precision). - let res = f.to_host().powi(i).to_soft(); + + let fixed_res = match (f.category(), i) { + // Standard specifies we can only fix to 1 if input is is not a Signaling NaN. + (_, 0) if !f.is_signaling() => Some(1.0f64.to_soft()), + + // ±0^x = 0 with x an even integer. + // TODO: Isn't this done by the implementation itself? + (Category::Zero, x) if x % 2 == 0 => Some(0.0f64.to_soft()), + + // ±0^x = ±0 with x an odd integer. + (Category::Zero, x) if x % 2 != 0 => Some(f), // preserve sign of zero. + _ => None, + }; + let res = fixed_res.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, 2, // log2(4) + ) + }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; } @@ -522,3 +633,36 @@ fn apply_random_float_error_to_imm<'tcx>( interp_ok(ImmTy::from_scalar_int(res, val.layout)) } + +/// For the operations: +/// - sinf32, sinf64 +/// - cosf32, cosf64 +/// - expf32, expf64, exp2f32, exp2f64 +/// - logf32, logf64, log2f32, log2f64, log10f32, log10f64 +/// +/// Returns Some(`output`) if the operation results in a defined fixed `output` when given `input` +/// as an input, else None. +fn fixed_float_value( + intrinsic_name: &str, + input: IeeeFloat, +) -> Option> +where + IeeeFloat: std::cmp::PartialEq, +{ + // TODO: Should we really fix to ±0? Applying an error has no effect. + let one = IeeeFloat::::one(); + match intrinsic_name { + "sinf32" | "sinf64" if input.is_pos_zero() => Some(IeeeFloat::::ZERO), + "sinf32" | "sinf64" if input.is_neg_zero() => Some(-IeeeFloat::::ZERO), + "cosf32" | "cosf64" if input.is_zero() => Some(one), + "expf32" | "expf64" | "exp2f32" | "exp2f64" if input.is_zero() => Some(one), + #[rustfmt::skip] + "logf32" + | "logf64" + | "log10f32" + | "log10f64" + | "log2f32" + | "log2f64" if input == one => Some(IeeeFloat::::ZERO), + _ => None, + } +} diff --git a/src/tools/miri/src/math.rs b/src/tools/miri/src/math.rs index fdd021f85394b..351b0df3ee37c 100644 --- a/src/tools/miri/src/math.rs +++ b/src/tools/miri/src/math.rs @@ -125,6 +125,19 @@ pub(crate) fn sqrt(x: IeeeFloat) -> IeeeFl } } +impl IeeeExt for IeeeFloat {} +/// Extend functionality of rustc_apfloat softfloats +pub trait IeeeExt: rustc_apfloat::Float { + fn one() -> Self { + Self::from_u128(1).value + } + + #[inline] + fn clamp(self, min: Self, max: Self) -> Self { + self.maximum(min).minimum(max) + } +} + #[cfg(test)] mod tests { use rustc_apfloat::ieee::{DoubleS, HalfS, IeeeFloat, QuadS, SingleS}; From 072830fc34d60cc6f554e085207b2f8f492f965a Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Wed, 5 Mar 2025 15:20:10 +0100 Subject: [PATCH 03/22] set 2^100 directly in integer decode tests because powf is non-deterministic --- library/coretests/tests/num/dec2flt/float.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/coretests/tests/num/dec2flt/float.rs b/library/coretests/tests/num/dec2flt/float.rs index b5afd3e3b2436..4e813743581fe 100644 --- a/library/coretests/tests/num/dec2flt/float.rs +++ b/library/coretests/tests/num/dec2flt/float.rs @@ -4,7 +4,8 @@ use core::num::dec2flt::float::RawFloat; fn test_f32_integer_decode() { assert_eq!(3.14159265359f32.integer_decode(), (13176795, -22, 1)); assert_eq!((-8573.5918555f32).integer_decode(), (8779358, -10, -1)); - assert_eq!(2f32.powf(100.0).integer_decode(), (8388608, 77, 1)); + // Set 2^100 directly instead of using powf, because it doesn't guarentee precision + assert_eq!(1.2676506e30_f32.integer_decode(), (8388608, 77, 1)); assert_eq!(0f32.integer_decode(), (0, -150, 1)); assert_eq!((-0f32).integer_decode(), (0, -150, -1)); assert_eq!(f32::INFINITY.integer_decode(), (8388608, 105, 1)); @@ -20,7 +21,8 @@ fn test_f32_integer_decode() { fn test_f64_integer_decode() { assert_eq!(3.14159265359f64.integer_decode(), (7074237752028906, -51, 1)); assert_eq!((-8573.5918555f64).integer_decode(), (4713381968463931, -39, -1)); - assert_eq!(2f64.powf(100.0).integer_decode(), (4503599627370496, 48, 1)); + // Set 2^100 directly instead of using powf, because it doesn't guarentee precision + assert_eq!(1.2676506002282294e30_f64.integer_decode(), (4503599627370496, 48, 1)); assert_eq!(0f64.integer_decode(), (0, -1075, 1)); assert_eq!((-0f64).integer_decode(), (0, -1075, -1)); assert_eq!(f64::INFINITY.integer_decode(), (4503599627370496, 972, 1)); From aa357afb5660a44fbe2aafafa347c2de8af087ca Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Wed, 5 Mar 2025 15:20:52 +0100 Subject: [PATCH 04/22] assert_approx_eq! now accepts up to 1e-4 --- library/std/tests/floats/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/tests/floats/lib.rs b/library/std/tests/floats/lib.rs index de5a3cdbd0f93..74d173c18de0b 100644 --- a/library/std/tests/floats/lib.rs +++ b/library/std/tests/floats/lib.rs @@ -3,9 +3,9 @@ use std::fmt; use std::ops::{Add, Div, Mul, Rem, Sub}; -/// Verify that floats are within a tolerance of each other, 1.0e-6 by default. +/// Verify that floats are within a tolerance of each other, 1.0e-4 by default so we do not break test in Miri. macro_rules! assert_approx_eq { - ($a:expr, $b:expr) => {{ assert_approx_eq!($a, $b, 1.0e-6) }}; + ($a:expr, $b:expr) => {{ assert_approx_eq!($a, $b, 1.0e-4) }}; ($a:expr, $b:expr, $lim:expr) => {{ let (a, b) = (&$a, &$b); let diff = (*a - *b).abs(); From 50f7653e179a06aa4cb9d9ada484369cc83faa11 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Wed, 5 Mar 2025 15:21:09 +0100 Subject: [PATCH 05/22] change tests that made wrong assumptions about the Ieee and C standards --- library/std/tests/floats/f32.rs | 10 +++++----- library/std/tests/floats/f64.rs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/library/std/tests/floats/f32.rs b/library/std/tests/floats/f32.rs index d99b03cb255f7..28d1ff62938c2 100644 --- a/library/std/tests/floats/f32.rs +++ b/library/std/tests/floats/f32.rs @@ -442,7 +442,7 @@ fn test_powi() { let nan: f32 = f32::NAN; let inf: f32 = f32::INFINITY; let neg_inf: f32 = f32::NEG_INFINITY; - assert_eq!(1.0f32.powi(1), 1.0); + assert_approx_eq!(1.0f32.powi(1), 1.0); assert_approx_eq!((-3.1f32).powi(2), 9.61); assert_approx_eq!(5.9f32.powi(-2), 0.028727); assert_eq!(8.3f32.powi(0), 1.0); @@ -494,7 +494,7 @@ fn test_exp() { #[test] fn test_exp2() { - assert_eq!(32.0, 5.0f32.exp2()); + assert_approx_eq!(32.0, 5.0f32.exp2()); assert_eq!(1.0, 0.0f32.exp2()); let inf: f32 = f32::INFINITY; @@ -525,9 +525,9 @@ fn test_log() { let nan: f32 = f32::NAN; let inf: f32 = f32::INFINITY; let neg_inf: f32 = f32::NEG_INFINITY; - assert_eq!(10.0f32.log(10.0), 1.0); + assert_approx_eq!(10.0f32.log(10.0), 1.0); assert_approx_eq!(2.3f32.log(3.5), 0.664858); - assert_eq!(1.0f32.exp().log(1.0f32.exp()), 1.0); + assert_approx_eq!(1.0f32.exp().log(1.0f32.exp()), 1.0); assert!(1.0f32.log(1.0).is_nan()); assert!(1.0f32.log(-13.9).is_nan()); assert!(nan.log(2.3).is_nan()); @@ -559,7 +559,7 @@ fn test_log10() { let nan: f32 = f32::NAN; let inf: f32 = f32::INFINITY; let neg_inf: f32 = f32::NEG_INFINITY; - assert_eq!(10.0f32.log10(), 1.0); + assert_approx_eq!(10.0f32.log10(), 1.0); assert_approx_eq!(2.3f32.log10(), 0.361728); assert_approx_eq!(1.0f32.exp().log10(), 0.434294); assert_eq!(1.0f32.log10(), 0.0); diff --git a/library/std/tests/floats/f64.rs b/library/std/tests/floats/f64.rs index 611670751bb52..bdea2bfdc61cb 100644 --- a/library/std/tests/floats/f64.rs +++ b/library/std/tests/floats/f64.rs @@ -427,7 +427,7 @@ fn test_powi() { let nan: f64 = f64::NAN; let inf: f64 = f64::INFINITY; let neg_inf: f64 = f64::NEG_INFINITY; - assert_eq!(1.0f64.powi(1), 1.0); + assert_approx_eq!(1.0f64.powi(1), 1.0); assert_approx_eq!((-3.1f64).powi(2), 9.61); assert_approx_eq!(5.9f64.powi(-2), 0.028727); assert_eq!(8.3f64.powi(0), 1.0); @@ -479,7 +479,7 @@ fn test_exp() { #[test] fn test_exp2() { - assert_eq!(32.0, 5.0f64.exp2()); + assert_approx_eq!(32.0, 5.0f64.exp2()); assert_eq!(1.0, 0.0f64.exp2()); let inf: f64 = f64::INFINITY; @@ -510,9 +510,9 @@ fn test_log() { let nan: f64 = f64::NAN; let inf: f64 = f64::INFINITY; let neg_inf: f64 = f64::NEG_INFINITY; - assert_eq!(10.0f64.log(10.0), 1.0); + assert_approx_eq!(10.0f64.log(10.0), 1.0); assert_approx_eq!(2.3f64.log(3.5), 0.664858); - assert_eq!(1.0f64.exp().log(1.0f64.exp()), 1.0); + assert_approx_eq!(1.0f64.exp().log(1.0f64.exp()), 1.0); assert!(1.0f64.log(1.0).is_nan()); assert!(1.0f64.log(-13.9).is_nan()); assert!(nan.log(2.3).is_nan()); @@ -544,7 +544,7 @@ fn test_log10() { let nan: f64 = f64::NAN; let inf: f64 = f64::INFINITY; let neg_inf: f64 = f64::NEG_INFINITY; - assert_eq!(10.0f64.log10(), 1.0); + assert_approx_eq!(10.0f64.log10(), 1.0); assert_approx_eq!(2.3f64.log10(), 0.361728); assert_approx_eq!(1.0f64.exp().log10(), 0.434294); assert_eq!(1.0f64.log10(), 0.0); From 4d352c53b362154d22f6aaca353e3da86f053939 Mon Sep 17 00:00:00 2001 From: Lorrens Pantelis <100197010+LorrensP-2158466@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:11:49 +0100 Subject: [PATCH 06/22] fix copy/paste mistake Co-authored-by: Dante Broggi <34220985+Dante-Broggi@users.noreply.github.com> --- src/tools/miri/tests/pass/float.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 9f8879129822a..b97670d533d5d 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1104,7 +1104,7 @@ pub fn libm() { // sin() should be clamped to [-1, 1] so asin() can never return NaN assert!(!halve_pi_single.sin().asin().is_nan()); assert!(!halve_pi_double.sin().asin().is_nan()); - // sin() should be clamped to [-1, 1] so acos() can never return NaN + // cos() should be clamped to [-1, 1] so acos() can never return NaN assert!(!pi_single.cos().acos().is_nan()); assert!(!pi_double.cos().acos().is_nan()); } From 8d6de60a9f19884637f527c75683c695a38b6fac Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Mon, 10 Mar 2025 17:18:43 +0100 Subject: [PATCH 07/22] apply simple requests from reviewer --- src/tools/miri/src/intrinsics/mod.rs | 14 +++++++------- src/tools/miri/tests/pass/float.rs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index ee1fa2bc3046e..e7a1dd009d749 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -263,6 +263,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { 2, // log2(4) ); + // Clamp the result to the guaranteed range of this function according to the C standard, + // if any. match intrinsic_name { // sin and cos: [-1, 1] "sinf32" | "cosf32" => res.clamp(Single::one().neg(), Single::one()), @@ -311,7 +313,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { 2, // log2(4) ); - // Clamp values to the output range defined in IEEE 754 9.1. + // Clamp the result to the guaranteed range of this function according to the C standard, + // if any. match intrinsic_name { // sin and cos: [-1, 1] "sinf64" | "cosf64" => res.clamp(Double::one().neg(), Double::one()), @@ -383,8 +386,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let f2 = this.read_scalar(f2)?.to_f32()?; let fixed_res = match (f1.category(), f2.category()) { - // 1^y = 1 for any y even a NaN. - // TODO: C Standard says any NaN, IEEE says not a Signaling NaN + // 1^y = 1 for any y, even a NaN. (Category::Normal, _) if f1 == 1.0f32.to_soft() => Some(1.0f32.to_soft()), // (-1)^(±INF) = 1 @@ -415,7 +417,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let fixed_res = match (f1.category(), f2.category()) { // 1^y = 1 for any y even a NaN. - // TODO: C says any NaN, IEEE says no a Sign NaN (Category::Normal, _) if f1 == 1.0f64.to_soft() => Some(1.0f64.to_soft()), // (-1)^(±INF) = 1 @@ -649,11 +650,10 @@ fn fixed_float_value( where IeeeFloat: std::cmp::PartialEq, { - // TODO: Should we really fix to ±0? Applying an error has no effect. let one = IeeeFloat::::one(); match intrinsic_name { - "sinf32" | "sinf64" if input.is_pos_zero() => Some(IeeeFloat::::ZERO), - "sinf32" | "sinf64" if input.is_neg_zero() => Some(-IeeeFloat::::ZERO), + // sin(+- 0) = +- 0. + "sinf32" | "sinf64" if input.is_zero() => Some(input), "cosf32" | "cosf64" if input.is_zero() => Some(one), "expf32" | "expf64" | "exp2f32" | "exp2f64" if input.is_zero() => Some(one), #[rustfmt::skip] diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index b97670d533d5d..5b8e41616df98 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1094,8 +1094,8 @@ pub fn libm() { assert_approx_eq!(2.0f32.asinh(), 1.443635475178810342493276740273105f32); assert_approx_eq!((-2.0f64).asinh(), -1.443635475178810342493276740273105f64); - // from #4207 - // TODO: should this be the behaviour? I haven't found anything in the IEEE Standard + // Ensure `sin` always returns something that is a valid input for `asin`, and same for + // `cos` and `acos` let halve_pi_single = std::f32::consts::FRAC_PI_2; let halve_pi_double = std::f64::consts::FRAC_PI_2; let pi_single = std::f32::consts::PI; From 776c9af27e178bd99fc061a51a0036d33b7ac173 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Mon, 10 Mar 2025 17:19:32 +0100 Subject: [PATCH 08/22] change assert_approx_eq! back to old version and manually set error tolerance to pass the tests in miri --- library/std/tests/floats/f32.rs | 53 +++++++++++++++++++++++++++------ library/std/tests/floats/lib.rs | 2 +- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/library/std/tests/floats/f32.rs b/library/std/tests/floats/f32.rs index 28d1ff62938c2..73d32d9687dec 100644 --- a/library/std/tests/floats/f32.rs +++ b/library/std/tests/floats/f32.rs @@ -443,7 +443,11 @@ fn test_powi() { let inf: f32 = f32::INFINITY; let neg_inf: f32 = f32::NEG_INFINITY; assert_approx_eq!(1.0f32.powi(1), 1.0); - assert_approx_eq!((-3.1f32).powi(2), 9.61); + assert_approx_eq!( + (-3.1f32).powi(2), + 9.61, + 1e-4 /* Miri float-non-det: Make tests pass for now */ + ); assert_approx_eq!(5.9f32.powi(-2), 0.028727); assert_eq!(8.3f32.powi(0), 1.0); assert!(nan.powi(2).is_nan()); @@ -457,9 +461,17 @@ fn test_powf() { let inf: f32 = f32::INFINITY; let neg_inf: f32 = f32::NEG_INFINITY; assert_eq!(1.0f32.powf(1.0), 1.0); - assert_approx_eq!(3.4f32.powf(4.5), 246.408218); + assert_approx_eq!( + 3.4f32.powf(4.5), + 246.408218, + 1e-4 /* Miri float-non-det: Make tests pass for now */ + ); assert_approx_eq!(2.7f32.powf(-3.2), 0.041652); - assert_approx_eq!((-3.1f32).powf(2.0), 9.61); + assert_approx_eq!( + (-3.1f32).powf(2.0), + 9.61, + 1e-4 /* Miri float-non-det: Make tests pass for now */ + ); assert_approx_eq!(5.9f32.powf(-2.0), 0.028727); assert_eq!(8.3f32.powf(0.0), 1.0); assert!(nan.powf(2.0).is_nan()); @@ -482,7 +494,11 @@ fn test_sqrt_domain() { fn test_exp() { assert_eq!(1.0, 0.0f32.exp()); assert_approx_eq!(2.718282, 1.0f32.exp()); - assert_approx_eq!(148.413162, 5.0f32.exp()); + assert_approx_eq!( + 148.413162, + 5.0f32.exp(), + 1e-4 /* Miri float-non-det: Make tests pass for now */ + ); let inf: f32 = f32::INFINITY; let neg_inf: f32 = f32::NEG_INFINITY; @@ -494,7 +510,11 @@ fn test_exp() { #[test] fn test_exp2() { - assert_approx_eq!(32.0, 5.0f32.exp2()); + assert_approx_eq!( + 32.0, + 5.0f32.exp2(), + 1e-4 /* Miri float-non-det: Make tests pass for now */ + ); assert_eq!(1.0, 0.0f32.exp2()); let inf: f32 = f32::INFINITY; @@ -517,7 +537,11 @@ fn test_ln() { assert!((-2.3f32).ln().is_nan()); assert_eq!((-0.0f32).ln(), neg_inf); assert_eq!(0.0f32.ln(), neg_inf); - assert_approx_eq!(4.0f32.ln(), 1.386294); + assert_approx_eq!( + 4.0f32.ln(), + 1.386294, + 1e-4 /* Miri float-non-det: Make tests pass for now */ + ); } #[test] @@ -543,7 +567,10 @@ fn test_log2() { let nan: f32 = f32::NAN; let inf: f32 = f32::INFINITY; let neg_inf: f32 = f32::NEG_INFINITY; - assert_approx_eq!(10.0f32.log2(), 3.321928); + assert_approx_eq!( + 10.0f32.log2(), + 3.321928 /* Miri float-non-det: Make tests pass for now */ + ); assert_approx_eq!(2.3f32.log2(), 1.201634); assert_approx_eq!(1.0f32.exp().log2(), 1.442695); assert!(nan.log2().is_nan()); @@ -639,7 +666,11 @@ fn test_acosh() { assert_approx_eq!(3.0f32.acosh(), 1.76274717403908605046521864995958461f32); // test for low accuracy from issue 104548 - assert_approx_eq!(60.0f32, 60.0f32.cosh().acosh()); + assert_approx_eq!( + 60.0f32, + 60.0f32.cosh().acosh(), + 1e-4 /* Miri float-non-det: Make tests pass for now */ + ); } #[test] @@ -718,7 +749,11 @@ fn test_real_consts() { let ln_10: f32 = consts::LN_10; assert_approx_eq!(frac_pi_2, pi / 2f32); - assert_approx_eq!(frac_pi_3, pi / 3f32); + assert_approx_eq!( + frac_pi_3, + pi / 3f32, + 1e-4 /* Miri float-non-det: Make tests pass for now */ + ); assert_approx_eq!(frac_pi_4, pi / 4f32); assert_approx_eq!(frac_pi_6, pi / 6f32); assert_approx_eq!(frac_pi_8, pi / 8f32); diff --git a/library/std/tests/floats/lib.rs b/library/std/tests/floats/lib.rs index 74d173c18de0b..43c43a53a120b 100644 --- a/library/std/tests/floats/lib.rs +++ b/library/std/tests/floats/lib.rs @@ -5,7 +5,7 @@ use std::ops::{Add, Div, Mul, Rem, Sub}; /// Verify that floats are within a tolerance of each other, 1.0e-4 by default so we do not break test in Miri. macro_rules! assert_approx_eq { - ($a:expr, $b:expr) => {{ assert_approx_eq!($a, $b, 1.0e-4) }}; + ($a:expr, $b:expr) => {{ assert_approx_eq!($a, $b, 1.0e-6) }}; ($a:expr, $b:expr, $lim:expr) => {{ let (a, b) = (&$a, &$b); let diff = (*a - *b).abs(); From a2b9890fe5a16e0fcfcc811e7bbda2657ef3d8a0 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Mon, 10 Mar 2025 18:52:18 +0100 Subject: [PATCH 09/22] change doc-tests to increase EPSILON to make the tests pass --- library/std/src/f32.rs | 32 ++++++++++++++++---------------- library/std/src/f64.rs | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/library/std/src/f32.rs b/library/std/src/f32.rs index baf7002f3803c..439977c77ece1 100644 --- a/library/std/src/f32.rs +++ b/library/std/src/f32.rs @@ -303,7 +303,7 @@ impl f32 { /// ``` /// let x = 2.0_f32; /// let abs_difference = (x.powi(2) - (x * x)).abs(); - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 8.0 * f32::EPSILON); /// /// assert_eq!(f32::powi(f32::NAN, 0), 1.0); /// ``` @@ -327,7 +327,7 @@ impl f32 { /// ``` /// let x = 2.0_f32; /// let abs_difference = (x.powf(2.0) - (x * x)).abs(); - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 8.0 * f32::EPSILON); /// /// assert_eq!(f32::powf(1.0, f32::NAN), 1.0); /// assert_eq!(f32::powf(f32::NAN, 0.0), 1.0); @@ -387,7 +387,7 @@ impl f32 { /// // ln(e) - 1 == 0 /// let abs_difference = (e.ln() - 1.0).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` #[rustc_allow_incoherent_impl] #[must_use = "method returns a new number and does not mutate the original value"] @@ -412,7 +412,7 @@ impl f32 { /// // 2^2 - 4 == 0 /// let abs_difference = (f.exp2() - 4.0).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 8.0 * f32::EPSILON); /// ``` #[rustc_allow_incoherent_impl] #[must_use = "method returns a new number and does not mutate the original value"] @@ -441,7 +441,7 @@ impl f32 { /// // ln(e) - 1 == 0 /// let abs_difference = (e.ln() - 1.0).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` /// /// Non-positive values: @@ -478,7 +478,7 @@ impl f32 { /// // log5(5) - 1 == 0 /// let abs_difference = (five.log(5.0) - 1.0).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` /// /// Non-positive values: @@ -511,7 +511,7 @@ impl f32 { /// // log2(2) - 1 == 0 /// let abs_difference = (two.log2() - 1.0).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` /// /// Non-positive values: @@ -544,7 +544,7 @@ impl f32 { /// // log10(10) - 1 == 0 /// let abs_difference = (ten.log10() - 1.0).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` /// /// Non-positive values: @@ -650,7 +650,7 @@ impl f32 { /// // sqrt(x^2 + y^2) /// let abs_difference = (x.hypot(y) - (x.powi(2) + y.powi(2)).sqrt()).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` #[rustc_allow_incoherent_impl] #[must_use = "method returns a new number and does not mutate the original value"] @@ -674,7 +674,7 @@ impl f32 { /// /// let abs_difference = (x.sin() - 1.0).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` #[rustc_allow_incoherent_impl] #[must_use = "method returns a new number and does not mutate the original value"] @@ -698,7 +698,7 @@ impl f32 { /// /// let abs_difference = (x.cos() - 1.0).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` #[rustc_allow_incoherent_impl] #[must_use = "method returns a new number and does not mutate the original value"] @@ -782,7 +782,7 @@ impl f32 { /// // acos(cos(pi/4)) /// let abs_difference = (f.cos().acos() - std::f32::consts::FRAC_PI_4).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` #[doc(alias = "arccos")] #[rustc_allow_incoherent_impl] @@ -882,8 +882,8 @@ impl f32 { /// let abs_difference_0 = (f.0 - x.sin()).abs(); /// let abs_difference_1 = (f.1 - x.cos()).abs(); /// - /// assert!(abs_difference_0 <= f32::EPSILON); - /// assert!(abs_difference_1 <= f32::EPSILON); + /// assert!(abs_difference_0 <= 4.0 * f32::EPSILON); + /// assert!(abs_difference_1 <= 4.0 * f32::EPSILON); /// ``` #[doc(alias = "sincos")] #[rustc_allow_incoherent_impl] @@ -1065,7 +1065,7 @@ impl f32 { /// /// let abs_difference = (f - x).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` #[doc(alias = "arcsinh")] #[rustc_allow_incoherent_impl] @@ -1093,7 +1093,7 @@ impl f32 { /// /// let abs_difference = (f - x).abs(); /// - /// assert!(abs_difference <= f32::EPSILON); + /// assert!(abs_difference <= 4.0 * f32::EPSILON); /// ``` #[doc(alias = "arccosh")] #[rustc_allow_incoherent_impl] diff --git a/library/std/src/f64.rs b/library/std/src/f64.rs index 84fd9bfb7b680..5bf9767b56fe6 100644 --- a/library/std/src/f64.rs +++ b/library/std/src/f64.rs @@ -303,7 +303,7 @@ impl f64 { /// ``` /// let x = 2.0_f64; /// let abs_difference = (x.powi(2) - (x * x)).abs(); - /// assert!(abs_difference <= f64::EPSILON); + /// assert!(abs_difference <= 8.0 * f64::EPSILON); /// /// assert_eq!(f64::powi(f64::NAN, 0), 1.0); /// ``` @@ -327,7 +327,7 @@ impl f64 { /// ``` /// let x = 2.0_f64; /// let abs_difference = (x.powf(2.0) - (x * x)).abs(); - /// assert!(abs_difference <= f64::EPSILON); + /// assert!(abs_difference <= 8.0 * f64::EPSILON); /// /// assert_eq!(f64::powf(1.0, f64::NAN), 1.0); /// assert_eq!(f64::powf(f64::NAN, 0.0), 1.0); From 6342266fbe5a736a28438eee0ae54053aa83a2c3 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Mon, 10 Mar 2025 20:17:20 +0100 Subject: [PATCH 10/22] deduplicate code of pow functions to use a macro instead --- src/tools/miri/src/intrinsics/mod.rs | 152 +++++++++++---------------- 1 file changed, 62 insertions(+), 90 deletions(-) diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index e7a1dd009d749..cc7a0e709a66d 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -8,7 +8,7 @@ use std::ops::Neg; use rand::Rng; use rustc_abi::Size; use rustc_apfloat::ieee::{Double, IeeeFloat, Semantics, Single}; -use rustc_apfloat::{self, Category, Float, Round}; +use rustc_apfloat::{self, Float, Round}; use rustc_middle::mir; use rustc_middle::ty::{self, FloatTy, ScalarInt}; use rustc_span::{Symbol, sym}; @@ -19,6 +19,63 @@ use self::simd::EvalContextExt as _; use crate::math::{IeeeExt, apply_random_float_error_ulp}; use crate::*; +macro_rules! pow_impl { + ($this: expr, powf($f1: expr, $f2: expr)) => {{ + let host1 = $f1.to_host(); + let host2 = $f2.to_host(); + + let fixed_res = match (host1, host2) { + // 1^y = 1 for any y even a NaN. + (one @ 1.0, _) => Some(one), + + // (-1)^(±INF) = 1 + (-1.0, exp) if exp.is_infinite() => Some(1.0), + + // x^(±0) = 1 for any x, even a NaN + (_, 0.0) => Some(1.0), + + _ => None, + }; + fixed_res.map_or_else(|| { + // Using host floats (but it's fine, this operation does not have guaranteed precision). + let res = host1.powf(host2).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, 2, // log2(4) + ) + }, ToSoft::to_soft) + }}; + ($this: expr, powi($f: expr, $i: expr)) => {{ + let host = $f.to_host(); + let exp = $i; + + let fixed_res = match (host, exp) { + // ±0^x = ±0 with x an odd integer. + (zero @ 0.0, x) if x % 2 != 0 => Some(zero), // preserve sign of zero. + + // ±0^x = +0 with x an even integer. + (0.0, x) if x % 2 == 0 => Some(0.0), + + // x^0 = 1: + // Standard specifies we can only fix to 1 if x is is not a Signaling NaN. + // TODO: How to do this in normal rust? + (_, 0) /* if !f.is_signaling() */ => Some(1.0), + + _ => None, + }; + fixed_res.map_or_else(|| { + // Using host floats (but it's fine, this operation does not have guaranteed precision). + let res = host.powi(exp).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, 2, // log2(4) + ) + }, ToSoft::to_soft) + }}; +} + impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { fn call_intrinsic( @@ -385,28 +442,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let f1 = this.read_scalar(f1)?.to_f32()?; let f2 = this.read_scalar(f2)?.to_f32()?; - let fixed_res = match (f1.category(), f2.category()) { - // 1^y = 1 for any y, even a NaN. - (Category::Normal, _) if f1 == 1.0f32.to_soft() => Some(1.0f32.to_soft()), - - // (-1)^(±INF) = 1 - (Category::Normal, Category::Infinity) if f1 == (-1.0f32).to_soft() => - Some(1.0f32.to_soft()), - - // x^(±0) = 1 for any x, even a NaN - (_, Category::Zero) => Some(1.0f32.to_soft()), - - _ => None, - }; - let res = fixed_res.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, 2, // log2(4) - ) - }); + let res = pow_impl!(this, powf(f1, f2)); let res = this.adjust_nan(res, &[f1, f2]); this.write_scalar(res, dest)?; } @@ -415,30 +451,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let f1 = this.read_scalar(f1)?.to_f64()?; let f2 = this.read_scalar(f2)?.to_f64()?; - let fixed_res = match (f1.category(), f2.category()) { - // 1^y = 1 for any y even a NaN. - (Category::Normal, _) if f1 == 1.0f64.to_soft() => Some(1.0f64.to_soft()), - - // (-1)^(±INF) = 1 - (Category::Normal, Category::Infinity) if f1 == (-1.0f64).to_soft() => - Some(1.0f64.to_soft()), - - // x^(±0) = 1 for any x, even a NaN - (_, Category::Zero) => Some(1.0f64.to_soft()), - - // TODO: pow has a lot of "edge" cases which mostly result in ±0 or ±INF - // do we have to catch them all? - _ => None, - }; - let res = fixed_res.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, 2, // log2(4) - ) - }); + let res = pow_impl!(this, powf(f1, f2)); let res = this.adjust_nan(res, &[f1, f2]); this.write_scalar(res, dest)?; } @@ -448,27 +461,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 fixed_res = match (f.category(), i) { - // Standard specifies we can only fix to 1 if input is is not a Signaling NaN. - (_, 0) if !f.is_signaling() => Some(1.0f32.to_soft()), - - // TODO: Isn't this done by the implementation? And ULP error on 0.0 doesn't have an effect - (Category::Zero, x) if x % 2 == 0 => Some(0.0f32.to_soft()), - - // ±0^x = ±0 with x an odd integer. - (Category::Zero, x) if x % 2 != 0 => Some(f), // preserve sign of zero. - _ => None, - }; - let res = fixed_res.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, 2, // log2(4) - ) - }); + let res = pow_impl!(this, powi(f, i)); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; } @@ -477,28 +470,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let f = this.read_scalar(f)?.to_f64()?; let i = this.read_scalar(i)?.to_i32()?; - let fixed_res = match (f.category(), i) { - // Standard specifies we can only fix to 1 if input is is not a Signaling NaN. - (_, 0) if !f.is_signaling() => Some(1.0f64.to_soft()), - - // ±0^x = 0 with x an even integer. - // TODO: Isn't this done by the implementation itself? - (Category::Zero, x) if x % 2 == 0 => Some(0.0f64.to_soft()), - - // ±0^x = ±0 with x an odd integer. - (Category::Zero, x) if x % 2 != 0 => Some(f), // preserve sign of zero. - _ => None, - }; - let res = fixed_res.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, 2, // log2(4) - ) - }); + let res = pow_impl!(this, powi(f, i)); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; } From a9efc0d78b65b1ca0115b7159058a42af0472e5a Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Tue, 11 Mar 2025 20:06:01 +0100 Subject: [PATCH 11/22] extend `fixed_float_value` to accept powf + create fix_float_value fn for powi + a function that clamps a float based on the operation used --- src/tools/miri/src/intrinsics/mod.rs | 240 ++++++++++++++++----------- 1 file changed, 147 insertions(+), 93 deletions(-) diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index cc7a0e709a66d..38a66e68d459f 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -7,8 +7,8 @@ use std::ops::Neg; use rand::Rng; use rustc_abi::Size; -use rustc_apfloat::ieee::{Double, IeeeFloat, Semantics, Single}; -use rustc_apfloat::{self, Float, Round}; +use rustc_apfloat::ieee::{IeeeFloat, Semantics}; +use rustc_apfloat::{self, Category, Float, Round}; use rustc_middle::mir; use rustc_middle::ty::{self, FloatTy, ScalarInt}; use rustc_span::{Symbol, sym}; @@ -19,63 +19,6 @@ use self::simd::EvalContextExt as _; use crate::math::{IeeeExt, apply_random_float_error_ulp}; use crate::*; -macro_rules! pow_impl { - ($this: expr, powf($f1: expr, $f2: expr)) => {{ - let host1 = $f1.to_host(); - let host2 = $f2.to_host(); - - let fixed_res = match (host1, host2) { - // 1^y = 1 for any y even a NaN. - (one @ 1.0, _) => Some(one), - - // (-1)^(±INF) = 1 - (-1.0, exp) if exp.is_infinite() => Some(1.0), - - // x^(±0) = 1 for any x, even a NaN - (_, 0.0) => Some(1.0), - - _ => None, - }; - fixed_res.map_or_else(|| { - // Using host floats (but it's fine, this operation does not have guaranteed precision). - let res = host1.powf(host2).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, 2, // log2(4) - ) - }, ToSoft::to_soft) - }}; - ($this: expr, powi($f: expr, $i: expr)) => {{ - let host = $f.to_host(); - let exp = $i; - - let fixed_res = match (host, exp) { - // ±0^x = ±0 with x an odd integer. - (zero @ 0.0, x) if x % 2 != 0 => Some(zero), // preserve sign of zero. - - // ±0^x = +0 with x an even integer. - (0.0, x) if x % 2 == 0 => Some(0.0), - - // x^0 = 1: - // Standard specifies we can only fix to 1 if x is is not a Signaling NaN. - // TODO: How to do this in normal rust? - (_, 0) /* if !f.is_signaling() */ => Some(1.0), - - _ => None, - }; - fixed_res.map_or_else(|| { - // Using host floats (but it's fine, this operation does not have guaranteed precision). - let res = host.powi(exp).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, 2, // log2(4) - ) - }, ToSoft::to_soft) - }}; -} - impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { fn call_intrinsic( @@ -296,7 +239,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(intrinsic_name, f).unwrap_or_else(||{ + let res = fixed_float_value(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(); @@ -322,13 +265,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. - match intrinsic_name { - // sin and cos: [-1, 1] - "sinf32" | "cosf32" => res.clamp(Single::one().neg(), Single::one()), - // exp: [0, +INF] - "expf32" | "exp2f32" => res.maximum(Single::ZERO), - _ => res, - } + clamp_float_value(intrinsic_name, res) }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; @@ -346,7 +283,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(intrinsic_name, f).unwrap_or_else(||{ + let res = fixed_float_value(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(); @@ -372,13 +309,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. - match intrinsic_name { - // sin and cos: [-1, 1] - "sinf64" | "cosf64" => res.clamp(Double::one().neg(), Double::one()), - // exp: [0, +INF] - "expf64" | "exp2f64" => res.maximum(Double::ZERO), - _ => res, - } + clamp_float_value(intrinsic_name, res) }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; @@ -442,7 +373,16 @@ 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 = pow_impl!(this, powf(f1, f2)); + let res = fixed_float_value(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, 2, // log2(4) + ) + }); let res = this.adjust_nan(res, &[f1, f2]); this.write_scalar(res, dest)?; } @@ -451,7 +391,16 @@ 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 = pow_impl!(this, powf(f1, f2)); + let res = fixed_float_value(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, 2, // log2(4) + ) + }); let res = this.adjust_nan(res, &[f1, f2]); this.write_scalar(res, dest)?; } @@ -461,7 +410,16 @@ 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 = pow_impl!(this, powi(f, i)); + let res = fixed_powi_float_value(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, 2, // log2(4) + ) + }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; } @@ -470,7 +428,16 @@ 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 = pow_impl!(this, powi(f, i)); + let res = fixed_powi_float_value(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, 2, // log2(4) + ) + }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; } @@ -607,34 +574,121 @@ fn apply_random_float_error_to_imm<'tcx>( interp_ok(ImmTy::from_scalar_int(res, val.layout)) } -/// For the operations: +// TODO(lorrens): This can be moved to `helpers` when we implement the other intrinsics. +/// For the intrinsics: /// - sinf32, sinf64 /// - cosf32, cosf64 /// - expf32, expf64, exp2f32, exp2f64 /// - logf32, logf64, log2f32, log2f64, log10f32, log10f64 +/// - powf32, powf64 /// -/// Returns Some(`output`) if the operation results in a defined fixed `output` when given `input` -/// as an input, else None. +/// Returns Some(`output`) if the operation results in a defined fixed `output` specified in the C standard when given `args` +/// as arguments, else None. fn fixed_float_value( intrinsic_name: &str, - input: IeeeFloat, -) -> Option> -where - IeeeFloat: std::cmp::PartialEq, -{ + args: &[IeeeFloat], +) -> Option> { + // TODO: not sure about this pattern matching stuff. It's definitly cleaner than if-else chains + // Error code 0158 explains this: https://doc.rust-lang.org/stable/error_codes/E0158.html + // The only reason I did this is to use the same function for powf as for sin/cos/exp/log + // TODO: I can't fit powi logic in this because of the exponent being a i32 -> seperate fn "fixed_powi_float_value" for now let one = IeeeFloat::::one(); - match intrinsic_name { + match (intrinsic_name, args) { // sin(+- 0) = +- 0. - "sinf32" | "sinf64" if input.is_zero() => Some(input), - "cosf32" | "cosf64" if input.is_zero() => Some(one), - "expf32" | "expf64" | "exp2f32" | "exp2f64" if input.is_zero() => Some(one), + ("sinf32" | "sinf64", [input]) if input.is_zero() => Some(*input), + + // cos(+- 0) = 1 + ("cosf32" | "cosf64", [input]) if input.is_zero() => Some(one), + + // e^0 = 1 + ("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) + if input.is_zero() => Some(one), + + // log(1) = 0 #[rustfmt::skip] - "logf32" + ("logf32" | "logf64" | "log10f32" | "log10f64" | "log2f32" - | "log2f64" if input == one => Some(IeeeFloat::::ZERO), + | "log2f64", [input]) if *input == one => Some(IeeeFloat::::ZERO), + + // 1^y = 1 for any y, even a NaN. + ("powf32" | "powf64", [base, _]) if *base == one => Some(one), + + // (-1)^(±INF) = 1 + ("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => Some(one), + + // x^(±0) = 1 for any x, even a NaN + ("powf32" | "powf64", [_, exp]) if exp.is_zero() => Some(one), + + // C standard doesn't specify or invalid combination _ => None, } } + +/// Returns Some(`output`) if powi results in a fixed value specified in the C standard when doing `base^exp` else None. +fn fixed_powi_float_value(base: IeeeFloat, exp: i32) -> Option> { + match (base.category(), exp) { + // ±0^x = ±0 with x an odd integer. + (Category::Zero, x) if x % 2 != 0 => Some(base), // preserve sign of zero. + + // ±0^x = +0 with x an even integer. + (Category::Zero, x) if x % 2 == 0 => Some(IeeeFloat::::ZERO), + + // x^y = 1, if y is not a Signaling NaN + (_, 0) if !base.is_signaling() => Some(IeeeFloat::::one()), + + _ => 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(intrinsic_name: &str, val: IeeeFloat) -> IeeeFloat { + match intrinsic_name { + // sin and cos: [-1, 1] + "sinf32" | "cosf32" | "sinf64" | "cosf64" => + val.clamp(IeeeFloat::::one().neg(), IeeeFloat::::one()), + // exp: [0, +INF] + "expf32" | "exp2f32" | "expf64" | "exp2f64" => val.maximum(IeeeFloat::::ZERO), + _ => val, + } +} + +// TODO: clean up when I'm sure this is not needed, because powf is now included in fixed_float_value +// fn powf_impl<'tcx, S: Semantics, Op>( +// ecx: &mut MiriInterpCx<'tcx>, +// base: IeeeFloat, +// exp: IeeeFloat, +// op: Op, +// ) -> IeeeFloat +// where +// IeeeFloat: ToHost, +// Op: Fn(IeeeFloat, IeeeFloat) -> IeeeFloat, +// { +// let one = IeeeFloat::::one(); +// let fixed_res = match (base.category(), exp.category()) { +// // 1^y = 1 for any y, even a NaN. +// (Category::Normal, _) if base == one => Some(one), + +// // (-1)^(±INF) = 1 +// (Category::Normal, Category::Infinity) if base == -one => Some(one), + +// // x^(±0) = 1 for any x, even a NaN +// (_, Category::Zero) => Some(one), + +// // TODO: pow has a lot of "edge" cases which mostly result in ±0 or ±INF +// // do we have to catch them all? +// _ => None, +// }; + +// fixed_res.unwrap_or_else(|| { +// let res = op(base, exp); +// // Apply a relative error of 4ULP to introduce some non-determinism +// // simulating imprecise implementations and optimizations. +// apply_random_float_error_ulp( +// ecx, res, 2, // log2(4) +// ) +// }) +// } From 362829b99d473226d3403e93cd282b8d113238da Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Wed, 12 Mar 2025 19:45:47 +0100 Subject: [PATCH 12/22] clean up --- library/std/tests/floats/f32.rs | 5 +---- library/std/tests/floats/lib.rs | 2 +- src/tools/miri/src/intrinsics/mod.rs | 6 ++---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/library/std/tests/floats/f32.rs b/library/std/tests/floats/f32.rs index 73d32d9687dec..94ca12fea50c3 100644 --- a/library/std/tests/floats/f32.rs +++ b/library/std/tests/floats/f32.rs @@ -567,10 +567,7 @@ fn test_log2() { let nan: f32 = f32::NAN; let inf: f32 = f32::INFINITY; let neg_inf: f32 = f32::NEG_INFINITY; - assert_approx_eq!( - 10.0f32.log2(), - 3.321928 /* Miri float-non-det: Make tests pass for now */ - ); + assert_approx_eq!(10.0f32.log2(), 3.321928); assert_approx_eq!(2.3f32.log2(), 1.201634); assert_approx_eq!(1.0f32.exp().log2(), 1.442695); assert!(nan.log2().is_nan()); diff --git a/library/std/tests/floats/lib.rs b/library/std/tests/floats/lib.rs index 43c43a53a120b..de5a3cdbd0f93 100644 --- a/library/std/tests/floats/lib.rs +++ b/library/std/tests/floats/lib.rs @@ -3,7 +3,7 @@ use std::fmt; use std::ops::{Add, Div, Mul, Rem, Sub}; -/// Verify that floats are within a tolerance of each other, 1.0e-4 by default so we do not break test in Miri. +/// Verify that floats are within a tolerance of each other, 1.0e-6 by default. macro_rules! assert_approx_eq { ($a:expr, $b:expr) => {{ assert_approx_eq!($a, $b, 1.0e-6) }}; ($a:expr, $b:expr, $lim:expr) => {{ diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index 38a66e68d459f..994fb5ae8fcd1 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -269,8 +269,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; - } + #[rustfmt::skip] | "sinf64" | "cosf64" @@ -313,7 +313,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { }); let res = this.adjust_nan(res, &[f]); this.write_scalar(res, dest)?; - } "fmaf32" => { @@ -601,8 +600,7 @@ fn fixed_float_value( ("cosf32" | "cosf64", [input]) if input.is_zero() => Some(one), // e^0 = 1 - ("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) - if input.is_zero() => Some(one), + ("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => Some(one), // log(1) = 0 #[rustfmt::skip] From 8430fed073ae115e3afab95cf395618c96447ecf Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Wed, 19 Mar 2025 20:55:00 +0100 Subject: [PATCH 13/22] apply changes from review + minor doc fixes --- src/tools/miri/src/intrinsics/mod.rs | 56 ++++------------------------ src/tools/miri/tests/pass/float.rs | 18 ++++++++- 2 files changed, 24 insertions(+), 50 deletions(-) diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index 994fb5ae8fcd1..db57c330be0d2 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -550,7 +550,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } } -/// Applies a random 16ULP floating point error to `val` and returns the new value. +/// Applies a random ULP floating point error to `val` and returns the new value. +/// So if you want an X ULP error, `ulp_exponent` should be log2(X). /// Will fail if `val` is not a floating point number. fn apply_random_float_error_to_imm<'tcx>( ecx: &mut MiriInterpCx<'tcx>, @@ -573,7 +574,6 @@ fn apply_random_float_error_to_imm<'tcx>( interp_ok(ImmTy::from_scalar_int(res, val.layout)) } -// TODO(lorrens): This can be moved to `helpers` when we implement the other intrinsics. /// For the intrinsics: /// - sinf32, sinf64 /// - cosf32, cosf64 @@ -581,16 +581,12 @@ fn apply_random_float_error_to_imm<'tcx>( /// - logf32, logf64, log2f32, log2f64, log10f32, log10f64 /// - powf32, powf64 /// -/// Returns Some(`output`) if the operation results in a defined fixed `output` specified in the C standard when given `args` +/// Returns Some(`output`) if the `intrinsic` results in a defined fixed `output` specified in the C standard when given `args` /// as arguments, else None. fn fixed_float_value( intrinsic_name: &str, args: &[IeeeFloat], ) -> Option> { - // TODO: not sure about this pattern matching stuff. It's definitly cleaner than if-else chains - // Error code 0158 explains this: https://doc.rust-lang.org/stable/error_codes/E0158.html - // The only reason I did this is to use the same function for powf as for sin/cos/exp/log - // TODO: I can't fit powi logic in this because of the exponent being a i32 -> seperate fn "fixed_powi_float_value" for now let one = IeeeFloat::::one(); match (intrinsic_name, args) { // sin(+- 0) = +- 0. @@ -620,21 +616,22 @@ fn fixed_float_value( // x^(±0) = 1 for any x, even a NaN ("powf32" | "powf64", [_, exp]) if exp.is_zero() => Some(one), - // C standard doesn't specify or invalid combination + // C standard doesn't specify any fixed outputs for other combinations of `intrinsic_name` and `args`, + // or an invalid combination was given. _ => None, } } -/// Returns Some(`output`) if powi results in a fixed value specified in the C standard when doing `base^exp` else None. +/// Returns Some(`output`) if `powi` results in a fixed value specified in the C standard when doing `base^exp` else None. fn fixed_powi_float_value(base: IeeeFloat, exp: i32) -> Option> { match (base.category(), exp) { // ±0^x = ±0 with x an odd integer. - (Category::Zero, x) if x % 2 != 0 => Some(base), // preserve sign of zero. + (Category::Zero, x) if x % 2 != 0 => Some(base), // preserve sign of zero // ±0^x = +0 with x an even integer. (Category::Zero, x) if x % 2 == 0 => Some(IeeeFloat::::ZERO), - // x^y = 1, if y is not a Signaling NaN + // x^0 = 1, if x is not a Signaling NaN (_, 0) if !base.is_signaling() => Some(IeeeFloat::::one()), _ => None, @@ -653,40 +650,3 @@ fn clamp_float_value(intrinsic_name: &str, val: IeeeFloat) -> I _ => val, } } - -// TODO: clean up when I'm sure this is not needed, because powf is now included in fixed_float_value -// fn powf_impl<'tcx, S: Semantics, Op>( -// ecx: &mut MiriInterpCx<'tcx>, -// base: IeeeFloat, -// exp: IeeeFloat, -// op: Op, -// ) -> IeeeFloat -// where -// IeeeFloat: ToHost, -// Op: Fn(IeeeFloat, IeeeFloat) -> IeeeFloat, -// { -// let one = IeeeFloat::::one(); -// let fixed_res = match (base.category(), exp.category()) { -// // 1^y = 1 for any y, even a NaN. -// (Category::Normal, _) if base == one => Some(one), - -// // (-1)^(±INF) = 1 -// (Category::Normal, Category::Infinity) if base == -one => Some(one), - -// // x^(±0) = 1 for any x, even a NaN -// (_, Category::Zero) => Some(one), - -// // TODO: pow has a lot of "edge" cases which mostly result in ±0 or ±INF -// // do we have to catch them all? -// _ => None, -// }; - -// fixed_res.unwrap_or_else(|| { -// let res = op(base, exp); -// // Apply a relative error of 4ULP to introduce some non-determinism -// // simulating imprecise implementations and optimizations. -// apply_random_float_error_ulp( -// ecx, res, 2, // log2(4) -// ) -// }) -// } diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 5b8e41616df98..9f2aa6e1fe53f 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1009,12 +1009,14 @@ pub fn libm() { // Some inputs to powf and powi result in fixed outputs // and thus must be exactly equal to that value - // TODO: How to test NaN inputs? f*::NAN is not guaranteed - // to be any specific bit pattern (in std). + // C standard says: + // 1^y = 1 for any y, even a NaN. assert_eq!(1f32.powf(10.0), 1f32); assert_eq!(1f64.powf(100.0), 1f64); assert_eq!(1f32.powf(f32::INFINITY), 1f32); assert_eq!(1f64.powf(f64::INFINITY), 1f64); + assert_eq!(1f32.powf(f32::NAN), 1f32); + assert_eq!(1f64.powf(f64::NAN), 1f64); assert_eq!((-1f32).powf(f32::INFINITY), 1f32); assert_eq!((-1f32).powf(f32::NEG_INFINITY), 1f32); @@ -1031,6 +1033,18 @@ pub fn libm() { assert_eq!(0f32.powi(9), 0f32); assert_eq!(0f64.powi(99), 0f64); + // C standard says: + // x^0 = 1, if x is not a Signaling NaN + assert_eq!(10.0f32.powi(0), 1.0f32); + assert_eq!(10.0f64.powi(0), 1.0f64); + assert_eq!(f32::INFINITY.powi(0), 1.0f32); + assert_eq!(f64::INFINITY.powi(0), 1.0f64); + // f*::NAN doesn't specify which what kind of bit pattern + // the NAN will have. + // We **assume** f*::NAN is not signaling. + assert_eq!(f32::NAN.powi(0), 1.0f32); + assert_eq!(f64::NAN.powi(0), 1.0f64); + assert_eq!((-0f32).powi(10), 0f32); assert_eq!((-0f64).powi(100), 0f64); assert_eq!((-0f32).powi(9), -0f32); From 4ea76ec7f905a016d404c8f301a97b7499a7145a Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Mon, 7 Apr 2025 01:03:02 +0200 Subject: [PATCH 14/22] approx delta different when running in miri --- library/std/tests/floats/f32.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/library/std/tests/floats/f32.rs b/library/std/tests/floats/f32.rs index 94ca12fea50c3..3b397eebea763 100644 --- a/library/std/tests/floats/f32.rs +++ b/library/std/tests/floats/f32.rs @@ -22,6 +22,11 @@ const NAN_MASK1: u32 = 0x002a_aaaa; /// Second pattern over the mantissa const NAN_MASK2: u32 = 0x0055_5555; +/// Miri adds some extra errors to float functions; make sure the tests still pass. +/// These values are purely used as a canary to test against and are thus not a stable guarantee Rust provides. +/// They serve as a way to get an idea of the real precision of floating point operations on different platforms. +const APPROX_DETLA: f32 = if cfg!(miri) { 1e-4 } else { 1e-6 }; + #[allow(unused_macros)] macro_rules! assert_f32_biteq { ($left : expr, $right : expr) => { @@ -446,7 +451,7 @@ fn test_powi() { assert_approx_eq!( (-3.1f32).powi(2), 9.61, - 1e-4 /* Miri float-non-det: Make tests pass for now */ + APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ ); assert_approx_eq!(5.9f32.powi(-2), 0.028727); assert_eq!(8.3f32.powi(0), 1.0); @@ -464,13 +469,13 @@ fn test_powf() { assert_approx_eq!( 3.4f32.powf(4.5), 246.408218, - 1e-4 /* Miri float-non-det: Make tests pass for now */ + APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ ); assert_approx_eq!(2.7f32.powf(-3.2), 0.041652); assert_approx_eq!( (-3.1f32).powf(2.0), 9.61, - 1e-4 /* Miri float-non-det: Make tests pass for now */ + APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ ); assert_approx_eq!(5.9f32.powf(-2.0), 0.028727); assert_eq!(8.3f32.powf(0.0), 1.0); @@ -497,7 +502,7 @@ fn test_exp() { assert_approx_eq!( 148.413162, 5.0f32.exp(), - 1e-4 /* Miri float-non-det: Make tests pass for now */ + APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ ); let inf: f32 = f32::INFINITY; @@ -513,7 +518,7 @@ fn test_exp2() { assert_approx_eq!( 32.0, 5.0f32.exp2(), - 1e-4 /* Miri float-non-det: Make tests pass for now */ + APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ ); assert_eq!(1.0, 0.0f32.exp2()); @@ -540,7 +545,7 @@ fn test_ln() { assert_approx_eq!( 4.0f32.ln(), 1.386294, - 1e-4 /* Miri float-non-det: Make tests pass for now */ + APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ ); } @@ -666,7 +671,7 @@ fn test_acosh() { assert_approx_eq!( 60.0f32, 60.0f32.cosh().acosh(), - 1e-4 /* Miri float-non-det: Make tests pass for now */ + APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ ); } @@ -749,7 +754,7 @@ fn test_real_consts() { assert_approx_eq!( frac_pi_3, pi / 3f32, - 1e-4 /* Miri float-non-det: Make tests pass for now */ + APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ ); assert_approx_eq!(frac_pi_4, pi / 4f32); assert_approx_eq!(frac_pi_6, pi / 6f32); From 199c92c2d5b9a8c80409b9956c76c7e8abb966b7 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Mon, 7 Apr 2025 17:31:07 +0200 Subject: [PATCH 15/22] 2 more crashes fixed --- library/std/tests/floats/f32.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/library/std/tests/floats/f32.rs b/library/std/tests/floats/f32.rs index 3b397eebea763..36e67efc8b364 100644 --- a/library/std/tests/floats/f32.rs +++ b/library/std/tests/floats/f32.rs @@ -25,7 +25,7 @@ const NAN_MASK2: u32 = 0x0055_5555; /// Miri adds some extra errors to float functions; make sure the tests still pass. /// These values are purely used as a canary to test against and are thus not a stable guarantee Rust provides. /// They serve as a way to get an idea of the real precision of floating point operations on different platforms. -const APPROX_DETLA: f32 = if cfg!(miri) { 1e-4 } else { 1e-6 }; +const APPROX_DELTA: f32 = if cfg!(miri) { 1e-4 } else { 1e-6 }; #[allow(unused_macros)] macro_rules! assert_f32_biteq { @@ -451,7 +451,7 @@ fn test_powi() { assert_approx_eq!( (-3.1f32).powi(2), 9.61, - APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ + APPROX_DELTA /* Miri float-non-det: Make tests pass for now */ ); assert_approx_eq!(5.9f32.powi(-2), 0.028727); assert_eq!(8.3f32.powi(0), 1.0); @@ -469,13 +469,13 @@ fn test_powf() { assert_approx_eq!( 3.4f32.powf(4.5), 246.408218, - APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ + APPROX_DELTA /* Miri float-non-det: Make tests pass for now */ ); assert_approx_eq!(2.7f32.powf(-3.2), 0.041652); assert_approx_eq!( (-3.1f32).powf(2.0), 9.61, - APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ + APPROX_DELTA /* Miri float-non-det: Make tests pass for now */ ); assert_approx_eq!(5.9f32.powf(-2.0), 0.028727); assert_eq!(8.3f32.powf(0.0), 1.0); @@ -498,11 +498,11 @@ fn test_sqrt_domain() { #[test] fn test_exp() { assert_eq!(1.0, 0.0f32.exp()); - assert_approx_eq!(2.718282, 1.0f32.exp()); + assert_approx_eq!(2.718282, 1.0f32.exp(), APPROX_DELTA); assert_approx_eq!( 148.413162, 5.0f32.exp(), - APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ + APPROX_DELTA /* Miri float-non-det: Make tests pass for now */ ); let inf: f32 = f32::INFINITY; @@ -518,7 +518,7 @@ fn test_exp2() { assert_approx_eq!( 32.0, 5.0f32.exp2(), - APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ + APPROX_DELTA /* Miri float-non-det: Make tests pass for now */ ); assert_eq!(1.0, 0.0f32.exp2()); @@ -545,7 +545,7 @@ fn test_ln() { assert_approx_eq!( 4.0f32.ln(), 1.386294, - APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ + APPROX_DELTA /* Miri float-non-det: Make tests pass for now */ ); } @@ -671,7 +671,7 @@ fn test_acosh() { assert_approx_eq!( 60.0f32, 60.0f32.cosh().acosh(), - APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ + APPROX_DELTA /* Miri float-non-det: Make tests pass for now */ ); } @@ -754,7 +754,7 @@ fn test_real_consts() { assert_approx_eq!( frac_pi_3, pi / 3f32, - APPROX_DETLA /* Miri float-non-det: Make tests pass for now */ + APPROX_DELTA /* Miri float-non-det: Make tests pass for now */ ); assert_approx_eq!(frac_pi_4, pi / 4f32); assert_approx_eq!(frac_pi_6, pi / 6f32); @@ -767,7 +767,7 @@ fn test_real_consts() { assert_approx_eq!(log2_e, e.log2()); assert_approx_eq!(log10_e, e.log10()); assert_approx_eq!(ln_2, 2f32.ln()); - assert_approx_eq!(ln_10, 10f32.ln()); + assert_approx_eq!(ln_10, 10f32.ln(), APPROX_DELTA); } #[test] From 802085d042e5b8f883c7c60b784f7237300b7506 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Tue, 8 Apr 2025 20:40:11 +0200 Subject: [PATCH 16/22] inline FloatExt::one and move this traitimpl for IEEE --- src/tools/miri/src/math.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/miri/src/math.rs b/src/tools/miri/src/math.rs index 351b0df3ee37c..859c3c420f683 100644 --- a/src/tools/miri/src/math.rs +++ b/src/tools/miri/src/math.rs @@ -125,9 +125,9 @@ pub(crate) fn sqrt(x: IeeeFloat) -> IeeeFl } } -impl IeeeExt for IeeeFloat {} /// Extend functionality of rustc_apfloat softfloats pub trait IeeeExt: rustc_apfloat::Float { + #[inline] fn one() -> Self { Self::from_u128(1).value } @@ -137,6 +137,7 @@ pub trait IeeeExt: rustc_apfloat::Float { self.maximum(min).minimum(max) } } +impl IeeeExt for IeeeFloat {} #[cfg(test)] mod tests { From 3cd4fc41627b5d04908b1c0e49d106dbfd5848d8 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Fri, 18 Apr 2025 17:31:02 +0200 Subject: [PATCH 17/22] handling of sign nans --- src/tools/miri/src/intrinsics/mod.rs | 2 ++ src/tools/miri/tests/pass/float.rs | 40 ++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index db57c330be0d2..83472f1e91cd2 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -632,6 +632,8 @@ fn fixed_powi_float_value(base: IeeeFloat, exp: i32) -> Option< (Category::Zero, x) if x % 2 == 0 => Some(IeeeFloat::::ZERO), // x^0 = 1, if x is not a Signaling NaN + // FIXME: The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate + // the NaN. We should return either 1 or the NaN non-deterministically here. (_, 0) if !base.is_signaling() => Some(IeeeFloat::::one()), _ => None, diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 9f2aa6e1fe53f..6f7ae2ee81760 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -44,6 +44,31 @@ macro_rules! assert_approx_eq { }; } + +/// From IEEE 754 a Signaling NaN for single precision has the following representation: +/// ``` +/// s | 1111 1111 | 0x..x +/// ```` +/// Were at least one `x` is a 1. +/// +/// This sNaN has the following representation and is used for testing purposes.: +/// ``` +/// 0 | 1111111 | 01..0 +/// ``` +const SINGLE_SNAN: f32 = f32::from_bits(0x7fa00000); + +/// From IEEE 754 a Signaling NaN for double precision has the following representation: +/// ``` +/// s | 1111 1111 111 | 0x..x +/// ```` +/// Were at least one `x` is a 1. +/// +/// This sNaN has the following representation and is used for testing purposes.: +/// ``` +/// 0 | 1111 1111 111 | 01..0 +/// ``` +const DOUBLE_SNAN: f64 = f64::from_bits(0x7ff4000000000000); + fn main() { basic(); casts(); @@ -1018,6 +1043,16 @@ pub fn libm() { assert_eq!(1f32.powf(f32::NAN), 1f32); assert_eq!(1f64.powf(f64::NAN), 1f64); + // For pown (powi in rust) the C standard says: + // x^0 = 1 for all x not a sNaN. + assert_ne!((SINGLE_SNAN).powi(0), 1.0); + assert_ne!((DOUBLE_SNAN).powi(0), 1.0); + assert_ne!((SINGLE_SNAN).powi(0), 1.0); + assert_ne!((DOUBLE_SNAN).powi(0), 1.0); + // f*::NAN is a quiet NAN and should return 1. + assert_eq!(f32::NAN.powi(0), 1f32); + assert_eq!(f64::NAN.powi(0), 1f64); + assert_eq!((-1f32).powf(f32::INFINITY), 1f32); assert_eq!((-1f32).powf(f32::NEG_INFINITY), 1f32); assert_eq!((-1f64).powf(f64::INFINITY), 1f64); @@ -1039,11 +1074,6 @@ pub fn libm() { assert_eq!(10.0f64.powi(0), 1.0f64); assert_eq!(f32::INFINITY.powi(0), 1.0f32); assert_eq!(f64::INFINITY.powi(0), 1.0f64); - // f*::NAN doesn't specify which what kind of bit pattern - // the NAN will have. - // We **assume** f*::NAN is not signaling. - assert_eq!(f32::NAN.powi(0), 1.0f32); - assert_eq!(f64::NAN.powi(0), 1.0f64); assert_eq!((-0f32).powi(10), 0f32); assert_eq!((-0f64).powi(100), 0f64); From 06d35ce339a124da41755b5dfef3e8cfcbd6ad38 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Fri, 18 Apr 2025 18:12:59 +0200 Subject: [PATCH 18/22] consistend handling of fixed outputs --- src/tools/miri/src/intrinsics/mod.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index 83472f1e91cd2..35946e81fc0d6 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -552,6 +552,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { /// Applies a random ULP floating point error to `val` and returns the new value. /// So if you want an X ULP error, `ulp_exponent` should be log2(X). +/// /// Will fail if `val` is not a floating point number. fn apply_random_float_error_to_imm<'tcx>( ecx: &mut MiriInterpCx<'tcx>, @@ -582,31 +583,19 @@ fn apply_random_float_error_to_imm<'tcx>( /// - powf32, powf64 /// /// Returns Some(`output`) if the `intrinsic` results in a defined fixed `output` specified in the C standard when given `args` -/// as arguments, else None. +/// as arguments. Outputs such as INF and zero are not considered. Otherwise this returns None. fn fixed_float_value( intrinsic_name: &str, args: &[IeeeFloat], ) -> Option> { let one = IeeeFloat::::one(); match (intrinsic_name, args) { - // sin(+- 0) = +- 0. - ("sinf32" | "sinf64", [input]) if input.is_zero() => Some(*input), - // cos(+- 0) = 1 ("cosf32" | "cosf64", [input]) if input.is_zero() => Some(one), // e^0 = 1 ("expf32" | "expf64" | "exp2f32" | "exp2f64", [input]) if input.is_zero() => Some(one), - // log(1) = 0 - #[rustfmt::skip] - ("logf32" - | "logf64" - | "log10f32" - | "log10f64" - | "log2f32" - | "log2f64", [input]) if *input == one => Some(IeeeFloat::::ZERO), - // 1^y = 1 for any y, even a NaN. ("powf32" | "powf64", [base, _]) if *base == one => Some(one), @@ -616,8 +605,8 @@ fn fixed_float_value( // x^(±0) = 1 for any x, even a NaN ("powf32" | "powf64", [_, exp]) if exp.is_zero() => Some(one), - // C standard doesn't specify any fixed outputs for other combinations of `intrinsic_name` and `args`, - // or an invalid combination was given. + // 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. _ => None, } } From 66311a1ce8d80b649a237b34f8fcb176aa932698 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Fri, 18 Apr 2025 18:13:18 +0200 Subject: [PATCH 19/22] cleanup messy tests and remove duplicates --- src/tools/miri/tests/pass/float.rs | 100 ++++++++++++++++------------- 1 file changed, 54 insertions(+), 46 deletions(-) diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 6f7ae2ee81760..9641119617bdf 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1036,67 +1036,75 @@ pub fn libm() { // and thus must be exactly equal to that value // C standard says: // 1^y = 1 for any y, even a NaN. - assert_eq!(1f32.powf(10.0), 1f32); - assert_eq!(1f64.powf(100.0), 1f64); - assert_eq!(1f32.powf(f32::INFINITY), 1f32); - assert_eq!(1f64.powf(f64::INFINITY), 1f64); - assert_eq!(1f32.powf(f32::NAN), 1f32); - assert_eq!(1f64.powf(f64::NAN), 1f64); + assert_eq!(1f32.powf(10.0), 1.0); + assert_eq!(1f64.powf(100.0), 1.0); + assert_eq!(1f32.powf(f32::INFINITY), 1.0); + assert_eq!(1f64.powf(f64::INFINITY), 1.0); + assert_eq!(1f32.powf(f32::NAN), 1.0); + assert_eq!(1f64.powf(f64::NAN), 1.0); + + + // For pow (powf in rust) the C standard says: + // x^0 = 1 for all x even a sNaN + // FIXME: But the ecosystem is inconistent with this, in the future we should return either + // 1 or the NaN. Add correct tests when we actually do this. + // Currently we return 1. + assert_eq!(SINGLE_SNAN.powf(0.0), 1.0); + assert_eq!(DOUBLE_SNAN.powf(0.0), 1.0); + // f*::NAN is a quiet NAN and should return 1 as well. + assert_eq!(f32::NAN.powf(0.0), 1.0); + assert_eq!(f64::NAN.powf(0.0), 1.0); + + assert_eq!(42f32.powf(0.0), 1.0); + assert_eq!(42f64.powf(0.0), 1.0); + assert_eq!(f32::INFINITY.powf(0.0), 1.0); + assert_eq!(f64::INFINITY.powf(0.0), 1.0); // For pown (powi in rust) the C standard says: // x^0 = 1 for all x not a sNaN. - assert_ne!((SINGLE_SNAN).powi(0), 1.0); - assert_ne!((DOUBLE_SNAN).powi(0), 1.0); - assert_ne!((SINGLE_SNAN).powi(0), 1.0); - assert_ne!((DOUBLE_SNAN).powi(0), 1.0); - // f*::NAN is a quiet NAN and should return 1. - assert_eq!(f32::NAN.powi(0), 1f32); - assert_eq!(f64::NAN.powi(0), 1f64); - - assert_eq!((-1f32).powf(f32::INFINITY), 1f32); - assert_eq!((-1f32).powf(f32::NEG_INFINITY), 1f32); - assert_eq!((-1f64).powf(f64::INFINITY), 1f64); - assert_eq!((-1f64).powf(f64::NEG_INFINITY), 1f64); - - assert_eq!(42f32.powf(0.0), 1f32); - assert_eq!(42f32.powf(-0.0), 1f32); - assert_eq!(42f64.powf(0.0), 1f64); - assert_eq!(42f64.powf(-0.0), 1f64); - - assert_eq!(0f32.powi(10), 0f32); - assert_eq!(0f64.powi(100), 0f64); - assert_eq!(0f32.powi(9), 0f32); - assert_eq!(0f64.powi(99), 0f64); - - // C standard says: - // x^0 = 1, if x is not a Signaling NaN - assert_eq!(10.0f32.powi(0), 1.0f32); - assert_eq!(10.0f64.powi(0), 1.0f64); - assert_eq!(f32::INFINITY.powi(0), 1.0f32); - assert_eq!(f64::INFINITY.powi(0), 1.0f64); - - assert_eq!((-0f32).powi(10), 0f32); - assert_eq!((-0f64).powi(100), 0f64); - assert_eq!((-0f32).powi(9), -0f32); - assert_eq!((-0f64).powi(99), -0f64); + assert_ne!(SINGLE_SNAN.powi(0), 1.0); + assert_ne!(DOUBLE_SNAN.powi(0), 1.0); + // f*::NAN is a quiet NAN and should return 1 as well. + assert_eq!(f32::NAN.powi(0), 1.0); + assert_eq!(f64::NAN.powi(0), 1.0); + + assert_eq!(10.0f32.powi(0), 1.0); + assert_eq!(10.0f64.powi(0), 1.0); + assert_eq!(f32::INFINITY.powi(0), 1.0); + assert_eq!(f64::INFINITY.powi(0), 1.0); + + assert_eq!((-1f32).powf(f32::INFINITY), 1.0); + assert_eq!((-1f64).powf(f64::INFINITY), 1.0); + assert_eq!((-1f32).powf(f32::NEG_INFINITY), 1.0); + assert_eq!((-1f64).powf(f64::NEG_INFINITY), 1.0); + + assert_eq!(0f32.powi(10), 0.0); + assert_eq!(0f64.powi(100), 0.0); + assert_eq!(0f32.powi(9), 0.0); + assert_eq!(0f64.powi(99), 0.0); + + assert_eq!((-0f32).powi(10), 0.0); + assert_eq!((-0f64).powi(100), 0.0); + assert_eq!((-0f32).powi(9), -0.0); + assert_eq!((-0f64).powi(99), -0.0); assert_approx_eq!(1f32.exp(), f32::consts::E); assert_approx_eq!(1f64.exp(), f64::consts::E); - assert_eq!(0f32.exp(), 1f32); - assert_eq!(0f64.exp(), 1f64); + assert_eq!(0f32.exp(), 1.0); + assert_eq!(0f64.exp(), 1.0); assert_approx_eq!(1f32.exp_m1(), f32::consts::E - 1.0); assert_approx_eq!(1f64.exp_m1(), f64::consts::E - 1.0); assert_approx_eq!(10f32.exp2(), 1024f32); assert_approx_eq!(50f64.exp2(), 1125899906842624f64); - assert_eq!(0f32.exp2(), 1f32); - assert_eq!(0f64.exp2(), 1f64); + assert_eq!(0f32.exp2(), 1.0); + assert_eq!(0f64.exp2(), 1.0); assert_approx_eq!(f32::consts::E.ln(), 1f32); assert_approx_eq!(f64::consts::E.ln(), 1f64); - assert_eq!(1f32.ln(), 0f32); - assert_eq!(1f64.ln(), 0f64); + assert_eq!(1f32.ln(), 0.0); + assert_eq!(1f64.ln(), 0.0); assert_approx_eq!(0f32.ln_1p(), 0f32); assert_approx_eq!(0f64.ln_1p(), 0f64); From 90bbebc0bb243b69ebdc517569a39b3b5f0bd25f Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Sun, 20 Apr 2025 14:28:06 +0200 Subject: [PATCH 20/22] add fixme regarding powi(snan, 0) --- src/tools/miri/tests/pass/float.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 9641119617bdf..5620390749612 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1046,9 +1046,6 @@ pub fn libm() { // For pow (powf in rust) the C standard says: // x^0 = 1 for all x even a sNaN - // FIXME: But the ecosystem is inconistent with this, in the future we should return either - // 1 or the NaN. Add correct tests when we actually do this. - // Currently we return 1. assert_eq!(SINGLE_SNAN.powf(0.0), 1.0); assert_eq!(DOUBLE_SNAN.powf(0.0), 1.0); // f*::NAN is a quiet NAN and should return 1 as well. @@ -1062,8 +1059,11 @@ pub fn libm() { // For pown (powi in rust) the C standard says: // x^0 = 1 for all x not a sNaN. - assert_ne!(SINGLE_SNAN.powi(0), 1.0); - assert_ne!(DOUBLE_SNAN.powi(0), 1.0); + // FIXME: But the ecosystem is inconistent with this, in the future we should return either + // 1 or the NaN. Add correct tests when we actually do this. + // Should we follow glibc and libm or the C Standard itself? + // assert_ne!(SINGLE_SNAN.powi(0), 1.0); + // assert_ne!(DOUBLE_SNAN.powi(0), 1.0); // f*::NAN is a quiet NAN and should return 1 as well. assert_eq!(f32::NAN.powi(0), 1.0); assert_eq!(f64::NAN.powi(0), 1.0); From f7709485b5a813424d281c4b192e3d87754acdcb Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Sun, 20 Apr 2025 14:30:00 +0200 Subject: [PATCH 21/22] fix the fixme comment --- src/tools/miri/tests/pass/float.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 5620390749612..27830f06c25d7 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1060,10 +1060,8 @@ pub fn libm() { // For pown (powi in rust) the C standard says: // x^0 = 1 for all x not a sNaN. // FIXME: But the ecosystem is inconistent with this, in the future we should return either - // 1 or the NaN. Add correct tests when we actually do this. - // Should we follow glibc and libm or the C Standard itself? - // assert_ne!(SINGLE_SNAN.powi(0), 1.0); - // assert_ne!(DOUBLE_SNAN.powi(0), 1.0); + // 1 or the NaN. -> Add correct tests when we actually do this. + // f*::NAN is a quiet NAN and should return 1 as well. assert_eq!(f32::NAN.powi(0), 1.0); assert_eq!(f64::NAN.powi(0), 1.0); From 69133017b4f63964ad4e03e696c05ec2d00d14f2 Mon Sep 17 00:00:00 2001 From: LorrensP-2158466 Date: Tue, 22 Apr 2025 12:58:58 +0200 Subject: [PATCH 22/22] update fixmes to inlcude the issue --- src/tools/miri/src/intrinsics/mod.rs | 8 ++++++-- src/tools/miri/tests/pass/float.rs | 5 ----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/tools/miri/src/intrinsics/mod.rs b/src/tools/miri/src/intrinsics/mod.rs index 35946e81fc0d6..490dfd2294e97 100644 --- a/src/tools/miri/src/intrinsics/mod.rs +++ b/src/tools/miri/src/intrinsics/mod.rs @@ -602,6 +602,9 @@ fn fixed_float_value( // (-1)^(±INF) = 1 ("powf32" | "powf64", [base, exp]) if *base == -one && exp.is_infinite() => Some(one), + // FIXME(miri/#4286): The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate + // the NaN. We should return either 1 or the NaN non-deterministically here. + // But for now, just handle them all the same // x^(±0) = 1 for any x, even a NaN ("powf32" | "powf64", [_, exp]) if exp.is_zero() => Some(one), @@ -621,9 +624,10 @@ fn fixed_powi_float_value(base: IeeeFloat, exp: i32) -> Option< (Category::Zero, x) if x % 2 == 0 => Some(IeeeFloat::::ZERO), // x^0 = 1, if x is not a Signaling NaN - // FIXME: The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate + // FIXME(miri/#4286): The C ecosystem is inconsistent with handling sNaN's, some return 1 others propogate // the NaN. We should return either 1 or the NaN non-deterministically here. - (_, 0) if !base.is_signaling() => Some(IeeeFloat::::one()), + // But for now, just handle them all the same + (_, 0) => Some(IeeeFloat::::one()), _ => None, } diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs index 27830f06c25d7..ea3e294dd920e 100644 --- a/src/tools/miri/tests/pass/float.rs +++ b/src/tools/miri/tests/pass/float.rs @@ -1057,11 +1057,6 @@ pub fn libm() { assert_eq!(f32::INFINITY.powf(0.0), 1.0); assert_eq!(f64::INFINITY.powf(0.0), 1.0); - // For pown (powi in rust) the C standard says: - // x^0 = 1 for all x not a sNaN. - // FIXME: But the ecosystem is inconistent with this, in the future we should return either - // 1 or the NaN. -> Add correct tests when we actually do this. - // f*::NAN is a quiet NAN and should return 1 as well. assert_eq!(f32::NAN.powi(0), 1.0); assert_eq!(f64::NAN.powi(0), 1.0);