Skip to content

Commit 5cb12de

Browse files
committed
libm
1 parent 2f693d9 commit 5cb12de

File tree

8 files changed

+250
-82
lines changed

8 files changed

+250
-82
lines changed

Cargo.toml

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

66
[features]
77
# Turning on this feature on aarch64-apple-darwin helps bit representation compatibility
88
# See also: https://github.com/python/cpython/issues/132763
99
mul_add = []
1010

11+
[dependencies]
12+
errno = "0.3"
13+
libc = "0.2"
14+
1115
[dev-dependencies]
1216
proptest = "1.6.0"
1317
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

+10-78
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ const GAMMA_INTEGRAL: [f64; NGAMMA_INTEGRAL] = [
117117
1124000727777607680000.0,
118118
];
119119

120-
pub fn tgamma(x: f64) -> Result<f64, Error> {
120+
// tgamma
121+
pub fn gamma(x: f64) -> crate::Result<f64> {
121122
// special cases
122123
if !x.is_finite() {
123124
if x.is_nan() || x > 0.0 {
@@ -213,7 +214,7 @@ pub fn tgamma(x: f64) -> Result<f64, Error> {
213214

214215
// natural log of the absolute value of the Gamma function.
215216
// For large arguments, Lanczos' formula works extremely well here.
216-
pub fn lgamma(x: f64) -> Result<f64, Error> {
217+
pub fn lgamma(x: f64) -> crate::Result<f64> {
217218
// special cases
218219
if !x.is_finite() {
219220
if x.is_nan() {
@@ -258,79 +259,10 @@ pub fn lgamma(x: f64) -> Result<f64, Error> {
258259
Ok(r)
259260
}
260261

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

src/lib.rs

+117-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,120 @@
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::{gamma, lgamma};
9+
10+
macro_rules! libm {
11+
// Reset errno and handle errno when return type contains Result
12+
(fn $name:ident($arg:ident: $ty:ty) -> Result<$ret:ty>) => {
13+
#[inline(always)]
14+
pub fn $name($arg: $ty) -> Result<$ret> {
15+
errno::set_errno(errno::Errno(0));
16+
let r = unsafe { m::$name($arg) };
17+
crate::is_error(r)
18+
}
19+
};
20+
// Skip errno checking when return type is not Result
21+
(fn $name:ident($arg:ident: $ty:ty) -> $ret:ty) => {
22+
#[inline(always)]
23+
pub fn $name($arg: $ty) -> $ret {
24+
unsafe { m::$name($arg) }
25+
}
26+
};
27+
}
28+
29+
macro_rules! pyo3_proptest {
30+
($fn_name:ident(Result<_>), $test_name:ident, $proptest_name:ident, $edgetest_name:ident) => {
31+
#[cfg(test)]
32+
fn $test_name(x: f64) {
33+
use pyo3::prelude::*;
34+
35+
let rs_result = $fn_name(x);
36+
37+
pyo3::prepare_freethreaded_python();
38+
Python::with_gil(|py| {
39+
let math = PyModule::import(py, "math").unwrap();
40+
let py_func = math
41+
.getattr(stringify!($fn_name))
42+
.unwrap();
43+
let r = py_func.call1((x,));
44+
let Some((py_result, rs_result)) = crate::test::unwrap(py, r, rs_result) else {
45+
return;
46+
};
47+
let py_result_repr = py_result.to_bits();
48+
let rs_result_repr = rs_result.to_bits();
49+
assert_eq!(py_result_repr, rs_result_repr, "x = {x}, py_result = {py_result}, rs_result = {rs_result}");
50+
});
51+
}
52+
53+
crate::pyo3_proptest!(@proptest, $test_name, $proptest_name);
54+
crate::pyo3_proptest!(@edgetest, $test_name, $edgetest_name);
55+
};
56+
($fn_name:ident(_), $test_name:ident, $proptest_name:ident, $edgetest_name:ident) => {
57+
#[cfg(test)]
58+
fn $test_name(x: f64) {
59+
use pyo3::prelude::*;
60+
61+
let rs_result = Ok($fn_name(x));
62+
63+
pyo3::prepare_freethreaded_python();
64+
Python::with_gil(|py| {
65+
let math = PyModule::import(py, "math").unwrap();
66+
let py_func = math
67+
.getattr(stringify!($fn_name))
68+
.unwrap();
69+
let r = py_func.call1((x,));
70+
let Some((py_result, rs_result)) = crate::test::unwrap(py, r, rs_result) else {
71+
return;
72+
};
73+
let py_result_repr = py_result.to_bits();
74+
let rs_result_repr = rs_result.to_bits();
75+
assert_eq!(py_result_repr, rs_result_repr, "x = {x}, py_result = {py_result}, rs_result = {rs_result}");
76+
});
77+
}
78+
crate::pyo3_proptest!(@proptest, $test_name, $proptest_name);
79+
};
80+
(@proptest, $test_name:ident, $proptest_name:ident) => {
81+
#[cfg(test)]
82+
proptest::proptest! {
83+
#[test]
84+
fn $proptest_name(x: f64) {
85+
$test_name(x);
86+
}
87+
}
88+
};
89+
(@edgetest, $test_name:ident, $edgetest_name:ident) => {
90+
#[test]
91+
fn $edgetest_name() {
92+
$test_name(f64::MIN);
93+
$test_name(-f64::MIN);
94+
$test_name(f64::NAN);
95+
$test_name(-f64::NAN);
96+
$test_name(f64::INFINITY);
97+
$test_name(-f64::NEG_INFINITY);
98+
$test_name(0.0);
99+
$test_name(-0.0);
100+
}
101+
};
102+
}
103+
104+
libm!(fn erf(n: f64) -> f64);
105+
pyo3_proptest!(erf(_), test_erf, proptest_erf, edgetest_erf);
106+
107+
libm!(fn erfc(n: f64) -> f64);
108+
pyo3_proptest!(erfc(_), test_erfc, proptest_erfc, edgetest_erfc);
109+
110+
/// Call is_error when errno != 0, and where x is the result libm
111+
/// returned. is_error will usually set up an exception and return
112+
/// true (1), but may return false (0) without setting up an exception.
113+
// fn is_error(x: f64) -> crate::Result<f64> {
114+
// match errno::errno() {
115+
// errno::Errno(0) => Ok(x),
116+
// errno::Errno(libc::ERANGE) if x.abs() < 1.5 => Ok(0f64),
117+
// errno::Errno(errno) => Err(errno.try_into().unwrap()),
118+
// }
119+
// }
120+
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)