| //! 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 {} |