blob: 4e4a782a16988731c3fb361a3e4802a05b7695b6 [file] [log] [blame] [edit]
//! A generator that checks a handful of cases near infinities, zeros, asymptotes, and NaNs.
use libm::support::{CastInto, Float, Int, MinInt};
use crate::domain::get_domain;
use crate::generate::KnownSize;
use crate::op::OpITy;
use crate::run_cfg::{check_near_count, check_point_count};
use crate::{BaseName, CheckCtx, FloatExt, FloatTy, MathOp, test_log};
/// Generate a sequence of edge cases, e.g. numbers near zeroes and infiniteis.
pub trait EdgeCaseInput<Op> {
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self> + Send, u64);
}
/// Create a list of values around interesting points (infinities, zeroes, NaNs).
fn float_edge_cases<Op>(
ctx: &CheckCtx,
argnum: usize,
) -> (impl Iterator<Item = Op::FTy> + Clone, u64)
where
Op: MathOp,
{
let mut ret = Vec::new();
let one = OpITy::<Op>::ONE;
let values = &mut ret;
let domain = get_domain::<_, i8>(ctx.fn_ident, argnum).unwrap_float();
let domain_start = domain.range_start();
let domain_end = domain.range_end();
let check_points = check_point_count(ctx);
let near_points = check_near_count(ctx);
// Check near some notable constants
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(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);
count_up(domain_end, near_points, values);
count_down(domain_end, near_points, values);
// Check some special values that aren't included in the above ranges
values.push(Op::FTy::NAN);
values.push(Op::FTy::NEG_NAN);
values.extend(Op::FTy::consts().iter());
// Check around the maximum subnormal value
let sub_max = Op::FTy::from_bits(Op::FTy::SIG_MASK);
count_up(sub_max, near_points, values);
count_down(sub_max, near_points, values);
count_up(-sub_max, near_points, values);
count_down(-sub_max, near_points, values);
// Check a few values around the subnormal range
for shift in (0..Op::FTy::SIG_BITS).step_by(Op::FTy::SIG_BITS as usize / 5) {
let v = Op::FTy::from_bits(one << shift);
count_up(v, 2, values);
count_down(v, 2, values);
count_up(-v, 2, values);
count_down(-v, 2, values);
}
// Check around asymptotes
if let Some(f) = domain.check_points {
let iter = f();
for x in iter.take(check_points) {
count_up(x, near_points, values);
count_down(x, near_points, values);
}
}
// Some results may overlap so deduplicate the vector to save test cycles.
values.sort_by_key(|x| x.to_bits());
values.dedup_by_key(|x| x.to_bits());
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 `points` values starting at and including `x` and counting up. Uses the smallest possible
/// increments (1 ULP).
fn count_up<F: Float>(mut x: F, points: u64, values: &mut Vec<F>) {
assert!(!x.is_nan());
let mut count = 0;
while x < F::INFINITY && count < points {
values.push(x);
x = x.next_up();
count += 1;
}
}
/// Add `points` values starting at and including `x` and counting down. Uses the smallest possible
/// increments (1 ULP).
fn count_down<F: Float>(mut x: F, points: u64, values: &mut Vec<F>) {
assert!(!x.is_nan());
let mut count = 0;
while x > F::NEG_INFINITY && count < points {
values.push(x);
x = x.next_down();
count += 1;
}
}
/// Create a list of values around interesting integer points (min, zero, max).
pub fn int_edge_cases<I: Int>(
ctx: &CheckCtx,
argnum: usize,
) -> (impl Iterator<Item = I> + Clone, u64)
where
i32: CastInto<I>,
{
let mut values = Vec::new();
let near_points = check_near_count(ctx);
// Check around max/min and zero
int_count_around(I::MIN, near_points, &mut values);
int_count_around(I::MAX, near_points, &mut values);
int_count_around(I::ZERO, near_points, &mut values);
int_count_around(I::ZERO, near_points, &mut values);
if matches!(ctx.base_name, BaseName::Scalbn | BaseName::Ldexp) {
assert_eq!(argnum, 1, "scalbn integer argument should be arg1");
let (emax, emin, emin_sn) = match ctx.fn_ident.math_op().float_ty {
FloatTy::F16 => {
#[cfg(not(f16_enabled))]
unreachable!();
#[cfg(f16_enabled)]
(f16::EXP_MAX, f16::EXP_MIN, f16::EXP_MIN_SUBNORM)
}
FloatTy::F32 => (f32::EXP_MAX, f32::EXP_MIN, f32::EXP_MIN_SUBNORM),
FloatTy::F64 => (f64::EXP_MAX, f64::EXP_MIN, f64::EXP_MIN_SUBNORM),
FloatTy::F128 => {
#[cfg(not(f128_enabled))]
unreachable!();
#[cfg(f128_enabled)]
(f128::EXP_MAX, f128::EXP_MIN, f128::EXP_MIN_SUBNORM)
}
};
// `scalbn`/`ldexp` have their trickiest behavior around exponent limits
int_count_around(emax.cast(), near_points, &mut values);
int_count_around(emin.cast(), near_points, &mut values);
int_count_around(emin_sn.cast(), near_points, &mut values);
int_count_around((-emin_sn).cast(), near_points, &mut values);
// Also check values that cause the maximum possible difference in exponents
int_count_around((emax - emin).cast(), near_points, &mut values);
int_count_around((emin - emax).cast(), near_points, &mut values);
int_count_around((emax - emin_sn).cast(), near_points, &mut values);
int_count_around((emin_sn - emax).cast(), near_points, &mut values);
}
values.sort();
values.dedup();
let count = values.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(),
));
(values.into_iter(), count)
}
/// Add `points` values both up and down, starting at and including `x`.
fn int_count_around<I: Int>(x: I, points: u64, values: &mut Vec<I>) {
let mut current = x;
for _ in 0..points {
values.push(current);
current = match current.checked_add(I::ONE) {
Some(v) => v,
None => break,
};
}
current = x;
for _ in 0..points {
values.push(current);
current = match current.checked_sub(I::ONE) {
Some(v) => v,
None => break,
};
}
}
macro_rules! impl_edge_case_input {
($fty:ty) => {
impl<Op> EdgeCaseInput<Op> for ($fty,)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let (iter0, steps0) = float_edge_cases::<Op>(ctx, 0);
let iter0 = iter0.map(|v| (v,));
(iter0, steps0)
}
}
impl<Op> EdgeCaseInput<Op> for ($fty, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let (iter0, steps0) = float_edge_cases::<Op>(ctx, 0);
let (iter1, steps1) = float_edge_cases::<Op>(ctx, 1);
let iter =
iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second)));
let count = steps0.checked_mul(steps1).unwrap();
(iter, count)
}
}
impl<Op> EdgeCaseInput<Op> for ($fty, $fty, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let (iter0, steps0) = float_edge_cases::<Op>(ctx, 0);
let (iter1, steps1) = float_edge_cases::<Op>(ctx, 1);
let (iter2, steps2) = float_edge_cases::<Op>(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();
(iter, count)
}
}
impl<Op> EdgeCaseInput<Op> for (i32, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let (iter0, steps0) = int_edge_cases(ctx, 0);
let (iter1, steps1) = float_edge_cases::<Op>(ctx, 1);
let iter =
iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second)));
let count = steps0.checked_mul(steps1).unwrap();
(iter, count)
}
}
impl<Op> EdgeCaseInput<Op> for ($fty, i32)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let (iter0, steps0) = float_edge_cases::<Op>(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();
(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<Op>(
ctx: &CheckCtx,
) -> (impl Iterator<Item = Op::RustArgs> + Send + use<'_, Op>, u64)
where
Op: MathOp,
Op::RustArgs: EdgeCaseInput<Op>,
{
let (iter, count) = Op::RustArgs::get_cases(ctx);
// Wrap in `KnownSize` so we get an assertion if the cuunt is wrong.
(KnownSize::new(iter, count), count)
}