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