diff --git a/Cargo.toml b/Cargo.toml index 2f68ecc0..743b5004 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,6 @@ edition = "2018" default = ['stable'] stable = [] -# Generate tests which are random inputs and the outputs are calculated with -# musl libc. -musl-reference-tests = ['rand'] - # Used checked array indexing instead of unchecked array indexing in this # library. checked = [] @@ -26,12 +22,10 @@ checked = [] [workspace] members = [ "crates/compiler-builtins-smoke-test", + "crates/libm-test" ] [dev-dependencies] no-panic = "0.1.8" rand = "0.6.5" paste = "0.1.5" - -[build-dependencies] -rand = { version = "0.6.5", optional = true } diff --git a/build.rs b/build.rs index 9af6dec9..80837c5b 100644 --- a/build.rs +++ b/build.rs @@ -3,9 +3,6 @@ use std::env; fn main() { println!("cargo:rerun-if-changed=build.rs"); - #[cfg(feature = "musl-reference-tests")] - musl_reference_tests::generate(); - if !cfg!(feature = "checked") { let lvl = env::var("OPT_LEVEL").unwrap(); if lvl != "0" { @@ -13,432 +10,3 @@ fn main() { } } } - -#[cfg(feature = "musl-reference-tests")] -mod musl_reference_tests { - use rand::seq::SliceRandom; - use rand::Rng; - use std::fs; - use std::process::Command; - - // Number of tests to generate for each function - const NTESTS: usize = 500; - - // These files are all internal functions or otherwise miscellaneous, not - // defining a function we want to test. - const IGNORED_FILES: &[&str] = &["fenv.rs"]; - - struct Function { - name: String, - args: Vec, - ret: Vec, - tests: Vec, - } - - enum Ty { - F32, - F64, - I32, - Bool, - } - - struct Test { - inputs: Vec, - outputs: Vec, - } - - pub fn generate() { - let files = fs::read_dir("src/math") - .unwrap() - .map(|f| f.unwrap().path()) - .collect::>(); - - let mut math = Vec::new(); - for file in files { - if IGNORED_FILES.iter().any(|f| file.ends_with(f)) { - continue; - } - - println!("generating musl reference tests in {:?}", file); - - let contents = fs::read_to_string(file).unwrap(); - let mut functions = contents.lines().filter(|f| f.starts_with("pub fn")); - while let Some(function_to_test) = functions.next() { - math.push(parse(function_to_test)); - } - } - - // Generate a bunch of random inputs for each function. This will - // attempt to generate a good set of uniform test cases for exercising - // all the various functionality. - generate_random_tests(&mut math, &mut rand::thread_rng()); - - // After we have all our inputs, use the x86_64-unknown-linux-musl - // target to generate the expected output. - generate_test_outputs(&mut math); - //panic!("Boo"); - // ... and now that we have both inputs and expected outputs, do a bunch - // of codegen to create the unit tests which we'll actually execute. - generate_unit_tests(&math); - } - - /// A "poor man's" parser for the signature of a function - fn parse(s: &str) -> Function { - let s = eat(s, "pub fn "); - let pos = s.find('(').unwrap(); - let name = &s[..pos]; - let s = &s[pos + 1..]; - let end = s.find(')').unwrap(); - let args = s[..end] - .split(',') - .map(|arg| { - let colon = arg.find(':').unwrap(); - parse_ty(arg[colon + 1..].trim()) - }) - .collect::>(); - let tail = &s[end + 1..]; - let tail = eat(tail, " -> "); - let ret = parse_retty(tail.replace("{", "").trim()); - - return Function { - name: name.to_string(), - args, - ret, - tests: Vec::new(), - }; - - fn parse_ty(s: &str) -> Ty { - match s { - "f32" => Ty::F32, - "f64" => Ty::F64, - "i32" => Ty::I32, - "bool" => Ty::Bool, - other => panic!("unknown type `{}`", other), - } - } - - fn parse_retty(s: &str) -> Vec { - match s { - "(f32, f32)" => vec![Ty::F32, Ty::F32], - "(f32, i32)" => vec![Ty::F32, Ty::I32], - "(f64, f64)" => vec![Ty::F64, Ty::F64], - "(f64, i32)" => vec![Ty::F64, Ty::I32], - other => vec![parse_ty(other)], - } - } - - fn eat<'a>(s: &'a str, prefix: &str) -> &'a str { - if s.starts_with(prefix) { - &s[prefix.len()..] - } else { - panic!("{:?} didn't start with {:?}", s, prefix) - } - } - } - - fn generate_random_tests(functions: &mut [Function], rng: &mut R) { - for function in functions { - for _ in 0..NTESTS { - function.tests.push(generate_test(function, rng)); - } - } - - fn generate_test(function: &Function, rng: &mut R) -> Test { - let mut inputs = function - .args - .iter() - .map(|ty| ty.gen_i64(rng)) - .collect::>(); - - // First argument to this function appears to be a number of - // iterations, so passing in massive random numbers causes it to - // take forever to execute, so make sure we're not running random - // math code until the heat death of the universe. - if function.name == "jn" || function.name == "jnf" { - inputs[0] &= 0xffff; - } - - Test { - inputs, - // zero output for now since we'll generate it later - outputs: vec![], - } - } - } - - impl Ty { - fn gen_i64(&self, r: &mut R) -> i64 { - use std::f32; - use std::f64; - - return match self { - Ty::F32 => { - if r.gen_range(0, 20) < 1 { - let i = *[f32::NAN, f32::INFINITY, f32::NEG_INFINITY] - .choose(r) - .unwrap(); - i.to_bits().into() - } else { - r.gen::().to_bits().into() - } - } - Ty::F64 => { - if r.gen_range(0, 20) < 1 { - let i = *[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] - .choose(r) - .unwrap(); - i.to_bits() as i64 - } else { - r.gen::().to_bits() as i64 - } - } - Ty::I32 => { - if r.gen_range(0, 10) < 1 { - let i = *[i32::max_value(), 0, i32::min_value()].choose(r).unwrap(); - i.into() - } else { - r.gen::().into() - } - } - Ty::Bool => r.gen::() as i64, - }; - } - - fn libc_ty(&self) -> &'static str { - match self { - Ty::F32 => "f32", - Ty::F64 => "f64", - Ty::I32 => "i32", - Ty::Bool => "i32", - } - } - - fn libc_pty(&self) -> &'static str { - match self { - Ty::F32 => "*mut f32", - Ty::F64 => "*mut f64", - Ty::I32 => "*mut i32", - Ty::Bool => "*mut i32", - } - } - - fn default(&self) -> &'static str { - match self { - Ty::F32 => "0_f32", - Ty::F64 => "0_f64", - Ty::I32 => "0_i32", - Ty::Bool => "false", - } - } - - fn to_i64(&self) -> &'static str { - match self { - Ty::F32 => ".to_bits() as i64", - Ty::F64 => ".to_bits() as i64", - Ty::I32 => " as i64", - Ty::Bool => " as i64", - } - } - } - - fn generate_test_outputs(functions: &mut [Function]) { - let mut src = String::new(); - let dst = std::env::var("OUT_DIR").unwrap(); - - // Generate a program which will run all tests with all inputs in - // `functions`. This program will write all outputs to stdout (in a - // binary format). - src.push_str("use std::io::Write;"); - src.push_str("fn main() {"); - src.push_str("let mut result = Vec::new();"); - for function in functions.iter_mut() { - src.push_str("unsafe {"); - src.push_str("extern { fn "); - src.push_str(&function.name); - src.push_str("("); - - let (ret, retptr) = match function.name.as_str() { - "sincos" | "sincosf" => (None, &function.ret[..]), - _ => (Some(&function.ret[0]), &function.ret[1..]), - }; - for (i, arg) in function.args.iter().enumerate() { - src.push_str(&format!("arg{}: {},", i, arg.libc_ty())); - } - for (i, ret) in retptr.iter().enumerate() { - src.push_str(&format!("argret{}: {},", i, ret.libc_pty())); - } - src.push_str(")"); - if let Some(ty) = ret { - src.push_str(" -> "); - src.push_str(ty.libc_ty()); - } - src.push_str("; }"); - - src.push_str(&format!("static TESTS: &[[i64; {}]]", function.args.len())); - src.push_str(" = &["); - for test in function.tests.iter() { - src.push_str("["); - for val in test.inputs.iter() { - src.push_str(&val.to_string()); - src.push_str(","); - } - src.push_str("],"); - } - src.push_str("];"); - - src.push_str("for test in TESTS {"); - for (i, arg) in retptr.iter().enumerate() { - src.push_str(&format!("let mut argret{} = {};", i, arg.default())); - } - src.push_str("let output = "); - src.push_str(&function.name); - src.push_str("("); - for (i, arg) in function.args.iter().enumerate() { - src.push_str(&match arg { - Ty::F32 => format!("f32::from_bits(test[{}] as u32)", i), - Ty::F64 => format!("f64::from_bits(test[{}] as u64)", i), - Ty::I32 => format!("test[{}] as i32", i), - Ty::Bool => format!("test[{}] as i32", i), - }); - src.push_str(","); - } - for (i, _) in retptr.iter().enumerate() { - src.push_str(&format!("&mut argret{},", i)); - } - src.push_str(");"); - if let Some(ty) = &ret { - src.push_str(&format!("let output = output{};", ty.to_i64())); - src.push_str("result.extend_from_slice(&output.to_le_bytes());"); - } - - for (i, ret) in retptr.iter().enumerate() { - src.push_str(&format!( - "result.extend_from_slice(&(argret{}{}).to_le_bytes());", - i, - ret.to_i64(), - )); - } - src.push_str("}"); - - src.push_str("}"); - } - - src.push_str("std::io::stdout().write_all(&result).unwrap();"); - - src.push_str("}"); - - let path = format!("{}/gen.rs", dst); - fs::write(&path, src).unwrap(); - - // Make it somewhat pretty if something goes wrong - drop(Command::new("rustfmt").arg(&path).status()); - - // Compile and execute this tests for the musl target, assuming we're an - // x86_64 host effectively. - let status = Command::new("rustc") - .current_dir(&dst) - .arg(&path) - .arg("--target=x86_64-unknown-linux-musl") - .status() - .unwrap(); - assert!(status.success()); - let output = Command::new("./gen").current_dir(&dst).output().unwrap(); - assert!(output.status.success()); - assert!(output.stderr.is_empty()); - - // Map all the output bytes back to an `i64` and then shove it all into - // the expected results. - let mut results = output.stdout.chunks_exact(8).map(|buf| { - let mut exact = [0; 8]; - exact.copy_from_slice(buf); - i64::from_le_bytes(exact) - }); - - for f in functions.iter_mut() { - for test in f.tests.iter_mut() { - test.outputs = (0..f.ret.len()).map(|_| results.next().unwrap()).collect(); - } - } - assert!(results.next().is_none()); - } - - /// Codegens a file which has a ton of `#[test]` annotations for all the - /// tests that we generated above. - fn generate_unit_tests(functions: &[Function]) { - let mut src = String::new(); - let dst = std::env::var("OUT_DIR").unwrap(); - - for function in functions { - src.push_str("#[test]"); - src.push_str("fn "); - src.push_str(&function.name); - src.push_str("_matches_musl() {"); - src.push_str(&format!( - "static TESTS: &[([i64; {}], [i64; {}])]", - function.args.len(), - function.ret.len(), - )); - src.push_str(" = &["); - for test in function.tests.iter() { - src.push_str("(["); - for val in test.inputs.iter() { - src.push_str(&val.to_string()); - src.push_str(","); - } - src.push_str("],"); - src.push_str("["); - for val in test.outputs.iter() { - src.push_str(&val.to_string()); - src.push_str(","); - } - src.push_str("],"); - src.push_str("),"); - } - src.push_str("];"); - - src.push_str("for (test, expected) in TESTS {"); - src.push_str("let output = "); - src.push_str(&function.name); - src.push_str("("); - for (i, arg) in function.args.iter().enumerate() { - src.push_str(&match arg { - Ty::F32 => format!("f32::from_bits(test[{}] as u32)", i), - Ty::F64 => format!("f64::from_bits(test[{}] as u64)", i), - Ty::I32 => format!("test[{}] as i32", i), - Ty::Bool => format!("test[{}] as i32", i), - }); - src.push_str(","); - } - src.push_str(");"); - - for (i, ret) in function.ret.iter().enumerate() { - let get = if function.ret.len() == 1 { - String::new() - } else { - format!(".{}", i) - }; - src.push_str(&(match ret { - Ty::F32 => format!("if _eqf(output{}, f32::from_bits(expected[{}] as u32)).is_ok() {{ continue }}", get, i), - Ty::F64 => format!("if _eq(output{}, f64::from_bits(expected[{}] as u64)).is_ok() {{ continue }}", get, i), - Ty::I32 => format!("if output{} as i64 == expected[{}] {{ continue }}", get, i), - Ty::Bool => unreachable!(), - })); - } - - src.push_str( - r#" - panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", test, expected, output); - "#, - ); - src.push_str("}"); - - src.push_str("}"); - } - - let path = format!("{}/musl-tests.rs", dst); - fs::write(&path, src).unwrap(); - - // Try to make it somewhat pretty - drop(Command::new("rustfmt").arg(&path).status()); - } -} diff --git a/ci/run.sh b/ci/run.sh index 42c24164..4ea9399f 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -6,6 +6,5 @@ TARGET=$1 cargo test --target $TARGET cargo test --target $TARGET --release -cargo test --features 'checked musl-reference-tests' --target $TARGET - -cargo test --features 'checked musl-reference-tests' --target $TARGET --release +cargo test -p libm-test --features 'checked' --target $TARGET +cargo test -p libm-test --features 'checked' --target $TARGET --release diff --git a/crates/libm-test/Cargo.toml b/crates/libm-test/Cargo.toml new file mode 100644 index 00000000..a2a47661 --- /dev/null +++ b/crates/libm-test/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "libm-test" +version = "0.1.0" +authors = ["Benjamin Schultzer "] + +[dependencies] +libm = { version = "0.*", path = "../../" } +paste = "0.1.5" +rand = "0.6.5" + +[features] +# Used checked array indexing instead of unchecked array indexing in this +# library. +checked = [] diff --git a/crates/libm-test/build.rs b/crates/libm-test/build.rs new file mode 100644 index 00000000..ac799cf1 --- /dev/null +++ b/crates/libm-test/build.rs @@ -0,0 +1,21 @@ +use std::env; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + if !cfg!(feature = "checked") { + let lvl = env::var("OPT_LEVEL").unwrap(); + if lvl != "0" { + println!("cargo:rustc-cfg=assert_no_panic"); + } + } + + let opt_level = env::var("OPT_LEVEL") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + let profile = env::var("PROFILE").unwrap_or(String::new()); + if profile == "release" || opt_level >= 2 { + println!("cargo:rustc-cfg=optimized"); + } +} diff --git a/crates/libm-test/src/conformance.rs b/crates/libm-test/src/conformance.rs new file mode 100644 index 00000000..2a1d478b --- /dev/null +++ b/crates/libm-test/src/conformance.rs @@ -0,0 +1,425 @@ +#[cfg(test)] +use core::{f32, f64}; +#[cfg(test)] +use rand::Rng; + +macro_rules! forward { + ($func:ident, $x:ident) => ( + paste::item! { + #[cfg(test)] + extern "C" { pub fn [<$func>](x: $x) -> $x; } + + #[test] + pub fn [<$func _matches>]() { + let mut r = rand::thread_rng(); + for x in [$x::NAN, -$x::NAN, $x::INFINITY, $x::NEG_INFINITY].iter() { + let expected = unsafe { $func(*x) }; + let result = libm::$func(*x); + if !crate::[](expected, result) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, expected, result); + } + } + for _ in 0..500 { + let x = r.gen::<$x>(); + let expected = unsafe { $func(x) }; + let result = libm::$func(x); + if !crate::[](expected, result) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, expected, result); + } + } + } + } + ); + ($func:ident, $x:ty, $y:ty) => ( + paste::item! { + #[cfg(test)] + extern "C" { pub fn [<$func>](x: $x, y: $y) -> $x; } + + #[test] + pub fn [<$func _matches>]() { + let mut r = rand::thread_rng(); + for _ in 0..500 { + let x = r.gen::<$x>(); + let y = r.gen::<$y>(); + let expected = unsafe { $func(x, y) }; + let result = libm::$func(x, y); + if !crate::[](expected, result) { + panic!("INPUT: {:?} {:?} EXPECTED: {:?} ACTUAL {:?}", x, y, expected, result); + } + } + } + } +); + ($func:ident, $x:ty, $y:ty, $z:ty) => ( + paste::item! { + #[cfg(test)] + extern "C" { pub fn [<$func>](x: $x, y: $y, z: $z) -> $x; } + + #[test] + pub fn [<$func _matches>]() { + let mut r = rand::thread_rng(); + for _ in 0..500 { + let x = r.gen::<$x>(); + let y = r.gen::<$y>(); + let z = r.gen::<$z>(); + let expected = unsafe { $func(x, y, z) }; + let result = libm::$func(x, y, z); + if !crate::[](expected, result) { + panic!("INPUT: {:?} {:?} {:?} EXPECTED: {:?} ACTUAL {:?}", x, y, z, expected, result); + } + } + } + } + ); +} +macro_rules! bessel { + ($($func:ident),*) => ($( + paste::item! { + #[cfg(test)] + extern "C" { pub fn [<$func>](n: i32, x: f64) -> f64; } + + #[test] + pub fn [<$func _matches>]() { + let mut r = rand::thread_rng(); + for _ in 0..500 { + let mut n = r.gen::(); + n &= 0xffff; + let x = r.gen::(); + let expected = unsafe { [<$func>](n, x) }; + let result = libm::[<$func>](n, x); + if !crate::equalf64(expected, result) { + panic!("INPUT: {:?} {:?} EXPECTED: {:?} ACTUAL {:?}", n, x, expected, result); + } + } + } + + #[cfg(test)] + extern "C" { pub fn [<$func f>](n: i32, x: f32) -> f32; } + + #[test] + pub fn [<$func f _matches>]() { + let mut r = rand::thread_rng(); + for _ in 0..500 { + let mut n = r.gen::(); + n &= 0xffff; + let x = r.gen::(); + let expected = unsafe { [<$func f>](n, x) }; + let result = libm::[<$func f>](n, x); + if !crate::equalf32(expected, result) { + panic!("INPUT: {:?} {:?} EXPECTED: {:?} ACTUAL {:?}", n, x, expected, result); + } + } + } + } +)*); +} + +macro_rules! unary { + ($($func:ident),*) => ($( + paste::item! { + forward!($func, f64); + forward!([<$func f>], f32); + } + )*); +} + +macro_rules! binary { + ($($func:ident),*) => ($( + paste::item! { + forward!($func, f64, f64); + forward!([<$func f>], f32, f32); + } + )*); +} + +macro_rules! trinary {($($func:ident),*) => {$( + paste::item! { + forward!($func, f64, f64, f64); + forward!([<$func f>], f32, f32, f32); + } +)*}} + +unary!( + acos, acosh, asin, atan, cbrt, ceil, cos, cosh, erf, exp, exp2, exp10, expm1, fabs, floor, j0, + j1, lgamma, log, log1p, log2, log10, round, sin, sinh, sqrt, tan, tanh, tgamma, trunc, y0, y1 +); +binary!(atan2, copysign, fdim, fmax, fmin, fmod, hypot, pow); +trinary!(fma); +bessel!(jn, yn); + +// special cases +paste::item! { + #[cfg(test)] + extern "C" { pub fn scalbn(x: f64, n: i32) -> f64; } + + #[test] + pub fn scalbn_matches() { + let mut r = rand::thread_rng(); + for _ in 0..500 { + let n = r.gen::(); + let x = r.gen::(); + let expected = unsafe { scalbn(x, n) }; + let result = libm::scalbn(x, n); + if !crate::equalf64(expected, result) { + panic!("INPUT: {:?} {:?} EXPECTED: {:?} ACTUAL {:?}", x, n, expected, result); + } + } + } + + #[cfg(test)] + extern "C" { pub fn scalbnf(x: f32, n: i32) -> f32; } + + #[test] + pub fn scalbnf_matches() { + let mut r = rand::thread_rng(); + for _ in 0..500 { + let n = r.gen::(); + let x = r.gen::(); + let expected = unsafe { scalbnf(x, n) }; + let result = libm::scalbnf(x, n); + if !crate::equalf32(expected, result) { + panic!("INPUT: {:?} {:?} EXPECTED: {:?} ACTUAL {:?}", x, n, expected, result); + } + } + } + + #[cfg(test)] + extern "C" { pub fn ilogb(x: f64) -> i32; } + + #[test] + pub fn ilogb_matches() { + for x in [f64::NAN, -f64::NAN, f64::INFINITY, f64::NEG_INFINITY].iter() { + let expected = unsafe { ilogb(*x) }; + let result = libm::ilogb(*x); + if !crate::equali32(expected, result) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, expected, result); + } + } + + let mut r = rand::thread_rng(); + for _ in 0..500 { + let x = r.gen::(); + let expected = unsafe { ilogb(x) }; + let result = libm::ilogb(x); + if !crate::equali32(expected, result) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, expected, result); + } + } + } + + #[cfg(test)] + extern "C" { pub fn ilogbf(x: f32) -> i32; } + + #[test] + pub fn ilogbf_matches() { + for x in [f32::NAN, -f32::NAN, f32::INFINITY, f32::NEG_INFINITY].iter() { + let expected = unsafe { ilogbf(*x) }; + let result = libm::ilogbf(*x); + if !crate::equali32(expected, result) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, expected, result); + } + } + + let mut r = rand::thread_rng(); + for _ in 0..500 { + let x = r.gen::(); + let expected = unsafe { ilogbf(x) }; + let result = libm::ilogbf(x); + if !crate::equali32(expected, result) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, expected, result); + } + } + } + + #[cfg(test)] + extern "C" { pub fn modf(x: f64, y: *mut f64) -> f64; } + + #[test] + pub fn modf_matches() { + for x in [f64::NAN, -f64::NAN, f64::INFINITY, f64::NEG_INFINITY].iter() { + let mut b = 0.; + let a = unsafe { modf(*x, &mut b)}; + let (c, d) = libm::modf(*x); + if !crate::equalf64(a, c) || !crate::equalf64(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + let mut r = rand::thread_rng(); + for _ in 0..500 { + let mut b = 0.; + let x = r.gen::(); + let a = unsafe { modf(x, &mut b)}; + let (c, d) = libm::modf(x); + if !crate::equalf64(a, c) || !crate::equalf64(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + } + + #[cfg(test)] + extern "C" { pub fn modff(x: f32, y: *mut f32) -> f32; } + + #[test] + pub fn modff_matches() { + for x in [f32::NAN, -f32::NAN, f32::INFINITY, f32::NEG_INFINITY].iter() { + let mut b = 0.; + let a = unsafe { modff(*x, &mut b) }; + let (c, d) = libm::modff(*x); + if !crate::equalf32(a, c) || !crate::equalf32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + let mut r = rand::thread_rng(); + for _ in 0..500 { + let mut b = 0.; + let x = r.gen::(); + let a = unsafe { modff(x, &mut b) }; + let (c, d) = libm::modff(x); + if !crate::equalf32(a, c) || !crate::equalf32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + } + + #[cfg(test)] + extern "C" { pub fn remquo(x: f64, y: f64, b: *mut i32) -> f64; } + + #[test] + pub fn remquo_matches() { + let mut r = rand::thread_rng(); + for _ in 0..500 { + let mut b = 0; + let x = r.gen::(); + let y = r.gen::(); + let a = unsafe { remquo(x, y, &mut b)}; + let (c, d) = libm::remquo(x, y); + if !crate::equalf64(a, c) || !crate::equali32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + } + + #[cfg(test)] + extern "C" { pub fn remquof(x: f32, y: f32, b: *mut i32) -> f32; } + + #[test] + pub fn remquof_matches() { + let mut r = rand::thread_rng(); + for _ in 0..500 { + let mut b = 0; + let x = r.gen::(); + let y = r.gen::(); + let a = unsafe { remquof(x, y, &mut b)}; + let (c, d) = libm::remquof(x, y); + if !crate::equalf32(a, c) || !crate::equali32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + } + + #[cfg(test)] + extern "C" { pub fn frexp(x: f64, y: *mut i32) -> f64; } + + #[test] + pub fn frexp_matches() { + let mut r = rand::thread_rng(); + for x in [f64::NAN, -f64::NAN, f64::INFINITY, f64::NEG_INFINITY].iter() { + let mut b = r.gen::(); + let a = unsafe { frexp(*x, &mut b) }; + let (c, d) = libm::frexp(*x); + if !crate::equalf64(a, c) || !crate::equali32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + for _ in 0..500 { + let x = r.gen::(); + let mut b = r.gen::(); + let a = unsafe { frexp(x, &mut b) }; + let (c, d) = libm::frexp(x); + if !crate::equalf64(a, c) || !crate::equali32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + } + + #[cfg(test)] + extern "C" { pub fn frexpf(x: f32, y: *mut i32) -> f32; } + + #[test] + pub fn frexpf_matches() { + let mut r = rand::thread_rng(); + for x in [f32::NAN, -f32::NAN, f32::INFINITY, f32::NEG_INFINITY].iter() { + let mut b = 0; + let a = unsafe { frexpf(*x, &mut b) }; + let (c, d) = libm::frexpf(*x); + if !crate::equalf32(a, c) || !crate::equali32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + for _ in 0..500 { + let x = r.gen::(); + let mut b = 0; + let a = unsafe { frexpf(x, &mut b) }; + let (c, d) = libm::frexpf(x); + if !crate::equalf32(a, c) || !crate::equali32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } + } + + #[cfg(test)] + extern "C" { pub fn sincos(x: f64, sin: *mut f64, cos: *mut f64); } + + #[test] + pub fn sincos_matches() { + for x in [f64::NAN, -f64::NAN, f64::INFINITY, f64::NEG_INFINITY].iter() { + let mut sin = 0.; + let mut cos = 0.; + unsafe { sincos(*x, &mut sin, &mut cos) }; + let result = libm::sincos(*x); + if !crate::equalf64(sin, result.0) || !crate::equalf64(cos, result.1) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (sin, cos), result); + } + } + + let mut r = rand::thread_rng(); + for _ in 0..500 { + let x = r.gen::(); + let mut sin = 0.; + let mut cos = 0.; + unsafe { sincos(x, &mut sin, &mut cos) }; + let result = libm::sincos(x); + if !crate::equalf64(sin, result.0) || !crate::equalf64(cos, result.1) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (sin, cos), result); + } + } + } + + #[cfg(test)] + extern "C" { pub fn sincosf(x: f32, sin: *mut f32, cos: *mut f32); } + + #[test] + pub fn sincosf_matches() { + for x in [f32::NAN, -f32::NAN, f32::INFINITY, f32::NEG_INFINITY].iter() { + let mut sin = 0.; + let mut cos = 0.; + unsafe { sincosf(*x, &mut sin, &mut cos) }; + let result = libm::sincosf(*x); + if !crate::equalf32(sin, result.0) || !crate::equalf32(cos, result.1) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (sin, cos), result); + } + } + + let mut r = rand::thread_rng(); + for _ in 0..500 { + let x = r.gen::(); + let mut sin = 0.; + let mut cos = 0.; + unsafe { sincosf(x, &mut sin, &mut cos) }; + let result = libm::sincosf(x); + if !crate::equalf32(sin, result.0) || !crate::equalf32(cos, result.1) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (sin, cos), result); + } + } + } +} diff --git a/crates/libm-test/src/lib.rs b/crates/libm-test/src/lib.rs new file mode 100644 index 00000000..33e6bd9d --- /dev/null +++ b/crates/libm-test/src/lib.rs @@ -0,0 +1,66 @@ +#![cfg(test)] +extern crate core; +extern crate libm; +extern crate paste; +extern crate rand; + +pub mod conformance; + +#[cfg(target_arch = "x86_64")] +pub mod validation; + +#[cfg(test)] +pub fn equalf64(x: f64, y: f64) -> bool { + if x.is_nan() != y.is_nan() { + // one is nan but the other is not + return false; + } + if x.is_nan() && y.is_nan() { + return true; + } + if x.is_infinite() != y.is_infinite() { + // one is inf but the other is not + return false; + } + if x.is_infinite() != y.is_infinite() { + // one is inf but the other is not + return false; + } + let xi: i64 = unsafe { core::intrinsics::transmute(x) }; + let yi: i64 = unsafe { core::intrinsics::transmute(y) }; + if (xi < 0) != (yi < 0) { + // different sign + return false; + } + let ulps = (xi - yi).abs(); + ulps <= 1 +} + +#[cfg(test)] +pub fn equalf32(x: f32, y: f32) -> bool { + if x.is_nan() != y.is_nan() { + // one is nan but the other is not + return false; + } + if x.is_nan() && y.is_nan() { + return true; + } + if x.is_infinite() != y.is_infinite() { + // one is inf but the other is not + return false; + } + let xi: i32 = unsafe { core::intrinsics::transmute(x) }; + let yi: i32 = unsafe { core::intrinsics::transmute(y) }; + if (xi < 0) != (yi < 0) { + // different sign + return false; + } + let ulps = (xi - yi).abs(); + ulps <= 1 +} + +#[cfg(test)] +pub fn equali32(x: i32, y: i32) -> bool { + let ulps = (x - y).abs(); + ulps <= 1 +} diff --git a/crates/libm-test/src/validation.rs b/crates/libm-test/src/validation.rs new file mode 100644 index 00000000..c9b08ef1 --- /dev/null +++ b/crates/libm-test/src/validation.rs @@ -0,0 +1,119 @@ +macro_rules! unary { + ($($func:ident),*) => ($( + paste::item! { + #[cfg(test)] + extern "C" { pub fn [<$func f>](x: f32) -> f32; } + + #[test] + pub fn [<$func f _validation>]() { + if !cfg!(optimized) { return } + + for i in 0..u32::max_value() { + let x = f32::from_bits(i); + let expected = unsafe { [<$func f>](x) }; + let result = libm::[<$func f>](x); + if !crate::equalf32(expected, result) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, expected, result); + } + } + } + } + )*); +} + +unary!( + acos, acosh, asin, atan, cbrt, ceil, cos, cosh, erf, exp, exp2, exp10, expm1, fabs, floor, j0, + j1, lgamma, log, log1p, log2, log10, round, sin, sinh, sqrt, tan, tanh, tgamma, trunc, y0, y1 +); + +#[cfg(test)] +extern "C" { + pub fn ilogbf(x: f32) -> i32; +} + +#[test] +pub fn ilogbf_validation() { + if !cfg!(optimized) { + return; + } + for i in 0..u32::max_value() { + let x = f32::from_bits(i); + let expected = unsafe { ilogbf(x) }; + let result = libm::ilogbf(x); + if !crate::equali32(expected, result) { + panic!( + "INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", + x, expected, result + ); + } + } +} + +#[cfg(test)] +extern "C" { + pub fn modff(x: f32, y: *mut f32) -> f32; +} + +#[test] +pub fn modff_validation() { + if !cfg!(optimized) { + return; + } + for i in 0..u32::max_value() { + let mut b = 0.; + let x = f32::from_bits(i); + let a = unsafe { modff(x, &mut b) }; + let (c, d) = libm::modff(x); + if !crate::equalf32(a, c) || !crate::equalf32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } +} + +#[cfg(test)] +extern "C" { + pub fn frexpf(x: f32, y: *mut i32) -> f32; +} + +#[test] +pub fn frexpf_validation() { + if !cfg!(optimized) { + return; + } + for i in 0..u32::max_value() { + let mut b = 0; + let x = f32::from_bits(i); + let a = unsafe { frexpf(x, &mut b) }; + let (c, d) = libm::frexpf(x); + if !crate::equalf32(a, c) || !crate::equali32(b, d) { + panic!("INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", x, (a, b), (c, d)); + } + } +} + +#[cfg(test)] +extern "C" { + pub fn sincosf(x: f32, sin: *mut f32, cos: *mut f32); +} + +#[test] +pub fn sincosf_validation() { + if !cfg!(optimized) { + return; + } + for i in 0..u32::max_value() { + let x = f32::from_bits(i); + let mut sin = 0.; + let mut cos = 0.; + unsafe { sincosf(x, &mut sin, &mut cos) }; + let result = libm::sincosf(x); + if !crate::equalf32(sin, result.0) || !crate::equalf32(cos, result.1) { + panic!( + "INPUT: {:?} EXPECTED: {:?} ACTUAL {:?}", + x, + (sin, cos), + result + ); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a47883d8..824819db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,39 +22,6 @@ use core::{f32, f64}; pub use self::math::*; -/// Approximate equality with 1 ULP of tolerance -#[doc(hidden)] -#[inline] -pub fn _eqf(a: f32, b: f32) -> Result<(), u32> { - if a.is_nan() && b.is_nan() { - Ok(()) - } else { - let err = (a.to_bits() as i32).wrapping_sub(b.to_bits() as i32).abs(); - - if err <= 1 { - Ok(()) - } else { - Err(err as u32) - } - } -} - -#[doc(hidden)] -#[inline] -pub fn _eq(a: f64, b: f64) -> Result<(), u64> { - if a.is_nan() && b.is_nan() { - Ok(()) - } else { - let err = (a.to_bits() as i64).wrapping_sub(b.to_bits() as i64).abs(); - - if err <= 1 { - Ok(()) - } else { - Err(err as u64) - } - } -} - /// Math support for `f32` /// /// This trait is sealed and cannot be implemented outside of `libm`. @@ -637,6 +604,3 @@ mod private { impl Sealed for f32 {} impl Sealed for f64 {} } - -#[cfg(all(test, feature = "musl-reference-tests"))] -include!(concat!(env!("OUT_DIR"), "/musl-tests.rs"));