Merge pull request rust-lang/libm#348 from tgross35/function-domains

Introduce generators that respect function domains
This commit is contained in:
Trevor Gross 2024-12-29 03:00:36 -05:00 committed by GitHub
commit 49aa452fd9
15 changed files with 1694 additions and 57 deletions

View file

@ -62,22 +62,26 @@ esac
cargo check --no-default-features
cargo check --features "force-soft-floats"
# Always enable `unstable-float` since it expands available API but does not
# change any implementations.
extra_flags="$extra_flags --features unstable-float"
if [ "${BUILD_ONLY:-}" = "1" ]; then
cmd="cargo build --target $target --package libm"
$cmd
$cmd --features "unstable-intrinsics"
$cmd --features unstable-intrinsics
echo "can't run tests on $target; skipping"
else
cmd="cargo test --all --target $target $extra_flags"
# stable by default
# Test without intrinsics
$cmd
$cmd --release
# unstable with a feature
$cmd --features "unstable-intrinsics"
$cmd --release --features "unstable-intrinsics"
# Test with intrinsic use
$cmd --features unstable-intrinsics
$cmd --release --features unstable-intrinsics
# Make sure benchmarks have correct results
$cmd --benches

View file

@ -0,0 +1,105 @@
//! Program to write all inputs from a generator to a file, then invoke a Julia script to plot
//! them. Output is in `target/plots`.
//!
//! Requires Julia with the `CairoMakie` dependency.
//!
//! Note that running in release mode by default generates a _lot_ more datapoints, which
//! causes plotting to be extremely slow (some simplification to be done in the script).
use std::fmt::Write as _;
use std::io::{BufWriter, Write};
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::{MathOp, op};
const JL_PLOT: &str = "examples/plot_file.jl";
fn main() {
let manifest_env = env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_dir = Path::new(&manifest_env);
let out_dir = manifest_dir.join("../../target/plots");
if !out_dir.exists() {
fs::create_dir(&out_dir).unwrap();
}
let jl_script = manifest_dir.join(JL_PLOT);
let mut config = format!(r#"out_dir = "{}""#, out_dir.display());
config.write_str("\n\n").unwrap();
// Plot a few domains with some functions that use them.
plot_one_operator::<op::sqrtf::Routine>(&out_dir, &mut config);
plot_one_operator::<op::cosf::Routine>(&out_dir, &mut config);
plot_one_operator::<op::cbrtf::Routine>(&out_dir, &mut config);
let config_path = out_dir.join("config.toml");
fs::write(&config_path, config).unwrap();
// The script expects a path to `config.toml` to be passed as its only argument
let mut cmd = Command::new("julia");
if cfg!(optimizations_enabled) {
cmd.arg("-O3");
}
cmd.arg(jl_script).arg(config_path);
println!("launching script... {cmd:?}");
cmd.status().unwrap();
}
/// Run multiple generators for a single operator.
fn plot_one_operator<Op>(out_dir: &Path, config: &mut String)
where
Op: MathOp<FTy = f32> + HasDomain<f32>,
{
plot_one_generator(
out_dir,
Op::BASE_NAME.as_str(),
"logspace",
config,
domain_logspace::get_test_cases::<Op>(),
);
plot_one_generator(
out_dir,
Op::BASE_NAME.as_str(),
"edge_cases",
config,
edge_cases::get_test_cases::<Op, _>(),
);
}
/// Plot the output of a single generator.
fn plot_one_generator(
out_dir: &Path,
fn_name: &str,
gen_name: &str,
config: &mut String,
gen: impl Iterator<Item = (f32,)>,
) {
let text_file = out_dir.join(format!("input-{fn_name}-{gen_name}.txt"));
let f = fs::File::create(&text_file).unwrap();
let mut w = BufWriter::new(f);
let mut count = 0u64;
for input in gen {
writeln!(w, "{:e}", input.0).unwrap();
count += 1;
}
w.flush().unwrap();
println!("generated {count} inputs for {fn_name}-{gen_name}");
writeln!(
config,
r#"[[input]]
function = "{fn_name}"
generator = "{gen_name}"
input_file = "{}"
"#,
text_file.to_str().unwrap()
)
.unwrap()
}

View file

@ -0,0 +1,157 @@
"A quick script for plotting a list of floats.
Takes a path to a TOML file (Julia has builtin TOML support but not JSON) which
specifies a list of source files to plot. Plots are done with both a linear and
a log scale.
Requires [Makie] (specifically CairoMakie) for plotting.
[Makie]: https://docs.makie.org/stable/
"
using CairoMakie
using TOML
function main()::Nothing
CairoMakie.activate!(px_per_unit=10)
config_path = ARGS[1]
cfg = Dict()
open(config_path, "r") do f
cfg = TOML.parse(f)
end
out_dir = cfg["out_dir"]
for input in cfg["input"]
fn_name = input["function"]
gen_name = input["generator"]
input_file = input["input_file"]
plot_one(input_file, out_dir, fn_name, gen_name)
end
end
"Read inputs from a file, create both linear and log plots for one function"
function plot_one(
input_file::String,
out_dir::String,
fn_name::String,
gen_name::String,
)::Nothing
fig = Figure()
lin_out_file = joinpath(out_dir, "plot-$fn_name-$gen_name.png")
log_out_file = joinpath(out_dir, "plot-$fn_name-$gen_name-log.png")
# Map string function names to callable functions
if fn_name == "cos"
orig_func = cos
xlims = (-6.0, 6.0)
xlims_log = (-pi * 10, pi * 10)
elseif fn_name == "cbrt"
orig_func = cbrt
xlims = (-2.0, 2.0)
xlims_log = (-1000.0, 1000.0)
elseif fn_name == "sqrt"
orig_func = sqrt
xlims = (-1.1, 6.0)
xlims_log = (-1.1, 5000.0)
else
println("unrecognized function name `$fn_name`; update plot_file.jl")
exit(1)
end
# Edge cases don't do much beyond +/-1, except for infinity.
if gen_name == "edge_cases"
xlims = (-1.1, 1.1)
xlims_log = (-1.1, 1.1)
end
# Turn domain errors into NaN
func(x) = map_or(x, orig_func, NaN)
# Parse a series of X values produced by the generator
inputs = readlines(input_file)
gen_x = map((v) -> parse(Float32, v), inputs)
do_plot(
fig, gen_x, func, xlims[1], xlims[2],
"$fn_name $gen_name (linear scale)",
lin_out_file, false,
)
do_plot(
fig, gen_x, func, xlims_log[1], xlims_log[2],
"$fn_name $gen_name (log scale)",
log_out_file, true,
)
end
"Create a single plot"
function do_plot(
fig::Figure,
gen_x::Vector{F},
func::Function,
xmin::AbstractFloat,
xmax::AbstractFloat,
title::String,
out_file::String,
logscale::Bool,
)::Nothing where F<:AbstractFloat
println("plotting $title")
# `gen_x` is the values the generator produces. `actual_x` is for plotting a
# continuous function.
input_min = xmin - 1.0
input_max = xmax + 1.0
gen_x = filter((v) -> v >= input_min && v <= input_max, gen_x)
markersize = length(gen_x) < 10_000 ? 6.0 : 4.0
steps = 10_000
if logscale
r = LinRange(symlog10(input_min), symlog10(input_max), steps)
actual_x = sympow10.(r)
xscale = Makie.pseudolog10
else
actual_x = LinRange(input_min, input_max, steps)
xscale = identity
end
gen_y = @. func(gen_x)
actual_y = @. func(actual_x)
ax = Axis(fig[1, 1], xscale=xscale, title=title)
lines!(
ax, actual_x, actual_y, color=(:lightblue, 0.6),
linewidth=6.0, label="true function",
)
scatter!(
ax, gen_x, gen_y, color=(:darkblue, 0.9),
markersize=markersize, label="checked inputs",
)
axislegend(ax, position=:rb, framevisible=false)
save(out_file, fig)
delete!(ax)
end
"Apply a function, returning the default if there is a domain error"
function map_or(
input::AbstractFloat,
f::Function,
default::Any
)::Union{AbstractFloat,Any}
try
return f(input)
catch
return default
end
end
# Operations for logarithms that are symmetric about 0
C = 10
symlog10(x::Number) = sign(x) * (log10(1 + abs(x)/(10^C)))
sympow10(x::Number) = (10^C) * (10^x - 1)
main()

View file

@ -0,0 +1,186 @@
//! Traits and operations related to bounds of a function.
use std::fmt;
use std::ops::{self, Bound};
use crate::{Float, FloatExt};
/// Representation of a function's domain.
#[derive(Clone, Debug)]
pub struct Domain<T> {
/// Start of the region for which a function is defined (ignoring poles).
pub start: Bound<T>,
/// Endof the region for which a function is defined (ignoring poles).
pub end: Bound<T>,
/// Additional points to check closer around. These can be e.g. undefined asymptotes or
/// inflection points.
pub check_points: Option<fn() -> BoxIter<T>>,
}
type BoxIter<T> = Box<dyn Iterator<Item = T>>;
impl<F: FloatExt> Domain<F> {
/// The start of this domain, saturating at negative infinity.
pub fn range_start(&self) -> F {
match self.start {
Bound::Included(v) => v,
Bound::Excluded(v) => v.next_up(),
Bound::Unbounded => F::NEG_INFINITY,
}
}
/// The end of this domain, saturating at infinity.
pub fn range_end(&self) -> F {
match self.end {
Bound::Included(v) => v,
Bound::Excluded(v) => v.next_down(),
Bound::Unbounded => F::INFINITY,
}
}
}
impl<F: Float> Domain<F> {
/// x ∈
pub const UNBOUNDED: Self =
Self { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None };
/// x ∈ >= 0
pub const POSITIVE: Self =
Self { start: Bound::Included(F::ZERO), end: Bound::Unbounded, check_points: None };
/// x ∈ > 0
pub const STRICTLY_POSITIVE: Self =
Self { start: Bound::Excluded(F::ZERO), end: Bound::Unbounded, check_points: None };
/// Used for versions of `asin` and `acos`.
pub const INVERSE_TRIG_PERIODIC: Self = Self {
start: Bound::Included(F::NEG_ONE),
end: Bound::Included(F::ONE),
check_points: None,
};
/// Domain for `acosh`
pub const ACOSH: Self =
Self { start: Bound::Included(F::ONE), end: Bound::Unbounded, check_points: None };
/// Domain for `atanh`
pub const ATANH: Self = Self {
start: Bound::Excluded(F::NEG_ONE),
end: Bound::Excluded(F::ONE),
check_points: None,
};
/// Domain for `sin`, `cos`, and `tan`
pub const TRIG: Self = Self {
// TODO
check_points: Some(|| Box::new([-F::PI, -F::FRAC_PI_2, F::FRAC_PI_2, F::PI].into_iter())),
..Self::UNBOUNDED
};
/// Domain for `log` in various bases
pub const LOG: Self = 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 };
/// Domain for `sqrt`
pub const SQRT: Self = Self::POSITIVE;
/// Domain for `gamma`
pub const GAMMA: Self = Self {
check_points: Some(|| {
// Negative integers are asymptotes
Box::new((0..u8::MAX).map(|scale| {
let mut base = F::ZERO;
for _ in 0..scale {
base = base - F::ONE;
}
base
}))
}),
// Whether or not gamma is defined for negative numbers is implementation dependent
..Self::UNBOUNDED
};
/// Domain for `loggamma`
pub const LGAMMA: Self = Self::STRICTLY_POSITIVE;
}
/// Implement on `op::*` types to indicate how they are bounded.
pub trait HasDomain<T>
where
T: Copy + fmt::Debug + ops::Add<Output = T> + ops::Sub<Output = T> + PartialOrd + 'static,
{
const DOMAIN: Domain<T>;
}
/// 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<f64> for $crate::op::$fn_name::Routine {
const DOMAIN: Domain<f64> = Domain::<f64>::$domain;
}
// Implement for f32 functions
impl HasDomain<f32> for $crate::op::[< $fn_name f >]::Routine {
const DOMAIN: Domain<f32> = Domain::<f32>::$domain;
}
)*
}
};
}
// 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;
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;
}
/* Manual implementations, these functions don't follow `foo`->`foof` naming */
impl HasDomain<f32> for crate::op::lgammaf_r::Routine {
const DOMAIN: Domain<f32> = Domain::<f32>::LGAMMA;
}
impl HasDomain<f64> for crate::op::lgamma_r::Routine {
const DOMAIN: Domain<f64> = Domain::<f64>::LGAMMA;
}

