Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit f36264a

Browse files
committed
Add a test against MPFR using random inputs
1 parent a81a68f commit f36264a

File tree

7 files changed

+235
-47
lines changed

7 files changed

+235
-47
lines changed

crates/libm-test/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ build-musl = ["dep:musl-math-sys"]
1818
[dependencies]
1919
anyhow = "1.0.90"
2020
az = { version = "1.2.1", optional = true }
21+
either = "1.13.0"
2122
libm = { path = "../.." }
2223
libm-macros = { path = "../libm-macros" }
2324
musl-math-sys = { path = "../musl-math-sys", optional = true }

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
44
use std::sync::LazyLock;
55

6+
use either::Either;
67
use rand::{Rng, SeedableRng};
78
use rand_chacha::ChaCha8Rng;
89

910
use super::CachedInput;
10-
use crate::GenerateInput;
11+
use crate::{CheckBasis, CheckCtx, GenerateInput};
1112

1213
const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
1314

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

5253
// These functions are extremely slow, limit them
53-
cases.inputs_i32.truncate((NTESTS / 1000).max(80));
54-
cases.inputs_f32.truncate((NTESTS / 1000).max(80));
55-
cases.inputs_f64.truncate((NTESTS / 1000).max(80));
54+
let ntests_jn = (NTESTS / 1000).max(80);
55+
cases.inputs_i32.truncate(ntests_jn);
56+
cases.inputs_f32.truncate(ntests_jn);
57+
cases.inputs_f64.truncate(ntests_jn);
5658

5759
// It is easy to overflow the stack with these in debug mode
5860
let max_iterations = if cfg!(optimizations_enabled) && cfg!(target_pointer_width = "64") {
@@ -115,11 +117,17 @@ fn make_test_cases(ntests: usize) -> CachedInput {
115117
}
116118

117119
/// Create a test case iterator.
118-
pub fn get_test_cases<RustArgs>(fname: &str) -> impl Iterator<Item = RustArgs>
120+
pub fn get_test_cases<RustArgs>(ctx: &CheckCtx) -> impl Iterator<Item = RustArgs>
119121
where
120122
CachedInput: GenerateInput<RustArgs>,
121123
{
122-
let inputs = if fname == "jn" || fname == "jnf" { &TEST_CASES_JN } else { &TEST_CASES };
123-
124-
CachedInput::get_cases(inputs)
124+
// jn is extremely slow on MPFR, we need to reduce it further
125+
let ntests_jn_multi = (NTESTS / 5000).max(20);
126+
127+
match (&ctx.basis, ctx.fname) {
128+
(CheckBasis::Musl, "jnf") => Either::Left(TEST_CASES_JN.get_cases()),
129+
(_, "jn") => Either::Left(TEST_CASES_JN.get_cases()),
130+
(CheckBasis::Mpfr, "jnf") => Either::Right(TEST_CASES_JN.get_cases().take(ntests_jn_multi)),
131+
_ => Either::Left(TEST_CASES.get_cases()),
132+
}
125133
}

crates/libm-test/src/lib.rs

Lines changed: 28 additions & 2 deletions
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,28 @@ 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+
"sincosf" => 500,
67+
"tanh" | "tanhf" => 2,
68+
"tgamma" => 20,
69+
_ => MULTIPREC_DEFAULT_ULP,
70+
}
71+
}
72+
4773
/// Return the unsuffixed version of a function name; e.g. `abs` and `absf` both return `abs`,
4874
/// `lgamma_r` and `lgammaf_r` both return `lgamma_r`.
4975
pub fn canonical_name(name: &str) -> &str {

crates/libm-test/src/special_case.rs

Lines changed: 125 additions & 36 deletions
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,48 @@ 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
73+
if ctx.fname == "sincosf" {
74+
let factor_frac_pi_2 = input.0.abs() / f32::consts::FRAC_PI_2;
75+
if (factor_frac_pi_2 - factor_frac_pi_2.round()).abs() < 1e-2 {
76+
// we have a bad approximation near multiples of pi/2
8877
return XFAIL;
8978
}
9079
}
9180

81+
if ctx.fname == "acoshf" && input.0 < -1.0 {
82+
// acoshf is undefined for x <= 1.0, but we return a random result at lower
83+
// values.
84+
return XFAIL;
85+
}
86+
87+
if ctx.fname == "lgammaf" || ctx.fname == "lgammaf_r" && input.0 < 0.0 {
88+
// loggamma should not be defined for x < 0, yet we both return results
89+
return XFAIL;
90+
}
91+
9292
maybe_check_nan_bits(actual, expected, ctx)
9393
}
94+
95+
fn check_int<I: Int>(
96+
input: (f32,),
97+
actual: I,
98+
expected: I,
99+
ctx: &CheckCtx,
100+
) -> Option<anyhow::Result<()>> {
101+
// On MPFR for lgammaf_r, we set -1 as the integer result for negative infinity but MPFR
102+
// sets +1
103+
if ctx.basis == CheckBasis::Mpfr
104+
&& ctx.fname == "lgammaf_r"
105+
&& input.0 == f32::NEG_INFINITY
106+
&& actual.abs() == expected.abs()
107+
{
108+
XFAIL
109+
} else {
110+
None
111+
}
112+
}
94113
}
95114

