diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 926e3c19e..a2c779526 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,38 +2,103 @@ name: CI on: [push, pull_request] env: + CARGO_TERM_VERBOSE: true RUSTDOCFLAGS: -Dwarnings RUSTFLAGS: -Dwarnings + RUST_BACKTRACE: full jobs: - docker: + test: name: Docker - runs-on: ubuntu-latest + timeout-minutes: 20 strategy: + fail-fast: false matrix: - target: - - aarch64-unknown-linux-gnu - - arm-unknown-linux-gnueabi - - arm-unknown-linux-gnueabihf - - armv7-unknown-linux-gnueabihf - # - i686-unknown-linux-gnu - # MIPS targets disabled since they are dropped to tier 3. - # See https://github.com/rust-lang/compiler-team/issues/648 - #- mips-unknown-linux-gnu - #- mips64-unknown-linux-gnuabi64 - #- mips64el-unknown-linux-gnuabi64 - - powerpc-unknown-linux-gnu - - powerpc64-unknown-linux-gnu - - powerpc64le-unknown-linux-gnu - - x86_64-unknown-linux-gnu + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + - target: aarch64-pc-windows-msvc + os: windows-latest + build_only: 1 # Can't run on x86 hosts + - target: arm-unknown-linux-gnueabi + os: ubuntu-latest + - target: arm-unknown-linux-gnueabihf + os: ubuntu-latest + - target: armv7-unknown-linux-gnueabihf + os: ubuntu-latest + - target: i586-unknown-linux-gnu + os: ubuntu-latest + - target: i686-unknown-linux-gnu + os: ubuntu-latest + - target: powerpc-unknown-linux-gnu + os: ubuntu-latest + - target: powerpc64-unknown-linux-gnu + os: ubuntu-latest + - target: powerpc64le-unknown-linux-gnu + os: ubuntu-latest + - target: riscv64gc-unknown-linux-gnu + os: ubuntu-latest + - target: thumbv6m-none-eabi + os: ubuntu-latest + - target: thumbv7em-none-eabi + os: ubuntu-latest + - target: thumbv7em-none-eabihf + os: ubuntu-latest + - target: thumbv7m-none-eabi + os: ubuntu-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-13 + - target: i686-pc-windows-msvc + os: windows-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: i686-pc-windows-gnu + os: windows-latest + channel: nightly-i686-gnu + - target: x86_64-pc-windows-gnu + os: windows-latest + channel: nightly-x86_64-gnu + runs-on: ${{ matrix.os }} + env: + BUILD_ONLY: ${{ matrix.build_only }} steps: - - uses: actions/checkout@master - - name: Install Rust - run: rustup update nightly --no-self-update && rustup default nightly - - run: rustup target add ${{ matrix.target }} - - run: rustup target add x86_64-unknown-linux-musl - - run: cargo generate-lockfile - - run: ./ci/run-docker.sh ${{ matrix.target }} + - name: Print runner information + run: uname -a + - uses: actions/checkout@v4 + - name: Install Rust (rustup) + shell: bash + run: | + channel="nightly" + # Account for channels that have required components (MinGW) + [ -n "${{ matrix.channel }}" ] && channel="${{ matrix.channel }}" + rustup update "$channel" --no-self-update + rustup default "$channel" + rustup target add ${{ matrix.target }} + rustup component add llvm-tools-preview + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + + - name: Download musl source + run: ./ci/download-musl.sh + shell: bash + + # Non-linux tests just use our raw script + - name: Run locally + if: matrix.os != 'ubuntu-latest' + shell: bash + run: ./ci/run.sh ${{ matrix.target }} + + # Otherwise we use our docker containers to run builds + - name: Run in Docker + if: matrix.os == 'ubuntu-latest' + run: | + rustup target add x86_64-unknown-linux-musl + cargo generate-lockfile && ./ci/run-docker.sh ${{ matrix.target }} wasm: name: WebAssembly @@ -45,7 +110,7 @@ jobs: - run: rustup target add wasm32-unknown-unknown - run: cargo build --target wasm32-unknown-unknown - cb: + builtins: name: "The compiler-builtins crate works" runs-on: ubuntu-latest steps: @@ -61,6 +126,8 @@ jobs: - uses: actions/checkout@master - name: Install Rust run: rustup update nightly --no-self-update && rustup default nightly + - name: Download musl source + run: ./ci/download-musl.sh - run: cargo bench --all msrv: @@ -92,9 +159,9 @@ jobs: success: needs: - - docker + - test - wasm - - cb + - builtins - benchmarks - msrv - rustfmt diff --git a/.gitignore b/.gitignore index 39950911a..b6a532751 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -**/*.rs.bk +**.bk .#* /bin /math/src /math/target /target -/tests Cargo.lock +musl/ +**.tar.gz diff --git a/Cargo.toml b/Cargo.toml index 9282b7157..72b6dcd5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,13 +24,17 @@ unstable = [] force-soft-floats = [] [workspace] +resolver = "2" members = [ "crates/compiler-builtins-smoke-test", "crates/libm-bench", + "crates/libm-macros", "crates/libm-test", + "crates/musl-math-sys", ] default-members = [ ".", + "crates/libm-macros", "crates/libm-test", ] diff --git a/ci/docker/i586-unknown-linux-gnu/Dockerfile b/ci/docker/i586-unknown-linux-gnu/Dockerfile new file mode 100644 index 000000000..3b0bfc0d3 --- /dev/null +++ b/ci/docker/i586-unknown-linux-gnu/Dockerfile @@ -0,0 +1,5 @@ +FROM ubuntu:24.04 + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc-multilib libc6-dev ca-certificates diff --git a/ci/docker/riscv64gc-unknown-linux-gnu/Dockerfile b/ci/docker/riscv64gc-unknown-linux-gnu/Dockerfile new file mode 100644 index 000000000..5f8a28924 --- /dev/null +++ b/ci/docker/riscv64gc-unknown-linux-gnu/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:24.04 + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc libc6-dev qemu-user-static ca-certificates \ + gcc-riscv64-linux-gnu libc6-dev-riscv64-cross \ + qemu-system-riscv64 + +ENV TOOLCHAIN_PREFIX=riscv64-linux-gnu- +ENV CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER="$TOOLCHAIN_PREFIX"gcc \ + CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_RUNNER=qemu-riscv64-static \ + AR_riscv64gc_unknown_linux_gnu="$TOOLCHAIN_PREFIX"ar \ + CC_riscv64gc_unknown_linux_gnu="$TOOLCHAIN_PREFIX"gcc \ + QEMU_LD_PREFIX=/usr/riscv64-linux-gnu \ + RUST_TEST_THREADS=1 diff --git a/ci/docker/thumbv6m-none-eabi/Dockerfile b/ci/docker/thumbv6m-none-eabi/Dockerfile new file mode 100644 index 000000000..ad0d4351e --- /dev/null +++ b/ci/docker/thumbv6m-none-eabi/Dockerfile @@ -0,0 +1,9 @@ +ARG IMAGE=ubuntu:24.04 +FROM $IMAGE + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc libc6-dev ca-certificates \ + gcc-arm-none-eabi \ + libnewlib-arm-none-eabi +ENV BUILD_ONLY=1 diff --git a/ci/docker/thumbv7em-none-eabi/Dockerfile b/ci/docker/thumbv7em-none-eabi/Dockerfile new file mode 100644 index 000000000..ad0d4351e --- /dev/null +++ b/ci/docker/thumbv7em-none-eabi/Dockerfile @@ -0,0 +1,9 @@ +ARG IMAGE=ubuntu:24.04 +FROM $IMAGE + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc libc6-dev ca-certificates \ + gcc-arm-none-eabi \ + libnewlib-arm-none-eabi +ENV BUILD_ONLY=1 diff --git a/ci/docker/thumbv7em-none-eabihf/Dockerfile b/ci/docker/thumbv7em-none-eabihf/Dockerfile new file mode 100644 index 000000000..ad0d4351e --- /dev/null +++ b/ci/docker/thumbv7em-none-eabihf/Dockerfile @@ -0,0 +1,9 @@ +ARG IMAGE=ubuntu:24.04 +FROM $IMAGE + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc libc6-dev ca-certificates \ + gcc-arm-none-eabi \ + libnewlib-arm-none-eabi +ENV BUILD_ONLY=1 diff --git a/ci/docker/thumbv7m-none-eabi/Dockerfile b/ci/docker/thumbv7m-none-eabi/Dockerfile new file mode 100644 index 000000000..ad0d4351e --- /dev/null +++ b/ci/docker/thumbv7m-none-eabi/Dockerfile @@ -0,0 +1,9 @@ +ARG IMAGE=ubuntu:24.04 +FROM $IMAGE + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc libc6-dev ca-certificates \ + gcc-arm-none-eabi \ + libnewlib-arm-none-eabi +ENV BUILD_ONLY=1 diff --git a/ci/download-musl.sh b/ci/download-musl.sh new file mode 100755 index 000000000..d0d8b310e --- /dev/null +++ b/ci/download-musl.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Download the expected version of musl to a directory `musl` + +set -eux + +fname=musl-1.2.5.tar.gz +sha=a9a118bbe84d8764da0ea0d28b3ab3fae8477fc7e4085d90102b8596fc7c75e4 + +mkdir musl +curl "https://musl.libc.org/releases/$fname" -O + +case "$(uname -s)" in + MINGW*) + # Need to extract the second line because certutil does human output + fsha=$(certutil -hashfile "$fname" SHA256 | sed -n '2p') + [ "$sha" = "$fsha" ] || exit 1 + ;; + *) + echo "$sha $fname" | shasum -a 256 --check || exit 1 + ;; +esac + +tar -xzf "$fname" -C musl --strip-components 1 +rm "$fname" diff --git a/ci/run-docker.sh b/ci/run-docker.sh index 9191a17e2..2e09dd41a 100755 --- a/ci/run-docker.sh +++ b/ci/run-docker.sh @@ -21,6 +21,7 @@ run() { -e RUSTFLAGS \ -e CARGO_HOME=/cargo \ -e CARGO_TARGET_DIR=/target \ + -e EMULATED=1 \ -v "${HOME}/.cargo:/cargo" \ -v "$(pwd)/target:/target" \ -v "$(pwd):/checkout:ro" \ diff --git a/ci/run.sh b/ci/run.sh index 505e25891..f61fff843 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -2,21 +2,64 @@ set -eux -target="$1" +export RUST_BACKTRACE="${RUST_BACKTRACE:-full}" +# Needed for no-panic to correct detect a lack of panics +export RUSTFLAGS="${RUSTFLAGS:-} -Ccodegen-units=1" -cmd="cargo test --all --target $target" +target="${1:-}" -# Needed for no-panic to correct detect a lack of panics -export RUSTFLAGS="$RUSTFLAGS -Ccodegen-units=1" +if [ -z "$target" ]; then + host_target=$(rustc -vV | awk '/^host/ { print $2 }') + echo "Defaulted to host target $host_target" + target="$host_target" +fi + +extra_flags="" + +# We need to specifically skip tests for musl-math-sys on systems that can't +# build musl since otherwise `--all` will activate it. +case "$target" in + # Can't build at all on MSVC, WASM, or thumb + *windows-msvc*) extra_flags="$extra_flags --exclude musl-math-sys" ;; + *wasm*) extra_flags="$extra_flags --exclude musl-math-sys" ;; + *thumb*) extra_flags="$extra_flags --exclude musl-math-sys" ;; + + # We can build musl on MinGW but running tests gets a stack overflow + *windows-gnu*) ;; + # FIXME(#309): LE PPC crashes calling the musl version of some functions. It + # seems like a qemu bug but should be investigated further at some point. + # See . + *powerpc64le*) ;; + + # Everything else gets musl enabled + *) extra_flags="$extra_flags --features libm-test/build-musl" ;; +esac + +# FIXME: `STATUS_DLL_NOT_FOUND` testing macros on CI. +# +case "$target" in + *windows-gnu) extra_flags="$extra_flags --exclude libm-macros" ;; +esac + +if [ "$(uname -a)" = "Linux" ]; then + # also run the reference tests when we can. requires a Linux host. + extra_flags="$extra_flags --features libm-test/test-musl-serialized" +fi + +if [ "${BUILD_ONLY:-}" = "1" ]; then + cmd="cargo build --target $target --package libm" + $cmd + $cmd --features 'unstable' -# stable by default -$cmd -$cmd --release + echo "can't run tests on $target" +else + cmd="cargo test --all --target $target $extra_flags" -# unstable with a feature -$cmd --features 'unstable' -$cmd --release --features 'unstable' + # stable by default + $cmd + $cmd --release -# also run the reference tests -$cmd --features 'unstable libm-test/test-musl-serialized' -$cmd --release --features 'unstable libm-test/test-musl-serialized' + # unstable with a feature + $cmd --features 'unstable' + $cmd --release --features 'unstable' +fi diff --git a/crates/libm-macros/Cargo.toml b/crates/libm-macros/Cargo.toml new file mode 100644 index 000000000..9d2b08e2d --- /dev/null +++ b/crates/libm-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "libm-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.88" +quote = "1.0.37" +syn = { version = "2.0.79", features = ["full", "extra-traits", "visit-mut"] } diff --git a/crates/libm-macros/src/lib.rs b/crates/libm-macros/src/lib.rs new file mode 100644 index 000000000..dc78598ca --- /dev/null +++ b/crates/libm-macros/src/lib.rs @@ -0,0 +1,541 @@ +mod parse; +use std::sync::LazyLock; + +use parse::{Invocation, StructuredInput}; +use proc_macro as pm; +use proc_macro2::{self as pm2, Span}; +use quote::{ToTokens, quote}; +use syn::Ident; +use syn::visit_mut::VisitMut; + +const ALL_FUNCTIONS: &[(Signature, Option, &[&str])] = &[ + ( + // `fn(f32) -> f32` + Signature { args: &[Ty::F32], returns: &[Ty::F32] }, + 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", + ], + ), + ( + // `(f64) -> f64` + Signature { args: &[Ty::F64], returns: &[Ty::F64] }, + 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", + ], + ), + ( + // `(f32, f32) -> f32` + Signature { args: &[Ty::F32, Ty::F32], returns: &[Ty::F32] }, + None, + &[ + "atan2f", + "copysignf", + "fdimf", + "fmaxf", + "fminf", + "fmodf", + "hypotf", + "nextafterf", + "powf", + "remainderf", + ], + ), + ( + // `(f64, f64) -> f64` + Signature { args: &[Ty::F64, Ty::F64], returns: &[Ty::F64] }, + None, + &[ + "atan2", + "copysign", + "fdim", + "fmax", + "fmin", + "fmod", + "hypot", + "nextafter", + "pow", + "remainder", + ], + ), + ( + // `(f32, f32, f32) -> f32` + Signature { args: &[Ty::F32, Ty::F32, Ty::F32], returns: &[Ty::F32] }, + None, + &["fmaf"], + ), + ( + // `(f64, f64, f64) -> f64` + Signature { args: &[Ty::F64, Ty::F64, Ty::F64], returns: &[Ty::F64] }, + None, + &["fma"], + ), + ( + // `(f32) -> i32` + Signature { args: &[Ty::F32], returns: &[Ty::I32] }, + None, + &["ilogbf"], + ), + ( + // `(f64) -> i32` + Signature { args: &[Ty::F64], returns: &[Ty::I32] }, + None, + &["ilogb"], + ), + ( + // `(i32, f32) -> f32` + Signature { args: &[Ty::I32, Ty::F32], returns: &[Ty::F32] }, + None, + &["jnf"], + ), + ( + // `(i32, f64) -> f64` + Signature { args: &[Ty::I32, Ty::F64], returns: &[Ty::F64] }, + None, + &["jn"], + ), + ( + // `(f32, i32) -> f32` + Signature { args: &[Ty::F32, Ty::I32], returns: &[Ty::F32] }, + None, + &["scalbnf", "ldexpf"], + ), + ( + // `(f64, i64) -> f64` + Signature { args: &[Ty::F64, Ty::I32], returns: &[Ty::F64] }, + None, + &["scalbn", "ldexp"], + ), + ( + // `(f32, &mut f32) -> f32` as `(f32) -> (f32, f32)` + Signature { args: &[Ty::F32], returns: &[Ty::F32, Ty::F32] }, + Some(Signature { args: &[Ty::F32, Ty::MutF32], returns: &[Ty::F32] }), + &["modff"], + ), + ( + // `(f64, &mut f64) -> f64` as `(f64) -> (f64, f64)` + Signature { args: &[Ty::F64], returns: &[Ty::F64, Ty::F64] }, + Some(Signature { args: &[Ty::F64, Ty::MutF64], returns: &[Ty::F64] }), + &["modf"], + ), + ( + // `(f32, &mut c_int) -> f32` as `(f32) -> (f32, i32)` + Signature { args: &[Ty::F32], returns: &[Ty::F32, Ty::I32] }, + Some(Signature { args: &[Ty::F32, Ty::MutCInt], returns: &[Ty::F32] }), + &["frexpf", "lgammaf_r"], + ), + ( + // `(f64, &mut c_int) -> f64` as `(f64) -> (f64, i32)` + Signature { args: &[Ty::F64], returns: &[Ty::F64, Ty::I32] }, + Some(Signature { args: &[Ty::F64, Ty::MutCInt], returns: &[Ty::F64] }), + &["frexp", "lgamma_r"], + ), + ( + // `(f32, f32, &mut c_int) -> f32` as `(f32, f32) -> (f32, i32)` + Signature { args: &[Ty::F32, Ty::F32], returns: &[Ty::F32, Ty::I32] }, + Some(Signature { args: &[Ty::F32, Ty::F32, Ty::MutCInt], returns: &[Ty::F32] }), + &["remquof"], + ), + ( + // `(f64, f64, &mut c_int) -> f64` as `(f64, f64) -> (f64, i32)` + Signature { args: &[Ty::F64, Ty::F64], returns: &[Ty::F64, Ty::I32] }, + Some(Signature { args: &[Ty::F64, Ty::F64, Ty::MutCInt], returns: &[Ty::F64] }), + &["remquo"], + ), + ( + // `(f32, &mut f32, &mut f32)` as `(f32) -> (f32, f32)` + Signature { args: &[Ty::F32], returns: &[Ty::F32, Ty::F32] }, + Some(Signature { args: &[Ty::F32, Ty::MutF32, Ty::MutF32], returns: &[] }), + &["sincosf"], + ), + ( + // `(f64, &mut f64, &mut f64)` as `(f64) -> (f64, f64)` + Signature { args: &[Ty::F64], returns: &[Ty::F64, Ty::F64] }, + Some(Signature { args: &[Ty::F64, Ty::MutF64, Ty::MutF64], returns: &[] }), + &["sincos"], + ), +]; + +/// A type used in a function signature. +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +enum Ty { + F16, + F32, + F64, + F128, + I32, + CInt, + MutF16, + MutF32, + MutF64, + MutF128, + MutI32, + MutCInt, +} + +impl ToTokens for Ty { + fn to_tokens(&self, tokens: &mut pm2::TokenStream) { + let ts = match self { + Ty::F16 => quote! { f16 }, + Ty::F32 => quote! { f32 }, + Ty::F64 => quote! { f64 }, + Ty::F128 => quote! { f128 }, + Ty::I32 => quote! { i32 }, + Ty::CInt => quote! { ::core::ffi::c_int }, + Ty::MutF16 => quote! { &mut f16 }, + Ty::MutF32 => quote! { &mut f32 }, + Ty::MutF64 => quote! { &mut f64 }, + Ty::MutF128 => quote! { &mut f128 }, + Ty::MutI32 => quote! { &mut i32 }, + Ty::MutCInt => quote! { &mut core::ffi::c_int }, + }; + + tokens.extend(ts); + } +} + +/// Representation of e.g. `(f32, f32) -> f32` +#[derive(Debug, Clone)] +struct Signature { + args: &'static [Ty], + returns: &'static [Ty], +} + +/// Combined information about a function implementation. +#[derive(Debug, Clone)] +struct FunctionInfo { + name: &'static str, + /// Function signature for C implementations + c_sig: Signature, + /// Function signature for Rust implementations + rust_sig: Signature, +} + +/// A flat representation of `ALL_FUNCTIONS`. +static ALL_FUNCTIONS_FLAT: LazyLock> = LazyLock::new(|| { + let mut ret = Vec::new(); + + for (rust_sig, c_sig, names) in ALL_FUNCTIONS { + for name in *names { + let api = FunctionInfo { + name, + rust_sig: rust_sig.clone(), + c_sig: c_sig.clone().unwrap_or_else(|| rust_sig.clone()), + }; + ret.push(api); + } + } + + ret.sort_by_key(|item| item.name); + ret +}); + +/// Do something for each function present in this crate. +/// +/// Takes a callback macro and invokes it multiple times, once for each function that +/// this crate exports. This makes it easy to create generic tests, benchmarks, or other checks +/// and apply it to each symbol. +/// +/// Additionally, the `extra` and `fn_extra` patterns can make use of magic identifiers: +/// +/// - `MACRO_FN_NAME`: gets replaced with the name of the function on that invocation. +/// - `MACRO_FN_NAME_NORMALIZED`: similar to the above, but removes sufixes so e.g. `sinf` becomes +/// `sin`, `cosf128` becomes `cos`, etc. +/// +/// Invoke as: +/// +/// ``` +/// // Macro that is invoked once per function +/// macro_rules! callback_macro { +/// ( +/// // Name of that function +/// fn_name: $fn_name:ident, +/// // Function signature of the C version (e.g. `fn(f32, &mut f32) -> f32`) +/// CFn: $CFn:ty, +/// // A tuple representing the C version's arguments (e.g. `(f32, &mut f32)`) +/// CArgs: $CArgs:ty, +/// // The C version's return type (e.g. `f32`) +/// CRet: $CRet:ty, +/// // Function signature of the Rust version (e.g. `fn(f32) -> (f32, f32)`) +/// RustFn: $RustFn:ty, +/// // A tuple representing the Rust version's arguments (e.g. `(f32,)`) +/// RustArgs: $RustArgs:ty, +/// // The Rust version's return type (e.g. `(f32, f32)`) +/// RustRet: $RustRet:ty, +/// // Attributes for the current function, if any +/// attrs: [$($meta:meta)*] +/// // Extra tokens passed directly (if any) +/// extra: [$extra:ident], +/// // Extra function-tokens passed directly (if any) +/// fn_extra: $fn_extra:expr, +/// ) => { }; +/// } +/// +/// libm_macros::for_each_function! { +/// // The macro to invoke as a callback +/// callback: callback_macro, +/// // Functions to skip, i.e. `callback` shouldn't be called at all for these. +/// // +/// // This is an optional field. +/// skip: [sin, cos], +/// // Attributes passed as `attrs` for specific functions. For example, here the invocation +/// // with `sinf` and that with `cosf` will both get `meta1` and `meta2`, but no others will. +/// // +/// // This is an optional field. +/// attributes: [ +/// #[meta1] +/// #[meta2] +/// [sinf, cosf], +/// ], +/// // Any tokens that should be passed directly to all invocations of the callback. This can +/// // be used to pass local variables or other things the macro needs access to. +/// // +/// // This is an optional field. +/// extra: [foo], +/// // Similar to `extra`, but allow providing a pattern for only specific functions. Uses +/// // a simplified match-like syntax. +/// fn_extra: match MACRO_FN_NAME { +/// hypot | hypotf => |x| x.hypot(), +/// _ => |x| x, +/// }, +/// } +/// ``` +#[proc_macro] +pub fn for_each_function(tokens: pm::TokenStream) -> pm::TokenStream { + let input = syn::parse_macro_input!(tokens as Invocation); + + let res = StructuredInput::from_fields(input) + .and_then(|s_in| validate(&s_in).map(|fn_list| (s_in, fn_list))) + .and_then(|(s_in, fn_list)| expand(s_in, &fn_list)); + + match res { + Ok(ts) => ts.into(), + Err(e) => e.into_compile_error().into(), + } +} + +/// Check for any input that is structurally correct but has other problems. +/// +/// Returns the list of function names that we should expand for. +fn validate(input: &StructuredInput) -> syn::Result> { + // Collect lists of all functions that are provied as macro inputs in various fields (only, + // skip, attributes). + let attr_mentions = input + .attributes + .iter() + .flat_map(|map_list| map_list.iter()) + .flat_map(|attr_map| attr_map.names.iter()); + let only_mentions = input.only.iter().flat_map(|only_list| only_list.iter()); + let fn_extra_mentions = + input.fn_extra.iter().flat_map(|v| v.keys()).filter(|name| *name != "_"); + let all_mentioned_fns = + input.skip.iter().chain(only_mentions).chain(attr_mentions).chain(fn_extra_mentions); + + // Make sure that every function mentioned is a real function + for mentioned in all_mentioned_fns { + if !ALL_FUNCTIONS_FLAT.iter().any(|func| mentioned == func.name) { + let e = syn::Error::new( + mentioned.span(), + format!("unrecognized function name `{mentioned}`"), + ); + return Err(e); + } + } + + if !input.skip.is_empty() && input.only.is_some() { + let e = syn::Error::new( + input.only_span.unwrap(), + format!("only one of `skip` or `only` may be specified"), + ); + return Err(e); + } + + // Construct a list of what we intend to expand + let mut fn_list = Vec::new(); + for func in ALL_FUNCTIONS_FLAT.iter() { + let fn_name = func.name; + // If we have an `only` list and it does _not_ contain this function name, skip it + if input.only.as_ref().is_some_and(|only| !only.iter().any(|o| o == fn_name)) { + continue; + } + + // If there is a `skip` list that contains this function name, skip it + if input.skip.iter().any(|s| s == fn_name) { + continue; + } + + // Run everything else + fn_list.push(func); + } + + if let Some(map) = &input.fn_extra { + if !map.keys().any(|key| key == "_") { + // No default provided; make sure every expected function is covered + let mut fns_not_covered = Vec::new(); + for func in &fn_list { + if !map.keys().any(|key| key == func.name) { + // `name` was not mentioned in the `match` statement + fns_not_covered.push(func); + } + } + + if !fns_not_covered.is_empty() { + let e = syn::Error::new( + input.fn_extra_span.unwrap(), + format!( + "`fn_extra`: no default `_` pattern specified and the following \ + patterns are not covered: {fns_not_covered:#?}" + ), + ); + return Err(e); + } + } + }; + + Ok(fn_list) +} + +/// Expand our structured macro input into invocations of the callback macro. +fn expand(input: StructuredInput, fn_list: &[&FunctionInfo]) -> syn::Result { + let mut out = pm2::TokenStream::new(); + let default_ident = Ident::new("_", Span::call_site()); + let callback = input.callback; + + for func in fn_list { + let fn_name = Ident::new(func.name, Span::call_site()); + + // Prepare attributes in an `attrs: ...` field + let meta_field = match &input.attributes { + Some(attrs) => { + let meta = attrs + .iter() + .filter(|map| map.names.contains(&fn_name)) + .flat_map(|map| &map.meta); + quote! { attrs: [ #( #meta )* ] } + } + None => pm2::TokenStream::new(), + }; + + // Prepare extra in an `extra: ...` field, running the replacer + let extra_field = match input.extra.clone() { + Some(mut extra) => { + let mut v = MacroReplace::new(func.name); + v.visit_expr_mut(&mut extra); + v.finish()?; + + quote! { extra: #extra, } + } + None => pm2::TokenStream::new(), + }; + + // Prepare function-specific extra in a `fn_extra: ...` field, running the replacer + let fn_extra_field = match input.fn_extra { + Some(ref map) => { + let mut fn_extra = + map.get(&fn_name).or_else(|| map.get(&default_ident)).unwrap().clone(); + + let mut v = MacroReplace::new(func.name); + v.visit_expr_mut(&mut fn_extra); + v.finish()?; + + quote! { fn_extra: #fn_extra, } + } + None => pm2::TokenStream::new(), + }; + + let c_args = &func.c_sig.args; + let c_ret = &func.c_sig.returns; + let rust_args = &func.rust_sig.args; + let rust_ret = &func.rust_sig.returns; + + let new = quote! { + #callback! { + fn_name: #fn_name, + CFn: fn( #(#c_args),* ,) -> ( #(#c_ret),* ), + CArgs: ( #(#c_args),* ,), + CRet: ( #(#c_ret),* ), + RustFn: fn( #(#rust_args),* ,) -> ( #(#rust_ret),* ), + RustArgs: ( #(#rust_args),* ,), + RustRet: ( #(#rust_ret),* ), + #meta_field + #extra_field + #fn_extra_field + } + }; + + out.extend(new); + } + + Ok(out) +} + +/// Visitor to replace "magic" identifiers that we allow: `MACRO_FN_NAME` and +/// `MACRO_FN_NAME_NORMALIZED`. +struct MacroReplace { + fn_name: &'static str, + /// Remove the trailing `f` or `f128` to make + norm_name: String, + error: Option, +} + +impl MacroReplace { + fn new(name: &'static str) -> Self { + // Keep this in sync with `libm_test::canonical_name` + let known_mappings = &[ + ("erff", "erf"), + ("erf", "erf"), + ("lgammaf_r", "lgamma_r"), + ("modff", "modf"), + ("modf", "modf"), + ]; + + let norm_name = match known_mappings.iter().find(|known| known.0 == name) { + Some(found) => found.1, + None => name + .strip_suffix("f") + .or_else(|| name.strip_suffix("f16")) + .or_else(|| name.strip_suffix("f128")) + .unwrap_or(name), + }; + + Self { fn_name: name, norm_name: norm_name.to_owned(), error: None } + } + + fn finish(self) -> syn::Result<()> { + match self.error { + Some(e) => Err(e), + None => Ok(()), + } + } + + fn visit_ident_inner(&mut self, i: &mut Ident) { + let s = i.to_string(); + if !s.starts_with("MACRO") || self.error.is_some() { + return; + } + + match s.as_str() { + "MACRO_FN_NAME" => *i = Ident::new(self.fn_name, i.span()), + "MACRO_FN_NAME_NORMALIZED" => *i = Ident::new(&self.norm_name, i.span()), + _ => { + self.error = + Some(syn::Error::new(i.span(), format!("unrecognized meta expression `{s}`"))); + } + } + } +} + +impl VisitMut for MacroReplace { + fn visit_ident_mut(&mut self, i: &mut Ident) { + self.visit_ident_inner(i); + syn::visit_mut::visit_ident_mut(self, i); + } +} diff --git a/crates/libm-macros/src/parse.rs b/crates/libm-macros/src/parse.rs new file mode 100644 index 000000000..ee9bd524b --- /dev/null +++ b/crates/libm-macros/src/parse.rs @@ -0,0 +1,236 @@ +use std::collections::BTreeMap; + +use proc_macro2::Span; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream, Parser}; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::Comma; +use syn::{Arm, Attribute, Expr, ExprMatch, Ident, Meta, Token, bracketed}; + +/// The input to our macro; just a list of `field: value` items. +#[derive(Debug)] +pub struct Invocation { + fields: Punctuated, +} + +impl Parse for Invocation { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { fields: input.parse_terminated(Mapping::parse, Token![,])? }) + } +} + +/// A `key: expression` mapping with nothing else. Basically a simplified `syn::Field`. +#[derive(Debug)] +struct Mapping { + name: Ident, + _sep: Token![:], + expr: Expr, +} + +impl Parse for Mapping { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { name: input.parse()?, _sep: input.parse()?, expr: input.parse()? }) + } +} + +/// The input provided to our proc macro, after parsing into the form we expect. +#[derive(Debug)] +pub struct StructuredInput { + /// Macro to invoke once per function + pub callback: Ident, + /// Skip these functions + pub skip: Vec, + /// Invoke only for these functions + pub only: Option>, + /// Attributes that get applied to specific functions + pub attributes: Option>, + /// Extra expressions to pass to all invocations of the macro + pub extra: Option, + /// Per-function extra expressions to pass to the macro + pub fn_extra: Option>, + // For diagnostics + pub only_span: Option, + pub fn_extra_span: Option, +} + +impl StructuredInput { + pub fn from_fields(input: Invocation) -> syn::Result { + let mut map: Vec<_> = input.fields.into_iter().collect(); + let cb_expr = expect_field(&mut map, "callback")?; + let skip_expr = expect_field(&mut map, "skip").ok(); + let only_expr = expect_field(&mut map, "only").ok(); + let attr_expr = expect_field(&mut map, "attributes").ok(); + let extra = expect_field(&mut map, "extra").ok(); + let fn_extra = expect_field(&mut map, "fn_extra").ok(); + + if !map.is_empty() { + Err(syn::Error::new( + map.first().unwrap().name.span(), + format!("unexpected fields {map:?}"), + ))?; + } + + let skip = match skip_expr { + Some(expr) => Parser::parse2(parse_ident_array, expr.into_token_stream())?, + None => Vec::new(), + }; + + let only_span = only_expr.as_ref().map(|expr| expr.span()); + let only = match only_expr { + Some(expr) => Some(Parser::parse2(parse_ident_array, expr.into_token_stream())?), + None => None, + }; + + let attributes = match attr_expr { + Some(expr) => { + let mut attributes = Vec::new(); + let attr_exprs = Parser::parse2(parse_expr_array, expr.into_token_stream())?; + + for attr in attr_exprs { + attributes.push(syn::parse2(attr.into_token_stream())?); + } + Some(attributes) + } + None => None, + }; + + let fn_extra_span = fn_extra.as_ref().map(|expr| expr.span()); + let fn_extra = match fn_extra { + Some(expr) => Some(extract_fn_extra_field(expr)?), + None => None, + }; + + Ok(Self { + callback: expect_ident(cb_expr)?, + skip, + only, + only_span, + attributes, + extra, + fn_extra, + fn_extra_span, + }) + } +} + +fn extract_fn_extra_field(expr: Expr) -> syn::Result> { + let Expr::Match(mexpr) = expr else { + let e = syn::Error::new(expr.span(), "`fn_extra` expects a match expression"); + return Err(e); + }; + + let ExprMatch { attrs, match_token: _, expr, brace_token: _, arms } = mexpr; + + expect_empty_attrs(&attrs)?; + + let match_on = expect_ident(*expr)?; + if match_on != "MACRO_FN_NAME" { + let e = syn::Error::new(match_on.span(), "only allowed to match on `MACRO_FN_NAME`"); + return Err(e); + } + + let mut res = BTreeMap::new(); + + for arm in arms { + let Arm { attrs, pat, guard, fat_arrow_token: _, body, comma: _ } = arm; + + expect_empty_attrs(&attrs)?; + + let keys = match pat { + syn::Pat::Wild(w) => vec![Ident::new("_", w.span())], + _ => Parser::parse2(parse_ident_pat, pat.into_token_stream())?, + }; + + if let Some(guard) = guard { + let e = syn::Error::new(guard.0.span(), "no guards allowed in this position"); + return Err(e); + } + + for key in keys { + let inserted = res.insert(key.clone(), *body.clone()); + if inserted.is_some() { + let e = syn::Error::new(key.span(), format!("key `{key}` specified twice")); + return Err(e); + } + } + } + + Ok(res) +} + +fn expect_empty_attrs(attrs: &[Attribute]) -> syn::Result<()> { + if attrs.is_empty() { + return Ok(()); + } + + let e = + syn::Error::new(attrs.first().unwrap().span(), "no attributes allowed in this position"); + Err(e) +} + +/// Extract a named field from a map, raising an error if it doesn't exist. +fn expect_field(v: &mut Vec, name: &str) -> syn::Result { + let pos = v.iter().position(|v| v.name == name).ok_or_else(|| { + syn::Error::new(Span::call_site(), format!("missing expected field `{name}`")) + })?; + + Ok(v.remove(pos).expr) +} + +/// Coerce an expression into a simple identifier. +fn expect_ident(expr: Expr) -> syn::Result { + syn::parse2(expr.into_token_stream()) +} + +/// Parse an array of expressions. +fn parse_expr_array(input: ParseStream) -> syn::Result> { + let content; + let _ = bracketed!(content in input); + let fields = content.parse_terminated(Expr::parse, Token![,])?; + Ok(fields.into_iter().collect()) +} + +/// Parse an array of idents, e.g. `[foo, bar, baz]`. +fn parse_ident_array(input: ParseStream) -> syn::Result> { + let content; + let _ = bracketed!(content in input); + let fields = content.parse_terminated(Ident::parse, Token![,])?; + Ok(fields.into_iter().collect()) +} + +/// Parse an pattern of idents, specifically `(foo | bar | baz)`. +fn parse_ident_pat(input: ParseStream) -> syn::Result> { + if !input.peek2(Token![|]) { + return Ok(vec![input.parse()?]); + } + + let fields = Punctuated::::parse_separated_nonempty(input)?; + Ok(fields.into_iter().collect()) +} + +/// A mapping of attributes to identifiers (just a simplified `Expr`). +/// +/// Expressed as: +/// +/// ```ignore +/// #[meta1] +/// #[meta2] +/// [foo, bar, baz] +/// ``` +#[derive(Debug)] +pub struct AttributeMap { + pub meta: Vec, + pub names: Vec, +} + +impl Parse for AttributeMap { + fn parse(input: ParseStream) -> syn::Result { + let attrs = input.call(Attribute::parse_outer)?; + + Ok(Self { + meta: attrs.into_iter().map(|a| a.meta).collect(), + names: parse_ident_array(input)?, + }) + } +} diff --git a/crates/libm-macros/tests/basic.rs b/crates/libm-macros/tests/basic.rs new file mode 100644 index 000000000..8f8c09f1b --- /dev/null +++ b/crates/libm-macros/tests/basic.rs @@ -0,0 +1,96 @@ +// `STATUS_DLL_NOT_FOUND` on i686 MinGW, not worth looking into. +#![cfg(not(all(target_arch = "x86", target_os = "windows", target_env = "gnu")))] + +macro_rules! basic { + ( + fn_name: $fn_name:ident, + CFn: $CFn:ty, + CArgs: $CArgs:ty, + CRet: $CRet:ty, + RustFn: $RustFn:ty, + RustArgs: $RustArgs:ty, + RustRet: $RustRet:ty, + attrs: [$($meta:meta)*] + extra: [$($extra_tt:tt)*], + fn_extra: $fn_extra:expr, + ) => { + $(#[$meta])* + mod $fn_name { + #[allow(unused)] + type CFnTy = $CFn; + // type CArgsTy<'_> = $CArgs; + // type CRetTy<'_> = $CRet; + #[allow(unused)] + type RustFnTy = $RustFn; + #[allow(unused)] + type RustArgsTy = $RustArgs; + #[allow(unused)] + type RustRetTy = $RustRet; + #[allow(unused)] + const A: &[&str] = &[$($extra_tt)*]; + #[allow(unused)] + fn foo(a: f32) -> f32 { + $fn_extra(a) + } + } + }; +} + +mod test_basic { + libm_macros::for_each_function! { + callback: basic, + skip: [sin, cos], + attributes: [ + // just some random attributes + #[allow(clippy::pedantic)] + #[allow(dead_code)] + [sinf, cosf] + ], + extra: ["foo", "bar"], + fn_extra: match MACRO_FN_NAME { + sin => |x| x + 2.0, + cos | cosf => |x: f32| x.MACRO_FN_NAME_NORMALIZED(), + _ => |_x| 100.0 + } + } +} + +macro_rules! basic_no_extra { + ( + fn_name: $fn_name:ident, + CFn: $CFn:ty, + CArgs: $CArgs:ty, + CRet: $CRet:ty, + RustFn: $RustFn:ty, + RustArgs: $RustArgs:ty, + RustRet: $RustRet:ty, + ) => { + mod $fn_name { + #[allow(unused)] + type CFnTy = $CFn; + // type CArgsTy<'_> = $CArgs; + // type CRetTy<'_> = $CRet; + #[allow(unused)] + type RustFnTy = $RustFn; + #[allow(unused)] + type RustArgsTy = $RustArgs; + #[allow(unused)] + type RustRetTy = $RustRet; + } + }; +} + +mod test_basic_no_extra { + // Test with no extra, no skip, and no attributes + libm_macros::for_each_function! { + callback: basic_no_extra, + } +} + +mod test_only { + // Test that only works + libm_macros::for_each_function! { + callback: basic_no_extra, + only: [sin, sinf], + } +} diff --git a/crates/libm-test/Cargo.toml b/crates/libm-test/Cargo.toml index 6367bdca5..703524bcd 100644 --- a/crates/libm-test/Cargo.toml +++ b/crates/libm-test/Cargo.toml @@ -11,8 +11,21 @@ default = [] # musl libc. test-musl-serialized = ["rand"] +# Build our own musl for testing and benchmarks +build-musl = ["dep:musl-math-sys"] + [dependencies] +anyhow = "1.0.90" libm = { path = "../.." } +libm-macros = { path = "../libm-macros" } +musl-math-sys = { path = "../musl-math-sys", optional = true } +paste = "1.0.15" +rand = "0.8.5" +rand_chacha = "0.3.1" + +[target.'cfg(target_family = "wasm")'.dependencies] +# Enable randomness on WASM +getrandom = { version = "0.2", features = ["js"] } [build-dependencies] rand = { version = "0.8.5", optional = true } diff --git a/crates/libm-test/build.rs b/crates/libm-test/build.rs index 9653bd830..472dec9d3 100644 --- a/crates/libm-test/build.rs +++ b/crates/libm-test/build.rs @@ -1,10 +1,106 @@ +use std::fmt::Write; +use std::path::PathBuf; +use std::{env, fs}; + fn main() { + let cfg = Config::from_env(); + + emit_optimization_cfg(&cfg); + emit_cfg_shorthands(&cfg); + list_all_tests(&cfg); + #[cfg(feature = "test-musl-serialized")] - musl_reference_tests::generate(); + musl_serialized_tests::generate(); +} + +#[allow(dead_code)] +struct Config { + manifest_dir: PathBuf, + out_dir: PathBuf, + opt_level: u8, + target_arch: String, + target_env: String, + target_family: Option, + target_os: String, + target_string: String, + target_vendor: String, + target_features: Vec, +} + +impl Config { + fn from_env() -> Self { + let target_features = env::var("CARGO_CFG_TARGET_FEATURE") + .map(|feats| feats.split(',').map(ToOwned::to_owned).collect()) + .unwrap_or_default(); + + Self { + manifest_dir: PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()), + out_dir: PathBuf::from(env::var("OUT_DIR").unwrap()), + opt_level: env::var("OPT_LEVEL").unwrap().parse().unwrap(), + target_arch: env::var("CARGO_CFG_TARGET_ARCH").unwrap(), + target_env: env::var("CARGO_CFG_TARGET_ENV").unwrap(), + target_family: env::var("CARGO_CFG_TARGET_FAMILY").ok(), + target_os: env::var("CARGO_CFG_TARGET_OS").unwrap(), + target_string: env::var("TARGET").unwrap(), + target_vendor: env::var("CARGO_CFG_TARGET_VENDOR").unwrap(), + target_features, + } + } +} + +/// Some tests are extremely slow. Emit a config option based on optimization level. +fn emit_optimization_cfg(cfg: &Config) { + println!("cargo::rustc-check-cfg=cfg(optimizations_enabled)"); + + if cfg.opt_level >= 2 { + println!("cargo::rustc-cfg=optimizations_enabled"); + } +} + +/// Provide an alias for common longer config combinations. +fn emit_cfg_shorthands(cfg: &Config) { + println!("cargo::rustc-check-cfg=cfg(x86_no_sse)"); + if cfg.target_arch == "x86" && !cfg.target_features.iter().any(|f| f == "sse") { + // Shorthand to detect i586 targets + println!("cargo::rustc-cfg=x86_no_sse"); + } +} + +/// 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(); } +/// At build time, generate the output of what the corresponding `*musl` target does with a range +/// of inputs. +/// +/// Serialize that target's output, run the same thing with our symbols, then load and compare +/// the resulting values. #[cfg(feature = "test-musl-serialized")] -mod musl_reference_tests { +mod musl_serialized_tests { use std::path::PathBuf; use std::process::Command; use std::{env, fs}; diff --git a/crates/libm-test/src/gen.rs b/crates/libm-test/src/gen.rs new file mode 100644 index 000000000..3e9eca37a --- /dev/null +++ b/crates/libm-test/src/gen.rs @@ -0,0 +1,72 @@ +//! Different generators that can create random or systematic bit patterns. + +use crate::GenerateInput; +pub mod random; + +/// Helper type to turn any reusable input into a generator. +#[derive(Clone, Debug, Default)] +pub struct CachedInput { + pub inputs_f32: Vec<(f32, f32, f32)>, + pub inputs_f64: Vec<(f64, f64, f64)>, + pub inputs_i32: Vec<(i32, i32, i32)>, +} + +impl GenerateInput<(f32,)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + self.inputs_f32.iter().map(|f| (f.0,)) + } +} + +impl GenerateInput<(f32, f32)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + self.inputs_f32.iter().map(|f| (f.0, f.1)) + } +} + +impl GenerateInput<(i32, f32)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + self.inputs_i32.iter().zip(self.inputs_f32.iter()).map(|(i, f)| (i.0, f.0)) + } +} + +impl GenerateInput<(f32, i32)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + GenerateInput::<(i32, f32)>::get_cases(self).map(|(i, f)| (f, i)) + } +} + +impl GenerateInput<(f32, f32, f32)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + self.inputs_f32.iter().copied() + } +} + +impl GenerateInput<(f64,)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + self.inputs_f64.iter().map(|f| (f.0,)) + } +} + +impl GenerateInput<(f64, f64)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + self.inputs_f64.iter().map(|f| (f.0, f.1)) + } +} + +impl GenerateInput<(i32, f64)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + self.inputs_i32.iter().zip(self.inputs_f64.iter()).map(|(i, f)| (i.0, f.0)) + } +} + +impl GenerateInput<(f64, i32)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + GenerateInput::<(i32, f64)>::get_cases(self).map(|(i, f)| (f, i)) + } +} + +impl GenerateInput<(f64, f64, f64)> for CachedInput { + fn get_cases(&self) -> impl Iterator { + self.inputs_f64.iter().copied() + } +} diff --git a/crates/libm-test/src/gen/random.rs b/crates/libm-test/src/gen/random.rs new file mode 100644 index 000000000..e59643195 --- /dev/null +++ b/crates/libm-test/src/gen/random.rs @@ -0,0 +1,125 @@ +//! A simple generator that produces deterministic random input, caching to use the same +//! inputs for all functions. + +use std::sync::LazyLock; + +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +use super::CachedInput; +use crate::GenerateInput; + +const SEED: [u8; 32] = *b"3.141592653589793238462643383279"; + +/// Number of tests to run. +const NTESTS: usize = { + let ntests = if cfg!(optimizations_enabled) { + if cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64") { + 5_000_000 + } else if !cfg!(target_pointer_width = "64") + || cfg!(all(target_arch = "x86_64", target_vendor = "apple")) + || option_env!("EMULATED").is_some() + && cfg!(any(target_arch = "aarch64", target_arch = "powerpc64")) + { + // Tests are pretty slow on: + // - Non-64-bit targets + // - Emulated ppc + // - Emulated aarch64 + // - x86 MacOS + // So reduce the number of iterations + 100_000 + } else { + // Most everything else gets tested in docker and works okay, but we still + // don't need 20 minutes of tests. + 1_000_000 + } + } else { + 800 + }; + + ntests +}; + +/// Tested inputs. +static TEST_CASES: LazyLock = LazyLock::new(|| make_test_cases(NTESTS)); + +/// The first argument to `jn` and `jnf` is the number of iterations. Make this a reasonable +/// value so tests don't run forever. +static TEST_CASES_JN: LazyLock = LazyLock::new(|| { + // Start with regular test cases + let mut cases = (&*TEST_CASES).clone(); + + // These functions are extremely slow, limit them + cases.inputs_i32.truncate((NTESTS / 1000).max(80)); + cases.inputs_f32.truncate((NTESTS / 1000).max(80)); + cases.inputs_f64.truncate((NTESTS / 1000).max(80)); + + // It is easy to overflow the stack with these in debug mode + let max_iterations = if cfg!(optimizations_enabled) && cfg!(target_pointer_width = "64") { + 0xffff + } else if cfg!(windows) { + 0x00ff + } else { + 0x0fff + }; + + let mut rng = ChaCha8Rng::from_seed(SEED); + + for case in cases.inputs_i32.iter_mut() { + case.0 = rng.gen_range(3..=max_iterations); + } + + cases +}); + +fn make_test_cases(ntests: usize) -> CachedInput { + let mut rng = ChaCha8Rng::from_seed(SEED); + + // make sure we include some basic cases + let mut inputs_i32 = vec![(0, 0, 0), (1, 1, 1), (-1, -1, -1)]; + let mut inputs_f32 = vec![ + (0.0, 0.0, 0.0), + (f32::EPSILON, f32::EPSILON, f32::EPSILON), + (f32::INFINITY, f32::INFINITY, f32::INFINITY), + (f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY), + (f32::MAX, f32::MAX, f32::MAX), + (f32::MIN, f32::MIN, f32::MIN), + (f32::MIN_POSITIVE, f32::MIN_POSITIVE, f32::MIN_POSITIVE), + (f32::NAN, f32::NAN, f32::NAN), + ]; + let mut inputs_f64 = vec![ + (0.0, 0.0, 0.0), + (f64::EPSILON, f64::EPSILON, f64::EPSILON), + (f64::INFINITY, f64::INFINITY, f64::INFINITY), + (f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY), + (f64::MAX, f64::MAX, f64::MAX), + (f64::MIN, f64::MIN, f64::MIN), + (f64::MIN_POSITIVE, f64::MIN_POSITIVE, f64::MIN_POSITIVE), + (f64::NAN, f64::NAN, f64::NAN), + ]; + + inputs_i32.extend((0..(ntests - inputs_i32.len())).map(|_| rng.gen::<(i32, i32, i32)>())); + + // Generate integers to get a full range of bitpatterns, then convert back to + // floats. + inputs_f32.extend((0..(ntests - inputs_f32.len())).map(|_| { + let ints = rng.gen::<(u32, u32, u32)>(); + (f32::from_bits(ints.0), f32::from_bits(ints.1), f32::from_bits(ints.2)) + })); + inputs_f64.extend((0..(ntests - inputs_f64.len())).map(|_| { + let ints = rng.gen::<(u64, u64, u64)>(); + (f64::from_bits(ints.0), f64::from_bits(ints.1), f64::from_bits(ints.2)) + })); + + CachedInput { inputs_f32, inputs_f64, inputs_i32 } +} + +/// Create a test case iterator. +pub fn get_test_cases(fname: &str) -> impl Iterator +where + CachedInput: GenerateInput, +{ + let inputs = if fname == "jn" || fname == "jnf" { &TEST_CASES_JN } else { &TEST_CASES }; + + CachedInput::get_cases(inputs) +} diff --git a/crates/libm-test/src/lib.rs b/crates/libm-test/src/lib.rs index 8b1378917..3baf77524 100644 --- a/crates/libm-test/src/lib.rs +++ b/crates/libm-test/src/lib.rs @@ -1 +1,64 @@ +pub mod gen; +mod num_traits; +mod special_case; +mod test_traits; +pub use num_traits::{Float, Hex, Int}; +pub use special_case::{MaybeOverride, SpecialCase}; +pub use test_traits::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, TupleCall}; + +/// Result type for tests is usually from `anyhow`. Most times there is no success value to +/// propagate. +pub type TestResult = Result; + +// List of all files present in libm's source +include!(concat!(env!("OUT_DIR"), "/all_files.rs")); + +/// ULP allowed to differ from musl (note that musl itself may not be accurate). +const MUSL_DEFAULT_ULP: u32 = 2; + +/// Certain functions have different allowed ULP (consider these xfail). +/// +/// Note that these results were obtained using 400,000,000 rounds of random inputs, which +/// is not a value used by default. +pub fn musl_allowed_ulp(name: &str) -> u32 { + match name { + #[cfg(x86_no_sse)] + "asinh" | "asinhf" => 6, + "lgamma" | "lgamma_r" | "lgammaf" | "lgammaf_r" => 400, + "tanh" | "tanhf" => 4, + "tgamma" => 20, + "j0" | "j0f" | "j1" | "j1f" => { + // Results seem very target-dependent + if cfg!(target_arch = "x86_64") { 4000 } else { 800_000 } + } + "jn" | "jnf" => 1000, + "sincosf" => 500, + #[cfg(not(target_pointer_width = "64"))] + "exp10" => 4, + #[cfg(not(target_pointer_width = "64"))] + "exp10f" => 4, + _ => MUSL_DEFAULT_ULP, + } +} + +/// Return the unsuffixed version of a function name; e.g. `abs` and `absf` both return `abs`, +/// `lgamma_r` and `lgammaf_r` both return `lgamma_r`. +pub fn canonical_name(name: &str) -> &str { + let known_mappings = &[ + ("erff", "erf"), + ("erf", "erf"), + ("lgammaf_r", "lgamma_r"), + ("modff", "modf"), + ("modf", "modf"), + ]; + + match known_mappings.iter().find(|known| known.0 == name) { + Some(found) => found.1, + None => name + .strip_suffix("f") + .or_else(|| name.strip_suffix("f16")) + .or_else(|| name.strip_suffix("f128")) + .unwrap_or(name), + } +} diff --git a/crates/libm-test/src/num_traits.rs b/crates/libm-test/src/num_traits.rs new file mode 100644 index 000000000..e16f4e4dc --- /dev/null +++ b/crates/libm-test/src/num_traits.rs @@ -0,0 +1,214 @@ +use std::fmt; + +use crate::{MaybeOverride, SpecialCase, TestResult}; + +/// Common types and methods for floating point numbers. +pub trait Float: Copy + fmt::Display + fmt::Debug + PartialEq { + type Int: Int; + type SignedInt: Int + Int; + + const ZERO: Self; + const ONE: Self; + + /// The bitwidth of the float type + const BITS: u32; + + /// The bitwidth of the significand + const SIGNIFICAND_BITS: u32; + + /// The bitwidth of the exponent + const EXPONENT_BITS: u32 = Self::BITS - Self::SIGNIFICAND_BITS - 1; + + fn is_nan(self) -> bool; + fn is_infinite(self) -> bool; + fn to_bits(self) -> Self::Int; + fn from_bits(bits: Self::Int) -> Self; + fn signum(self) -> Self; +} + +macro_rules! impl_float { + ($($fty:ty, $ui:ty, $si:ty, $significand_bits:expr;)+) => { + $( + impl Float for $fty { + type Int = $ui; + type SignedInt = $si; + + const ZERO: Self = 0.0; + const ONE: Self = 1.0; + + const BITS: u32 = <$ui>::BITS; + const SIGNIFICAND_BITS: u32 = $significand_bits; + + fn is_nan(self) -> bool { + self.is_nan() + } + fn is_infinite(self) -> bool { + self.is_infinite() + } + fn to_bits(self) -> Self::Int { + self.to_bits() + } + fn from_bits(bits: Self::Int) -> Self { + Self::from_bits(bits) + } + fn signum(self) -> Self { + self.signum() + } + } + + impl Hex for $fty { + fn hex(self) -> String { + self.to_bits().hex() + } + } + )+ + } +} + +impl_float!( + f32, u32, i32, 23; + f64, u64, i64, 52; +); + +/// Common types and methods for integers. +pub trait Int: Copy + fmt::Display + fmt::Debug + PartialEq { + type OtherSign: Int; + type Unsigned: Int; + const BITS: u32; + const SIGNED: bool; + + fn signed(self) -> ::OtherSign; + fn unsigned(self) -> Self::Unsigned; + fn checked_sub(self, other: Self) -> Option; + fn abs(self) -> Self; +} + +macro_rules! impl_int { + ($($ui:ty, $si:ty ;)+) => { + $( + impl Int for $ui { + type OtherSign = $si; + type Unsigned = Self; + const BITS: u32 = <$ui>::BITS; + const SIGNED: bool = false; + fn signed(self) -> Self::OtherSign { + self as $si + } + fn unsigned(self) -> Self { + self + } + fn checked_sub(self, other: Self) -> Option { + self.checked_sub(other) + } + fn abs(self) -> Self { + unimplemented!() + } + } + + impl Int for $si { + type OtherSign = $ui; + type Unsigned = $ui; + const BITS: u32 = <$ui>::BITS; + const SIGNED: bool = true; + fn signed(self) -> Self { + self + } + fn unsigned(self) -> $ui { + self as $ui + } + fn checked_sub(self, other: Self) -> Option { + self.checked_sub(other) + } + fn abs(self) -> Self { + self.abs() + } + } + + impl_int!(@for_both $si); + impl_int!(@for_both $ui); + + )+ + }; + + (@for_both $ty:ty) => { + impl Hex for $ty { + fn hex(self) -> String { + format!("{self:#0width$x}", width = ((Self::BITS / 4) + 2) as usize) + } + } + + impl $crate::CheckOutput for $ty + where + Input: Hex + fmt::Debug, + SpecialCase: MaybeOverride, + { + fn validate<'a>( + self, + expected: Self, + input: Input, + ctx: &$crate::CheckCtx, + ) -> TestResult { + if let Some(res) = SpecialCase::check_int(input, self, expected, ctx) { + return res; + } + + anyhow::ensure!( + self == expected, + "\ + \n input: {input:?} {ibits}\ + \n expected: {expected:<22?} {expbits}\ + \n actual: {self:<22?} {actbits}\ + ", + actbits = self.hex(), + expbits = expected.hex(), + ibits = input.hex(), + ); + + Ok(()) + } + } + } +} + +impl_int!( + u32, i32; + u64, i64; +); + +/// A helper trait to print something as hex with the correct number of nibbles, e.g. a `u32` +/// will always print with `0x` followed by 8 digits. +/// +/// This is only used for printing errors so allocating is okay. +pub trait Hex: Copy { + fn hex(self) -> String; +} + +impl Hex for (T1,) +where + T1: Hex, +{ + fn hex(self) -> String { + format!("({},)", self.0.hex()) + } +} + +impl Hex for (T1, T2) +where + T1: Hex, + T2: Hex, +{ + fn hex(self) -> String { + format!("({}, {})", self.0.hex(), self.1.hex()) + } +} + +impl Hex for (T1, T2, T3) +where + T1: Hex, + T2: Hex, + T3: Hex, +{ + fn hex(self) -> String { + format!("({}, {}, {})", self.0.hex(), self.1.hex(), self.2.hex()) + } +} diff --git a/crates/libm-test/src/special_case.rs b/crates/libm-test/src/special_case.rs new file mode 100644 index 000000000..df263d742 --- /dev/null +++ b/crates/libm-test/src/special_case.rs @@ -0,0 +1,239 @@ +//! Configuration for skipping or changing the result for individual test cases (inputs) rather +//! than ignoring entire tests. + +use core::f32; + +use crate::{CheckBasis, CheckCtx, Float, Int, TestResult}; + +/// Type implementing [`IgnoreCase`]. +pub struct SpecialCase; + +/// Don't run further validation on this test case. +const SKIP: Option = Some(Ok(())); + +/// Return this to skip checks on a test that currently fails but shouldn't. Looks +/// the same as skip, but we keep them separate to better indicate purpose. +const XFAIL: Option = Some(Ok(())); + +/// Allow overriding the outputs of specific test cases. +/// +/// There are some cases where we want to xfail specific cases or handle certain inputs +/// differently than the rest of calls to `validate`. This provides a hook to do that. +/// +/// If `None` is returned, checks will proceed as usual. If `Some(result)` is returned, checks +/// are skipped and the provided result is returned instead. +/// +/// This gets implemented once per input type, then the functions provide further filtering +/// based on function name and values. +/// +/// `ulp` can also be set to adjust the ULP for that specific test, even if `None` is still +/// returned. +pub trait MaybeOverride { + fn check_float( + _input: Input, + _actual: F, + _expected: F, + _ulp: &mut u32, + _ctx: &CheckCtx, + ) -> Option { + None + } + + fn check_int( + _input: Input, + _actual: I, + _expected: I, + _ctx: &CheckCtx, + ) -> Option { + None + } +} + +impl MaybeOverride<(f32,)> for SpecialCase { + fn check_float( + input: (f32,), + actual: F, + expected: F, + _ulp: &mut u32, + ctx: &CheckCtx, + ) -> Option { + if ctx.basis == CheckBasis::Musl { + if ctx.fname == "acoshf" && input.0 < -1.0 { + // acoshf is undefined for x <= 1.0, but we return a random result at lower + // values. + return XFAIL; + } + + if ctx.fname == "sincosf" { + let factor_frac_pi_2 = input.0.abs() / f32::consts::FRAC_PI_2; + if (factor_frac_pi_2 - factor_frac_pi_2.round()).abs() < 1e-2 { + // we have a bad approximation near multiples of pi/2 + return XFAIL; + } + } + + if ctx.fname == "expm1f" && input.0 > 80.0 && actual.is_infinite() { + // we return infinity but the number is representable + return XFAIL; + } + + if ctx.fname == "sinhf" && input.0.abs() > 80.0 && actual.is_nan() { + // we return some NaN that should be real values or infinite + // doesn't seem to happen on x86 + return XFAIL; + } + + if ctx.fname == "lgammaf" || ctx.fname == "lgammaf_r" && input.0 < 0.0 { + // loggamma should not be defined for x < 0, yet we both return results + return XFAIL; + } + } + + maybe_check_nan_bits(actual, expected, ctx) + } +} + +impl MaybeOverride<(f64,)> for SpecialCase { + fn check_float( + input: (f64,), + actual: F, + expected: F, + _ulp: &mut u32, + ctx: &CheckCtx, + ) -> Option { + if ctx.basis == CheckBasis::Musl { + if cfg!(target_arch = "x86") && ctx.fname == "acosh" && input.0 < 1.0 { + // The function is undefined, both implementations return random results + return SKIP; + } + + if cfg!(x86_no_sse) + && ctx.fname == "ceil" + && input.0 < 0.0 + && input.0 > -1.0 + && expected == F::ZERO + && actual == F::ZERO + { + // musl returns -0.0, we return +0.0 + return XFAIL; + } + + if ctx.fname == "lgamma" || ctx.fname == "lgamma_r" && input.0 < 0.0 { + // loggamma should not be defined for x < 0, yet we both return results + return XFAIL; + } + } + + maybe_check_nan_bits(actual, expected, ctx) + } +} + +/// Check NaN bits if the function requires it +fn maybe_check_nan_bits(actual: F, expected: F, ctx: &CheckCtx) -> Option { + if !(ctx.canonical_name == "fabs" || ctx.canonical_name == "copysign") { + return None; + } + + // LLVM currently uses x87 instructions which quieten signalling NaNs to handle the i686 + // `extern "C"` `f32`/`f64` return ABI. + // LLVM issue + // Rust issue + if cfg!(target_arch = "x86") && ctx.basis == CheckBasis::Musl { + return SKIP; + } + + // abs and copysign require signaling NaNs to be propagated, so verify bit equality. + if actual.to_bits() == expected.to_bits() { + return SKIP; + } else { + Some(Err(anyhow::anyhow!("NaNs have different bitpatterns"))) + } +} + +impl MaybeOverride<(f32, f32)> for SpecialCase { + fn check_float( + input: (f32, f32), + _actual: F, + expected: F, + _ulp: &mut u32, + ctx: &CheckCtx, + ) -> Option { + maybe_skip_min_max_nan(input, expected, ctx) + } +} +impl MaybeOverride<(f64, f64)> for SpecialCase { + fn check_float( + input: (f64, f64), + _actual: F, + expected: F, + _ulp: &mut u32, + ctx: &CheckCtx, + ) -> Option { + maybe_skip_min_max_nan(input, expected, ctx) + } +} + +/// Musl propagates NaNs if one is provided as the input, but we return the other input. +// F1 and F2 are always the same type, this is just to please generics +fn maybe_skip_min_max_nan( + input: (F1, F1), + expected: F2, + ctx: &CheckCtx, +) -> Option { + if (ctx.canonical_name == "fmax" || ctx.canonical_name == "fmin") + && (input.0.is_nan() || input.1.is_nan()) + && expected.is_nan() + { + return XFAIL; + } else { + None + } +} + +impl MaybeOverride<(i32, f32)> for SpecialCase { + fn check_float( + input: (i32, f32), + _actual: F, + _expected: F, + ulp: &mut u32, + ctx: &CheckCtx, + ) -> Option { + bessel_prec_dropoff(input, ulp, ctx) + } +} +impl MaybeOverride<(i32, f64)> for SpecialCase { + fn check_float( + input: (i32, f64), + _actual: F, + _expected: F, + ulp: &mut u32, + ctx: &CheckCtx, + ) -> Option { + bessel_prec_dropoff(input, ulp, ctx) + } +} + +/// Our bessel functions blow up with large N values +fn bessel_prec_dropoff( + input: (i32, F), + ulp: &mut u32, + ctx: &CheckCtx, +) -> Option { + if ctx.canonical_name == "jn" { + if input.0 > 4000 { + return XFAIL; + } else if input.0 > 2000 { + // *ulp = 20_000; + *ulp = 20000; + } else if input.0 > 1000 { + *ulp = 4000; + } + } + + None +} + +impl MaybeOverride<(f32, f32, f32)> for SpecialCase {} +impl MaybeOverride<(f64, f64, f64)> for SpecialCase {} +impl MaybeOverride<(f32, i32)> for SpecialCase {} +impl MaybeOverride<(f64, i32)> for SpecialCase {} diff --git a/crates/libm-test/src/test_traits.rs b/crates/libm-test/src/test_traits.rs new file mode 100644 index 000000000..c24ac6e43 --- /dev/null +++ b/crates/libm-test/src/test_traits.rs @@ -0,0 +1,248 @@ +//! Traits related to testing. +//! +//! There are three main traits in this module: +//! +//! - `GenerateInput`: implemented on any types that create test cases. +//! - `TupleCall`: implemented on tuples to allow calling them as function arguments. +//! - `CheckOutput`: implemented on anything that is an output type for validation against an +//! expected value. + +use std::fmt; + +use anyhow::{Context, bail, ensure}; + +use crate::{Float, Hex, Int, MaybeOverride, SpecialCase, TestResult}; + +/// Implement this on types that can generate a sequence of tuples for test input. +pub trait GenerateInput { + fn get_cases(&self) -> impl Iterator; +} + +/// Trait for calling a function with a tuple as arguments. +/// +/// Implemented on the tuple with the function signature as the generic (so we can use the same +/// tuple for multiple signatures). +pub trait TupleCall: fmt::Debug { + type Output; + fn call(self, f: Func) -> Self::Output; +} + +/// Context passed to [`CheckOutput`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CheckCtx { + /// Allowed ULP deviation + pub ulp: u32, + /// Function name. + pub fname: &'static str, + /// Return the unsuffixed version of the function name. + pub canonical_name: &'static str, + /// Source of truth for tests. + pub basis: CheckBasis, +} + +impl CheckCtx { + pub fn new(ulp: u32, fname: &'static str, basis: CheckBasis) -> Self { + let canonical_fname = crate::canonical_name(fname); + Self { ulp, fname, canonical_name: canonical_fname, basis } + } +} + +/// Possible items to test against +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CheckBasis { + /// Check against Musl's math sources. + Musl, +} + +/// A trait to implement on any output type so we can verify it in a generic way. +pub trait CheckOutput: Sized { + /// Validate `self` (actual) and `expected` are the same. + /// + /// `input` is only used here for error messages. + fn validate<'a>(self, expected: Self, input: Input, ctx: &CheckCtx) -> TestResult; +} + +impl TupleCall R> for (T1,) +where + T1: fmt::Debug, +{ + type Output = R; + + fn call(self, f: fn(T1) -> R) -> Self::Output { + f(self.0) + } +} + +impl TupleCall R> for (T1, T2) +where + T1: fmt::Debug, + T2: fmt::Debug, +{ + type Output = R; + + fn call(self, f: fn(T1, T2) -> R) -> Self::Output { + f(self.0, self.1) + } +} + +impl TupleCall R> for (T1,) +where + T1: fmt::Debug, + T2: fmt::Debug + Default, +{ + type Output = (R, T2); + + fn call(self, f: fn(T1, &mut T2) -> R) -> Self::Output { + let mut t2 = T2::default(); + (f(self.0, &mut t2), t2) + } +} + +impl TupleCall R> for (T1, T2, T3) +where + T1: fmt::Debug, + T2: fmt::Debug, + T3: fmt::Debug, +{ + type Output = R; + + fn call(self, f: fn(T1, T2, T3) -> R) -> Self::Output { + f(self.0, self.1, self.2) + } +} + +impl TupleCall R> for (T1, T2) +where + T1: fmt::Debug, + T2: fmt::Debug, + T3: fmt::Debug + Default, +{ + type Output = (R, T3); + + fn call(self, f: fn(T1, T2, &mut T3) -> R) -> Self::Output { + let mut t3 = T3::default(); + (f(self.0, self.1, &mut t3), t3) + } +} + +impl TupleCall for (T1,) +where + T1: fmt::Debug, + T2: fmt::Debug + Default, + T3: fmt::Debug + Default, +{ + type Output = (T2, T3); + + fn call(self, f: fn(T1, &mut T2, &mut T3)) -> Self::Output { + let mut t2 = T2::default(); + let mut t3 = T3::default(); + f(self.0, &mut t2, &mut t3); + (t2, t3) + } +} + +// Implement for floats +impl CheckOutput for F +where + F: Float + Hex, + Input: Hex + fmt::Debug, + u32: TryFrom, + SpecialCase: MaybeOverride, +{ + fn validate<'a>(self, expected: Self, input: Input, ctx: &CheckCtx) -> TestResult { + // Create a wrapper function so we only need to `.with_context` once. + let inner = || -> TestResult { + let mut allowed_ulp = ctx.ulp; + + // If the tested function requires a nonstandard test, run it here. + if let Some(res) = + SpecialCase::check_float(input, self, expected, &mut allowed_ulp, ctx) + { + return res; + } + + // Check when both are NaNs + if self.is_nan() && expected.is_nan() { + // By default, NaNs have nothing special to check. + return Ok(()); + } else if self.is_nan() || expected.is_nan() { + // Check when only one is a NaN + bail!("real value != NaN") + } + + // Make sure that the signs are the same before checing ULP to avoid wraparound + let act_sig = self.signum(); + let exp_sig = expected.signum(); + ensure!(act_sig == exp_sig, "mismatched signs {act_sig} {exp_sig}"); + + if self.is_infinite() ^ expected.is_infinite() { + bail!("mismatched infinities"); + } + + let act_bits = self.to_bits().signed(); + let exp_bits = expected.to_bits().signed(); + + let ulp_diff = act_bits.checked_sub(exp_bits).unwrap().abs(); + + let ulp_u32 = u32::try_from(ulp_diff) + .map_err(|e| anyhow::anyhow!("{e:?}: ulp of {ulp_diff} exceeds u32::MAX"))?; + + ensure!(ulp_u32 <= allowed_ulp, "ulp {ulp_diff} > {allowed_ulp}",); + + Ok(()) + }; + + inner().with_context(|| { + format!( + "\ + \n input: {input:?} {ibits}\ + \n expected: {expected:<22?} {expbits}\ + \n actual: {self:<22?} {actbits}\ + ", + actbits = self.hex(), + expbits = expected.hex(), + ibits = input.hex(), + ) + }) + } +} + +/// Implement `CheckOutput` for combinations of types. +macro_rules! impl_tuples { + ($(($a:ty, $b:ty);)*) => { + $( + impl CheckOutput for ($a, $b) + where + Input: Hex + fmt::Debug, + SpecialCase: MaybeOverride, + { + fn validate<'a>( + self, + expected: Self, + input: Input, + ctx: &CheckCtx, + ) -> TestResult { + self.0.validate(expected.0, input, ctx) + .and_then(|()| self.1.validate(expected.1, input, ctx)) + .with_context(|| format!( + "full context:\ + \n input: {input:?} {ibits}\ + \n expected: {expected:?} {expbits}\ + \n actual: {self:?} {actbits}\ + ", + actbits = self.hex(), + expbits = expected.hex(), + ibits = input.hex(), + )) + } + } + )* + }; +} + +impl_tuples!( + (f32, i32); + (f64, i32); + (f32, f32); + (f64, f64); +); diff --git a/crates/libm-test/tests/check_coverage.rs b/crates/libm-test/tests/check_coverage.rs new file mode 100644 index 000000000..ef6d21fdb --- /dev/null +++ b/crates/libm-test/tests/check_coverage.rs @@ -0,0 +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", +]; + +macro_rules! callback { + ( + fn_name: $name:ident, + CFn: $_CFn:ty, + CArgs: $_CArgs:ty, + CRet: $_CRet:ty, + RustFn: $_RustFn:ty, + RustArgs: $_RustArgs:ty, + RustRet: $_RustRet:ty, + extra: [$push_to:ident], + ) => { + $push_to.push(stringify!($name)); + }; +} + +#[test] +fn test_for_each_function_all_included() { + let mut included = Vec::new(); + let mut missing = Vec::new(); + + libm_macros::for_each_function! { + callback: callback, + extra: [included], + }; + + for f in libm_test::ALL_FUNCTIONS { + if !included.contains(f) && !ALLOWED_SKIPS.contains(f) { + missing.push(f) + } + } + + if !missing.is_empty() { + panic!( + "missing tests for the following: {missing:#?} \ + \nmake sure any new functions are entered in \ + `ALL_FUNCTIONS` (in `libm-macros`)." + ); + } +} diff --git a/crates/libm-test/tests/compare_built_musl.rs b/crates/libm-test/tests/compare_built_musl.rs new file mode 100644 index 000000000..208b8e286 --- /dev/null +++ b/crates/libm-test/tests/compare_built_musl.rs @@ -0,0 +1,52 @@ +//! Compare our implementations with the result of musl functions, as provided by `musl-math-sys`. +//! +//! Currently this only tests randomized inputs. In the future this may be improved to test edge +//! cases or run exhaustive tests. +//! +//! Note that musl functions do not always provide 0.5ULP rounding, so our functions can do better +//! than these results. + +// There are some targets we can't build musl for +#![cfg(feature = "build-musl")] + +use libm_test::gen::random; +use libm_test::{CheckBasis, CheckCtx, CheckOutput, TupleCall, musl_allowed_ulp}; +use musl_math_sys as musl; + +macro_rules! musl_rand_tests { + ( + fn_name: $fn_name:ident, + CFn: $CFn:ty, + CArgs: $CArgs:ty, + CRet: $CRet:ty, + RustFn: $RustFn:ty, + RustArgs: $RustArgs:ty, + RustRet: $RustRet:ty, + attrs: [$($meta:meta)*] + ) => { paste::paste! { + #[test] + $(#[$meta])* + fn [< musl_random_ $fn_name >]() { + let fname = stringify!($fn_name); + let ulp = musl_allowed_ulp(fname); + let cases = random::get_test_cases::<$RustArgs>(fname); + let ctx = CheckCtx::new(ulp, fname, CheckBasis::Musl); + + for input in cases { + let musl_res = input.call(musl::$fn_name as $CFn); + let crate_res = input.call(libm::$fn_name as $RustFn); + + crate_res.validate(musl_res, input, &ctx).unwrap(); + } + } + } }; +} + +libm_macros::for_each_function! { + callback: musl_rand_tests, + skip: [], + attributes: [ + #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 + [exp10, exp10f, exp2, exp2f, rint] + ], +} diff --git a/crates/musl-math-sys/Cargo.toml b/crates/musl-math-sys/Cargo.toml new file mode 100644 index 000000000..449ce4f3e --- /dev/null +++ b/crates/musl-math-sys/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "musl-math-sys" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[dev-dependencies] +libm = { path = "../../" } + +[build-dependencies] +cc = "1.1.24" diff --git a/crates/musl-math-sys/build.rs b/crates/musl-math-sys/build.rs new file mode 100644 index 000000000..03df06c79 --- /dev/null +++ b/crates/musl-math-sys/build.rs @@ -0,0 +1,328 @@ +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::{env, fs, str}; + +/// Static library that will be built +const LIB_NAME: &str = "musl_math_prefixed"; + +/// Files that have more than one symbol. Map of file names to the symbols defined in that file. +const MULTIPLE_SYMBOLS: &[(&str, &[&str])] = &[ + ("__invtrigl", &["__invtrigl", "__invtrigl_R", "__pio2_hi", "__pio2_lo"]), + ("__polevll", &["__polevll", "__p1evll"]), + ("erf", &["erf", "erfc"]), + ("erff", &["erff", "erfcf"]), + ("erfl", &["erfl", "erfcl"]), + ("exp10", &["exp10", "pow10"]), + ("exp10f", &["exp10f", "pow10f"]), + ("exp10l", &["exp10l", "pow10l"]), + ("exp2f_data", &["exp2f_data", "__exp2f_data"]), + ("exp_data", &["exp_data", "__exp_data"]), + ("j0", &["j0", "y0"]), + ("j0f", &["j0f", "y0f"]), + ("j1", &["j1", "y1"]), + ("j1f", &["j1f", "y1f"]), + ("jn", &["jn", "yn"]), + ("jnf", &["jnf", "ynf"]), + ("lgamma", &["lgamma", "__lgamma_r"]), + ("remainder", &["remainder", "drem"]), + ("remainderf", &["remainderf", "dremf"]), + ("lgammaf", &["lgammaf", "lgammaf_r", "__lgammaf_r"]), + ("lgammal", &["lgammal", "lgammal_r", "__lgammal_r"]), + ("log2_data", &["log2_data", "__log2_data"]), + ("log2f_data", &["log2f_data", "__log2f_data"]), + ("log_data", &["log_data", "__log_data"]), + ("logf_data", &["logf_data", "__logf_data"]), + ("pow_data", &["pow_data", "__pow_log_data"]), + ("powf_data", &["powf_data", "__powf_log2_data"]), + ("signgam", &["signgam", "__signgam"]), + ("sqrt_data", &["sqrt_data", "__rsqrt_tab"]), +]; + +fn main() { + let cfg = Config::from_env(); + + if cfg.target_env == "msvc" + || cfg.target_family == "wasm" + || cfg.target_features.iter().any(|f| f == "thumb-mode") + { + println!( + "cargo::warning=Musl doesn't compile with the current \ + target {}; skipping build", + &cfg.target_string + ); + return; + } + + build_musl_math(&cfg); +} + +#[allow(dead_code)] +#[derive(Debug)] +struct Config { + manifest_dir: PathBuf, + out_dir: PathBuf, + musl_dir: PathBuf, + musl_arch: String, + target_arch: String, + target_env: String, + target_family: String, + target_os: String, + target_string: String, + target_vendor: String, + target_features: Vec, +} + +impl Config { + fn from_env() -> Self { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let target_features = env::var("CARGO_CFG_TARGET_FEATURE") + .map(|feats| feats.split(',').map(ToOwned::to_owned).collect()) + .unwrap_or_default(); + + // Default to the `{workspace_root}/musl` if not specified + let musl_dir = env::var("MUSL_SOURCE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| manifest_dir.parent().unwrap().parent().unwrap().join("musl")); + + let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + let musl_arch = if target_arch == "x86" { "i386".to_owned() } else { target_arch.clone() }; + + println!("cargo::rerun-if-changed={}/c_patches", manifest_dir.display()); + println!("cargo::rerun-if-env-changed=MUSL_SOURCE_DIR"); + println!("cargo::rerun-if-changed={}", musl_dir.display()); + + Self { + manifest_dir, + out_dir: PathBuf::from(env::var("OUT_DIR").unwrap()), + musl_dir, + musl_arch, + target_arch, + target_env: env::var("CARGO_CFG_TARGET_ENV").unwrap(), + target_family: env::var("CARGO_CFG_TARGET_FAMILY").unwrap(), + target_os: env::var("CARGO_CFG_TARGET_OS").unwrap(), + target_string: env::var("TARGET").unwrap(), + target_vendor: env::var("CARGO_CFG_TARGET_VENDOR").unwrap(), + target_features, + } + } +} + +/// Build musl math symbols to a static library +fn build_musl_math(cfg: &Config) { + let musl_dir = &cfg.musl_dir; + assert!( + musl_dir.exists(), + "musl source is missing. it can be downloaded with ./ci/download-musl.sh" + ); + + let math = musl_dir.join("src/math"); + let arch_dir = musl_dir.join("arch").join(&cfg.musl_arch); + let source_map = find_math_source(&math, cfg); + let out_path = cfg.out_dir.join(format!("lib{LIB_NAME}.a")); + + // Run configuration steps. Usually done as part of the musl `Makefile`. + let obj_include = cfg.out_dir.join("musl_obj/include"); + fs::create_dir_all(&obj_include).unwrap(); + fs::create_dir_all(&obj_include.join("bits")).unwrap(); + let sed_stat = Command::new("sed") + .arg("-f") + .arg(musl_dir.join("tools/mkalltypes.sed")) + .arg(arch_dir.join("bits/alltypes.h.in")) + .arg(musl_dir.join("include/alltypes.h.in")) + .stderr(Stdio::inherit()) + .output() + .unwrap(); + assert!(sed_stat.status.success(), "sed command failed: {:?}", sed_stat.status); + + fs::write(obj_include.join("bits/alltypes.h"), sed_stat.stdout).unwrap(); + + let mut cbuild = cc::Build::new(); + cbuild + .extra_warnings(false) + .warnings(false) + .flag_if_supported("-Wno-bitwise-op-parentheses") + .flag_if_supported("-Wno-literal-range") + .flag_if_supported("-Wno-parentheses") + .flag_if_supported("-Wno-shift-count-overflow") + .flag_if_supported("-Wno-shift-op-parentheses") + .flag_if_supported("-Wno-unused-but-set-variable") + .flag_if_supported("-std=c99") + .flag_if_supported("-ffreestanding") + .flag_if_supported("-nostdinc") + .define("_ALL_SOURCE", "1") + .opt_level(3) + .define( + "ROOT_INCLUDE_FEATURES", + Some(musl_dir.join("include/features.h").to_str().unwrap()), + ) + // Our overrides are in this directory + .include(cfg.manifest_dir.join("c_patches")) + .include(musl_dir.join("arch").join(&cfg.musl_arch)) + .include(musl_dir.join("arch/generic")) + .include(musl_dir.join("src/include")) + .include(musl_dir.join("src/internal")) + .include(obj_include) + .include(musl_dir.join("include")) + .file(cfg.manifest_dir.join("c_patches/alias.c")); + + for (sym_name, src_file) in source_map { + // Build the source file + cbuild.file(src_file); + + // Trickery! Redefine the symbol names to have the prefix `musl_`, which allows us to + // differentiate these symbols from whatever we provide. + if let Some((_names, syms)) = + MULTIPLE_SYMBOLS.iter().find(|(name, _syms)| *name == sym_name) + { + // Handle the occasional file that defines multiple symbols + for sym in *syms { + cbuild.define(sym, Some(format!("musl_{sym}").as_str())); + } + } else { + // If the file doesn't define multiple symbols, the file name will be the symbol + cbuild.define(&sym_name, Some(format!("musl_{sym_name}").as_str())); + } + } + + if cfg!(windows) { + // On Windows we don't have a good way to check symbols, so skip that step. + cbuild.compile(LIB_NAME); + return; + } + + let objfiles = cbuild.compile_intermediates(); + + // We create the archive ourselves with relocations rather than letting `cc` do it so we can + // encourage it to resolve symbols now. This should help avoid accidentally linking the wrong + // thing. + let stat = cbuild + .get_compiler() + .to_command() + .arg("-r") + .arg("-o") + .arg(&out_path) + .args(objfiles) + .status() + .unwrap(); + assert!(stat.success()); + + println!("cargo::rustc-link-lib={LIB_NAME}"); + println!("cargo::rustc-link-search=native={}", cfg.out_dir.display()); + + validate_archive_symbols(&out_path); +} + +/// Build a map of `name -> path`. `name` is typically the symbol name, but this doesn't account +/// for files that provide multiple symbols. +fn find_math_source(math_root: &Path, cfg: &Config) -> BTreeMap { + let mut map = BTreeMap::new(); + let mut arch_dir = None; + + // Locate all files and directories + for item in fs::read_dir(math_root).unwrap() { + let path = item.unwrap().path(); + let meta = fs::metadata(&path).unwrap(); + + if meta.is_dir() { + // Make note of the arch-specific directory if it exists + if path.file_name().unwrap() == cfg.target_arch.as_str() { + arch_dir = Some(path); + } + continue; + } + + // Skip non-source files + if path.extension().is_some_and(|ext| ext == "h") { + continue; + } + + let sym_name = path.file_stem().unwrap(); + map.insert(sym_name.to_str().unwrap().to_owned(), path.to_owned()); + } + + // If arch-specific versions are available, build those instead. + if let Some(arch_dir) = arch_dir { + for item in fs::read_dir(arch_dir).unwrap() { + let path = item.unwrap().path(); + let sym_name = path.file_stem().unwrap(); + + if path.extension().unwrap() == "s" { + // FIXME: we never build assembly versions since we have no good way to + // rename the symbol (our options are probably preprocessor or objcopy). + continue; + } + map.insert(sym_name.to_str().unwrap().to_owned(), path); + } + } + + map +} + +/// Make sure we don't have something like a loose unprefixed `_cos` called somewhere, which could +/// wind up linking to system libraries rather than the built musl library. +fn validate_archive_symbols(out_path: &Path) { + const ALLOWED_UNDEF_PFX: &[&str] = &[ + // PIC and arch-specific + ".TOC", + "_GLOBAL_OFFSET_TABLE_", + "__x86.get_pc_thunk", + // gcc/compiler-rt/compiler-builtins symbols + "__add", + "__aeabi_", + "__div", + "__eq", + "__extend", + "__fix", + "__float", + "__gcc_", + "__ge", + "__gt", + "__le", + "__lshr", + "__lt", + "__mul", + "__ne", + "__stack_chk_fail", + "__stack_chk_guard", + "__sub", + "__trunc", + "__undef", + // string routines + "__bzero", + "bzero", + // FPENV interfaces + "feclearexcept", + "fegetround", + "feraiseexcept", + "fesetround", + "fetestexcept", + ]; + + // List global undefined symbols + let out = + Command::new("nm").arg("-guj").arg(out_path).stderr(Stdio::inherit()).output().unwrap(); + + let undef = str::from_utf8(&out.stdout).unwrap(); + let mut undef = undef.lines().collect::>(); + undef.retain(|sym| { + // Account for file formats that add a leading `_` + !ALLOWED_UNDEF_PFX.iter().any(|pfx| sym.starts_with(pfx) || sym[1..].starts_with(pfx)) + }); + + assert!(undef.is_empty(), "found disallowed undefined symbols: {undef:#?}"); + + // Find any symbols that are missing the `_musl_` prefix` + let out = + Command::new("nm").arg("-gUj").arg(out_path).stderr(Stdio::inherit()).output().unwrap(); + + let defined = str::from_utf8(&out.stdout).unwrap(); + let mut defined = defined.lines().collect::>(); + defined.retain(|sym| { + !(sym.starts_with("_musl_") + || sym.starts_with("musl_") + || sym.starts_with("__x86.get_pc_thunk")) + }); + + assert!(defined.is_empty(), "found unprefixed symbols: {defined:#?}"); +} diff --git a/crates/musl-math-sys/c_patches/alias.c b/crates/musl-math-sys/c_patches/alias.c new file mode 100644 index 000000000..63e0f08d5 --- /dev/null +++ b/crates/musl-math-sys/c_patches/alias.c @@ -0,0 +1,40 @@ +/* On platforms that don't support weak symbols, define required aliases + * as wrappers. See comments in `features.h` for more. + */ +#if defined(__APPLE__) || defined(__MINGW32__) + +double __lgamma_r(double a, int *b); +float __lgammaf_r(float a, int *b); +long __lgammal_r(long double a, int *b); +double exp10(double a); +float exp10f(float a); +long exp10l(long double a); +double remainder(double a, double b); +float remainderf(float a, float b); + +double lgamma_r(double a, int *b) { + return __lgamma_r(a, b); +} +float lgammaf_r(float a, int *b) { + return __lgammaf_r(a, b); +} +long double lgammal_r(long double a, int *b) { + return __lgammal_r(a, b); +} +double pow10(double a) { + return exp10(a); +} +float pow10f(float a) { + return exp10f(a); +} +long double pow10l(long double a) { + return exp10l(a); +} +double drem(double a, double b) { + return remainder(a, b); +} +float dremf(float a, float b) { + return remainderf(a, b); +} + +#endif diff --git a/crates/musl-math-sys/c_patches/features.h b/crates/musl-math-sys/c_patches/features.h new file mode 100644 index 000000000..97af93597 --- /dev/null +++ b/crates/musl-math-sys/c_patches/features.h @@ -0,0 +1,39 @@ +/* This is meant to override Musl's src/include/features.h + * + * We use a separate file here to redefine some attributes that don't work on + * all platforms that we would like to build on. + */ + +#ifndef FEATURES_H +#define FEATURES_H + +/* Get the required `#include "../../include/features.h"` since we can't use + * the relative path. The C macros need double indirection to get a usable + * string. */ +#define _stringify_inner(s) #s +#define _stringify(s) _stringify_inner(s) +#include _stringify(ROOT_INCLUDE_FEATURES) + +#if defined(__APPLE__) +#define weak __attribute__((__weak__)) +#define hidden __attribute__((__visibility__("hidden"))) + +/* We _should_ be able to define this as: + * _Pragma(_stringify(weak musl_ ## new = musl_ ## old)) + * However, weak symbols aren't handled correctly [1]. So we manually write + * wrappers, which are in `alias.c`. + * + * [1]: https://github.com/llvm/llvm-project/issues/111321 + */ +#define weak_alias(old, new) /* nothing */ + +#else +#define weak __attribute__((__weak__)) +#define hidden __attribute__((__visibility__("hidden"))) +#define weak_alias(old, new) \ + extern __typeof(old) musl_ ## new \ + __attribute__((__weak__, __alias__(_stringify(musl_ ## old)))) + +#endif /* defined(__APPLE__) */ + +#endif diff --git a/crates/musl-math-sys/src/lib.rs b/crates/musl-math-sys/src/lib.rs new file mode 100644 index 000000000..fe3c89229 --- /dev/null +++ b/crates/musl-math-sys/src/lib.rs @@ -0,0 +1,279 @@ +//! Bindings to Musl math functions (these are built in `build.rs`). + +use std::ffi::{c_char, c_int, c_long}; + +/// Macro for creating bindings and exposing a safe function (since the implementations have no +/// preconditions). Included functions must have correct signatures, otherwise this will be +/// unsound. +macro_rules! functions { + ( $( + $pfx_name:ident: $name:ident( $($arg:ident: $aty:ty),+ ) -> $rty:ty; + )* ) => { + extern "C" { + $( fn $pfx_name( $($arg: $aty),+ ) -> $rty; )* + } + + $( + // Expose a safe version + pub fn $name( $($arg: $aty),+ ) -> $rty { + // SAFETY: FFI calls with no preconditions + unsafe { $pfx_name( $($arg),+ ) } + } + )* + + #[cfg(test)] + mod tests { + use super::*; + use test_support::CallTest; + + $( functions!( + @single_test + $name($($arg: $aty),+) -> $rty + ); )* + } + }; + + (@single_test + $name:ident( $($arg:ident: $aty:ty),+ ) -> $rty:ty + ) => { + // Run a simple check to ensure we can link and call the function without crashing. + #[test] + // FIXME(#309): LE PPC crashes calling some musl functions + #[cfg_attr(all(target_arch = "powerpc64", target_endian = "little"), ignore)] + fn $name() { + $rty>::check(super::$name); + } + }; +} + +#[cfg(test)] +mod test_support { + use core::ffi::c_char; + + /// Just verify that we are able to call the function. + pub trait CallTest { + fn check(f: Self); + } + + macro_rules! impl_calltest { + ($( ($($arg:ty),*) -> $ret:ty; )*) => { + $( + impl CallTest for fn($($arg),*) -> $ret { + fn check(f: Self) { + f($(1 as $arg),*); + } + } + )* + }; + } + + impl_calltest! { + (f32) -> f32; + (f64) -> f64; + (f32, f32) -> f32; + (f64, f64) -> f64; + (i32, f32) -> f32; + (i32, f64) -> f64; + (f32, f32, f32) -> f32; + (f64, f64, f64) -> f64; + (f32, i32) -> f32; + (f32, i64) -> f32; + (f32) -> i32; + (f64) -> i32; + (f64, i32) -> f64; + (f64, i64) -> f64; + } + + impl CallTest for fn(f32, &mut f32) -> f32 { + fn check(f: Self) { + let mut tmp = 0.0; + f(0.0, &mut tmp); + } + } + impl CallTest for fn(f64, &mut f64) -> f64 { + fn check(f: Self) { + let mut tmp = 0.0; + f(0.0, &mut tmp); + } + } + impl CallTest for fn(f32, &mut i32) -> f32 { + fn check(f: Self) { + let mut tmp = 1; + f(0.0, &mut tmp); + } + } + impl CallTest for fn(f64, &mut i32) -> f64 { + fn check(f: Self) { + let mut tmp = 1; + f(0.0, &mut tmp); + } + } + impl CallTest for fn(f32, f32, &mut i32) -> f32 { + fn check(f: Self) { + let mut tmp = 1; + f(0.0, 0.0, &mut tmp); + } + } + impl CallTest for fn(f64, f64, &mut i32) -> f64 { + fn check(f: Self) { + let mut tmp = 1; + f(0.0, 0.0, &mut tmp); + } + } + impl CallTest for fn(f32, &mut f32, &mut f32) { + fn check(f: Self) { + let mut tmp1 = 1.0; + let mut tmp2 = 1.0; + f(0.0, &mut tmp1, &mut tmp2); + } + } + impl CallTest for fn(f64, &mut f64, &mut f64) { + fn check(f: Self) { + let mut tmp1 = 1.0; + let mut tmp2 = 1.0; + f(0.0, &mut tmp1, &mut tmp2); + } + } + impl CallTest for fn(*const c_char) -> f32 { + fn check(f: Self) { + f(c"1".as_ptr()); + } + } + impl CallTest for fn(*const c_char) -> f64 { + fn check(f: Self) { + f(c"1".as_ptr()); + } + } +} + +functions! { + musl_acos: acos(a: f64) -> f64; + musl_acosf: acosf(a: f32) -> f32; + musl_acosh: acosh(a: f64) -> f64; + musl_acoshf: acoshf(a: f32) -> f32; + musl_asin: asin(a: f64) -> f64; + musl_asinf: asinf(a: f32) -> f32; + musl_asinh: asinh(a: f64) -> f64; + musl_asinhf: asinhf(a: f32) -> f32; + musl_atan2: atan2(a: f64, b: f64) -> f64; + musl_atan2f: atan2f(a: f32, b: f32) -> f32; + musl_atan: atan(a: f64) -> f64; + musl_atanf: atanf(a: f32) -> f32; + musl_atanh: atanh(a: f64) -> f64; + musl_atanhf: atanhf(a: f32) -> f32; + musl_cbrt: cbrt(a: f64) -> f64; + musl_cbrtf: cbrtf(a: f32) -> f32; + musl_ceil: ceil(a: f64) -> f64; + musl_ceilf: ceilf(a: f32) -> f32; + musl_copysign: copysign(a: f64, b: f64) -> f64; + musl_copysignf: copysignf(a: f32, b: f32) -> f32; + musl_cos: cos(a: f64) -> f64; + musl_cosf: cosf(a: f32) -> f32; + musl_cosh: cosh(a: f64) -> f64; + musl_coshf: coshf(a: f32) -> f32; + musl_drem: drem(a: f64, b: f64) -> f64; + musl_dremf: dremf(a: f32, b: f32) -> f32; + musl_erf: erf(a: f64) -> f64; + musl_erfc: erfc(a: f64) -> f64; + musl_erfcf: erfcf(a: f32) -> f32; + musl_erff: erff(a: f32) -> f32; + musl_exp10: exp10(a: f64) -> f64; + musl_exp10f: exp10f(a: f32) -> f32; + musl_exp2: exp2(a: f64) -> f64; + musl_exp2f: exp2f(a: f32) -> f32; + musl_exp: exp(a: f64) -> f64; + musl_expf: expf(a: f32) -> f32; + musl_expm1: expm1(a: f64) -> f64; + musl_expm1f: expm1f(a: f32) -> f32; + musl_fabs: fabs(a: f64) -> f64; + musl_fabsf: fabsf(a: f32) -> f32; + musl_fdim: fdim(a: f64, b: f64) -> f64; + musl_fdimf: fdimf(a: f32, b: f32) -> f32; + musl_finite: finite(a: f64) -> c_int; + musl_finitef: finitef(a: f32) -> c_int; + musl_floor: floor(a: f64) -> f64; + musl_floorf: floorf(a: f32) -> f32; + musl_fma: fma(a: f64, b: f64, c: f64) -> f64; + musl_fmaf: fmaf(a: f32, b: f32, c: f32) -> f32; + musl_fmax: fmax(a: f64, b: f64) -> f64; + musl_fmaxf: fmaxf(a: f32, b: f32) -> f32; + musl_fmin: fmin(a: f64, b: f64) -> f64; + musl_fminf: fminf(a: f32, b: f32) -> f32; + musl_fmod: fmod(a: f64, b: f64) -> f64; + musl_fmodf: fmodf(a: f32, b: f32) -> f32; + musl_frexp: frexp(a: f64, b: &mut c_int) -> f64; + musl_frexpf: frexpf(a: f32, b: &mut c_int) -> f32; + musl_hypot: hypot(a: f64, b: f64) -> f64; + musl_hypotf: hypotf(a: f32, b: f32) -> f32; + musl_ilogb: ilogb(a: f64) -> c_int; + musl_ilogbf: ilogbf(a: f32) -> c_int; + musl_j0: j0(a: f64) -> f64; + musl_j0f: j0f(a: f32) -> f32; + musl_j1: j1(a: f64) -> f64; + musl_j1f: j1f(a: f32) -> f32; + musl_jn: jn(a: c_int, b: f64) -> f64; + musl_jnf: jnf(a: c_int, b: f32) -> f32; + musl_ldexp: ldexp(a: f64, b: c_int) -> f64; + musl_ldexpf: ldexpf(a: f32, b: c_int) -> f32; + musl_lgamma: lgamma(a: f64) -> f64; + musl_lgamma_r: lgamma_r(a: f64, b: &mut c_int) -> f64; + musl_lgammaf: lgammaf(a: f32) -> f32; + musl_lgammaf_r: lgammaf_r(a: f32, b: &mut c_int) -> f32; + musl_log10: log10(a: f64) -> f64; + musl_log10f: log10f(a: f32) -> f32; + musl_log1p: log1p(a: f64) -> f64; + musl_log1pf: log1pf(a: f32) -> f32; + musl_log2: log2(a: f64) -> f64; + musl_log2f: log2f(a: f32) -> f32; + musl_log: log(a: f64) -> f64; + musl_logb: logb(a: f64) -> f64; + musl_logbf: logbf(a: f32) -> f32; + musl_logf: logf(a: f32) -> f32; + musl_modf: modf(a: f64, b: &mut f64) -> f64; + musl_modff: modff(a: f32, b: &mut f32) -> f32; + musl_nan: nan(a: *const c_char) -> f64; + musl_nanf: nanf(a: *const c_char) -> f32; + musl_nearbyint: nearbyint(a: f64) -> f64; + musl_nearbyintf: nearbyintf(a: f32) -> f32; + musl_nextafter: nextafter(a: f64, b: f64) -> f64; + musl_nextafterf: nextafterf(a: f32, b: f32) -> f32; + musl_pow10: pow10(a: f64) -> f64; + musl_pow10f: pow10f(a: f32) -> f32; + musl_pow: pow(a: f64, b: f64) -> f64; + musl_powf: powf(a: f32, b: f32) -> f32; + musl_remainder: remainder(a: f64, b: f64) -> f64; + musl_remainderf: remainderf(a: f32, b: f32) -> f32; + musl_remquo: remquo(a: f64, b: f64, c: &mut c_int) -> f64; + musl_remquof: remquof(a: f32, b: f32, c: &mut c_int) -> f32; + musl_rint: rint(a: f64) -> f64; + musl_rintf: rintf(a: f32) -> f32; + musl_round: round(a: f64) -> f64; + musl_roundf: roundf(a: f32) -> f32; + musl_scalbln: scalbln(a: f64, b: c_long) -> f64; + musl_scalblnf: scalblnf(a: f32, b: c_long) -> f32; + musl_scalbn: scalbn(a: f64, b: c_int) -> f64; + musl_scalbnf: scalbnf(a: f32, b: c_int) -> f32; + musl_significand: significand(a: f64) -> f64; + musl_significandf: significandf(a: f32) -> f32; + musl_sin: sin(a: f64) -> f64; + musl_sincos: sincos(a: f64, b: &mut f64, c: &mut f64) -> (); + musl_sincosf: sincosf(a: f32, b: &mut f32, c: &mut f32) -> (); + musl_sinf: sinf(a: f32) -> f32; + musl_sinh: sinh(a: f64) -> f64; + musl_sinhf: sinhf(a: f32) -> f32; + musl_sqrt: sqrt(a: f64) -> f64; + musl_sqrtf: sqrtf(a: f32) -> f32; + musl_tan: tan(a: f64) -> f64; + musl_tanf: tanf(a: f32) -> f32; + musl_tanh: tanh(a: f64) -> f64; + musl_tanhf: tanhf(a: f32) -> f32; + musl_tgamma: tgamma(a: f64) -> f64; + musl_tgammaf: tgammaf(a: f32) -> f32; + musl_trunc: trunc(a: f64) -> f64; + musl_truncf: truncf(a: f32) -> f32; + musl_y0: y0(a: f64) -> f64; + musl_y0f: y0f(a: f32) -> f32; + musl_y1: y1(a: f64) -> f64; + musl_y1f: y1f(a: f32) -> f32; + musl_ynf: ynf(a: c_int, b: f32) -> f32; +} diff --git a/src/math/rem_pio2.rs b/src/math/rem_pio2.rs index 6be23a43c..4dfb8c658 100644 --- a/src/math/rem_pio2.rs +++ b/src/math/rem_pio2.rs @@ -194,6 +194,8 @@ mod tests { use super::rem_pio2; #[test] + // FIXME(correctness): inaccurate results on i586 + #[cfg_attr(all(target_arch = "x86", not(target_feature = "sse")), ignore)] fn test_near_pi() { let arg = 3.141592025756836; let arg = force_eval!(arg);