diff --git a/crates/libm-test/examples/plot_domains.rs b/crates/libm-test/examples/plot_domains.rs index 62651124..fb7b854d 100644 --- a/crates/libm-test/examples/plot_domains.rs +++ b/crates/libm-test/examples/plot_domains.rs @@ -12,9 +12,9 @@ use std::path::Path; use std::process::Command; use std::{env, fs}; -use libm_test::domain::HasDomain; -use libm_test::gen::{domain_logspace, edge_cases}; -use libm_test::{CheckBasis, CheckCtx, MathOp, op}; +use libm_test::gen::spaced::SpacedInput; +use libm_test::gen::{edge_cases, spaced}; +use libm_test::{CheckBasis, CheckCtx, GeneratorKind, MathOp, op}; const JL_PLOT: &str = "examples/plot_file.jl"; @@ -52,23 +52,13 @@ fn main() { /// Run multiple generators for a single operator. fn plot_one_operator(out_dir: &Path, config: &mut String) where - Op: MathOp + HasDomain, + Op: MathOp, + Op::RustArgs: SpacedInput, { - let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr); - plot_one_generator( - out_dir, - &ctx, - "logspace", - config, - domain_logspace::get_test_cases::(&ctx), - ); - plot_one_generator( - out_dir, - &ctx, - "edge_cases", - config, - edge_cases::get_test_cases::(&ctx), - ); + let mut ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr, GeneratorKind::QuickSpaced); + plot_one_generator(out_dir, &ctx, "logspace", config, spaced::get_test_cases::(&ctx).0); + ctx.gen_kind = GeneratorKind::EdgeCases; + plot_one_generator(out_dir, &ctx, "edge_cases", config, edge_cases::get_test_cases::(&ctx)); } /// Plot the output of a single generator. diff --git a/crates/libm-test/src/domain.rs b/crates/libm-test/src/domain.rs index 68b91bf0..fde775c9 100644 --- a/crates/libm-test/src/domain.rs +++ b/crates/libm-test/src/domain.rs @@ -1,11 +1,13 @@ //! Traits and operations related to bounds of a function. use std::fmt; -use std::ops::{self, Bound}; +use std::ops::Bound; -use crate::{Float, FloatExt}; +use libm::support::Int; -/// Representation of a function's domain. +use crate::{BaseName, Float, FloatExt, Identifier}; + +/// Representation of a single dimension of a function's domain. #[derive(Clone, Debug)] pub struct Domain { /// Start of the region for which a function is defined (ignoring poles). @@ -39,56 +41,129 @@ impl Domain { } } +/// A value that may be any float type or any integer type. +#[derive(Clone, Debug)] +pub enum EitherPrim { + Float(F), + Int(I), +} + +impl EitherPrim { + pub fn unwrap_float(self) -> F { + match self { + EitherPrim::Float(f) => f, + EitherPrim::Int(_) => panic!("expected float; got {self:?}"), + } + } + + pub fn unwrap_int(self) -> I { + match self { + EitherPrim::Float(_) => panic!("expected int; got {self:?}"), + EitherPrim::Int(i) => i, + } + } +} + +/// Convenience 1-dimensional float domains. impl Domain { /// x ∈ ℝ - pub const UNBOUNDED: Self = + const UNBOUNDED: Self = Self { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None }; /// x ∈ ℝ >= 0 - pub const POSITIVE: Self = + const POSITIVE: Self = Self { start: Bound::Included(F::ZERO), end: Bound::Unbounded, check_points: None }; /// x ∈ ℝ > 0 - pub const STRICTLY_POSITIVE: Self = + const STRICTLY_POSITIVE: Self = Self { start: Bound::Excluded(F::ZERO), end: Bound::Unbounded, check_points: None }; + const fn into_prim_f(self) -> EitherPrim> { + EitherPrim::Float(self) + } +} + +/// Convenience 1-dimensional integer domains. +impl Domain { + /// x ∈ ℝ + const UNBOUNDED_INT: Self = + Self { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None }; + + const fn into_prim_i(self) -> EitherPrim, Self> { + EitherPrim::Int(self) + } +} + +/// Multidimensional domains, represented as an array of 1-D domains. +impl EitherPrim, Domain> { + /// x ∈ ℝ + const UNBOUNDED1: [Self; 1] = + [Domain { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None } + .into_prim_f()]; + + /// {x1, x2} ∈ ℝ + const UNBOUNDED2: [Self; 2] = + [Domain::UNBOUNDED.into_prim_f(), Domain::UNBOUNDED.into_prim_f()]; + + /// {x1, x2, x3} ∈ ℝ + const UNBOUNDED3: [Self; 3] = [ + Domain::UNBOUNDED.into_prim_f(), + Domain::UNBOUNDED.into_prim_f(), + Domain::UNBOUNDED.into_prim_f(), + ]; + + /// {x1, x2} ∈ ℝ, one float and one int + const UNBOUNDED_F_I: [Self; 2] = + [Domain::UNBOUNDED.into_prim_f(), Domain::UNBOUNDED_INT.into_prim_i()]; + + /// x ∈ ℝ >= 0 + const POSITIVE: [Self; 1] = [Domain::POSITIVE.into_prim_f()]; + + /// x ∈ ℝ > 0 + const STRICTLY_POSITIVE: [Self; 1] = [Domain::STRICTLY_POSITIVE.into_prim_f()]; + /// Used for versions of `asin` and `acos`. - pub const INVERSE_TRIG_PERIODIC: Self = Self { + const INVERSE_TRIG_PERIODIC: [Self; 1] = [Domain { start: Bound::Included(F::NEG_ONE), end: Bound::Included(F::ONE), check_points: None, - }; + } + .into_prim_f()]; /// Domain for `acosh` - pub const ACOSH: Self = - Self { start: Bound::Included(F::ONE), end: Bound::Unbounded, check_points: None }; + const ACOSH: [Self; 1] = + [Domain { start: Bound::Included(F::ONE), end: Bound::Unbounded, check_points: None } + .into_prim_f()]; /// Domain for `atanh` - pub const ATANH: Self = Self { + const ATANH: [Self; 1] = [Domain { start: Bound::Excluded(F::NEG_ONE), end: Bound::Excluded(F::ONE), check_points: None, - }; + } + .into_prim_f()]; /// Domain for `sin`, `cos`, and `tan` - pub const TRIG: Self = Self { + const TRIG: [Self; 1] = [Domain { // TODO check_points: Some(|| Box::new([-F::PI, -F::FRAC_PI_2, F::FRAC_PI_2, F::PI].into_iter())), - ..Self::UNBOUNDED - }; + ..Domain::UNBOUNDED + } + .into_prim_f()]; /// Domain for `log` in various bases - pub const LOG: Self = Self::STRICTLY_POSITIVE; + const LOG: [Self; 1] = Self::STRICTLY_POSITIVE; /// Domain for `log1p` i.e. `log(1 + x)` - pub const LOG1P: Self = - Self { start: Bound::Excluded(F::NEG_ONE), end: Bound::Unbounded, check_points: None }; + const LOG1P: [Self; 1] = + [Domain { start: Bound::Excluded(F::NEG_ONE), end: Bound::Unbounded, check_points: None } + .into_prim_f()]; /// Domain for `sqrt` - pub const SQRT: Self = Self::POSITIVE; + const SQRT: [Self; 1] = Self::POSITIVE; /// Domain for `gamma` - pub const GAMMA: Self = Self { + const GAMMA: [Self; 1] = [Domain { check_points: Some(|| { // Negative integers are asymptotes Box::new((0..u8::MAX).map(|scale| { @@ -100,122 +175,83 @@ impl Domain { })) }), // Whether or not gamma is defined for negative numbers is implementation dependent - ..Self::UNBOUNDED - }; + ..Domain::UNBOUNDED + } + .into_prim_f()]; /// Domain for `loggamma` - pub const LGAMMA: Self = Self::STRICTLY_POSITIVE; -} + const LGAMMA: [Self; 1] = Self::STRICTLY_POSITIVE; -/// Implement on `op::*` types to indicate how they are bounded. -pub trait HasDomain -where - T: Copy + fmt::Debug + ops::Add + ops::Sub + PartialOrd + 'static, -{ - const DOMAIN: Domain; + /// Domain for `jn` and `yn`. + // FIXME: the domain should provide some sort of "reasonable range" so we don't actually test + // the entire system unbounded. + const BESSEL_N: [Self; 2] = Self::UNBOUNDED_F_I; } -/// Implement [`HasDomain`] for both the `f32` and `f64` variants of a function. -macro_rules! impl_has_domain { - ($($fn_name:ident => $domain:expr;)*) => { - paste::paste! { - $( - // Implement for f64 functions - impl HasDomain for $crate::op::$fn_name::Routine { - const DOMAIN: Domain = Domain::::$domain; - } - - // Implement for f32 functions - impl HasDomain for $crate::op::[< $fn_name f >]::Routine { - const DOMAIN: Domain = Domain::::$domain; - } - )* - } +/// Get the domain for a given function. +pub fn get_domain( + id: Identifier, + argnum: usize, +) -> EitherPrim, Domain> { + let x = match id.base_name() { + BaseName::Acos => &EitherPrim::INVERSE_TRIG_PERIODIC[..], + BaseName::Acosh => &EitherPrim::ACOSH[..], + BaseName::Asin => &EitherPrim::INVERSE_TRIG_PERIODIC[..], + BaseName::Asinh => &EitherPrim::UNBOUNDED1[..], + BaseName::Atan => &EitherPrim::UNBOUNDED1[..], + BaseName::Atan2 => &EitherPrim::UNBOUNDED2[..], + BaseName::Cbrt => &EitherPrim::UNBOUNDED1[..], + BaseName::Atanh => &EitherPrim::ATANH[..], + BaseName::Ceil => &EitherPrim::UNBOUNDED1[..], + BaseName::Cosh => &EitherPrim::UNBOUNDED1[..], + BaseName::Copysign => &EitherPrim::UNBOUNDED2[..], + BaseName::Cos => &EitherPrim::TRIG[..], + BaseName::Exp => &EitherPrim::UNBOUNDED1[..], + BaseName::Erf => &EitherPrim::UNBOUNDED1[..], + BaseName::Erfc => &EitherPrim::UNBOUNDED1[..], + BaseName::Expm1 => &EitherPrim::UNBOUNDED1[..], + BaseName::Exp10 => &EitherPrim::UNBOUNDED1[..], + BaseName::Exp2 => &EitherPrim::UNBOUNDED1[..], + BaseName::Frexp => &EitherPrim::UNBOUNDED1[..], + BaseName::Fabs => &EitherPrim::UNBOUNDED1[..], + BaseName::Fdim => &EitherPrim::UNBOUNDED2[..], + BaseName::Floor => &EitherPrim::UNBOUNDED1[..], + BaseName::Fma => &EitherPrim::UNBOUNDED3[..], + BaseName::Fmax => &EitherPrim::UNBOUNDED2[..], + BaseName::Fmin => &EitherPrim::UNBOUNDED2[..], + BaseName::Fmod => &EitherPrim::UNBOUNDED2[..], + BaseName::Hypot => &EitherPrim::UNBOUNDED2[..], + BaseName::Ilogb => &EitherPrim::UNBOUNDED1[..], + BaseName::J0 => &EitherPrim::UNBOUNDED1[..], + BaseName::J1 => &EitherPrim::UNBOUNDED1[..], + BaseName::Jn => &EitherPrim::BESSEL_N[..], + BaseName::Ldexp => &EitherPrim::UNBOUNDED_F_I[..], + BaseName::Lgamma => &EitherPrim::LGAMMA[..], + BaseName::LgammaR => &EitherPrim::LGAMMA[..], + BaseName::Log => &EitherPrim::LOG[..], + BaseName::Log10 => &EitherPrim::LOG[..], + BaseName::Log1p => &EitherPrim::LOG1P[..], + BaseName::Log2 => &EitherPrim::LOG[..], + BaseName::Modf => &EitherPrim::UNBOUNDED1[..], + BaseName::Nextafter => &EitherPrim::UNBOUNDED2[..], + BaseName::Pow => &EitherPrim::UNBOUNDED2[..], + BaseName::Remainder => &EitherPrim::UNBOUNDED2[..], + BaseName::Remquo => &EitherPrim::UNBOUNDED2[..], + BaseName::Rint => &EitherPrim::UNBOUNDED1[..], + BaseName::Round => &EitherPrim::UNBOUNDED1[..], + BaseName::Scalbn => &EitherPrim::UNBOUNDED_F_I[..], + BaseName::Sin => &EitherPrim::TRIG[..], + BaseName::Sincos => &EitherPrim::TRIG[..], + BaseName::Sinh => &EitherPrim::UNBOUNDED1[..], + BaseName::Sqrt => &EitherPrim::SQRT[..], + BaseName::Tan => &EitherPrim::TRIG[..], + BaseName::Tanh => &EitherPrim::UNBOUNDED1[..], + BaseName::Tgamma => &EitherPrim::GAMMA[..], + BaseName::Trunc => &EitherPrim::UNBOUNDED1[..], + BaseName::Y0 => &EitherPrim::UNBOUNDED1[..], + BaseName::Y1 => &EitherPrim::UNBOUNDED1[..], + BaseName::Yn => &EitherPrim::BESSEL_N[..], }; -} - -// Tie functions together with their domains. -impl_has_domain! { - acos => INVERSE_TRIG_PERIODIC; - acosh => ACOSH; - asin => INVERSE_TRIG_PERIODIC; - asinh => UNBOUNDED; - atan => UNBOUNDED; - atanh => ATANH; - cbrt => UNBOUNDED; - ceil => UNBOUNDED; - cos => TRIG; - cosh => UNBOUNDED; - erf => UNBOUNDED; - erfc => UNBOUNDED; - exp => UNBOUNDED; - exp10 => UNBOUNDED; - exp2 => UNBOUNDED; - expm1 => UNBOUNDED; - fabs => UNBOUNDED; - floor => UNBOUNDED; - frexp => UNBOUNDED; - ilogb => UNBOUNDED; - j0 => UNBOUNDED; - j1 => UNBOUNDED; - lgamma => LGAMMA; - log => LOG; - log10 => LOG; - log1p => LOG1P; - log2 => LOG; - modf => UNBOUNDED; - rint => UNBOUNDED; - round => UNBOUNDED; - sin => TRIG; - sincos => TRIG; - sinh => UNBOUNDED; - sqrt => SQRT; - tan => TRIG; - tanh => UNBOUNDED; - tgamma => GAMMA; - trunc => UNBOUNDED; - y0 => UNBOUNDED; - y1 => UNBOUNDED; -} - -/* Manual implementations, these functions don't follow `foo`->`foof` naming */ - -impl HasDomain for crate::op::lgammaf_r::Routine { - const DOMAIN: Domain = Domain::::LGAMMA; -} - -impl HasDomain for crate::op::lgamma_r::Routine { - const DOMAIN: Domain = Domain::::LGAMMA; -} - -/* Not all `f16` and `f128` functions exist yet so we can't easily use the macros. */ - -#[cfg(f16_enabled)] -impl HasDomain for crate::op::fabsf16::Routine { - const DOMAIN: Domain = Domain::::UNBOUNDED; -} - -#[cfg(f128_enabled)] -impl HasDomain for crate::op::fabsf128::Routine { - const DOMAIN: Domain = Domain::::UNBOUNDED; -} - -#[cfg(f16_enabled)] -impl HasDomain for crate::op::fdimf16::Routine { - const DOMAIN: Domain = Domain::::UNBOUNDED; -} - -#[cfg(f128_enabled)] -impl HasDomain for crate::op::fdimf128::Routine { - const DOMAIN: Domain = Domain::::UNBOUNDED; -} - -#[cfg(f16_enabled)] -impl HasDomain for crate::op::truncf16::Routine { - const DOMAIN: Domain = Domain::::UNBOUNDED; -} -#[cfg(f128_enabled)] -impl HasDomain for crate::op::truncf128::Routine { - const DOMAIN: Domain = Domain::::UNBOUNDED; + x[argnum].clone() } diff --git a/crates/libm-test/src/gen.rs b/crates/libm-test/src/gen.rs index e2bfcdf3..e0a7f576 100644 --- a/crates/libm-test/src/gen.rs +++ b/crates/libm-test/src/gen.rs @@ -1,9 +1,8 @@ //! Different generators that can create random or systematic bit patterns. -pub mod domain_logspace; pub mod edge_cases; -pub mod extensive; pub mod random; +pub mod spaced; /// A wrapper to turn any iterator into an `ExactSizeIterator`. Asserts the final result to ensure /// the provided size was correct. diff --git a/crates/libm-test/src/gen/domain_logspace.rs b/crates/libm-test/src/gen/domain_logspace.rs deleted file mode 100644 index c6963ad4..00000000 --- a/crates/libm-test/src/gen/domain_logspace.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! A generator that produces logarithmically spaced values within domain bounds. - -use std::ops::RangeInclusive; - -use libm::support::{IntTy, MinInt}; - -use crate::domain::HasDomain; -use crate::op::OpITy; -use crate::run_cfg::{GeneratorKind, iteration_count}; -use crate::{CheckCtx, MathOp, logspace}; - -/// Create a range of logarithmically spaced inputs within a function's domain. -/// -/// This allows us to get reasonably thorough coverage without wasting time on values that are -/// NaN or out of range. Random tests will still cover values that are excluded here. -pub fn get_test_cases(ctx: &CheckCtx) -> impl Iterator -where - Op: MathOp + HasDomain, - IntTy: TryFrom, - RangeInclusive>: Iterator, -{ - let domain = Op::DOMAIN; - let ntests = iteration_count(ctx, GeneratorKind::Domain, 0); - - // We generate logspaced inputs within a specific range, excluding values that are out of - // range in order to make iterations useful (random tests still cover the full range). - let start = domain.range_start(); - let end = domain.range_end(); - let steps = OpITy::::try_from(ntests).unwrap_or(OpITy::::MAX); - logspace(start, end, steps).0.map(|v| (v,)) -} diff --git a/crates/libm-test/src/gen/edge_cases.rs b/crates/libm-test/src/gen/edge_cases.rs index 1f27c146..944b5c83 100644 --- a/crates/libm-test/src/gen/edge_cases.rs +++ b/crates/libm-test/src/gen/edge_cases.rs @@ -1,20 +1,23 @@ //! A generator that checks a handful of cases near infinities, zeros, asymptotes, and NaNs. -use libm::support::Float; +use libm::support::{Float, Int}; -use crate::domain::HasDomain; +use crate::domain::get_domain; +use crate::gen::KnownSize; use crate::run_cfg::{check_near_count, check_point_count}; -use crate::{CheckCtx, FloatExt, MathOp}; +use crate::{CheckCtx, FloatExt, MathOp, test_log}; /// Create a list of values around interesting points (infinities, zeroes, NaNs). -pub fn get_test_cases(ctx: &CheckCtx) -> impl Iterator +fn float_edge_cases( + ctx: &CheckCtx, + argnum: usize, +) -> (impl Iterator + Clone, u64) where - Op: MathOp + HasDomain, - F: Float, + Op: MathOp, { let mut ret = Vec::new(); let values = &mut ret; - let domain = Op::DOMAIN; + let domain = get_domain::<_, i8>(ctx.fn_ident, argnum).unwrap_float(); let domain_start = domain.range_start(); let domain_end = domain.range_end(); @@ -22,17 +25,17 @@ where let near_points = check_near_count(ctx); // Check near some notable constants - count_up(F::ONE, near_points, values); - count_up(F::ZERO, near_points, values); - count_up(F::NEG_ONE, near_points, values); - count_down(F::ONE, near_points, values); - count_down(F::ZERO, near_points, values); - count_down(F::NEG_ONE, near_points, values); - values.push(F::NEG_ZERO); + count_up(Op::FTy::ONE, near_points, values); + count_up(Op::FTy::ZERO, near_points, values); + count_up(Op::FTy::NEG_ONE, near_points, values); + count_down(Op::FTy::ONE, near_points, values); + count_down(Op::FTy::ZERO, near_points, values); + count_down(Op::FTy::NEG_ONE, near_points, values); + values.push(Op::FTy::NEG_ZERO); // Check values near the extremes - count_up(F::NEG_INFINITY, near_points, values); - count_down(F::INFINITY, near_points, values); + count_up(Op::FTy::NEG_INFINITY, near_points, values); + count_down(Op::FTy::INFINITY, near_points, values); count_down(domain_end, near_points, values); count_up(domain_start, near_points, values); count_down(domain_start, near_points, values); @@ -40,8 +43,8 @@ where count_down(domain_end, near_points, values); // Check some special values that aren't included in the above ranges - values.push(F::NAN); - values.extend(F::consts().iter()); + values.push(Op::FTy::NAN); + values.extend(Op::FTy::consts().iter()); // Check around asymptotes if let Some(f) = domain.check_points { @@ -56,7 +59,18 @@ where values.sort_by_key(|x| x.to_bits()); values.dedup_by_key(|x| x.to_bits()); - ret.into_iter().map(|v| (v,)) + let count = ret.len().try_into().unwrap(); + + test_log(&format!( + "{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {count} edge cases", + gen_kind = ctx.gen_kind, + basis = ctx.basis, + fn_ident = ctx.fn_ident, + arg = argnum + 1, + args = ctx.input_count(), + )); + + (ret.into_iter(), count) } /// Add `AROUND` values starting at and including `x` and counting up. Uses the smallest possible @@ -84,3 +98,142 @@ fn count_down(mut x: F, points: u64, values: &mut Vec) { count += 1; } } + +pub fn int_edge_cases( + ctx: &CheckCtx, + _argnum: usize, +) -> (impl Iterator + Clone, u64) { + let mut values = Vec::new(); + let near_points = check_near_count(ctx); + + let mut x = I::MIN; + for _ in 0..near_points { + values.push(x); + x += I::ONE; + } + + let mut x = I::ZERO; + for _ in 0..near_points { + values.push(x); + x -= I::ONE; + } + + let mut x = I::ZERO; + for _ in 0..near_points { + values.push(x); + x += I::ONE; + } + + let mut x = I::MAX; + for _ in 0..near_points { + values.push(x); + x -= I::ONE; + } + + values.sort(); + values.dedup(); + let len = values.len().try_into().unwrap(); + (values.into_iter(), len) +} + +pub trait EdgeCaseInput { + fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator + Send; +} + +macro_rules! impl_edge_case_input { + ($fty:ty) => { + impl EdgeCaseInput for ($fty,) + where + Op: MathOp, + { + fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { + let (iter0, steps0) = float_edge_cases::(ctx, 0); + let iter0 = iter0.map(|v| (v,)); + KnownSize::new(iter0, steps0) + } + } + + impl EdgeCaseInput for ($fty, $fty) + where + Op: MathOp, + { + fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { + let (iter0, steps0) = float_edge_cases::(ctx, 0); + let (iter1, steps1) = float_edge_cases::(ctx, 1); + let iter = + iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second))); + let count = steps0.checked_mul(steps1).unwrap(); + KnownSize::new(iter, count) + } + } + + impl EdgeCaseInput for ($fty, $fty, $fty) + where + Op: MathOp, + { + fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { + let (iter0, steps0) = float_edge_cases::(ctx, 0); + let (iter1, steps1) = float_edge_cases::(ctx, 1); + let (iter2, steps2) = float_edge_cases::(ctx, 2); + + let iter = iter0 + .flat_map(move |first| iter1.clone().map(move |second| (first, second))) + .flat_map(move |(first, second)| { + iter2.clone().map(move |third| (first, second, third)) + }); + let count = steps0.checked_mul(steps1).unwrap().checked_mul(steps2).unwrap(); + + KnownSize::new(iter, count) + } + } + + impl EdgeCaseInput for (i32, $fty) + where + Op: MathOp, + { + fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { + let (iter0, steps0) = int_edge_cases(ctx, 0); + let (iter1, steps1) = float_edge_cases::(ctx, 1); + + let iter = + iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second))); + let count = steps0.checked_mul(steps1).unwrap(); + + KnownSize::new(iter, count) + } + } + + impl EdgeCaseInput for ($fty, i32) + where + Op: MathOp, + { + fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { + let (iter0, steps0) = float_edge_cases::(ctx, 0); + let (iter1, steps1) = int_edge_cases(ctx, 1); + + let iter = + iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second))); + let count = steps0.checked_mul(steps1).unwrap(); + + KnownSize::new(iter, count) + } + } + }; +} + +#[cfg(f16_enabled)] +impl_edge_case_input!(f16); +impl_edge_case_input!(f32); +impl_edge_case_input!(f64); +#[cfg(f128_enabled)] +impl_edge_case_input!(f128); + +pub fn get_test_cases( + ctx: &CheckCtx, +) -> impl ExactSizeIterator + use<'_, Op> +where + Op: MathOp, + Op::RustArgs: EdgeCaseInput, +{ + Op::RustArgs::get_cases(ctx) +} diff --git a/crates/libm-test/src/gen/random.rs b/crates/libm-test/src/gen/random.rs index 6b08e560..56c39981 100644 --- a/crates/libm-test/src/gen/random.rs +++ b/crates/libm-test/src/gen/random.rs @@ -9,8 +9,8 @@ use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; use super::KnownSize; +use crate::CheckCtx; use crate::run_cfg::{int_range, iteration_count}; -use crate::{CheckCtx, GeneratorKind}; pub(crate) const SEED_ENV: &str = "LIBM_SEED"; @@ -52,7 +52,7 @@ macro_rules! impl_random_input { ($fty:ty) => { impl RandomInput for ($fty,) { fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { - let count = iteration_count(ctx, GeneratorKind::Random, 0); + let count = iteration_count(ctx, 0); let iter = random_floats(count).map(|f: $fty| (f,)); KnownSize::new(iter, count) } @@ -60,8 +60,8 @@ macro_rules! impl_random_input { impl RandomInput for ($fty, $fty) { fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { - let count0 = iteration_count(ctx, GeneratorKind::Random, 0); - let count1 = iteration_count(ctx, GeneratorKind::Random, 1); + let count0 = iteration_count(ctx, 0); + let count1 = iteration_count(ctx, 1); let iter = random_floats(count0) .flat_map(move |f1: $fty| random_floats(count1).map(move |f2: $fty| (f1, f2))); KnownSize::new(iter, count0 * count1) @@ -70,9 +70,9 @@ macro_rules! impl_random_input { impl RandomInput for ($fty, $fty, $fty) { fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { - let count0 = iteration_count(ctx, GeneratorKind::Random, 0); - let count1 = iteration_count(ctx, GeneratorKind::Random, 1); - let count2 = iteration_count(ctx, GeneratorKind::Random, 2); + let count0 = iteration_count(ctx, 0); + let count1 = iteration_count(ctx, 1); + let count2 = iteration_count(ctx, 2); let iter = random_floats(count0).flat_map(move |f1: $fty| { random_floats(count1).flat_map(move |f2: $fty| { random_floats(count2).map(move |f3: $fty| (f1, f2, f3)) @@ -84,9 +84,9 @@ macro_rules! impl_random_input { impl RandomInput for (i32, $fty) { fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { - let count0 = iteration_count(ctx, GeneratorKind::Random, 0); - let count1 = iteration_count(ctx, GeneratorKind::Random, 1); - let range0 = int_range(ctx, GeneratorKind::Random, 0); + let count0 = iteration_count(ctx, 0); + let count1 = iteration_count(ctx, 1); + let range0 = int_range(ctx, 0); let iter = random_ints(count0, range0) .flat_map(move |f1: i32| random_floats(count1).map(move |f2: $fty| (f1, f2))); KnownSize::new(iter, count0 * count1) @@ -95,9 +95,9 @@ macro_rules! impl_random_input { impl RandomInput for ($fty, i32) { fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator { - let count0 = iteration_count(ctx, GeneratorKind::Random, 0); - let count1 = iteration_count(ctx, GeneratorKind::Random, 1); - let range1 = int_range(ctx, GeneratorKind::Random, 1); + let count0 = iteration_count(ctx, 0); + let count1 = iteration_count(ctx, 1); + let range1 = int_range(ctx, 1); let iter = random_floats(count0).flat_map(move |f1: $fty| { random_ints(count1, range1.clone()).map(move |f2: i32| (f1, f2)) }); diff --git a/crates/libm-test/src/gen/extensive.rs b/crates/libm-test/src/gen/spaced.rs similarity index 76% rename from crates/libm-test/src/gen/extensive.rs rename to crates/libm-test/src/gen/spaced.rs index fb709e54..bea3f4c7 100644 --- a/crates/libm-test/src/gen/extensive.rs +++ b/crates/libm-test/src/gen/spaced.rs @@ -3,23 +3,23 @@ use std::ops::RangeInclusive; use libm::support::{Float, MinInt}; -use crate::domain::HasDomain; +use crate::domain::get_domain; use crate::op::OpITy; use crate::run_cfg::{int_range, iteration_count}; -use crate::{CheckCtx, GeneratorKind, MathOp, linear_ints, logspace}; +use crate::{CheckCtx, MathOp, linear_ints, logspace}; -/// Generate a sequence of inputs that either cover the domain in completeness (for smaller float +/// Generate a sequence of inputs that eiher cover the domain in completeness (for smaller float /// types and single argument functions) or provide evenly spaced inputs across the domain with /// approximately `u32::MAX` total iterations. -pub trait ExtensiveInput { +pub trait SpacedInput { fn get_cases(ctx: &CheckCtx) -> (impl Iterator + Send, u64); } /// Construct an iterator from `logspace` and also calculate the total number of steps expected /// for that iterator. fn logspace_steps( - start: Op::FTy, - end: Op::FTy, + ctx: &CheckCtx, + argnum: usize, max_steps: u64, ) -> (impl Iterator + Clone, u64) where @@ -28,6 +28,11 @@ where u64: TryFrom, Error: fmt::Debug>, RangeInclusive>: Iterator, { + // i8 is a dummy type here, it can be any integer. + let domain = get_domain::(ctx.fn_ident, argnum).unwrap_float(); + let start = domain.range_start(); + let end = domain.range_end(); + let max_steps = OpITy::::try_from(max_steps).unwrap_or(OpITy::::MAX); let (iter, steps) = logspace(start, end, max_steps); @@ -76,15 +81,14 @@ where (F::Int::MIN..=F::Int::MAX).map(|bits| F::from_bits(bits)) } -macro_rules! impl_extensive_input { +macro_rules! impl_spaced_input { ($fty:ty) => { - impl ExtensiveInput for ($fty,) + impl SpacedInput for ($fty,) where Op: MathOp, - Op: HasDomain, { fn get_cases(ctx: &CheckCtx) -> (impl Iterator, u64) { - let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0); + let max_steps0 = iteration_count(ctx, 0); // `f16` and `f32` can have exhaustive tests. match value_count::() { Some(steps0) if steps0 <= max_steps0 => { @@ -93,9 +97,7 @@ macro_rules! impl_extensive_input { (EitherIter::A(iter0), steps0) } _ => { - let start = Op::DOMAIN.range_start(); - let end = Op::DOMAIN.range_end(); - let (iter0, steps0) = logspace_steps::(start, end, max_steps0); + let (iter0, steps0) = logspace_steps::(ctx, 0, max_steps0); let iter0 = iter0.map(|v| (v,)); (EitherIter::B(iter0), steps0) } @@ -103,13 +105,13 @@ macro_rules! impl_extensive_input { } } - impl ExtensiveInput for ($fty, $fty) + impl SpacedInput for ($fty, $fty) where Op: MathOp, { fn get_cases(ctx: &CheckCtx) -> (impl Iterator, u64) { - let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0); - let max_steps1 = iteration_count(ctx, GeneratorKind::Extensive, 1); + let max_steps0 = iteration_count(ctx, 0); + let max_steps1 = iteration_count(ctx, 1); // `f16` can have exhaustive tests. match value_count::() { Some(count) if count <= max_steps0 && count <= max_steps1 => { @@ -118,10 +120,8 @@ macro_rules! impl_extensive_input { (EitherIter::A(iter), count.checked_mul(count).unwrap()) } _ => { - let start = <$fty>::NEG_INFINITY; - let end = <$fty>::INFINITY; - let (iter0, steps0) = logspace_steps::(start, end, max_steps0); - let (iter1, steps1) = logspace_steps::(start, end, max_steps1); + let (iter0, steps0) = logspace_steps::(ctx, 0, max_steps0); + let (iter1, steps1) = logspace_steps::(ctx, 1, max_steps1); let iter = iter0.flat_map(move |first| { iter1.clone().map(move |second| (first, second)) }); @@ -132,14 +132,14 @@ macro_rules! impl_extensive_input { } } - impl ExtensiveInput for ($fty, $fty, $fty) + impl SpacedInput for ($fty, $fty, $fty) where Op: MathOp, { fn get_cases(ctx: &CheckCtx) -> (impl Iterator, u64) { - let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0); - let max_steps1 = iteration_count(ctx, GeneratorKind::Extensive, 1); - let max_steps2 = iteration_count(ctx, GeneratorKind::Extensive, 2); + let max_steps0 = iteration_count(ctx, 0); + let max_steps1 = iteration_count(ctx, 1); + let max_steps2 = iteration_count(ctx, 2); // `f16` can be exhaustive tested if `LIBM_EXTENSIVE_TESTS` is incresed. match value_count::() { Some(count) @@ -153,12 +153,9 @@ macro_rules! impl_extensive_input { (EitherIter::A(iter), count.checked_pow(3).unwrap()) } _ => { - let start = <$fty>::NEG_INFINITY; - let end = <$fty>::INFINITY; - - let (iter0, steps0) = logspace_steps::(start, end, max_steps0); - let (iter1, steps1) = logspace_steps::(start, end, max_steps1); - let (iter2, steps2) = logspace_steps::(start, end, max_steps2); + let (iter0, steps0) = logspace_steps::(ctx, 0, max_steps0); + let (iter1, steps1) = logspace_steps::(ctx, 1, max_steps1); + let (iter2, steps2) = logspace_steps::(ctx, 2, max_steps2); let iter = iter0 .flat_map(move |first| iter1.clone().map(move |second| (first, second))) @@ -174,14 +171,14 @@ macro_rules! impl_extensive_input { } } - impl ExtensiveInput for (i32, $fty) + impl SpacedInput for (i32, $fty) where Op: MathOp, { fn get_cases(ctx: &CheckCtx) -> (impl Iterator, u64) { - let range0 = int_range(ctx, GeneratorKind::Extensive, 0); - let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0); - let max_steps1 = iteration_count(ctx, GeneratorKind::Extensive, 1); + let range0 = int_range(ctx, 0); + let max_steps0 = iteration_count(ctx, 0); + let max_steps1 = iteration_count(ctx, 1); match value_count::() { Some(count1) if count1 <= max_steps1 => { let (iter0, steps0) = linear_ints(range0, max_steps0); @@ -190,11 +187,8 @@ macro_rules! impl_extensive_input { (EitherIter::A(iter), steps0.checked_mul(count1).unwrap()) } _ => { - let start = <$fty>::NEG_INFINITY; - let end = <$fty>::INFINITY; - let (iter0, steps0) = linear_ints(range0, max_steps0); - let (iter1, steps1) = logspace_steps::(start, end, max_steps1); + let (iter1, steps1) = logspace_steps::(ctx, 1, max_steps1); let iter = iter0.flat_map(move |first| { iter1.clone().map(move |second| (first, second)) @@ -207,14 +201,14 @@ macro_rules! impl_extensive_input { } } - impl ExtensiveInput for ($fty, i32) + impl SpacedInput for ($fty, i32) where Op: MathOp, { fn get_cases(ctx: &CheckCtx) -> (impl Iterator, u64) { - let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0); - let range1 = int_range(ctx, GeneratorKind::Extensive, 1); - let max_steps1 = iteration_count(ctx, GeneratorKind::Extensive, 1); + let max_steps0 = iteration_count(ctx, 0); + let range1 = int_range(ctx, 1); + let max_steps1 = iteration_count(ctx, 1); match value_count::() { Some(count0) if count0 <= max_steps0 => { let (iter1, steps1) = linear_ints(range1, max_steps1); @@ -224,10 +218,7 @@ macro_rules! impl_extensive_input { (EitherIter::A(iter), count0.checked_mul(steps1).unwrap()) } _ => { - let start = <$fty>::NEG_INFINITY; - let end = <$fty>::INFINITY; - - let (iter0, steps0) = logspace_steps::(start, end, max_steps0); + let (iter0, steps0) = logspace_steps::(ctx, 0, max_steps0); let (iter1, steps1) = linear_ints(range1, max_steps1); let iter = iter0.flat_map(move |first| { @@ -244,11 +235,11 @@ macro_rules! impl_extensive_input { } #[cfg(f16_enabled)] -impl_extensive_input!(f16); -impl_extensive_input!(f32); -impl_extensive_input!(f64); +impl_spaced_input!(f16); +impl_spaced_input!(f32); +impl_spaced_input!(f64); #[cfg(f128_enabled)] -impl_extensive_input!(f128); +impl_spaced_input!(f128); /// Create a test case iterator for extensive inputs. Also returns the total test case count. pub fn get_test_cases( @@ -256,7 +247,7 @@ pub fn get_test_cases( ) -> (impl Iterator + Send + use<'_, Op>, u64) where Op: MathOp, - Op::RustArgs: ExtensiveInput, + Op::RustArgs: SpacedInput, { Op::RustArgs::get_cases(ctx) } diff --git a/crates/libm-test/src/run_cfg.rs b/crates/libm-test/src/run_cfg.rs index 4a52091f..a350443f 100644 --- a/crates/libm-test/src/run_cfg.rs +++ b/crates/libm-test/src/run_cfg.rs @@ -39,11 +39,12 @@ pub struct CheckCtx { pub base_name_str: &'static str, /// Source of truth for tests. pub basis: CheckBasis, + pub gen_kind: GeneratorKind, } impl CheckCtx { /// Create a new check context, using the default ULP for the function. - pub fn new(fn_ident: Identifier, basis: CheckBasis) -> Self { + pub fn new(fn_ident: Identifier, basis: CheckBasis, gen_kind: GeneratorKind) -> Self { let mut ret = Self { ulp: 0, fn_ident, @@ -51,10 +52,15 @@ impl CheckCtx { base_name: fn_ident.base_name(), base_name_str: fn_ident.base_name().as_str(), basis, + gen_kind, }; ret.ulp = crate::default_ulp(&ret); ret } + + pub fn input_count(&self) -> usize { + self.fn_ident.math_op().rust_sig.args.len() + } } /// Possible items to test against @@ -66,11 +72,13 @@ pub enum CheckBasis { Mpfr, } -/// The different kinds of generators that provide test input. +/// The different kinds of generators that provide test input, which account for input pattern +/// and quantity. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum GeneratorKind { - Domain, + EdgeCases, Extensive, + QuickSpaced, Random, } @@ -155,7 +163,7 @@ impl TestEnv { } /// The number of iterations to run for a given test. -pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> u64 { +pub fn iteration_count(ctx: &CheckCtx, argnum: usize) -> u64 { let t_env = TestEnv::from_env(ctx); // Ideally run 5M tests @@ -185,10 +193,13 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) - // Run fewer random tests than domain tests. let random_iter_count = domain_iter_count / 100; - let mut total_iterations = match gen_kind { - GeneratorKind::Domain => domain_iter_count, + let mut total_iterations = match ctx.gen_kind { + GeneratorKind::QuickSpaced => domain_iter_count, GeneratorKind::Random => random_iter_count, GeneratorKind::Extensive => *EXTENSIVE_MAX_ITERATIONS, + GeneratorKind::EdgeCases => { + unimplemented!("edge case tests shoudn't need `iteration_count`") + } }; // FMA has a huge domain but is reasonably fast to run, so increase iterations. @@ -213,16 +224,18 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) - }; let total = ntests.pow(t_env.input_count.try_into().unwrap()); - let seed_msg = match gen_kind { - GeneratorKind::Domain | GeneratorKind::Extensive => String::new(), + let seed_msg = match ctx.gen_kind { + GeneratorKind::QuickSpaced | GeneratorKind::Extensive => String::new(), GeneratorKind::Random => { format!(" using `{SEED_ENV}={}`", str::from_utf8(SEED.as_slice()).unwrap()) } + GeneratorKind::EdgeCases => unreachable!(), }; test_log(&format!( "{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {ntests} iterations \ ({total} total){seed_msg}", + gen_kind = ctx.gen_kind, basis = ctx.basis, fn_ident = ctx.fn_ident, arg = argnum + 1, @@ -233,7 +246,7 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) - } /// Some tests require that an integer be kept within reasonable limits; generate that here. -pub fn int_range(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> RangeInclusive { +pub fn int_range(ctx: &CheckCtx, argnum: usize) -> RangeInclusive { let t_env = TestEnv::from_env(ctx); if !matches!(ctx.base_name, BaseName::Jn | BaseName::Yn) { @@ -252,21 +265,32 @@ pub fn int_range(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> Rang let extensive_range = (-0xfff)..=0xfffff; - match gen_kind { + match ctx.gen_kind { GeneratorKind::Extensive => extensive_range, - GeneratorKind::Domain | GeneratorKind::Random => non_extensive_range, + GeneratorKind::QuickSpaced | GeneratorKind::Random => non_extensive_range, + GeneratorKind::EdgeCases => extensive_range, } } /// For domain tests, limit how many asymptotes or specified check points we test. pub fn check_point_count(ctx: &CheckCtx) -> usize { + assert_eq!( + ctx.gen_kind, + GeneratorKind::EdgeCases, + "check_point_count is intended for edge case tests" + ); let t_env = TestEnv::from_env(ctx); if t_env.slow_platform || !cfg!(optimizations_enabled) { 4 } else { 10 } } /// When validating points of interest (e.g. asymptotes, inflection points, extremes), also check /// this many surrounding values. -pub fn check_near_count(_ctx: &CheckCtx) -> u64 { +pub fn check_near_count(ctx: &CheckCtx) -> u64 { + assert_eq!( + ctx.gen_kind, + GeneratorKind::EdgeCases, + "check_near_count is intended for edge case tests" + ); if cfg!(optimizations_enabled) { 100 } else { 10 } } diff --git a/crates/libm-test/src/spaced.rs b/crates/libm-test/src/spaced.rs new file mode 100644 index 00000000..d685b152 --- /dev/null +++ b/crates/libm-test/src/spaced.rs @@ -0,0 +1 @@ +//! Interfaces for evenly spaced tests. diff --git a/crates/libm-test/tests/compare_built_musl.rs b/crates/libm-test/tests/compare_built_musl.rs index 836c425a..fc3dc5e1 100644 --- a/crates/libm-test/tests/compare_built_musl.rs +++ b/crates/libm-test/tests/compare_built_musl.rs @@ -9,34 +9,16 @@ // There are some targets we can't build musl for #![cfg(feature = "build-musl")] -use libm_test::domain::HasDomain; -use libm_test::gen::random::RandomInput; -use libm_test::gen::{domain_logspace, edge_cases, random}; -use libm_test::{CheckBasis, CheckCtx, CheckOutput, MathOp, TupleCall}; +use libm_test::gen::{edge_cases, random, spaced}; +use libm_test::{CheckBasis, CheckCtx, CheckOutput, GeneratorKind, MathOp, TupleCall}; -macro_rules! musl_rand_tests { - ( - fn_name: $fn_name:ident, - attrs: [$($attr:meta),*], - ) => { - paste::paste! { - #[test] - $(#[$attr])* - fn [< musl_random_ $fn_name >]() { - test_one_random::(musl_math_sys::$fn_name); - } - } - }; -} - -fn test_one_random(musl_fn: Op::CFn) -where - Op: MathOp, - Op::RustArgs: RandomInput, -{ - let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Musl); - let cases = random::get_test_cases::(&ctx); +const BASIS: CheckBasis = CheckBasis::Musl; +fn musl_runner( + ctx: &CheckCtx, + cases: impl Iterator, + musl_fn: Op::CFn, +) { for input in cases { let musl_res = input.call(musl_fn); let crate_res = input.call(Op::ROUTINE); @@ -45,114 +27,60 @@ where } } -libm_macros::for_each_function! { - callback: musl_rand_tests, - // Musl does not support `f16` and `f128` on all platforms. - skip: [ - copysignf128, - copysignf16, - fabsf128, - fabsf16, - fdimf128, - fdimf16, - truncf128, - truncf16, - ], - attributes: [ - #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 - [exp10, exp10f, exp2, exp2f, rint] - ], -} - /// Test against musl with generators from a domain. -macro_rules! musl_domain_tests { +macro_rules! musl_tests { ( fn_name: $fn_name:ident, attrs: [$($attr:meta),*], ) => { paste::paste! { + #[test] + $(#[$attr])* + fn [< musl_random_ $fn_name >]() { + type Op = libm_test::op::$fn_name::Routine; + let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::Random); + let cases = random::get_test_cases::<::RustArgs>(&ctx); + musl_runner::(&ctx, cases, musl_math_sys::$fn_name); + } + #[test] $(#[$attr])* fn [< musl_edge_case_ $fn_name >]() { type Op = libm_test::op::$fn_name::Routine; - domain_test_runner::( - edge_cases::get_test_cases::, - musl_math_sys::$fn_name, - ); + let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::EdgeCases); + let cases = edge_cases::get_test_cases::(&ctx); + musl_runner::(&ctx, cases, musl_math_sys::$fn_name); } #[test] $(#[$attr])* - fn [< musl_logspace_ $fn_name >]() { + fn [< musl_quickspace_ $fn_name >]() { type Op = libm_test::op::$fn_name::Routine; - domain_test_runner::( - domain_logspace::get_test_cases::, - musl_math_sys::$fn_name, - ); + let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::QuickSpaced); + let cases = spaced::get_test_cases::(&ctx).0; + musl_runner::(&ctx, cases, musl_math_sys::$fn_name); } } }; } -/// Test a single routine against domaine-aware inputs. -fn domain_test_runner(gen: impl FnOnce(&CheckCtx) -> I, musl_fn: Op::CFn) -where - Op: MathOp, - Op: HasDomain, - I: Iterator, -{ - let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Musl); - let cases = gen(&ctx); - - for input in cases { - let musl_res = input.call(musl_fn); - let crate_res = input.call(Op::ROUTINE); - - crate_res.validate(musl_res, input, &ctx).unwrap(); - } -} - libm_macros::for_each_function! { - callback: musl_domain_tests, + callback: musl_tests, attributes: [], skip: [ - // Functions with multiple inputs - atan2, - atan2f, - copysign, - copysignf, - copysignf16, - copysignf128, - fdim, - fdimf, - fma, - fmaf, - fmax, - fmaxf, - fmin, - fminf, - fmod, - fmodf, - hypot, - hypotf, + // TODO integer inputs jn, jnf, ldexp, ldexpf, - nextafter, - nextafterf, - pow, - powf, - remainder, - remainderf, - remquo, - remquof, scalbn, scalbnf, yn, ynf, // Not provided by musl + copysignf128, + copysignf16, fabsf128, fabsf16, fdimf128, diff --git a/crates/libm-test/tests/multiprecision.rs b/crates/libm-test/tests/multiprecision.rs index 123abfda..af3ff53d 100644 --- a/crates/libm-test/tests/multiprecision.rs +++ b/crates/libm-test/tests/multiprecision.rs @@ -2,38 +2,14 @@ #![cfg(feature = "build-mpfr")] -use libm_test::domain::HasDomain; -use libm_test::gen::random::RandomInput; -use libm_test::gen::{domain_logspace, edge_cases, random}; +use libm_test::gen::{edge_cases, random, spaced}; use libm_test::mpfloat::MpOp; -use libm_test::{CheckBasis, CheckCtx, CheckOutput, MathOp, OpFTy, OpRustFn, OpRustRet, TupleCall}; +use libm_test::{CheckBasis, CheckCtx, CheckOutput, GeneratorKind, MathOp, TupleCall}; -/// Test against MPFR with random inputs. -macro_rules! mp_rand_tests { - ( - fn_name: $fn_name:ident, - attrs: [$($attr:meta),*], - ) => { - paste::paste! { - #[test] - $(#[$attr])* - fn [< mp_random_ $fn_name >]() { - test_one_random::(); - } - } - }; -} +const BASIS: CheckBasis = CheckBasis::Mpfr; -/// Test a single routine with random inputs -fn test_one_random() -where - Op: MathOp + MpOp, - Op::RustArgs: RandomInput, -{ +fn mp_runner(ctx: &CheckCtx, cases: impl Iterator) { let mut mp_vals = Op::new_mp(); - let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr); - let cases = random::get_test_cases::(&ctx); - for input in cases { let mp_res = Op::run(&mut mp_vals, input); let crate_res = input.call(Op::ROUTINE); @@ -42,111 +18,53 @@ where } } -libm_macros::for_each_function! { - callback: mp_rand_tests, - attributes: [ - // Also an assertion failure on i686: at `MPFR_ASSERTN (! mpfr_erangeflag_p ())` - #[ignore = "large values are infeasible in MPFR"] - [jn, jnf, yn, ynf], - ], - skip: [ - // FIXME: test needed, see - // https://github.com/rust-lang/libm/pull/311#discussion_r1818273392 - nextafter, - nextafterf, - ], -} - -/// Test against MPFR with generators from a domain. -macro_rules! mp_domain_tests { +macro_rules! mp_tests { ( fn_name: $fn_name:ident, attrs: [$($attr:meta),*], ) => { paste::paste! { + #[test] + $(#[$attr])* + fn [< mp_random_ $fn_name >]() { + type Op = libm_test::op::$fn_name::Routine; + let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::Random); + let cases = random::get_test_cases::<::RustArgs>(&ctx); + mp_runner::(&ctx, cases); + } + #[test] $(#[$attr])* fn [< mp_edge_case_ $fn_name >]() { type Op = libm_test::op::$fn_name::Routine; - domain_test_runner::(edge_cases::get_test_cases::); + let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::EdgeCases); + let cases = edge_cases::get_test_cases::(&ctx); + mp_runner::(&ctx, cases); } #[test] $(#[$attr])* - fn [< mp_logspace_ $fn_name >]() { + fn [< mp_quickspace_ $fn_name >]() { type Op = libm_test::op::$fn_name::Routine; - domain_test_runner::(domain_logspace::get_test_cases::); + let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::QuickSpaced); + let cases = spaced::get_test_cases::(&ctx).0; + mp_runner::(&ctx, cases); } } }; } -/// Test a single routine against domaine-aware inputs. -fn domain_test_runner(gen: impl FnOnce(&CheckCtx) -> I) -where - // Complicated generics... - // The operation must take a single float argument (unary only) - Op: MathOp::FTy,)>, - // It must also support multiprecision operations - Op: MpOp, - // And it must have a domain specified - Op: HasDomain, - // The single float argument tuple must be able to call the `RustFn` and return `RustRet` - (OpFTy,): TupleCall, Output = OpRustRet>, - I: Iterator, -{ - let mut mp_vals = Op::new_mp(); - let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr); - let cases = gen(&ctx); - - for input in cases { - let mp_res = Op::run(&mut mp_vals, input); - let crate_res = input.call(Op::ROUTINE); - - crate_res.validate(mp_res, input, &ctx).unwrap(); - } -} - libm_macros::for_each_function! { - callback: mp_domain_tests, - attributes: [], + callback: mp_tests, + attributes: [ + // Also an assertion failure on i686: at `MPFR_ASSERTN (! mpfr_erangeflag_p ())` + #[ignore = "large values are infeasible in MPFR"] + [jn, jnf, yn, ynf], + ], skip: [ - // Functions with multiple inputs - atan2, - atan2f, - copysign, - copysignf, - copysignf16, - copysignf128, - fdim, - fdimf, - fdimf16, - fdimf128, - fma, - fmaf, - fmax, - fmaxf, - fmin, - fminf, - fmod, - fmodf, - hypot, - hypotf, - jn, - jnf, - ldexp, - ldexpf, + // FIXME: test needed, see + // https://github.com/rust-lang/libm/pull/311#discussion_r1818273392 nextafter, nextafterf, - pow, - powf, - remainder, - remainderf, - remquo, - remquof, - scalbn, - scalbnf, - yn, - ynf, ], } diff --git a/crates/libm-test/tests/z_extensive/run.rs b/crates/libm-test/tests/z_extensive/run.rs index 7ee96785..d459c061 100644 --- a/crates/libm-test/tests/z_extensive/run.rs +++ b/crates/libm-test/tests/z_extensive/run.rs @@ -6,13 +6,18 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; use indicatif::{ProgressBar, ProgressStyle}; -use libm_test::gen::extensive::{self, ExtensiveInput}; +use libm_test::gen::spaced; use libm_test::mpfloat::MpOp; use libm_test::{ - CheckBasis, CheckCtx, CheckOutput, MathOp, TestResult, TupleCall, skip_extensive_test, + CheckBasis, CheckCtx, CheckOutput, GeneratorKind, MathOp, TestResult, TupleCall, + skip_extensive_test, }; use libtest_mimic::{Arguments, Trial}; use rayon::prelude::*; +use spaced::SpacedInput; + +const BASIS: CheckBasis = CheckBasis::Mpfr; +const GEN_KIND: GeneratorKind = GeneratorKind::Extensive; /// Run the extensive test suite. pub fn run() { @@ -62,10 +67,10 @@ fn register_all_tests() -> Vec { fn register_single_test(all: &mut Vec) where Op: MathOp + MpOp, - Op::RustArgs: ExtensiveInput + Send, + Op::RustArgs: SpacedInput + Send, { let test_name = format!("mp_extensive_{}", Op::NAME); - let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr); + let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GEN_KIND); let skip = skip_extensive_test(&ctx); let runner = move || { @@ -73,7 +78,7 @@ where panic!("extensive tests should be run with --release"); } - let res = run_single_test::(); + let res = run_single_test::(&ctx); let e = match res { Ok(()) => return Ok(()), Err(e) => e, @@ -91,18 +96,17 @@ where } /// Test runner for a signle routine. -fn run_single_test() -> TestResult +fn run_single_test(ctx: &CheckCtx) -> TestResult where Op: MathOp + MpOp, - Op::RustArgs: ExtensiveInput + Send, + Op::RustArgs: SpacedInput + Send, { // Small delay before printing anything so other output from the runner has a chance to flush. std::thread::sleep(Duration::from_millis(500)); eprintln!(); let completed = AtomicU64::new(0); - let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr); - let (ref mut cases, total) = extensive::get_test_cases::(&ctx); + let (ref mut cases, total) = spaced::get_test_cases::(&ctx); let pb = Progress::new(Op::NAME, total); let test_single_chunk = |mp_vals: &mut Op::MpTy, input_vec: Vec| -> TestResult {