96115
impl MaybeOverride<(f64,)> for SpecialCase {
@@ -117,15 +136,40 @@ impl MaybeOverride<(f64,)> for SpecialCase {
117136
// musl returns -0.0, we return +0.0
118137
return XFAIL;
119138
}
139+
}
120140

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-
}
141+
if ctx.fname == "acosh" && input.0 < 1.0 {
142+
// The function is undefined for the inputs, musl and our libm both return
143+
// random results.
144+
return XFAIL;
145+
}
146+
147+
if ctx.fname == "lgamma" || ctx.fname == "lgamma_r" && input.0 < 0.0 {
148+
// loggamma should not be defined for x < 0, yet we both return results
149+
return XFAIL;
125150
}
126151

127152
maybe_check_nan_bits(actual, expected, ctx)
128153
}
154+
155+
fn check_int<I: Int>(
156+
input: (f64,),
157+
actual: I,
158+
expected: I,
159+
ctx: &CheckCtx,
160+
) -> Option<anyhow::Result<()>> {
161+
// On MPFR for lgamma_r, we set -1 as the integer result for negative infinity but MPFR
162+
// sets +1
163+
if ctx.basis == CheckBasis::Mpfr
164+
&& ctx.fname == "lgamma_r"
165+
&& input.0 == f64::NEG_INFINITY
166+
&& actual.abs() == expected.abs()
167+
{
168+
XFAIL
169+
} else {
170+
None
171+
}
172+
}
129173
}
130174

131175
/// Check NaN bits if the function requires it
@@ -138,6 +182,12 @@ fn maybe_check_nan_bits<F: Float>(actual: F, expected: F, ctx: &CheckCtx) -> Opt
138182
if cfg!(target_arch = "x86") && ctx.basis == CheckBasis::Musl && ctx.canonical_name == "fabs" {
139183
return SKIP;
140184
}
185+
186+
// MPFR only has one NaN bitpattern; allow the default `.is_nan()` checks to validate.
187+
if ctx.basis == CheckBasis::Mpfr {
188+
return SKIP;
189+
}
190+
141191
// abs and copysign require signaling NaNs to be propagated, so verify bit equality.
142192
if actual.to_bits() == expected.to_bits() {
143193
return SKIP;
@@ -154,7 +204,7 @@ impl MaybeOverride<(f32, f32)> for SpecialCase {
154204
_ulp: &mut u32,
155205
ctx: &CheckCtx,
156206
) -> Option<TestResult> {
157-
maybe_skip_min_max_nan(input, expected, ctx)
207+
maybe_skip_binop_nan(input, expected, ctx)
158208
}
159209
}
160210
impl MaybeOverride<(f64, f64)> for SpecialCase {
@@ -165,47 +215,86 @@ impl MaybeOverride<(f64, f64)> for SpecialCase {
165215
_ulp: &mut u32,
166216
ctx: &CheckCtx,
167217
) -> Option<TestResult> {
168-
maybe_skip_min_max_nan(input, expected, ctx)
218+
maybe_skip_binop_nan(input, expected, ctx)
169219
}
170220
}
171221

