Skip to content

Commit a6e8313

Browse files
committed
libm
1 parent d991c6e commit a6e8313

File tree

8 files changed

+190
-82
lines changed

8 files changed

+190
-82
lines changed

Cargo.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
[package]
22
name = "pymath"
3-
version = "0.1.0"
3+
version = "0.0.1"
44
edition = "2024"
55

6+
[dependencies]
7+
libc = "0.2"
8+
69
[dev-dependencies]
710
proptest = "1.6.0"
811
pyo3 = { version = "0.24", features = ["abi3"] }

proptest-regressions/gamma.txt

+3
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ cc e8ed768221998086795d95c68921437e80c4b7fe68fe9da15ca40faa216391b5 # shrinks to
88
cc 23c7f86ab299daa966772921d8c615afda11e1b77944bed40e88264a68e62ac3 # shrinks to x = -19.80948467648103
99
cc f57954d91904549b9431755f196b630435a43cbefd558b932efad487a403c6c8 # shrinks to x = 0.003585187864492183
1010
cc 7a9a04aed4ed7e3d23eb7b32b748542b1062e349ae83cc1fad39672a5b2156cd # shrinks to x = -3.8510064710745118
11+
cc d884d4ef56bcd40d025660e0dec152754fd4fd4e48bc0bdf41e73ea001798fd8 # shrinks to x = 0.9882904125102558
12+
cc 3f1d36f364ce29810d0c37003465b186c07460861c7a3bf4b8962401b376f2d9 # shrinks to x = 1.402608516799205
13+
cc 4439ce674d91257d104063e2d5ade7908c83462d195f98a0c304ea25b022d0f4 # shrinks to x = 3.6215752811868267

proptest-regressions/lib.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 531a136f9fcde9d1da1ba5d173e62eee8ec8f7c877eb34abbc6d47611a641bc7 # shrinks to x = 0.0

src/err.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1-
// defined in libc
1+
// The values are defined in libc
22
#[derive(Debug, PartialEq, Eq)]
33
pub enum Error {
44
EDOM = 33,
55
ERANGE = 34,
66
}
7+
8+
pub type Result<T> = std::result::Result<T, Error>;
9+
10+
impl TryFrom<libc::c_int> for Error {
11+
type Error = libc::c_int;
12+
13+
fn try_from(value: libc::c_int) -> std::result::Result<Self, Self::Error> {
14+
match value {
15+
33 => Ok(Error::EDOM),
16+
34 => Ok(Error::ERANGE),
17+
_ => Err(value),
18+
}
19+
}
20+
}

src/gamma.rs

+5-78
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ const GAMMA_INTEGRAL: [f64; NGAMMA_INTEGRAL] = [
109109
1124000727777607680000.0,
110110
];
111111

