| //! Configuration for skipping or changing the result for individual test cases (inputs) rather |
| //! than ignoring entire tests. |
| |
| use BaseName as Bn; |
| use CheckBasis::{Mpfr, Musl}; |
| use Identifier as Id; |
| use libm::support::CastFrom; |
| |
| use crate::{BaseName, CheckBasis, CheckCtx, Float, Identifier, Int, TestResult}; |
| |
| /// Type implementing [`IgnoreCase`]. |
| pub struct SpecialCase; |
| |
| /// ULP allowed to differ from the results returned by a test basis. |
| #[allow(clippy::single_match)] |
| pub fn default_ulp(ctx: &CheckCtx) -> Option<u32> { |
| // ULP compared to the infinite (MPFR) result. |
| let mut ulp = match ctx.base_name { |
| // Basic arithmetic needs to always be precise. |
| Bn::Add | Bn::Sub | Bn::Mul | Bn::Div => 0, |
| // FIXME(correctness): we need a better powi implementation (though this is no worse |
| // than C). |
| Bn::Powi if ctx.fn_ident == Id::Powif64 => 500_000, |
| Bn::Powi => 1000, |
| |
| // Operations that only return non-float results |
| Bn::Eq | Bn::Ne | Bn::Gt | Bn::Ge | Bn::Lt | Bn::Le | Bn::Unord | Bn::Ilogb => return None, |
| |
| // Integer ops |
| Bn::Ashl |
| | Bn::Ashr |
| | Bn::Lshr |
| | Bn::LeadingZeros |
| | Bn::TrailingZeros |
| | Bn::Iadd |
| | Bn::Iaddo |
| | Bn::Isub |
| | Bn::Isubo |
| | Bn::Imul |
| | Bn::Imulo |
| | Bn::Idiv |
| | Bn::Imod |
| | Bn::Idivmod => return None, |
| |
| // Convrsion operations must be precise. |
| Bn::Extend | Bn::Narrow | Bn::Ftoi | Bn::Itof => 0, |
| |
| // Operations that require exact results. This list should correlate with what we |
| // have documented at <https://doc.rust-lang.org/std/primitive.f32.html>. |
| Bn::Ceil |
| | Bn::Copysign |
| | Bn::Fabs |
| | Bn::Fdim |
| | Bn::Floor |
| | Bn::Fma |
| | Bn::Fmax |
| | Bn::Fmaximum |
| | Bn::FmaximumNum |
| | Bn::Fmin |
| | Bn::Fminimum |
| | Bn::FminimumNum |
| | Bn::Fmod |
| | Bn::Frexp |
| | Bn::Ldexp |
| | Bn::Modf |
| | Bn::Nextafter |
| | Bn::Remainder |
| | Bn::Remquo |
| | Bn::Rint |
| | Bn::Round |
| | Bn::Roundeven |
| | Bn::Scalbn |
| | Bn::Sqrt |
| | Bn::Trunc => 0, |
| |
| // Operations that aren't required to be exact, but our implementations are. |
| Bn::Cbrt => 0, |
| Bn::Hypot if ctx.fn_ident == Id::Hypot => 0, |
| |
| // Bessel functions have large inaccuracies. |
| Bn::J0 | Bn::J1 | Bn::Y0 | Bn::Y1 | Bn::Jn | Bn::Yn => 8_000_000, |
| |
| // For all other operations, specify our implementation's worst case precision. |
| Bn::Acos => 1, |
| Bn::Acosh => 4, |
| Bn::Asin => 1, |
| Bn::Asinh => 2, |
| Bn::Atan => 1, |
| Bn::Atan2 => 2, |
| Bn::Atanh => 2, |
| Bn::Cos => 1, |
| Bn::Cosh => 1, |
| Bn::Erf => 1, |
| Bn::Erfc => 4, |
| Bn::Exp => 1, |
| Bn::Exp10 => 6, |
| Bn::Exp2 => 1, |
| Bn::Expm1 => 1, |
| Bn::Hypot => 1, |
| Bn::Lgamma | Bn::LgammaR => 4, |
| Bn::Log => 1, |
| Bn::Log10 => 1, |
| Bn::Log1p => 1, |
| Bn::Log2 => 1, |
| Bn::Pow => 1, |
| Bn::Sin => 1, |
| Bn::Sincos => 1, |
| Bn::Sinh => 2, |
| Bn::Tan => 1, |
| Bn::Tanh => 2, |
| // tgammaf has higher accuracy than tgamma. |
| Bn::Tgamma if ctx.fn_ident != Id::Tgamma => 1, |
| Bn::Tgamma => 20, |
| }; |
| |
| let mut orig_ulp = ulp; |
| |
| // These have a separate implementation on i586 which is more accurate. |
| if cfg!(x86_no_sse2) { |
| match ctx.fn_ident { |
| Id::Exp => ulp = 1, |
| Id::Exp2 => ulp = 1, |
| Id::Exp10 => ulp = 1, |
| Id::Expf => ulp = 0, |
| Id::Exp2f => ulp = 0, |
| Id::Exp10f => ulp = 0, |
| _ => (), |
| } |
| |
| assert!(ulp <= orig_ulp, "pattern can be deleted {ctx:?}"); |
| orig_ulp = ulp; |
| |
| // Due to rust-lang/rust#114479 (unsound floating point behavior on x86 without SSE), the |
| // following operations have worse precision. |
| match ctx.fn_ident { |
| // FIXME: these need to be correctly rounded but are not, likely due to LLVM bugs |
| // around precision without SSE float ops. It may be worth looking into an assembly |
| // implementation. |
| Id::Fma => ulp = 1, |
| Id::Fmaf => ulp = 1, |
| Id::Fdim => ulp = 1, |
| Id::Round => ulp = 1, |
| |
| Id::Asinh => ulp = 3, |
| Id::Asinhf => ulp = 3, |
| Id::Cbrt => ulp = 1, |
| Id::Hypot => ulp = 1, |
| Id::Log1p => ulp = 2, |
| Id::Log1pf => ulp = 2, |
| Id::Tan => ulp = 2, |
| _ => (), |
| } |
| |
| assert!(ulp >= orig_ulp, "pattern can be deleted {ctx:?}"); |
| orig_ulp = ulp; |
| } |
| |
| // There are some cases where musl's approximation is less accurate than ours, either due to |
| // the implementation itself or because of x87 inaccuracy problems. For these cases, increase |
| // the allowed ULP. |
| if ctx.basis == Musl { |
| match ctx.fn_ident { |
| // Musl probably runs into issues with the x87 ABI here which we don't have. |
| Id::Fma if cfg!(target_arch = "x86") => ulp = 1, |
| Id::Fdim => ulp = 2, |
| |
| Id::Asinhf => ulp = 3, |
| Id::Cbrt => ulp = 2, |
| Id::Cosh => ulp = 2, |
| Id::Coshf => ulp = 2, |
| Id::Exp10 if cfg!(x86_no_sse2) => ulp = 4, |
| Id::Exp10f if cfg!(x86_no_sse2) => ulp = 4, |
| Id::Exp2f => ulp = 1, |
| Id::Expf => ulp = 1, |
| Id::Hypot => ulp = 1, |
| Id::Tanh => ulp = 4, |
| Id::Tanhf => ulp = 4, |
| _ => (), |
| } |
| |
| assert!(ulp >= orig_ulp, "pattern can be deleted {ctx:?}"); |
| } |
| |
| Some(ulp) |
| } |
| |
| /// Result of checking for possible overrides. |
| #[derive(Debug, Default)] |
| pub enum CheckAction { |
| /// The check should pass. Default case. |
| #[default] |
| AssertSuccess, |
| |
| /// Override the ULP for this check. |
| AssertWithUlp(u32), |
| |
| /// Failure is expected, ensure this is the case (xfail). Takes a contxt string to help trace |
| /// back exactly why we expect this to fail. |
| AssertFailure(&'static str), |
| |
| /// The override somehow validated the result, here it is. |
| Custom(TestResult), |
| |
| /// Disregard the output. |
| Skip, |
| } |
| |
| /// Don't run further validation on this test case. |
| const SKIP: CheckAction = CheckAction::Skip; |
| |
| /// Return this to skip checks on a test that currently fails but shouldn't. Takes a description |
| /// of context. |
| const XFAIL: fn(&'static str) -> CheckAction = CheckAction::AssertFailure; |
| |
| /// Indicates that we expect a test to fail but we aren't asserting that it does (e.g. some results |
| /// within a range do actually pass). |
| /// |
| /// Same as `SKIP`, just indicates we have something to eventually fix. |
| const XFAIL_NOCHECK: CheckAction = CheckAction::Skip; |
| |
| /// By default, all tests should pass. |
| const DEFAULT: CheckAction = CheckAction::AssertSuccess; |
| |
| /// Allow overriding the outputs of specific test cases. |
| /// |
| /// There are some cases where we want to xfail specific cases or handle certain inputs |
| /// differently than the rest of calls to `validate`. This provides a hook to do that. |
| /// |
| /// If `None` is returned, checks will proceed as usual. If `Some(result)` is returned, checks |
| /// are skipped and the provided result is returned instead. |
| /// |
| /// This gets implemented once per input type, then the functions provide further filtering |
| /// based on function name and values. |
| /// |
| /// `ulp` can also be set to adjust the ULP for that specific test, even if `None` is still |
| /// returned. |
| pub trait MaybeOverride<Input> { |
| fn check_float<F: Float>( |
| _input: Input, |
| _actual: F, |
| _expected: F, |
| _ctx: &CheckCtx, |
| ) -> CheckAction { |
| DEFAULT |
| } |
| |
| fn check_int<I: Int>(_input: Input, _actual: I, _expected: I, _ctx: &CheckCtx) -> CheckAction { |
| DEFAULT |
| } |
| } |
| |
| #[cfg(f16_enabled)] |
| impl MaybeOverride<(f16,)> for SpecialCase {} |
| |
| impl MaybeOverride<(f32,)> for SpecialCase { |
| fn check_float<F: Float>(input: (f32,), actual: F, expected: F, ctx: &CheckCtx) -> CheckAction { |
| if ctx.base_name == BaseName::J0 && input.0 < -1e34 { |
| // Errors get huge close to -inf |
| return XFAIL_NOCHECK; |
| } |
| |
| // the testing infrastructure doesn't account for allowed ulp in the case of overflow |
| if matches!(ctx.base_name, BaseName::Lgamma | BaseName::LgammaR) |
| && input.0 == 4.0850034e36 |
| && expected.is_infinite() |
| && actual == F::MAX |
| { |
| return XFAIL_NOCHECK; |
| } |
| |
| // FIXME(correctness): lgammaf has high relative inaccuracy near its zeroes |
| if matches!(ctx.base_name, BaseName::Lgamma | BaseName::LgammaR) |
| && input.0 > -13.0625 |
| && input.0 < -2.0 |
| && (expected.abs() < F::ONE || (input.0 - input.0.round()).abs() < 0.02) |
| { |
| return XFAIL_NOCHECK; |
| } |
| |
| unop_common(input, actual, expected, ctx) |
| } |
| |
| fn check_int<I: Int>(input: (f32,), actual: I, expected: I, ctx: &CheckCtx) -> CheckAction { |
| // On MPFR for lgammaf_r, we set -1 as the integer result for negative infinity but MPFR |
| // sets +1 |
| if ctx.basis == CheckBasis::Mpfr |
| && ctx.base_name == BaseName::LgammaR |
| && input.0 == f32::NEG_INFINITY |
| && actual.abs() == expected.abs() |
| { |
| return XFAIL("lgammar integer result"); |
| } |
| |
| DEFAULT |
| } |
| } |
| |
| impl MaybeOverride<(f64,)> for SpecialCase { |
| fn check_float<F: Float>(input: (f64,), actual: F, expected: F, ctx: &CheckCtx) -> CheckAction { |
| if ctx.base_name == BaseName::J0 && input.0 < -1e300 { |
| // Errors get huge close to -inf |
| return XFAIL_NOCHECK; |
| } |
| |
| if ctx.base_name == BaseName::Acosh |
| && input.0 < 1.0 |
| && actual.is_nan() |
| && ctx.basis == CheckBasis::Musl |
| { |
| // Musl sometimes evaluates acosh(negative) to a numeric value |
| return XFAIL_NOCHECK; |
| } |
| |
| // FIXME(correctness): lgamma has high relative inaccuracy near its zeroes |
| if matches!(ctx.base_name, BaseName::Lgamma | BaseName::LgammaR) |
| && input.0 > -32.0 |
| && input.0 < -2.0 |
| && (expected.abs() < F::ONE || (input.0 - input.0.round()).abs() < 0.02) |
| { |
| return XFAIL_NOCHECK; |
| } |
| |
| // maybe_check_nan_bits(actual, expected, ctx) |
| unop_common(input, actual, expected, ctx) |
| } |
| |
| fn check_int<I: Int>(input: (f64,), actual: I, expected: I, ctx: &CheckCtx) -> CheckAction { |
| // On MPFR for lgamma_r, we set -1 as the integer result for negative infinity but MPFR |
| // sets +1 |
| if ctx.basis == CheckBasis::Mpfr |
| && ctx.base_name == BaseName::LgammaR |
| && input.0 == f64::NEG_INFINITY |
| && actual.abs() == expected.abs() |
| { |
| return XFAIL("lgammar integer result"); |
| } |
| |
| DEFAULT |
| } |
| } |
| |
| #[cfg(f128_enabled)] |
| impl MaybeOverride<(f128,)> for SpecialCase {} |
| |
| // F1 and F2 are always the same type, this is just to please generics |
| fn unop_common<F1: Float, F2: Float>( |
| input: (F1,), |
| actual: F2, |
| expected: F2, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| // fabs and copysign must leave NaNs untouched. |
| if ctx.base_name == BaseName::Fabs && input.0.is_nan() { |
| // LLVM currently uses x87 instructions which quieten signalling NaNs to handle the i686 |
| // `extern "C"` `f32`/`f64` return ABI. |
| // LLVM issue <https://github.com/llvm/llvm-project/issues/66803> |
| // Rust issue <https://github.com/rust-lang/rust/issues/115567> |
| if cfg!(target_arch = "x86") && ctx.basis == CheckBasis::Musl && actual.is_nan() { |
| return XFAIL_NOCHECK; |
| } |
| |
| // MPFR only has one NaN bitpattern; allow the default `.is_nan()` checks to validate. |
| if ctx.basis == CheckBasis::Mpfr { |
| return DEFAULT; |
| } |
| |
| // abs and copysign require signaling NaNs to be propagated, so verify bit equality. |
| if actual.biteq(expected) { |
| return CheckAction::Custom(Ok(())); |
| } else { |
| return CheckAction::Custom(Err(anyhow::anyhow!("NaNs have different bitpatterns"))); |
| } |
| } |
| |
| DEFAULT |
| } |
| |
| #[cfg(f16_enabled)] |
| impl MaybeOverride<(f16, f16)> for SpecialCase { |
| fn check_float<F: Float>( |
| input: (f16, f16), |
| actual: F, |
| expected: F, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| binop_common(input, actual, expected, ctx) |
| } |
| } |
| |
| impl MaybeOverride<(f32, f32)> for SpecialCase { |
| fn check_float<F: Float>( |
| input: (f32, f32), |
| actual: F, |
| expected: F, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| binop_common(input, actual, expected, ctx) |
| } |
| } |
| |
| impl MaybeOverride<(f64, f64)> for SpecialCase { |
| fn check_float<F: Float>( |
| input: (f64, f64), |
| actual: F, |
| expected: F, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| binop_common(input, actual, expected, ctx) |
| } |
| } |
| |
| #[cfg(f128_enabled)] |
| impl MaybeOverride<(f128, f128)> for SpecialCase { |
| fn check_float<F: Float>( |
| input: (f128, f128), |
| actual: F, |
| expected: F, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| binop_common(input, actual, expected, ctx) |
| } |
| } |
| |
| // F1 and F2 are always the same type, this is just to please generics |
| fn binop_common<F1: Float, F2: Float>( |
| input: (F1, F1), |
| actual: F2, |
| expected: F2, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| // MPFR only has one NaN bitpattern; skip tests in cases where the first argument would take |
| // the sign of a NaN second argument. The default NaN checks cover other cases. |
| if ctx.base_name == BaseName::Copysign && ctx.basis == CheckBasis::Mpfr && input.1.is_nan() { |
| return SKIP; |
| } |
| |
| /* FIXME(#439): our fmin and fmax do not compare signed zeros */ |
| |
| if ctx.base_name == BaseName::Fmin |
| && input.0.biteq(F1::NEG_ZERO) |
| && input.1.biteq(F1::ZERO) |
| && expected.biteq(F2::NEG_ZERO) |
| && actual.biteq(F2::ZERO) |
| { |
| return XFAIL("fmin signed zeroes"); |
| } |
| |
| if ctx.base_name == BaseName::Fmax |
| && input.0.biteq(F1::NEG_ZERO) |
| && input.1.biteq(F1::ZERO) |
| && expected.biteq(F2::ZERO) |
| && actual.biteq(F2::NEG_ZERO) |
| { |
| return XFAIL("fmax signed zeroes"); |
| } |
| |
| // Musl propagates NaNs if one is provided as the input, but we return the other input. |
| if (ctx.base_name == BaseName::Fmax || ctx.base_name == BaseName::Fmin) |
| && ctx.basis == Musl |
| && (input.0.is_nan() ^ input.1.is_nan()) |
| && expected.is_nan() |
| { |
| return XFAIL("fmax/fmin musl NaN"); |
| } |
| |
| DEFAULT |
| } |
| |
| impl MaybeOverride<(i32, f32)> for SpecialCase { |
| fn check_float<F: Float>( |
| input: (i32, f32), |
| actual: F, |
| expected: F, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| // `ynf(213, 109.15641) = -inf` with our library, should be finite. |
| if ctx.basis == Mpfr |
| && ctx.base_name == BaseName::Yn |
| && input.0 > 200 |
| && !expected.is_infinite() |
| && actual.is_infinite() |
| { |
| return XFAIL("ynf infinity mismatch"); |
| } |
| |
| int_float_common(input, actual, expected, ctx) |
| } |
| } |
| |
| impl MaybeOverride<(i32, f64)> for SpecialCase { |
| fn check_float<F: Float>( |
| input: (i32, f64), |
| actual: F, |
| expected: F, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| int_float_common(input, actual, expected, ctx) |
| } |
| } |
| |
| fn int_float_common<F1: Float, F2: Float>( |
| input: (i32, F1), |
| actual: F2, |
| expected: F2, |
| ctx: &CheckCtx, |
| ) -> CheckAction { |
| if ctx.basis == Mpfr |
| && (ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn) |
| && input.1 == F1::NEG_INFINITY |
| && actual == F2::ZERO |
| && expected == F2::ZERO |
| { |
| return XFAIL("we disagree with MPFR on the sign of zero"); |
| } |
| |
| // Values near infinity sometimes get cut off for us. `ynf(681, 509.90924) = -inf` but should |
| // be -3.2161271e38. |
| if ctx.basis == Musl |
| && ctx.fn_ident == Identifier::Ynf |
| && !expected.is_infinite() |
| && actual.is_infinite() |
| && (expected.abs().to_bits().abs_diff(actual.abs().to_bits()) |
| < F2::Int::cast_from(10_000_000u32)) |
| { |
| return XFAIL_NOCHECK; |
| } |
| |
| // Our bessel functions blow up with large N values |
| if ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn { |
| if cfg!(x86_no_sse2) { |
| // Precision is especially bad on i586, not worth checking. |
| return XFAIL_NOCHECK; |
| } |
| |
| if input.0 > 140 { |
| return XFAIL_NOCHECK; |
| } else if input.0 > 80 { |
| return CheckAction::AssertWithUlp(10_000_000); |
| } |
| } |
| |
| DEFAULT |
| } |
| |
| #[cfg(f16_enabled)] |
| impl MaybeOverride<(f16, i32)> for SpecialCase {} |
| impl MaybeOverride<(f32, i32)> for SpecialCase {} |
| impl MaybeOverride<(f64, i32)> for SpecialCase {} |
| #[cfg(f128_enabled)] |
| impl MaybeOverride<(f128, i32)> for SpecialCase {} |
| |
| #[cfg(f16_enabled)] |
| impl MaybeOverride<(f16, f16, f16)> for SpecialCase {} |
| impl MaybeOverride<(f32, f32, f32)> for SpecialCase {} |
| impl MaybeOverride<(f64, f64, f64)> for SpecialCase {} |
| #[cfg(f128_enabled)] |
| impl MaybeOverride<(f128, f128, f128)> for SpecialCase {} |
| |
| impl MaybeOverride<(i32,)> for SpecialCase {} |
| impl MaybeOverride<(i64,)> for SpecialCase {} |
| impl MaybeOverride<(i128,)> for SpecialCase {} |
| impl MaybeOverride<(u32,)> for SpecialCase {} |
| impl MaybeOverride<(u64,)> for SpecialCase {} |
| impl MaybeOverride<(u128,)> for SpecialCase {} |
| |
| impl MaybeOverride<(i32, i32)> for SpecialCase {} |
| impl MaybeOverride<(i64, i64)> for SpecialCase {} |
| impl MaybeOverride<(i128, i128)> for SpecialCase {} |
| impl MaybeOverride<(u32, u32)> for SpecialCase {} |
| impl MaybeOverride<(u64, u64)> for SpecialCase {} |
| impl MaybeOverride<(u128, u128)> for SpecialCase {} |
| |
| impl MaybeOverride<(i32, u32)> for SpecialCase {} |
| impl MaybeOverride<(i64, u32)> for SpecialCase {} |
| impl MaybeOverride<(i128, u32)> for SpecialCase {} |
| impl MaybeOverride<(u64, u32)> for SpecialCase {} |
| impl MaybeOverride<(u128, u32)> for SpecialCase {} |