Merge pull request rust-lang/libm#300 from tgross35/test-refactoring

Test more targets against a custom-built musl libm
This commit is contained in:
Trevor Gross 2024-10-28 13:17:11 -05:00 committed by GitHub
commit 73fd634908
32 changed files with 3008 additions and 45 deletions

View file

@ -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

View file

@ -1,8 +1,9 @@
**/*.rs.bk
**.bk
.#*
/bin
/math/src
/math/target
/target
/tests
Cargo.lock
musl/
**.tar.gz

View file

@ -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",
]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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" \

View file

@ -2,21 +2,64 @@
set -eux
target="$1"
cmd="cargo test --all --target $target"
export RUST_BACKTRACE="${RUST_BACKTRACE:-full}"
# Needed for no-panic to correct detect a lack of panics
export RUSTFLAGS="$RUSTFLAGS -Ccodegen-units=1"
export RUSTFLAGS="${RUSTFLAGS:-} -Ccodegen-units=1"
# stable by default
$cmd
$cmd --release
target="${1:-}"
# unstable with a feature
$cmd --features 'unstable'
$cmd --release --features 'unstable'
if [ -z "$target" ]; then
host_target=$(rustc -vV | awk '/^host/ { print $2 }')
echo "Defaulted to host target $host_target"
target="$host_target"
fi
# also run the reference tests
$cmd --features 'unstable libm-test/test-musl-serialized'
$cmd --release --features 'unstable libm-test/test-musl-serialized'
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 <https://github.com/rust-lang/libm/issues/309>.
*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.
# <https://github.com/rust-lang/rust/issues/128944>
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'
echo "can't run tests on $target"
else
cmd="cargo test --all --target $target $extra_flags"
# stable by default
$cmd
$cmd --release
# unstable with a feature
$cmd --features 'unstable'
$cmd --release --features 'unstable'
fi

View file

@ -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"] }

View file

@ -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<Signature>, &[&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<Vec<FunctionInfo>> = 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<Vec<&'static FunctionInfo>> {
// 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<pm2::TokenStream> {
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<syn::Error>,
}
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);
}
}

View file

@ -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<Mapping, Comma>,
}
impl Parse for Invocation {
fn parse(input: ParseStream) -> syn::Result<Self> {
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<Self> {
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<Ident>,
/// Invoke only for these functions
pub only: Option<Vec<Ident>>,
/// Attributes that get applied to specific functions
pub attributes: Option<Vec<AttributeMap>>,
/// Extra expressions to pass to all invocations of the macro
pub extra: Option<Expr>,
/// Per-function extra expressions to pass to the macro
pub fn_extra: Option<BTreeMap<Ident, Expr>>,
// For diagnostics
pub only_span: Option<Span>,
pub fn_extra_span: Option<Span>,
}
impl StructuredInput {
pub fn from_fields(input: Invocation) -> syn::Result<Self> {
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<BTreeMap<Ident, Expr>> {
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<Mapping>, name: &str) -> syn::Result<Expr> {
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<Ident> {
syn::parse2(expr.into_token_stream())
}
/// Parse an array of expressions.
fn parse_expr_array(input: ParseStream) -> syn::Result<Vec<Expr>> {
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<Vec<Ident>> {
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<Vec<Ident>> {
if !input.peek2(Token![|]) {
return Ok(vec![input.parse()?]);
}
let fields = Punctuated::<Ident, Token![|]>::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<Meta>,
pub names: Vec<Ident>,
}
impl Parse for AttributeMap {
fn parse(input: ParseStream) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
Ok(Self {
meta: attrs.into_iter().map(|a| a.meta).collect(),
names: parse_ident_array(input)?,
})
}
}

View file

@ -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],
}
}

View file

@ -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 }

View file

@ -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<String>,
target_os: String,
target_string: String,
target_vendor: String,
target_features: Vec<String>,
}
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::<Vec<_>>();
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};

View file

@ -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<Item = (f32,)> {
self.inputs_f32.iter().map(|f| (f.0,))
}
}
impl GenerateInput<(f32, f32)> for CachedInput {
fn get_cases(&self) -> impl Iterator<Item = (f32, f32)> {
self.inputs_f32.iter().map(|f| (f.0, f.1))
}
}
impl GenerateInput<(i32, f32)> for CachedInput {
fn get_cases(&self) -> impl Iterator<Item = (i32, f32)> {
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<Item = (f32, i32)> {
GenerateInput::<(i32, f32)>::get_cases(self).map(|(i, f)| (f, i))
}
}
impl GenerateInput<(f32, f32, f32)> for CachedInput {
fn get_cases(&self) -> impl Iterator<Item = (f32, f32, f32)> {
self.inputs_f32.iter().copied()
}
}
impl GenerateInput<(f64,)> for CachedInput {
fn get_cases(&self) -> impl Iterator<Item = (f64,)> {
self.inputs_f64.iter().map(|f| (f.0,))
}
}
impl GenerateInput<(f64, f64)> for CachedInput {
fn get_cases(&self) -> impl Iterator<Item = (f64, f64)> {
self.inputs_f64.iter().map(|f| (f.0, f.1))
}
}
impl GenerateInput<(i32, f64)> for CachedInput {
fn get_cases(&self) -> impl Iterator<Item = (i32, f64)> {
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<Item = (f64, i32)> {
GenerateInput::<(i32, f64)>::get_cases(self).map(|(i, f)| (f, i))
}
}
impl GenerateInput<(f64, f64, f64)> for CachedInput {
fn get_cases(&self) -> impl Iterator<Item = (f64, f64, f64)> {
self.inputs_f64.iter().copied()
}
}

View file

@ -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<CachedInput> = 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<CachedInput> = 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<RustArgs>(fname: &str) -> impl Iterator<Item = RustArgs>
where
CachedInput: GenerateInput<RustArgs>,
{
let inputs = if fname == "jn" || fname == "jnf" { &TEST_CASES_JN } else { &TEST_CASES };
CachedInput::get_cases(inputs)
}

View file

@ -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<T = (), E = anyhow::Error> = Result<T, E>;
// 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),
}
}

View file

@ -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<Self> {
type Int: Int<OtherSign = Self::SignedInt, Unsigned = Self::Int>;
type SignedInt: Int + Int<OtherSign = Self::Int, Unsigned = Self::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<Self> {
type OtherSign: Int;
type Unsigned: Int;
const BITS: u32;
const SIGNED: bool;
fn signed(self) -> <Self::Unsigned as Int>::OtherSign;
fn unsigned(self) -> Self::Unsigned;
fn checked_sub(self, other: Self) -> Option<Self>;
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> {
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> {
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<Input> $crate::CheckOutput<Input> for $ty
where
Input: Hex + fmt::Debug,
SpecialCase: MaybeOverride<Input>,
{
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<T1> Hex for (T1,)
where
T1: Hex,
{
fn hex(self) -> String {
format!("({},)", self.0.hex())
}
}
impl<T1, T2> Hex for (T1, T2)
where
T1: Hex,
T2: Hex,
{
fn hex(self) -> String {
format!("({}, {})", self.0.hex(), self.1.hex())
}
}
impl<T1, T2, T3> 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())
}
}

View file

@ -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<TestResult> = 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<TestResult> = 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<Input> {
fn check_float<F: Float>(
_input: Input,
_actual: F,
_expected: F,
_ulp: &mut u32,
_ctx: &CheckCtx,
) -> Option<TestResult> {
None
}
fn check_int<I: Int>(
_input: Input,
_actual: I,
_expected: I,
_ctx: &CheckCtx,
) -> Option<TestResult> {
None
}
}
impl MaybeOverride<(f32,)> for SpecialCase {
fn check_float<F: Float>(
input: (f32,),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
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<F: Float>(
input: (f64,),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
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<F: Float>(actual: F, expected: F, ctx: &CheckCtx) -> Option<TestResult> {
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 <https://github.com/llvm/llvm-project/issues/66803>
// Rust issue <https://github.com/rust-lang/rust/issues/115567>
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<F: Float>(
input: (f32, f32),
_actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
maybe_skip_min_max_nan(input, expected, ctx)
}
}
impl MaybeOverride<(f64, f64)> for SpecialCase {
fn check_float<F: Float>(
input: (f64, f64),
_actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
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<F1: Float, F2: Float>(
input: (F1, F1),
expected: F2,
ctx: &CheckCtx,
) -> Option<TestResult> {
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<F: Float>(
input: (i32, f32),
_actual: F,
_expected: F,
ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
bessel_prec_dropoff(input, ulp, ctx)
}
}
impl MaybeOverride<(i32, f64)> for SpecialCase {
fn check_float<F: Float>(
input: (i32, f64),
_actual: F,
_expected: F,
ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
bessel_prec_dropoff(input, ulp, ctx)
}
}
/// Our bessel functions blow up with large N values
fn bessel_prec_dropoff<F: Float>(
input: (i32, F),
ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
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 {}

View file

@ -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<TupleArgs> {
fn get_cases(&self) -> impl Iterator<Item = TupleArgs>;
}
/// 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<Func>: 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<Input>: 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<T1, R> TupleCall<fn(T1) -> R> for (T1,)
where
T1: fmt::Debug,
{
type Output = R;
fn call(self, f: fn(T1) -> R) -> Self::Output {
f(self.0)
}
}
impl<T1, T2, R> TupleCall<fn(T1, T2) -> 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<T1, T2, R> TupleCall<fn(T1, &mut T2) -> 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<T1, T2, T3, R> TupleCall<fn(T1, T2, T3) -> 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<T1, T2, T3, R> TupleCall<fn(T1, T2, &mut T3) -> 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<T1, T2, T3> TupleCall<fn(T1, &mut T2, &mut T3)> 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<F, Input> CheckOutput<Input> for F
where
F: Float + Hex,
Input: Hex + fmt::Debug,
u32: TryFrom<F::SignedInt, Error: fmt::Debug>,
SpecialCase: MaybeOverride<Input>,
{
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<Input> CheckOutput<Input> for ($a, $b)
where
Input: Hex + fmt::Debug,
SpecialCase: MaybeOverride<Input>,
{
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);
);

View file

@ -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`)."
);
}
}

View file

@ -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]
],
}

View file

@ -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"

View file

@ -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<String>,
}
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<String, PathBuf> {
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::<Vec<_>>();
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::<Vec<_>>();
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:#?}");
}

View file

@ -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

View file

@ -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

View file

@ -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() {
<fn($($aty),+) -> $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;
}

View file

@ -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);