Skip to content

Commit 319285b

Browse files
committed
Add a test against MPFR using random inputs
1 parent 92b365e commit 319285b

File tree

7 files changed

+228
-70
lines changed

7 files changed

+228
-70
lines changed

crates/libm-test/src/gen/random.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use rand::{Rng, SeedableRng};
77
use rand_chacha::ChaCha8Rng;
88

99
use super::CachedInput;
10-
use crate::GenerateInput;
10+
use crate::{CheckCtx, GenerateInput};
1111

1212
const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
1313

@@ -40,9 +40,10 @@ static TEST_CASES_JN: LazyLock<CachedInput> = LazyLock::new(|| {
4040
let mut cases = (&*TEST_CASES).clone();
4141

4242
// These functions are extremely slow, limit them
43-
cases.inputs_i32.truncate((NTESTS / 1000).max(80));
44-
cases.inputs_f32.truncate((NTESTS / 1000).max(80));
45-
cases.inputs_f64.truncate((NTESTS / 1000).max(80));
43+
let ntests_jn = (NTESTS / 1000).max(80);
44+
cases.inputs_i32.truncate(ntests_jn);
45+
cases.inputs_f32.truncate(ntests_jn);
46+
cases.inputs_f64.truncate(ntests_jn);
4647

4748
// It is easy to overflow the stack with these in debug mode
4849
let max_iterations = if cfg!(optimizations_enabled) && cfg!(target_pointer_width = "64") {
@@ -105,11 +106,10 @@ fn make_test_cases(ntests: usize) -> CachedInput {
105106
}
106107

107108
/// Create a test case iterator.
108-
pub fn get_test_cases<RustArgs>(fname: &str) -> impl Iterator<Item = RustArgs>
109+
pub fn get_test_cases<RustArgs>(ctx: &CheckCtx) -> impl Iterator<Item = RustArgs>
109110
where
110111
CachedInput: GenerateInput<RustArgs>,
111112
{
112-
let inputs = if fname == "jn" || fname == "jnf" { &TEST_CASES_JN } else { &TEST_CASES };
113-
114-
CachedInput::get_cases(inputs)
113+
let inputs = if ctx.fname == "jn" || ctx.fname == "jnf" { &TEST_CASES_JN } else { &TEST_CASES };
114+
inputs.get_cases()
115115
}

crates/libm-test/src/lib.rs

+27-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@ pub type TestResult<T = (), E = anyhow::Error> = Result<T, E>;
1616
// List of all files present in libm's source
1717
include!(concat!(env!("OUT_DIR"), "/all_files.rs"));
1818

19-
/// ULP allowed to differ from musl (note that musl itself may not be accurate).
19+
/// Default ULP allowed to differ from musl (note that musl itself may not be accurate).
2020
const MUSL_DEFAULT_ULP: u32 = 2;
2121

22-
/// Certain functions have different allowed ULP (consider these xfail).
22+
/// Default ULP allowed to differ from multiprecision (i.e. infinite) results.
23+
const MULTIPREC_DEFAULT_ULP: u32 = 1;
24+
25+
/// ULP allowed to differ from muls results.
2326
///
2427
/// Note that these results were obtained using 400,000,000 rounds of random inputs, which
2528
/// is not a value used by default.
2629
pub fn musl_allowed_ulp(name: &str) -> u32 {
30+
// Consider overrides xfail
2731
match name {
2832
#[cfg(x86_no_sse)]
2933
"asinh" | "asinhf" => 6,
@@ -44,6 +48,27 @@ pub fn musl_allowed_ulp(name: &str) -> u32 {
4448
}
4549
}
4650

51+
/// ULP allowed to differ from multiprecision results.
52+
pub fn multiprec_allowed_ulp(name: &str) -> u32 {
53+
// Consider overrides xfail
54+
match name {
55+
"asinh" | "asinhf" => 2,
56+
"acoshf" => 4,
57+
"atanh" | "atanhf" => 2,
58+
"exp10" | "exp10f" => 3,
59+
"j0" | "j0f" | "j1" | "j1f" => {
60+
// Results seem very target-dependent
61+
if cfg!(target_arch = "x86_64") { 4000 } else { 800_000 }
62+
}
63+
"jn" | "jnf" => 1000,
64+
"lgamma" | "lgammaf" | "lgamma_r" | "lgammaf_r" => 16,
65+
"sinh" | "sinhf" => 2,
66+
"tanh" | "tanhf" => 2,
67+
"tgamma" => 20,
68+
_ => MULTIPREC_DEFAULT_ULP,
69+
}
70+
}
71+
4772
/// Return the unsuffixed version of a function name; e.g. `abs` and `absf` both return `abs`,
4873
/// `lgamma_r` and `lgammaf_r` both return `lgamma_r`.
4974
pub fn canonical_name(name: &str) -> &str {

crates/libm-test/src/mpfloat.rs

-21
Original file line numberDiff line numberDiff line change
@@ -248,27 +248,6 @@ macro_rules! impl_op_for_ty {
248248
}
249249
}
250250

251-
pub mod [<nextafter $suffix>] {
252-
use super::*;
253-
pub struct Operation(MpFloat, MpFloat);
254-
255-
impl MpOp for Operation {
256-
type Input = ($fty, $fty);
257-
type Output = $fty;
258-
259-
fn new() -> Self {
260-
Self(new_mpfloat::<$fty>(), new_mpfloat::<$fty>())
261-
}
262-
263-
fn run(&mut self, input: Self::Input) -> Self::Output {
264-
self.0.assign(input.0);
265-
self.1.assign(input.1);
266-
self.0.next_toward(&self.1);
267-
prep_retval::<Self::Output>(&mut self.0, Ordering::Equal)
268-
}
269-
}
270-
}
271-
272251
pub mod [<pow $suffix>] {
273252
use super::*;
274253
pub struct Operation(MpFloat, MpFloat);

crates/libm-test/src/special_case.rs

+119-38
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,6 @@ impl MaybeOverride<(f32,)> for SpecialCase {
5858
ctx: &CheckCtx,
5959
) -> Option<TestResult> {
6060
if ctx.basis == CheckBasis::Musl {
61-
if ctx.fname == "acoshf" && input.0 < -1.0 {
62-
// acoshf is undefined for x <= 1.0, but we return a random result at lower
63-
// values.
64-
return XFAIL;
65-
}
66-
67-
if ctx.fname == "sincosf" {
68-
let factor_frac_pi_2 = input.0.abs() / f32::consts::FRAC_PI_2;
69-
if (factor_frac_pi_2 - factor_frac_pi_2.round()).abs() < 1e-2 {
70-
// we have a bad approximation near multiples of pi/2
71-
return XFAIL;
72-
}
73-
}
74-
7561
if ctx.fname == "expm1f" && input.0 > 80.0 && actual.is_infinite() {
7662
// we return infinity but the number is representable
7763
return XFAIL;
@@ -82,15 +68,40 @@ impl MaybeOverride<(f32,)> for SpecialCase {
8268
// doesn't seem to happen on x86
8369
return XFAIL;
8470
}
71+
}
8572

86-
if ctx.fname == "lgammaf" || ctx.fname == "lgammaf_r" && input.0 < 0.0 {
87-
// loggamma should not be defined for x < 0, yet we both return results
88-
return XFAIL;
89-
}
73+
if ctx.fname == "acoshf" && input.0 < -1.0 {
74+
// acoshf is undefined for x <= 1.0, but we return a random result at lower
75+
// values.
76+
return XFAIL;
77+
}
78+
79+
if ctx.fname == "lgammaf" || ctx.fname == "lgammaf_r" && input.0 < 0.0 {
80+
// loggamma should not be defined for x < 0, yet we both return results
81+
return XFAIL;
9082
}
9183

9284
maybe_check_nan_bits(actual, expected, ctx)
9385
}
86+
87+
fn check_int<I: Int>(
88+
input: (f32,),
89+
actual: I,
90+
expected: I,
91+
ctx: &CheckCtx,
92+
) -> Option<anyhow::Result<()>> {
93+
// On MPFR for lgammaf_r, we set -1 as the integer result for negative infinity but MPFR
94+
// sets +1
95+
if ctx.basis == CheckBasis::Mpfr
96+
&& ctx.fname == "lgammaf_r"
97+
&& input.0 == f32::NEG_INFINITY
98+
&& actual.abs() == expected.abs()
99+
{
100+
XFAIL
101+
} else {
102+
None
103+
}
104+
}
94105
}
95106

96107
impl MaybeOverride<(f64,)> for SpecialCase {
@@ -117,15 +128,40 @@ impl MaybeOverride<(f64,)> for SpecialCase {
117128
// musl returns -0.0, we return +0.0
118129
return XFAIL;
119130
}
131+
}
120132

121-
if ctx.fname == "lgamma" || ctx.fname == "lgamma_r" && input.0 < 0.0 {
122-
// loggamma should not be defined for x < 0, yet we both return results
123-
return XFAIL;
124-
}
133+
if ctx.fname == "acosh" && input.0 < 1.0 {
134+
// The function is undefined for the inputs, musl and our libm both return
135+
// random results.
136+
return XFAIL;
137+
}
138+
139+
if ctx.fname == "lgamma" || ctx.fname == "lgamma_r" && input.0 < 0.0 {
140+
// loggamma should not be defined for x < 0, yet we both return results
141+
return XFAIL;
125142
}
126143

127144
maybe_check_nan_bits(actual, expected, ctx)
128145
}
146+
147+
fn check_int<I: Int>(
148+
input: (f64,),
149+
actual: I,
150+
expected: I,
151+
ctx: &CheckCtx,
152+
) -> Option<anyhow::Result<()>> {
153+
// On MPFR for lgamma_r, we set -1 as the integer result for negative infinity but MPFR
154+
// sets +1
155+
if ctx.basis == CheckBasis::Mpfr
156+
&& ctx.fname == "lgamma_r"
157+
&& input.0 == f64::NEG_INFINITY
158+
&& actual.abs() == expected.abs()
159+
{
160+
XFAIL
161+
} else {
162+
None
163+
}
164+
}
129165
}
130166

131167
/// Check NaN bits if the function requires it
@@ -142,6 +178,11 @@ fn maybe_check_nan_bits<F: Float>(actual: F, expected: F, ctx: &CheckCtx) -> Opt
142178
return SKIP;
143179
}
144180

181+
// MPFR only has one NaN bitpattern; allow the default `.is_nan()` checks to validate.
182+
if ctx.basis == CheckBasis::Mpfr {
183+
return SKIP;
184+
}
185+
145186
// abs and copysign require signaling NaNs to be propagated, so verify bit equality.
146187
if actual.to_bits() == expected.to_bits() {
147188
return SKIP;
@@ -158,9 +199,10 @@ impl MaybeOverride<(f32, f32)> for SpecialCase {
158199
_ulp: &mut u32,
159200
ctx: &CheckCtx,
160201
) -> Option<TestResult> {
161-
maybe_skip_min_max_nan(input, expected, ctx)
202+
maybe_skip_binop_nan(input, expected, ctx)
162203
}
163204
}
205+
164206
impl MaybeOverride<(f64, f64)> for SpecialCase {
165207
fn check_float<F: Float>(
166208
input: (f64, f64),
@@ -169,47 +211,86 @@ impl MaybeOverride<(f64, f64)> for SpecialCase {
169211
_ulp: &mut u32,
170212
ctx: &CheckCtx,
171213
) -> Option<TestResult> {
172-
maybe_skip_min_max_nan(input, expected, ctx)
214+
maybe_skip_binop_nan(input, expected, ctx)
173215
}
174216
}
175217

176218
/// Musl propagates NaNs if one is provided as the input, but we return the other input.
177219
// F1 and F2 are always the same type, this is just to please generics
178-
fn maybe_skip_min_max_nan<F1: Float, F2: Float>(
220+
fn maybe_skip_binop_nan<F1: Float, F2: Float>(
179221
input: (F1, F1),
180222
expected: F2,
181223
ctx: &CheckCtx,
182224
) -> Option<TestResult> {
183-
if (ctx.canonical_name == "fmax" || ctx.canonical_name == "fmin")
184-
&& (input.0.is_nan() || input.1.is_nan())
185-
&& expected.is_nan()
186-
{
187-
return XFAIL;
188-
} else {
189-
None
225+
match ctx.basis {
226+
CheckBasis::Musl => {
227+
if (ctx.canonical_name == "fmax" || ctx.canonical_name == "fmin")
228+
&& (input.0.is_nan() || input.1.is_nan())
229+
&& expected.is_nan()
230+
{
231+
XFAIL
232+
} else {
233+
None
234+
}
235+
}
236+
CheckBasis::Mpfr => {
237+
if ctx.canonical_name == "copysign" && input.1.is_nan() {
238+
SKIP
239+
} else {
240+
None
241+
}
242+
}
190243
}
191244
}
192245

193246
impl MaybeOverride<(i32, f32)> for SpecialCase {
194247
fn check_float<F: Float>(
195248
input: (i32, f32),
196-
_actual: F,
197-
_expected: F,
249+
actual: F,
250+
expected: F,
198251
ulp: &mut u32,
199252
ctx: &CheckCtx,
200253
) -> Option<TestResult> {
201-
bessel_prec_dropoff(input, ulp, ctx)
254+
match ctx.basis {
255+
CheckBasis::Musl => bessel_prec_dropoff(input, ulp, ctx),
256+
CheckBasis::Mpfr => {
257+
// We return +0.0, MPFR returns -0.0
258+
if ctx.fname == "jnf"
259+
&& input.1 == f32::NEG_INFINITY
260+
&& actual == F::ZERO
261+
&& expected == F::ZERO
262+
{
263+
XFAIL
264+
} else {
265+
None
266+
}
267+
}
268+
}
202269
}
203270
}
204271
impl MaybeOverride<(i32, f64)> for SpecialCase {
205272
fn check_float<F: Float>(
206273
input: (i32, f64),
207-
_actual: F,
208-
_expected: F,
274+
actual: F,
275+
expected: F,
209276
ulp: &mut u32,
210277
ctx: &CheckCtx,
211278
) -> Option<TestResult> {
212-
bessel_prec_dropoff(input, ulp, ctx)
279+
match ctx.basis {
280+
CheckBasis::Musl => bessel_prec_dropoff(input, ulp, ctx),
281+
CheckBasis::Mpfr => {
282+
// We return +0.0, MPFR returns -0.0
283+
if ctx.fname == "jn"
284+
&& input.1 == f64::NEG_INFINITY
285+
&& actual == F::ZERO
286+
&& expected == F::ZERO
287+
{
288+
XFAIL
289+
} else {
290+
bessel_prec_dropoff(input, ulp, ctx)
291+
}
292+
}
293+
}
213294
}
214295
}
215296

crates/libm-test/src/test_traits.rs

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ impl CheckCtx {
5252
pub enum CheckBasis {
5353
/// Check against Musl's math sources.
5454
Musl,
55+
/// Check against infinite precision (MPFR).
56+
Mpfr,
5557
}
5658

5759
/// A trait to implement on any output type so we can verify it in a generic way.

crates/libm-test/tests/compare_built_musl.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ macro_rules! musl_rand_tests {
2929
fn [< musl_random_ $fn_name >]() {
3030
let fname = stringify!($fn_name);
3131
let ulp = musl_allowed_ulp(fname);
32-
let cases = random::get_test_cases::<$RustArgs>(fname);
3332
let ctx = CheckCtx::new(ulp, fname, CheckBasis::Musl);
33+
let cases = random::get_test_cases::<$RustArgs>(&ctx);
3434

3535
for input in cases {
3636
let musl_res = input.call(musl::$fn_name as $CFn);

0 commit comments

Comments
 (0)