Replace libm_test::{Float, Int} with libm::{Float, Int}

This involves moving some things from full generic implementations (e.g.
`impl<F: Float> SomeTrait for F { /* ... */ }` to generic functions and
macros to implement traits that call them, due to orphan rule violations
after `Float` became a not-in-crate trait.

`Hex` was moved to `test_traits` so we can eliminate `num_traits`.
This commit is contained in:
Trevor Gross
2024-10-31 23:53:03 -05:00
committed by Trevor Gross
parent 4b12a8404f
commit dbf8a4ebe5
4 changed files with 198 additions and 282 deletions

View File

@@ -1,13 +1,12 @@
pub mod gen;
#[cfg(feature = "test-multiprecision")]
pub mod mpfloat;
mod num_traits;
mod precision;
mod test_traits;
pub use num_traits::{Float, Hex, Int};
pub use libm::support::{Float, Int};
pub use precision::{MaybeOverride, SpecialCase, multiprec_allowed_ulp, musl_allowed_ulp};
pub use test_traits::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, TupleCall};
pub use test_traits::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, Hex, TupleCall};
/// Result type for tests is usually from `anyhow`. Most times there is no success value to
/// propagate.

View File

@@ -1,214 +0,0 @@
use std::fmt;
use crate::{MaybeOverride, SpecialCase, TestResult};
/// Common types and methods for floating point numbers.
pub trait Float: Copy + fmt::Display + fmt::Debug + PartialEq<Self> {
type Int: Int<OtherSign = Self::SignedInt, Unsigned = Self::Int>;
type SignedInt: Int + Int<OtherSign = Self::Int, Unsigned = Self::Int>;
const ZERO: Self;
const ONE: Self;
/// The bitwidth of the float type
const BITS: u32;
/// The bitwidth of the significand
const SIGNIFICAND_BITS: u32;
/// The bitwidth of the exponent
const EXPONENT_BITS: u32 = Self::BITS - Self::SIGNIFICAND_BITS - 1;
fn is_nan(self) -> bool;
fn is_infinite(self) -> bool;
fn to_bits(self) -> Self::Int;
fn from_bits(bits: Self::Int) -> Self;
fn signum(self) -> Self;
}
macro_rules! impl_float {
($($fty:ty, $ui:ty, $si:ty, $significand_bits:expr;)+) => {
$(
impl Float for $fty {
type Int = $ui;
type SignedInt = $si;
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
const BITS: u32 = <$ui>::BITS;
const SIGNIFICAND_BITS: u32 = $significand_bits;
fn is_nan(self) -> bool {
self.is_nan()
}
fn is_infinite(self) -> bool {
self.is_infinite()
}
fn to_bits(self) -> Self::Int {
self.to_bits()
}
fn from_bits(bits: Self::Int) -> Self {
Self::from_bits(bits)
}
fn signum(self) -> Self {
self.signum()
}
}
impl Hex for $fty {
fn hex(self) -> String {
self.to_bits().hex()
}
}
)+
}
}
impl_float!(
f32, u32, i32, 23;
f64, u64, i64, 52;
);
/// Common types and methods for integers.
pub trait Int: Copy + fmt::Display + fmt::Debug + PartialEq<Self> {
type OtherSign: Int;
type Unsigned: Int;
const BITS: u32;
const SIGNED: bool;
fn signed(self) -> <Self::Unsigned as Int>::OtherSign;
fn unsigned(self) -> Self::Unsigned;
fn checked_sub(self, other: Self) -> Option<Self>;
fn abs(self) -> Self;
}
macro_rules! impl_int {
($($ui:ty, $si:ty ;)+) => {
$(
impl Int for $ui {
type OtherSign = $si;
type Unsigned = Self;
const BITS: u32 = <$ui>::BITS;
const SIGNED: bool = false;
fn signed(self) -> Self::OtherSign {
self as $si
}
fn unsigned(self) -> Self {
self
}
fn checked_sub(self, other: Self) -> Option<Self> {
self.checked_sub(other)
}
fn abs(self) -> Self {
unimplemented!()
}
}
impl Int for $si {
type OtherSign = $ui;
type Unsigned = $ui;
const BITS: u32 = <$ui>::BITS;
const SIGNED: bool = true;
fn signed(self) -> Self {
self
}
fn unsigned(self) -> $ui {
self as $ui
}
fn checked_sub(self, other: Self) -> Option<Self> {
self.checked_sub(other)
}
fn abs(self) -> Self {
self.abs()
}
}
impl_int!(@for_both $si);
impl_int!(@for_both $ui);
)+
};
(@for_both $ty:ty) => {
impl Hex for $ty {
fn hex(self) -> String {
format!("{self:#0width$x}", width = ((Self::BITS / 4) + 2) as usize)
}
}
impl<Input> $crate::CheckOutput<Input> for $ty
where
Input: Hex + fmt::Debug,
SpecialCase: MaybeOverride<Input>,
{
fn validate<'a>(
self,
expected: Self,
input: Input,
ctx: &$crate::CheckCtx,
) -> TestResult {
if let Some(res) = SpecialCase::check_int(input, self, expected, ctx) {
return res;
}
anyhow::ensure!(
self == expected,
"\
\n input: {input:?} {ibits}\
\n expected: {expected:<22?} {expbits}\
\n actual: {self:<22?} {actbits}\
",
actbits = self.hex(),
expbits = expected.hex(),
ibits = input.hex(),
);
Ok(())
}
}
}
}
impl_int!(
u32, i32;
u64, i64;
);
/// A helper trait to print something as hex with the correct number of nibbles, e.g. a `u32`
/// will always print with `0x` followed by 8 digits.
///
/// This is only used for printing errors so allocating is okay.
pub trait Hex: Copy {
fn hex(self) -> String;
}
impl<T1> Hex for (T1,)
where
T1: Hex,
{
fn hex(self) -> String {
format!("({},)", self.0.hex())
}
}
impl<T1, T2> Hex for (T1, T2)
where
T1: Hex,
T2: Hex,
{
fn hex(self) -> String {
format!("({}, {})", self.0.hex(), self.1.hex())
}
}
impl<T1, T2, T3> Hex for (T1, T2, T3)
where
T1: Hex,
T2: Hex,
T3: Hex,
{
fn hex(self) -> String {
format!("({}, {}, {})", self.0.hex(), self.1.hex(), self.2.hex())
}
}

