From 37859022dd1339a2d6cab6927d1fb67b3653e005 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sun, 22 Dec 2024 10:14:56 +0000 Subject: [PATCH 1/3] Forward the `CI` environment variable when running in Docker We want to be able to adjust our configuration based on whether we are running in CI, propagate this so our tests can use it. --- ci/run-docker.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/run-docker.sh b/ci/run-docker.sh index a040126df..d9f29656d 100755 --- a/ci/run-docker.sh +++ b/ci/run-docker.sh @@ -28,6 +28,7 @@ run() { docker run \ --rm \ --user "$(id -u):$(id -g)" \ + -e CI \ -e RUSTFLAGS \ -e CARGO_HOME=/cargo \ -e CARGO_TARGET_DIR=/target \ From 0840d59d3a543095f89e923b0ac8bbd92f3607ae Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Tue, 31 Dec 2024 22:56:36 +0000 Subject: [PATCH 2/3] Use `rustdoc` output to create a list of public API Rather than collecting a list of file names in `libm-test/build.rs`, just use a script to parse rustdoc's JSON output. --- .github/workflows/main.yml | 4 + crates/libm-test/build.rs | 34 ------- crates/libm-test/src/lib.rs | 12 ++- crates/libm-test/tests/check_coverage.rs | 70 +++++++------- etc/function-list.txt | 115 ++++++++++++++++++++++ etc/update-api-list.py | 117 +++++++++++++++++++++++ 6 files changed, 283 insertions(+), 69 deletions(-) create mode 100644 etc/function-list.txt create mode 100755 etc/update-api-list.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 83875f368..0f5becf73 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -96,6 +96,10 @@ jobs: run: ./ci/download-musl.sh shell: bash + - name: Verify API list + if: matrix.os == 'ubuntu-24.04' + run: python3 etc/update-api-list.py --check + # Non-linux tests just use our raw script - name: Run locally if: matrix.os != 'ubuntu-24.04' || contains(matrix.target, 'wasm') diff --git a/crates/libm-test/build.rs b/crates/libm-test/build.rs index f2cd298ba..134fb11ce 100644 --- a/crates/libm-test/build.rs +++ b/crates/libm-test/build.rs @@ -1,42 +1,8 @@ -use std::fmt::Write; -use std::fs; - #[path = "../../configure.rs"] mod configure; use configure::Config; fn main() { let cfg = Config::from_env(); - - list_all_tests(&cfg); - configure::emit_test_config(&cfg); } - -/// Create a list of all source files in an array. This can be used for making sure that -/// all functions are tested or otherwise covered in some way. -// FIXME: it would probably be better to use rustdoc JSON output to get public functions. -fn list_all_tests(cfg: &Config) { - let math_src = cfg.manifest_dir.join("../../src/math"); - - let mut files = fs::read_dir(math_src) - .unwrap() - .map(|f| f.unwrap().path()) - .filter(|entry| entry.is_file()) - .map(|f| f.file_stem().unwrap().to_str().unwrap().to_owned()) - .collect::>(); - files.sort(); - - let mut s = "pub const ALL_FUNCTIONS: &[&str] = &[".to_owned(); - for f in files { - if f == "mod" { - // skip mod.rs - continue; - } - write!(s, "\"{f}\",").unwrap(); - } - write!(s, "];").unwrap(); - - let outfile = cfg.out_dir.join("all_files.rs"); - fs::write(outfile, s).unwrap(); -} diff --git a/crates/libm-test/src/lib.rs b/crates/libm-test/src/lib.rs index eb457b0ae..fdba0357f 100644 --- a/crates/libm-test/src/lib.rs +++ b/crates/libm-test/src/lib.rs @@ -23,9 +23,6 @@ pub use test_traits::{CheckOutput, GenerateInput, Hex, TupleCall}; /// propagate. pub type TestResult = Result; -// List of all files present in libm's source -include!(concat!(env!("OUT_DIR"), "/all_files.rs")); - /// True if `EMULATED` is set and nonempty. Used to determine how many iterations to run. pub const fn emulated() -> bool { match option_env!("EMULATED") { @@ -34,3 +31,12 @@ pub const fn emulated() -> bool { Some(_) => true, } } + +/// True if `CI` is set and nonempty. +pub const fn ci() -> bool { + match option_env!("CI") { + Some(s) if s.is_empty() => false, + None => false, + Some(_) => true, + } +} diff --git a/crates/libm-test/tests/check_coverage.rs b/crates/libm-test/tests/check_coverage.rs index b7988660e..9f85d6424 100644 --- a/crates/libm-test/tests/check_coverage.rs +++ b/crates/libm-test/tests/check_coverage.rs @@ -1,54 +1,60 @@ //! Ensure that `for_each_function!` isn't missing any symbols. -/// Files in `src/` that do not export a testable symbol. -const ALLOWED_SKIPS: &[&str] = &[ - // Not a generic test function - "fenv", - // Nonpublic functions - "expo2", - "k_cos", - "k_cosf", - "k_expo2", - "k_expo2f", - "k_sin", - "k_sinf", - "k_tan", - "k_tanf", - "rem_pio2", - "rem_pio2_large", - "rem_pio2f", -]; +use std::collections::HashSet; +use std::env; +use std::path::Path; +use std::process::Command; macro_rules! callback { ( fn_name: $name:ident, - extra: [$push_to:ident], + extra: [$set:ident], ) => { - $push_to.push(stringify!($name)); + let name = stringify!($name); + let new = $set.insert(name); + assert!(new, "duplicate function `{name}` in `ALL_OPERATIONS`"); }; } #[test] fn test_for_each_function_all_included() { - let mut included = Vec::new(); - let mut missing = Vec::new(); + let all_functions: HashSet<_> = include_str!("../../../etc/function-list.txt") + .lines() + .filter(|line| !line.starts_with("#")) + .collect(); + + let mut tested = HashSet::new(); libm_macros::for_each_function! { callback: callback, - extra: [included], + extra: [tested], }; - for f in libm_test::ALL_FUNCTIONS { - if !included.contains(f) && !ALLOWED_SKIPS.contains(f) { - missing.push(f) - } - } - - if !missing.is_empty() { + let untested = all_functions.difference(&tested); + if untested.clone().next().is_some() { panic!( - "missing tests for the following: {missing:#?} \ + "missing tests for the following: {untested:#?} \ \nmake sure any new functions are entered in \ - `ALL_FUNCTIONS` (in `libm-macros`)." + `ALL_OPERATIONS` (in `libm-macros`)." ); } + assert_eq!(all_functions, tested); +} + +#[test] +fn ensure_list_updated() { + if libm_test::ci() { + // Most CI tests run in Docker where we don't have Python or Rustdoc, so it's easiest + // to just run the python file directly when it is available. + eprintln!("skipping test; CI runs the python file directly"); + return; + } + + let res = Command::new("python3") + .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../etc/update-api-list.py")) + .arg("--check") + .status() + .unwrap(); + + assert!(res.success(), "May need to run `./etc/update-api-list.py`"); } diff --git a/etc/function-list.txt b/etc/function-list.txt new file mode 100644 index 000000000..51f5b221c --- /dev/null +++ b/etc/function-list.txt @@ -0,0 +1,115 @@ +# autogenerated by update-api-list.py +acos +acosf +acosh +acoshf +asin +asinf +asinh +asinhf +atan +atan2 +atan2f +atanf +atanh +atanhf +cbrt +cbrtf +ceil +ceilf +copysign +copysignf +cos +cosf +cosh +coshf +erf +erfc +erfcf +erff +exp +exp10 +exp10f +exp2 +exp2f +expf +expm1 +expm1f +fabs +fabsf +fdim +fdimf +floor +floorf +fma +fmaf +fmax +fmaxf +fmin +fminf +fmod +fmodf +frexp +frexpf +hypot +hypotf +ilogb +ilogbf +j0 +j0f +j1 +j1f +jn +jnf +ldexp +ldexpf +lgamma +lgamma_r +lgammaf +lgammaf_r +log +log10 +log10f +log1p +log1pf +log2 +log2f +logf +modf +modff +nextafter +nextafterf +pow +powf +remainder +remainderf +remquo +remquof +rint +rintf +round +roundf +scalbn +scalbnf +sin +sincos +sincosf +sinf +sinh +sinhf +sqrt +sqrtf +tan +tanf +tanh +tanhf +tgamma +tgammaf +trunc +truncf +y0 +y0f +y1 +y1f +yn +ynf diff --git a/etc/update-api-list.py b/etc/update-api-list.py new file mode 100755 index 000000000..7284a628c --- /dev/null +++ b/etc/update-api-list.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Create a text file listing all public API. This can be used to ensure that all +functions are covered by our macros. +""" + +import json +import subprocess as sp +import sys +import difflib +from pathlib import Path +from typing import Any + +ETC_DIR = Path(__file__).parent + + +def get_rustdoc_json() -> dict[Any, Any]: + """Get rustdoc's JSON output for the `libm` crate.""" + + librs_path = ETC_DIR.joinpath("../src/lib.rs") + j = sp.check_output( + [ + "rustdoc", + librs_path, + "--edition=2021", + "--output-format=json", + "-Zunstable-options", + "-o-", + ], + text=True, + ) + j = json.loads(j) + return j + + +def list_public_functions() -> list[str]: + """Get a list of public functions from rustdoc JSON output. + + Note that this only finds functions that are reexported in `lib.rs`, this will + need to be adjusted if we need to account for functions that are defined there. + """ + names = [] + index: dict[str, dict[str, Any]] = get_rustdoc_json()["index"] + for item in index.values(): + # Find public items + if item["visibility"] != "public": + continue + + # Find only reexports + if "use" not in item["inner"].keys(): + continue + + # Locate the item that is reexported + id = item["inner"]["use"]["id"] + srcitem = index.get(str(id)) + + # External crate + if srcitem is None: + continue + + # Skip if not a function + if "function" not in srcitem["inner"].keys(): + continue + + names.append(srcitem["name"]) + + names.sort() + return names + + +def diff_and_exit(actual: str, expected: str): + """If the two strings are different, print a diff between them and then exit + with an error. + """ + if actual == expected: + print("output matches expected; success") + return + + a = [f"{line}\n" for line in actual.splitlines()] + b = [f"{line}\n" for line in expected.splitlines()] + + diff = difflib.unified_diff(a, b, "actual", "expected") + sys.stdout.writelines(diff) + print("mismatched function list") + exit(1) + + +def main(): + """By default overwrite the file. If `--check` is passed, print a diff instead and + error if the files are different. + """ + match sys.argv: + case [_]: + check = False + case [_, "--check"]: + check = True + case _: + print("unrecognized arguments") + exit(1) + + names = list_public_functions() + output = "# autogenerated by update-api-list.py\n" + for name in names: + output += f"{name}\n" + + out_file = ETC_DIR.joinpath("function-list.txt") + + if check: + with open(out_file, "r") as f: + current = f.read() + diff_and_exit(current, output) + else: + with open(out_file, "w") as f: + f.write(output) + + +if __name__ == "__main__": + main() From 780519d753c517c27144cfabc48cd9f00246c2a6 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Tue, 31 Dec 2024 22:57:21 +0000 Subject: [PATCH 3/3] Add missing functions to the macro list Now that we are using rustdoc output to locate public functions, the test is indicating a few that were missed since they don't have their own function. Update everything to now include the following routines: * `erfc` * `erfcf` * `y0` * `y0f` * `y1` * `y1f` * `yn` * `ynf` --- crates/libm-macros/src/shared.rs | 16 ++++++++-------- crates/libm-test/src/domain.rs | 3 +++ crates/libm-test/src/gen/random.rs | 6 +++++- crates/libm-test/src/mpfloat.rs | 17 ++++++++++++++++- crates/libm-test/src/precision.rs | 14 ++++++-------- crates/libm-test/tests/multiprecision.rs | 4 +++- crates/musl-math-sys/src/lib.rs | 1 + 7 files changed, 42 insertions(+), 19 deletions(-) diff --git a/crates/libm-macros/src/shared.rs b/crates/libm-macros/src/shared.rs index 100bcc7ad..ef0f18801 100644 --- a/crates/libm-macros/src/shared.rs +++ b/crates/libm-macros/src/shared.rs @@ -11,9 +11,9 @@ const ALL_OPERATIONS_NESTED: &[(FloatTy, Signature, Option, &[&str])] None, &[ "acosf", "acoshf", "asinf", "asinhf", "atanf", "atanhf", "cbrtf", "ceilf", "cosf", - "coshf", "erff", "exp10f", "exp2f", "expf", "expm1f", "fabsf", "floorf", "j0f", "j1f", - "lgammaf", "log10f", "log1pf", "log2f", "logf", "rintf", "roundf", "sinf", "sinhf", - "sqrtf", "tanf", "tanhf", "tgammaf", "truncf", + "coshf", "erff", "erfcf", "exp10f", "exp2f", "expf", "expm1f", "fabsf", "floorf", + "j0f", "j1f", "lgammaf", "log10f", "log1pf", "log2f", "logf", "rintf", "roundf", + "sinf", "sinhf", "sqrtf", "tanf", "tanhf", "tgammaf", "truncf", "y0f", "y1f", ], ), ( @@ -23,9 +23,9 @@ const ALL_OPERATIONS_NESTED: &[(FloatTy, Signature, Option, &[&str])] None, &[ "acos", "acosh", "asin", "asinh", "atan", "atanh", "cbrt", "ceil", "cos", "cosh", - "erf", "exp10", "exp2", "exp", "expm1", "fabs", "floor", "j0", "j1", "lgamma", "log10", - "log1p", "log2", "log", "rint", "round", "sin", "sinh", "sqrt", "tan", "tanh", - "tgamma", "trunc", + "erf", "erfc", "exp10", "exp2", "exp", "expm1", "fabs", "floor", "j0", "j1", "lgamma", + "log10", "log1p", "log2", "log", "rint", "round", "sin", "sinh", "sqrt", "tan", "tanh", + "tgamma", "trunc", "y0", "y1", ], ), ( @@ -97,14 +97,14 @@ const ALL_OPERATIONS_NESTED: &[(FloatTy, Signature, Option, &[&str])] FloatTy::F32, Signature { args: &[Ty::I32, Ty::F32], returns: &[Ty::F32] }, None, - &["jnf"], + &["jnf", "ynf"], ), ( // `(i32, f64) -> f64` FloatTy::F64, Signature { args: &[Ty::I32, Ty::F64], returns: &[Ty::F64] }, None, - &["jn"], + &["jn", "yn"], ), ( // `(f32, i32) -> f32` diff --git a/crates/libm-test/src/domain.rs b/crates/libm-test/src/domain.rs index 9ee8a19b9..7b5a01b96 100644 --- a/crates/libm-test/src/domain.rs +++ b/crates/libm-test/src/domain.rs @@ -147,6 +147,7 @@ impl_has_domain! { cos => TRIG; cosh => UNBOUNDED; erf => UNBOUNDED; + erfc => UNBOUNDED; exp => UNBOUNDED; exp10 => UNBOUNDED; exp2 => UNBOUNDED; @@ -173,6 +174,8 @@ impl_has_domain! { tanh => UNBOUNDED; tgamma => GAMMA; trunc => UNBOUNDED; + y0 => UNBOUNDED; + y1 => UNBOUNDED; } /* Manual implementations, these functions don't follow `foo`->`foof` naming */ diff --git a/crates/libm-test/src/gen/random.rs b/crates/libm-test/src/gen/random.rs index 527cd1351..4f75da07b 100644 --- a/crates/libm-test/src/gen/random.rs +++ b/crates/libm-test/src/gen/random.rs @@ -110,6 +110,10 @@ pub fn get_test_cases(ctx: &CheckCtx) -> impl Iterator, { - let inputs = if ctx.base_name == BaseName::Jn { &TEST_CASES_JN } else { &TEST_CASES }; + let inputs = if ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn { + &TEST_CASES_JN + } else { + &TEST_CASES + }; inputs.get_cases() } diff --git a/crates/libm-test/src/mpfloat.rs b/crates/libm-test/src/mpfloat.rs index 507b077b3..28df916bd 100644 --- a/crates/libm-test/src/mpfloat.rs +++ b/crates/libm-test/src/mpfloat.rs @@ -130,7 +130,7 @@ libm_macros::for_each_function! { fabsf, ceilf, copysignf, floorf, rintf, roundf, truncf, fmod, fmodf, frexp, frexpf, ilogb, ilogbf, jn, jnf, ldexp, ldexpf, lgamma_r, lgammaf_r, modf, modff, nextafter, nextafterf, pow,powf, - remquo, remquof, scalbn, scalbnf, sincos, sincosf, + remquo, remquof, scalbn, scalbnf, sincos, sincosf, yn, ynf, ], fn_extra: match MACRO_FN_NAME { // Remap function names that are different between mpfr and libm @@ -266,6 +266,21 @@ macro_rules! impl_op_for_ty { ) } } + + impl MpOp for crate::op::[]::Routine { + type MpTy = (i32, MpFloat); + + fn new_mp() -> Self::MpTy { + (0, new_mpfloat::()) + } + + fn run(this: &mut Self::MpTy, input: Self::RustArgs) -> Self::RustRet { + this.0 = input.0; + this.1.assign(input.1); + let ord = this.1.yn_round(this.0, Nearest); + prep_retval::(&mut this.1, ord) + } + } } }; } diff --git a/crates/libm-test/src/precision.rs b/crates/libm-test/src/precision.rs index 058d01c6e..6d4561c43 100644 --- a/crates/libm-test/src/precision.rs +++ b/crates/libm-test/src/precision.rs @@ -26,11 +26,9 @@ pub fn default_ulp(ctx: &CheckCtx) -> u32 { // Overrides that apply to either basis // FMA is expected to be infinite precision. (_, Id::Fma | Id::Fmaf) => 0, - (_, Id::J0 | Id::J0f | Id::J1 | Id::J1f) => { - // Results seem very target-dependent - if cfg!(target_arch = "x86_64") { 4000 } else { 800_000 } - } - (_, Id::Jn | Id::Jnf) => 1000, + (_, Id::J0 | Id::J0f | Id::J1 | Id::J1f | Id::Y0 | Id::Y0f | Id::Y1 | Id::Y1f) => 800_000, + (_, Id::Jn | Id::Jnf | Id::Yn | Id::Ynf) => 1000, + (_, Id::Erfc | Id::Erfcf) => 4, // Overrides for musl #[cfg(x86_no_sse)] @@ -297,7 +295,7 @@ impl MaybeOverride<(i32, f32)> for SpecialCase { (Musl, _) => bessel_prec_dropoff(input, ulp, ctx), // We return +0.0, MPFR returns -0.0 - (Mpfr, BaseName::Jn) + (Mpfr, BaseName::Jn | BaseName::Yn) if input.1 == f32::NEG_INFINITY && actual == F::ZERO && expected == F::ZERO => { XFAIL @@ -319,7 +317,7 @@ impl MaybeOverride<(i32, f64)> for SpecialCase { (Musl, _) => bessel_prec_dropoff(input, ulp, ctx), // We return +0.0, MPFR returns -0.0 - (Mpfr, BaseName::Jn) + (Mpfr, BaseName::Jn | BaseName::Yn) if input.1 == f64::NEG_INFINITY && actual == F::ZERO && expected == F::ZERO => { XFAIL @@ -336,7 +334,7 @@ fn bessel_prec_dropoff( ulp: &mut u32, ctx: &CheckCtx, ) -> Option { - if ctx.base_name == BaseName::Jn { + if ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn { if input.0 > 4000 { return XFAIL; } else if input.0 > 2000 { diff --git a/crates/libm-test/tests/multiprecision.rs b/crates/libm-test/tests/multiprecision.rs index 2675ca018..4821f7446 100644 --- a/crates/libm-test/tests/multiprecision.rs +++ b/crates/libm-test/tests/multiprecision.rs @@ -48,7 +48,7 @@ libm_macros::for_each_function! { attributes: [ // Also an assertion failure on i686: at `MPFR_ASSERTN (! mpfr_erangeflag_p ())` #[ignore = "large values are infeasible in MPFR"] - [jn, jnf], + [jn, jnf, yn, ynf], ], skip: [ // FIXME: MPFR tests needed @@ -157,6 +157,8 @@ libm_macros::for_each_function! { remquof, scalbn, scalbnf, + yn, + ynf, // FIXME: MPFR tests needed frexp, diff --git a/crates/musl-math-sys/src/lib.rs b/crates/musl-math-sys/src/lib.rs index db352fab8..07277ef3e 100644 --- a/crates/musl-math-sys/src/lib.rs +++ b/crates/musl-math-sys/src/lib.rs @@ -282,5 +282,6 @@ functions! { musl_y0f: y0f(a: f32) -> f32; musl_y1: y1(a: f64) -> f64; musl_y1f: y1f(a: f32) -> f32; + musl_yn: yn(a: c_int, b: f64) -> f64; musl_ynf: ynf(a: c_int, b: f32) -> f32; }