View file

@ -0,0 +1,487 @@
//! An IEEE-compliant 8-bit float type for testing purposes.
use std::cmp::{self, Ordering};
use std::{fmt, ops};
use crate::Float;
/// Sometimes verifying float logic is easiest when all values can quickly be checked exhaustively
/// or by hand.
///
/// IEEE-754 compliant type that includes a 1 bit sign, 4 bit exponent, and 3 bit significand.
/// Bias is -7.
///
/// Based on <https://en.wikipedia.org/wiki/Minifloat#Example_8-bit_float_(1.4.3)>.
#[derive(Clone, Copy)]
#[repr(transparent)]
#[allow(non_camel_case_types)]
pub struct f8(u8);
impl Float for f8 {
type Int = u8;
type SignedInt = i8;
type ExpInt = i8;
const ZERO: Self = Self(0b0_0000_000);
const NEG_ZERO: Self = Self(0b1_0000_000);
const ONE: Self = Self(0b0_0111_000);
const NEG_ONE: Self = Self(0b1_0111_000);
const MAX: Self = Self(0b0_1110_111);
const MIN: Self = Self(0b1_1110_111);
const INFINITY: Self = Self(0b0_1111_000);
const NEG_INFINITY: Self = Self(0b1_1111_000);
const NAN: Self = Self(0b0_1111_100);
const PI: Self = Self::ZERO;
const NEG_PI: Self = Self::ZERO;
const FRAC_PI_2: Self = Self::ZERO;
const BITS: u32 = 8;
const SIG_BITS: u32 = 3;
const SIGN_MASK: Self::Int = 0b1_0000_000;
const SIG_MASK: Self::Int = 0b0_0000_111;
const EXP_MASK: Self::Int = 0b0_1111_000;
const IMPLICIT_BIT: Self::Int = 0b0_0001_000;
fn to_bits(self) -> Self::Int {
self.0
}
fn to_bits_signed(self) -> Self::SignedInt {
self.0 as i8
}
fn is_nan(self) -> bool {
self.0 & Self::EXP_MASK == Self::EXP_MASK && self.0 & Self::SIG_MASK != 0
}
fn is_infinite(self) -> bool {
self.0 & Self::EXP_MASK == Self::EXP_MASK && self.0 & Self::SIG_MASK == 0
}
fn is_sign_negative(self) -> bool {
self.0 & Self::SIGN_MASK != 0
}
fn exp(self) -> Self::ExpInt {
unimplemented!()
}
fn from_bits(a: Self::Int) -> Self {
Self(a)
}
fn normalize(_significand: Self::Int) -> (i32, Self::Int) {
unimplemented!()
}
}
impl f8 {
pub const ALL_LEN: usize = 240;
/// All non-infinite non-NaN values of `f8`
pub const ALL: [Self; Self::ALL_LEN] = [
// -m*2^7
Self(0b1_1110_111), // -240
Self(0b1_1110_110),
Self(0b1_1110_101),
Self(0b1_1110_100),
Self(0b1_1110_011),
Self(0b1_1110_010),
Self(0b1_1110_001),
Self(0b1_1110_000), // -128
// -m*2^6
Self(0b1_1101_111), // -120
Self(0b1_1101_110),
Self(0b1_1101_101),
Self(0b1_1101_100),
Self(0b1_1101_011),
Self(0b1_1101_010),
Self(0b1_1101_001),
Self(0b1_1101_000), // -64
// -m*2^5
Self(0b1_1100_111), // -60
Self(0b1_1100_110),
Self(0b1_1100_101),
Self(0b1_1100_100),
Self(0b1_1100_011),
Self(0b1_1100_010),
Self(0b1_1100_001),
Self(0b1_1100_000), // -32
// -m*2^4
Self(0b1_1011_111), // -30
Self(0b1_1011_110),
Self(0b1_1011_101),
Self(0b1_1011_100),
Self(0b1_1011_011),
Self(0b1_1011_010),
Self(0b1_1011_001),
Self(0b1_1011_000), // -16
// -m*2^3
Self(0b1_1010_111), // -15
Self(0b1_1010_110),
Self(0b1_1010_101),
Self(0b1_1010_100),
Self(0b1_1010_011),
Self(0b1_1010_010),
Self(0b1_1010_001),
Self(0b1_1010_000), // -8
// -m*2^2
Self(0b1_1001_111), // -7.5
Self(0b1_1001_110),
Self(0b1_1001_101),
Self(0b1_1001_100),
Self(0b1_1001_011),
Self(0b1_1001_010),
Self(0b1_1001_001),
Self(0b1_1001_000), // -4
// -m*2^1
Self(0b1_1000_111), // -3.75
Self(0b1_1000_110),
Self(0b1_1000_101),
Self(0b1_1000_100),
Self(0b1_1000_011),
Self(0b1_1000_010),
Self(0b1_1000_001),
Self(0b1_1000_000), // -2
// -m*2^0
Self(0b1_0111_111), // -1.875
Self(0b1_0111_110),
Self(0b1_0111_101),
Self(0b1_0111_100),
Self(0b1_0111_011),
Self(0b1_0111_010),
Self(0b1_0111_001),
Self(0b1_0111_000), // -1
// -m*2^-1
Self(0b1_0110_111), // 0.9375
Self(0b1_0110_110),
Self(0b1_0110_101),
Self(0b1_0110_100),
Self(0b1_0110_011),
Self(0b1_0110_010),
Self(0b1_0110_001),
Self(0b1_0110_000), // -0.5
// -m*2^-2
Self(0b1_0101_111), // 0.46875
Self(0b1_0101_110),
Self(0b1_0101_101),
Self(0b1_0101_100),
Self(0b1_0101_011),
Self(0b1_0101_010),
Self(0b1_0101_001),
Self(0b1_0101_000), // -0.25
// -m*2^-3
Self(0b1_0100_111), // 0.234375
Self(0b1_0100_110),
Self(0b1_0100_101),
Self(0b1_0100_100),
Self(0b1_0100_011),
Self(0b1_0100_010),
Self(0b1_0100_001),
Self(0b1_0100_000), // -0.125
// -m*2^-4
Self(0b1_0011_111), // 0.1171875
Self(0b1_0011_110),
Self(0b1_0011_101),
Self(0b1_0011_100),
Self(0b1_0011_011),
Self(0b1_0011_010),
Self(0b1_0011_001),
Self(0b1_0011_000), // 0.0625
// -m*2^-5
Self(0b1_0010_111), // 0.05859375
Self(0b1_0010_110),
Self(0b1_0010_101),
Self(0b1_0010_100),
Self(0b1_0010_011),
Self(0b1_0010_010),
Self(0b1_0010_001),
Self(0b1_0010_000), // 0.03125
// -m*2^-6
Self(0b1_0001_111), // 0.029296875
Self(0b1_0001_110),
Self(0b1_0001_101),
Self(0b1_0001_100),
Self(0b1_0001_011),
Self(0b1_0001_010),
Self(0b1_0001_001),
Self(0b1_0001_000), // 0.015625
// -m*2^-7 subnormal numbers
Self(0b1_0000_111), // 0.013671875
Self(0b1_0000_110),
Self(0b1_0000_101),
Self(0b1_0000_100),
Self(0b1_0000_011),
Self(0b1_0000_010),
Self(0b1_0000_001), // 0.001953125
// Zeroes
Self(0b1_0000_000), // -0.0
Self(0b0_0000_000), // 0.0
// m*2^-7 // subnormal numbers
Self(0b0_0000_001),
Self(0b0_0000_010),
Self(0b0_0000_011),
Self(0b0_0000_100),
Self(0b0_0000_101),
Self(0b0_0000_110),
Self(0b0_0000_111), // 0.013671875
// m*2^-6
Self(0b0_0001_000), // 0.015625
Self(0b0_0001_001),
Self(0b0_0001_010),
Self(0b0_0001_011),
Self(0b0_0001_100),
Self(0b0_0001_101),
Self(0b0_0001_110),
Self(0b0_0001_111), // 0.029296875
// m*2^-5
Self(0b0_0010_000), // 0.03125
Self(0b0_0010_001),
Self(0b0_0010_010),
Self(0b0_0010_011),
Self(0b0_0010_100),
Self(0b0_0010_101),
Self(0b0_0010_110),
Self(0b0_0010_111), // 0.05859375
// m*2^-4
Self(0b0_0011_000), // 0.0625
Self(0b0_0011_001),
Self(0b0_0011_010),
Self(0b0_0011_011),
Self(0b0_0011_100),
Self(0b0_0011_101),
Self(0b0_0011_110),
Self(0b0_0011_111), // 0.1171875
// m*2^-3
Self(0b0_0100_000), // 0.125
Self(0b0_0100_001),
Self(0b0_0100_010),
Self(0b0_0100_011),
Self(0b0_0100_100),
Self(0b0_0100_101),
Self(0b0_0100_110),
Self(0b0_0100_111), // 0.234375
// m*2^-2
Self(0b0_0101_000), // 0.25
Self(0b0_0101_001),
Self(0b0_0101_010),
Self(0b0_0101_011),
Self(0b0_0101_100),
Self(0b0_0101_101),
Self(0b0_0101_110),
Self(0b0_0101_111), // 0.46875
// m*2^-1
Self(0b0_0110_000), // 0.5
Self(0b0_0110_001),
Self(0b0_0110_010),
Self(0b0_0110_011),
Self(0b0_0110_100),
Self(0b0_0110_101),
Self(0b0_0110_110),
Self(0b0_0110_111), // 0.9375
// m*2^0
Self(0b0_0111_000), // 1
Self(0b0_0111_001),
Self(0b0_0111_010),
Self(0b0_0111_011),
Self(0b0_0111_100),
Self(0b0_0111_101),
Self(0b0_0111_110),
Self(0b0_0111_111), // 1.875
// m*2^1
Self(0b0_1000_000), // 2
Self(0b0_1000_001),
Self(0b0_1000_010),
Self(0b0_1000_011),
Self(0b0_1000_100),
Self(0b0_1000_101),
Self(0b0_1000_110),
Self(0b0_1000_111), // 3.75
// m*2^2
Self(0b0_1001_000), // 4
Self(0b0_1001_001),
Self(0b0_1001_010),
Self(0b0_1001_011),
Self(0b0_1001_100),
Self(0b0_1001_101),
Self(0b0_1001_110),
Self(0b0_1001_111), // 7.5
// m*2^3
Self(0b0_1010_000), // 8
Self(0b0_1010_001),
Self(0b0_1010_010),
Self(0b0_1010_011),
Self(0b0_1010_100),
Self(0b0_1010_101),
Self(0b0_1010_110),
Self(0b0_1010_111), // 15
// m*2^4
Self(0b0_1011_000), // 16
Self(0b0_1011_001),
Self(0b0_1011_010),
Self(0b0_1011_011),
Self(0b0_1011_100),
Self(0b0_1011_101),
Self(0b0_1011_110),
Self(0b0_1011_111), // 30
// m*2^5
Self(0b0_1100_000), // 32
Self(0b0_1100_001),
Self(0b0_1100_010),
Self(0b0_1100_011),
Self(0b0_1100_100),
Self(0b0_1100_101),
Self(0b0_1100_110),
Self(0b0_1100_111), // 60
// m*2^6
Self(0b0_1101_000), // 64
Self(0b0_1101_001),
Self(0b0_1101_010),
Self(0b0_1101_011),
Self(0b0_1101_100),
Self(0b0_1101_101),
Self(0b0_1101_110),
Self(0b0_1101_111), // 120
// m*2^7
Self(0b0_1110_000), // 128
Self(0b0_1110_001),
Self(0b0_1110_010),
Self(0b0_1110_011),
Self(0b0_1110_100),
Self(0b0_1110_101),
Self(0b0_1110_110),
Self(0b0_1110_111), // 240
];
}
impl ops::Add for f8 {
type Output = Self;
fn add(self, _rhs: Self) -> Self::Output {
unimplemented!()
}
}
impl ops::Sub for f8 {
type Output = Self;
fn sub(self, _rhs: Self) -> Self::Output {
unimplemented!()
}
}
impl ops::Mul for f8 {
type Output = Self;
fn mul(self, _rhs: Self) -> Self::Output {
unimplemented!()
}
}
impl ops::Div for f8 {
type Output = Self;
fn div(self, _rhs: Self) -> Self::Output {
unimplemented!()
}
}
impl ops::Neg for f8 {
type Output = Self;
fn neg(self) -> Self::Output {
Self(self.0 ^ Self::SIGN_MASK)
}
}
impl ops::Rem for f8 {
type Output = Self;
fn rem(self, _rhs: Self) -> Self::Output {
unimplemented!()
}
}
impl ops::AddAssign for f8 {
fn add_assign(&mut self, _rhs: Self) {
unimplemented!()
}
}
impl ops::SubAssign for f8 {
fn sub_assign(&mut self, _rhs: Self) {
unimplemented!()
}
}
impl ops::MulAssign for f8 {
fn mul_assign(&mut self, _rhs: Self) {
unimplemented!()
}
}
impl cmp::PartialEq for f8 {
fn eq(&self, other: &Self) -> bool {
if self.is_nan() || other.is_nan() {
false
} else if self.abs().to_bits() | other.abs().to_bits() == 0 {
true
} else {
self.0 == other.0
}
}
}
impl cmp::PartialOrd for f8 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let inf_rep = f8::EXP_MASK;
let a_abs = self.abs().to_bits();
let b_abs = other.abs().to_bits();
// If either a or b is NaN, they are unordered.
if a_abs > inf_rep || b_abs > inf_rep {
return None;
}
// If a and b are both zeros, they are equal.
if a_abs | b_abs == 0 {
return Some(Ordering::Equal);
}
let a_srep = self.to_bits_signed();
let b_srep = other.to_bits_signed();
let res = a_srep.cmp(&b_srep);
if a_srep & b_srep >= 0 {
// If at least one of a and b is positive, we get the same result comparing
// a and b as signed integers as we would with a fp_ting-point compare.
Some(res)
} else {
// Otherwise, both are negative, so we need to flip the sense of the
// comparison to get the correct result.
Some(res.reverse())
}
}
}
impl fmt::Display for f8 {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
unimplemented!()
}
}
impl fmt::Debug for f8 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Binary::fmt(self, f)
}
}
impl fmt::Binary for f8 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let v = self.0;
write!(
f,
"0b{:b}_{:04b}_{:03b}",
v >> 7,
(v & Self::EXP_MASK) >> Self::SIG_BITS,
v & Self::SIG_MASK
)
}
}
impl fmt::LowerHex for f8 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

