Skip to content

Commit ad6a083

Browse files
committed
Add a test against musl libm
Check our functions against `musl-math-sys`. This is similar to the existing musl tests that go through binary serialization, but works on more platforms.
1 parent 50919ef commit ad6a083

File tree

5 files changed

+247
-16
lines changed

5 files changed

+247
-16
lines changed

libm/crates/libm-test/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ default = []
1111
# musl libc.
1212
test-musl-serialized = ["rand"]
1313

14+
# Build our own musl for testing and benchmarks
15+
build-musl = ["dep:musl-math-sys"]
16+
1417
[dependencies]
1518
anyhow = "1.0.90"
1619
libm = { path = "../.." }
1720
libm-macros = { path = "../libm-macros" }
21+
musl-math-sys = { path = "../musl-math-sys", optional = true }
22+
paste = "1.0.15"
1823
rand = "0.8.5"
1924
rand_chacha = "0.3.1"
2025

libm/crates/libm-test/src/lib.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@ pub type TestResult<T = (), E = anyhow::Error> = Result<T, E>;
1414
// List of all files present in libm's source
1515
include!(concat!(env!("OUT_DIR"), "/all_files.rs"));
1616

17+
/// ULP allowed to differ from musl (note that musl itself may not be accurate).
18+
const MUSL_DEFAULT_ULP: u32 = 2;
19+
20+
/// Certain functions have different allowed ULP (consider these xfail).
21+
///
22+
/// Note that these results were obtained using 400,000,000 rounds of random inputs, which
23+
/// is not a value used by default.
24+
pub fn musl_allowed_ulp(name: &str) -> u32 {
25+
match name {
26+
#[cfg(x86_no_sse)]
27+
"asinh" | "asinhf" => 6,
28+
"lgamma" | "lgamma_r" | "lgammaf" | "lgammaf_r" => 400,
29+
"tanh" | "tanhf" => 4,
30+
"tgamma" => 20,
31+
"j0" | "j0f" | "j1" | "j1f" => {
32+
// Results seem very target-dependent
33+
if cfg!(target_arch = "x86_64") { 4000 } else { 800_000 }
34+
}
35+
"jn" | "jnf" => 1000,
36+
"sincosf" => 500,
37+
#[cfg(not(target_pointer_width = "64"))]
38+
"exp10" => 4,
39+
#[cfg(not(target_pointer_width = "64"))]
40+
"exp10f" => 4,
41+
_ => MUSL_DEFAULT_ULP,
42+
}
43+
}
44+
1745
/// Return the unsuffixed version of a function name; e.g. `abs` and `absf` both return `abs`,
1846
/// `lgamma_r` and `lgammaf_r` both return `lgamma_r`.
1947
pub fn canonical_name(name: &str) -> &str {

libm/crates/libm-test/src/special_case.rs

Lines changed: 157 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! Configuration for skipping or changing the result for individual test cases (inputs) rather
22
//! than ignoring entire tests.
33
4-
use crate::{CheckCtx, Float, Int, TestResult};
4+
use core::f32;
5+
6+
use crate::{CheckBasis, CheckCtx, Float, Int, TestResult};
57

68
/// Type implementing [`IgnoreCase`].
79
pub struct SpecialCase;
@@ -49,47 +51,189 @@ pub trait MaybeOverride<Input> {
4951

5052
impl MaybeOverride<(f32,)> for SpecialCase {
5153
fn check_float<F: Float>(
52-
_input: (f32,),
54+
input: (f32,),
5355
actual: F,
5456
expected: F,
5557
_ulp: &mut u32,
5658
ctx: &CheckCtx,
5759
) -> Option<TestResult> {
60+
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+
75+
if ctx.fname == "expm1f" && input.0 > 80.0 && actual.is_infinite() {
76+
// we return infinity but the number is representable
77+
return XFAIL;
78+
}
79+
80+
if ctx.fname == "sinhf" && input.0.abs() > 80.0 && actual.is_nan() {
81+
// we return some NaN that should be real values or infinite
82+
// doesn't seem to happen on x86
83+
return XFAIL;
84+
}
85+
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+
}
90+
}
91+
5892
maybe_check_nan_bits(actual, expected, ctx)
5993
}
6094
}
6195

