Add a utility crate for quick evaluation

Introduce a simple binary that can run arbitrary input against any of
the available implementations (musl, MPFR, our libm). This provides an
easy way to check results, or run specific cases against a debugger.

Examples:

    $ cargo run -p util -- eval libm pow 1.6 2.4
    3.089498284311124
    $ cargo run -p util -- eval mpfr pow 1.6 2.4
    3.089498284311124
    $ cargo run -p util -- eval musl tgamma 1.2344597839132
    0.9097442657960874
    $ cargo run -p util -- eval mpfr tgamma 1.2344597839132
    0.9097442657960874
    $ cargo run -p util -- eval libm tgamma 1.2344597839132
    0.9097442657960871
    $ cargo run -p util -- eval musl sincos 3.1415926535
    (8.979318433952318e-11, -1.0)
This commit is contained in:
Trevor Gross 2025-01-13 03:12:12 +00:00 committed by Trevor Gross
parent 4d973b6144
commit dfa694a8e4
5 changed files with 298 additions and 0 deletions

View file

@ -46,6 +46,7 @@ members = [
"crates/libm-macros",
"crates/libm-test",
"crates/musl-math-sys",
"crates/util",
]
default-members = [
".",

View file

@ -471,6 +471,32 @@ impl MpOp for crate::op::lgammaf_r::Routine {
}
}
/* stub implementations so we don't need to special case them */
impl MpOp for crate::op::nextafter::Routine {
type MpTy = MpFloat;
fn new_mp() -> Self::MpTy {
unimplemented!("nextafter does not yet have a MPFR operation");
}
fn run(_this: &mut Self::MpTy, _input: Self::RustArgs) -> Self::RustRet {
unimplemented!("nextafter does not yet have a MPFR operation");
}
}
impl MpOp for crate::op::nextafterf::Routine {
type MpTy = MpFloat;
fn new_mp() -> Self::MpTy {
unimplemented!("nextafter does not yet have a MPFR operation");
}
fn run(_this: &mut Self::MpTy, _input: Self::RustArgs) -> Self::RustRet {
unimplemented!("nextafter does not yet have a MPFR operation");
}
}
/// `rug` does not provide `remquo` so this exposes `mpfr_remquo`. See rug#76.
fn mpfr_remquo(r: &mut MpFloat, x: &MpFloat, y: &MpFloat, round: Round) -> (Ordering, c_long) {
let r = r.as_raw_mut();

View file

@ -0,0 +1,19 @@
[package]
name = "util"
version = "0.1.0"
edition = "2021"
publish = false
[features]
default = ["build-musl", "build-mpfr", "unstable-float"]
build-musl = ["libm-test/build-musl", "dep:musl-math-sys"]
build-mpfr = ["libm-test/build-mpfr", "dep:az", "dep:rug"]
unstable-float = ["libm/unstable-float", "libm-test/unstable-float", "rug?/nightly-float"]
[dependencies]
az = { version = "1.2.1", optional = true }
libm = { path = "../..", default-features = false }
libm-macros = { path = "../libm-macros" }
libm-test = { path = "../libm-test", default-features = false }
musl-math-sys = { path = "../musl-math-sys", optional = true }
rug = { version = "1.26.1", optional = true, default-features = false, features = ["float", "std"] }

View file

@ -0,0 +1,9 @@
#![allow(unexpected_cfgs)]
#[path = "../../configure.rs"]
mod configure;
fn main() {
let cfg = configure::Config::from_env();
configure::emit_libm_config(&cfg);
}

View file

@ -0,0 +1,243 @@
//! Helper CLI utility for common tasks.
#![cfg_attr(f16_enabled, feature(f16))]
#![cfg_attr(f128_enabled, feature(f128))]
use std::any::type_name;
use std::env;
use std::str::FromStr;
#[cfg(feature = "build-mpfr")]
use az::Az;
#[cfg(feature = "build-mpfr")]
use libm_test::mpfloat::MpOp;
use libm_test::{MathOp, TupleCall};
const USAGE: &str = "\
usage:
cargo run -p util -- <SUBCOMMAND>
SUBCOMMAND:
eval <BASIS> <OP> inputs...
Evaulate the expression with a given basis. This can be useful for
running routines with a debugger, or quickly checking input. Examples:
* eval musl sinf 1.234 # print the results of musl sinf(1.234f32)
* eval mpfr pow 1.234 2.432 # print the results of mpfr pow(1.234, 2.432)
";
fn main() {
let args = env::args().collect::<Vec<_>>();
let str_args = args.iter().map(|s| s.as_str()).collect::<Vec<_>>();
match &str_args.as_slice()[1..] {
["eval", basis, op, inputs @ ..] => do_eval(basis, op, inputs),
_ => {
println!("{USAGE}\nunrecognized input `{str_args:?}`");
std::process::exit(1);
}
}
}
macro_rules! handle_call {
(
fn_name: $fn_name:ident,
CFn: $CFn:ty,
RustFn: $RustFn:ty,
RustArgs: $RustArgs:ty,
attrs: [$($attr:meta),*],
extra: ($basis:ident, $op:ident, $inputs:ident),
fn_extra: $musl_fn:expr,
) => {
$(#[$attr])*
if $op == stringify!($fn_name) {
type Op = libm_test::op::$fn_name::Routine;
let input = <$RustArgs>::parse($inputs);
let libm_fn: <Op as MathOp>::RustFn = libm::$fn_name;
let output = match $basis {
"libm" => input.call(libm_fn),
#[cfg(feature = "build-musl")]
"musl" => {
let musl_fn: <Op as MathOp>::CFn =
$musl_fn.unwrap_or_else(|| panic!("no musl function for {}", $op));
input.call(musl_fn)
}
#[cfg(feature = "build-mpfr")]
"mpfr" => {
let mut mp = <Op as MpOp>::new_mp();
Op::run(&mut mp, input)
}
_ => panic!("unrecognized or disabled basis '{}'", $basis),
};
println!("{output:?}");
return;
}
};
}
/// Evaluate the specified operation with a given basis.
fn do_eval(basis: &str, op: &str, inputs: &[&str]) {
libm_macros::for_each_function! {
callback: handle_call,
emit_types: [CFn, RustFn, RustArgs],
extra: (basis, op, inputs),
fn_extra: match MACRO_FN_NAME {
copysignf16 | copysignf128 | fabsf16 | fabsf128 => None,
_ => Some(musl_math_sys::MACRO_FN_NAME)
}
}
panic!("no operation matching {op}");
}
/// Parse a tuple from a space-delimited string.
trait ParseTuple {
fn parse(input: &[&str]) -> Self;
}
macro_rules! impl_parse_tuple {
($ty:ty) => {
impl ParseTuple for ($ty,) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 1, "expected a single argument, got {input:?}");
(parse(input, 0),)
}
}
impl ParseTuple for ($ty, $ty) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
(parse(input, 0), parse(input, 1))
}
}
impl ParseTuple for ($ty, i32) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
(parse(input, 0), parse(input, 1))
}
}
impl ParseTuple for (i32, $ty) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
(parse(input, 0), parse(input, 1))
}
}
impl ParseTuple for ($ty, $ty, $ty) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 2, "expected three arguments, got {input:?}");
(parse(input, 0), parse(input, 1), parse(input, 3))
}
}
};
}
#[allow(unused_macros)]
#[cfg(feature = "build-mpfr")]
macro_rules! impl_parse_tuple_via_rug {
($ty:ty) => {
impl ParseTuple for ($ty,) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 1, "expected a single argument, got {input:?}");
(parse_rug(input, 0),)
}
}
impl ParseTuple for ($ty, $ty) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
(parse_rug(input, 0), parse_rug(input, 1))
}
}
impl ParseTuple for ($ty, i32) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
(parse_rug(input, 0), parse(input, 1))
}
}
impl ParseTuple for (i32, $ty) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
(parse(input, 0), parse_rug(input, 1))
}
}
impl ParseTuple for ($ty, $ty, $ty) {
fn parse(input: &[&str]) -> Self {
assert_eq!(input.len(), 2, "expected three arguments, got {input:?}");
(parse_rug(input, 0), parse_rug(input, 1), parse_rug(input, 3))
}
}
};
}
// Fallback for when Rug is not built.
#[allow(unused_macros)]
#[cfg(not(feature = "build-mpfr"))]
macro_rules! impl_parse_tuple_via_rug {
($ty:ty) => {
impl ParseTuple for ($ty,) {
fn parse(_input: &[&str]) -> Self {
panic!("parsing this type requires the `build-mpfr` feature")
}
}
impl ParseTuple for ($ty, $ty) {
fn parse(_input: &[&str]) -> Self {
panic!("parsing this type requires the `build-mpfr` feature")
}
}
impl ParseTuple for ($ty, i32) {
fn parse(_input: &[&str]) -> Self {
panic!("parsing this type requires the `build-mpfr` feature")
}
}
impl ParseTuple for (i32, $ty) {
fn parse(_input: &[&str]) -> Self {
panic!("parsing this type requires the `build-mpfr` feature")
}
}
impl ParseTuple for ($ty, $ty, $ty) {
fn parse(_input: &[&str]) -> Self {
panic!("parsing this type requires the `build-mpfr` feature")
}
}
};
}
impl_parse_tuple!(f32);
impl_parse_tuple!(f64);
#[cfg(f16_enabled)]
impl_parse_tuple_via_rug!(f16);
#[cfg(f128_enabled)]
impl_parse_tuple_via_rug!(f128);
/// Try to parse the number, printing a nice message on failure.
fn parse<F: FromStr>(input: &[&str], idx: usize) -> F {
let s = input[idx];
s.parse().unwrap_or_else(|_| panic!("invalid {} input '{s}'", type_name::<F>()))
}
/// Try to parse the float type going via `rug`, for `f16` and `f128` which don't yet implement
/// `FromStr`.
#[cfg(feature = "build-mpfr")]
fn parse_rug<F: libm_test::Float>(input: &[&str], idx: usize) -> F
where
rug::Float: az::Cast<F>,
{
let s = input[idx];
let x =
rug::Float::parse(s).unwrap_or_else(|_| panic!("invalid {} input '{s}'", type_name::<F>()));
let x = rug::Float::with_val(F::BITS, x);
x.az()
}