View File

@@ -11,21 +11,7 @@ use std::fmt;
use anyhow::{Context, bail, ensure};
use crate::{Float, Hex, Int, MaybeOverride, SpecialCase, TestResult};
/// Implement this on types that can generate a sequence of tuples for test input.
pub trait GenerateInput<TupleArgs> {
fn get_cases(&self) -> impl Iterator<Item = TupleArgs>;
}
/// Trait for calling a function with a tuple as arguments.
///
/// Implemented on the tuple with the function signature as the generic (so we can use the same
/// tuple for multiple signatures).
pub trait TupleCall<Func>: fmt::Debug {
type Output;
fn call(self, f: Func) -> Self::Output;
}
use crate::{Float, Int, MaybeOverride, SpecialCase, TestResult};
/// Context passed to [`CheckOutput`].
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -56,14 +42,38 @@ pub enum CheckBasis {
Mpfr,
}
/// Implement this on types that can generate a sequence of tuples for test input.
pub trait GenerateInput<TupleArgs> {
fn get_cases(&self) -> impl Iterator<Item = TupleArgs>;
}
/// Trait for calling a function with a tuple as arguments.
///
/// Implemented on the tuple with the function signature as the generic (so we can use the same
/// tuple for multiple signatures).
pub trait TupleCall<Func>: fmt::Debug {
type Output;
fn call(self, f: Func) -> Self::Output;
}
/// A trait to implement on any output type so we can verify it in a generic way.
pub trait CheckOutput<Input>: Sized {
/// Validate `self` (actual) and `expected` are the same.
///
/// `input` is only used here for error messages.
fn validate<'a>(self, expected: Self, input: Input, ctx: &CheckCtx) -> TestResult;
fn validate(self, expected: Self, input: Input, ctx: &CheckCtx) -> TestResult;
}
/// A helper trait to print something as hex with the correct number of nibbles, e.g. a `u32`
/// will always print with `0x` followed by 8 digits.
///
/// This is only used for printing errors so allocating is okay.
pub trait Hex: Copy {
fn hex(self) -> String;
}
/* implement `TupleCall` */
impl<T1, R> TupleCall<fn(T1) -> R> for (T1,)
where
T1: fmt::Debug,
@@ -143,72 +153,193 @@ where
}
}
// Implement for floats
impl<F, Input> CheckOutput<Input> for F
/* implement `Hex` */
impl<T1> Hex for (T1,)
where
T1: Hex,
{
fn hex(self) -> String {
format!("({},)", self.0.hex())
}
}
impl<T1, T2> Hex for (T1, T2)
where
T1: Hex,
T2: Hex,
{
fn hex(self) -> String {
format!("({}, {})", self.0.hex(), self.1.hex())
}
}
impl<T1, T2, T3> Hex for (T1, T2, T3)
where
T1: Hex,
T2: Hex,
T3: Hex,
{
fn hex(self) -> String {
format!("({}, {}, {})", self.0.hex(), self.1.hex(), self.2.hex())
}
}
/* trait implementations for ints */
macro_rules! impl_int {
($($ty:ty),*) => {
$(
impl Hex for $ty {
fn hex(self) -> String {
format!("{self:#0width$x}", width = ((Self::BITS / 4) + 2) as usize)
}
}
impl<Input> $crate::CheckOutput<Input> for $ty
where
Input: Hex + fmt::Debug,
SpecialCase: MaybeOverride<Input>,
{
fn validate<'a>(
self,
expected: Self,
input: Input,
ctx: &$crate::CheckCtx,
) -> TestResult {
validate_int(self, expected, input, ctx)
}
}
)*
};
}
fn validate_int<'a, I, Input>(actual: I, expected: I, input: Input, ctx: &CheckCtx) -> TestResult
where
I: Int + Hex,
Input: Hex + fmt::Debug,
SpecialCase: MaybeOverride<Input>,
{
if let Some(res) = SpecialCase::check_int(input, actual, expected, ctx) {
return res;
}
anyhow::ensure!(
actual == expected,
"\
\n input: {input:?} {ibits}\
\n expected: {expected:<22?} {expbits}\
\n actual: {actual:<22?} {actbits}\
",
actbits = actual.hex(),
expbits = expected.hex(),
ibits = input.hex(),
);
Ok(())
}
impl_int!(u32, i32, u64, i64);
/* trait implementations for floats */
macro_rules! impl_float {
($($ty:ty),*) => {
$(
impl Hex for $ty {
fn hex(self) -> String {
format!(
"{:#0width$x}",
self.to_bits(),
width = ((Self::BITS / 4) + 2) as usize
)
}
}
impl<Input> $crate::CheckOutput<Input> for $ty
where
Input: Hex + fmt::Debug,
SpecialCase: MaybeOverride<Input>,
{
fn validate<'a>(
self,
expected: Self,
input: Input,
ctx: &$crate::CheckCtx,
) -> TestResult {
validate_float(self, expected, input, ctx)
}
}
)*
};
}
fn validate_float<'a, F, Input>(actual: F, expected: F, input: Input, ctx: &CheckCtx) -> TestResult
where
F: Float + Hex,
Input: Hex + fmt::Debug,
u32: TryFrom<F::SignedInt, Error: fmt::Debug>,
SpecialCase: MaybeOverride<Input>,
{
fn validate<'a>(self, expected: Self, input: Input, ctx: &CheckCtx) -> TestResult {
// Create a wrapper function so we only need to `.with_context` once.
let inner = || -> TestResult {
let mut allowed_ulp = ctx.ulp;
// Create a wrapper function so we only need to `.with_context` once.
let inner = || -> TestResult {
let mut allowed_ulp = ctx.ulp;
// If the tested function requires a nonstandard test, run it here.
if let Some(res) =
SpecialCase::check_float(input, self, expected, &mut allowed_ulp, ctx)
{
return res;
}
// If the tested function requires a nonstandard test, run it here.
if let Some(res) = SpecialCase::check_float(input, actual, expected, &mut allowed_ulp, ctx)
{
return res;
}
// Check when both are NaNs
if self.is_nan() && expected.is_nan() {
// By default, NaNs have nothing special to check.
return Ok(());
} else if self.is_nan() || expected.is_nan() {
// Check when only one is a NaN
bail!("real value != NaN")
}
// Check when both are NaNs
if actual.is_nan() && expected.is_nan() {
// By default, NaNs have nothing special to check.
return Ok(());
} else if actual.is_nan() || expected.is_nan() {
// Check when only one is a NaN
bail!("real value != NaN")
}
// Make sure that the signs are the same before checing ULP to avoid wraparound
let act_sig = self.signum();
let exp_sig = expected.signum();
ensure!(act_sig == exp_sig, "mismatched signs {act_sig} {exp_sig}");
// Make sure that the signs are the same before checing ULP to avoid wraparound
let act_sig = actual.signum();
let exp_sig = expected.signum();
ensure!(act_sig == exp_sig, "mismatched signs {act_sig} {exp_sig}");
if self.is_infinite() ^ expected.is_infinite() {
bail!("mismatched infinities");
}
if actual.is_infinite() ^ expected.is_infinite() {
bail!("mismatched infinities");
}
let act_bits = self.to_bits().signed();
let exp_bits = expected.to_bits().signed();
let act_bits = actual.to_bits().signed();
let exp_bits = expected.to_bits().signed();
let ulp_diff = act_bits.checked_sub(exp_bits).unwrap().abs();
let ulp_diff = act_bits.checked_sub(exp_bits).unwrap().abs();
let ulp_u32 = u32::try_from(ulp_diff)
.map_err(|e| anyhow::anyhow!("{e:?}: ulp of {ulp_diff} exceeds u32::MAX"))?;
let ulp_u32 = u32::try_from(ulp_diff)
.map_err(|e| anyhow::anyhow!("{e:?}: ulp of {ulp_diff} exceeds u32::MAX"))?;
ensure!(ulp_u32 <= allowed_ulp, "ulp {ulp_diff} > {allowed_ulp}",);
ensure!(ulp_u32 <= allowed_ulp, "ulp {ulp_diff} > {allowed_ulp}",);
Ok(())
};
Ok(())
};
inner().with_context(|| {
format!(
"\
\n input: {input:?} {ibits}\
\n expected: {expected:<22?} {expbits}\
\n actual: {self:<22?} {actbits}\
",
actbits = self.hex(),
expbits = expected.hex(),
ibits = input.hex(),
)
})
}
inner().with_context(|| {
format!(
"\
\n input: {input:?} {ibits}\
\n expected: {expected:<22?} {expbits}\
\n actual: {actual:<22?} {actbits}\
",
actbits = actual.hex(),
expbits = expected.hex(),
ibits = input.hex(),
)
})
}
impl_float!(f32, f64);
/* trait implementations for compound types */
/// Implement `CheckOutput` for combinations of types.
macro_rules! impl_tuples {
($(($a:ty, $b:ty);)*) => {