View file

@ -1,6 +1,8 @@
//! Different generators that can create random or systematic bit patterns.
use crate::GenerateInput;
pub mod domain_logspace;
pub mod edge_cases;
pub mod random;
/// Helper type to turn any reusable input into a generator.

View file

@ -0,0 +1,43 @@
//! A generator that produces logarithmically spaced values within domain bounds.
use libm::support::{IntTy, MinInt};
use crate::domain::HasDomain;
use crate::op::OpITy;
use crate::{MathOp, logspace};
/// Number of tests to run.
// FIXME(ntests): replace this with a more logical algorithm
const NTESTS: usize = {
if cfg!(optimizations_enabled) {
if crate::emulated()
|| !cfg!(target_pointer_width = "64")
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"))
{
// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run
// in QEMU.
100_000
} else {
5_000_000
}
} else {
// Without optimizations just run a quick check
800
}
};
/// 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<Op>() -> impl Iterator<Item = (Op::FTy,)>
where
Op: MathOp + HasDomain<Op::FTy>,
IntTy<Op::FTy>: TryFrom<usize>,
{
let domain = Op::DOMAIN;
let start = domain.range_start();
let end = domain.range_end();
let steps = OpITy::<Op>::try_from(NTESTS).unwrap_or(OpITy::<Op>::MAX);
logspace(start, end, steps).map(|v| (v,))
}