6296
impl MaybeOverride<(f64,)> for SpecialCase {
6397
fn check_float<F: Float>(
64-
_input: (f64,),
98+
input: (f64,),
6599
actual: F,
66100
expected: F,
67101
_ulp: &mut u32,
68102
ctx: &CheckCtx,
69103
) -> Option<TestResult> {
104+
if ctx.basis == CheckBasis::Musl {
105+
if cfg!(target_arch = "x86") && ctx.fname == "acosh" && input.0 < 1.0 {
106+
// The function is undefined, both implementations return random results
107+
return SKIP;
108+
}
109+
110+
if cfg!(x86_no_sse)
111+
&& ctx.fname == "ceil"
112+
&& input.0 < 0.0
113+
&& input.0 > -1.0
114+
&& expected == F::ZERO
115+
&& actual == F::ZERO
116+
{
117+
// musl returns -0.0, we return +0.0
118+
return XFAIL;
119+
}
120+
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+
}
125+
}
126+
70127
maybe_check_nan_bits(actual, expected, ctx)
71128
}
72129
}
73130

74-
impl MaybeOverride<(f32, f32)> for SpecialCase {}
75-
impl MaybeOverride<(f64, f64)> for SpecialCase {}
76-
impl MaybeOverride<(f32, f32, f32)> for SpecialCase {}
77-
impl MaybeOverride<(f64, f64, f64)> for SpecialCase {}
78-
impl MaybeOverride<(i32, f32)> for SpecialCase {}
79-
impl MaybeOverride<(i32, f64)> for SpecialCase {}
80-
impl MaybeOverride<(f32, i32)> for SpecialCase {}
81-
impl MaybeOverride<(f64, i32)> for SpecialCase {}
82-
83131
/// Check NaN bits if the function requires it
84132
fn maybe_check_nan_bits<F: Float>(actual: F, expected: F, ctx: &CheckCtx) -> Option<TestResult> {
85-
if !(ctx.canonical_name == "abs" || ctx.canonical_name == "copysigh") {
133+
if !(ctx.canonical_name == "fabs" || ctx.canonical_name == "copysign") {
86134
return None;
87135
}
88136

137+
// LLVM currently uses x87 instructions which quieten signalling NaNs to handle the i686
138+
// `extern "C"` `f32`/`f64` return ABI.
139+
// LLVM issue <https://github.com/llvm/llvm-project/issues/66803>
140+
// Rust issue <https://github.com/rust-lang/rust/issues/115567>
141+
if cfg!(target_arch = "x86") && ctx.basis == CheckBasis::Musl {
142+
return SKIP;
143+
}
144+
89145
// abs and copysign require signaling NaNs to be propagated, so verify bit equality.
90146
if actual.to_bits() == expected.to_bits() {
91147
return SKIP;
92148
} else {
93149
Some(Err(anyhow::anyhow!("NaNs have different bitpatterns")))
94150
}
95151
}
152+
153+
impl MaybeOverride<(f32, f32)> for SpecialCase {
154+
fn check_float<F: Float>(
155+
input: (f32, f32),
156+
_actual: F,
157+
expected: F,
158+
_ulp: &mut u32,
159+
ctx: &CheckCtx,
160+
) -> Option<TestResult> {
161+
maybe_skip_min_max_nan(input, expected, ctx)
162+
}
163+
}
164+
impl MaybeOverride<(f64, f64)> for SpecialCase {
165+
fn check_float<F: Float>(
166+
input: (f64, f64),
167+
_actual: F,
168+
expected: F,
169+
_ulp: &mut u32,
170+
ctx: &CheckCtx,
171+
) -> Option<TestResult> {
172+
maybe_skip_min_max_nan(input, expected, ctx)
173+
}
174+
}
175+
176+
/// Musl propagates NaNs if one is provided as the input, but we return the other input.
177+
// 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>(
179+
input: (F1, F1),
180+
expected: F2,
181+
ctx: &CheckCtx,
182+
) -> 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
190+
}
191+
}
192+
193+
impl MaybeOverride<(i32, f32)> for SpecialCase {
194+
fn check_float<F: Float>(
195+
input: (i32, f32),
196+
_actual: F,
197+
_expected: F,
198+
ulp: &mut u32,
199+
ctx: &CheckCtx,
200+
) -> Option<TestResult> {
201+
bessel_prec_dropoff(input, ulp, ctx)
202+
}
203+
}
204+
impl MaybeOverride<(i32, f64)> for SpecialCase {
205+
fn check_float<F: Float>(
206+
input: (i32, f64),
207+
_actual: F,
208+
_expected: F,
209+
ulp: &mut u32,
210+
ctx: &CheckCtx,
211+
) -> Option<TestResult> {
212+
bessel_prec_dropoff(input, ulp, ctx)
213+
}
214+
}
215+
216+
/// Our bessel functions blow up with large N values
217+
fn bessel_prec_dropoff<F: Float>(
218+
input: (i32, F),
219+
ulp: &mut u32,
220+
ctx: &CheckCtx,
221+
) -> Option<TestResult> {
222+
if ctx.canonical_name == "jn" {
223+
if input.0 > 4000 {
224+
return XFAIL;
225+
} else if input.0 > 2000 {
226+
// *ulp = 20_000;
227+
*ulp = 20000;
228+
} else if input.0 > 1000 {
229+
*ulp = 4000;
230+
}
231+
}
232+
233+
None
234+
}
235+
236+
impl MaybeOverride<(f32, f32, f32)> for SpecialCase {}
237+
impl MaybeOverride<(f64, f64, f64)> for SpecialCase {}
238+
impl MaybeOverride<(f32, i32)> for SpecialCase {}
239+
impl MaybeOverride<(f64, i32)> for SpecialCase {}