172222
/// Musl propagates NaNs if one is provided as the input, but we return the other input.
173223
// F1 and F2 are always the same type, this is just to please generics
174-
fn maybe_skip_min_max_nan<F1: Float, F2: Float>(
224+
fn maybe_skip_binop_nan<F1: Float, F2: Float>(
175225
input: (F1, F1),
176226
expected: F2,
177227
ctx: &CheckCtx,
178228
) -> Option<TestResult> {
179-
if (ctx.canonical_name == "fmax" || ctx.canonical_name == "fmin")
180-
&& (input.0.is_nan() || input.1.is_nan())
181-
&& expected.is_nan()
182-
{
183-
return XFAIL;
184-
} else {
185-
None
229+
match ctx.basis {
230+
CheckBasis::Musl => {
231+
if (ctx.canonical_name == "fmax" || ctx.canonical_name == "fmin")
232+
&& (input.0.is_nan() || input.1.is_nan())
233+
&& expected.is_nan()
234+
{
235+
XFAIL
236+
} else {
237+
None
238+
}
239+
}
240+
CheckBasis::Mpfr => {
241+
if ctx.canonical_name == "copysign" && input.1.is_nan() {
242+
SKIP
243+
} else {
244+
None
245+
}
246+
}
186247
}
187248
}
188249

189250
impl MaybeOverride<(i32, f32)> for SpecialCase {
190251
fn check_float<F: Float>(
191252
input: (i32, f32),
192-
_actual: F,
193-
_expected: F,
253+
actual: F,
254+
expected: F,
194255
ulp: &mut u32,
195256
ctx: &CheckCtx,
196257
) -> Option<TestResult> {
197-
bessel_prec_dropoff(input, ulp, ctx)
258+
match ctx.basis {
259+
CheckBasis::Musl => bessel_prec_dropoff(input, ulp, ctx),
260+
CheckBasis::Mpfr => {
261+
// We return +0.0, MPFR returns -0.0
262+
if ctx.fname == "jnf"
263+
&& input.1 == f32::NEG_INFINITY
264+
&& actual == F::ZERO
265+
&& expected == F::ZERO
266+
{
267+
XFAIL
268+
} else {
269+
None
270+
}
271+
}
272+
}
198273
}
199274
}
200275
impl MaybeOverride<(i32, f64)> for SpecialCase {
201276
fn check_float<F: Float>(
202277
input: (i32, f64),
203-
_actual: F,
204-
_expected: F,
278+
actual: F,
279+
expected: F,
205280
ulp: &mut u32,
206281
ctx: &CheckCtx,
207282
) -> Option<TestResult> {
208-
bessel_prec_dropoff(input, ulp, ctx)
283+
match ctx.basis {
284+
CheckBasis::Musl => bessel_prec_dropoff(input, ulp, ctx),
285+
CheckBasis::Mpfr => {
286+
// We return +0.0, MPFR returns -0.0
287+
if ctx.fname == "jn"
288+
&& input.1 == f64::NEG_INFINITY
289+
&& actual == F::ZERO
290+
&& expected == F::ZERO
291+
{
292+
XFAIL
293+
} else {
294+
None
295+
}
296+
}
297+
}
209298
}
210299
}
211300

crates/libm-test/src/test_traits.rs

Lines changed: 2 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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)