View file

@ -0,0 +1,90 @@
//! A generator that checks a handful of cases near infinities, zeros, asymptotes, and NaNs.
use libm::support::Float;
use crate::domain::HasDomain;
use crate::{FloatExt, MathOp};
/// Number of values near an interesting point to check.
// FIXME(ntests): replace this with a more logical algorithm
const AROUND: usize = 100;
/// Functions have infinite asymptotes, limit how many we check.
// FIXME(ntests): replace this with a more logical algorithm
const MAX_CHECK_POINTS: usize = 10;
/// Create a list of values around interesting points (infinities, zeroes, NaNs).
pub fn get_test_cases<Op, F>() -> impl Iterator<Item = (F,)>
where
Op: MathOp<FTy = F> + HasDomain<F>,
F: Float,
{
let mut ret = Vec::new();
let values = &mut ret;
let domain = Op::DOMAIN;
let domain_start = domain.range_start();
let domain_end = domain.range_end();
// Check near some notable constants
count_up(F::ONE, values);
count_up(F::ZERO, values);
count_up(F::NEG_ONE, values);
count_down(F::ONE, values);
count_down(F::ZERO, values);
count_down(F::NEG_ONE, values);
values.push(F::NEG_ZERO);
// Check values near the extremes
count_up(F::NEG_INFINITY, values);
count_down(F::INFINITY, values);
count_down(domain_end, values);
count_up(domain_start, values);
count_down(domain_start, values);
count_up(domain_end, values);
count_down(domain_end, values);
// Check some special values that aren't included in the above ranges
values.push(F::NAN);
values.extend(F::consts().iter());
// Check around asymptotes
if let Some(f) = domain.check_points {
let iter = f();
for x in iter.take(MAX_CHECK_POINTS) {
count_up(x, values);
count_down(x, 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());
ret.into_iter().map(|v| (v,))
}
/// Add `AROUND` values starting at and including `x` and counting up. Uses the smallest possible
/// increments (1 ULP).
fn count_up<F: Float>(mut x: F, values: &mut Vec<F>) {
assert!(!x.is_nan());
let mut count = 0;
while x < F::INFINITY && count < AROUND {
values.push(x);
x = x.next_up();
count += 1;
}
}
/// Add `AROUND` values starting at and including `x` and counting down. Uses the smallest possible
/// increments (1 ULP).
fn count_down<F: Float>(mut x: F, values: &mut Vec<F>) {
assert!(!x.is_nan());
let mut count = 0;
while x > F::NEG_INFINITY && count < AROUND {
values.push(x);
x = x.next_down();
count += 1;
}
}

View file

@ -1,11 +1,18 @@
#![allow(clippy::unusual_byte_groupings)] // sometimes we group by sign_exp_sig
pub mod domain;
mod f8_impl;
pub mod gen;
#[cfg(feature = "test-multiprecision")]
pub mod mpfloat;
mod num;
pub mod op;
mod precision;
mod test_traits;
pub use libm::support::{Float, Int, IntTy};
pub use f8_impl::f8;
pub use libm::support::{Float, Int, IntTy, MinInt};
pub use num::{FloatExt, logspace};
pub use op::{BaseName, Identifier, MathOp, OpCFn, OpFTy, OpRustFn, OpRustRet};
pub use precision::{MaybeOverride, SpecialCase, default_ulp};
pub use test_traits::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, Hex, TupleCall};

View file

@ -0,0 +1,458 @@
//! Helpful numeric operations.
use std::cmp::min;
use libm::support::{CastInto, Float};
use crate::{Int, MinInt};
/// Extension to `libm`'s `Float` trait with methods that are useful for tests but not
/// needed in `libm` itself.
pub trait FloatExt: Float {
/// The minimum subnormal number.
const TINY_BITS: Self::Int = Self::Int::ONE;
/// Retrieve additional constants for this float type.
fn consts() -> Consts<Self> {
Consts::new()
}
/// Increment by one ULP, saturating at infinity.
fn next_up(self) -> Self {
let bits = self.to_bits();
if self.is_nan() || bits == Self::INFINITY.to_bits() {
return self;
}
let abs = self.abs().to_bits();
let next_bits = if abs == Self::Int::ZERO {
// Next up from 0 is the smallest subnormal
Self::TINY_BITS
} else if bits == abs {
// Positive: counting up is more positive
bits + Self::Int::ONE
} else {
// Negative: counting down is more positive
bits - Self::Int::ONE
};
Self::from_bits(next_bits)
}
/// A faster way to effectively call `next_up` `n` times.
fn n_up(self, n: Self::Int) -> Self {
let bits = self.to_bits();
if self.is_nan() || bits == Self::INFINITY.to_bits() || n == Self::Int::ZERO {
return self;
}
let abs = self.abs().to_bits();
let is_positive = bits == abs;
let crosses_zero = !is_positive && n > abs;
let inf_bits = Self::INFINITY.to_bits();
let next_bits = if abs == Self::Int::ZERO {
min(n, inf_bits)
} else if crosses_zero {
min(n - abs, inf_bits)
} else if is_positive {
// Positive, counting up is more positive but this may overflow
match bits.checked_add(n) {
Some(v) if v >= inf_bits => inf_bits,
Some(v) => v,
None => inf_bits,
}
} else {
// Negative, counting down is more positive
bits - n
};
Self::from_bits(next_bits)
}
/// Decrement by one ULP, saturating at negative infinity.
fn next_down(self) -> Self {
let bits = self.to_bits();
if self.is_nan() || bits == Self::NEG_INFINITY.to_bits() {
return self;
}
let abs = self.abs().to_bits();
let next_bits = if abs == Self::Int::ZERO {
// Next up from 0 is the smallest negative subnormal
Self::TINY_BITS | Self::SIGN_MASK
} else if bits == abs {
// Positive: counting down is more negative
bits - Self::Int::ONE
} else {
// Negative: counting up is more negative
bits + Self::Int::ONE
};
Self::from_bits(next_bits)
}
/// A faster way to effectively call `next_down` `n` times.
fn n_down(self, n: Self::Int) -> Self {
let bits = self.to_bits();
if self.is_nan() || bits == Self::NEG_INFINITY.to_bits() || n == Self::Int::ZERO {
return self;
}
let abs = self.abs().to_bits();
let is_positive = bits == abs;
let crosses_zero = is_positive && n > abs;
let inf_bits = Self::INFINITY.to_bits();
let ninf_bits = Self::NEG_INFINITY.to_bits();
let next_bits = if abs == Self::Int::ZERO {
min(n, inf_bits) | Self::SIGN_MASK
} else if crosses_zero {
min(n - abs, inf_bits) | Self::SIGN_MASK
} else if is_positive {
// Positive, counting down is more negative
bits - n
} else {
// Negative, counting up is more negative but this may overflow
match bits.checked_add(n) {
Some(v) if v > ninf_bits => ninf_bits,
Some(v) => v,
None => ninf_bits,
}
};
Self::from_bits(next_bits)
}
}
impl<F> FloatExt for F where F: Float {}
/// Extra constants that are useful for tests.
#[derive(Debug, Clone, Copy)]
pub struct Consts<F> {
/// The default quiet NaN, which is also the minimum quiet NaN.
pub pos_nan: F,
/// The default quiet NaN with negative sign.
pub neg_nan: F,
/// NaN with maximum (unsigned) significand to be a quiet NaN. The significand is saturated.
pub max_qnan: F,
/// NaN with minimum (unsigned) significand to be a signaling NaN.
pub min_snan: F,
/// NaN with maximum (unsigned) significand to be a signaling NaN.
pub max_snan: F,
pub neg_max_qnan: F,
pub neg_min_snan: F,
pub neg_max_snan: F,
}
impl<F: FloatExt> Consts<F> {
fn new() -> Self {
let top_sigbit_mask = F::Int::ONE << (F::SIG_BITS - 1);
let pos_nan = F::EXP_MASK | top_sigbit_mask;
let max_qnan = F::EXP_MASK | F::SIG_MASK;
let min_snan = F::EXP_MASK | F::Int::ONE;
let max_snan = (F::EXP_MASK | F::SIG_MASK) ^ top_sigbit_mask;
let neg_nan = pos_nan | F::SIGN_MASK;
let neg_max_qnan = max_qnan | F::SIGN_MASK;
let neg_min_snan = min_snan | F::SIGN_MASK;
let neg_max_snan = max_snan | F::SIGN_MASK;
Self {
pos_nan: F::from_bits(pos_nan),
neg_nan: F::from_bits(neg_nan),
max_qnan: F::from_bits(max_qnan),
min_snan: F::from_bits(min_snan),
max_snan: F::from_bits(max_snan),
neg_max_qnan: F::from_bits(neg_max_qnan),
neg_min_snan: F::from_bits(neg_min_snan),
neg_max_snan: F::from_bits(neg_max_snan),
}
}
pub fn iter(self) -> impl Iterator<Item = F> {
// Destructure so we get unused warnings if we forget a list entry.
let Self {
pos_nan,
neg_nan,
max_qnan,
min_snan,
max_snan,
neg_max_qnan,
neg_min_snan,
neg_max_snan,
} = self;
[pos_nan, neg_nan, max_qnan, min_snan, max_snan, neg_max_qnan, neg_min_snan, neg_max_snan]
.into_iter()
}
}
/// Return the number of steps between two floats, returning `None` if either input is NaN.
///
/// This is the number of steps needed for `n_up` or `n_down` to go between values. Infinities
/// are treated the same as those functions (will return the nearest finite value), and only one
/// of `-0` or `+0` is counted. It does not matter which value is greater.
pub fn ulp_between<F: Float>(x: F, y: F) -> Option<F::Int> {
let a = as_ulp_steps(x)?;
let b = as_ulp_steps(y)?;
Some(a.abs_diff(b))
}
/// Return the (signed) number of steps from zero to `x`.
fn as_ulp_steps<F: Float>(x: F) -> Option<F::SignedInt> {
let s = x.to_bits_signed();
let val = if s >= F::SignedInt::ZERO {
// each increment from `s = 0` is one step up from `x = 0.0`
s
} else {
// each increment from `s = F::SignedInt::MIN` is one step down from `x = -0.0`
F::SignedInt::MIN - s
};
// If `x` is NaN, return `None`
(!x.is_nan()).then_some(val)
}
/// An iterator that returns floats with linearly spaced integer representations, which translates
/// to logarithmic spacing of their values.
///
/// Note that this tends to skip negative zero, so that needs to be checked explicitly.
pub fn logspace<F: FloatExt>(start: F, end: F, steps: F::Int) -> impl Iterator<Item = F> {
assert!(!start.is_nan());
assert!(!end.is_nan());
assert!(end >= start);
let mut steps = steps.checked_sub(F::Int::ONE).expect("`steps` must be at least 2");
let between = ulp_between(start, end).expect("`start` or `end` is NaN");
let spacing = (between / steps).max(F::Int::ONE);
steps = steps.min(between); // At maximum, one step per ULP
let mut x = start;
(0..=steps.cast()).map(move |_| {
let ret = x;
x = x.n_up(spacing);
ret
})
}
#[cfg(test)]
mod tests {
use std::cmp::max;
use super::*;
use crate::f8;
#[test]
fn test_next_up_down() {
for (i, v) in f8::ALL.into_iter().enumerate() {
let down = v.next_down().to_bits();
let up = v.next_up().to_bits();
if i == 0 {
assert_eq!(down, f8::NEG_INFINITY.to_bits(), "{i} next_down({v:#010b})");
} else {
let expected =
if v == f8::ZERO { 1 | f8::SIGN_MASK } else { f8::ALL[i - 1].to_bits() };
assert_eq!(down, expected, "{i} next_down({v:#010b})");
}
if i == f8::ALL_LEN - 1 {
assert_eq!(up, f8::INFINITY.to_bits(), "{i} next_up({v:#010b})");
} else {
let expected = if v == f8::NEG_ZERO { 1 } else { f8::ALL[i + 1].to_bits() };
assert_eq!(up, expected, "{i} next_up({v:#010b})");
}
}
}
#[test]
fn test_next_up_down_inf_nan() {
assert_eq!(f8::NEG_INFINITY.next_up().to_bits(), f8::ALL[0].to_bits(),);
assert_eq!(f8::NEG_INFINITY.next_down().to_bits(), f8::NEG_INFINITY.to_bits(),);
assert_eq!(f8::INFINITY.next_down().to_bits(), f8::ALL[f8::ALL_LEN - 1].to_bits(),);
assert_eq!(f8::INFINITY.next_up().to_bits(), f8::INFINITY.to_bits(),);
assert_eq!(f8::NAN.next_up().to_bits(), f8::NAN.to_bits(),);
assert_eq!(f8::NAN.next_down().to_bits(), f8::NAN.to_bits(),);
}
#[test]
fn test_n_up_down_quick() {
assert_eq!(f8::ALL[0].n_up(4).to_bits(), f8::ALL[4].to_bits(),);
assert_eq!(
f8::ALL[f8::ALL_LEN - 1].n_down(4).to_bits(),
f8::ALL[f8::ALL_LEN - 5].to_bits(),
);
// Check around zero
assert_eq!(f8::from_bits(0b0).n_up(7).to_bits(), 0b0_0000_111);
assert_eq!(f8::from_bits(0b0).n_down(7).to_bits(), 0b1_0000_111);
// Check across zero
assert_eq!(f8::from_bits(0b1_0000_111).n_up(8).to_bits(), 0b0_0000_001);
assert_eq!(f8::from_bits(0b0_0000_111).n_down(8).to_bits(), 0b1_0000_001);
}
#[test]
fn test_n_up_down_one() {
// Verify that `n_up(1)` and `n_down(1)` are the same as `next_up()` and next_down()`.`
for i in 0..u8::MAX {
let v = f8::from_bits(i);
assert_eq!(v.next_up().to_bits(), v.n_up(1).to_bits());
assert_eq!(v.next_down().to_bits(), v.n_down(1).to_bits());
}
}
#[test]
fn test_n_up_down_inf_nan_zero() {
assert_eq!(f8::NEG_INFINITY.n_up(1).to_bits(), f8::ALL[0].to_bits());
assert_eq!(f8::NEG_INFINITY.n_up(239).to_bits(), f8::ALL[f8::ALL_LEN - 1].to_bits());
assert_eq!(f8::NEG_INFINITY.n_up(240).to_bits(), f8::INFINITY.to_bits());
assert_eq!(f8::NEG_INFINITY.n_down(u8::MAX).to_bits(), f8::NEG_INFINITY.to_bits());
assert_eq!(f8::INFINITY.n_down(1).to_bits(), f8::ALL[f8::ALL_LEN - 1].to_bits());
assert_eq!(f8::INFINITY.n_down(239).to_bits(), f8::ALL[0].to_bits());
assert_eq!(f8::INFINITY.n_down(240).to_bits(), f8::NEG_INFINITY.to_bits());
assert_eq!(f8::INFINITY.n_up(u8::MAX).to_bits(), f8::INFINITY.to_bits());
assert_eq!(f8::NAN.n_up(u8::MAX).to_bits(), f8::NAN.to_bits());
assert_eq!(f8::NAN.n_down(u8::MAX).to_bits(), f8::NAN.to_bits());
assert_eq!(f8::ZERO.n_down(1).to_bits(), f8::TINY_BITS | f8::SIGN_MASK);
assert_eq!(f8::NEG_ZERO.n_up(1).to_bits(), f8::TINY_BITS);
}
/// True if the specified range of `f8::ALL` includes both +0 and -0
fn crossed_zero(start: usize, end: usize) -> bool {
let crossed = &f8::ALL[start..=end];
crossed.iter().any(|f| f8::eq_repr(*f, f8::ZERO))
&& crossed.iter().any(|f| f8::eq_repr(*f, f8::NEG_ZERO))
}
#[test]
fn test_n_up_down() {
for (i, v) in f8::ALL.into_iter().enumerate() {
for n in 0..f8::ALL_LEN {
let down = v.n_down(n as u8).to_bits();
let up = v.n_up(n as u8).to_bits();
if let Some(down_exp_idx) = i.checked_sub(n) {
// No overflow
let mut expected = f8::ALL[down_exp_idx].to_bits();
if n >= 1 && crossed_zero(down_exp_idx, i) {
// If both -0 and +0 are included, we need to adjust our expected value
match down_exp_idx.checked_sub(1) {
Some(v) => expected = f8::ALL[v].to_bits(),
// Saturate to -inf if we are out of values
None => expected = f8::NEG_INFINITY.to_bits(),
}
}
assert_eq!(down, expected, "{i} {n} n_down({v:#010b})");
} else {
// Overflow to -inf
assert_eq!(down, f8::NEG_INFINITY.to_bits(), "{i} {n} n_down({v:#010b})");
}
let mut up_exp_idx = i + n;
if up_exp_idx < f8::ALL_LEN {
// No overflow
if n >= 1 && up_exp_idx < f8::ALL_LEN && crossed_zero(i, up_exp_idx) {
// If both -0 and +0 are included, we need to adjust our expected value
up_exp_idx += 1;
}
let expected = if up_exp_idx >= f8::ALL_LEN {
f8::INFINITY.to_bits()
} else {
f8::ALL[up_exp_idx].to_bits()
};
assert_eq!(up, expected, "{i} {n} n_up({v:#010b})");
} else {
// Overflow to +inf
assert_eq!(up, f8::INFINITY.to_bits(), "{i} {n} n_up({v:#010b})");
}
}
}
}
#[test]
fn test_ulp_between() {
for (i, x) in f8::ALL.into_iter().enumerate() {
for (j, y) in f8::ALL.into_iter().enumerate() {
let ulp = ulp_between(x, y).unwrap();
let make_msg = || format!("i: {i} j: {j} x: {x:b} y: {y:b} ulp {ulp}");
let i_low = min(i, j);
let i_hi = max(i, j);
let mut expected = u8::try_from(i_hi - i_low).unwrap();
if crossed_zero(i_low, i_hi) {
expected -= 1;
}
assert_eq!(ulp, expected, "{}", make_msg());
// Skip if either are zero since `next_{up,down}` will count over it
let either_zero = x == f8::ZERO || y == f8::ZERO;
if x < y && !either_zero {
assert_eq!(x.n_up(ulp).to_bits(), y.to_bits(), "{}", make_msg());
assert_eq!(y.n_down(ulp).to_bits(), x.to_bits(), "{}", make_msg());
} else if !either_zero {
assert_eq!(y.n_up(ulp).to_bits(), x.to_bits(), "{}", make_msg());
assert_eq!(x.n_down(ulp).to_bits(), y.to_bits(), "{}", make_msg());
}
}
}
}
#[test]
fn test_ulp_between_inf_nan_zero() {
assert_eq!(ulp_between(f8::NEG_INFINITY, f8::INFINITY).unwrap(), f8::ALL_LEN as u8);
assert_eq!(ulp_between(f8::INFINITY, f8::NEG_INFINITY).unwrap(), f8::ALL_LEN as u8);
assert_eq!(
ulp_between(f8::NEG_INFINITY, f8::ALL[f8::ALL_LEN - 1]).unwrap(),
f8::ALL_LEN as u8 - 1
);
assert_eq!(ulp_between(f8::INFINITY, f8::ALL[0]).unwrap(), f8::ALL_LEN as u8 - 1);
assert_eq!(ulp_between(f8::ZERO, f8::NEG_ZERO).unwrap(), 0);
assert_eq!(ulp_between(f8::NAN, f8::ZERO), None);
assert_eq!(ulp_between(f8::ZERO, f8::NAN), None);
}
#[test]
fn test_logspace() {
let ls: Vec<_> = logspace(f8::from_bits(0x0), f8::from_bits(0x4), 2).collect();
let exp = [f8::from_bits(0x0), f8::from_bits(0x4)];
assert_eq!(ls, exp);
let ls: Vec<_> = logspace(f8::from_bits(0x0), f8::from_bits(0x4), 3).collect();
let exp = [f8::from_bits(0x0), f8::from_bits(0x2), f8::from_bits(0x4)];
assert_eq!(ls, exp);
// Check that we include all values with no repeats if `steps` exceeds the maximum number
// of steps.
let ls: Vec<_> = logspace(f8::from_bits(0x0), f8::from_bits(0x3), 10).collect();
let exp = [f8::from_bits(0x0), f8::from_bits(0x1), f8::from_bits(0x2), f8::from_bits(0x3)];
assert_eq!(ls, exp);
}
#[test]
fn test_consts() {
let Consts {
pos_nan,
neg_nan,
max_qnan,
min_snan,
max_snan,
neg_max_qnan,
neg_min_snan,
neg_max_snan,
} = f8::consts();
assert_eq!(pos_nan.to_bits(), 0b0_1111_100);
assert_eq!(neg_nan.to_bits(), 0b1_1111_100);
assert_eq!(max_qnan.to_bits(), 0b0_1111_111);
assert_eq!(min_snan.to_bits(), 0b0_1111_001);
assert_eq!(max_snan.to_bits(), 0b0_1111_011);
assert_eq!(neg_max_qnan.to_bits(), 0b1_1111_111);
assert_eq!(neg_min_snan.to_bits(), 0b1_1111_001);
assert_eq!(neg_max_snan.to_bits(), 0b1_1111_011);
}
}

View file

@ -41,10 +41,11 @@ pub fn default_ulp(ctx: &CheckCtx) -> u32 {
(Musl, Id::Tgamma) => 20,
// Overrides for MPFR
(Mpfr, Id::Acosh) => 4,
(Mpfr, Id::Acoshf) => 4,
(Mpfr, Id::Asinh | Id::Asinhf) => 2,
(Mpfr, Id::Atanh | Id::Atanhf) => 2,
(Mpfr, Id::Exp10 | Id::Exp10f) => 3,
(Mpfr, Id::Exp10 | Id::Exp10f) => 6,
(Mpfr, Id::Lgamma | Id::LgammaR | Id::Lgammaf | Id::LgammafR) => 16,
(Mpfr, Id::Sinh | Id::Sinhf) => 2,
(Mpfr, Id::Tanh | Id::Tanhf) => 2,
@ -105,17 +106,14 @@ impl MaybeOverride<(f32,)> for SpecialCase {
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
if ctx.basis == CheckBasis::Musl {
if ctx.base_name == BaseName::Expm1 && input.0 > 80.0 && actual.is_infinite() {
// we return infinity but the number is representable
return XFAIL;
}
if ctx.base_name == BaseName::Expm1 && input.0 > 80.0 && actual.is_infinite() {
// we return infinity but the number is representable
return XFAIL;
}
if ctx.base_name == BaseName::Sinh && input.0.abs() > 80.0 && actual.is_nan() {
// we return some NaN that should be real values or infinite
// doesn't seem to happen on x86
return XFAIL;
}
if ctx.base_name == BaseName::Sinh && input.0.abs() > 80.0 && actual.is_nan() {
// we return some NaN that should be real values or infinite
return XFAIL;
}
if ctx.base_name == BaseName::Acosh && input.0 < -1.0 {

View file

@ -2,11 +2,14 @@
#![cfg(feature = "test-multiprecision")]
use libm_test::gen::{CachedInput, random};
use libm_test::domain::HasDomain;
use libm_test::gen::{CachedInput, domain_logspace, edge_cases, random};
use libm_test::mpfloat::MpOp;
use libm_test::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, MathOp, TupleCall};
use libm_test::{
CheckBasis, CheckCtx, CheckOutput, GenerateInput, MathOp, OpFTy, OpRustFn, OpRustRet, TupleCall,
};
/// Implement a test against MPFR with random inputs.
/// Test against MPFR with random inputs.
macro_rules! mp_rand_tests {
(
fn_name: $fn_name:ident,
@ -16,13 +19,14 @@ macro_rules! mp_rand_tests {
#[test]
$(#[$meta])*
fn [< mp_random_ $fn_name >]() {
test_one::<libm_test::op::$fn_name::Routine>();
test_one_random::<libm_test::op::$fn_name::Routine>();
}
}
};
}
fn test_one<Op>()
/// Test a single routine with random inputs
fn test_one_random<Op>()
where
Op: MathOp + MpOp,
CachedInput: GenerateInput<Op::RustArgs>,
@ -67,3 +71,97 @@ libm_macros::for_each_function! {
nextafterf,
],
}
/// Test against MPFR with generators from a domain.
macro_rules! mp_domain_tests {
(
fn_name: $fn_name:ident,
attrs: [$($meta:meta)*]
) => {
paste::paste! {
#[test]
$(#[$meta])*
fn [< mp_edge_case_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
domain_test_runner::<Op>(edge_cases::get_test_cases::<Op, _>());
}
#[test]
$(#[$meta])*
fn [< mp_logspace_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
domain_test_runner::<Op>(domain_logspace::get_test_cases::<Op>());
}
}
};
}
/// Test a single routine against domaine-aware inputs.
fn domain_test_runner<Op>(cases: impl Iterator<Item = (Op::FTy,)>)
where
// Complicated generics...
// The operation must take a single float argument (unary only)
Op: MathOp<RustArgs = (<Op as MathOp>::FTy,)>,
// It must also support multiprecision operations
Op: MpOp,
// And it must have a domain specified
Op: HasDomain<Op::FTy>,
// The single float argument tuple must be able to call the `RustFn` and return `RustRet`
(OpFTy<Op>,): TupleCall<OpRustFn<Op>, Output = OpRustRet<Op>>,
{
let mut mp_vals = Op::new_mp();
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
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: [],
skip: [
// Functions with multiple inputs
atan2,
atan2f,
copysign,
copysignf,
fdim,
fdimf,
fma,
fmaf,
fmax,
fmaxf,
fmin,
fminf,
fmod,
fmodf,
hypot,
hypotf,
jn,
jnf,
ldexp,
ldexpf,
nextafter,
nextafterf,
pow,
powf,
remainder,
remainderf,
remquo,
remquof,
scalbn,
scalbnf,
// FIXME: MPFR tests needed
frexp,
frexpf,
ilogb,
ilogbf,
modf,
modff,
],
}

View file

@ -38,6 +38,7 @@ pub trait Float:
const MAX: Self;
const MIN: Self;
const PI: Self;
const NEG_PI: Self;
const FRAC_PI_2: Self;
/// The bitwidth of the float type
@ -71,23 +72,15 @@ pub trait Float:
fn to_bits(self) -> Self::Int;
/// Returns `self` transmuted to `Self::SignedInt`
fn to_bits_signed(self) -> Self::SignedInt;
fn to_bits_signed(self) -> Self::SignedInt {
self.to_bits().signed()
}
/// Checks if two floats have the same bit representation. *Except* for NaNs! NaN can be
/// represented in multiple different ways. This method returns `true` if two NaNs are
/// compared.
fn eq_repr(self, rhs: Self) -> bool {
let is_nan = |x: Self| -> bool {
// }
// fn is_nan(x: Self) -> bool {
// When using mangled-names, the "real" compiler-builtins might not have the
// necessary builtin (__unordtf2) to test whether `f128` is NaN.
// FIXME(f16_f128): Remove once the nightly toolchain has the __unordtf2 builtin
// x is NaN if all the bits of the exponent are set and the significand is non-0
x.to_bits() & Self::EXP_MASK == Self::EXP_MASK
&& x.to_bits() & Self::SIG_MASK != Self::Int::ZERO
};
if is_nan(self) && is_nan(rhs) { true } else { self.to_bits() == rhs.to_bits() }
if self.is_nan() && rhs.is_nan() { true } else { self.to_bits() == rhs.to_bits() }
}
/// Returns true if the value is NaN.
@ -158,7 +151,15 @@ pub trait Float:
pub type IntTy<F> = <F as Float>::Int;
macro_rules! float_impl {
($ty:ident, $ity:ident, $sity:ident, $expty:ident, $bits:expr, $significand_bits:expr) => {
(
$ty:ident,
$ity:ident,
$sity:ident,
$expty:ident,
$bits:expr,
$significand_bits:expr,
$from_bits:path
) => {
impl Float for $ty {
type Int = $ity;
type SignedInt = $sity;
@ -173,13 +174,10 @@ macro_rules! float_impl {
const NAN: Self = Self::NAN;
const MAX: Self = -Self::MIN;
// Sign bit set, saturated mantissa, saturated exponent with last bit zeroed
// FIXME(msrv): just use `from_bits` when available
// SAFETY: POD cast with no preconditions
const MIN: Self = unsafe {
mem::transmute::<Self::Int, Self>(Self::Int::MAX & !(1 << Self::SIG_BITS))
};
const MIN: Self = $from_bits(Self::Int::MAX & !(1 << Self::SIG_BITS));
const PI: Self = core::$ty::consts::PI;
const NEG_PI: Self = -Self::PI;
const FRAC_PI_2: Self = core::$ty::consts::FRAC_PI_2;
const BITS: u32 = $bits;
@ -193,9 +191,6 @@ macro_rules! float_impl {
fn to_bits(self) -> Self::Int {
self.to_bits()
}
fn to_bits_signed(self) -> Self::SignedInt {
self.to_bits() as Self::SignedInt
}
fn is_nan(self) -> bool {
self.is_nan()
}
@ -220,8 +215,22 @@ macro_rules! float_impl {
}
#[cfg(f16_enabled)]
float_impl!(f16, u16, i16, i8, 16, 10);
float_impl!(f32, u32, i32, i16, 32, 23);
float_impl!(f64, u64, i64, i16, 64, 52);
float_impl!(f16, u16, i16, i8, 16, 10, f16::from_bits);
float_impl!(f32, u32, i32, i16, 32, 23, f32_from_bits);
float_impl!(f64, u64, i64, i16, 64, 52, f64_from_bits);
#[cfg(f128_enabled)]
float_impl!(f128, u128, i128, i16, 128, 112);
float_impl!(f128, u128, i128, i16, 128, 112, f128::from_bits);
/* FIXME(msrv): vendor some things that are not const stable at our MSRV */
/// `f32::from_bits`
pub const fn f32_from_bits(bits: u32) -> f32 {
// SAFETY: POD cast with no preconditions
unsafe { mem::transmute::<u32, f32>(bits) }
}
/// `f64::from_bits`
pub const fn f64_from_bits(bits: u64) -> f64 {
// SAFETY: POD cast with no preconditions
unsafe { mem::transmute::<u64, f64>(bits) }
}

View file

@ -2,6 +2,8 @@
#![allow(dead_code)] // FIXME: remove once this gets used
use super::{f32_from_bits, f64_from_bits};
/// Construct a 32-bit float from hex float representation (C-style)
pub const fn hf32(s: &str) -> f32 {
f32_from_bits(parse_any(s, 32, 23) as u32)
@ -159,16 +161,6 @@ const fn hex_digit(c: u8) -> u8 {
/* FIXME(msrv): vendor some things that are not const stable at our MSRV */
/// `f32::from_bits`
const fn f32_from_bits(v: u32) -> f32 {
unsafe { core::mem::transmute(v) }
}
/// `f64::from_bits`
const fn f64_from_bits(v: u64) -> f64 {
unsafe { core::mem::transmute(v) }
}
/// `u128::ilog2`
const fn u128_ilog2(v: u128) -> u32 {
assert!(v != 0);

View file

@ -6,6 +6,7 @@ mod int_traits;
#[allow(unused_imports)]
pub use float_traits::{Float, IntTy};
pub(crate) use float_traits::{f32_from_bits, f64_from_bits};
#[allow(unused_imports)]
pub use hex_float::{hf32, hf64};
pub use int_traits::{CastFrom, CastInto, DInt, HInt, Int, MinInt};