112-
pub fn tgamma(x: f64) -> Result<f64, Error> {
112+
// tgamma
113+
pub fn gamma(x: f64) -> crate::Result<f64> {
113114
// special cases
114115
if !x.is_finite() {
115116
if x.is_nan() || x > 0.0 {
@@ -205,7 +206,7 @@ pub fn tgamma(x: f64) -> Result<f64, Error> {
205206

206207
// natural log of the absolute value of the Gamma function.
207208
// For large arguments, Lanczos' formula works extremely well here.
208-
pub fn lgamma(x: f64) -> Result<f64, Error> {
209+
pub fn lgamma(x: f64) -> crate::Result<f64> {
209210
// special cases
210211
if !x.is_finite() {
211212
if x.is_nan() {
@@ -249,79 +250,5 @@ pub fn lgamma(x: f64) -> Result<f64, Error> {
249250
Ok(r)
250251
}
251252

252-
#[cfg(test)]
253-
mod tests {
254-
use super::*;
255-
use pyo3::Python;
256-
use pyo3::prelude::*;
257-
258-
use proptest::prelude::*;
259-
260-
fn unwrap<'a, T: 'a>(
261-
py: Python,
262-
py_v: PyResult<Bound<'a, PyAny>>,
263-
v: Result<T, crate::Error>,
264-
) -> Option<(T, T)>
265-
where
266-
T: PartialEq + std::fmt::Debug + FromPyObject<'a>,
267-
{
268-
match py_v {
269-
Ok(py_v) => {
270-
let py_v: T = py_v.extract().unwrap();
271-
Some((py_v, v.unwrap()))
272-
}
273-
Err(e) => {
274-
if e.is_instance_of::<pyo3::exceptions::PyValueError>(py) {
275-
assert_eq!(v.err(), Some(Error::EDOM));
276-
} else if e.is_instance_of::<pyo3::exceptions::PyOverflowError>(py) {
277-
assert_eq!(v.err(), Some(Error::ERANGE));
278-
} else {
279-
panic!();
280-
}
281-
None
282-
}
283-
}
284-
}
285-
286-
proptest! {
287-
#[test]
288-
fn test_tgamma(x: f64) {
289-
let rs_gamma = tgamma(x);
290-
291-
pyo3::prepare_freethreaded_python();
292-
Python::with_gil(|py| {
293-
let math = PyModule::import(py, "math").unwrap();
294-
let py_gamma_func = math
295-
.getattr("gamma")
296-
.unwrap();
297-
let r = py_gamma_func.call1((x,));
298-
let Some((py_gamma, rs_gamma)) = unwrap(py, r, rs_gamma) else {
299-
return;
300-
};
301-
let py_gamma_repr = py_gamma.to_bits();
302-
let rs_gamma_repr = rs_gamma.to_bits();
303-
assert_eq!(py_gamma_repr, rs_gamma_repr, "x = {x}, py_gamma = {py_gamma}, rs_gamma = {rs_gamma}");
304-
});
305-
}
306-
307-
#[test]
308-
fn test_lgamma(x: f64) {
309-
let rs_lgamma = lgamma(x);
310-
311-
pyo3::prepare_freethreaded_python();
312-
Python::with_gil(|py| {
313-
let math = PyModule::import(py, "math").unwrap();
314-
let py_lgamma_func = math
315-
.getattr("lgamma")
316-
.unwrap();
317-
let r = py_lgamma_func.call1((x,));
318-
let Some((py_lgamma, rs_lgamma)) = unwrap(py, r, rs_lgamma) else {
319-
return;
320-
};
321-
let py_lgamma_repr = py_lgamma.to_bits();
322-
let rs_lgamma_repr = rs_lgamma.to_bits();
323-
assert_eq!(py_lgamma_repr, rs_lgamma_repr, "x = {x}, py_lgamma = {py_lgamma}, rs_gamma = {rs_lgamma}");
324-
});
325-
}
326-
}
327-
}
253+
super::pyo3_proptest!(gamma, test_gamma);
254+
super::pyo3_proptest!(lgamma, test_lgamma);

src/lib.rs

+63-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,66 @@
11
mod err;
22
mod gamma;
3+
mod m;
4+
#[cfg(test)]
5+
mod test;
36

4-
pub use err::Error;
5-
pub use gamma::{lgamma, tgamma as gamma};
7+
pub use err::{Error, Result};
8+
pub use gamma::{lgamma, gamma};
9+
10+
macro_rules! libm {
11+
(fn $name:ident($arg:ident: $ty:ty) -> $ret:ty) => {
12+
#[inline(always)]
13+
pub fn $name($arg: $ty) -> $ret {
14+
let r = unsafe { m::$name($arg) };
15+
crate::is_error(r)
16+
}
17+
};
18+
}
19+
20+
macro_rules! pyo3_proptest {
21+
($fn_name:ident, $test_name:ident) => {
22+
#[cfg(test)]
23+
proptest::proptest! {
24+
#[test]
25+
fn $test_name(x: f64) {
26+
use pyo3::prelude::*;
27+
28+
let rs_result = $fn_name(x);
29+
30+
pyo3::prepare_freethreaded_python();
31+
Python::with_gil(|py| {
32+
let math = PyModule::import(py, "math").unwrap();
33+
let py_func = math
34+
.getattr(stringify!($fn_name))
35+
.unwrap();
36+
let r = py_func.call1((x,));
37+
let Some((py_result, rs_result)) = crate::test::unwrap(py, r, rs_result) else {
38+
return;
39+
};
40+
let py_result_repr = py_result.to_bits();
41+
let rs_result_repr = rs_result.to_bits();
42+
assert_eq!(py_result_repr, rs_result_repr, "x = {x}, py_result = {py_result}, rs_result = {rs_result}");
43+
});
44+
}
45+
}
46+
}
47+
}
48+
49+
libm!(fn erf(n: f64) -> Result<f64>);
50+
pyo3_proptest!(erf, test_erf);
51+
52+
libm!(fn erfc(n: f64) -> Result<f64>);
53+
pyo3_proptest!(erfc, test_erfc);
54+
55+
/// Call is_error when errno != 0, and where x is the result libm
56+
/// returned. is_error will usually set up an exception and return
57+
/// true (1), but may return false (0) without setting up an exception.
58+
fn is_error(x: f64) -> crate::Result<f64> {
59+
match std::io::Error::last_os_error().raw_os_error() {
60+
None | Some(0) => Ok(x),
61+
Some(libc::ERANGE) if x.abs() < 1.5 => Ok(0f64),
62+
Some(errno) => Err(errno.try_into().unwrap()),
63+
}
64+
}
65+
66+
use pyo3_proptest;

src/m.rs

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Partial copy of std::sys::_cmath
2+
3+
// These symbols are all defined by `libm`,
4+
// or by `compiler-builtins` on unsupported platforms.
5+
#[allow(dead_code)]
6+
unsafe extern "C" {
7+
pub fn acos(n: f64) -> f64;
8+
pub fn asin(n: f64) -> f64;
9+
pub fn atan(n: f64) -> f64;
10+
pub fn atan2(a: f64, b: f64) -> f64;
11+
pub fn cbrt(n: f64) -> f64;
12+
pub fn cbrtf(n: f32) -> f32;
13+
pub fn cosh(n: f64) -> f64;
14+
pub fn expm1(n: f64) -> f64;
15+
pub fn expm1f(n: f32) -> f32;
16+
pub fn fdim(a: f64, b: f64) -> f64;
17+
pub fn fdimf(a: f32, b: f32) -> f32;
18+
#[cfg_attr(target_env = "msvc", link_name = "_hypot")]
19+
pub fn hypot(x: f64, y: f64) -> f64;
20+
#[cfg_attr(target_env = "msvc", link_name = "_hypotf")]
21+
pub fn hypotf(x: f32, y: f32) -> f32;
22+
pub fn log1p(n: f64) -> f64;
23+
pub fn log1pf(n: f32) -> f32;
24+
pub fn sinh(n: f64) -> f64;
25+
pub fn tan(n: f64) -> f64;
26+
pub fn tanh(n: f64) -> f64;
27+
pub fn tgamma(n: f64) -> f64;
28+
pub fn tgammaf(n: f32) -> f32;
29+
pub fn lgamma_r(n: f64, s: &mut i32) -> f64;
30+
#[cfg(not(target_os = "aix"))]
31+
pub fn lgammaf_r(n: f32, s: &mut i32) -> f32;
32+
pub fn erf(n: f64) -> f64;
33+
pub fn erff(n: f32) -> f32;
34+
pub fn erfc(n: f64) -> f64;
35+
pub fn erfcf(n: f32) -> f32;
36+
37+
// pub fn acosf128(n: f128) -> f128;
38+
// pub fn asinf128(n: f128) -> f128;
39+
// pub fn atanf128(n: f128) -> f128;
40+
// pub fn atan2f128(a: f128, b: f128) -> f128;
41+
// pub fn cbrtf128(n: f128) -> f128;
42+
// pub fn coshf128(n: f128) -> f128;
43+
// pub fn expm1f128(n: f128) -> f128;
44+
// pub fn hypotf128(x: f128, y: f128) -> f128;
45+
// pub fn log1pf128(n: f128) -> f128;
46+
// pub fn sinhf128(n: f128) -> f128;
47+
// pub fn tanf128(n: f128) -> f128;
48+
// pub fn tanhf128(n: f128) -> f128;
49+
// pub fn tgammaf128(n: f128) -> f128;
50+
// pub fn lgammaf128_r(n: f128, s: &mut i32) -> f128;
51+
// pub fn erff128(n: f128) -> f128;
52+
// pub fn erfcf128(n: f128) -> f128;
53+
54+
// cfg_if::cfg_if! {
55+
// if #[cfg(not(all(target_os = "windows", target_env = "msvc", target_arch = "x86")))] {
56+
// pub fn acosf(n: f32) -> f32;
57+
// pub fn asinf(n: f32) -> f32;
58+
// pub fn atan2f(a: f32, b: f32) -> f32;
59+
// pub fn atanf(n: f32) -> f32;
60+
// pub fn coshf(n: f32) -> f32;
61+
// pub fn sinhf(n: f32) -> f32;
62+
// pub fn tanf(n: f32) -> f32;
63+
// pub fn tanhf(n: f32) -> f32;
64+
// }}
65+
}

src/test.rs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use crate::Error;
2+
use pyo3::{Python, prelude::*};
3+
4+
pub(crate) fn unwrap<'a, T: 'a>(
5+
py: Python,
6+
py_v: PyResult<Bound<'a, PyAny>>,
7+
v: Result<T, crate::Error>,
8+
) -> Option<(T, T)>
9+
where
10+
T: PartialEq + std::fmt::Debug + FromPyObject<'a>,
11+
{
12+
match py_v {
13+
Ok(py_v) => {
14+
let py_v: T = py_v.extract().unwrap();
15+
Some((py_v, v.unwrap()))
16+
}
17+
Err(e) => {
18+
if e.is_instance_of::<pyo3::exceptions::PyValueError>(py) {
19+
assert_eq!(v.err(), Some(Error::EDOM));
20+
} else if e.is_instance_of::<pyo3::exceptions::PyOverflowError>(py) {
21+
assert_eq!(v.err(), Some(Error::ERANGE));
22+
} else {
23+
panic!();
24+
}
25+
None
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)