From 65f981c8d72cbe2a3ecdb269a7a0feb68be8913d Mon Sep 17 00:00:00 2001 From: youknowdot <336763669+youknowdot@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:21:22 +0900 Subject: [PATCH 1/3] Add Python 3.15 floating-point math APIs Match CPython's extrema through the existing libm backends and add bounded native edge tests. Constrain the concrete malachite BigInt API to the 0.13 series for review. Assisted-by: OpenAI Codex:model version unavailable --- Cargo.toml | 2 +- README.md | 7 +++++- src/m.rs | 10 ++++++++ src/m_rust.rs | 10 ++++++++ src/m_sys.rs | 2 ++ src/math.rs | 4 ++-- src/math/misc.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 91 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af7fa13..8d1d4f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ num-complex = { version = "0.4", optional = true } num-bigint = { version = "0.4", optional = true } num-traits = { version = "0.2", optional = true } num-integer = { version = "0.1", optional = true } -malachite-bigint = { version = "0", optional = true } # malachite upgrades minor version a lot +malachite-bigint = { version = "0.13", optional = true } [dev-dependencies] divan = { version = "5.0.1", package = "codspeed-divan-compat" } diff --git a/README.md b/README.md index 3d73794..ff12eae 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Every function produces identical results to Python at the binary representation ## Compatibility Status -### math (56/56) +### math (61/61) - [x] `acos` - [x] `acosh` @@ -38,6 +38,8 @@ Every function produces identical results to Python at the binary representation - [x] `fabs` - [x] `floor` - [x] `fma` +- [x] `fmax` +- [x] `fmin` - [x] `fmod` - [x] `frexp` - [x] `fsum` @@ -48,6 +50,8 @@ Every function produces identical results to Python at the binary representation - [x] `isfinite` - [x] `isinf` - [x] `isnan` +- [x] `isnormal` +- [x] `issubnormal` - [x] `ldexp` - [x] `lgamma` - [x] `log` @@ -63,6 +67,7 @@ Every function produces identical results to Python at the binary representation - [x] `radians` - [x] `remainder` - [x] `sin` +- [x] `signbit` - [x] `sinh` - [x] `sqrt` - [x] `sumprod` (`sumprod`, `sumprod_int`) diff --git a/src/m.rs b/src/m.rs index 259ef22..080d6c4 100644 --- a/src/m.rs +++ b/src/m.rs @@ -157,6 +157,16 @@ pub fn floor(n: f64) -> f64 { unsafe { m_sys::floor(n) } } +#[inline(always)] +pub fn fmax(x: f64, y: f64) -> f64 { + unsafe { m_sys::fmax(x, y) } +} + +#[inline(always)] +pub fn fmin(x: f64, y: f64) -> f64 { + unsafe { m_sys::fmin(x, y) } +} + #[inline(always)] pub fn fmod(x: f64, y: f64) -> f64 { unsafe { m_sys::fmod(x, y) } diff --git a/src/m_rust.rs b/src/m_rust.rs index fa9ba14..6c9ea7f 100644 --- a/src/m_rust.rs +++ b/src/m_rust.rs @@ -151,6 +151,16 @@ pub fn floor(n: f64) -> f64 { libm::floor(n) } +#[inline(always)] +pub fn fmax(x: f64, y: f64) -> f64 { + libm::fmax(x, y) +} + +#[inline(always)] +pub fn fmin(x: f64, y: f64) -> f64 { + libm::fmin(x, y) +} + #[inline(always)] pub fn fmod(x: f64, y: f64) -> f64 { libm::fmod(x, y) diff --git a/src/m_sys.rs b/src/m_sys.rs index 60406a8..1679ce8 100644 --- a/src/m_sys.rs +++ b/src/m_sys.rs @@ -48,6 +48,8 @@ unsafe extern "C" { // pub fn fdim(a: f64, b: f64) -> f64; pub fn fdimf(a: f32, b: f32) -> f32; pub fn floor(n: f64) -> f64; + pub fn fmax(x: f64, y: f64) -> f64; + pub fn fmin(x: f64, y: f64) -> f64; pub fn fmod(x: f64, y: f64) -> f64; pub fn frexp(n: f64, exp: *mut i32) -> f64; pub fn ldexp(x: f64, n: i32) -> f64; diff --git a/src/math.rs b/src/math.rs index 02e0a31..ae0f4bc 100644 --- a/src/math.rs +++ b/src/math.rs @@ -18,8 +18,8 @@ pub use bigint::{comb_bigint, ldexp_bigint, log_bigint, log2_bigint, log10_bigin pub use exponential::{cbrt, exp, exp2, expm1, log, log1p, log2, log10, pow, sqrt}; pub use gamma::{erf, erfc, gamma, lgamma}; pub use misc::{ - ceil, copysign, fabs, floor, fma, fmod, frexp, isclose, isfinite, isinf, isnan, ldexp, modf, - nextafter, remainder, trunc, ulp, + ceil, copysign, fabs, floor, fma, fmax, fmin, fmod, frexp, isclose, isfinite, isinf, isnan, + isnormal, issubnormal, ldexp, modf, nextafter, remainder, signbit, trunc, ulp, }; pub use trigonometric::{ acos, acosh, asin, asinh, atan, atan2, atanh, cos, cosh, sin, sinh, tan, tanh, diff --git a/src/math/misc.rs b/src/math/misc.rs index adbe40c..d976a37 100644 --- a/src/math/misc.rs +++ b/src/math/misc.rs @@ -3,6 +3,7 @@ use crate::{Error, Result, m}; super::libm_simple!(@1 ceil, floor, trunc); +super::libm_simple!(@2 fmax, fmin); /// Return the next floating-point value after x towards y. /// @@ -104,6 +105,24 @@ pub fn isnan(x: f64) -> bool { x.is_nan() } +/// Return True if x is normal, False otherwise. +#[inline] +pub fn isnormal(x: f64) -> bool { + x.is_normal() +} + +/// Return True if x is subnormal, False otherwise. +#[inline] +pub fn issubnormal(x: f64) -> bool { + x.is_subnormal() +} + +/// Return True if the sign of x is negative, False otherwise. +#[inline] +pub fn signbit(x: f64) -> bool { + x.is_sign_negative() +} + /// Return True if a and b are close to each other. /// /// Whether or not two values are considered close is determined according to @@ -259,6 +278,47 @@ pub fn ulp(x: f64) -> f64 { mod tests { use super::*; + #[test] + fn edgetest_python315_classification() { + for &x in crate::test::EDGE_VALUES { + let bits = x.to_bits(); + let exponent = (bits >> 52) & 0x7ff; + let fraction = bits & ((1_u64 << 52) - 1); + assert_eq!(isnormal(x), exponent != 0 && exponent != 0x7ff); + assert_eq!(issubnormal(x), exponent == 0 && fraction != 0); + assert_eq!(signbit(x), bits >> 63 != 0); + } + } + + #[test] + fn edgetest_python315_extrema() { + for &x in crate::test::EDGE_VALUES { + for &y in crate::test::EDGE_VALUES { + for (result, maximum) in [(fmax(x, y), true), (fmin(x, y), false)] { + if x.is_nan() && y.is_nan() { + assert!(result.is_nan()); + } else if x.is_nan() || y.is_nan() { + // Signaling NaNs can be quieted by the platform libm. + let signaling = |v: f64| v.is_nan() && v.to_bits() & (1_u64 << 51) == 0; + let expected = if x.is_nan() { y } else { x }; + if !signaling(x) && !signaling(y) { + assert_eq!(result.to_bits(), expected.to_bits()); + } else { + assert!(result.is_nan() || result.to_bits() == expected.to_bits()); + } + } else { + let expected = if (x > y) == maximum { x } else { y }; + // The sign of a zero tie is platform-dependent. + assert_eq!(result, expected); + if x != y { + assert_eq!(result.to_bits(), expected.to_bits()); + } + } + } + } + } + } + /// Edge integer values for testing functions like ldexp const EDGE_INTS: &[i32] = &[0, 1, -1, 100, -100, 1024, -1024, i32::MAX, i32::MIN]; From 0d664fd1494aedb7ce4fa253f7471075d3490160 Mon Sep 17 00:00:00 2001 From: youknowdot <336763669+youknowdot@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:47:18 +0900 Subject: [PATCH 2/3] Preserve upstream malachite dependency range Keep dependency alignment in RustPython instead of restricting pymath. Assisted-by: OpenAI Codex (model version unavailable) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8d1d4f6..af7fa13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ num-complex = { version = "0.4", optional = true } num-bigint = { version = "0.4", optional = true } num-traits = { version = "0.2", optional = true } num-integer = { version = "0.1", optional = true } -malachite-bigint = { version = "0.13", optional = true } +malachite-bigint = { version = "0", optional = true } # malachite upgrades minor version a lot [dev-dependencies] divan = { version = "5.0.1", package = "codspeed-divan-compat" } From 3a5f7f235f5da7b6852b65d2fc0c69ad7287d596 Mon Sep 17 00:00:00 2001 From: youknowdot <336763669+youknowdot@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:54:46 +0900 Subject: [PATCH 3/3] Keep extrema edge assertions observable under optimization --- src/math/misc.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/math/misc.rs b/src/math/misc.rs index d976a37..c855774 100644 --- a/src/math/misc.rs +++ b/src/math/misc.rs @@ -295,6 +295,10 @@ mod tests { for &x in crate::test::EDGE_VALUES { for &y in crate::test::EDGE_VALUES { for (result, maximum) in [(fmax(x, y), true), (fmin(x, y), false)] { + // Observe the computed value: LLVM can infer a non-NaN result + // from one numeric operand, but native extrema may quiet an + // sNaN to NaN (notably in optimized AArch64 builds). + let result = std::hint::black_box(result); if x.is_nan() && y.is_nan() { assert!(result.is_nan()); } else if x.is_nan() || y.is_nan() { @@ -304,7 +308,15 @@ mod tests { if !signaling(x) && !signaling(y) { assert_eq!(result.to_bits(), expected.to_bits()); } else { - assert!(result.is_nan() || result.to_bits() == expected.to_bits()); + assert!( + result.is_nan() || result.to_bits() == expected.to_bits(), + "f{}(0x{:016x}, 0x{:016x}) = 0x{:016x}; expected NaN or 0x{:016x}", + if maximum { "max" } else { "min" }, + x.to_bits(), + y.to_bits(), + result.to_bits(), + expected.to_bits(), + ); } } else { let expected = if (x > y) == maximum { x } else { y };