libm/crates/libm-test/src/test_traits.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ impl CheckCtx {
4949

5050
/// Possible items to test against
5151
#[derive(Clone, Debug, PartialEq, Eq)]
52-
pub enum CheckBasis {}
52+
pub enum CheckBasis {
53+
/// Check against Musl's math sources.
54+
Musl,
55+
}
5356

5457
/// A trait to implement on any output type so we can verify it in a generic way.
5558
pub trait CheckOutput<Input>: Sized {
@@ -160,8 +163,7 @@ where
160163

161164
// Check when both are NaNs
162165
if self.is_nan() && expected.is_nan() {
163-
ensure!(self.to_bits() == expected.to_bits(), "NaNs have different bitpatterns");
164-
// Nothing else to check
166+
// By default, NaNs have nothing special to check.
165167
return Ok(());
166168
} else if self.is_nan() || expected.is_nan() {
167169
// Check when only one is a NaN
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! Compare our implementations with the result of musl functions, as provided by `musl-math-sys`.
2+
//!
3+
//! Currently this only tests randomized inputs. In the future this may be improved to test edge
4+
//! cases or run exhaustive tests.
5+
//!
6+
//! Note that musl functions do not always provide 0.5ULP rounding, so our functions can do better
7+
//! than these results.
8+
9+
// There are some targets we can't build musl for
10+
#![cfg(feature = "build-musl")]
11+
12+
use libm_test::gen::random;
13+
use libm_test::{CheckBasis, CheckCtx, CheckOutput, TupleCall, musl_allowed_ulp};
14+
use musl_math_sys as musl;
15+
16+
macro_rules! musl_rand_tests {
17+
(
18+
fn_name: $fn_name:ident,
19+
CFn: $CFn:ty,
20+
CArgs: $CArgs:ty,
21+
CRet: $CRet:ty,
22+
RustFn: $RustFn:ty,
23+
RustArgs: $RustArgs:ty,
24+
RustRet: $RustRet:ty,
25+
attrs: [$($meta:meta)*]
26+
) => { paste::paste! {
27+
#[test]
28+
$(#[$meta])*
29+
fn [< musl_random_ $fn_name >]() {
30+
let fname = stringify!($fn_name);
31+
let ulp = musl_allowed_ulp(fname);
32+
let cases = random::get_test_cases::<$RustArgs>(fname);
33+
let ctx = CheckCtx::new(ulp, fname, CheckBasis::Musl);
34+
35+
for input in cases {
36+
let musl_res = input.call(musl::$fn_name as $CFn);
37+
let crate_res = input.call(libm::$fn_name as $RustFn);
38+
39+
crate_res.validate(musl_res, input, &ctx).unwrap();
40+
}
41+
}
42+
} };
43+
}
44+
45+
libm_macros::for_each_function! {
46+
callback: musl_rand_tests,
47+
skip: [],
48+
attributes: [
49+
#[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586
50+
[exp10, exp10f, exp2, exp2f, rint]
51+
],
52+
}

0 commit comments

Comments
 (0)