From 393289680bc91952d142f957b2ad373f2d6f248e Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:25:16 -0500 Subject: [PATCH 01/25] Introduce `musl-math-sys` for bindings to musl math symbols This crate builds math symbols from a musl checkout and provides a Rust interface. The intent is that we will be able to compare our implementations against musl on more than just linux (which are the only currently the only targets we run `*-musl` targets against for comparison). Musl libc can't compile on anything other than Linux; however, the routines in `src/math` are cross platform enough to build on MacOS and windows-gnu with only minor adjustments. We take advantage of this and build only needed files using `cc`. The build script also performs remapping (via defines) so that e.g. `cos` gets defined as `musl_cos`. This gives us more certainty that we are actually testing against the intended symbol; without it, it is easy to unknowingly link to system libraries or even Rust's `libm` itself and wind up with an ineffective test. There is also a small procedure to verify remapping worked correctly by checking symbols in object files. --- .gitignore | 5 +- Cargo.toml | 2 + crates/musl-math-sys/Cargo.toml | 12 + crates/musl-math-sys/build.rs | 363 ++++++++++++++++++++++ crates/musl-math-sys/c_patches/alias.c | 40 +++ crates/musl-math-sys/c_patches/features.h | 39 +++ crates/musl-math-sys/src/lib.rs | 279 +++++++++++++++++ 7 files changed, 738 insertions(+), 2 deletions(-) create mode 100644 crates/musl-math-sys/Cargo.toml create mode 100644 crates/musl-math-sys/build.rs create mode 100644 crates/musl-math-sys/c_patches/alias.c create mode 100644 crates/musl-math-sys/c_patches/features.h create mode 100644 crates/musl-math-sys/src/lib.rs 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 181000f34..e9c4134cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,12 @@ unstable = [] force-soft-floats = [] [workspace] +resolver = "2" members = [ "crates/compiler-builtins-smoke-test", "crates/libm-bench", "crates/libm-test", + "crates/musl-math-sys", ] default-members = [ ".", 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..ed97ffc6a --- /dev/null +++ b/crates/musl-math-sys/build.rs @@ -0,0 +1,363 @@ +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_string_lossy().into_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_string_lossy().into_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; +} From 62061f268401f55ca22c02dc6d9c86995a094db6 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:36:05 -0500 Subject: [PATCH 02/25] Add a script for downloading musl --- ci/download-musl.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100755 ci/download-musl.sh 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" From edae1a2f09bb49fbaa9147108b43b33e2f093423 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:37:44 -0500 Subject: [PATCH 03/25] Add a macro for expanding all function signatures Introduce `for_each_function`. This macro takes a callback macro and invokes it once per function signature. This should provide an easier way of registering various tests and benchmarks without duplicating the function names and signatures each time. This lives in `libm` rather than `libm-test` so we will be able to use it for e.g. reexports in `compiler-builtins`. It is gated behind a new `_internal-features` Cargo feature. --- Cargo.toml | 3 + src/lib.rs | 1 + src/macros.rs | 245 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 src/macros.rs diff --git a/Cargo.toml b/Cargo.toml index e9c4134cd..3448648bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ unstable = [] # Used to prevent using any intrinsics or arch-specific code. force-soft-floats = [] +# Features that aren't meant to be part of the public API +_internal-features = [] + [workspace] resolver = "2" members = [ diff --git a/src/lib.rs b/src/lib.rs index 23885ecf8..6c55df76f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ #![allow(clippy::assign_op_pattern)] mod libm_helper; +mod macros; mod math; use core::{f32, f64}; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 000000000..792c1a94c --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,245 @@ +/// 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. +#[macro_export] +#[cfg(feature = "_internal-features")] +macro_rules! for_each_function { + // Main invocation + // + // Just calls back to this macro with a list of all functions in this crate. + ($call_this:ident) => { + $crate::for_each_function! { + @implementation + user_macro: $call_this; + + // Up to date list of all functions in this crate, grouped by signature. + // Some signatures have a different signature in the system libraries and in Rust, e.g. + // when multiple values are returned. These are the cases with multiple signatures + // (`as` in between). + (f32) -> f32 { + acosf; + acoshf; + asinf; + asinhf; + atanf; + atanhf; + cbrtf; + ceilf; + cosf; + coshf; + erff; + #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 + exp10f; + #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 + exp2f; + expf; + expm1f; + fabsf; + floorf; + j0f; + j1f; + lgammaf; + log10f; + log1pf; + log2f; + logf; + rintf; + roundf; + sinf; + sinhf; + sqrtf; + tanf; + tanhf; + tgammaf; + truncf; + }; + + (f64) -> f64 { + acos; + acosh; + asin; + asinh; + atan; + atanh; + cbrt; + ceil; + cos; + cosh; + erf; + #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 + exp10; + #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 + exp2; + exp; + expm1; + fabs; + floor; + j0; + j1; + lgamma; + log10; + log1p; + log2; + log; + rint; + round; + sin; + sinh; + sqrt; + tan; + tanh; + tgamma; + trunc; + }; + + (f32, f32) -> f32 { + atan2f; + copysignf; + fdimf; + fmaxf; + fminf; + fmodf; + hypotf; + nextafterf; + powf; + remainderf; + }; + + (f64, f64) -> f64 { + atan2; + copysign; + fdim; + fmax; + fmin; + fmod; + hypot; + nextafter; + pow; + remainder; + }; + + (f32, f32, f32) -> f32 { + fmaf; + }; + + (f64, f64, f64) -> f64 { + fma; + }; + + (f32) -> i32 { + ilogbf; + }; + + (f64) -> i32 { + ilogb; + }; + + (i32, f32) -> f32 { + jnf; + }; + + (f32, i32) -> f32 { + scalbnf; + ldexpf; + }; + + (i32, f64) -> f64 { + jn; + }; + + (f64, i32) -> f64 { + scalbn; + ldexp; + }; + + (f32, &mut f32) -> f32 as (f32) -> (f32, f32) { + modff; + }; + + (f64, &mut f64) -> f64 as (f64) -> (f64, f64) { + modf; + }; + + (f32, &mut c_int) -> f32 as (f32) -> (f32, i32) { + frexpf; + lgammaf_r; + }; + + (f64, &mut c_int) -> f64 as (f64) -> (f64, i32) { + frexp; + lgamma_r; + }; + + (f32, f32, &mut c_int) -> f32 as (f32, f32) -> (f32, i32) { + remquof; + }; + + (f64, f64, &mut c_int) -> f64 as (f64, f64) -> (f64, i32) { + remquo; + }; + + (f32, &mut f32, &mut f32) -> () as (f32) -> (f32, f32) { + sincosf; + }; + + (f64, &mut f64, &mut f64) -> () as (f64) -> (f64, f64) { + sincos; + }; + } + }; + + // This branch processes the function list and passes it to the user macro callback. + ( + @implementation + user_macro: $call_this:ident; + $( + // Main signature + ($($sys_arg:ty),+) -> $sys_ret:ty + // If the Rust signature is different from system, it is provided with `as` + $(as ($($rust_arg:ty),+) -> $rust_ret:ty)? { + $( + $(#[$fn_meta:meta])* // applied to the test + $name:ident; + )* + }; + )* + ) => { + // The user macro can have an `@all_items` pattern where it gets a list of the functions + $call_this! { + @all_items + fn_names: [$($( $name ),*),*] + } + + $( + // Invoke the user macro once for each signature type. + $call_this! { + @each_signature + // The input type, represented as a tuple. E.g. `(f32, f32)` for a + // `fn(f32, f32) -> f32` signature. + SysArgsTupleTy: ($($sys_arg),+ ,), + // The tuple type to call the Rust function. So if the system signature is + // `fn(f32, &mut f32) -> f32`, this type will only be `(f32, )`. + RustArgsTupleTy: $crate::for_each_function!( + @coalesce [($($sys_arg),+ ,)] $( [($($rust_arg),+ ,)] )? + ), + // A function signature type for the system function. + SysFnTy: fn($($sys_arg),+) -> $sys_ret, + // A function signature type for the Rust function. + RustFnTy: $crate::for_each_function!( + @coalesce [fn($($sys_arg),+) -> $sys_ret] $([fn($($rust_arg),+) -> $rust_ret])? + ), + // The list of all functions that have this signature. + functions: [$( { + attrs: [$($fn_meta),*], + fn_name: $name, + } ),*], + } + )* + }; + + // Macro helper to return the second item if two are provided, otherwise a default + (@coalesce [$($tt1:tt)*]) => { $($tt1)* } ; + (@coalesce [$($tt1:tt)*] [$($tt2:tt)*]) => { $($tt2)* } ; +} From ebda29580d3e91ff94b54d3e9c7f9517bdc70dbf Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:41:07 -0500 Subject: [PATCH 04/25] Collect all function names to an array Use a build script for `libm-test` to enumerate all symbols provided by `libm` and provide this list in a variable. This will allow us to make sure no functions are missed anytime they must be manually listed. Additionally, introduce some helper config options. --- crates/libm-test/build.rs | 100 +++++++++++++++++++++++++++++++++++- crates/libm-test/src/lib.rs | 2 + 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/crates/libm-test/build.rs b/crates/libm-test/build.rs index c2c4b0bd2..fb726b16d 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 = "musl-bitwise-tests")] - 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 = "musl-bitwise-tests")] -mod musl_reference_tests { +mod musl_serialized_tests { use rand::seq::SliceRandom; use rand::Rng; use std::env; diff --git a/crates/libm-test/src/lib.rs b/crates/libm-test/src/lib.rs index 8b1378917..87d8a6900 100644 --- a/crates/libm-test/src/lib.rs +++ b/crates/libm-test/src/lib.rs @@ -1 +1,3 @@ +// List of all files present in libm's source +include!(concat!(env!("OUT_DIR"), "/all_files.rs")); From 74a0c39ec41e5d8554780c84fa6e742add93b992 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:44:31 -0500 Subject: [PATCH 05/25] Add a test that `for_each_fn` correctly lists all functions Create a new test that checks `for_each_fn` against `ALL_FUNCTIONS`, i.e. the manually entered function list against the automatically collected list. If any are missing (e.g. new symbol added), then this will produce an error. --- crates/libm-test/Cargo.toml | 2 +- crates/libm-test/tests/check_coverage.rs | 51 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 crates/libm-test/tests/check_coverage.rs diff --git a/crates/libm-test/Cargo.toml b/crates/libm-test/Cargo.toml index 7c193d3bb..b6f26964e 100644 --- a/crates/libm-test/Cargo.toml +++ b/crates/libm-test/Cargo.toml @@ -12,7 +12,7 @@ default = [] musl-bitwise-tests = ["rand"] [dependencies] -libm = { path = "../.." } +libm = { path = "../..", features = ["_internal-features"] } [build-dependencies] rand = { version = "0.8.5", optional = true } diff --git a/crates/libm-test/tests/check_coverage.rs b/crates/libm-test/tests/check_coverage.rs new file mode 100644 index 000000000..284f980ee --- /dev/null +++ b/crates/libm-test/tests/check_coverage.rs @@ -0,0 +1,51 @@ +//! 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! function_names { + ( + @all_items + fn_names: [ $( $name:ident ),* ] + ) => { + const INCLUDED_FUNCTIONS: &[&str] = &[ $( stringify!($name) ),* ]; + }; + (@each_signature $($tt:tt)*) => {}; +} + +libm::for_each_function!(function_names); + +#[test] +fn test_for_each_function_all_included() { + let mut missing = Vec::new(); + + for f in libm_test::ALL_FUNCTIONS { + if !INCLUDED_FUNCTIONS.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 the \ + `for_each_function` macro definition." + ); + } +} From 55255214412ebf8f80426ec9a3877a47e8451c5b Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:47:55 -0500 Subject: [PATCH 06/25] Add numeric traits These traits are simplified versions of what we have in `compiler_builtins` and will be used for tests. --- crates/libm-test/src/lib.rs | 3 + crates/libm-test/src/num_traits.rs | 160 +++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 crates/libm-test/src/num_traits.rs diff --git a/crates/libm-test/src/lib.rs b/crates/libm-test/src/lib.rs index 87d8a6900..5444709d8 100644 --- a/crates/libm-test/src/lib.rs +++ b/crates/libm-test/src/lib.rs @@ -1,3 +1,6 @@ +mod num_traits; + +pub use num_traits::{Float, Hex, Int}; // List of all files present in libm's source include!(concat!(env!("OUT_DIR"), "/all_files.rs")); diff --git a/crates/libm-test/src/num_traits.rs b/crates/libm-test/src/num_traits.rs new file mode 100644 index 000000000..fdf8c4253 --- /dev/null +++ b/crates/libm-test/src/num_traits.rs @@ -0,0 +1,160 @@ +use std::fmt; + +/// 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 BITS: u32; + fn is_nan(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;)+) => { + $( + impl Float for $fty { + type Int = $ui; + type SignedInt = $si; + const BITS: u32 = <$ui>::BITS; + fn is_nan(self) -> bool { + self.is_nan() + } + 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; + f64, u64, i64; +); + +/// 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 / 8) + 2) as usize) + } + } + } +} + +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()) + } +} From 3d9cbc917b397c54e47be7cde979e13efce85be5 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:55:05 -0500 Subject: [PATCH 07/25] Add traits for testing These traits give us a more generic way to interface with tuples used for (1) test input, (2) function arguments, and (3) test input. --- crates/libm-test/src/lib.rs | 2 + crates/libm-test/src/num_traits.rs | 14 ++ crates/libm-test/src/test_traits.rs | 245 ++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 crates/libm-test/src/test_traits.rs diff --git a/crates/libm-test/src/lib.rs b/crates/libm-test/src/lib.rs index 5444709d8..eef6120f2 100644 --- a/crates/libm-test/src/lib.rs +++ b/crates/libm-test/src/lib.rs @@ -1,6 +1,8 @@ mod num_traits; +mod test_traits; pub use num_traits::{Float, Hex, Int}; +pub use test_traits::{CheckOutput, GenerateInput, TupleCall}; // List of all files present in libm's source include!(concat!(env!("OUT_DIR"), "/all_files.rs")); diff --git a/crates/libm-test/src/num_traits.rs b/crates/libm-test/src/num_traits.rs index fdf8c4253..4921594e0 100644 --- a/crates/libm-test/src/num_traits.rs +++ b/crates/libm-test/src/num_traits.rs @@ -113,6 +113,20 @@ macro_rules! impl_int { format!("{self:#0width$x}", width = ((Self::BITS / 8) + 2) as usize) } } + + impl $crate::CheckOutput for $ty { + fn validate(self, expected: Self, input: Input, _allowed_ulp: u32) + { + assert_eq!( + self, + expected, + "expected {expected:?} crate {self:?} ({expbits}, {actbits}) input {input:?} ({ibits})", + expbits = expected.hex(), + actbits = self.hex(), + ibits = input.hex() + ); + } + } } } diff --git a/crates/libm-test/src/test_traits.rs b/crates/libm-test/src/test_traits.rs new file mode 100644 index 000000000..52434638c --- /dev/null +++ b/crates/libm-test/src/test_traits.rs @@ -0,0 +1,245 @@ +//! 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 crate::{Float, Hex, Int}; +use std::ffi::c_int; +use std::fmt; + +/// Implement this on types that can generate a sequence of tuples for test input. +pub trait GenerateInput { + fn get_cases(&self) -> impl ExactSizeIterator; +} + +/// 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; +} + +/// A trait to implement on any output type so we can verify it in a generic way. +pub trait CheckOutput: Sized { + /// Assert that `self` and `expected` are the same. + /// + /// `input` is only used here for error messages. + fn validate(self, expected: Self, input: Input, allowed_ulp: u32); +} + +/// Implement `TupleCall` for signatures with no `&mut`. +macro_rules! impl_tupl_call { + ($( ($($argty:ty),*) -> $ret:ty; )+) => { + $( + impl TupleCall $ret> for ( $($argty,)* ) { + type Output = $ret; + + fn call(self, f: fn($($argty),*) -> $ret) -> Self::Output { + impl_tupl_call!(@call f, self, $($argty),*) + } + } + )* + }; + + (@call $f:ident, $this:ident, $a1:ty, $a2:ty, $a3:ty) => { + $f($this.0, $this.1, $this.2) + }; + (@call $f:ident, $this:ident, $a1:ty, $a2:ty) => { + $f($this.0, $this.1) + }; + (@call $f:ident, $this:ident, $a1:ty) => { + $f($this.0) + }; +} + +impl_tupl_call! { + (f32) -> f32; + (f64) -> f64; + (f32) -> i32; + (f64) -> i32; + (f32, f32) -> f32; + (f64, f64) -> f64; + (f32, i32) -> f32; + (f64, i32) -> f64; + (i32, f32) -> f32; + (i32, f64) -> f64; + (f32, f32, f32) -> f32; + (f64, f64, f64) -> f64; + (f32) -> (f32, f32); + (f64) -> (f64, f64); + (f32) -> (f32, c_int); + (f64) -> (f64, c_int); + (f32, f32) -> (f32, c_int); + (f64, f64) -> (f64, c_int); +} + +/* Implement `TupleCall` for signatures that use `&mut` (i.e. system symbols that return + * more than one value) */ + +impl TupleCall f32> for (f32,) { + type Output = (f32, c_int); + + fn call(self, f: fn(f32, &mut c_int) -> f32) -> Self::Output { + let mut iret = 0; + let fret = f(self.0, &mut iret); + (fret, iret) + } +} + +impl TupleCall f64> for (f64,) { + type Output = (f64, c_int); + + fn call(self, f: fn(f64, &mut c_int) -> f64) -> Self::Output { + let mut iret = 0; + let fret = f(self.0, &mut iret); + (fret, iret) + } +} + +impl TupleCall f32> for (f32,) { + type Output = (f32, f32); + + fn call(self, f: fn(f32, &mut f32) -> f32) -> Self::Output { + let mut ret2 = 0.0; + let ret1 = f(self.0, &mut ret2); + (ret1, ret2) + } +} + +impl TupleCall f64> for (f64,) { + type Output = (f64, f64); + + fn call(self, f: fn(f64, &mut f64) -> f64) -> Self::Output { + let mut ret2 = 0.0; + let ret1 = f(self.0, &mut ret2); + (ret1, ret2) + } +} + +impl TupleCall f32> for (f32, f32) { + type Output = (f32, c_int); + + fn call(self, f: fn(f32, f32, &mut c_int) -> f32) -> Self::Output { + let mut iret = 0; + let fret = f(self.0, self.1, &mut iret); + (fret, iret) + } +} + +impl TupleCall f64> for (f64, f64) { + type Output = (f64, c_int); + + fn call(self, f: fn(f64, f64, &mut c_int) -> f64) -> Self::Output { + let mut iret = 0; + let fret = f(self.0, self.1, &mut iret); + (fret, iret) + } +} + +impl TupleCall for (f32,) { + type Output = (f32, f32); + + fn call(self, f: fn(f32, &mut f32, &mut f32)) -> Self::Output { + let mut ret1 = 0.0; + let mut ret2 = 0.0; + f(self.0, &mut ret1, &mut ret2); + (ret1, ret2) + } +} + +impl TupleCall for (f64,) { + type Output = (f64, f64); + + fn call(self, f: fn(f64, &mut f64, &mut f64)) -> Self::Output { + let mut ret1 = 0.0; + let mut ret2 = 0.0; + f(self.0, &mut ret1, &mut ret2); + (ret1, ret2) + } +} + +// Implement for floats +impl CheckOutput for F +where + F: Float + Hex, + Input: Hex + fmt::Debug, + u32: TryFrom, +{ + fn validate(self, expected: Self, input: Input, allowed_ulp: u32) { + let make_msg = || { + format!( + "expected {expected:?} crate {self:?} ({expbits}, {actbits}) input {input:?} ({ibits})", + expbits = expected.hex(), + actbits = self.hex(), + ibits = input.hex() + ) + }; + + // Check when both are NaN + if self.is_nan() && expected.is_nan() { + assert_eq!( + self.to_bits(), + expected.to_bits(), + "NaN have different bitpatterns: {}", + make_msg() + ); + // Nothing else to check + return; + } else if self.is_nan() || expected.is_nan() { + panic!("mismatched NaN: {}", make_msg()); + } + + // Make sure that the signs are the same before checing ULP + assert_eq!( + self.signum(), + expected.signum(), + "mismatched signs: {}", + make_msg() + ); + + let ulp_diff = self + .to_bits() + .signed() + .checked_sub(expected.to_bits().signed()) + .unwrap() + .abs(); + + let ulp_u32 = u32::try_from(ulp_diff).unwrap_or_else(|e| { + panic!("{e:?}: ulp of {ulp_diff} exceeds u32::MAX: {}", make_msg()) + }); + + assert!( + ulp_u32 <= allowed_ulp, + "ulp {ulp_diff} > {allowed_ulp}: {}", + make_msg() + ); + } +} + +/// Implement `CheckOutput` for combinations of types. +macro_rules! impl_tuples { + ($(($a:ty, $b:ty);)*) => { + $( + impl CheckOutput for ($a, $b) { + fn validate(self, expected: Self, input: Input, allowed_ulp: u32) + { + self.0.validate(expected.0, input, allowed_ulp); + self.1.validate(expected.1, input, allowed_ulp); + } + } + )* + }; +} + +impl_tuples!( + (f32, i32); + (f64, i32); + (f32, f32); + (f64, f64); +); From 9b3ec5e8f1ef0971507b32a79a444fb0c19ad960 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:56:28 -0500 Subject: [PATCH 08/25] Add a cached test generator Add a single generator that creates the needed function signatures after being populated with values. This provides the same input for all functions. --- crates/libm-test/src/gen.rs | 78 +++++++++++++++++++++++++++++++++++++ crates/libm-test/src/lib.rs | 1 + 2 files changed, 79 insertions(+) create mode 100644 crates/libm-test/src/gen.rs diff --git a/crates/libm-test/src/gen.rs b/crates/libm-test/src/gen.rs new file mode 100644 index 000000000..1d8b33fc8 --- /dev/null +++ b/crates/libm-test/src/gen.rs @@ -0,0 +1,78 @@ +//! Different generators that can create random or systematic bit patterns. + +use crate::GenerateInput; + +/// Store inputs if the same values are used for multiple tests. The `CachedInput` then acts as 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 ExactSizeIterator { + self.inputs_f32.iter().map(|f| (f.0,)) + } +} + +impl GenerateInput<(f32, f32)> for CachedInput { + fn get_cases(&self) -> impl ExactSizeIterator { + self.inputs_f32.iter().map(|f| (f.0, f.1)) + } +} + +impl GenerateInput<(i32, f32)> for CachedInput { + fn get_cases(&self) -> impl ExactSizeIterator { + 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 ExactSizeIterator { + GenerateInput::<(i32, f32)>::get_cases(self).map(|(i, f)| (f, i)) + } +} + +impl GenerateInput<(f32, f32, f32)> for CachedInput { + fn get_cases(&self) -> impl ExactSizeIterator { + self.inputs_f32.iter().copied() + } +} + +impl GenerateInput<(f64,)> for CachedInput { + fn get_cases(&self) -> impl ExactSizeIterator { + self.inputs_f64.iter().map(|f| (f.0,)) + } +} + +impl GenerateInput<(f64, f64)> for CachedInput { + fn get_cases(&self) -> impl ExactSizeIterator { + self.inputs_f64.iter().map(|f| (f.0, f.1)) + } +} + +impl GenerateInput<(i32, f64)> for CachedInput { + fn get_cases(&self) -> impl ExactSizeIterator { + 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 ExactSizeIterator { + GenerateInput::<(i32, f64)>::get_cases(self).map(|(i, f)| (f, i)) + } +} + +impl GenerateInput<(f64, f64, f64)> for CachedInput { + fn get_cases(&self) -> impl ExactSizeIterator { + self.inputs_f64.iter().copied() + } +} diff --git a/crates/libm-test/src/lib.rs b/crates/libm-test/src/lib.rs index eef6120f2..166cd93b8 100644 --- a/crates/libm-test/src/lib.rs +++ b/crates/libm-test/src/lib.rs @@ -1,3 +1,4 @@ +pub mod gen; mod num_traits; mod test_traits; From 119a1824831932fd071d3e6713032ac95624840a Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:59:17 -0500 Subject: [PATCH 09/25] Add a test against musl libm Check our functions against `musl-math-sys`. This is similar to the existing musl tests that go through binary serialization, but works on more platforms. --- crates/libm-test/Cargo.toml | 9 ++ crates/libm-test/tests/compare_built_musl.rs | 147 +++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 crates/libm-test/tests/compare_built_musl.rs diff --git a/crates/libm-test/Cargo.toml b/crates/libm-test/Cargo.toml index b6f26964e..25bcdf91d 100644 --- a/crates/libm-test/Cargo.toml +++ b/crates/libm-test/Cargo.toml @@ -13,6 +13,15 @@ musl-bitwise-tests = ["rand"] [dependencies] libm = { path = "../..", features = ["_internal-features"] } +paste = "1.0.15" + +# We can't build musl on MSVC or wasm +[target.'cfg(not(any(target_env = "msvc", target_family = "wasm", target_feature = "thumb-mode")))'.dependencies] +musl-math-sys = { path = "../musl-math-sys" } + +[dev-dependencies] +rand = "0.8.5" +rand_chacha = "0.3.1" [build-dependencies] rand = { version = "0.8.5", optional = true } 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..6dd17226a --- /dev/null +++ b/crates/libm-test/tests/compare_built_musl.rs @@ -0,0 +1,147 @@ +//! 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. + +// Targets that we can't compile musl for +#![cfg(not(any(target_env = "msvc", target_family = "wasm")))] +// These wind up with stack overflows +#![cfg(not(all(target_family = "windows", target_env = "gnu")))] +// FIXME(#309): LE PPC crashes calling the musl version of some of these and are disabled. It +// seems like a qemu bug but should be investigated further at some point. See +// . +#![cfg(not(all(target_arch = "powerpc64", target_endian = "little")))] + +use std::ffi::c_int; +use std::sync::LazyLock; + +use libm_test::gen::CachedInput; +use libm_test::{CheckOutput, GenerateInput, TupleCall}; +use musl_math_sys as musl; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +const SEED: [u8; 32] = *b"3.141592653589793238462643383279"; + +const NTESTS: usize = { + let mut ntests = if cfg!(optimizations_enabled) { + 5000 + } else { + 500 + }; + + // Tests can be pretty slow on non-64-bit targets and, for some reason, ppc. + if !cfg!(target_pointer_width = "64") || cfg!(target_arch = "powerpc64") { + ntests /= 5; + } + + ntests +}; + +/// ULP allowed to differ from musl (note that musl itself may not be accurate). +const ALLOWED_ULP: u32 = 2; + +/// Certain functions have different allowed ULP (consider these xfail). +/// +/// Currently this includes: +/// - gamma functions that have higher errors +/// - 32-bit functions fall back to a less precise algorithm. +const ULP_OVERRIDES: &[(&str, u32)] = &[ + #[cfg(x86_no_sse)] + ("asinhf", 6), + ("lgamma", 6), + ("lgamma_r", 6), + ("lgammaf", 6), + ("lgammaf_r", 6), + ("tanh", 4), + ("tgamma", 8), + #[cfg(not(target_pointer_width = "64"))] + ("exp10", 4), + #[cfg(not(target_pointer_width = "64"))] + ("exp10f", 4), +]; + +/// 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(|| { + // It is easy to overflow the stack with these in debug mode + let iterations = if cfg!(optimizations_enabled) && cfg!(target_pointer_width = "64") { + 0xffff + } else if cfg!(windows) { + 0x00ff + } else { + 0x0fff + }; + + let mut cases = (&*TEST_CASES).clone(); + for case in cases.inputs_i32.iter_mut() { + case.0 = iterations; + } + for case in cases.inputs_i32.iter_mut() { + case.0 = iterations; + } + cases +}); + +fn make_test_cases(ntests: usize) -> CachedInput { + let mut rng = ChaCha8Rng::from_seed(SEED); + + let inputs_i32 = (0..ntests).map(|_| rng.gen::<(i32, i32, i32)>()).collect(); + let inputs_f32 = (0..ntests).map(|_| rng.gen::<(f32, f32, f32)>()).collect(); + let inputs_f64 = (0..ntests).map(|_| rng.gen::<(f64, f64, f64)>()).collect(); + + CachedInput { + inputs_f32, + inputs_f64, + inputs_i32, + } +} + +macro_rules! musl_rand_tests { + (@each_signature + SysArgsTupleTy: $_sys_argty:ty, + RustArgsTupleTy: $argty:ty, + SysFnTy: $fnty_sys:ty, + RustFnTy: $fnty_rust:ty, + functions: [$( { + attrs: [$($fn_meta:meta),*], + fn_name: $name:ident, + } ),*], + ) => { paste::paste! { + $( + #[test] + $(#[$fn_meta])* + fn [< musl_random_ $name >]() { + let fname = stringify!($name); + let inputs = if fname == "jn" || fname == "jnf" { + &TEST_CASES_JN + } else { + &TEST_CASES + }; + + let ulp = match ULP_OVERRIDES.iter().find(|(name, _val)| name == &fname) { + Some((_name, val)) => *val, + None => ALLOWED_ULP, + }; + + let cases = >::get_cases(inputs); + for input in cases { + let mres = input.call(musl::$name as $fnty_sys); + let cres = input.call(libm::$name as $fnty_rust); + + mres.validate(cres, input, ulp); + } + } + )* + } }; + + (@all_items$($tt:tt)*) => {}; +} + +libm::for_each_function!(musl_rand_tests); From bb5e8d0f275683054d8dae627fcc3552792d8739 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:33:27 -0500 Subject: [PATCH 10/25] Add dockerfiles for i586, riscv, and thumb These targets are tested in `compiler-builtins`, but not yet `libm`. Add dockerfiles to prepare for this. --- ci/docker/i586-unknown-linux-gnu/Dockerfile | 5 +++++ ci/docker/riscv64gc-unknown-linux-gnu/Dockerfile | 15 +++++++++++++++ ci/docker/thumbv6m-none-eabi/Dockerfile | 9 +++++++++ ci/docker/thumbv7em-none-eabi/Dockerfile | 9 +++++++++ ci/docker/thumbv7em-none-eabihf/Dockerfile | 9 +++++++++ ci/docker/thumbv7m-none-eabi/Dockerfile | 9 +++++++++ 6 files changed, 56 insertions(+) create mode 100644 ci/docker/i586-unknown-linux-gnu/Dockerfile create mode 100644 ci/docker/riscv64gc-unknown-linux-gnu/Dockerfile create mode 100644 ci/docker/thumbv6m-none-eabi/Dockerfile create mode 100644 ci/docker/thumbv7em-none-eabi/Dockerfile create mode 100644 ci/docker/thumbv7em-none-eabihf/Dockerfile create mode 100644 ci/docker/thumbv7m-none-eabi/Dockerfile 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 From ee24e8867aca9c73b28f96c2c95da2fc2507c699 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 23:00:17 -0500 Subject: [PATCH 11/25] Disable a unit test that is failing on i586 --- src/math/rem_pio2.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/math/rem_pio2.rs b/src/math/rem_pio2.rs index 644616f2d..6eca1c539 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); From 9dd315644a578e16b3237dbaf8a65947997b47c8 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Wed, 16 Oct 2024 22:59:59 -0500 Subject: [PATCH 12/25] Enable more targets on CI This brings the targets tested here in line with those tested in `compiler-builtins`. --- .github/workflows/main.yml | 130 ++++++++++++++++++++++++++++--------- ci/run.sh | 54 +++++++++++---- 2 files changed, 142 insertions(+), 42 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 322043d85..8b1433da2 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: name: Docker - runs-on: ubuntu-latest + timeout-minutes: 10 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 + - 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 }} steps: - - uses: actions/checkout@master - - name: Install Rust - run: rustup update nightly && 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 + run: ./ci/run.sh ${{ matrix.target }} + if: matrix.os != 'ubuntu-latest' + shell: bash + env: + BUILD_ONLY: ${{ matrix.build_only }} + + # 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 }} rustfmt: name: Rustfmt @@ -41,7 +106,10 @@ jobs: steps: - uses: actions/checkout@master - name: Install Rust - run: rustup update stable && rustup default stable && rustup component add rustfmt + run: | + rustup update stable --no-self-update + rustup default stable + rustup component add rustfmt - run: cargo fmt -- --check wasm: @@ -50,17 +118,19 @@ jobs: steps: - uses: actions/checkout@master - name: Install Rust - run: rustup update nightly && rustup default nightly + run: rustup update nightly --no-self-update && rustup default nightly - run: rustup target add wasm32-unknown-unknown + - name: Download MUSL source + run: ./ci/download-musl.sh - run: cargo build --target wasm32-unknown-unknown - cb: + builtins: name: "The compiler-builtins crate works" runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Install Rust - run: rustup update nightly && rustup default nightly + run: rustup update nightly --no-self-update && rustup default nightly - run: cargo build -p cb benchmarks: @@ -69,7 +139,9 @@ jobs: steps: - uses: actions/checkout@master - name: Install Rust - run: rustup update nightly && rustup default nightly + run: rustup update nightly --no-self-update && rustup default nightly + - name: Download MUSL source + run: ./ci/download-musl.sh - run: cargo bench --all success: @@ -77,7 +149,7 @@ jobs: - docker - rustfmt - wasm - - cb + - builtins - benchmarks runs-on: ubuntu-latest # GitHub branch protection is exceedingly silly and treats "jobs skipped because a dependency diff --git a/ci/run.sh b/ci/run.sh index 1b016cc4f..b94572eeb 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -2,21 +2,49 @@ 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" -# stable by default -$cmd -$cmd --release +target="${1:-}" + +if [ -z "$target" ]; then + host_target=$(rustc -vV | awk '/^host/ { print $2 }') + echo "Defaulted to host target $host_target" + target="$host_target" +fi + + +# We nceed to specifically skip tests for this crate on systems that can't +# build musl since otherwise `--all` will activate it. +case "$target" in + *msvc*) exclude_flag="--exclude musl-math-sys" ;; + *wasm*) exclude_flag="--exclude musl-math-sys" ;; + *thumb*) exclude_flag="--exclude musl-math-sys" ;; + *) exclude_flag="" ;; +esac + +if [ "${BUILD_ONLY:-}" = "1" ]; then + cmd="cargo build --target $target --package libm" + $cmd + $cmd --features 'unstable' + + echo "no tests to run for no_std" +else + cmd="cargo test --all --target $target $exclude_flag" + + + # stable by default + $cmd + $cmd --release -# unstable with a feature -$cmd --features 'unstable' -$cmd --release --features 'unstable' + # unstable with a feature + $cmd --features 'unstable' + $cmd --release --features 'unstable' -# also run the reference tests -$cmd --features 'unstable libm-test/musl-bitwise-tests' -$cmd --release --features 'unstable libm-test/musl-bitwise-tests' + if [ "$(uname -a)" = "Linux" ]; then + # also run the reference tests when we can. requires a Linux host. + $cmd --features 'unstable libm-test/musl-bitwise-tests' + $cmd --release --features 'unstable libm-test/musl-bitwise-tests' + fi +fi From dfca74c9a5700639b0a224b48b46e17bd17c4e1f Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 15:22:49 -0500 Subject: [PATCH 13/25] Add macros crate --- Cargo.toml | 1 + crates/libm-macros/Cargo.toml | 12 + crates/libm-macros/src/lib.rs | 462 ++++++++++++++++++++++++++++++ crates/libm-macros/tests/basic.rs | 9 + 4 files changed, 484 insertions(+) create mode 100644 crates/libm-macros/Cargo.toml create mode 100644 crates/libm-macros/src/lib.rs create mode 100644 crates/libm-macros/tests/basic.rs diff --git a/Cargo.toml b/Cargo.toml index 3448648bf..024cc34e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ resolver = "2" members = [ "crates/compiler-builtins-smoke-test", "crates/libm-bench", + "crates/libm-macros", "crates/libm-test", "crates/musl-math-sys", ] diff --git a/crates/libm-macros/Cargo.toml b/crates/libm-macros/Cargo.toml new file mode 100644 index 000000000..5859df214 --- /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"] } diff --git a/crates/libm-macros/src/lib.rs b/crates/libm-macros/src/lib.rs new file mode 100644 index 000000000..41c0a6051 --- /dev/null +++ b/crates/libm-macros/src/lib.rs @@ -0,0 +1,462 @@ +#![allow(unused)] + +use std::{collections::BTreeMap, sync::LazyLock}; + +use proc_macro as pm; +use proc_macro2::{self as pm2, Span}; +use quote::ToTokens; +use syn::{ + bracketed, + parse::{self, Parse, ParseStream, Parser}, + punctuated::Punctuated, + spanned::Spanned, + token::Comma, + Attribute, Expr, ExprArray, ExprPath, Ident, Meta, PatPath, Path, Token, +}; + +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", "trunc", + ], + ), + ( + // `(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 +#[derive(Debug, Clone)] +enum Ty { + F16, + F32, + F64, + F128, + I32, + CInt, + MutF16, + MutF32, + MutF64, + MutF128, + MutI32, + MutCInt, +} + +/// Representation of e.g. `(f32, f32) -> f32` +#[derive(Debug, Clone)] +struct Signature { + args: &'static [Ty], + returns: &'static [Ty], +} + +#[derive(Debug, Clone)] +struct ApiSignature { + sys_sig: Signature, + rust_sig: Signature, + name: &'static str, +} + +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 = ApiSignature { + rust_sig: rust_sig.clone(), + sys_sig: c_sig.clone().unwrap_or_else(|| rust_sig.clone()), + name, + }; + ret.push(api); + } + } + + ret +}); + +/* + +Invoke as: + +for_each_function! { + callback: some_macro, + skip: [foo, bar], + attributes: [ + #[meta1] + #[meta2] + [baz, qux], + ] +} + +*/ + +#[proc_macro] +pub fn for_each_function(tokens: pm::TokenStream) -> pm::TokenStream { + let input = syn::parse_macro_input!(tokens as Invocation); + let structured = match Structured::from_fields(input) { + Ok(v) => v, + Err(e) => return e.into_compile_error().into(), + }; + + panic!("{structured:#?}"); + + todo!(); + tokens +} + +// fn inner(input: Invocation) -> syn::Result { + +// } + +#[derive(Debug)] +struct Invocation { + fields: Punctuated, +} + +impl Parse for Invocation { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + fields: input.parse_terminated(Field::parse, Token![,])?, + }) + } +} + +#[derive(Debug)] +struct Field { + name: Ident, + sep: Token![:], + expr: Expr, +} + +impl Parse for Field { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + name: input.parse()?, + sep: input.parse()?, + expr: input.parse()?, + }) + } +} + +#[derive(Debug)] +struct Structured { + callback: Ident, + skip: Vec, + attributes: Vec, +} + +impl Structured { + 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")?; + let attr_expr = expect_field(&mut map, "attributes")?; + + if !map.is_empty() { + Err(syn::Error::new( + map.first().unwrap().name.span(), + format!("unexpected fields {map:?}"), + ))? + } + + let skip = Parser::parse2(parse_ident_array, skip_expr.into_token_stream())?; + let attr_exprs = Parser::parse2(parse_expr_array, attr_expr.into_token_stream())?; + let mut attributes = Vec::new(); + + for attr in attr_exprs { + attributes.push(syn::parse2(attr.into_token_stream())?); + } + + Ok(Self { + callback: expect_ident(cb_expr)?, + skip, + attributes, + }) + } +} + +/// 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()) +} + +/// A mapping of attributes to identifiers (just a simplified `Expr`). +/// +/// Expressed as: +/// +/// ```ignore +/// #[meta1] +/// #[meta2] +/// [foo, bar, baz] +/// ``` +#[derive(Debug)] +struct AttributeMap { + meta: Vec, + 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..f6b2952f9 --- /dev/null +++ b/crates/libm-macros/tests/basic.rs @@ -0,0 +1,9 @@ +libm_macros::for_each_function! { + callback: foo, + skip: [foo, bar], + attributes: [ + #[meta1] + #[meta2] + [baz, corge] + ], +} From e9d35e5c924e40f7377067fbb85aedcade72b3ec Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 15:42:57 -0500 Subject: [PATCH 14/25] Basic expansion working --- crates/libm-macros/src/lib.rs | 165 +++++++----------------------- crates/libm-macros/src/parse.rs | 139 +++++++++++++++++++++++++ crates/libm-macros/tests/basic.rs | 16 ++- 3 files changed, 190 insertions(+), 130 deletions(-) create mode 100644 crates/libm-macros/src/parse.rs diff --git a/crates/libm-macros/src/lib.rs b/crates/libm-macros/src/lib.rs index 41c0a6051..a4f6e686d 100644 --- a/crates/libm-macros/src/lib.rs +++ b/crates/libm-macros/src/lib.rs @@ -1,13 +1,16 @@ #![allow(unused)] +mod parse; +use parse::{Invocation, StructuredInput}; + use std::{collections::BTreeMap, sync::LazyLock}; use proc_macro as pm; use proc_macro2::{self as pm2, Span}; -use quote::ToTokens; +use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ bracketed, - parse::{self, Parse, ParseStream, Parser}, + parse::{Parse, ParseStream, Parser}, punctuated::Punctuated, spanned::Spanned, token::Comma, @@ -320,143 +323,47 @@ for_each_function! { #[proc_macro] pub fn for_each_function(tokens: pm::TokenStream) -> pm::TokenStream { let input = syn::parse_macro_input!(tokens as Invocation); - let structured = match Structured::from_fields(input) { + let structured = match StructuredInput::from_fields(input) { Ok(v) => v, Err(e) => return e.into_compile_error().into(), }; - panic!("{structured:#?}"); - - todo!(); - tokens -} - -// fn inner(input: Invocation) -> syn::Result { - -// } - -#[derive(Debug)] -struct Invocation { - fields: Punctuated, -} - -impl Parse for Invocation { - fn parse(input: ParseStream) -> syn::Result { - Ok(Self { - fields: input.parse_terminated(Field::parse, Token![,])?, - }) - } -} - -#[derive(Debug)] -struct Field { - name: Ident, - sep: Token![:], - expr: Expr, -} - -impl Parse for Field { - fn parse(input: ParseStream) -> syn::Result { - Ok(Self { - name: input.parse()?, - sep: input.parse()?, - expr: input.parse()?, - }) - } -} - -#[derive(Debug)] -struct Structured { - callback: Ident, - skip: Vec, - attributes: Vec, + expand(structured).into() } -impl Structured { - 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")?; - let attr_expr = expect_field(&mut map, "attributes")?; +fn expand(input: StructuredInput) -> pm2::TokenStream { + let callback = input.callback; + let mut out = pm2::TokenStream::new(); - if !map.is_empty() { - Err(syn::Error::new( - map.first().unwrap().name.span(), - format!("unexpected fields {map:?}"), - ))? - } - - let skip = Parser::parse2(parse_ident_array, skip_expr.into_token_stream())?; - let attr_exprs = Parser::parse2(parse_expr_array, attr_expr.into_token_stream())?; - let mut attributes = Vec::new(); + for func in ALL_FUNCTIONS_FLAT.iter() { + let fn_name = Ident::new(func.name, Span::call_site()); - for attr in attr_exprs { - attributes.push(syn::parse2(attr.into_token_stream())?); + // No output on functions that should be skipped + if input.skip.contains(&fn_name) { + continue; } - Ok(Self { - callback: expect_ident(cb_expr)?, - skip, - attributes, - }) + let mut meta = input + .attributes + .iter() + .filter(|map| map.names.contains(&fn_name)) + .flat_map(|map| &map.meta); + + let new = quote! { + #callback! { + fn_name: #fn_name, + CArgsTuple: f32, + RustArgsTuple: f32, + CFnTy: f32, + RustFnTy: f32, + attrs: [ + #( #meta )* + ] + } + }; + + out.extend(new); } -} - -/// 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()) -} - -/// A mapping of attributes to identifiers (just a simplified `Expr`). -/// -/// Expressed as: -/// -/// ```ignore -/// #[meta1] -/// #[meta2] -/// [foo, bar, baz] -/// ``` -#[derive(Debug)] -struct AttributeMap { - meta: Vec, - 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)?, - }) - } + out } diff --git a/crates/libm-macros/src/parse.rs b/crates/libm-macros/src/parse.rs new file mode 100644 index 000000000..3cf929a03 --- /dev/null +++ b/crates/libm-macros/src/parse.rs @@ -0,0 +1,139 @@ +use proc_macro as pm; +use proc_macro2::{self as pm2, Span}; +use quote::ToTokens; +use syn::{ + bracketed, + parse::{self, Parse, ParseStream, Parser}, + punctuated::Punctuated, + spanned::Spanned, + token::Comma, + Attribute, Expr, ExprArray, ExprPath, Ident, Meta, PatPath, Path, Token, +}; + +#[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. +#[derive(Debug)] +pub struct StructuredInput { + pub callback: Ident, + pub skip: Vec, + pub attributes: Vec, +} + +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")?; + let attr_expr = expect_field(&mut map, "attributes")?; + + if !map.is_empty() { + Err(syn::Error::new( + map.first().unwrap().name.span(), + format!("unexpected fields {map:?}"), + ))? + } + + let skip = Parser::parse2(parse_ident_array, skip_expr.into_token_stream())?; + let attr_exprs = Parser::parse2(parse_expr_array, attr_expr.into_token_stream())?; + let mut attributes = Vec::new(); + + for attr in attr_exprs { + attributes.push(syn::parse2(attr.into_token_stream())?); + } + + Ok(Self { + callback: expect_ident(cb_expr)?, + skip, + attributes, + }) + } +} + +/// 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()) +} + +/// 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 index f6b2952f9..2864e522b 100644 --- a/crates/libm-macros/tests/basic.rs +++ b/crates/libm-macros/tests/basic.rs @@ -1,5 +1,19 @@ +macro_rules! basic { + ( + fn_name: $fn_name:ident, + CArgsTuple: $CArgsTuple:ty, + RustArgsTuple: $RustArgsTuple:ty, + CFnTy: $CFnTy:ty, + RustFnTy: $RustFnTy:ty, + attrs: [$($meta:meta)*] + + ) => { + + }; +} + libm_macros::for_each_function! { - callback: foo, + callback: basic, skip: [foo, bar], attributes: [ #[meta1] From b1e744b406246d6fa46b661da211769b3ce77a9e Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 16:14:37 -0500 Subject: [PATCH 15/25] Seems like the proc macro works fine --- crates/libm-macros/src/lib.rs | 81 ++++++++++++++++++++++++++----- crates/libm-macros/tests/basic.rs | 35 +++++++++---- 2 files changed, 93 insertions(+), 23 deletions(-) diff --git a/crates/libm-macros/src/lib.rs b/crates/libm-macros/src/lib.rs index a4f6e686d..ec3cdf00f 100644 --- a/crates/libm-macros/src/lib.rs +++ b/crates/libm-macros/src/lib.rs @@ -29,7 +29,7 @@ const ALL_FUNCTIONS: &[(Signature, Option, &[&str])] = &[ "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", "trunc", + "sqrtf", "tanf", "tanhf", "tgammaf", "truncf", ], ), ( @@ -256,8 +256,8 @@ const ALL_FUNCTIONS: &[(Signature, Option, &[&str])] = &[ ), ]; -/// A type used in a function signature -#[derive(Debug, Clone)] +/// A type used in a function signature. +#[derive(Debug, Clone, Copy)] enum Ty { F16, F32, @@ -273,6 +273,27 @@ enum Ty { 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 { @@ -280,21 +301,23 @@ struct Signature { returns: &'static [Ty], } +/// Combined information about a function implementation. #[derive(Debug, Clone)] -struct ApiSignature { - sys_sig: Signature, - rust_sig: Signature, +struct FunctionInfo { name: &'static str, + c_sig: Signature, + rust_sig: Signature, } -static ALL_FUNCTIONS_FLAT: LazyLock> = LazyLock::new(|| { +/// 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 = ApiSignature { + let api = FunctionInfo { rust_sig: rust_sig.clone(), - sys_sig: c_sig.clone().unwrap_or_else(|| rust_sig.clone()), + c_sig: c_sig.clone().unwrap_or_else(|| rust_sig.clone()), name, }; ret.push(api); @@ -328,9 +351,34 @@ pub fn for_each_function(tokens: pm::TokenStream) -> pm::TokenStream { Err(e) => return e.into_compile_error().into(), }; + if let Some(e) = validate(&structured) { + return e.into_compile_error().into(); + } + expand(structured).into() } +fn validate(input: &StructuredInput) -> Option { + let mentioned_functions = input.skip.iter().chain( + input + .attributes + .iter() + .flat_map(|attr_map| attr_map.names.iter()), + ); + + for mentioned in mentioned_functions { + if !ALL_FUNCTIONS_FLAT.iter().any(|func| mentioned == func.name) { + let e = syn::Error::new( + mentioned.span(), + format!("unrecognized function name `{mentioned}`"), + ); + return Some(e); + } + } + + None +} + fn expand(input: StructuredInput) -> pm2::TokenStream { let callback = input.callback; let mut out = pm2::TokenStream::new(); @@ -349,13 +397,20 @@ fn expand(input: StructuredInput) -> pm2::TokenStream { .filter(|map| map.names.contains(&fn_name)) .flat_map(|map| &map.meta); + 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, - CArgsTuple: f32, - RustArgsTuple: f32, - CFnTy: f32, - RustFnTy: f32, + CFn: fn( #(#c_args),* ,) -> ( #(#c_ret),* ), + CArgs: ( #(#c_args),* ,), + CRet: ( #(#c_ret),* ), + RustFn: fn( #(#rust_args),* ,) -> ( #(#rust_ret),* ), + RustArgs: ( #(#rust_args),* ,), + RustRet: ( #(#rust_ret),* ), attrs: [ #( #meta )* ] diff --git a/crates/libm-macros/tests/basic.rs b/crates/libm-macros/tests/basic.rs index 2864e522b..e52f9ad93 100644 --- a/crates/libm-macros/tests/basic.rs +++ b/crates/libm-macros/tests/basic.rs @@ -1,23 +1,38 @@ macro_rules! basic { ( fn_name: $fn_name:ident, - CArgsTuple: $CArgsTuple:ty, - RustArgsTuple: $RustArgsTuple:ty, - CFnTy: $CFnTy:ty, - RustFnTy: $RustFnTy:ty, + CFn: $CFn:ty, + CArgs: $CArgs:ty, + CRet: $CRet:ty, + RustFn: $RustFn:ty, + RustArgs: $RustArgs:ty, + RustRet: $RustRet:ty, attrs: [$($meta:meta)*] - + ) => { - + $(#[$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; + } }; } libm_macros::for_each_function! { callback: basic, - skip: [foo, bar], + skip: [sin, cos], attributes: [ - #[meta1] - #[meta2] - [baz, corge] + // just some random attributes + #[allow(clippy::pedantic)] + #[allow(dead_code)] + [sinf, cosf] ], } From 3d1d0aef0500b08f1b2b0ba2c9bba33694592633 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 16:42:02 -0500 Subject: [PATCH 16/25] Replace one use of for_each_function --- Cargo.toml | 1 + crates/libm-macros/src/lib.rs | 4 +++- crates/libm-macros/src/parse.rs | 3 +++ crates/libm-macros/tests/basic.rs | 4 ++++ crates/libm-test/Cargo.toml | 1 + crates/libm-test/tests/check_coverage.rs | 27 +++++++++++++++--------- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 024cc34e3..7d1d52155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ ] default-members = [ ".", + "crates/libm-macros", "crates/libm-test", ] diff --git a/crates/libm-macros/src/lib.rs b/crates/libm-macros/src/lib.rs index ec3cdf00f..961408591 100644 --- a/crates/libm-macros/src/lib.rs +++ b/crates/libm-macros/src/lib.rs @@ -380,8 +380,9 @@ fn validate(input: &StructuredInput) -> Option { } fn expand(input: StructuredInput) -> pm2::TokenStream { - let callback = input.callback; let mut out = pm2::TokenStream::new(); + let callback = input.callback; + let extra = input.extra; for func in ALL_FUNCTIONS_FLAT.iter() { let fn_name = Ident::new(func.name, Span::call_site()); @@ -405,6 +406,7 @@ fn expand(input: StructuredInput) -> pm2::TokenStream { let new = quote! { #callback! { fn_name: #fn_name, + extra: #extra, CFn: fn( #(#c_args),* ,) -> ( #(#c_ret),* ), CArgs: ( #(#c_args),* ,), CRet: ( #(#c_ret),* ), diff --git a/crates/libm-macros/src/parse.rs b/crates/libm-macros/src/parse.rs index 3cf929a03..565ebd5b5 100644 --- a/crates/libm-macros/src/parse.rs +++ b/crates/libm-macros/src/parse.rs @@ -47,6 +47,7 @@ pub struct StructuredInput { pub callback: Ident, pub skip: Vec, pub attributes: Vec, + pub extra: Expr, } impl StructuredInput { @@ -55,6 +56,7 @@ impl StructuredInput { let cb_expr = expect_field(&mut map, "callback")?; let skip_expr = expect_field(&mut map, "skip")?; let attr_expr = expect_field(&mut map, "attributes")?; + let extra = expect_field(&mut map, "extra")?; if !map.is_empty() { Err(syn::Error::new( @@ -75,6 +77,7 @@ impl StructuredInput { callback: expect_ident(cb_expr)?, skip, attributes, + extra, }) } } diff --git a/crates/libm-macros/tests/basic.rs b/crates/libm-macros/tests/basic.rs index e52f9ad93..1254bff3c 100644 --- a/crates/libm-macros/tests/basic.rs +++ b/crates/libm-macros/tests/basic.rs @@ -1,6 +1,7 @@ macro_rules! basic { ( fn_name: $fn_name:ident, + extra: [$($extra_tt:tt)*], CFn: $CFn:ty, CArgs: $CArgs:ty, CRet: $CRet:ty, @@ -22,6 +23,8 @@ macro_rules! basic { type RustArgsTy = $RustArgs; #[allow(unused)] type RustRetTy = $RustRet; + #[allow(unused)] + const A: &[&str] = &[$($extra_tt)*]; } }; } @@ -35,4 +38,5 @@ libm_macros::for_each_function! { #[allow(dead_code)] [sinf, cosf] ], + extra: ["foo", "bar"], } diff --git a/crates/libm-test/Cargo.toml b/crates/libm-test/Cargo.toml index 25bcdf91d..f3e571058 100644 --- a/crates/libm-test/Cargo.toml +++ b/crates/libm-test/Cargo.toml @@ -13,6 +13,7 @@ musl-bitwise-tests = ["rand"] [dependencies] libm = { path = "../..", features = ["_internal-features"] } +libm-macros = { path = "../libm-macros" } paste = "1.0.15" # We can't build musl on MSVC or wasm diff --git a/crates/libm-test/tests/check_coverage.rs b/crates/libm-test/tests/check_coverage.rs index 284f980ee..e84105d1a 100644 --- a/crates/libm-test/tests/check_coverage.rs +++ b/crates/libm-test/tests/check_coverage.rs @@ -19,24 +19,31 @@ const ALLOWED_SKIPS: &[&str] = &[ "rem_pio2f", ]; -macro_rules! function_names { +macro_rules! callback { ( - @all_items - fn_names: [ $( $name:ident ),* ] + fn_name: $name:ident, + extra: [$push_to:ident], + $($_rest:tt)* + ) => { - const INCLUDED_FUNCTIONS: &[&str] = &[ $( stringify!($name) ),* ]; + $push_to.push(stringify!($name)); }; - (@each_signature $($tt:tt)*) => {}; } -libm::for_each_function!(function_names); - #[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, + skip: [], + attributes: [], + extra: [included], + }; + for f in libm_test::ALL_FUNCTIONS { - if !INCLUDED_FUNCTIONS.contains(f) && !ALLOWED_SKIPS.contains(f) { + if !included.contains(f) && !ALLOWED_SKIPS.contains(f) { missing.push(f) } } @@ -44,8 +51,8 @@ fn test_for_each_function_all_included() { if !missing.is_empty() { panic!( "missing tests for the following: {missing:#?} \ - \nmake sure any new functions are entered in the \ - `for_each_function` macro definition." + \nmake sure any new functions are entered in \ + `ALL_FUNCTIONS` (in `libm-macros`)." ); } } From 1ebdc854a91a8ecb907744fb2ce54271499a48d1 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 16:47:12 -0500 Subject: [PATCH 17/25] replace another instance --- crates/libm-test/tests/compare_built_musl.rs | 78 ++++++++++---------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/crates/libm-test/tests/compare_built_musl.rs b/crates/libm-test/tests/compare_built_musl.rs index 6dd17226a..6d4a503f4 100644 --- a/crates/libm-test/tests/compare_built_musl.rs +++ b/crates/libm-test/tests/compare_built_musl.rs @@ -15,7 +15,6 @@ // . #![cfg(not(all(target_arch = "powerpc64", target_endian = "little")))] -use std::ffi::c_int; use std::sync::LazyLock; use libm_test::gen::CachedInput; @@ -104,44 +103,49 @@ fn make_test_cases(ntests: usize) -> CachedInput { } macro_rules! musl_rand_tests { - (@each_signature - SysArgsTupleTy: $_sys_argty:ty, - RustArgsTupleTy: $argty:ty, - SysFnTy: $fnty_sys:ty, - RustFnTy: $fnty_rust:ty, - functions: [$( { - attrs: [$($fn_meta:meta),*], - fn_name: $name:ident, - } ),*], + ( + fn_name: $fn_name:ident, + extra: [], + CFn: $CFn:ty, + CArgs: $CArgs:ty, + CRet: $CRet:ty, + RustFn: $RustFn:ty, + RustArgs: $RustArgs:ty, + RustRet: $RustRet:ty, + attrs: [$($meta:meta)*] ) => { paste::paste! { - $( - #[test] - $(#[$fn_meta])* - fn [< musl_random_ $name >]() { - let fname = stringify!($name); - let inputs = if fname == "jn" || fname == "jnf" { - &TEST_CASES_JN - } else { - &TEST_CASES - }; - - let ulp = match ULP_OVERRIDES.iter().find(|(name, _val)| name == &fname) { - Some((_name, val)) => *val, - None => ALLOWED_ULP, - }; - - let cases = >::get_cases(inputs); - for input in cases { - let mres = input.call(musl::$name as $fnty_sys); - let cres = input.call(libm::$name as $fnty_rust); - - mres.validate(cres, input, ulp); - } + #[test] + $(#[$meta])* + fn [< musl_random_ $fn_name >]() { + let fname = stringify!($fn_name); + let inputs = if fname == "jn" || fname == "jnf" { + &TEST_CASES_JN + } else { + &TEST_CASES + }; + + let ulp = match ULP_OVERRIDES.iter().find(|(name, _val)| name == &fname) { + Some((_name, val)) => *val, + None => ALLOWED_ULP, + }; + + let cases = >::get_cases(inputs); + for input in cases { + let mres = input.call(musl::$fn_name as $CFn); + let cres = input.call(libm::$fn_name as $RustFn); + + mres.validate(cres, input, ulp); } - )* + } } }; - - (@all_items$($tt:tt)*) => {}; } -libm::for_each_function!(musl_rand_tests); +libm_macros::for_each_function! { + callback: musl_rand_tests, + skip: [], + attributes: [ + #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 + [exp10f, exp2f] + ], + extra: [], +} From 384f4b83756ded60883a2496545ffc1c5f416bac Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 16:48:47 -0500 Subject: [PATCH 18/25] Clean up unused code --- Cargo.toml | 3 - crates/libm-test/Cargo.toml | 2 +- src/lib.rs | 1 - src/macros.rs | 245 ------------------------------------ 4 files changed, 1 insertion(+), 250 deletions(-) delete mode 100644 src/macros.rs diff --git a/Cargo.toml b/Cargo.toml index 7d1d52155..802a2dacb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,6 @@ unstable = [] # Used to prevent using any intrinsics or arch-specific code. force-soft-floats = [] -# Features that aren't meant to be part of the public API -_internal-features = [] - [workspace] resolver = "2" members = [ diff --git a/crates/libm-test/Cargo.toml b/crates/libm-test/Cargo.toml index f3e571058..dc5c457a3 100644 --- a/crates/libm-test/Cargo.toml +++ b/crates/libm-test/Cargo.toml @@ -12,7 +12,7 @@ default = [] musl-bitwise-tests = ["rand"] [dependencies] -libm = { path = "../..", features = ["_internal-features"] } +libm = { path = "../.." } libm-macros = { path = "../libm-macros" } paste = "1.0.15" diff --git a/src/lib.rs b/src/lib.rs index 6c55df76f..23885ecf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,6 @@ #![allow(clippy::assign_op_pattern)] mod libm_helper; -mod macros; mod math; use core::{f32, f64}; diff --git a/src/macros.rs b/src/macros.rs deleted file mode 100644 index 792c1a94c..000000000 --- a/src/macros.rs +++ /dev/null @@ -1,245 +0,0 @@ -/// 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. -#[macro_export] -#[cfg(feature = "_internal-features")] -macro_rules! for_each_function { - // Main invocation - // - // Just calls back to this macro with a list of all functions in this crate. - ($call_this:ident) => { - $crate::for_each_function! { - @implementation - user_macro: $call_this; - - // Up to date list of all functions in this crate, grouped by signature. - // Some signatures have a different signature in the system libraries and in Rust, e.g. - // when multiple values are returned. These are the cases with multiple signatures - // (`as` in between). - (f32) -> f32 { - acosf; - acoshf; - asinf; - asinhf; - atanf; - atanhf; - cbrtf; - ceilf; - cosf; - coshf; - erff; - #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 - exp10f; - #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 - exp2f; - expf; - expm1f; - fabsf; - floorf; - j0f; - j1f; - lgammaf; - log10f; - log1pf; - log2f; - logf; - rintf; - roundf; - sinf; - sinhf; - sqrtf; - tanf; - tanhf; - tgammaf; - truncf; - }; - - (f64) -> f64 { - acos; - acosh; - asin; - asinh; - atan; - atanh; - cbrt; - ceil; - cos; - cosh; - erf; - #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 - exp10; - #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 - exp2; - exp; - expm1; - fabs; - floor; - j0; - j1; - lgamma; - log10; - log1p; - log2; - log; - rint; - round; - sin; - sinh; - sqrt; - tan; - tanh; - tgamma; - trunc; - }; - - (f32, f32) -> f32 { - atan2f; - copysignf; - fdimf; - fmaxf; - fminf; - fmodf; - hypotf; - nextafterf; - powf; - remainderf; - }; - - (f64, f64) -> f64 { - atan2; - copysign; - fdim; - fmax; - fmin; - fmod; - hypot; - nextafter; - pow; - remainder; - }; - - (f32, f32, f32) -> f32 { - fmaf; - }; - - (f64, f64, f64) -> f64 { - fma; - }; - - (f32) -> i32 { - ilogbf; - }; - - (f64) -> i32 { - ilogb; - }; - - (i32, f32) -> f32 { - jnf; - }; - - (f32, i32) -> f32 { - scalbnf; - ldexpf; - }; - - (i32, f64) -> f64 { - jn; - }; - - (f64, i32) -> f64 { - scalbn; - ldexp; - }; - - (f32, &mut f32) -> f32 as (f32) -> (f32, f32) { - modff; - }; - - (f64, &mut f64) -> f64 as (f64) -> (f64, f64) { - modf; - }; - - (f32, &mut c_int) -> f32 as (f32) -> (f32, i32) { - frexpf; - lgammaf_r; - }; - - (f64, &mut c_int) -> f64 as (f64) -> (f64, i32) { - frexp; - lgamma_r; - }; - - (f32, f32, &mut c_int) -> f32 as (f32, f32) -> (f32, i32) { - remquof; - }; - - (f64, f64, &mut c_int) -> f64 as (f64, f64) -> (f64, i32) { - remquo; - }; - - (f32, &mut f32, &mut f32) -> () as (f32) -> (f32, f32) { - sincosf; - }; - - (f64, &mut f64, &mut f64) -> () as (f64) -> (f64, f64) { - sincos; - }; - } - }; - - // This branch processes the function list and passes it to the user macro callback. - ( - @implementation - user_macro: $call_this:ident; - $( - // Main signature - ($($sys_arg:ty),+) -> $sys_ret:ty - // If the Rust signature is different from system, it is provided with `as` - $(as ($($rust_arg:ty),+) -> $rust_ret:ty)? { - $( - $(#[$fn_meta:meta])* // applied to the test - $name:ident; - )* - }; - )* - ) => { - // The user macro can have an `@all_items` pattern where it gets a list of the functions - $call_this! { - @all_items - fn_names: [$($( $name ),*),*] - } - - $( - // Invoke the user macro once for each signature type. - $call_this! { - @each_signature - // The input type, represented as a tuple. E.g. `(f32, f32)` for a - // `fn(f32, f32) -> f32` signature. - SysArgsTupleTy: ($($sys_arg),+ ,), - // The tuple type to call the Rust function. So if the system signature is - // `fn(f32, &mut f32) -> f32`, this type will only be `(f32, )`. - RustArgsTupleTy: $crate::for_each_function!( - @coalesce [($($sys_arg),+ ,)] $( [($($rust_arg),+ ,)] )? - ), - // A function signature type for the system function. - SysFnTy: fn($($sys_arg),+) -> $sys_ret, - // A function signature type for the Rust function. - RustFnTy: $crate::for_each_function!( - @coalesce [fn($($sys_arg),+) -> $sys_ret] $([fn($($rust_arg),+) -> $rust_ret])? - ), - // The list of all functions that have this signature. - functions: [$( { - attrs: [$($fn_meta),*], - fn_name: $name, - } ),*], - } - )* - }; - - // Macro helper to return the second item if two are provided, otherwise a default - (@coalesce [$($tt1:tt)*]) => { $($tt1)* } ; - (@coalesce [$($tt1:tt)*] [$($tt2:tt)*]) => { $($tt2)* } ; -} From 7a0cda427a755d414059cf994137f5bd76421ee0 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 16:50:46 -0500 Subject: [PATCH 19/25] Fix skipped tests --- crates/libm-test/tests/compare_built_musl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/libm-test/tests/compare_built_musl.rs b/crates/libm-test/tests/compare_built_musl.rs index 6d4a503f4..221bceefd 100644 --- a/crates/libm-test/tests/compare_built_musl.rs +++ b/crates/libm-test/tests/compare_built_musl.rs @@ -145,7 +145,7 @@ libm_macros::for_each_function! { skip: [], attributes: [ #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 - [exp10f, exp2f] + [exp10, exp10f, exp2, exp2f] ], extra: [], } From bd59b179536247220cc0d7572392c859330160e4 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 16:56:15 -0500 Subject: [PATCH 20/25] Fix test maybe --- crates/libm-macros/tests/basic.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/libm-macros/tests/basic.rs b/crates/libm-macros/tests/basic.rs index 1254bff3c..e23aeeb56 100644 --- a/crates/libm-macros/tests/basic.rs +++ b/crates/libm-macros/tests/basic.rs @@ -1,3 +1,6 @@ +// `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, From 968ab1f06891b55d81a084a49ed15c53224cab38 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 17:17:11 -0500 Subject: [PATCH 21/25] Disable more for mingw on ci --- ci/run.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci/run.sh b/ci/run.sh index b94572eeb..62eb1ff2b 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -21,6 +21,9 @@ case "$target" in *msvc*) exclude_flag="--exclude musl-math-sys" ;; *wasm*) exclude_flag="--exclude musl-math-sys" ;; *thumb*) exclude_flag="--exclude musl-math-sys" ;; + # `STATUS_DLL_NOT_FOUND` on CI for some reason + # + *windows-gnu) exclude_flags="--exclude libm-macros" ;; *) exclude_flag="" ;; esac From 0ca00436b75465d4b46e458f5636dfd9f4b6ce98 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 17:19:29 -0500 Subject: [PATCH 22/25] Fix shell --- ci/run.sh | 2 +- crates/libm-macros/src/lib.rs | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/ci/run.sh b/ci/run.sh index 62eb1ff2b..2a07a8239 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -23,7 +23,7 @@ case "$target" in *thumb*) exclude_flag="--exclude musl-math-sys" ;; # `STATUS_DLL_NOT_FOUND` on CI for some reason # - *windows-gnu) exclude_flags="--exclude libm-macros" ;; + *windows-gnu) exclude_flag="--exclude libm-macros" ;; *) exclude_flag="" ;; esac diff --git a/crates/libm-macros/src/lib.rs b/crates/libm-macros/src/lib.rs index 961408591..43f95c471 100644 --- a/crates/libm-macros/src/lib.rs +++ b/crates/libm-macros/src/lib.rs @@ -331,18 +331,36 @@ static ALL_FUNCTIONS_FLAT: LazyLock> = LazyLock::new(|| { Invoke as: + +``` +macro_rules! foo { + ( + fn_name: $fn_name:ident, + extra: [], + CFn: $CFn:ty, + CArgs: $CArgs:ty, + CRet: $CRet:ty, + RustFn: $RustFn:ty, + RustArgs: $RustArgs:ty, + RustRet: $RustRet:ty, + attrs: [$($meta:meta)*] + ) => { }; +} + for_each_function! { - callback: some_macro, - skip: [foo, bar], + callback: foo, + skip: [sin, cos], attributes: [ #[meta1] #[meta2] - [baz, qux], + [sinf], ] } +``` */ +/// #[proc_macro] pub fn for_each_function(tokens: pm::TokenStream) -> pm::TokenStream { let input = syn::parse_macro_input!(tokens as Invocation); From 8de0799710a125e48a92f943f48b510e39cd814f Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 17:55:21 -0500 Subject: [PATCH 23/25] Clean up docs --- ci/run.sh | 2 +- crates/libm-macros/src/lib.rs | 146 +++++++++++-------- crates/libm-macros/src/parse.rs | 49 ++++--- crates/libm-macros/tests/basic.rs | 56 +++++-- crates/libm-test/tests/check_coverage.rs | 10 +- crates/libm-test/tests/compare_built_musl.rs | 2 - 6 files changed, 165 insertions(+), 100 deletions(-) diff --git a/ci/run.sh b/ci/run.sh index 2a07a8239..c13d3bdee 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -21,7 +21,7 @@ case "$target" in *msvc*) exclude_flag="--exclude musl-math-sys" ;; *wasm*) exclude_flag="--exclude musl-math-sys" ;; *thumb*) exclude_flag="--exclude musl-math-sys" ;; - # `STATUS_DLL_NOT_FOUND` on CI for some reason + # FIXME: `STATUS_DLL_NOT_FOUND` on CI for some reason # *windows-gnu) exclude_flag="--exclude libm-macros" ;; *) exclude_flag="" ;; diff --git a/crates/libm-macros/src/lib.rs b/crates/libm-macros/src/lib.rs index 43f95c471..81e1e942e 100644 --- a/crates/libm-macros/src/lib.rs +++ b/crates/libm-macros/src/lib.rs @@ -1,21 +1,12 @@ -#![allow(unused)] - mod parse; use parse::{Invocation, StructuredInput}; -use std::{collections::BTreeMap, sync::LazyLock}; +use std::sync::LazyLock; use proc_macro as pm; use proc_macro2::{self as pm2, Span}; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - bracketed, - parse::{Parse, ParseStream, Parser}, - punctuated::Punctuated, - spanned::Spanned, - token::Comma, - Attribute, Expr, ExprArray, ExprPath, Ident, Meta, PatPath, Path, Token, -}; +use quote::{quote, ToTokens}; +use syn::Ident; const ALL_FUNCTIONS: &[(Signature, Option, &[&str])] = &[ ( @@ -257,6 +248,7 @@ const ALL_FUNCTIONS: &[(Signature, Option, &[&str])] = &[ ]; /// A type used in a function signature. +#[allow(dead_code)] #[derive(Debug, Clone, Copy)] enum Ty { F16, @@ -327,40 +319,62 @@ static ALL_FUNCTIONS_FLAT: LazyLock> = LazyLock::new(|| { ret }); -/* - -Invoke as: - - -``` -macro_rules! foo { - ( - fn_name: $fn_name:ident, - extra: [], - CFn: $CFn:ty, - CArgs: $CArgs:ty, - CRet: $CRet:ty, - RustFn: $RustFn:ty, - RustArgs: $RustArgs:ty, - RustRet: $RustRet:ty, - attrs: [$($meta:meta)*] - ) => { }; -} - -for_each_function! { - callback: foo, - skip: [sin, cos], - attributes: [ - #[meta1] - #[meta2] - [sinf], - ] -} -``` - -*/ - -/// +/// 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. +/// +/// 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], +/// ) => { }; +/// } +/// +/// 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] +/// } +/// ``` #[proc_macro] pub fn for_each_function(tokens: pm::TokenStream) -> pm::TokenStream { let input = syn::parse_macro_input!(tokens as Invocation); @@ -376,13 +390,14 @@ pub fn for_each_function(tokens: pm::TokenStream) -> pm::TokenStream { expand(structured).into() } +/// Check for any input that is structurally correct but has other problems. fn validate(input: &StructuredInput) -> Option { - let mentioned_functions = input.skip.iter().chain( - input - .attributes - .iter() - .flat_map(|attr_map| attr_map.names.iter()), - ); + let attr_mentions = input + .attributes + .iter() + .flat_map(|map_list| map_list.iter()) + .flat_map(|attr_map| attr_map.names.iter()); + let mentioned_functions = input.skip.iter().chain(attr_mentions); for mentioned in mentioned_functions { if !ALL_FUNCTIONS_FLAT.iter().any(|func| mentioned == func.name) { @@ -400,7 +415,11 @@ fn validate(input: &StructuredInput) -> Option { fn expand(input: StructuredInput) -> pm2::TokenStream { let mut out = pm2::TokenStream::new(); let callback = input.callback; - let extra = input.extra; + + let extra_field = match input.extra { + Some(extra) => quote! { extra: #extra, }, + None => pm2::TokenStream::new(), + }; for func in ALL_FUNCTIONS_FLAT.iter() { let fn_name = Ident::new(func.name, Span::call_site()); @@ -410,11 +429,16 @@ fn expand(input: StructuredInput) -> pm2::TokenStream { continue; } - let mut meta = input - .attributes - .iter() - .filter(|map| map.names.contains(&fn_name)) - .flat_map(|map| &map.meta); + 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(), + }; let c_args = func.c_sig.args; let c_ret = func.c_sig.returns; @@ -424,16 +448,14 @@ fn expand(input: StructuredInput) -> pm2::TokenStream { let new = quote! { #callback! { fn_name: #fn_name, - extra: #extra, CFn: fn( #(#c_args),* ,) -> ( #(#c_ret),* ), CArgs: ( #(#c_args),* ,), CRet: ( #(#c_ret),* ), RustFn: fn( #(#rust_args),* ,) -> ( #(#rust_ret),* ), RustArgs: ( #(#rust_args),* ,), RustRet: ( #(#rust_ret),* ), - attrs: [ - #( #meta )* - ] + #meta_field + #extra_field } }; diff --git a/crates/libm-macros/src/parse.rs b/crates/libm-macros/src/parse.rs index 565ebd5b5..58228af0b 100644 --- a/crates/libm-macros/src/parse.rs +++ b/crates/libm-macros/src/parse.rs @@ -1,15 +1,14 @@ -use proc_macro as pm; -use proc_macro2::{self as pm2, Span}; +use proc_macro2::Span; use quote::ToTokens; use syn::{ bracketed, - parse::{self, Parse, ParseStream, Parser}, + parse::{Parse, ParseStream, Parser}, punctuated::Punctuated, - spanned::Spanned, token::Comma, - Attribute, Expr, ExprArray, ExprPath, Ident, Meta, PatPath, Path, Token, + Attribute, Expr, Ident, Meta, Token, }; +/// The input to our macro; just a list of `field: value` items. #[derive(Debug)] pub struct Invocation { fields: Punctuated, @@ -27,7 +26,7 @@ impl Parse for Invocation { #[derive(Debug)] struct Mapping { name: Ident, - sep: Token![:], + _sep: Token![:], expr: Expr, } @@ -35,28 +34,28 @@ impl Parse for Mapping { fn parse(input: ParseStream) -> syn::Result { Ok(Self { name: input.parse()?, - sep: input.parse()?, + _sep: input.parse()?, expr: input.parse()?, }) } } -/// The input provided to our proc macro. +/// The input provided to our proc macro, after parsing into the form we expect. #[derive(Debug)] pub struct StructuredInput { pub callback: Ident, pub skip: Vec, - pub attributes: Vec, - pub extra: Expr, + pub attributes: Option>, + pub extra: 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")?; - let attr_expr = expect_field(&mut map, "attributes")?; - let extra = expect_field(&mut map, "extra")?; + let skip_expr = expect_field(&mut map, "skip").ok(); + let attr_expr = expect_field(&mut map, "attributes").ok(); + let extra = expect_field(&mut map, "extra").ok(); if !map.is_empty() { Err(syn::Error::new( @@ -65,13 +64,23 @@ impl StructuredInput { ))? } - let skip = Parser::parse2(parse_ident_array, skip_expr.into_token_stream())?; - let attr_exprs = Parser::parse2(parse_expr_array, attr_expr.into_token_stream())?; - let mut attributes = Vec::new(); - - for attr in attr_exprs { - attributes.push(syn::parse2(attr.into_token_stream())?); - } + let skip = match skip_expr { + Some(expr) => Parser::parse2(parse_ident_array, expr.into_token_stream())?, + None => Vec::new(), + }; + + 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, + }; Ok(Self { callback: expect_ident(cb_expr)?, diff --git a/crates/libm-macros/tests/basic.rs b/crates/libm-macros/tests/basic.rs index e23aeeb56..911a1ea8a 100644 --- a/crates/libm-macros/tests/basic.rs +++ b/crates/libm-macros/tests/basic.rs @@ -4,7 +4,6 @@ macro_rules! basic { ( fn_name: $fn_name:ident, - extra: [$($extra_tt:tt)*], CFn: $CFn:ty, CArgs: $CArgs:ty, CRet: $CRet:ty, @@ -12,6 +11,7 @@ macro_rules! basic { RustArgs: $RustArgs:ty, RustRet: $RustRet:ty, attrs: [$($meta:meta)*] + extra: [$($extra_tt:tt)*], ) => { $(#[$meta])* @@ -32,14 +32,48 @@ macro_rules! 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"], +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"], + } +} + +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, + } } diff --git a/crates/libm-test/tests/check_coverage.rs b/crates/libm-test/tests/check_coverage.rs index e84105d1a..ef6d21fdb 100644 --- a/crates/libm-test/tests/check_coverage.rs +++ b/crates/libm-test/tests/check_coverage.rs @@ -22,9 +22,13 @@ const ALLOWED_SKIPS: &[&str] = &[ 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], - $($_rest:tt)* - ) => { $push_to.push(stringify!($name)); }; @@ -37,8 +41,6 @@ fn test_for_each_function_all_included() { libm_macros::for_each_function! { callback: callback, - skip: [], - attributes: [], extra: [included], }; diff --git a/crates/libm-test/tests/compare_built_musl.rs b/crates/libm-test/tests/compare_built_musl.rs index 221bceefd..e2819f55b 100644 --- a/crates/libm-test/tests/compare_built_musl.rs +++ b/crates/libm-test/tests/compare_built_musl.rs @@ -105,7 +105,6 @@ fn make_test_cases(ntests: usize) -> CachedInput { macro_rules! musl_rand_tests { ( fn_name: $fn_name:ident, - extra: [], CFn: $CFn:ty, CArgs: $CArgs:ty, CRet: $CRet:ty, @@ -147,5 +146,4 @@ libm_macros::for_each_function! { #[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586 [exp10, exp10f, exp2, exp2f] ], - extra: [], } From 73d18f57a79dcfa0d4e071fda44de217210b593b Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 19:40:57 -0500 Subject: [PATCH 24/25] rewrite visitor --- crates/libm-macros/Cargo.toml | 2 +- crates/libm-macros/src/lib.rs | 168 ++++++++++++++++++++++++++---- crates/libm-macros/src/parse.rs | 87 +++++++++++++++- crates/libm-macros/tests/basic.rs | 8 +- 4 files changed, 240 insertions(+), 25 deletions(-) diff --git a/crates/libm-macros/Cargo.toml b/crates/libm-macros/Cargo.toml index 5859df214..9d2b08e2d 100644 --- a/crates/libm-macros/Cargo.toml +++ b/crates/libm-macros/Cargo.toml @@ -9,4 +9,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.88" quote = "1.0.37" -syn = { version = "2.0.79", features = ["full", "extra-traits"] } +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 index 81e1e942e..985c0a799 100644 --- a/crates/libm-macros/src/lib.rs +++ b/crates/libm-macros/src/lib.rs @@ -6,7 +6,7 @@ use std::sync::LazyLock; use proc_macro as pm; use proc_macro2::{self as pm2, Span}; use quote::{quote, ToTokens}; -use syn::Ident; +use syn::{visit_mut::VisitMut, Ident}; const ALL_FUNCTIONS: &[(Signature, Option, &[&str])] = &[ ( @@ -349,6 +349,8 @@ static ALL_FUNCTIONS_FLAT: LazyLock> = LazyLock::new(|| { /// attrs: [$($meta:meta)*] /// // Extra tokens passed directly (if any) /// extra: [$extra:ident], +/// // Extra function-tokens passed directly (if any) +/// fn_extra: $fn_extra:expr, /// ) => { }; /// } /// @@ -372,55 +374,92 @@ static ALL_FUNCTIONS_FLAT: LazyLock> = LazyLock::new(|| { /// // be used to pass local variables or other things the macro needs access to. /// // /// // This is an optional field. -/// extra: [foo] +/// 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 structured = match StructuredInput::from_fields(input) { - Ok(v) => v, - Err(e) => return e.into_compile_error().into(), - }; - if let Some(e) = validate(&structured) { - return e.into_compile_error().into(); - } + let res = StructuredInput::from_fields(input) + .and_then(|v| { + validate(&v)?; + Ok(v) + }) + .and_then(|v| expand(v)); - expand(structured).into() + 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. -fn validate(input: &StructuredInput) -> Option { +fn validate(input: &StructuredInput) -> syn::Result<()> { let attr_mentions = input .attributes .iter() .flat_map(|map_list| map_list.iter()) .flat_map(|attr_map| attr_map.names.iter()); - let mentioned_functions = input.skip.iter().chain(attr_mentions); + let fn_extra_mentions = input + .fn_extra + .iter() + .flat_map(|v| v.keys()) + .filter(|name| *name != "_"); + let mentioned_fns = input + .skip + .iter() + .chain(attr_mentions) + .chain(fn_extra_mentions); - for mentioned in mentioned_functions { + // Make sure that every function mentioned is a real function + for mentioned in 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 Some(e); + return Err(e); } } - None + if let Some(map) = &input.fn_extra { + // If no default is provided, make sure + if !map.keys().any(|key| key == "_") { + let mut fns_not_covered = Vec::new(); + for name in ALL_FUNCTIONS_FLAT.iter().map(|func| func.name) { + if map.keys().any(|key| key == name) { + fns_not_covered.push(name); + } + } + + 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 expand(input: StructuredInput) -> pm2::TokenStream { +fn expand(input: StructuredInput) -> syn::Result { let mut out = pm2::TokenStream::new(); + let default_ident = Ident::new("_", Span::call_site()); let callback = input.callback; - let extra_field = match input.extra { - Some(extra) => quote! { extra: #extra, }, - None => pm2::TokenStream::new(), - }; - for func in ALL_FUNCTIONS_FLAT.iter() { let fn_name = Ident::new(func.name, Span::call_site()); @@ -440,6 +479,34 @@ fn expand(input: StructuredInput) -> pm2::TokenStream { None => pm2::TokenStream::new(), }; + 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(), + }; + + 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; @@ -456,11 +523,68 @@ fn expand(input: StructuredInput) -> pm2::TokenStream { RustRet: ( #(#rust_ret),* ), #meta_field #extra_field + #fn_extra_field } }; out.extend(new); } - out + 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 { + let norm_name = name + .strip_suffix("f") + .ok_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(), + "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 index 58228af0b..75d93ba85 100644 --- a/crates/libm-macros/src/parse.rs +++ b/crates/libm-macros/src/parse.rs @@ -1,11 +1,14 @@ +use std::collections::BTreeMap; + use proc_macro2::Span; use quote::ToTokens; use syn::{ bracketed, parse::{Parse, ParseStream, Parser}, punctuated::Punctuated, + spanned::Spanned, token::Comma, - Attribute, Expr, Ident, Meta, Token, + Arm, Attribute, Expr, ExprMatch, Ident, Meta, Token, }; /// The input to our macro; just a list of `field: value` items. @@ -47,6 +50,9 @@ pub struct StructuredInput { pub skip: Vec, pub attributes: Option>, pub extra: Option, + pub fn_extra: Option>, + // For diagnostics + pub fn_extra_span: Option, } impl StructuredInput { @@ -56,6 +62,7 @@ impl StructuredInput { let skip_expr = expect_field(&mut map, "skip").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( @@ -82,15 +89,93 @@ impl StructuredInput { 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, 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_array, 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() { + let e = syn::Error::new( + attrs.first().unwrap().span(), + "no attributes allowed in this position", + ); + Err(e) + } else { + Ok(()) + } +} + /// 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(|| { diff --git a/crates/libm-macros/tests/basic.rs b/crates/libm-macros/tests/basic.rs index 911a1ea8a..15abdece7 100644 --- a/crates/libm-macros/tests/basic.rs +++ b/crates/libm-macros/tests/basic.rs @@ -12,7 +12,7 @@ macro_rules! basic { RustRet: $RustRet:ty, attrs: [$($meta:meta)*] extra: [$($extra_tt:tt)*], - + fn_extra: $fn_extra:expr, ) => { $(#[$meta])* mod $fn_name { @@ -28,6 +28,8 @@ macro_rules! basic { type RustRetTy = $RustRet; #[allow(unused)] const A: &[&str] = &[$($extra_tt)*]; + #[allow(unused)] + const B: u32 = $fn_extra; } }; } @@ -43,6 +45,10 @@ mod test_basic { [sinf, cosf] ], extra: ["foo", "bar"], + fn_extra: match MACRO_FN_NAME { + [sin] => 2 + 2, + _ => 100 + } } } From 07e5a9cd951af8ccf2169b5446cddf49ff78fa65 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 18 Oct 2024 19:51:26 -0500 Subject: [PATCH 25/25] proc updates --- crates/libm-macros/src/lib.rs | 43 ++++++++++++++++++------------- crates/libm-macros/src/parse.rs | 18 ++++++------- crates/libm-macros/tests/basic.rs | 9 ++++--- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/crates/libm-macros/src/lib.rs b/crates/libm-macros/src/lib.rs index 985c0a799..a3a457058 100644 --- a/crates/libm-macros/src/lib.rs +++ b/crates/libm-macros/src/lib.rs @@ -268,17 +268,17 @@ enum Ty { 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::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 }, }; @@ -325,6 +325,12 @@ static ALL_FUNCTIONS_FLAT: LazyLock> = LazyLock::new(|| { /// 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: /// /// ``` @@ -388,11 +394,8 @@ 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(|v| { - validate(&v)?; - Ok(v) - }) - .and_then(|v| expand(v)); + .and_then(|v| validate(&v).map(|_| v)) + .and_then(expand); match res { Ok(ts) => ts.into(), @@ -455,6 +458,7 @@ fn validate(input: &StructuredInput) -> syn::Result<()> { Ok(()) } +/// Expand our structured macro input into invocations of the callback macro. fn expand(input: StructuredInput) -> syn::Result { let mut out = pm2::TokenStream::new(); let default_ident = Ident::new("_", Span::call_site()); @@ -468,6 +472,7 @@ fn expand(input: StructuredInput) -> syn::Result { continue; } + // Prepare attributes in an `attrs: ...` field let meta_field = match &input.attributes { Some(attrs) => { let meta = attrs @@ -479,6 +484,7 @@ fn expand(input: StructuredInput) -> syn::Result { 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); @@ -490,6 +496,7 @@ fn expand(input: StructuredInput) -> syn::Result { 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 @@ -570,13 +577,13 @@ impl MacroReplace { } match s.as_str() { - "MACRO_FN_NAME" => *i = Ident::new(&self.fn_name, i.span()), + "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(), "unrecognized meta expression `{s}`", - )) + )); } } } diff --git a/crates/libm-macros/src/parse.rs b/crates/libm-macros/src/parse.rs index 75d93ba85..f2bcb2b8d 100644 --- a/crates/libm-macros/src/parse.rs +++ b/crates/libm-macros/src/parse.rs @@ -68,7 +68,7 @@ impl StructuredInput { Err(syn::Error::new( map.first().unwrap().name.span(), format!("unexpected fields {map:?}"), - ))? + ))?; } let skip = match skip_expr { @@ -165,15 +165,15 @@ fn extract_fn_extra_field(expr: Expr) -> syn::Result> { } fn expect_empty_attrs(attrs: &[Attribute]) -> syn::Result<()> { - if !attrs.is_empty() { - let e = syn::Error::new( - attrs.first().unwrap().span(), - "no attributes allowed in this position", - ); - Err(e) - } else { - Ok(()) + 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. diff --git a/crates/libm-macros/tests/basic.rs b/crates/libm-macros/tests/basic.rs index 15abdece7..27b3d921a 100644 --- a/crates/libm-macros/tests/basic.rs +++ b/crates/libm-macros/tests/basic.rs @@ -29,7 +29,9 @@ macro_rules! basic { #[allow(unused)] const A: &[&str] = &[$($extra_tt)*]; #[allow(unused)] - const B: u32 = $fn_extra; + fn foo(a: f32) -> f32 { + $fn_extra(a) + } } }; } @@ -46,8 +48,9 @@ mod test_basic { ], extra: ["foo", "bar"], fn_extra: match MACRO_FN_NAME { - [sin] => 2 + 2, - _ => 100 + [sin] => |x| x + 2.0, + [cos, cosf] => |x: f32| x.MACRO_FN_NAME_NORMALIZED(), + _ => |_x| 100.0 } } }