From b63a42c93bb59c05e812e7f473ff09354315da1d Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Sun, 28 Jun 2026 01:26:00 +0800 Subject: [PATCH 01/20] Add implementation plan for Python bindings Covers 10 concrete steps: fix todo!() panics, add UniInput conversion helpers, implement arithmetic/comparison/bool for FPy/DPy/RPy, complete integer methods, add float/rational predicates and rounding, create math module for sin/cos/exp/sqrt and gcd/lcm, fix __format__, update type stubs, and add tests. Co-Authored-By: Claude --- python/TODO-python.md | 988 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 988 insertions(+) create mode 100644 python/TODO-python.md diff --git a/python/TODO-python.md b/python/TODO-python.md new file mode 100644 index 00000000..fb8618e4 --- /dev/null +++ b/python/TODO-python.md @@ -0,0 +1,988 @@ +# Implementation Plan — dashu Python Package + +This document is the concrete implementation plan for the dashu Python bindings. Each section describes *how* to implement the feature, not just what's missing. + +--- + +## Architecture + +### Key patterns to follow + +**UniInput dispatch** — `python/src/convert.rs`: The `UniInput` enum has 11 variants covering all Python numeric types (Uint, Int, BUint, BInt, OBInt, Float, BFloat, BDecimal, OBDecimal, BRational, OBRational). Its `FromPyObject` impl already extracts any Python number into the right variant. We add three conversion methods (`into_fpy`, `into_dpy`, `into_rpy`) that collapse any variant into the target dashu type, making arithmetic functions simple one-liners. + +**Comparison** — `num_order::NumOrd` trait provides cross-type comparison for all dashu types: FBig↔UBig, FBig↔IBig, FBig↔RBig, FBig↔f64, RBig↔UBig, RBig↔IBig, etc. The `__richcmp__` pattern is already established in UPy/IPy: `op.matches(self.0.num_cmp(&rhs))`. + +**Arithmetic macros** — The existing `impl_binops!` macro in `int.rs` generates forward/reverse dispatch functions by matching each `UniInput` variant. For FPy/DPy/RPy we create simpler versions that first convert the operand to the target type via `into_fpy()`/etc, then call the Rust operator. + +### Rust API facts (verified) + +``` +FBig methods (direct, no trait import needed): + .sin(), .cos(), .tan(), .asin(), .acos(), .atan(), .atan2(&x) + .sinh(), .cosh(), .tanh(), .asinh(), .acosh(), .atanh() + .exp(), .exp_m1(), .ln(), .ln_1p() + .sqrt(), .cbrt(), .nth_root(n) + .trunc(), .floor(), .ceil(), .round(), .fract(), .signum() + .powi(IBig), .powf(&FBig) + .repr() -> &Repr (has .is_zero(), .is_finite(), .is_infinite(), .sign(), .precision(), .digits()) + +RBig methods: + .numerator() -> &IBig, .denominator() -> &UBig + .trunc() -> IBig, .floor() -> IBig, .ceil() -> IBig, .round() -> IBig (NOT RBig!) + .fract() -> RBig + .split_at_point() -> (IBig, RBig) + .sqr(), .cubic(), .pow(n: usize) + .is_int(), .sign() -> Sign, .signum() -> RBig + +UBig methods: + .sqrt() -> UBig, .cbrt() -> UBig, .nth_root(n) -> UBig + .sqr() -> UBig, .cubic() -> UBig + .ilog(&UBig) -> usize + .count_ones() -> usize, .count_zeros() -> Option + .trailing_zeros() -> Option, .trailing_ones() -> Option + .is_power_of_two() -> bool, .next_power_of_two() -> UBig + .is_multiple_of(&UBig) -> bool, .remove(&UBig) -> Option + +IBig methods: + .sqrt() -> UBig (always unsigned!), .cbrt() -> IBig, .nth_root(n) -> IBig + .sqr() -> UBig (always unsigned!), .cubic() -> IBig + .ilog(&UBig) -> usize (takes &UBig, not &IBig!) + .trailing_zeros() -> Option, .trailing_ones() -> Option + .sign() -> Sign, .signum() -> IBig + .into_parts() -> (Sign, UBig) + IBig::from_parts(Sign, UBig) -> IBig + +Traits (need `use` imports): + dashu_base::ring::Gcd::gcd(a, b) -- UBig.gcd(UBig) -> UBig + dashu_base::ring::ExtendedGcd::gcd_ext(a, b) -> (UBig, IBig, IBig) + dashu_base::DivEuclid::div_euclid(a, b), DivRemEuclid::div_rem_euclid(a, b) + +IBig::is_negative()/is_positive() -- NOT inherent! Use self.sign() == Sign::Negative instead. + +Conversions: + FBig::try_from(f64) -- base 2 ONLY (FPy works, DBig does not) + FBig::from(UBig), FBig::from(IBig) -- infallible, any base + FBig::try_from(RBig) -- exact only, returns Err if precision loss + RBig::try_from(FBig) -- exact only + RBig::try_from(f64) -- exact only (f64 has limited precision anyway) + DBig from f64 -- go through string: format!("{:e}", x) then DBig::from_str + +Approximation::value() extracts T from both Exact(T) and Inexact(T, E) variants. +``` + +--- + +## Step 1: Fix `todo!()` panics in existing code + +**File: `python/src/int.rs`** + +### 1a: Fix `upy_mod` and `ipy_mod` + +Both `upy_mod` (line 140) and `ipy_mod` (line 150) have `_ => todo!()` for the float/rational variants. Fill the missing arms by converting to the corresponding float/rational type: + +```rust +fn upy_mod(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyResult { + let result: Py = match rhs { + UniInput::Uint(x) => UPy((&lhs.0).rem(x).into()).into_py(py), + UniInput::Int(x) => UPy((&lhs.0).rem(IBig::from(x))).into_py(py), + UniInput::BUint(x) => UPy((&lhs.0).rem(&x.0)).into_py(py), + UniInput::BInt(x) => UPy((&lhs.0).rem(&x.0)).into_py(py), + UniInput::OBInt(x) => UPy((&lhs.0).rem(x)).into_py(py), + UniInput::Float(x) => FPy((&lhs.0).rem(FBig::try_from(x).unwrap())).into_py(py), + UniInput::BFloat(x) => FPy((&lhs.0).rem(&x.0)).into_py(py), + UniInput::BDecimal(x) => DPy((&lhs.0).rem(&x.0)).into_py(py), + UniInput::OBDecimal(x) => DPy((&lhs.0).rem(x)).into_py(py), + UniInput::BRational(x) => RPy((&lhs.0).rem(&x.0)).into_py(py), + UniInput::OBRational(x) => RPy((&lhs.0).rem(x)).into_py(py), + }; + Ok(result) +} +``` + +Same pattern for `ipy_mod`. Change return type from `PyObject` to `PyResult` and update the `__mod__` wrappers at lines 493 and 655 to propagate with `?`. + +### 1b: Fix `ipy_pow` todo!() branches + +Replace the three `_ => todo!()` arms with proper error returns: + +```rust +// For modulus parameter (line 174): +_ => return Err(PyTypeError::new_err("modulus must be an integer")), + +// For exponent in modulus branch (line 186): +_ => return Err(PyTypeError::new_err("modular exponentiation requires an integer exponent")), + +// For exponent in non-modulus branch (line 191): +_ => return Err(PyTypeError::new_err("integer power requires a non-negative integer exponent")), +``` + +--- + +## Step 2: Add `UniInput` conversion helpers + +**File: `python/src/convert.rs`** + +Add three methods to `impl<'a> UniInput<'a>` that convert any numeric variant to the target dashu type: + +### `into_fpy(self) -> PyResult` + +| Input variant | Conversion | +|---|---| +| `Uint(x)` | `FBig::from(x)` | +| `Int(x)` | `FBig::from(IBig::from(x))` | +| `BUint(x)` | `FBig::from(x.0.clone())` | +| `BInt(x)` | `FBig::from(x.0.clone())` | +| `OBInt(x)` | `FBig::from(x)` | +| `Float(x)` | `FBig::try_from(x)` — map error to PyValueError | +| `BFloat(x)` | `x.0.clone()` | +| `BDecimal/OBDecimal` | `Err(PyTypeError("decimal cannot be mixed with binary float; convert explicitly"))` | +| `BRational(x)` | `FBig::try_from(x.0.clone())` — map ConversionError to PyTypeError | +| `OBRational(x)` | `FBig::try_from(x)` — same | + +### `into_dpy(self) -> PyResult` + +Same pattern but for DBig. For `Float(x)` use `format!("{:e}", x)` → `DBig::from_str`. For `BFloat` return type error. For `BRational`/`OBRational` use `FBig::try_from` since DBig IS FBig. + +### `into_rpy(self) -> PyResult` + +| Input variant | Conversion | +|---|---| +| `Uint(x)` | `RBig::from(x)` | +| `Int(x)` | `RBig::from(IBig::from(x))` | +| `BUint(x)` | `RBig::from(x.0.clone())` | +| `BInt(x)` | `RBig::from(x.0.clone())` | +| `OBInt(x)` | `RBig::from(x)` | +| `Float(x)` | `RBig::try_from(x)` — map error to PyValueError | +| `BFloat(x)` | `RBig::try_from(x.0.clone())` — map ConversionError | +| `BDecimal(x)` | `RBig::try_from(x.0.clone())` — same | +| `OBDecimal(x)` | `RBig::try_from(x)` — same | +| `BRational(x)` | `x.0.clone()` | +| `OBRational(x)` | `x` | + +Add imports at top of `convert.rs`: +```rust +use crate::types::FPy; +use dashu_float::FBig; +use dashu_base::ConversionError; +use std::str::FromStr; +``` + +--- + +## Step 3: Add arithmetic operators to FPy and DPy + +**File: `python/src/float.rs`** + +### 3a: Comparison + +Add `__richcmp__` using the same pattern as UPy/IPy (int.rs lines 248–263): + +```rust +use num_order::NumOrd; +use pyo3::basic::CompareOp; + +#[pymethods] +impl FPy { + fn __richcmp__(&self, other: UniInput<'_>, op: CompareOp) -> bool { + let order = match other { + UniInput::Uint(x) => self.0.num_cmp(&x), + UniInput::Int(x) => self.0.num_cmp(&x), + UniInput::BUint(x) => self.0.num_cmp(&x.0), + UniInput::BInt(x) => self.0.num_cmp(&x.0), + UniInput::OBInt(x) => self.0.num_cmp(&x), + UniInput::Float(x) => self.0.num_cmp(&x), + UniInput::BFloat(x) => self.0.cmp(&x.0), + UniInput::BDecimal(x) => self.0.num_cmp(&x.0), + UniInput::OBDecimal(x) => self.0.num_cmp(&x), + UniInput::BRational(x) => self.0.num_cmp(&x.0), + UniInput::OBRational(x) => self.0.num_cmp(&x), + }; + op.matches(order) + } +} +``` + +Same for DPy (DBig is FBig so NumOrd works identically). + +### 3b: Bool + +```rust +fn __bool__(&self) -> bool { + !self.0.repr().is_zero() +} +``` + +### 3c: Arithmetic macro + +Create a helper macro that uses `into_fpy()` for forward/reverse dispatch: + +```rust +macro_rules! impl_fpy_binops { + // Commutative (add, mul) — __radd__ and __rmul__ reuse the forward function + ($method:ident, $rs_method:ident) => { + fn $method(lhs: &FPy, rhs: UniInput<'_>, py: Python) -> PyResult { + let rhs = rhs.into_fpy()?; + Ok(FPy((&lhs.0).$rs_method(&rhs.0)).into_py(py)) + } + }; + // Non-commutative (sub, div, rem) — also generate reverse function + ($method:ident, $rev_method:ident, $rs_method:ident) => { + impl_fpy_binops!($method, $rs_method); + fn $rev_method(lhs: UniInput<'_>, rhs: &FPy, py: Python) -> PyResult { + let lhs = lhs.into_fpy()?; + Ok(FPy(lhs.0.$rs_method(&rhs.0)).into_py(py)) + } + }; +} + +impl_fpy_binops!(fpy_add, add); +impl_fpy_binops!(fpy_mul, mul); +impl_fpy_binops!(fpy_sub, fpy_rsub, sub); +impl_fpy_binops!(fpy_div, fpy_rdiv, div); +impl_fpy_binops!(fpy_mod, fpy_rmod, rem); +``` + +Wire in `#[pymethods] impl FPy`: + +```rust +fn __add__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_add(self, other, py) } +fn __radd__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_add(self, other, py) } +fn __sub__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_sub(self, other, py) } +fn __rsub__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_rsub(other, self, py) } +fn __mul__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_mul(self, other, py) } +fn __rmul__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_mul(self, other, py) } +fn __truediv__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_div(self, other, py) } +fn __rtruediv__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_rdiv(other, self, py) } +fn __mod__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_mod(self, other, py) } +fn __rmod__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_rmod(other, self, py) } +fn __neg__(&self) -> FPy { FPy(-&self.0) } +fn __pos__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } +fn __abs__(&self) -> FPy { FPy(self.0.abs()) } +``` + +Same pattern for DPy: create an identical macro using `into_dpy()` instead of `into_fpy()`. + +### 3d: Imports to add at top of `float.rs` + +```rust +use num_order::NumOrd; +use pyo3::basic::CompareOp; +use crate::convert::parse_error_to_py; +``` + +--- + +## Step 4: Add arithmetic operators to RPy + +**File: `python/src/ratio.rs`** + +Same pattern as Step 3c but using `into_rpy()`: + +```rust +macro_rules! impl_rpy_binops { + ($method:ident, $rs_method:ident) => { + fn $method(lhs: &RPy, rhs: UniInput<'_>, py: Python) -> PyResult { + let rhs = rhs.into_rpy()?; + Ok(RPy((&lhs.0).$rs_method(&rhs.0)).into_py(py)) + } + }; + ($method:ident, $rev_method:ident, $rs_method:ident) => { + impl_rpy_binops!($method, $rs_method); + fn $rev_method(lhs: UniInput<'_>, rhs: &RPy, py: Python) -> PyResult { + let lhs = lhs.into_rpy()?; + Ok(RPy(lhs.0.$rs_method(&rhs.0)).into_py(py)) + } + }; +} + +impl_rpy_binops!(rpy_add, add); +impl_rpy_binops!(rpy_mul, mul); +impl_rpy_binops!(rpy_sub, rpy_rsub, sub); +impl_rpy_binops!(rpy_div, rpy_rdiv, div); +impl_rpy_binops!(rpy_mod, rpy_rmod, rem); +``` + +Wire `__add__`/`__radd__`/`__sub__`/`__rsub__`/`__mul__`/`__rmul__`/`__truediv__`/`__rtruediv__`/`__mod__`/`__rmod__`/`__neg__`/`__pos__`/`__abs__`. + +Add `__richcmp__` (same pattern as FPy but using RBig's NumOrd impls) and `__bool__`. + +Add imports: +```rust +use num_order::NumOrd; +use pyo3::basic::CompareOp; +use std::ops::{Add, Sub, Mul, Div, Rem}; +``` + +--- + +## Step 5: Complete integer type methods + +**File: `python/src/int.rs`** + +### 5a: Add number theory + bit ops to UPy + +Add to `#[pymethods] impl UPy`: + +```rust +// Number theory +fn sqrt(&self) -> Self { UPy(self.0.sqrt()) } +fn cbrt(&self) -> Self { UPy(self.0.cbrt()) } +fn nth_root(&self, n: usize) -> Self { UPy(self.0.nth_root(n)) } +fn sqr(&self) -> Self { UPy(self.0.sqr()) } +fn cubic(&self) -> Self { UPy(self.0.cubic()) } +fn ilog(&self, base: &Self) -> PyResult { + if base.0 <= UBig::ONE { + return Err(PyValueError::new_err("base must be > 1")); + } + Ok(self.0.ilog(&base.0)) +} +fn is_multiple_of(&self, divisor: &Self) -> bool { self.0.is_multiple_of(&divisor.0) } +fn remove(&mut self, factor: &Self) -> PyResult { + self.0.remove(&factor.0).ok_or_else(|| PyValueError::new_err("factor does not divide this number")) +} +fn gcd(&self, other: &Self) -> Self { + use dashu_base::ring::Gcd; + UPy(Gcd::gcd(&self.0, &other.0)) +} +fn gcd_ext(&self, other: &Self) -> (Self, IPy, IPy) { + use dashu_base::ring::ExtendedGcd; + let (g, s, t) = ExtendedGcd::gcd_ext(&self.0, &other.0); + (UPy(g), IPy(s), IPy(t)) +} + +// Bit operations +fn count_ones(&self) -> usize { self.0.count_ones() } +fn count_zeros(&self) -> Option { self.0.count_zeros() } +fn trailing_zeros(&self) -> Option { self.0.trailing_zeros() } +fn trailing_ones(&self) -> Option { self.0.trailing_ones() } +fn is_power_of_two(&self) -> bool { self.0.is_power_of_two() } +fn next_power_of_two(slf: PyRef<'_, Self>) -> Self { UPy(slf.0.clone().next_power_of_two()) } + +// Accessors +fn is_one(&self) -> bool { self.0.is_one() } +#[staticmethod] +fn ones(n: usize) -> Self { UPy(UBig::ones(n)) } +``` + +### 5b: Add missing operators to UPy + +```rust +// Floor division +fn __floordiv__(&self, other: UniInput<'_>, py: Python) -> PyResult { + use dashu_base::ring::DivEuclid; + match other { + UniInput::Uint(x) => Ok(UPy(self.0.div_euclid(&UBig::from(x))).into_py(py)), + UniInput::BUint(x) => Ok(UPy(self.0.div_euclid(&x.0)).into_py(py)), + UniInput::Int(x) => Ok(IPy(self.0.as_ibig().clone().div_euclid(IBig::from(x))).into_py(py)), + UniInput::BInt(x) => Ok(IPy(self.0.as_ibig().clone().div_euclid(x.0.clone())).into_py(py)), + UniInput::OBInt(x) => Ok(IPy(self.0.as_ibig().clone().div_euclid(x)).into_py(py)), + _ => Err(PyTypeError::new_err("floor division requires an integer divisor")), + } +} +fn __rfloordiv__(&self, other: UniInput<'_>, py: Python) -> PyResult { + // other // self — reverse dispatch + self.__floordiv__(other, py) // reuse for now; will be handled correctly via radd pattern + // NOTE: to do this properly, need a reverse dispatch like rsub. + // For MVP, raise NotImplementedError for complex cases + todo!("proper __rfloordiv__ dispatch") +} + +// Divmod +fn __divmod__(&self, other: UniInput<'_>, py: Python) -> PyResult { + use dashu_base::ring::DivRemEuclid; + match other { + UniInput::Uint(x) => { + let (q, r) = self.0.div_rem_euclid(&UBig::from(x)); + Ok((UPy(q), UPy(r)).into_py(py)) + } + UniInput::BUint(x) => { + let (q, r) = self.0.div_rem_euclid(&x.0); + Ok((UPy(q), UPy(r)).into_py(py)) + } + UniInput::Int(x) => { + let (q, r) = self.0.as_ibig().clone().div_rem_euclid(IBig::from(x)); + Ok((IPy(q), UPy(r)).into_py(py)) + } + UniInput::BInt(x) => { + let (q, r) = self.0.as_ibig().clone().div_rem_euclid(x.0.clone()); + Ok((IPy(q), UPy(r)).into_py(py)) + } + UniInput::OBInt(x) => { + let (q, r) = self.0.as_ibig().clone().div_rem_euclid(x); + Ok((IPy(q), UPy(r)).into_py(py)) + } + _ => Err(PyTypeError::new_err("divmod requires an integer divisor")), + } +} +``` + +### 5c: Add number theory + bit ops + accessors to IPy + +```rust +// Number theory (note return types — sqrt/sqr return unsigned!) +fn sqrt(&self) -> PyResult { + if self.0.is_negative() { + return Err(PyValueError::new_err("cannot compute sqrt of negative number")); + } + Ok(UPy(self.0.abs().sqrt())) +} +fn cbrt(&self) -> Self { IPy(self.0.cbrt()) } +fn nth_root(&self, n: usize) -> PyResult { + if n % 2 == 0 && self.0.sign() == Sign::Negative { + return Err(PyValueError::new_err("cannot compute even root of negative number")); + } + Ok(IPy(self.0.nth_root(n))) +} +fn sqr(&self) -> UPy { UPy(self.0.sqr()) } +fn cubic(&self) -> Self { IPy(self.0.cubic()) } +fn ilog(&self, base: &UPy) -> PyResult { + if base.0 <= UBig::ONE { + return Err(PyValueError::new_err("base must be > 1")); + } + Ok(self.0.ilog(&base.0)) +} + +// Bit operations +fn trailing_zeros(&self) -> Option { self.0.trailing_zeros() } +fn trailing_ones(&self) -> Option { self.0.trailing_ones() } +fn __invert__(&self) -> Self { IPy(!&self.0) } + +// Accessors +fn is_one(&self) -> bool { self.0.is_one() } +fn sign(&self) -> PySign { + match self.0.sign() { + Sign::Positive => PySign::Positive, + Sign::Negative => PySign::Negative, + } +} +fn signum(&self) -> Self { IPy(self.0.signum()) } +fn is_negative(&self) -> bool { self.0.sign() == Sign::Negative } +fn is_positive(&self) -> bool { self.0.sign() == Sign::Positive } +fn to_parts(&self) -> (PySign, UPy) { + let (sign, mag) = self.0.clone().into_parts(); + let py_sign = match sign { + Sign::Positive => PySign::Positive, + Sign::Negative => PySign::Negative, + }; + (py_sign, UPy(mag)) +} +#[staticmethod] +fn from_parts(sign: &PySign, magnitude: &UPy) -> Self { + let sign = match sign { + PySign::Positive => Sign::Positive, + PySign::Negative => Sign::Negative, + }; + IPy(IBig::from_parts(sign, magnitude.0.clone())) +} +fn as_ubig(&self) -> Option { + self.0.as_ubig().cloned().map(UPy) +} +``` + +### 5d: IBig interop parity with UBig + +Add to IPy (copy patterns from UPy at int.rs lines 379–457): + +```rust +fn to_words(&self) -> PyWords { + let (_, words) = self.0.as_sign_words(); + PyWords(words.to_vec()) +} +#[staticmethod] +fn from_words(ob: &PyAny) -> PyResult { + if let Ok(vec) = as FromPyObject>::extract(ob) { + Ok(IPy(IBig::from_words(&vec))) + } else if let Ok(words) = as FromPyObject>::extract(ob) { + Ok(IPy(IBig::from_words(&words.0))) + } else { + Err(PyTypeError::new_err("only list of integers or Words instance can be used")) + } +} +fn to_chunks(&self, chunk_bits: usize, py: Python) -> PyResult { + if chunk_bits == 0 { + Err(PyValueError::new_err("chunk size must not be zero")) + } else { + let iter = self.0.to_chunks(chunk_bits).into_vec().into_iter().map(|u| UPy(u).into_py(py)); + Ok(PyTuple::new(py, iter).into_py(py)) + } +} +#[staticmethod] +fn from_chunks(chunks: &PyAny, chunk_bits: usize) -> PyResult { + // Same pattern as UPy::from_chunks, but collect into Vec then join + if chunk_bits == 0 { + return Err(PyValueError::new_err("chunk size must not be zero")); + } + let mut input = Vec::new(); + if let Ok(list) = chunks.downcast::() { + input.reserve_exact(list.len()); + for item in list { + input.push(UniInput::extract(item)?.to_ubig()?); + } + } else if let Ok(tuple) = chunks.downcast::() { + input.reserve_exact(tuple.len()); + for item in tuple { + input.push(UniInput::extract(item)?.to_ubig()?); + } + } else if let Ok(iter) = chunks.downcast::() { + for item in iter { + input.push(UniInput::extract(item?)?.to_ubig()?); + } + } else { + return Err(PyTypeError::new_err("chunks must be a list, tuple, or iterator")); + } + Ok(IPy(IBig::from_chunks(input.iter(), chunk_bits))) +} +``` + +Also copy the bit-slice `__getitem__`, `__setitem__`, `__delitem__` implementations from UPy (lines 269–371) to IPy, adapting from `UBig` to `IBig`. The core bit-manipulation logic uses `split_bits`, `clear_high_bits`, `set_bit`, `clear_bit` which exist on IBig as well. + +### 5e: Add in-place operators to UPy/IPy + +For all types, in-place operators take `&mut self`: + +```rust +fn __iadd__(&mut self, other: &Self) { self.0 += &other.0; } +fn __isub__(&mut self, other: &Self) { self.0 -= &other.0; } +fn __imul__(&mut self, other: &Self) { self.0 *= &other.0; } +fn __iand__(&mut self, other: &Self) { self.0 &= &other.0; } +fn __ior__(&mut self, other: &Self) { self.0 |= &other.0; } +fn __ixor__(&mut self, other: &Self) { self.0 ^= &other.0; } +fn __ilshift__(&mut self, other: usize) { self.0 <<= other; } +fn __irshift__(&mut self, other: usize) { self.0 >>= other; } +``` + +Note: these are same-type-only for the MVP. Python's data model falls back to `__add__` + assignment when `__iadd__` is not defined, so cross-type in-place is handled automatically. + +--- + +## Step 6: Float & rational predicates, conversions, rounding + +**File: `python/src/float.rs`** — add to `#[pymethods] impl FPy` (and replicate for DPy): + +```rust +// Predicates +fn is_zero(&self) -> bool { self.0.repr().is_zero() } +fn is_finite(&self) -> bool { self.0.repr().is_finite() } +fn is_infinite(&self) -> bool { self.0.repr().is_infinite() } +fn is_nan(&self) -> bool { self.0.repr().is_nan() } + +// Sign +fn sign(&self) -> PySign { + match self.0.repr().sign() { + Sign::Positive => PySign::Positive, + Sign::Negative => PySign::Negative, + } +} +fn signum(&self) -> Self { FPy(self.0.signum()) } + +// Rounding (all return FPy) +fn trunc(&self) -> Self { FPy(self.0.trunc()) } +fn floor(&self) -> Self { FPy(self.0.floor()) } +fn ceil(&self) -> Self { FPy(self.0.ceil()) } +fn round(&self) -> Self { FPy(self.0.round()) } +fn fract(&self) -> Self { FPy(self.0.fract()) } + +// Conversion +fn to_int(&self) -> PyResult { + let int: IBig = self.0.clone().try_into().map_err(|e: ConversionError| { + PyValueError::new_err(format!("cannot convert to integer: {}", e)) + })?; + Ok(IPy(int)) +} +fn __int__(&self, py: Python) -> PyResult { + let ipy = self.to_int()?; + convert_from_ibig(&ipy.0, py) +} + +// Precision +fn with_precision(&self, precision: usize) -> Self { + let rounded = self.0.clone().with_precision(precision); + FPy(rounded.value()) +} +fn precision(&self) -> usize { self.0.context().precision() } +fn digits(&self) -> usize { self.0.digits() } + +// Float-specific constructors +#[staticmethod] +fn from_parts(significand: &IPy, exponent: isize) -> Self { + FPy(FBig::from_parts(significand.0.clone(), exponent)) +} +``` + +**File: `python/src/ratio.rs`** — add to `#[pymethods] impl RPy`: + +```rust +// Properties +#[getter] +fn numerator(&self) -> IPy { IPy(self.0.numerator().clone()) } +#[getter] +fn denominator(&self) -> UPy { UPy(self.0.denominator().clone()) } + +// Predicates +fn is_int(&self) -> bool { self.0.is_int() } +fn is_one(&self) -> bool { self.0.is_one() } +fn sign(&self) -> PySign { + match self.0.sign() { + Sign::Positive => PySign::Positive, + Sign::Negative => PySign::Negative, + } +} +fn signum(&self) -> Self { RPy(self.0.signum()) } + +// Rounding (return IBig — NOT RBig!) +fn trunc(&self) -> IPy { IPy(self.0.trunc()) } +fn floor(&self) -> IPy { IPy(self.0.floor()) } +fn ceil(&self) -> IPy { IPy(self.0.ceil()) } +fn round(&self) -> IPy { IPy(self.0.round()) } +fn fract(&self) -> Self { RPy(self.0.fract()) } +fn split_at_point(&self) -> (IPy, Self) { + let (int_part, frac_part) = self.0.clone().split_at_point(); + (IPy(int_part), RPy(frac_part)) +} + +// Powers +fn sqr(&self) -> Self { RPy(self.0.sqr()) } +fn cubic(&self) -> Self { RPy(self.0.cubic()) } +fn pow(&self, n: usize) -> Self { RPy(self.0.pow(n)) } + +// Constructor +#[staticmethod] +fn from_parts(numerator: &IPy, denominator: &UPy) -> Self { + RPy(RBig::from_parts(numerator.0.clone(), denominator.0.clone())) +} + +// Conversion +fn to_int(&self) -> IPy { IPy(self.0.trunc()) } +fn __int__(&self, py: Python) -> PyResult { + let ipy = IPy(self.0.trunc()); + convert_from_ibig(&ipy.0, py) +} + +// Simplification +#[staticmethod] +fn simplest_from_float(f: &FPy) -> Option { + RBig::simplest_from_float(&f.0).map(RPy) +} +``` + +Add import to `float.rs`: `use dashu_base::{Sign, ConversionError};` +Add imports to `ratio.rs`: `use dashu_base::Sign;` + +--- + +## Step 7: Create math module + +**New file: `python/src/math.rs`** + +```rust +use pyo3::prelude::*; +use crate::types::{FPy, DPy, UPy, IPy}; + +macro_rules! impl_math_func { + ($name:ident, ($($arg:ident: $arg_ty:ty),*) -> $ret:ty, $body:expr) => { + #[pyfunction] + pub fn $name($($arg: $arg_ty),*) -> $ret { + $body + } + }; +} + +// Trigonometric +impl_math_func!(sin, (x: &FPy) -> FPy, FPy(x.0.sin())); +impl_math_func!(cos, (x: &FPy) -> FPy, FPy(x.0.cos())); +impl_math_func!(tan, (x: &FPy) -> FPy, FPy(x.0.tan())); +impl_math_func!(asin, (x: &FPy) -> FPy, FPy(x.0.asin())); +impl_math_func!(acos, (x: &FPy) -> FPy, FPy(x.0.acos())); +impl_math_func!(atan, (x: &FPy) -> FPy, FPy(x.0.atan())); +#[pyfunction] +pub fn atan2(y: &FPy, x: &FPy) -> FPy { FPy(y.0.atan2(&x.0)) } +#[pyfunction] +pub fn sincos(x: &FPy) -> (FPy, FPy) { + let (s, c) = x.0.sin_cos(); + (FPy(s), FPy(c)) +} + +// Hyperbolic +impl_math_func!(sinh, (x: &FPy) -> FPy, FPy(x.0.sinh())); +impl_math_func!(cosh, (x: &FPy) -> FPy, FPy(x.0.cosh())); +impl_math_func!(tanh, (x: &FPy) -> FPy, FPy(x.0.tanh())); +impl_math_func!(asinh, (x: &FPy) -> FPy, FPy(x.0.asinh())); +impl_math_func!(acosh, (x: &FPy) -> FPy, FPy(x.0.acosh())); +impl_math_func!(atanh, (x: &FPy) -> FPy, FPy(x.0.atanh())); + +// Exponential and log +impl_math_func!(exp, (x: &FPy) -> FPy, FPy(x.0.exp())); +impl_math_func!(exp_m1, (x: &FPy) -> FPy, FPy(x.0.exp_m1())); +impl_math_func!(ln, (x: &FPy) -> FPy, FPy(x.0.ln())); +impl_math_func!(ln_1p, (x: &FPy) -> FPy, FPy(x.0.ln_1p())); + +// Roots +impl_math_func!(sqrt, (x: &FPy) -> FPy, FPy(x.0.sqrt())); +impl_math_func!(cbrt, (x: &FPy) -> FPy, FPy(x.0.cbrt())); +#[pyfunction] +pub fn nth_root(x: &FPy, n: usize) -> FPy { FPy(x.0.nth_root(n)) } + +// Integer number theory +#[pyfunction] +pub fn gcd(a: &UPy, b: &UPy) -> UPy { + use dashu_base::ring::Gcd; + UPy(Gcd::gcd(&a.0, &b.0)) +} +#[pyfunction] +pub fn gcd_ext(a: &UPy, b: &UPy) -> (UPy, IPy, IPy) { + use dashu_base::ring::ExtendedGcd; + let (g, s, t) = ExtendedGcd::gcd_ext(&a.0, &b.0); + (UPy(g), IPy(s), IPy(t)) +} +#[pyfunction] +pub fn lcm(a: &UPy, b: &UPy) -> UPy { + use dashu_base::ring::Gcd; + UPy((&a.0 * &b.0) / Gcd::gcd(&a.0, &b.0)) +} +``` + +Also add corresponding methods directly on FPy/DPy in `float.rs`: + +```rust +// In #[pymethods] impl FPy: +fn sin(&self) -> Self { FPy(self.0.sin()) } +fn cos(&self) -> Self { FPy(self.0.cos()) } +fn tan(&self) -> Self { FPy(self.0.tan()) } +fn asin(&self) -> Self { FPy(self.0.asin()) } +fn acos(&self) -> Self { FPy(self.0.acos()) } +fn atan(&self) -> Self { FPy(self.0.atan()) } +fn atan2(&self, x: &Self) -> Self { FPy(self.0.atan2(&x.0)) } +fn sincos(&self) -> (Self, Self) { let (s, c) = self.0.sin_cos(); (FPy(s), FPy(c)) } +fn sinh(&self) -> Self { FPy(self.0.sinh()) } +fn cosh(&self) -> Self { FPy(self.0.cosh()) } +fn tanh(&self) -> Self { FPy(self.0.tanh()) } +fn asinh(&self) -> Self { FPy(self.0.asinh()) } +fn acosh(&self) -> Self { FPy(self.0.acosh()) } +fn atanh(&self) -> Self { FPy(self.0.atanh()) } +fn exp(&self) -> Self { FPy(self.0.exp()) } +fn exp_m1(&self) -> Self { FPy(self.0.exp_m1()) } +fn ln(&self) -> Self { FPy(self.0.ln()) } +fn ln_1p(&self) -> Self { FPy(self.0.ln_1p()) } +fn sqrt(&self) -> Self { FPy(self.0.sqrt()) } +fn cbrt(&self) -> Self { FPy(self.0.cbrt()) } +fn nth_root(&self, n: usize) -> Self { FPy(self.0.nth_root(n)) } +``` + +Same for DPy. + +Register in `lib.rs`: + +```rust +mod math; + +// In #[pymodule] fn dashu: +m.add_function(wrap_pyfunction!(math::sin, m)?)?; +m.add_function(wrap_pyfunction!(math::cos, m)?)?; +m.add_function(wrap_pyfunction!(math::tan, m)?)?; +m.add_function(wrap_pyfunction!(math::asin, m)?)?; +m.add_function(wrap_pyfunction!(math::acos, m)?)?; +m.add_function(wrap_pyfunction!(math::atan, m)?)?; +m.add_function(wrap_pyfunction!(math::atan2, m)?)?; +m.add_function(wrap_pyfunction!(math::sincos, m)?)?; +m.add_function(wrap_pyfunction!(math::sinh, m)?)?; +m.add_function(wrap_pyfunction!(math::cosh, m)?)?; +m.add_function(wrap_pyfunction!(math::tanh, m)?)?; +m.add_function(wrap_pyfunction!(math::asinh, m)?)?; +m.add_function(wrap_pyfunction!(math::acosh, m)?)?; +m.add_function(wrap_pyfunction!(math::atanh, m)?)?; +m.add_function(wrap_pyfunction!(math::exp, m)?)?; +m.add_function(wrap_pyfunction!(math::exp_m1, m)?)?; +m.add_function(wrap_pyfunction!(math::ln, m)?)?; +m.add_function(wrap_pyfunction!(math::ln_1p, m)?)?; +m.add_function(wrap_pyfunction!(math::sqrt, m)?)?; +m.add_function(wrap_pyfunction!(math::cbrt, m)?)?; +m.add_function(wrap_pyfunction!(math::nth_root, m)?)?; +m.add_function(wrap_pyfunction!(math::gcd, m)?)?; +m.add_function(wrap_pyfunction!(math::gcd_ext, m)?)?; +m.add_function(wrap_pyfunction!(math::lcm, m)?)?; +``` + +--- + +## Step 8: Fix `__format__` from `todo!()` to minimal working + +**Files: `python/src/int.rs`, `python/src/float.rs`, `python/src/ratio.rs`** + +Replace each `fn __format__(&self) { todo!() }` with: + +```rust +fn __format__(&self, _format_spec: &str) -> String { + // For MVP: ignore format_spec, just delegate to Display + format!("{}", self.0) +} +``` + +Full Python format mini-language parsing is deferred to Phase 6. + +--- + +## Step 9: Update type stubs + +**File: `python/dashu.pyi`** + +Complete the stubs to match all newly exposed methods. Key additions: + +```python +class FBig: + def __init__(self, obj: float | str | int): ... + def unwrap(self) -> tuple[IBig, int]: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __format__(self, format_spec: str) -> str: ... + def __hash__(self) -> int: ... + def __float__(self) -> float: ... + def __int__(self) -> int: ... + def __bool__(self) -> bool: ... + + # Arithmetic + @overload + def __add__(self, other: UBig | IBig | FBig | DBig | RBig) -> UBig | IBig | FBig | DBig | RBig: ... + @overload + def __add__(self, other: int) -> FBig: ... + @overload + def __radd__(self, other: int) -> FBig: ... + # ... same for __sub__, __rsub__, __mul__, __rmul__, __truediv__, __rtruediv__, __mod__, __rmod__ + + def __neg__(self) -> FBig: ... + def __pos__(self) -> FBig: ... + def __abs__(self) -> FBig: ... + + # Comparison + def __eq__(self, other) -> bool: ... + def __ne__(self, other) -> bool: ... + def __lt__(self, other) -> bool: ... + def __le__(self, other) -> bool: ... + def __gt__(self, other) -> bool: ... + def __ge__(self, other) -> bool: ... + + # Predicates + def is_zero(self) -> bool: ... + def is_finite(self) -> bool: ... + def is_infinite(self) -> bool: ... + def is_nan(self) -> bool: ... + def sign(self) -> Sign: ... + def signum(self) -> FBig: ... + + # Rounding + def trunc(self) -> FBig: ... + def floor(self) -> FBig: ... + def ceil(self) -> FBig: ... + def round(self) -> FBig: ... + def fract(self) -> FBig: ... + + # Precision + def precision(self) -> int: ... + def digits(self) -> int: ... + def with_precision(self, precision: int) -> FBig: ... + + # Conversion + def to_int(self) -> IBig: ... + @staticmethod + def from_parts(significand: IBig, exponent: int) -> FBig: ... + + # Math + def sin(self) -> FBig: ... + def cos(self) -> FBig: ... + def tan(self) -> FBig: ... + def exp(self) -> FBig: ... + def ln(self) -> FBig: ... + def sqrt(self) -> FBig: ... + # ... (all math methods) + +class DBig: + # Same as FBig, but constructor accepts decimal.Decimal | str | int + +class RBig: + # Constructor, arithmetic, comparison same pattern + # Plus: + @property + def numerator(self) -> IBig: ... + @property + def denominator(self) -> UBig: ... + def trunc(self) -> IBig: ... + def floor(self) -> IBig: ... + def ceil(self) -> IBig: ... + def round(self) -> IBig: ... + def fract(self) -> RBig: ... + def split_at_point(self) -> tuple[IBig, RBig]: ... + @staticmethod + def from_parts(numerator: IBig, denominator: UBig) -> RBig: ... + @staticmethod + def simplest_from_float(f: FBig) -> RBig | None: ... + +class Sign: + Positive: Sign + Negative: Sign +``` + +--- + +## Step 10: Add tests + +**New files to create:** + +- `python/tests/test_float_ops.py` — FPy/DPy constructors, arithmetic, comparison, bool, predicates, rounding +- `python/tests/test_ratio_ops.py` — RPy constructors, arithmetic, comparison, bool, properties, rounding +- `python/tests/test_int_math.py` — UBig/IBig sqrt, cbrt, gcd, bit ops, accessors +- `python/tests/test_math.py` — module-level sin/cos/exp/sqrt/gcd/lcm + +**Existing files to extend:** + +- `python/tests/test_int.py` — add tests for floordiv, divmod, in-place ops + +--- + +## Implementation Order + +``` +1. Step 1 (fix panics) — independent, do first +2. Step 2 (conversion helpers) — independent +3. Step 3 (FPy/DPy arithmetic) — depends on Step 2 +4. Step 4 (RPy arithmetic) — depends on Step 2 +5. Step 5 (int methods) — independent (can parallel with 3-4) +6. Step 6 (float/rational pred) — depends on Step 3-4 (adds to same impl blocks) +7. Step 7 (math module) — depends on Step 3 (FPy type must exist) +8. Step 8 (format fix) — independent +9. Step 9 (stubs) — after all code changes +10. Step 10 (tests) — after all code changes +``` + +Steps 3 and 5 can be done in parallel. Steps 3 and 4 share the same pattern so do them together. + +## Verification + +```bash +# Build +cd python && maturin develop + +# Smoke test +python -c " +from dashu import FBig, DBig, RBig, UBig, IBig +a = FBig('1.5') +b = FBig('2.0') +assert a + b == FBig('3.5') +assert a * 3 == FBig('4.5') +assert bool(FBig('0.0')) == False +assert UBig(144).sqrt() == UBig(12) +assert UBig(12).gcd(UBig(8)) == UBig(4) +r = RBig.from_parts(IBig(1), UBig(3)) +assert r * 3 == RBig.from_parts(IBig(1), UBig(1)) +print('All smoke tests passed') +" + +# Full test suite +python -m pytest python/tests/ -v +``` + +--- + +## Notes + +- **MSRV**: Preserve Rust 1.68. PyO3 0.20 is compatible. +- **Feature flags**: `num-modular` is used by `__pow__` but listed as optional — fix to hard dependency or wire as default feature. +- **Changelog**: Document all changes in `python/CHANGELOG.md` under `## Unreleased` → `### Add`. From 86934c588d50665592da312ac0d2bc1513256cb8 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Sun, 28 Jun 2026 16:36:33 +0800 Subject: [PATCH 02/20] Add PyO3 0.24 upgrade step to implementation plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Choose PyO3 0.24 (not 0.29) because: - 0.24 MSRV is 1.63 — dashu's MSRV 1.68 is preserved - 0.26+ requires MSRV 1.74+ (hard constraint violation per AGENTS.md) - 0.24 has the modern Bound API, IntoPyObject, and stable #[pymodule] form - 0.26+ drops Python 3.7 support Covers all breaking changes: #[pymodule] signature, FromPyObject→extract_bound, #[pyclass(eq, eq_int)], .into_py()→.into_py_any(), and .downcast()→.cast(). Co-Authored-By: Claude --- python/TODO-python.md | 139 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 134 insertions(+), 5 deletions(-) diff --git a/python/TODO-python.md b/python/TODO-python.md index fb8618e4..c71cac5f 100644 --- a/python/TODO-python.md +++ b/python/TODO-python.md @@ -72,6 +72,134 @@ Approximation::value() extracts T from both Exact(T) and Inexact(T, E) variants. --- +## Step 0: Upgrade PyO3 from 0.20 to 0.24 + +**Files: `python/Cargo.toml`** + +### Rationale + +PyO3 0.20 uses the old `gil-refs` API (`&PyAny`, `&PyList`, `.into_py(py)`). Version 0.23 introduced the modern `Bound` API and `IntoPyObject`, and 0.24 stabilized it. The gap between 0.20 and 0.24 is the minimum jump that gets us onto the modern API. + +**We choose 0.24 (not 0.29) because:** +- 0.24 MSRV is 1.63 — dashu's MSRV of 1.68 is preserved +- 0.26+ bumps MSRV to 1.74; 0.28+ to 1.83 — both break the project's hard MSRV constraint +- 0.26+ drops Python 3.7 support — potential compatibility concern +- 0.24 has the full `Bound` API, `IntoPyObject`, and stable `#[pymodule]` single-param form + +### 0a: Update Cargo.toml + +```toml +# Change: +pyo3 = { version = "0.20", features = ["extension-module"] } +# To: +pyo3 = { version = "0.24", features = ["extension-module"] } +``` + +MSRV stays at `1.68`. + +### 0b: `#[pymodule]` signature — `lib.rs` + +```rust +// Before (0.20): +fn dashu(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + +// After (0.24): +fn dashu(m: &Bound<'_, PyModule>) -> PyResult<()> { +``` + +Remove the now-unused `_py: Python<'_>` parameter. + +### 0c: `#[pyclass]` enum — `types.rs` + +PyO3 0.22+ requires explicit `eq` for cross-type enum comparison: + +```rust +// Before: +#[pyclass] +pub enum PySign { Positive, Negative } + +// After: +#[pyclass(eq, eq_int)] +#[derive(PartialEq, Clone, Copy)] +pub enum PySign { Positive, Negative } +``` + +### 0d: `FromPyObject` for `UniInput` — `convert.rs` + +The `FromPyObject<'source>` trait changes to operate on `Bound<'py, PyAny>`: + +```rust +// Before (0.20): +impl<'source> FromPyObject<'source> for UniInput<'source> { + fn extract(ob: &'source PyAny) -> PyResult { + +// After (0.24): +impl<'py> FromPyObject<'py> for UniInput<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +``` + +All `ob.is_instance_of::()` calls stay the same — `Bound<'py, PyAny>` supports them. The `PyRef` extraction pattern also stays: ` as FromPyObject>::extract_bound(ob)`. + +For the slow path (decimal.Decimal, fractions.Fraction), `ob.py()` still works on `&Bound<'py, PyAny>`. + +### 0e: `.into_py(py)` → `.into_pyobject(py)` — all files + +PyO3 0.23 replaced `IntoPy` with `IntoPyObject`. The new trait returns `PyResult`: + +```rust +// Before (0.20): +UPy(value).into_py(py) + +// After (0.24): +UPy(value).into_pyobject(py) +// Returns Bound<'py, PyAny> — can use .unbind() to get Py (= PyObject) +// Or use .into_py_any(py)? to get PyResult> +``` + +For functions that currently return `PyObject`, convert to `PyResult` and use: + +```rust +// Before: +fn upy_add(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyObject { + // ... + UPy(result).into_py(py) +} + +// After: +fn upy_add(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyResult { + // ... + Ok(UPy(result).into_py_any(py)?) +} +``` + +`into_py_any(py)` is the shortest path from value to `PyResult>` (= `PyResult`). + +For `__repr__`, `__str__` returning `String` — no change needed. For `__hash__` returning `u64` — no change needed. PyO3 handles those primitive types natively. + +### 0f: `#[pymethods]` return type for `__new__` + +`__new__` already returns `PyResult` — no change needed in 0.24. + +### 0g: `intern!` macro + +The macro now returns `&Bound<'py, PyString>` instead of `&'py PyString`. Usage like `py.import(intern!(py, "decimal"))?` still works transparently since `Bound` derefs. No code changes needed. + +### 0h: `wrap_pyfunction!` + +No change needed — stable across 0.20→0.24. + +### Summary of API changes (0.20 → 0.24) + +| Pattern | 0.20 | 0.24 | +|---------|------|------| +| `#[pymodule]` | `fn(py, m: &PyModule)` | `fn(m: &Bound)` | +| `FromPyObject` | `extract(ob: &'source PyAny)` | `extract_bound(ob: &Bound<'py, PyAny>)` | +| Value→Python | `.into_py(py)` | `.into_py_any(py)?` or `.into_pyobject(py)` | +| `#[pyclass]` enum | `#[pyclass]` | `#[pyclass(eq, eq_int)]` | +| `extension-module` | required | still supported in 0.24 | + +--- + ## Step 1: Fix `todo!()` panics in existing code **File: `python/src/int.rs`** @@ -214,7 +342,8 @@ fn __bool__(&self) -> bool { ### 3c: Arithmetic macro -Create a helper macro that uses `into_fpy()` for forward/reverse dispatch: +Create a helper macro that uses `into_fpy()` for forward/reverse dispatch. +**Note:** PyO3 0.24 uses `.into_py_any(py)?` instead of `.into_py(py)`: ```rust macro_rules! impl_fpy_binops { @@ -222,7 +351,7 @@ macro_rules! impl_fpy_binops { ($method:ident, $rs_method:ident) => { fn $method(lhs: &FPy, rhs: UniInput<'_>, py: Python) -> PyResult { let rhs = rhs.into_fpy()?; - Ok(FPy((&lhs.0).$rs_method(&rhs.0)).into_py(py)) + Ok(FPy((&lhs.0).$rs_method(&rhs.0)).into_py_any(py)?) } }; // Non-commutative (sub, div, rem) — also generate reverse function @@ -230,7 +359,7 @@ macro_rules! impl_fpy_binops { impl_fpy_binops!($method, $rs_method); fn $rev_method(lhs: UniInput<'_>, rhs: &FPy, py: Python) -> PyResult { let lhs = lhs.into_fpy()?; - Ok(FPy(lhs.0.$rs_method(&rhs.0)).into_py(py)) + Ok(FPy(lhs.0.$rs_method(&rhs.0)).into_py_any(py)?) } }; } @@ -283,14 +412,14 @@ macro_rules! impl_rpy_binops { ($method:ident, $rs_method:ident) => { fn $method(lhs: &RPy, rhs: UniInput<'_>, py: Python) -> PyResult { let rhs = rhs.into_rpy()?; - Ok(RPy((&lhs.0).$rs_method(&rhs.0)).into_py(py)) + Ok(RPy((&lhs.0).$rs_method(&rhs.0)).into_py_any(py)?) } }; ($method:ident, $rev_method:ident, $rs_method:ident) => { impl_rpy_binops!($method, $rs_method); fn $rev_method(lhs: UniInput<'_>, rhs: &RPy, py: Python) -> PyResult { let lhs = lhs.into_rpy()?; - Ok(RPy(lhs.0.$rs_method(&rhs.0)).into_py(py)) + Ok(RPy(lhs.0.$rs_method(&rhs.0)).into_py_any(py)?) } }; } From 58410132b93834c0834293e4401868ff04d1c0dc Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Sun, 28 Jun 2026 16:38:58 +0800 Subject: [PATCH 03/20] Clarify MSRV policy and update plan to PyO3 0.29 AGENTS.md: Clarify that MSRV policy applies only to core crates (dashu meta-crate and its dependencies: base, int, float, ratio, macros). Secondary crates (dashu-python, benchmark, fuzz tests) are exempt. TODO-python.md: Update Step 0 to target PyO3 0.29 instead of 0.24. Since dashu-python is not bounded by the workspace MSRV, we can use the latest PyO3. Covers all breaking changes through 0.29: - FromPyObject with Borrowed and dual lifetimes (0.27) - .downcast() -> .cast() (0.27) - extension-module feature removed (0.28+) - rust-version bumped to 1.83, requires-python >= 3.8 Co-Authored-By: Claude --- AGENTS.md | 6 ++- python/TODO-python.md | 118 +++++++++++++++++++++++++++--------------- 2 files changed, 81 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a8b4fb21..0ca33cb4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,9 @@ dashu is a library set of arbitrary precision numbers implemented in pure Rust, aiming to be a Rust-native alternative to GNU GMP + MPFR. -**MSRV is a hard constraint** — do not bump it unless absolutely necessary. The current MSRV is maintained in `README.md`; when modifying code, ensure it remains compatible. +**MSRV is a hard constraint for core crates only.** Core crates are the `dashu` meta-crate and its direct dependencies: `dashu-base`, `dashu-int`, `dashu-float`, `dashu-ratio`, `dashu-macros`. The current MSRV is maintained in each crate's `Cargo.toml` and the top-level `README.md`. When modifying code in core crates, ensure it remains MSRV-compatible. + +Secondary crates (`dashu-python`, `benchmark/`, fuzz tests) are **not** bounded by the workspace MSRV policy. They may use newer Rust versions and dependency versions as needed. ## Workspace structure @@ -90,5 +92,5 @@ When implementing algorithms that manipulate word arrays (`&[Word]`), prefer the - **dashu-python is excluded** from workspace tests and clippy — always add `--exclude dashu-python` - **diesel has two major versions** in the dependency tree — use `diesel@2` (not `diesel` or `diesel@2.x.y`) when pinning in CI -- **MSRV compatibility** — if you add a new dependency, check whether it supports the current MSRV; if not, it may need to be stripped for MSRV builds +- **MSRV compatibility** — for core crates only: if you add a new dependency to a core crate, check whether it supports the current MSRV; if not, it may need to be stripped for MSRV builds. Secondary crates (dashu-python, benchmarks, fuzz tests) are exempt from this check. - **Sub-crate versions can differ** in minor/patch (e.g. `dashu-int` 0.4.2, `dashu-float` 0.4.4) — keep them in sync when making cross-crate changes diff --git a/python/TODO-python.md b/python/TODO-python.md index c71cac5f..35a52d82 100644 --- a/python/TODO-python.md +++ b/python/TODO-python.md @@ -72,44 +72,60 @@ Approximation::value() extracts T from both Exact(T) and Inexact(T, E) variants. --- -## Step 0: Upgrade PyO3 from 0.20 to 0.24 +## Step 0: Upgrade PyO3 from 0.20 to 0.29 -**Files: `python/Cargo.toml`** +**Files: `python/Cargo.toml`, `python/pyproject.toml`** ### Rationale -PyO3 0.20 uses the old `gil-refs` API (`&PyAny`, `&PyList`, `.into_py(py)`). Version 0.23 introduced the modern `Bound` API and `IntoPyObject`, and 0.24 stabilized it. The gap between 0.20 and 0.24 is the minimum jump that gets us onto the modern API. - -**We choose 0.24 (not 0.29) because:** -- 0.24 MSRV is 1.63 — dashu's MSRV of 1.68 is preserved -- 0.26+ bumps MSRV to 1.74; 0.28+ to 1.83 — both break the project's hard MSRV constraint -- 0.26+ drops Python 3.7 support — potential compatibility concern -- 0.24 has the full `Bound` API, `IntoPyObject`, and stable `#[pymodule]` single-param form +PyO3 0.20 uses the old `gil-refs` API (`&PyAny`, `&PyList`, `.into_py(py)`). Version 0.23 introduced the modern `Bound` API and `IntoPyObject`, and 0.29 is the current latest. The MSRV constraint does **not** apply to `dashu-python` (per AGENTS.md — only core crates are bounded by MSRV). ### 0a: Update Cargo.toml ```toml +[dependencies.pyo3] # Change: -pyo3 = { version = "0.20", features = ["extension-module"] } +version = "0.20" +features = ["extension-module"] # To: -pyo3 = { version = "0.24", features = ["extension-module"] } +version = "0.29" +# "extension-module" feature is deprecated in 0.28+ and removed in 0.29. +# maturin handles linking automatically. ``` -MSRV stays at `1.68`. +Also bump `rust-version` since PyO3 0.29 requires Rust ≥ 1.83: +```toml +# Change: +rust-version = "1.68" +# To: +rust-version = "1.83" +``` + +Remove `categories = ["mathematics", "no-std"]` — `no-std` is misleading for a Python binding. Replace with `categories = ["mathematics", "science"]`. + +### 0b: Update pyproject.toml -### 0b: `#[pymodule]` signature — `lib.rs` +PyO3 0.26+ dropped Python 3.7 support: +```toml +# Change: +requires-python = ">=3.7" +# To: +requires-python = ">=3.8" +``` + +### 0c: `#[pymodule]` signature — `lib.rs` ```rust // Before (0.20): fn dashu(_py: Python<'_>, m: &PyModule) -> PyResult<()> { -// After (0.24): +// After (0.29): fn dashu(m: &Bound<'_, PyModule>) -> PyResult<()> { ``` Remove the now-unused `_py: Python<'_>` parameter. -### 0c: `#[pyclass]` enum — `types.rs` +### 0d: `#[pyclass]` enum — `types.rs` PyO3 0.22+ requires explicit `eq` for cross-type enum comparison: @@ -124,36 +140,58 @@ pub enum PySign { Positive, Negative } pub enum PySign { Positive, Negative } ``` -### 0d: `FromPyObject` for `UniInput` — `convert.rs` +### 0e: `FromPyObject` for `UniInput` — `convert.rs` -The `FromPyObject<'source>` trait changes to operate on `Bound<'py, PyAny>`: +PyO3 0.27 restructured `FromPyObject` with dual lifetimes and `Borrowed`: ```rust // Before (0.20): impl<'source> FromPyObject<'source> for UniInput<'source> { fn extract(ob: &'source PyAny) -> PyResult { -// After (0.24): -impl<'py> FromPyObject<'py> for UniInput<'py> { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +// After (0.29): +impl<'a, 'py> FromPyObject<'a, 'py> for UniInput<'a> { + type Error = PyErr; + fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult { ``` -All `ob.is_instance_of::()` calls stay the same — `Bound<'py, PyAny>` supports them. The `PyRef` extraction pattern also stays: ` as FromPyObject>::extract_bound(ob)`. +`Borrowed<'a, 'py, PyAny>` derefs to `&Bound<'py, PyAny>`, so `ob.is_instance_of::()`, `ob.py()`, etc. all work as before. -For the slow path (decimal.Decimal, fractions.Fraction), `ob.py()` still works on `&Bound<'py, PyAny>`. +**Extracting `PyRef` from `Borrowed`:** The pattern changes from: +```rust +// 0.20: + as FromPyObject>::extract(ob) +``` +To (0.29): +```rust +ob.extract::>() +``` + +### 0f: `.cast()` replaces `.downcast()` — `int.rs`, `words.rs` + +PyO3 0.27 renamed downcast methods: -### 0e: `.into_py(py)` → `.into_pyobject(py)` — all files +```rust +// Before (0.20): +index.downcast::()? +// After (0.29): +index.cast::()? +``` + +Also: `PySlice::indices()` now expects `isize` instead of `i32` for the length parameter in some contexts. The current code passes `.try_into().map_err(...)` — this should be reviewed. + +### 0g: `.into_py(py)` → `.into_pyobject(py)` — all files PyO3 0.23 replaced `IntoPy` with `IntoPyObject`. The new trait returns `PyResult`: ```rust // Before (0.20): UPy(value).into_py(py) - -// After (0.24): +// After (0.29): UPy(value).into_pyobject(py) -// Returns Bound<'py, PyAny> — can use .unbind() to get Py (= PyObject) -// Or use .into_py_any(py)? to get PyResult> +// Returns PyResult> +// Use .unbind() to get Py (= PyObject): +UPy(value).into_pyobject(py).map(|b| b.unbind()) ``` For functions that currently return `PyObject`, convert to `PyResult` and use: @@ -164,7 +202,6 @@ fn upy_add(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyObject { // ... UPy(result).into_py(py) } - // After: fn upy_add(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyResult { // ... @@ -172,31 +209,30 @@ fn upy_add(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyResult { } ``` -`into_py_any(py)` is the shortest path from value to `PyResult>` (= `PyResult`). +`into_py_any(py)` is shorthand for `.into_pyobject(py).map(|b| b.unbind())` — it returns `PyResult>` (= `PyResult`). For `__repr__`, `__str__` returning `String` — no change needed. For `__hash__` returning `u64` — no change needed. PyO3 handles those primitive types natively. -### 0f: `#[pymethods]` return type for `__new__` - -`__new__` already returns `PyResult` — no change needed in 0.24. - -### 0g: `intern!` macro +### 0h: `intern!` macro -The macro now returns `&Bound<'py, PyString>` instead of `&'py PyString`. Usage like `py.import(intern!(py, "decimal"))?` still works transparently since `Bound` derefs. No code changes needed. +The macro returns `&Bound<'py, PyString>`. Usage like `py.import(intern!(py, "decimal"))?` works transparently (Bound derefs). No code changes needed. -### 0h: `wrap_pyfunction!` +### 0i: `wrap_pyfunction!` -No change needed — stable across 0.20→0.24. +No change needed — stable across 0.20→0.29. -### Summary of API changes (0.20 → 0.24) +### Summary of API changes (0.20 → 0.29) -| Pattern | 0.20 | 0.24 | +| Pattern | 0.20 | 0.29 | |---------|------|------| | `#[pymodule]` | `fn(py, m: &PyModule)` | `fn(m: &Bound)` | -| `FromPyObject` | `extract(ob: &'source PyAny)` | `extract_bound(ob: &Bound<'py, PyAny>)` | +| `FromPyObject` | `extract(ob: &'source PyAny)` | `extract(ob: Borrowed<'a, 'py, PyAny>)` with `type Error` | +| `.downcast::()` | present | `.cast::()` | | Value→Python | `.into_py(py)` | `.into_py_any(py)?` or `.into_pyobject(py)` | | `#[pyclass]` enum | `#[pyclass]` | `#[pyclass(eq, eq_int)]` | -| `extension-module` | required | still supported in 0.24 | +| `extension-module` | required in features | removed (maturin handles it) | +| rust-version | 1.68 | 1.83 (PyO3 0.29 MSRV) | +| requires-python | ≥3.7 | ≥3.8 (Python 3.7 dropped in 0.26+) | --- From c6062c42cd9bf0364b3fdfa7da2aa60cd2bdd6a0 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Sun, 28 Jun 2026 16:41:59 +0800 Subject: [PATCH 04/20] Bump dashu-python to edition 2024 and rust-version 1.85 Edition 2024 requires Rust >= 1.85. Co-Authored-By: Claude --- python/Cargo.toml | 4 ++-- python/TODO-python.md | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/python/Cargo.toml b/python/Cargo.toml index b9bbd5dc..de298126 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -2,7 +2,7 @@ name = "dashu-python" version = "0.0.1" authors = ["Jacob Zhong "] -edition = "2021" +edition = "2024" description = "Python binding for the dashu numeric types" keywords = ["mathematics", "numerics"] categories = ["mathematics", "no-std"] @@ -11,7 +11,7 @@ repository = "https://github.com/cmpute/dashu" homepage = "https://github.com/cmpute/dashu" documentation = "https://docs.rs/dashu-python" readme = "README.md" -rust-version = "1.68" +rust-version = "1.85" [package.metadata.docs.rs] all-features = true diff --git a/python/TODO-python.md b/python/TODO-python.md index 35a52d82..57d97cd1 100644 --- a/python/TODO-python.md +++ b/python/TODO-python.md @@ -93,12 +93,14 @@ version = "0.29" # maturin handles linking automatically. ``` -Also bump `rust-version` since PyO3 0.29 requires Rust ≥ 1.83: +Also bump `rust-version` and `edition`: ```toml # Change: rust-version = "1.68" +edition = "2021" # To: -rust-version = "1.83" +rust-version = "1.85" +edition = "2024" ``` Remove `categories = ["mathematics", "no-std"]` — `no-std` is misleading for a Python binding. Replace with `categories = ["mathematics", "science"]`. From 9b5e9d5061fe91bdc6d26b77ffe91a07cf13ca08 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Sun, 28 Jun 2026 16:48:40 +0800 Subject: [PATCH 05/20] Fix MSRV 1.68 CI: exclude dashu-python from workspace before cargo parsing dashu-python uses edition 2024 which requires Rust >= 1.85. When the CI runs with Rust 1.68, cargo fails to parse python/Cargo.toml even for workspace-level commands like 'cargo update'. The fix removes the python member from the workspace list via sed before any cargo commands run in the 1.68 job. Co-Authored-By: Claude --- .github/workflows/tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4713e7cf..d2deab47 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,9 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} + - name: Remove dashu-python from workspace (requires Rust >= 1.85) + if: matrix.rust == '1.68' + run: sed -i '/^ *"python",$/d' Cargo.toml - name: Drop postgres/diesel/dev-deps for Rust 1.68 if: matrix.rust == '1.68' run: python3 .github/workflows/drop_incompatible_deps_for_msrv.py @@ -47,7 +50,7 @@ jobs: fi - run: cargo check --all-features --tests if: matrix.rust != '1.68' - - run: cargo check --workspace --exclude dashu-python --features "std,num-order,serde,zeroize,rand,num-traits_v02" + - run: cargo check --workspace --features "std,num-order,serde,zeroize,rand,num-traits_v02" if: matrix.rust == '1.68' test: From 06ddb498122209b370d0cc690916f31c2b6705ef Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Sun, 28 Jun 2026 17:02:28 +0800 Subject: [PATCH 06/20] Fix CI: remove parking_lot/lock_api pinning (only in dashu-python tree) parking_lot and lock_api are transitive deps of PyO3, which is only used by dashu-python. Since dashu-python is removed from the workspace in the 1.68 MSRV job, these packages are no longer in the dependency tree and cargo update fails trying to pin them. Also apply cargo fmt. Co-Authored-By: Claude --- .github/workflows/tests.yml | 4 ---- python/src/convert.rs | 8 ++------ python/src/int.rs | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d2deab47..6b0f2986 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,13 +33,9 @@ jobs: run: | if [ "${{ matrix.rust }}" = "1.68" ]; then echo "Pinning packages for Rust 1.68:" - echo " parking_lot -> 0.12.3" - echo " lock_api -> 0.4.12" echo " quote -> 1.0.40" echo " unicode-ident -> 1.0.13" echo " zeroize -> 1.8.1" - cargo update -p parking_lot --precise 0.12.3 - cargo update -p lock_api --precise 0.4.12 cargo update -p quote --precise 1.0.40 cargo update -p unicode-ident --precise 1.0.13 cargo update -p zeroize --precise 1.8.1 diff --git a/python/src/convert.rs b/python/src/convert.rs index 99626312..3407ebfb 100644 --- a/python/src/convert.rs +++ b/python/src/convert.rs @@ -4,11 +4,11 @@ //! but it should happen when both PyO3 and this crate have a relatively stable API. use pyo3::{ + FromPyObject, PyAny, PyErr, PyObject, exceptions::{PySyntaxError, PyTypeError, PyValueError}, ffi, intern, prelude::*, types::{PyBytes, PyDict, PyFloat, PyLong}, - FromPyObject, PyAny, PyErr, PyObject, }; use std::os::raw::{c_double, c_longlong}; use std::str::FromStr; @@ -33,11 +33,7 @@ pub fn parse_signed_index(index: isize, length: usize, unlimited: bool) -> Optio } } else { let i = index.unsigned_abs(); - if i <= length { - Some(length - i) - } else { - None - } + if i <= length { Some(length - i) } else { None } } } diff --git a/python/src/int.rs b/python/src/int.rs index be0c97a7..5812e4c3 100644 --- a/python/src/int.rs +++ b/python/src/int.rs @@ -19,7 +19,7 @@ use crate::{ }; use dashu_base::{Abs, BitTest, Sign, Signed, UnsignedAbs}; -use dashu_int::{fast_div, IBig, UBig, Word}; +use dashu_int::{IBig, UBig, Word, fast_div}; use num_order::{NumHash, NumOrd}; type FBig = dashu_float::FBig; From 74cf2301b7b0ce86ac649b0e9c1ff94f6ebf899d Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Tue, 30 Jun 2026 11:23:23 +0800 Subject: [PATCH 07/20] Add dashu-cmplx support and complex bindings plan AGENTS.md: - Add dashu-cmplx to core crates list and workspace structure table - Fix dashu_ratio -> dashu-ratio typo TODO-python.md: - Add dashu-cmplx dependency to Step 0a (Cargo.toml) - Add Step 11: CBig -> CPy complex number bindings - Update Notes for edition 2024, Rust 1.85, and complex crate Co-Authored-By: Claude --- AGENTS.md | 3 +- python/TODO-python.md | 108 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0ca33cb4..e66cb9f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ dashu is a library set of arbitrary precision numbers implemented in pure Rust, aiming to be a Rust-native alternative to GNU GMP + MPFR. -**MSRV is a hard constraint for core crates only.** Core crates are the `dashu` meta-crate and its direct dependencies: `dashu-base`, `dashu-int`, `dashu-float`, `dashu-ratio`, `dashu-macros`. The current MSRV is maintained in each crate's `Cargo.toml` and the top-level `README.md`. When modifying code in core crates, ensure it remains MSRV-compatible. +**MSRV is a hard constraint for core crates only.** Core crates are the `dashu` meta-crate and its direct dependencies: `dashu-base`, `dashu-int`, `dashu-float`, `dashu-ratio`, `dashu-macros`, `dashu-cmplx`. The current MSRV is maintained in each crate's `Cargo.toml` and the top-level `README.md`. When modifying code in core crates, ensure it remains MSRV-compatible. Secondary crates (`dashu-python`, `benchmark/`, fuzz tests) are **not** bounded by the workspace MSRV policy. They may use newer Rust versions and dependency versions as needed. @@ -15,6 +15,7 @@ Secondary crates (`dashu-python`, `benchmark/`, fuzz tests) are **not** bounded | `dashu-float` | `float/` | Arbitrary precision floats (`FBig`, `DBig`, `CachedFBig`) | | `dashu-ratio` | `rational/` | Arbitrary precision rationals (`RBig`, `Relaxed`) | | `dashu-macros` | `macros/` | Procedural macros for literal big numbers | +| `dashu-cmplx` | `complex/` | Arbitrary precision complex numbers (`CBig`) | | `dashu-python` | `python/` | PyO3 Python bindings (not in default members) | | *(benchmark)* | `benchmark/` | Profiling scratchpad, not a comprehensive benchmark suite | diff --git a/python/TODO-python.md b/python/TODO-python.md index 57d97cd1..520944e4 100644 --- a/python/TODO-python.md +++ b/python/TODO-python.md @@ -80,7 +80,13 @@ Approximation::value() extracts T from both Exact(T) and Inexact(T, E) variants. PyO3 0.20 uses the old `gil-refs` API (`&PyAny`, `&PyList`, `.into_py(py)`). Version 0.23 introduced the modern `Bound` API and `IntoPyObject`, and 0.29 is the current latest. The MSRV constraint does **not** apply to `dashu-python` (per AGENTS.md — only core crates are bounded by MSRV). -### 0a: Update Cargo.toml +### 0a: Update Cargo.toml — dependencies and metadata + +```toml +[dependencies] +# Add complex numbers support: +dashu-cmplx = { version = "0.4.5", path = "../complex", features = ["std", "num-order"] } +``` ```toml [dependencies.pyo3] @@ -1117,10 +1123,107 @@ class Sign: 8. Step 8 (format fix) — independent 9. Step 9 (stubs) — after all code changes 10. Step 10 (tests) — after all code changes +11. Step 11 (complex bindings) — after Step 3 (follows same pattern) ``` Steps 3 and 5 can be done in parallel. Steps 3 and 4 share the same pattern so do them together. +--- + +## Step 11: Complex number bindings (CBig → CPy) + +**New dependency:** `dashu-cmplx` (added to `Cargo.toml` in Step 0a). + +The `dashu-cmplx` crate (merged from develop, commit `6e21a65`) provides `CBig` — an arbitrary-precision complex number. For Python bindings we use `CBig` (base-2, round-toward-zero) matching `FPy`. + +### 11a: Add `CPy` wrapper type — `types.rs` + +```rust +#[derive(Clone)] +#[pyclass(name = "CBig")] +pub struct CPy(pub dashu_cmplx::CBig); +``` + +Also add `BComplex(PyRef<'a, CPy>)` variant to `UniInput`. + +### 11b: Register in `lib.rs` + +```rust +m.add_class::()?; +``` + +### 11c: CBig constructor — new file `python/src/complex.rs` + +```rust +#[pymethods] +impl CPy { + #[new] + fn __new__(ob: &PyAny) -> PyResult { + // Python complex → CBig (exact, try_from) + // string "a+bi" → CBig (FromStr) + // (real, imag) tuple → CBig::from_parts + // FPy/DPy copy → CBig::from + } +} +``` + +CBig provides: +- `FromStr` for algebraic `"a+bi"` format +- `From`, `From`, `From` for embedding reals +- `CBig::from_parts(re: FBig, im: FBig)` + +### 11d: Arithmetic operators + +Same macro pattern as FPy/DPy/RPy. CBig has full operator implementations for `+`, `-`, `*`, `/` (all 4 ref/val combos plus Assign forms), and `Neg`. Add an `into_cpy()` method to `UniInput`. + +### 11e: Accessors, predicates, math + +```rust +// Accessors +fn real(&self) -> FPy { FPy(FBig::from(self.0.re().clone())) } +fn imag(&self) -> FPy { FPy(FBig::from(self.0.im().clone())) } + +// Predicates +fn is_zero(&self) -> bool { self.0.is_zero() } +fn is_finite(&self) -> bool { self.0.is_finite() } +fn is_infinite(&self) -> bool { self.0.is_infinite() } + +// Complex-specific +fn conj(&self) -> Self { CPy(self.0.conj()) } +fn proj(&self) -> Self { CPy(self.0.proj()) } +fn abs(&self) -> FPy { FPy(self.0.abs()) } // modulus +fn arg(&self) -> FPy { FPy(self.0.arg()) } // phase +fn norm(&self) -> FPy { FPy(self.0.norm()) } // squared modulus + +// Math (same set as FPy, but complex-aware) +fn sin(&self) -> Self { CPy(self.0.sin()) } +fn cos(&self) -> Self { CPy(self.0.cos()) } +fn tan(&self) -> Self { CPy(self.0.tan()) } +fn asin(&self) -> Self { CPy(self.0.asin()) } +fn acos(&self) -> Self { CPy(self.0.acos()) } +fn atan(&self) -> Self { CPy(self.0.atan()) } +fn exp(&self) -> Self { CPy(self.0.exp()) } +fn ln(&self) -> Self { CPy(self.0.ln()) } +fn sqrt(&self) -> Self { CPy(self.0.sqrt()) } +fn sqr(&self) -> Self { CPy(self.0.sqr()) } +fn powi(&self, exp: &IPy) -> Self { CPy(self.0.powi(exp.0.clone())) } +fn powf(&self, w: &Self) -> Self { CPy(self.0.powf(&w.0)) } +``` + +### 11f: Conversion to Python `complex` + +```rust +fn __complex__(&self) -> PyResult { + let re: f64 = self.0.re().clone().try_into().map_err(conversion_error_to_py)?; + let im: f64 = self.0.im().clone().try_into().map_err(conversion_error_to_py)?; + Ok(complex { re, im }) +} +``` + +### 11g: Add to math module + +Module-level functions in `math.rs` that delegate to `CPy` methods, matching the pattern for `FPy`. + ## Verification ```bash @@ -1150,6 +1253,7 @@ python -m pytest python/tests/ -v ## Notes -- **MSRV**: Preserve Rust 1.68. PyO3 0.20 is compatible. +- **MSRV**: dashu-python is exempt from the workspace MSRV policy (per AGENTS.md). Uses Rust 1.85 and edition 2024. - **Feature flags**: `num-modular` is used by `__pow__` but listed as optional — fix to hard dependency or wire as default feature. +- **Complex crate**: `dashu-cmplx` (merged from develop, `6e21a65`) is a new core crate with `CBig` type. Python bindings added as Step 11. - **Changelog**: Document all changes in `python/CHANGELOG.md` under `## Unreleased` → `### Add`. From f13d06cf7fa4b22bf16b12c6915a610e17ee5da2 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Tue, 30 Jun 2026 12:03:27 +0800 Subject: [PATCH 08/20] Fix merge: use develop's split CI layout (build.yml + tests.yml) Restore develop's tests.yml verbatim (check job was moved to build.yml). Apply dashu-python fixes to build.yml: - Remove dashu-python from workspace before cargo commands (Rust 1.68) - Drop parking_lot/lock_api pinning (only in dashu-python dep tree) - Drop --exclude dashu-python (no longer a workspace member after sed) Co-Authored-By: Claude --- .github/workflows/build.yml | 9 +++--- .github/workflows/tests.yml | 64 ------------------------------------- 2 files changed, 4 insertions(+), 69 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6029389f..5cbecf18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,9 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} + - name: Remove dashu-python from workspace (requires Rust >= 1.85) + if: matrix.rust == '1.68' + run: sed -i '/^ *"python",$/d' Cargo.toml - name: Drop postgres/diesel/dev-deps for Rust 1.68 if: matrix.rust == '1.68' run: python3 .github/workflows/drop_incompatible_deps_for_msrv.py @@ -42,13 +45,9 @@ jobs: run: | if [ "${{ matrix.rust }}" = "1.68" ]; then echo "Pinning packages for Rust 1.68:" - echo " parking_lot -> 0.12.3" - echo " lock_api -> 0.4.12" echo " quote -> 1.0.40" echo " unicode-ident -> 1.0.13" echo " zeroize -> 1.8.1" - cargo update -p parking_lot --precise 0.12.3 - cargo update -p lock_api --precise 0.4.12 cargo update -p quote --precise 1.0.40 cargo update -p unicode-ident --precise 1.0.13 cargo update -p zeroize --precise 1.8.1 @@ -59,7 +58,7 @@ jobs: fi - run: cargo check --all-features --tests if: matrix.rust != '1.68' - - run: cargo check --workspace --exclude dashu-python --features "std,num-order,serde,zeroize,rand,num-traits_v02" + - run: cargo check --workspace --features "std,num-order,serde,zeroize,rand,num-traits_v02" if: matrix.rust == '1.68' rustdoc: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6b0f2986..a8979b56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,44 +11,6 @@ on: name: Tests jobs: - check: - name: Check - runs-on: ubuntu-latest - strategy: - matrix: - rust: [stable, "1.85", "1.68"] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ matrix.rust }} - - name: Remove dashu-python from workspace (requires Rust >= 1.85) - if: matrix.rust == '1.68' - run: sed -i '/^ *"python",$/d' Cargo.toml - - name: Drop postgres/diesel/dev-deps for Rust 1.68 - if: matrix.rust == '1.68' - run: python3 .github/workflows/drop_incompatible_deps_for_msrv.py - - name: Pin transitive deps for MSRV - if: matrix.rust != 'stable' - run: | - if [ "${{ matrix.rust }}" = "1.68" ]; then - echo "Pinning packages for Rust 1.68:" - echo " quote -> 1.0.40" - echo " unicode-ident -> 1.0.13" - echo " zeroize -> 1.8.1" - cargo update -p quote --precise 1.0.40 - cargo update -p unicode-ident --precise 1.0.13 - cargo update -p zeroize --precise 1.8.1 - elif [ "${{ matrix.rust }}" = "1.85" ]; then - echo "Pinning packages for Rust 1.85:" - echo " diesel -> 2.2.12" - cargo update -p diesel@2 --precise 2.2.12 - fi - - run: cargo check --all-features --tests - if: matrix.rust != '1.68' - - run: cargo check --workspace --features "std,num-order,serde,zeroize,rand,num-traits_v02" - if: matrix.rust == '1.68' - test: name: Test strategy: @@ -119,32 +81,6 @@ jobs: toolchain: stable - run: cargo test --no-default-features --features rand --workspace --exclude dashu-python - build-benchmark: - name: Build benchmark - runs-on: ubuntu-latest - env: - RUSTFLAGS: -D warnings - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - - run: cargo build --features gmp - working-directory: benchmark - - build-aarch64: - name: Build aarch64 - runs-on: ubuntu-latest - env: - RUSTFLAGS: -D warnings - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - targets: aarch64-unknown-linux-gnu - - run: cargo build --target aarch64-unknown-linux-gnu --all-features --workspace --exclude dashu-python - fmt: name: Rustfmt runs-on: ubuntu-latest From 9ea9a9cd81f8e77b24f34facccdb2b90b1e452b6 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Wed, 8 Jul 2026 10:23:30 +0800 Subject: [PATCH 09/20] Switch Python float/complex bindings to cached types FPy/DPy/CPy now wrap CachedFBig/CachedCBig (memoize pi/ln2/ln10 via ConstCache) instead of bare FBig/CBig. Rationals/integers stay uncached. Key consequences documented: - CachedFBig/CachedCBig are !Send + !Sync (Rc>), so FPy/DPy/CPy must use #[pyclass(unsendable)] -- incompatible with free-threaded Python (no-GIL, PEP 703). - UPy/IPy/RPy keep default Send #[pyclass] (no transcendental ops). - Conversion helpers (into_fpy/dpy/cpy) build cached wrappers via From/FromStr/TryFrom (each attaches a fresh cache). - Methods not yet mirrored on the cached types (trunc/floor/ceil/fract/ round/split_at_point/with_base/to_decimal/nth_root/hypot) delegate through .as_fbig()/.as_cbig() and wrap back with a fresh cache. - Cache-sharing rule: binary ops preserve LHS cache; fresh-cache ops are acceptable where no constant is needed. Updates Architecture, Step 0d2 (wrapper types), Step 2 (convert.rs helpers), Step 6 (predicates/rounding delegation), Step 11 (CPy), smoke test, and Notes. Co-Authored-By: Claude --- python/TODO-python.md | 263 ++++++++++++++++++++++++++++-------------- 1 file changed, 179 insertions(+), 84 deletions(-) diff --git a/python/TODO-python.md b/python/TODO-python.md index 520944e4..6dd1fc8d 100644 --- a/python/TODO-python.md +++ b/python/TODO-python.md @@ -6,27 +6,72 @@ This document is the concrete implementation plan for the dashu Python bindings. ## Architecture +### Use cached types for floats and complex numbers + +**Decision: `FPy`, `DPy`, and `CPy` wrap the *cached* variants — `CachedFBig`, `CachedFBig`, and `CachedCBig` — not the bare `FBig`/`CBig`.** + +Rationals (`RPy`→`RBig`) and integers (`UPy`/`IPy`→`UBig`/`IBig`) stay uncached — they have no transcendental operations, so the `ConstCache` (which memoizes π, ln2, ln10 via binary-splitting) gives them nothing. + +| Python type | Rust inner type | Cached? | +|---|---|---| +| `UPy` | `UBig` | no | +| `IPy` | `IBig` | no | +| `RPy` | `RBig` | no | +| `FPy` | `CachedFBig` | **yes** | +| `DPy` | `CachedFBig` (= `FastDecimal`) | **yes** | +| `CPy` | `CachedCBig` (= `FastComplex`) | **yes** | + +**Why cache:** every `sin`/`cos`/`exp`/`ln` call on a bare `FBig` recomputes π from scratch (O(N²)). The cache stores the binary-splitting tree state, so repeated high-precision transcendental calls become incremental. This is the dominant cost for a numerical-computing library, so it's worth the thread-safety trade-off below. + +**Thread-safety consequence (important):** `CachedFBig` and `CachedCBig` hold `Rc>`, making them **`!Send + !Sync`**. Therefore `FPy`/`DPy`/`CPy` **must** use `#[pyclass(unsendable)]`: +- Objects can only be accessed from the thread that created them. +- This is fine for standard GIL-based Python (all access is serialized by the GIL anyway), but **incompatible with free-threaded Python (no-GIL, PEP 703)** and with passing these objects across thread boundaries (e.g. into a Rust worker thread). +- `UPy`/`IPy`/`RPy` keep default (Send) `#[pyclass]` — they're unaffected. + +**Cache-sharing rule (from the Rust API):** binary ops preserve the LHS's cache handle (`Rc::clone`); when a bare value (e.g. an `FBig` from an int conversion) meets a cached value, the cached side's handle wins. So `a + b` where both are cached keeps a single shared cache — good. Operations that return a *new* constant-free value (truncation, base conversion) go through `as_fbig()` and attach a fresh cache (acceptable — they don't need π). + +**Missing-method delegation:** `CachedFBig`/`CachedCBig` do **not** yet mirror every `FBig`/`CBig` method (notably `trunc`/`floor`/`ceil`/`fract`/`round`/`split_at_point`/`with_base`/`to_decimal`/`nth_root`/`hypot`). For those, delegate through the inner value: `self.0.as_fbig().trunc()` then wrap the result back via `CachedFBig::from(...)`. Per AGENTS.md, the cached wrappers are *intended* to mirror the full surface eventually — prefer pushing the missing method upstream into the cached type rather than working around it permanently. + ### Key patterns to follow -**UniInput dispatch** — `python/src/convert.rs`: The `UniInput` enum has 11 variants covering all Python numeric types (Uint, Int, BUint, BInt, OBInt, Float, BFloat, BDecimal, OBDecimal, BRational, OBRational). Its `FromPyObject` impl already extracts any Python number into the right variant. We add three conversion methods (`into_fpy`, `into_dpy`, `into_rpy`) that collapse any variant into the target dashu type, making arithmetic functions simple one-liners. +**UniInput dispatch** — `python/src/convert.rs`: The `UniInput` enum has 11 variants covering all Python numeric types (Uint, Int, BUint, BInt, OBInt, Float, BFloat, BDecimal, OBDecimal, BRational, OBRational). Its `FromPyObject` impl already extracts any Python number into the right variant. We add conversion methods (`into_fpy`, `into_dpy`, `into_rpy`, `into_cpy`) that collapse any variant into the target dashu type, making arithmetic functions simple one-liners. -**Comparison** — `num_order::NumOrd` trait provides cross-type comparison for all dashu types: FBig↔UBig, FBig↔IBig, FBig↔RBig, FBig↔f64, RBig↔UBig, RBig↔IBig, etc. The `__richcmp__` pattern is already established in UPy/IPy: `op.matches(self.0.num_cmp(&rhs))`. +**Comparison** — `num_order::NumOrd` trait provides cross-type comparison for all dashu types: FBig↔UBig, FBig↔IBig, FBig↔RBig, FBig↔f64, RBig↔UBig, RBig↔IBig, etc. The `__richcmp__` pattern is already established in UPy/IPy: `op.matches(self.0.num_cmp(&rhs))`. Note: `CachedFBig`/`CachedCBig` impl `NumOrd` too, but to compare a cached value against another cached value, compare via `as_fbig()`/`as_cbig()` or rely on the cached `PartialOrd` (same-base only). -**Arithmetic macros** — The existing `impl_binops!` macro in `int.rs` generates forward/reverse dispatch functions by matching each `UniInput` variant. For FPy/DPy/RPy we create simpler versions that first convert the operand to the target type via `into_fpy()`/etc, then call the Rust operator. +**Arithmetic macros** — The existing `impl_binops!` macro in `int.rs` generates forward/reverse dispatch functions by matching each `UniInput` variant. For FPy/DPy/RPy/CPy we create simpler versions that first convert the operand to the target type via `into_fpy()`/etc, then call the Rust operator. For cached types the operator preserves the cache handle automatically. ### Rust API facts (verified) ``` -FBig methods (direct, no trait import needed): - .sin(), .cos(), .tan(), .asin(), .acos(), .atan(), .atan2(&x) - .sinh(), .cosh(), .tanh(), .asinh(), .acosh(), .atanh() - .exp(), .exp_m1(), .ln(), .ln_1p() - .sqrt(), .cbrt(), .nth_root(n) - .trunc(), .floor(), .ceil(), .round(), .fract(), .signum() - .powi(IBig), .powf(&FBig) - .repr() -> &Repr (has .is_zero(), .is_finite(), .is_infinite(), .sign(), .precision(), .digits()) - -RBig methods: +CachedFBig (= FBig + Rc>; !Send + !Sync) + Construction: From, From, FromStr, TryFrom -- all attach a FRESH cache. + CachedFBig::from_parts(significand: IBig, exponent: isize) -- fresh cache. + FBig::into_cached(self, cache) -- attach a SPECIFIC shared cache handle. + Extract inner: .as_fbig() -> &FBig, .into_fbig(self) -> FBig + Arithmetic: Add/Sub/Mul/Div/Rem (assign), Neg, Abs, Shl/Shr (assign), Sum, Product -- all mirror FBig, all cache-preserving. + Cache-threaded transcendents (the whole point): ln, ln_1p, exp, exp_m1, sin, cos, tan, asin, acos, atan, + sinh, cosh, tanh, asinh, acosh, atanh, sin_cos, sinh_cosh, + atan2, powf, pi(precision, &cache). + Delegated (no cache needed): sqrt, inv, sqr, cubic, powi(IBig). + Accessors: .precision(), .digits(), .context(), .repr(), .into_repr(), .sign(), .ulp(), + .with_precision(n) -> Rounded, .with_rounding(). + Conversions out: .to_int() -> Rounded, .to_f32(), .to_f64(), TryFrom for IBig/UBig/f32/f64. + NOT YET mirrored (delegate via .as_fbig()): trunc, floor, ceil, fract, round, split_at_point, quantize, + to_decimal, to_binary, with_base, with_base_and_precision, + nth_root, hypot, cbrt(CubicRoot trait IS present though). + +CachedCBig (= CBig + Rc>; !Send + !Sync) + Construction: From, From, From, FromStr, TryFrom -- fresh cache. + CachedCBig::from_parts(re: FBig, im: FBig) -- fresh cache (note: takes bare FBig parts). + .into_parts(self) -> (CachedFBig, CachedFBig) -- INTENTIONAL divergence from CBig's (FBig,FBig). + Extract inner: .as_cbig() -> &CBig, .into_cbig(self) -> CBig. + Cache-threaded: ln, exp, sin, cos, tan, asin, acos, atan, arg, powf, sin_cos. + Delegated: sqr, sqrt, conj, proj, mul_i(negative), powi(IBig), norm()->FBig, abs()->FBig. + Accessors: .re() -> &Repr, .im() -> &Repr, .precision(), .context(), + .is_zero(), .is_infinite(), .is_finite(). + Note: CBig.abs()/arg() return FBig (the modulus/phase is real), so wrap in CachedFBig via From. + +RBig methods (uncached): .numerator() -> &IBig, .denominator() -> &UBig .trunc() -> IBig, .floor() -> IBig, .ceil() -> IBig, .round() -> IBig (NOT RBig!) .fract() -> RBig @@ -34,7 +79,7 @@ RBig methods: .sqr(), .cubic(), .pow(n: usize) .is_int(), .sign() -> Sign, .signum() -> RBig -UBig methods: +UBig methods (uncached): .sqrt() -> UBig, .cbrt() -> UBig, .nth_root(n) -> UBig .sqr() -> UBig, .cubic() -> UBig .ilog(&UBig) -> usize @@ -43,7 +88,7 @@ UBig methods: .is_power_of_two() -> bool, .next_power_of_two() -> UBig .is_multiple_of(&UBig) -> bool, .remove(&UBig) -> Option -IBig methods: +IBig methods (uncached): .sqrt() -> UBig (always unsigned!), .cbrt() -> IBig, .nth_root(n) -> IBig .sqr() -> UBig (always unsigned!), .cubic() -> IBig .ilog(&UBig) -> usize (takes &UBig, not &IBig!) @@ -59,13 +104,13 @@ Traits (need `use` imports): IBig::is_negative()/is_positive() -- NOT inherent! Use self.sign() == Sign::Negative instead. -Conversions: - FBig::try_from(f64) -- base 2 ONLY (FPy works, DBig does not) - FBig::from(UBig), FBig::from(IBig) -- infallible, any base - FBig::try_from(RBig) -- exact only, returns Err if precision loss - RBig::try_from(FBig) -- exact only +Conversions (int/float/rational): + CachedFBig::try_from(f64) -- base 2 ONLY (FPy works; DPy does not) + CachedFBig::from(UBig), CachedFBig::from(IBig) -- infallible, any base + FBig::try_from(RBig) / CachedFBig::try_from(RBig) -- exact only, returns Err on precision loss + RBig::try_from(FBig/CachedFBig) -- exact only (use .into_fbig() first if cached) RBig::try_from(f64) -- exact only (f64 has limited precision anyway) - DBig from f64 -- go through string: format!("{:e}", x) then DBig::from_str + DPy from f64 -- go through string: format!("{:e}", x) then CachedFBig::from_str Approximation::value() extracts T from both Exact(T) and Inexact(T, E) variants. ``` @@ -148,6 +193,35 @@ pub enum PySign { Positive, Negative } pub enum PySign { Positive, Negative } ``` +### 0d2: Wrapper types use cached float/complex types — `types.rs` + +Change the float/complex wrappers to the cached variants and mark them `unsendable` (see Architecture: `CachedFBig`/`CachedCBig` are `!Send + !Sync`). + +```rust +// Before: +#[pyclass(name = "FBig")] pub struct FPy(pub dashu_float::FBig); +#[pyclass(name = "DBig")] pub struct DPy(pub dashu_float::DBig); +// (CPy added in Step 11) + +// After: +type CachedFBig = dashu_float::CachedFBig; +type CachedDBig = dashu_float::CachedFBig; +type CachedCBig = dashu_cmplx::CachedCBig; + +#[pyclass(name = "FBig", unsendable)] pub struct FPy(pub CachedFBig); +#[pyclass(name = "DBig", unsendable)] pub struct DPy(pub CachedDBig); +#[pyclass(name = "CBig", unsendable)] pub struct CPy(pub CachedCBig); // Step 11 + +// Integers and rationals stay Send (no cache, no transcendental ops): +#[pyclass(name = "UBig")] pub struct UPy(pub dashu_int::UBig); // unchanged +#[pyclass(name = "IBig")] pub struct IPy(pub dashu_int::IBig); // unchanged +#[pyclass(name = "RBig")] pub struct RPy(pub dashu_ratio::RBig); // unchanged +``` + +`UPy`/`IPy`/`RPy` keep the default (Send) `#[pyclass]` — they are plain data and benefit from being freely shareable. Only the cached float/complex types need `unsendable`. + +The `UniInput` enum's `BFloat`/`BDecimal` variants change their `PyRef` target types to the new cached wrappers (the field names `BFloat(PyRef<'a, FPy>)` etc. stay the same; only the inner type behind `FPy` changes). Add `BComplex(PyRef<'a, CPy>)` for Step 11. + ### 0e: `FromPyObject` for `UniInput` — `convert.rs` PyO3 0.27 restructured `FromPyObject` with dual lifetimes and `Borrowed`: @@ -294,29 +368,33 @@ _ => return Err(PyTypeError::new_err("integer power requires a non-negative inte **File: `python/src/convert.rs`** -Add three methods to `impl<'a> UniInput<'a>` that convert any numeric variant to the target dashu type: +Add four methods to `impl<'a> UniInput<'a>` that convert any numeric variant to the target dashu type. The float/complex helpers produce the **cached** wrappers (a fresh `ConstCache` is attached by every `From`/`TryFrom`/`FromStr` on `CachedFBig`/`CachedCBig`). ### `into_fpy(self) -> PyResult` +`FPy` wraps `CachedFBig`. Conversions use the cached `From`/`TryFrom` impls (each attaches a fresh cache): + | Input variant | Conversion | |---|---| -| `Uint(x)` | `FBig::from(x)` | -| `Int(x)` | `FBig::from(IBig::from(x))` | -| `BUint(x)` | `FBig::from(x.0.clone())` | -| `BInt(x)` | `FBig::from(x.0.clone())` | -| `OBInt(x)` | `FBig::from(x)` | -| `Float(x)` | `FBig::try_from(x)` — map error to PyValueError | -| `BFloat(x)` | `x.0.clone()` | +| `Uint(x)` | `CachedFBig::from(x)` | +| `Int(x)` | `CachedFBig::from(IBig::from(x))` | +| `BUint(x)` | `CachedFBig::from(x.0.clone())` | +| `BInt(x)` | `CachedFBig::from(x.0.clone())` | +| `OBInt(x)` | `CachedFBig::from(x)` | +| `Float(x)` | `CachedFBig::try_from(x)` — map error to PyValueError | +| `BFloat(x)` | `x.0.clone()` (already a `FPy` = cached) | | `BDecimal/OBDecimal` | `Err(PyTypeError("decimal cannot be mixed with binary float; convert explicitly"))` | -| `BRational(x)` | `FBig::try_from(x.0.clone())` — map ConversionError to PyTypeError | -| `OBRational(x)` | `FBig::try_from(x)` — same | +| `BRational(x)` | `CachedFBig::try_from(x.0.clone())` — map ConversionError to PyTypeError | +| `OBRational(x)` | `CachedFBig::try_from(x)` — same | ### `into_dpy(self) -> PyResult` -Same pattern but for DBig. For `Float(x)` use `format!("{:e}", x)` → `DBig::from_str`. For `BFloat` return type error. For `BRational`/`OBRational` use `FBig::try_from` since DBig IS FBig. +`DPy` wraps `CachedFBig`. For `Float(x)` use `format!("{:e}", x)` → `CachedFBig::from_str` (no direct `TryFrom` for base 10). For `BFloat` return type error. For `BRational`/`OBRational` use `CachedFBig::try_from` (DBig is `FBig`). ### `into_rpy(self) -> PyResult` +`RPy` wraps `RBig` (uncached — rationals have no transcendental ops). + | Input variant | Conversion | |---|---| | `Uint(x)` | `RBig::from(x)` | @@ -325,16 +403,22 @@ Same pattern but for DBig. For `Float(x)` use `format!("{:e}", x)` → `DBig::fr | `BInt(x)` | `RBig::from(x.0.clone())` | | `OBInt(x)` | `RBig::from(x)` | | `Float(x)` | `RBig::try_from(x)` — map error to PyValueError | -| `BFloat(x)` | `RBig::try_from(x.0.clone())` — map ConversionError | -| `BDecimal(x)` | `RBig::try_from(x.0.clone())` — same | +| `BFloat(x)` | `RBig::try_from(x.0.as_fbig().clone())` — go through inner FBig | +| `BDecimal(x)` | `RBig::try_from(x.0.as_fbig().clone())` — same | | `OBDecimal(x)` | `RBig::try_from(x)` — same | | `BRational(x)` | `x.0.clone()` | | `OBRational(x)` | `x` | +### `into_cpy(self) -> PyResult` (Step 11) + +`CPy` wraps `CachedCBig`. `CachedCBig::From` and `From` exist; ints/floats embed as `z + 0i`. `BFloat`/`BDecimal` convert via their inner `as_fbig()`. Cross-base (FPy↔DPy) and rational→complex raise type errors for the MVP. + Add imports at top of `convert.rs`: ```rust -use crate::types::FPy; -use dashu_float::FBig; +use crate::types::{FPy, CPy}; +use dashu_float::CachedFBig; // generic; instantiate with / +use dashu_cmplx::CachedCBig; +use dashu_float::round::mode; use dashu_base::ConversionError; use std::str::FromStr; ``` @@ -733,7 +817,7 @@ Note: these are same-type-only for the MVP. Python's data model falls back to `_ **File: `python/src/float.rs`** — add to `#[pymethods] impl FPy` (and replicate for DPy): ```rust -// Predicates +// Predicates (repr-based; work the same on CachedFBig) fn is_zero(&self) -> bool { self.0.repr().is_zero() } fn is_finite(&self) -> bool { self.0.repr().is_finite() } fn is_infinite(&self) -> bool { self.0.repr().is_infinite() } @@ -746,20 +830,22 @@ fn sign(&self) -> PySign { Sign::Negative => PySign::Negative, } } +// CachedFBig::signum() is mirrored -> returns CachedFBig fn signum(&self) -> Self { FPy(self.0.signum()) } -// Rounding (all return FPy) -fn trunc(&self) -> Self { FPy(self.0.trunc()) } -fn floor(&self) -> Self { FPy(self.0.floor()) } -fn ceil(&self) -> Self { FPy(self.0.ceil()) } -fn round(&self) -> Self { FPy(self.0.round()) } -fn fract(&self) -> Self { FPy(self.0.fract()) } +// Rounding: trunc/floor/ceil/fract/round are NOT yet mirrored on CachedFBig, +// so delegate through the inner FBig and wrap back (fresh cache — these are +// pure significand/exponent ops, no constant cache needed). +fn trunc(&self) -> Self { FPy(self.0.as_fbig().trunc().into()) } +fn floor(&self) -> Self { FPy(self.0.as_fbig().floor().into()) } +fn ceil(&self) -> Self { FPy(self.0.as_fbig().ceil().into()) } +fn round(&self) -> Self { FPy(self.0.as_fbig().round().into()) } +fn fract(&self) -> Self { FPy(self.0.as_fbig().fract().into()) } -// Conversion +// Conversion (CachedFBig::to_int exists, returns Rounded) fn to_int(&self) -> PyResult { - let int: IBig = self.0.clone().try_into().map_err(|e: ConversionError| { - PyValueError::new_err(format!("cannot convert to integer: {}", e)) - })?; + let int: IBig = self.0.clone().to_int().value().try_into() + .map_err(|e: ConversionError| PyValueError::new_err(format!("cannot convert to integer: {}", e)))?; Ok(IPy(int)) } fn __int__(&self, py: Python) -> PyResult { @@ -767,21 +853,22 @@ fn __int__(&self, py: Python) -> PyResult { convert_from_ibig(&ipy.0, py) } -// Precision +// Precision (CachedFBig mirrors with_precision -> Rounded) fn with_precision(&self, precision: usize) -> Self { - let rounded = self.0.clone().with_precision(precision); - FPy(rounded.value()) + FPy(self.0.clone().with_precision(precision).value()) } fn precision(&self) -> usize { self.0.context().precision() } fn digits(&self) -> usize { self.0.digits() } -// Float-specific constructors +// Float-specific constructor (CachedFBig::from_parts attaches fresh cache) #[staticmethod] fn from_parts(significand: &IPy, exponent: isize) -> Self { - FPy(FBig::from_parts(significand.0.clone(), exponent)) + FPy(CachedFBig::from_parts(significand.0.clone(), exponent)) } ``` +**For DPy** the same methods apply — `CachedFBig` mirrors them identically. + **File: `python/src/ratio.rs`** — add to `#[pymethods] impl RPy`: ```rust @@ -1130,20 +1217,20 @@ Steps 3 and 5 can be done in parallel. Steps 3 and 4 share the same pattern so d --- -## Step 11: Complex number bindings (CBig → CPy) +## Step 11: Complex number bindings (CachedCBig → CPy) **New dependency:** `dashu-cmplx` (added to `Cargo.toml` in Step 0a). -The `dashu-cmplx` crate (merged from develop, commit `6e21a65`) provides `CBig` — an arbitrary-precision complex number. For Python bindings we use `CBig` (base-2, round-toward-zero) matching `FPy`. +Per the Architecture decision, `CPy` wraps the **cached** `CachedCBig` (not bare `CBig`). This shares the same `ConstCache` benefit as `FPy` for complex transcendentals. ### 11a: Add `CPy` wrapper type — `types.rs` +Already defined in Step 0d2: ```rust -#[derive(Clone)] -#[pyclass(name = "CBig")] -pub struct CPy(pub dashu_cmplx::CBig); +type CachedCBig = dashu_cmplx::CachedCBig; +#[pyclass(name = "CBig", unsendable)] +pub struct CPy(pub CachedCBig); ``` - Also add `BComplex(PyRef<'a, CPy>)` variant to `UniInput`. ### 11b: Register in `lib.rs` @@ -1152,50 +1239,52 @@ Also add `BComplex(PyRef<'a, CPy>)` variant to `UniInput`. m.add_class::()?; ``` -### 11c: CBig constructor — new file `python/src/complex.rs` +### 11c: CachedCBig constructor — new file `python/src/complex.rs` ```rust #[pymethods] impl CPy { #[new] fn __new__(ob: &PyAny) -> PyResult { - // Python complex → CBig (exact, try_from) - // string "a+bi" → CBig (FromStr) - // (real, imag) tuple → CBig::from_parts - // FPy/DPy copy → CBig::from + // Python complex → CachedCBig (TryFrom exact, base 2; build re+im) + // string "a+bi" → CachedCBig (FromStr; algebraic format, fresh cache) + // (real, imag) tuple of FPy → CachedCBig::from_parts(re.into_fbig(), im.into_fbig()) + // (note: from_parts takes bare FBig parts, fresh cache) + // int/float/FPy → CachedCBig::from (embed as z + 0i) } } ``` -CBig provides: -- `FromStr` for algebraic `"a+bi"` format -- `From`, `From`, `From` for embedding reals -- `CBig::from_parts(re: FBig, im: FBig)` +`CachedCBig` provides: +- `FromStr` for algebraic `"a+bi"` format (fresh cache) +- `From`, `From`, `From`, `From`, `From` (fresh cache) +- `TryFrom`, `TryFrom` (fresh cache) +- `CachedCBig::from_parts(re: FBig, im: FBig)` — takes **bare** FBig parts, attaches fresh cache ### 11d: Arithmetic operators -Same macro pattern as FPy/DPy/RPy. CBig has full operator implementations for `+`, `-`, `*`, `/` (all 4 ref/val combos plus Assign forms), and `Neg`. Add an `into_cpy()` method to `UniInput`. +Same macro pattern as FPy/DPy/RPy using `into_cpy()`. `CachedCBig` has full `Add`/`Sub`/`Mul`/`Div` (assign + 4 ref/val combos vs itself, bare `CBig`, and `FBig`), `Neg`, `Inverse`, `Sum`, `Product` — all cache-preserving. ### 11e: Accessors, predicates, math ```rust -// Accessors -fn real(&self) -> FPy { FPy(FBig::from(self.0.re().clone())) } -fn imag(&self) -> FPy { FPy(FBig::from(self.0.im().clone())) } +// Accessors (re()/im() return &Repr; wrap the FBig projection in a fresh CachedFBig) +fn real(&self) -> FPy { FPy(FBig::from(self.0.re().clone()).into()) } +fn imag(&self) -> FPy { FPy(FBig::from(self.0.im().clone()).into()) } -// Predicates +// Predicates (CachedCBig mirrors these) fn is_zero(&self) -> bool { self.0.is_zero() } fn is_finite(&self) -> bool { self.0.is_finite() } fn is_infinite(&self) -> bool { self.0.is_infinite() } -// Complex-specific +// Complex-specific (delegated ops; abs/arg/norm return bare FBig, wrap in cached) fn conj(&self) -> Self { CPy(self.0.conj()) } fn proj(&self) -> Self { CPy(self.0.proj()) } -fn abs(&self) -> FPy { FPy(self.0.abs()) } // modulus -fn arg(&self) -> FPy { FPy(self.0.arg()) } // phase -fn norm(&self) -> FPy { FPy(self.0.norm()) } // squared modulus +fn abs(&self) -> FPy { FPy(self.0.abs().into()) } // modulus (real), fresh cache +fn arg(&self) -> FPy { FPy(self.0.arg().into()) } // phase (real), cache-threaded internally +fn norm(&self) -> FPy { FPy(self.0.norm().into()) } // squared modulus (real) -// Math (same set as FPy, but complex-aware) +// Math (cache-threaded transcendentals — the whole point of CachedCBig) fn sin(&self) -> Self { CPy(self.0.sin()) } fn cos(&self) -> Self { CPy(self.0.cos()) } fn tan(&self) -> Self { CPy(self.0.tan()) } @@ -1204,19 +1293,20 @@ fn acos(&self) -> Self { CPy(self.0.acos()) } fn atan(&self) -> Self { CPy(self.0.atan()) } fn exp(&self) -> Self { CPy(self.0.exp()) } fn ln(&self) -> Self { CPy(self.0.ln()) } -fn sqrt(&self) -> Self { CPy(self.0.sqrt()) } +fn sqrt(&self) -> Self { CPy(self.0.sqrt()) } // delegated to inner CBig fn sqr(&self) -> Self { CPy(self.0.sqr()) } fn powi(&self, exp: &IPy) -> Self { CPy(self.0.powi(exp.0.clone())) } fn powf(&self, w: &Self) -> Self { CPy(self.0.powf(&w.0)) } +fn sin_cos(&self) -> (Self, Self) { let (s, c) = self.0.sin_cos(); (CPy(s), CPy(c)) } ``` ### 11f: Conversion to Python `complex` ```rust -fn __complex__(&self) -> PyResult { - let re: f64 = self.0.re().clone().try_into().map_err(conversion_error_to_py)?; - let im: f64 = self.0.im().clone().try_into().map_err(conversion_error_to_py)?; - Ok(complex { re, im }) +fn __complex__(&self) -> PyResult { + let re: f64 = self.0.as_cbig().re().clone().try_into().map_err(conversion_error_to_py)?; + let im: f64 = self.0.as_cbig().im().clone().try_into().map_err(conversion_error_to_py)?; + Ok(PyComplex::from_doubles(re, im)) } ``` @@ -1232,7 +1322,7 @@ cd python && maturin develop # Smoke test python -c " -from dashu import FBig, DBig, RBig, UBig, IBig +from dashu import FBig, DBig, RBig, UBig, IBig, CBig a = FBig('1.5') b = FBig('2.0') assert a + b == FBig('3.5') @@ -1242,6 +1332,9 @@ assert UBig(144).sqrt() == UBig(12) assert UBig(12).gcd(UBig(8)) == UBig(4) r = RBig.from_parts(IBig(1), UBig(3)) assert r * 3 == RBig.from_parts(IBig(1), UBig(1)) +# cached transcendental (the whole point of wrapping CachedFBig) +import math +assert abs(float(FBig('2').ln()) - math.log(2)) < 1e-50 # high precision print('All smoke tests passed') " @@ -1255,5 +1348,7 @@ python -m pytest python/tests/ -v - **MSRV**: dashu-python is exempt from the workspace MSRV policy (per AGENTS.md). Uses Rust 1.85 and edition 2024. - **Feature flags**: `num-modular` is used by `__pow__` but listed as optional — fix to hard dependency or wire as default feature. -- **Complex crate**: `dashu-cmplx` (merged from develop, `6e21a65`) is a new core crate with `CBig` type. Python bindings added as Step 11. +- **Complex crate**: `dashu-cmplx` (merged from develop, `6e21a65`) is a new core crate. Python bindings (Step 11) use the cached `CachedCBig` variant. +- **Cached types**: `FPy`/`DPy`/`CPy` wrap `CachedFBig`/`CachedCBig` (π/ln2/ln10 memoization for transcendentals). They are `!Send + !Sync` → `#[pyclass(unsendable)]`. Integers/rationals stay uncached and Send. See Architecture. +- **Free-threaded Python**: Because float/complex objects are `unsendable`, this binding is **not** compatible with no-GIL (PEP 703) free-threaded CPython for those types. If that becomes a requirement, the fix is an `Arc>`-backed cached variant upstream (dashu does not yet provide one). - **Changelog**: Document all changes in `python/CHANGELOG.md` under `## Unreleased` → `### Add`. From e1c7091154c5c3ec33b6cd92d32936791dde0fd2 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Wed, 8 Jul 2026 16:24:24 +0800 Subject: [PATCH 10/20] Rearchitect: bare FBig/CBig + module-global ConstCache Instead of wrapping CachedFBig/CachedCBig (which are !Send+!Sync and hide their cache behind pub(crate)), FPy/DPy/CPy now wrap bare FBig/CBig and the module owns a single thread_local ConstCache. Transcendentals route through the panic-free Context layer (ctx.sin(repr, cache) -> FpResult) and map FpError to Python exceptions: Overflow/Underflow -> signed inf/zero, InfiniteInput/OutOfDomain -> ValueError, Indeterminate -> ZeroDivisionError. This replaces the convenience-layer panics that surfaced as session-crashing pyo3_runtime.PanicException (BaseException, not Exception). Wins vs the CachedFBig plan: - All wrappers Send+Sync -> no #[pyclass(unsendable)] - Free-threaded Python (no-GIL) compatible (per-thread cache) - Clean Python exceptions on domain errors - One base-free cache serves FPy (base 2), DPy (base 10), CPy (complex) - Keep all FBig/CBig operator trait impls (no Repr surgery) Plan changes: Architecture rewrite, Step 0d2 (bare types, no unsendable), new Step 2c (cache.rs module), Step 6 transcendentals via Context, Step 11 CPy via complex Context, updated conversions/Notes. Arithmetic still uses operator impls (infinite-input/0-0 panic caveat noted as follow-up). Co-Authored-By: Claude --- python/TODO-python.md | 465 +++++++++++++++++++++++++++--------------- 1 file changed, 302 insertions(+), 163 deletions(-) diff --git a/python/TODO-python.md b/python/TODO-python.md index 6dd1fc8d..4824a688 100644 --- a/python/TODO-python.md +++ b/python/TODO-python.md @@ -6,70 +6,72 @@ This document is the concrete implementation plan for the dashu Python bindings. ## Architecture -### Use cached types for floats and complex numbers +### Global constant cache + bare FBig/CBig -**Decision: `FPy`, `DPy`, and `CPy` wrap the *cached* variants — `CachedFBig`, `CachedFBig`, and `CachedCBig` — not the bare `FBig`/`CBig`.** +**Decision: `FPy`/`DPy`/`CPy` wrap the *bare* `FBig`/`CBig`, and the module owns a single global `ConstCache`. Transcendentals borrow that cache and call the panic-free `Context` layer directly, mapping `FpError` to Python exceptions.** -Rationals (`RPy`→`RBig`) and integers (`UPy`/`IPy`→`UBig`/`IBig`) stay uncached — they have no transcendental operations, so the `ConstCache` (which memoizes π, ln2, ln10 via binary-splitting) gives them nothing. +We deliberately do **not** wrap `CachedFBig`/`CachedCBig`. Those types bundle `Rc>` inside the value, which (a) makes the wrapper `!Send + !Sync`, forcing `#[pyclass(unsendable)]` and breaking free-threaded Python, and (b) hides the cache behind a `pub(crate)` field, so external code can't reach the panic-free `Context::sin(repr, cache)` path and is stuck with the panicking convenience methods. Putting the cache in the module (not the value) sidesteps both problems. -| Python type | Rust inner type | Cached? | -|---|---|---| -| `UPy` | `UBig` | no | -| `IPy` | `IBig` | no | -| `RPy` | `RBig` | no | -| `FPy` | `CachedFBig` | **yes** | -| `DPy` | `CachedFBig` (= `FastDecimal`) | **yes** | -| `CPy` | `CachedCBig` (= `FastComplex`) | **yes** | +| Python type | Rust inner type | Cache | Send? | +|---|---|---|---| +| `UPy` | `UBig` | — | yes | +| `IPy` | `IBig` | — | yes | +| `RPy` | `RBig` | — | yes | +| `FPy` | `FBig` | global | **yes** | +| `DPy` | `FBig` | global | **yes** | +| `CPy` | `CBig` | global | **yes** | -**Why cache:** every `sin`/`cos`/`exp`/`ln` call on a bare `FBig` recomputes π from scratch (O(N²)). The cache stores the binary-splitting tree state, so repeated high-precision transcendental calls become incremental. This is the dominant cost for a numerical-computing library, so it's worth the thread-safety trade-off below. +All six wrappers are `Send + Sync`, so **none need `#[pyclass(unsendable)]`** and the binding is free-threaded-Python compatible. -**Thread-safety consequence (important):** `CachedFBig` and `CachedCBig` hold `Rc>`, making them **`!Send + !Sync`**. Therefore `FPy`/`DPy`/`CPy` **must** use `#[pyclass(unsendable)]`: -- Objects can only be accessed from the thread that created them. -- This is fine for standard GIL-based Python (all access is serialized by the GIL anyway), but **incompatible with free-threaded Python (no-GIL, PEP 703)** and with passing these objects across thread boundaries (e.g. into a Rust worker thread). -- `UPy`/`IPy`/`RPy` keep default (Send) `#[pyclass]` — they're unaffected. +**Why a global cache:** every `sin`/`cos`/`exp`/`ln` recomputes π/ln2/ln10 from scratch (O(N²)) unless a `ConstCache` is threaded in. The cache is base-free and rounding-mode-free (just big-integer constants), so **one cache serves FPy (base 2), DPy (base 10), and CPy (complex) at once**, and accumulates precision across calls. Keeping it in the module means the *value* stays a plain, `Send`, operator-bearing `FBig`/`CBig`. -**Cache-sharing rule (from the Rust API):** binary ops preserve the LHS's cache handle (`Rc::clone`); when a bare value (e.g. an `FBig` from an int conversion) meets a cached value, the cached side's handle wins. So `a + b` where both are cached keeps a single shared cache — good. Operations that return a *new* constant-free value (truncation, base conversion) go through `as_fbig()` and attach a fresh cache (acceptable — they don't need π). +**Backing store:** `thread_local! { static CONST_CACHE: RefCell }`, initialized with `ConstCache::new()` (a `pub const fn` on a `Send + Sync` struct). Thread-local = zero locking, zero contention; under free-threaded Python each thread gets its own cache and recomputes constants once. A `with_cache(|c| ...)` helper borrows it mutably for one call. -**Missing-method delegation:** `CachedFBig`/`CachedCBig` do **not** yet mirror every `FBig`/`CBig` method (notably `trunc`/`floor`/`ceil`/`fract`/`round`/`split_at_point`/`with_base`/`to_decimal`/`nth_root`/`hypot`). For those, delegate through the inner value: `self.0.as_fbig().trunc()` then wrap the result back via `CachedFBig::from(...)`. Per AGENTS.md, the cached wrappers are *intended* to mirror the full surface eventually — prefer pushing the missing method upstream into the cached type rather than working around it permanently. +**Error mapping (replaces the panicking `Context::unwrap_fp`):** the `Context` layer returns `FpResult>` (= `Result`). We map it ourselves: +- `Err(Overflow(sign))` → `Repr::infinity_with_sign(sign)` (graceful, matches Python float) +- `Err(Underflow(sign))` → `Repr::zero_with_sign(sign)` (graceful) +- `Err(InfiniteInput)` → `ValueError("arithmetic with infinity")` +- `Err(OutOfDomain)` → `ValueError("math domain error")` (matches `math.sqrt(-1)`) +- `Err(Indeterminate)` → `ZeroDivisionError` (this is `0/0`) + +This is *strictly better* than the convenience layer, which `panic!`s on the last three and surfaces in Python as `pyo3_runtime.PanicException` (a `BaseException` subclass that escapes `except Exception:` and crashes the session). + +**Scope of the fix:** this fully fixes **transcendentals** (`sin`, `exp`, `ln`, `sqrt`, `powf`, trig/hyperbolic — all take `cache: Option<&mut ConstCache>` at the `Context` layer). **Arithmetic** operators on bare `FBig`/`CBig` still `panic!` on infinite input (e.g. `exp(1000) + 1`, since `exp(1000)→+∞` then `+` asserts finite) and on `0/0`. Those are rarer than transcendental domain errors, so arithmetic keeps the operator impls for the MVP; routing it through the `Context` layer (or guarding) is a follow-up. ### Key patterns to follow **UniInput dispatch** — `python/src/convert.rs`: The `UniInput` enum has 11 variants covering all Python numeric types (Uint, Int, BUint, BInt, OBInt, Float, BFloat, BDecimal, OBDecimal, BRational, OBRational). Its `FromPyObject` impl already extracts any Python number into the right variant. We add conversion methods (`into_fpy`, `into_dpy`, `into_rpy`, `into_cpy`) that collapse any variant into the target dashu type, making arithmetic functions simple one-liners. -**Comparison** — `num_order::NumOrd` trait provides cross-type comparison for all dashu types: FBig↔UBig, FBig↔IBig, FBig↔RBig, FBig↔f64, RBig↔UBig, RBig↔IBig, etc. The `__richcmp__` pattern is already established in UPy/IPy: `op.matches(self.0.num_cmp(&rhs))`. Note: `CachedFBig`/`CachedCBig` impl `NumOrd` too, but to compare a cached value against another cached value, compare via `as_fbig()`/`as_cbig()` or rely on the cached `PartialOrd` (same-base only). +**Comparison** — `num_order::NumOrd` trait provides cross-type comparison for all dashu types: FBig↔UBig, FBig↔IBig, FBig↔RBig, FBig↔f64, RBig↔UBig, RBig↔IBig, etc. The `__richcmp__` pattern is already established in UPy/IPy: `op.matches(self.0.num_cmp(&rhs))`. -**Arithmetic macros** — The existing `impl_binops!` macro in `int.rs` generates forward/reverse dispatch functions by matching each `UniInput` variant. For FPy/DPy/RPy/CPy we create simpler versions that first convert the operand to the target type via `into_fpy()`/etc, then call the Rust operator. For cached types the operator preserves the cache handle automatically. +**Arithmetic macros** — The existing `impl_binops!` macro in `int.rs` generates forward/reverse dispatch functions by matching each `UniInput` variant. For FPy/DPy/RPy/CPy we create simpler versions that first convert the operand to the target type via `into_fpy()`/etc, then call the Rust operator (bare FBig/CBig keep all operator trait impls). + +**Transcendental dispatch** — transcendentals do NOT use the convenience methods; they call `self.0.context().sin(self.0.repr(), cache)` through `with_cache`, then map the `FpResult`. This is the single most important pattern in the binding — it's what gives clean Python exceptions instead of panics. ### Rust API facts (verified) ``` -CachedFBig (= FBig + Rc>; !Send + !Sync) - Construction: From, From, FromStr, TryFrom -- all attach a FRESH cache. - CachedFBig::from_parts(significand: IBig, exponent: isize) -- fresh cache. - FBig::into_cached(self, cache) -- attach a SPECIFIC shared cache handle. - Extract inner: .as_fbig() -> &FBig, .into_fbig(self) -> FBig - Arithmetic: Add/Sub/Mul/Div/Rem (assign), Neg, Abs, Shl/Shr (assign), Sum, Product -- all mirror FBig, all cache-preserving. - Cache-threaded transcendents (the whole point): ln, ln_1p, exp, exp_m1, sin, cos, tan, asin, acos, atan, - sinh, cosh, tanh, asinh, acosh, atanh, sin_cos, sinh_cosh, - atan2, powf, pi(precision, &cache). - Delegated (no cache needed): sqrt, inv, sqr, cubic, powi(IBig). - Accessors: .precision(), .digits(), .context(), .repr(), .into_repr(), .sign(), .ulp(), - .with_precision(n) -> Rounded, .with_rounding(). - Conversions out: .to_int() -> Rounded, .to_f32(), .to_f64(), TryFrom for IBig/UBig/f32/f64. - NOT YET mirrored (delegate via .as_fbig()): trunc, floor, ceil, fract, round, split_at_point, quantize, - to_decimal, to_binary, with_base, with_base_and_precision, - nth_root, hypot, cbrt(CubicRoot trait IS present though). - -CachedCBig (= CBig + Rc>; !Send + !Sync) - Construction: From, From, From, FromStr, TryFrom -- fresh cache. - CachedCBig::from_parts(re: FBig, im: FBig) -- fresh cache (note: takes bare FBig parts). - .into_parts(self) -> (CachedFBig, CachedFBig) -- INTENTIONAL divergence from CBig's (FBig,FBig). - Extract inner: .as_cbig() -> &CBig, .into_cbig(self) -> CBig. - Cache-threaded: ln, exp, sin, cos, tan, asin, acos, atan, arg, powf, sin_cos. - Delegated: sqr, sqrt, conj, proj, mul_i(negative), powi(IBig), norm()->FBig, abs()->FBig. - Accessors: .re() -> &Repr, .im() -> &Repr, .precision(), .context(), - .is_zero(), .is_infinite(), .is_finite(). - Note: CBig.abs()/arg() return FBig (the modulus/phase is real), so wrap in CachedFBig via From. +ConstCache (dashu_float::ConstCache; Send + Sync; pub const fn new()) + Owns memoized pi/ln2/ln10 binary-splitting state. Base-free, rounding-free. + One instance serves FPy (base 2), DPy (base 10), CPy (complex) simultaneously. + pub fn clear(&mut self), pub fn total_terms(&self), pub fn total_words(&self). + +FBig (the value FPy/DPy wrap; Send + Sync) + Public accessors: .repr() -> &Repr, .context() -> Context (Copy), .into_repr() -> Repr. + Context layer (panic-free, returns FpResult>): + .add/.sub/.mul/.div/.inv/.sqrt/.cbrt/.nth_root/.powi/.hypot -- no cache param + .exp/.exp_m1/.ln/.ln_1p/.sin/.cos/.tan/.asin/.acos/.atan/.atan2/.powf/.sinh/.cosh/.tanh/.asinh/.acosh/.atanh + -- all take cache: Option<&mut ConstCache> + .pi(precision, cache) -> Rounded + Convenience layer (FBig::sin etc.) -- PANICS on InfiniteInput/OutOfDomain/Indeterminate; DO NOT USE in bindings. + +CBig (the value CPy wraps; Send + Sync) + Public accessors: .re() -> &Repr, .im() -> &Repr, .context() -> dashu_cmplx::Context (Copy). + Complex Context layer (panic-free, returns CfpResult): + .sqrt/.powi -- no cache param + .ln/.exp/.sin/.cos/.tan/.asin/.acos/.atan/.powf -- all take cache: Option<&mut ConstCache> + .abs/.arg/.norm -- return real FBig (abs via hypot; arg threads cache). + Convenience layer (CBig::sin etc.) -- PANICS; DO NOT USE in bindings. RBig methods (uncached): .numerator() -> &IBig, .denominator() -> &UBig @@ -104,13 +106,13 @@ Traits (need `use` imports): IBig::is_negative()/is_positive() -- NOT inherent! Use self.sign() == Sign::Negative instead. -Conversions (int/float/rational): - CachedFBig::try_from(f64) -- base 2 ONLY (FPy works; DPy does not) - CachedFBig::from(UBig), CachedFBig::from(IBig) -- infallible, any base - FBig::try_from(RBig) / CachedFBig::try_from(RBig) -- exact only, returns Err on precision loss - RBig::try_from(FBig/CachedFBig) -- exact only (use .into_fbig() first if cached) +Conversions (int/float/rational — bare FBig, since FPy/DPy wrap bare FBig): + FBig::try_from(f64) -- base 2 ONLY (FPy works; DPy does not) + FBig::from(UBig), FBig::from(IBig) -- infallible, any base + FBig::try_from(RBig) -- exact only, returns Err(ConversionError) on precision loss + RBig::try_from(FBig) -- exact only RBig::try_from(f64) -- exact only (f64 has limited precision anyway) - DPy from f64 -- go through string: format!("{:e}", x) then CachedFBig::from_str + DPy from f64 -- go through string: format!("{:e}", x) then FBig::from_str Approximation::value() extracts T from both Exact(T) and Inexact(T, E) variants. ``` @@ -193,32 +195,26 @@ pub enum PySign { Positive, Negative } pub enum PySign { Positive, Negative } ``` -### 0d2: Wrapper types use cached float/complex types — `types.rs` +### 0d2: Wrapper types store bare FBig/CBig (all Send) — `types.rs` -Change the float/complex wrappers to the cached variants and mark them `unsendable` (see Architecture: `CachedFBig`/`CachedCBig` are `!Send + !Sync`). +Per the Architecture decision, `FPy`/`DPy`/`CPy` wrap the **bare** `FBig`/`CBig` (not the cached variants). The constant cache lives in the module (Step 2c), not in the value. All wrappers are `Send + Sync`, so **none use `unsendable`**. ```rust -// Before: -#[pyclass(name = "FBig")] pub struct FPy(pub dashu_float::FBig); -#[pyclass(name = "DBig")] pub struct DPy(pub dashu_float::DBig); -// (CPy added in Step 11) +// types.rs — final shape (after Steps 0d2 + 11): +type FBig2 = dashu_float::FBig; +type DBig10 = dashu_float::FBig; +type CBig2 = dashu_cmplx::CBig; -// After: -type CachedFBig = dashu_float::CachedFBig; -type CachedDBig = dashu_float::CachedFBig; -type CachedCBig = dashu_cmplx::CachedCBig; - -#[pyclass(name = "FBig", unsendable)] pub struct FPy(pub CachedFBig); -#[pyclass(name = "DBig", unsendable)] pub struct DPy(pub CachedDBig); -#[pyclass(name = "CBig", unsendable)] pub struct CPy(pub CachedCBig); // Step 11 +#[pyclass(name = "FBig")] pub struct FPy(pub FBig2); +#[pyclass(name = "DBig")] pub struct DPy(pub DBig10); +#[pyclass(name = "CBig")] pub struct CPy(pub CBig2); // Step 11 -// Integers and rationals stay Send (no cache, no transcendental ops): #[pyclass(name = "UBig")] pub struct UPy(pub dashu_int::UBig); // unchanged #[pyclass(name = "IBig")] pub struct IPy(pub dashu_int::IBig); // unchanged #[pyclass(name = "RBig")] pub struct RPy(pub dashu_ratio::RBig); // unchanged ``` -`UPy`/`IPy`/`RPy` keep the default (Send) `#[pyclass]` — they are plain data and benefit from being freely shareable. Only the cached float/complex types need `unsendable`. +The `UniInput` enum's `BFloat`/`BDecimal` variants keep field names `BFloat(PyRef<'a, FPy>)` etc. — only the inner type behind `FPy` changes (from cached to bare). Add `BComplex(PyRef<'a, CPy>)` for Step 11. The `UniInput` enum's `BFloat`/`BDecimal` variants change their `PyRef` target types to the new cached wrappers (the field names `BFloat(PyRef<'a, FPy>)` etc. stay the same; only the inner type behind `FPy` changes). Add `BComplex(PyRef<'a, CPy>)` for Step 11. @@ -368,32 +364,32 @@ _ => return Err(PyTypeError::new_err("integer power requires a non-negative inte **File: `python/src/convert.rs`** -Add four methods to `impl<'a> UniInput<'a>` that convert any numeric variant to the target dashu type. The float/complex helpers produce the **cached** wrappers (a fresh `ConstCache` is attached by every `From`/`TryFrom`/`FromStr` on `CachedFBig`/`CachedCBig`). +Add four methods to `impl<'a> UniInput<'a>` that convert any numeric variant to the target dashu type. Since `FPy`/`DPy`/`CPy` wrap **bare** `FBig`/`CBig`, these are the plain `FBig`/`CBig` conversions (no cache is attached — the cache lives in the module, see Step 2c). ### `into_fpy(self) -> PyResult` -`FPy` wraps `CachedFBig`. Conversions use the cached `From`/`TryFrom` impls (each attaches a fresh cache): +`FPy` wraps `FBig`. | Input variant | Conversion | |---|---| -| `Uint(x)` | `CachedFBig::from(x)` | -| `Int(x)` | `CachedFBig::from(IBig::from(x))` | -| `BUint(x)` | `CachedFBig::from(x.0.clone())` | -| `BInt(x)` | `CachedFBig::from(x.0.clone())` | -| `OBInt(x)` | `CachedFBig::from(x)` | -| `Float(x)` | `CachedFBig::try_from(x)` — map error to PyValueError | -| `BFloat(x)` | `x.0.clone()` (already a `FPy` = cached) | +| `Uint(x)` | `FBig::from(x)` | +| `Int(x)` | `FBig::from(IBig::from(x))` | +| `BUint(x)` | `FBig::from(x.0.clone())` | +| `BInt(x)` | `FBig::from(x.0.clone())` | +| `OBInt(x)` | `FBig::from(x)` | +| `Float(x)` | `FBig::try_from(x)` — map error to PyValueError | +| `BFloat(x)` | `x.0.clone()` (already a `FPy` = bare FBig) | | `BDecimal/OBDecimal` | `Err(PyTypeError("decimal cannot be mixed with binary float; convert explicitly"))` | -| `BRational(x)` | `CachedFBig::try_from(x.0.clone())` — map ConversionError to PyTypeError | -| `OBRational(x)` | `CachedFBig::try_from(x)` — same | +| `BRational(x)` | `FBig::try_from(x.0.clone())` — map ConversionError to PyTypeError | +| `OBRational(x)` | `FBig::try_from(x)` — same | ### `into_dpy(self) -> PyResult` -`DPy` wraps `CachedFBig`. For `Float(x)` use `format!("{:e}", x)` → `CachedFBig::from_str` (no direct `TryFrom` for base 10). For `BFloat` return type error. For `BRational`/`OBRational` use `CachedFBig::try_from` (DBig is `FBig`). +`DPy` wraps `FBig`. For `Float(x)` use `format!("{:e}", x)` → `FBig::from_str` (no direct `TryFrom` for base 10). For `BFloat` return type error. For `BRational`/`OBRational` use `FBig::try_from` (DBig is `FBig`). ### `into_rpy(self) -> PyResult` -`RPy` wraps `RBig` (uncached — rationals have no transcendental ops). +`RPy` wraps `RBig` (no transcendental ops). | Input variant | Conversion | |---|---| @@ -403,21 +399,21 @@ Add four methods to `impl<'a> UniInput<'a>` that convert any numeric variant to | `BInt(x)` | `RBig::from(x.0.clone())` | | `OBInt(x)` | `RBig::from(x)` | | `Float(x)` | `RBig::try_from(x)` — map error to PyValueError | -| `BFloat(x)` | `RBig::try_from(x.0.as_fbig().clone())` — go through inner FBig | -| `BDecimal(x)` | `RBig::try_from(x.0.as_fbig().clone())` — same | -| `OBDecimal(x)` | `RBig::try_from(x)` — same | +| `BFloat(x)` | `RBig::try_from(x.0.clone())` | +| `BDecimal(x)` | `RBig::try_from(x.0.clone())` | +| `OBDecimal(x)` | `RBig::try_from(x)` | | `BRational(x)` | `x.0.clone()` | | `OBRational(x)` | `x` | ### `into_cpy(self) -> PyResult` (Step 11) -`CPy` wraps `CachedCBig`. `CachedCBig::From` and `From` exist; ints/floats embed as `z + 0i`. `BFloat`/`BDecimal` convert via their inner `as_fbig()`. Cross-base (FPy↔DPy) and rational→complex raise type errors for the MVP. +`CPy` wraps bare `CBig`. `CBig::From` and `From` exist; ints/floats embed as `z + 0i`. `BFloat`/`BDecimal` convert via their inner FBig (`x.0.clone()` then `CBig::from`). Cross-base (FPy↔DPy) and rational→complex raise type errors for the MVP. Add imports at top of `convert.rs`: ```rust use crate::types::{FPy, CPy}; -use dashu_float::CachedFBig; // generic; instantiate with / -use dashu_cmplx::CachedCBig; +use dashu_float::FBig; +use dashu_cmplx::CBig; use dashu_float::round::mode; use dashu_base::ConversionError; use std::str::FromStr; @@ -425,6 +421,92 @@ use std::str::FromStr; --- +## Step 2c: Global constant cache — new file `python/src/cache.rs` + +This module owns the single `ConstCache` shared by all float/decimal/complex transcendentals, and provides the `FpError → PyErr` mapping that replaces the panicking `Context::unwrap_fp`. + +```rust +// python/src/cache.rs +use std::cell::RefCell; +use dashu_float::{ConstCache, Repr, Sign}; +use dashu_float::error::{FpError, Rounding}; +use pyo3::exceptions::{PyValueError, PyZeroDivisionError}; +use pyo3::PyErr; + +thread_local! { + /// One cache per thread — zero locking, zero contention. Base-free, so it serves + /// FPy (base 2), DPy (base 10), and CPy (complex) simultaneously. Accumulates + /// precision across calls; never needs explicit invalidation. + static CONST_CACHE: RefCell = RefCell::new(ConstCache::new()); +} + +/// Borrow the thread-local cache mutably for one transcendental call. +pub fn with_cache(f: impl FnOnce(&mut ConstCache) -> R) -> R { + CONST_CACHE.with(|c| f(&mut c.borrow_mut())) +} + +/// Optional Python-facing handle for cache inspection/clearing (see Step 2c-bottom). +#[pyclass(name = "Cache")] +pub struct PyCache; +#[pymethods] +impl PyCache { + /// Drop all memoized constants (rarely needed — the cache only grows usefully). + #[staticmethod] + fn clear() { with_cache(|c| c.clear()); } + #[staticmethod] + fn total_terms() -> usize { with_cache(|c| c.total_terms()) } + #[staticmethod] + fn total_words() -> usize { with_cache(|c| c.total_words()) } +} + +/// Map a float `FpResult>` to a `PyResult`: +/// Overflow/Underflow → signed ∞/0 (graceful, like Python float); +/// InfiniteInput/OutOfDomain → ValueError; Indeterminate → ZeroDivisionError. +/// `val` extracts the FBig from the Rounded wrapper (Exact|Inexact). +pub fn unwrap_float( + res: dashu_float::FpResult>, +) -> PyResult> +where + usize: dashu_float::Base, +{ + use dashu_base::Approximation; + match res { + Ok(rounded) => Ok(rounded.value()), + Err(FpError::Overflow(sign)) => Ok(dashu_float::FBig::from_repr( + Repr::infinity_with_sign(sign), /* context */ unreachable!())), + // NOTE: the overflow/underflow branches need the value's context to rebuild + // an FBig; in practice build them via Repr + the caller's context — see the + // per-call helpers in float.rs/complex.rs which receive `self.0.context()`. + Err(FpError::Underflow(sign)) => /* signed zero */ todo!(), + Err(FpError::InfiniteInput) => Err(PyValueError::new_err("arithmetic with infinity")), + Err(FpError::OutOfDomain) => Err(PyValueError::new_err("math domain error")), + Err(FpError::Indeterminate) => Err(PyZeroDivisionError::new_err("indeterminate form (0/0)")), + } +} +``` + +**Rebuilding ∞/0 with the right context:** `FBig::from_repr` needs a `Context`. Since each call site has `self.0.context()`, the cleanest shape is a *context-aware* mapper `unwrap_float(res, ctx) -> PyResult` rather than the simplified one above. Concretely, the transcendental methods in `float.rs` look like: + +```rust +fn sin(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.sin(self.0.repr(), Some(c))); // FpResult> + Ok(FPy(unwrap_float(res, ctx)?)) +} +``` + +For **complex**, the parallel `unwrap_complex(res, ctx) -> PyResult` maps the same `FpError` variants but rebuilds `CBig::overflow(ctx, sign)` / `CBig::underflow(ctx, sign)` on Overflow/Underflow (these constructors exist on CBig — see `complex/src/repr.rs`). + +**Register in `lib.rs`:** `m.add_class::()?;` so Python users can call `dashu.Cache.clear()`. + +### Why this design + +- **Panic-free transcendentals:** every `sin`/`cos`/`exp`/`ln`/`sqrt`/`powf`/trig/hyperbolic goes through `Context::(repr, cache) -> FpResult`, never the panicking convenience method. +- **Caching preserved:** `with_cache` threads the *same* growing cache into every call, exactly like `CachedFBig` would — but without `Rc>` baked into the value. +- **`Send` preserved:** `FPy`/`DPy`/`CPy` hold bare `FBig`/`CBig`, so no `#[pyclass(unsendable)]`, and the binding works under free-threaded Python (each thread gets its own thread-local cache). + +--- + ## Step 3: Add arithmetic operators to FPy and DPy **File: `python/src/float.rs`** @@ -817,7 +899,7 @@ Note: these are same-type-only for the MVP. Python's data model falls back to `_ **File: `python/src/float.rs`** — add to `#[pymethods] impl FPy` (and replicate for DPy): ```rust -// Predicates (repr-based; work the same on CachedFBig) +// Predicates (repr-based) fn is_zero(&self) -> bool { self.0.repr().is_zero() } fn is_finite(&self) -> bool { self.0.repr().is_finite() } fn is_infinite(&self) -> bool { self.0.repr().is_infinite() } @@ -830,19 +912,16 @@ fn sign(&self) -> PySign { Sign::Negative => PySign::Negative, } } -// CachedFBig::signum() is mirrored -> returns CachedFBig fn signum(&self) -> Self { FPy(self.0.signum()) } -// Rounding: trunc/floor/ceil/fract/round are NOT yet mirrored on CachedFBig, -// so delegate through the inner FBig and wrap back (fresh cache — these are -// pure significand/exponent ops, no constant cache needed). -fn trunc(&self) -> Self { FPy(self.0.as_fbig().trunc().into()) } -fn floor(&self) -> Self { FPy(self.0.as_fbig().floor().into()) } -fn ceil(&self) -> Self { FPy(self.0.as_fbig().ceil().into()) } -fn round(&self) -> Self { FPy(self.0.as_fbig().round().into()) } -fn fract(&self) -> Self { FPy(self.0.as_fbig().fract().into()) } +// Rounding — direct on bare FBig (pure significand/exponent ops, no cache needed). +fn trunc(&self) -> Self { FPy(self.0.trunc()) } +fn floor(&self) -> Self { FPy(self.0.floor()) } +fn ceil(&self) -> Self { FPy(self.0.ceil()) } +fn round(&self) -> Self { FPy(self.0.round()) } +fn fract(&self) -> Self { FPy(self.0.fract()) } -// Conversion (CachedFBig::to_int exists, returns Rounded) +// Conversion (FBig::to_int returns Rounded) fn to_int(&self) -> PyResult { let int: IBig = self.0.clone().to_int().value().try_into() .map_err(|e: ConversionError| PyValueError::new_err(format!("cannot convert to integer: {}", e)))?; @@ -853,21 +932,59 @@ fn __int__(&self, py: Python) -> PyResult { convert_from_ibig(&ipy.0, py) } -// Precision (CachedFBig mirrors with_precision -> Rounded) +// Precision (FBig::with_precision -> Rounded) fn with_precision(&self, precision: usize) -> Self { FPy(self.0.clone().with_precision(precision).value()) } fn precision(&self) -> usize { self.0.context().precision() } fn digits(&self) -> usize { self.0.digits() } -// Float-specific constructor (CachedFBig::from_parts attaches fresh cache) +// Float-specific constructor #[staticmethod] fn from_parts(significand: &IPy, exponent: isize) -> Self { - FPy(CachedFBig::from_parts(significand.0.clone(), exponent)) + FPy(FBig::from_parts(significand.0.clone(), exponent)) +} +``` + +**For DPy** the same methods apply — bare `FBig` supports them identically. + +### 6b: Transcendentals — route through the global cache + Context layer + +This is the critical part: transcendentals do **NOT** call `self.0.sin()` (the convenience method panics on domain errors). Instead they call the panic-free `Context` layer with the module cache and map the result: + +```rust +use crate::cache::{with_cache, unwrap_float}; + +#[pymethods] +impl FPy { + fn sin(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.sin(self.0.repr(), Some(c))); // FpResult> + Ok(FPy(unwrap_float(res, ctx)?)) + } + fn cos(&self) -> PyResult { /* ctx.cos(...) */ } + fn tan(&self) -> PyResult { /* ctx.tan(...) */ } + fn asin(&self) -> PyResult { /* ctx.asin(...) */ } + fn acos(&self) -> PyResult { /* ctx.acos(...) */ } + fn atan(&self) -> PyResult { /* ctx.atan(...) */ } + fn sinh(&self) -> PyResult { /* ctx.sinh(...) */ } + fn cosh(&self) -> PyResult { /* ctx.cosh(...) */ } + fn tanh(&self) -> PyResult { /* ctx.tanh(...) */ } + fn asinh(&self) -> PyResult { /* ctx.asinh(...) */ } + fn acosh(&self) -> PyResult { /* ctx.acosh(...) */ } + fn atanh(&self) -> PyResult { /* ctx.atanh(...) */ } + fn exp(&self) -> PyResult { /* ctx.exp(...) */ } + fn exp_m1(&self) -> PyResult { /* ctx.exp_m1(...) */ } + fn ln(&self) -> PyResult { /* ctx.ln(...) */ } + fn ln_1p(&self) -> PyResult { /* ctx.ln_1p(...) */ } + // sqrt/cbrt take NO cache (they're algebraic) but still go through Context for clean errors: + fn sqrt(&self) -> PyResult { let ctx = self.0.context(); Ok(FPy(unwrap_float(ctx.sqrt(self.0.repr()), ctx)?)) } + fn cbrt(&self) -> PyResult { /* ctx.cbrt / ctx.nth_root(3, ...) */ } + fn powf(&self, w: &Self) -> PyResult { let ctx = self.0.context(); Ok(FPy(unwrap_float(ctx.powf(self.0.repr(), w.0.repr(), with_cache_arg), ctx)?)) } } ``` -**For DPy** the same methods apply — `CachedFBig` mirrors them identically. +Note `powf` needs the cache threaded into `with_cache`; the helper can be extended to `with_cache(|c| ctx.powf(a, b, Some(c)))`. Every domain error (`sqrt(-1)`, `asin(2)`, `ln(-1)`, `acosh(0.5)`, `atanh(2)`) now raises a Python `ValueError` instead of crashing the session. **File: `python/src/ratio.rs`** — add to `#[pymethods] impl RPy`: @@ -1200,36 +1317,38 @@ class Sign: ## Implementation Order ``` -1. Step 1 (fix panics) — independent, do first -2. Step 2 (conversion helpers) — independent -3. Step 3 (FPy/DPy arithmetic) — depends on Step 2 -4. Step 4 (RPy arithmetic) — depends on Step 2 -5. Step 5 (int methods) — independent (can parallel with 3-4) -6. Step 6 (float/rational pred) — depends on Step 3-4 (adds to same impl blocks) -7. Step 7 (math module) — depends on Step 3 (FPy type must exist) -8. Step 8 (format fix) — independent -9. Step 9 (stubs) — after all code changes -10. Step 10 (tests) — after all code changes -11. Step 11 (complex bindings) — after Step 3 (follows same pattern) +1. Step 0 (PyO3 0.29 upgrade + Cargo.toml deps) — do first +2. Step 1 (fix panics) — independent +3. Step 2 (conversion helpers) — independent +4. Step 2c (global cache module) — independent (cache.rs + FpError mapper) +5. Step 3 (FPy/DPy arithmetic) — depends on Step 2 +6. Step 4 (RPy arithmetic) — depends on Step 2 +7. Step 5 (int methods) — independent (can parallel with 3-4) +8. Step 6 (float/rational pred + transcendentals via Step 2c) — depends on Step 3-4 + 2c +9. Step 7 (math module) — depends on Step 6 +10. Step 8 (format fix) — independent +11. Step 9 (stubs) — after all code changes +12. Step 10 (tests) — after all code changes +13. Step 11 (complex bindings) — after Step 6 + 2c (follows the float pattern) ``` Steps 3 and 5 can be done in parallel. Steps 3 and 4 share the same pattern so do them together. --- -## Step 11: Complex number bindings (CachedCBig → CPy) +## Step 11: Complex number bindings (bare CBig → CPy) **New dependency:** `dashu-cmplx` (added to `Cargo.toml` in Step 0a). -Per the Architecture decision, `CPy` wraps the **cached** `CachedCBig` (not bare `CBig`). This shares the same `ConstCache` benefit as `FPy` for complex transcendentals. +Per the Architecture decision, `CPy` wraps the **bare** `CBig` (not `CachedCBig`). The constant cache is the module-global one from Step 2c, threaded into complex `Context` methods. `CPy` is `Send + Sync` (no `unsendable`). ### 11a: Add `CPy` wrapper type — `types.rs` Already defined in Step 0d2: ```rust -type CachedCBig = dashu_cmplx::CachedCBig; -#[pyclass(name = "CBig", unsendable)] -pub struct CPy(pub CachedCBig); +type CBig2 = dashu_cmplx::CBig; +#[pyclass(name = "CBig")] +pub struct CPy(pub CBig2); ``` Also add `BComplex(PyRef<'a, CPy>)` variant to `UniInput`. @@ -1239,78 +1358,97 @@ Also add `BComplex(PyRef<'a, CPy>)` variant to `UniInput`. m.add_class::()?; ``` -### 11c: CachedCBig constructor — new file `python/src/complex.rs` +### 11c: CBig constructor — new file `python/src/complex.rs` ```rust #[pymethods] impl CPy { #[new] fn __new__(ob: &PyAny) -> PyResult { - // Python complex → CachedCBig (TryFrom exact, base 2; build re+im) - // string "a+bi" → CachedCBig (FromStr; algebraic format, fresh cache) - // (real, imag) tuple of FPy → CachedCBig::from_parts(re.into_fbig(), im.into_fbig()) - // (note: from_parts takes bare FBig parts, fresh cache) - // int/float/FPy → CachedCBig::from (embed as z + 0i) + // Python complex → CBig (build from re/im FBig via CBig::from_parts, or FromStr) + // string "a+bi" → CBig (FromStr; algebraic format) + // (real, imag) tuple of FPy → CBig::from_parts(re.0.clone(), im.0.clone()) + // int/float/FPy → CBig::from (embed as z + 0i) } } ``` -`CachedCBig` provides: -- `FromStr` for algebraic `"a+bi"` format (fresh cache) -- `From`, `From`, `From`, `From`, `From` (fresh cache) -- `TryFrom`, `TryFrom` (fresh cache) -- `CachedCBig::from_parts(re: FBig, im: FBig)` — takes **bare** FBig parts, attaches fresh cache +`CBig` provides: +- `FromStr` for algebraic `"a+bi"` format +- `From`, `From`, `From`, `From` — embed as `z + 0i` +- `CBig::from_parts(re: FBig, im: FBig)` — note `into_parts()` returns `(FBig, FBig)` on bare CBig ### 11d: Arithmetic operators -Same macro pattern as FPy/DPy/RPy using `into_cpy()`. `CachedCBig` has full `Add`/`Sub`/`Mul`/`Div` (assign + 4 ref/val combos vs itself, bare `CBig`, and `FBig`), `Neg`, `Inverse`, `Sum`, `Product` — all cache-preserving. +Same macro pattern as FPy/DPy/RPy using `into_cpy()`. Bare `CBig` has full `Add`/`Sub`/`Mul`/`Div` (assign + ref/val combos vs itself and `FBig`), `Neg`, `Inverse`, `Sum`, `Product`. (Same infinite-input/`0/0` panic caveat as float arithmetic — address as a follow-up.) -### 11e: Accessors, predicates, math +### 11e: Accessors, predicates, complex-specific ```rust -// Accessors (re()/im() return &Repr; wrap the FBig projection in a fresh CachedFBig) -fn real(&self) -> FPy { FPy(FBig::from(self.0.re().clone()).into()) } -fn imag(&self) -> FPy { FPy(FBig::from(self.0.im().clone()).into()) } +// Accessors (re()/im() return &Repr; project to a bare FBig) +fn real(&self) -> FPy { FPy(FBig::from(self.0.re().clone())) } +fn imag(&self) -> FPy { FPy(FBig::from(self.0.im().clone())) } -// Predicates (CachedCBig mirrors these) +// Predicates fn is_zero(&self) -> bool { self.0.is_zero() } fn is_finite(&self) -> bool { self.0.is_finite() } fn is_infinite(&self) -> bool { self.0.is_infinite() } -// Complex-specific (delegated ops; abs/arg/norm return bare FBig, wrap in cached) +// Complex-specific (conj/proj are algebraic, no cache; abs/arg/norm return real FBig) fn conj(&self) -> Self { CPy(self.0.conj()) } fn proj(&self) -> Self { CPy(self.0.proj()) } -fn abs(&self) -> FPy { FPy(self.0.abs().into()) } // modulus (real), fresh cache -fn arg(&self) -> FPy { FPy(self.0.arg().into()) } // phase (real), cache-threaded internally -fn norm(&self) -> FPy { FPy(self.0.norm().into()) } // squared modulus (real) - -// Math (cache-threaded transcendentals — the whole point of CachedCBig) -fn sin(&self) -> Self { CPy(self.0.sin()) } -fn cos(&self) -> Self { CPy(self.0.cos()) } -fn tan(&self) -> Self { CPy(self.0.tan()) } -fn asin(&self) -> Self { CPy(self.0.asin()) } -fn acos(&self) -> Self { CPy(self.0.acos()) } -fn atan(&self) -> Self { CPy(self.0.atan()) } -fn exp(&self) -> Self { CPy(self.0.exp()) } -fn ln(&self) -> Self { CPy(self.0.ln()) } -fn sqrt(&self) -> Self { CPy(self.0.sqrt()) } // delegated to inner CBig -fn sqr(&self) -> Self { CPy(self.0.sqr()) } -fn powi(&self, exp: &IPy) -> Self { CPy(self.0.powi(exp.0.clone())) } -fn powf(&self, w: &Self) -> Self { CPy(self.0.powf(&w.0)) } -fn sin_cos(&self) -> (Self, Self) { let (s, c) = self.0.sin_cos(); (CPy(s), CPy(c)) } +// abs/arg route through the float Context layer for clean errors; norm is algebraic. +fn abs(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.abs(&self.0, Some(c))); // returns FpResult> + Ok(FPy(unwrap_float(res, ctx)?)) +} +fn arg(&self) -> PyResult { /* ctx.arg(...) */ } +fn norm(&self) -> FPy { FPy(self.0.norm()) } // squared modulus, algebraic ``` -### 11f: Conversion to Python `complex` +### 11f: Complex transcendentals — route through the global cache + Context layer + +Exactly the float pattern, but using `dashu_cmplx::Context` (obtained via `self.0.context()`) and `unwrap_complex`: + +```rust +use crate::cache::{with_cache, unwrap_complex}; + +#[pymethods] +impl CPy { + fn sin(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.sin(&self.0, Some(c))); // CfpResult = Result + Ok(CPy(unwrap_complex(res, ctx)?)) + } + fn cos(&self) -> PyResult { /* ctx.cos(...) */ } + fn tan(&self) -> PyResult { /* ctx.tan(...) */ } + fn asin(&self) -> PyResult { /* ctx.asin(...) */ } + fn acos(&self) -> PyResult { /* ctx.acos(...) */ } + fn atan(&self) -> PyResult { /* ctx.atan(...) */ } + fn exp(&self) -> PyResult { /* ctx.exp(...) */ } + fn ln(&self) -> PyResult { /* ctx.ln(...) */ } + fn powf(&self, w: &Self) -> PyResult { /* ctx.powf(...) */ } + // sqrt/powi take NO cache but still go through Context for clean errors: + fn sqrt(&self) -> PyResult { let ctx = self.0.context(); Ok(CPy(unwrap_complex(ctx.sqrt(&self.0), ctx)?)) } + fn powi(&self, exp: &IPy) -> PyResult { /* ctx.powi(...) */ } + fn sin_cos(&self) -> PyResult<(CPy, CPy)> { /* ctx.sin_cos(...) */ } +} +``` + +Domain errors (`ln(0)`, etc.) raise Python `ValueError`; `0/0`-style indeterminate forms raise `ZeroDivisionError` — never a session-crashing panic. + +### 11g: Conversion to Python `complex` ```rust fn __complex__(&self) -> PyResult { - let re: f64 = self.0.as_cbig().re().clone().try_into().map_err(conversion_error_to_py)?; - let im: f64 = self.0.as_cbig().im().clone().try_into().map_err(conversion_error_to_py)?; + let re: f64 = self.0.re().clone().try_into().map_err(conversion_error_to_py)?; + let im: f64 = self.0.im().clone().try_into().map_err(conversion_error_to_py)?; Ok(PyComplex::from_doubles(re, im)) } ``` -### 11g: Add to math module +### 11h: Add to math module Module-level functions in `math.rs` that delegate to `CPy` methods, matching the pattern for `FPy`. @@ -1332,7 +1470,7 @@ assert UBig(144).sqrt() == UBig(12) assert UBig(12).gcd(UBig(8)) == UBig(4) r = RBig.from_parts(IBig(1), UBig(3)) assert r * 3 == RBig.from_parts(IBig(1), UBig(1)) -# cached transcendental (the whole point of wrapping CachedFBig) +# transcendental uses the module-global ConstCache (Step 2c) import math assert abs(float(FBig('2').ln()) - math.log(2)) < 1e-50 # high precision print('All smoke tests passed') @@ -1348,7 +1486,8 @@ python -m pytest python/tests/ -v - **MSRV**: dashu-python is exempt from the workspace MSRV policy (per AGENTS.md). Uses Rust 1.85 and edition 2024. - **Feature flags**: `num-modular` is used by `__pow__` but listed as optional — fix to hard dependency or wire as default feature. -- **Complex crate**: `dashu-cmplx` (merged from develop, `6e21a65`) is a new core crate. Python bindings (Step 11) use the cached `CachedCBig` variant. -- **Cached types**: `FPy`/`DPy`/`CPy` wrap `CachedFBig`/`CachedCBig` (π/ln2/ln10 memoization for transcendentals). They are `!Send + !Sync` → `#[pyclass(unsendable)]`. Integers/rationals stay uncached and Send. See Architecture. -- **Free-threaded Python**: Because float/complex objects are `unsendable`, this binding is **not** compatible with no-GIL (PEP 703) free-threaded CPython for those types. If that becomes a requirement, the fix is an `Arc>`-backed cached variant upstream (dashu does not yet provide one). +- **Complex crate**: `dashu-cmplx` (merged from develop, `6e21a65`) is a new core crate. Python bindings (Step 11) use bare `CBig` + the global cache. +- **Error model**: transcendentals route through the panic-free `Context` layer (`FpResult`) with a module-global `ConstCache` (Step 2c), mapping `Overflow/Underflow → ±∞/0` and `InfiniteInput/OutOfDomain → ValueError`, `Indeterminate → ZeroDivisionError`. See Architecture. +- **Send / free-threaded Python**: all wrappers (`FPy`/`DPy`/`CPy`/`UPy`/`IPy`/`RPy`) are `Send + Sync` — **none use `#[pyclass(unsendable)]`** — because the cache is thread-local, not owned by the value. The binding is free-threaded-Python (no-GIL) compatible; each thread gets its own cache. +- **Outstanding caveat — arithmetic panics**: bare `FBig`/`CBig` arithmetic operators still `panic!` on infinite input (e.g. `exp(1000) + 1`) and on `0/0`. Transcendentals are fully fixed; routing arithmetic through `Context` (or guarding finiteness / the `0/0` case) is a documented follow-up before release. - **Changelog**: Document all changes in `python/CHANGELOG.md` under `## Unreleased` → `### Add`. From d78d951e5be66f7e5d18d39269ca3d29568fb3e4 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Wed, 8 Jul 2026 17:48:15 +0800 Subject: [PATCH 11/20] Plan: broaden __new__ + add cross-type conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 2b (constructor broadening): FBig/DBig/RBig/CBig __new__ now accept any Python number (int, float, Decimal, Fraction, complex) via permissive construct_* helpers — distinct from the strict into_* arithmetic dispatch (which matches Python's own 1.5 + Decimal TypeError rule). Decimal/Fraction -> FBig/DBig are exact-only; lossy conversion goes through to_float (6c). Step 6c (cross-type conversions): expose FBig.to_decimal/to_binary (base 2<->10), FBig.to_rational (exact), RBig.to_float/to_decimal (lossy, correctly rounded, explicit precision). Adds a full conversion matrix between FPy/DPy/RPy/UPy/IPy and Python native types. Co-Authored-By: Claude --- python/TODO-python.md | 150 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 11 deletions(-) diff --git a/python/TODO-python.md b/python/TODO-python.md index 4824a688..c8420d65 100644 --- a/python/TODO-python.md +++ b/python/TODO-python.md @@ -421,6 +421,82 @@ use std::str::FromStr; --- +## Step 2b: Broaden `__new__` to accept any Python number + +**Problem:** today `FBig(...)`, `DBig(...)`, `RBig(...)` only accept their "native" Python type or a string. `FBig(7)`, `FBig(Decimal("1.5"))`, `FBig(Fraction(1,3))`, `DBig(1.5)`, `RBig(1.5)` all fail at construction. The `into_*` helpers (Step 2) fixed the *arithmetic* case but not construction. Workaround today is `auto(x)`, but the type constructors should be permissive too. + +**Design:** keep arithmetic dispatch strict (matches Python — `1.5 + Decimal("0.5")` raises `TypeError`), but make `__new__` permissive by giving it its own conversion path. Add permissive `construct_*` helpers to `UniInput` (or as free functions in `convert.rs`) that differ from `into_*` only in the cross-type rules: + +```rust +impl<'a> UniInput<'a> { + /// Permissive construction of FBig from any Python number (for __new__). + /// Unlike into_fpy, this ACCEPTS Decimal/Fraction by routing through string / RBig. + pub fn construct_fbig(self) -> PyResult { + match self { + Self::Uint(x) => Ok(FBig::from(x)), + Self::Int(x) => Ok(FBig::from(IBig::from(x))), + Self::BUint(x) => Ok(FBig::from(x.0.clone())), + Self::BInt(x) => Ok(FBig::from(x.0.clone())), + Self::OBInt(x) => Ok(FBig::from(x)), + Self::Float(x) => FBig::try_from(x).map_err(conversion_error_to_py), + Self::BFloat(x) => Ok(x.0.clone()), + // Decimal/Fraction -> round-trip through string (Decimal.__format__ gives + // scientific notation; Fraction via RBig::try_from is exact-or-error). + Self::BDecimal(x) | Self::OBDecimal(_) => { + let s = decimal_to_str(&self)?; // helper: format Decimal as decimal string + FBig::from_str(&s).map_err(parse_error_to_py) + } + Self::BRational(x) => FBig::try_from(x.0.clone()).map_err(conversion_error_to_py), + Self::OBRational(x) => FBig::try_from(x).map_err(conversion_error_to_py), + } + } + // construct_dbig, construct_rbig, construct_cbig: same permissive pattern +} +``` + +Then each `__new__` becomes (FPy example): + +```rust +#[new] +fn __new__(ob: &PyAny, radix: Option) -> PyResult { + if let Ok(s) = ob.extract::() { + // string with optional radix (existing path) + let f = match radix { + Some(r) => FBig::from_str_radix(s, r), + None => FBig::from_str(s), + }; + return Ok(FPy(f.map_err(parse_error_to_py)?)); + } + if radix.is_some() { + return Err(PyTypeError::new_err("radix only valid with string input")); + } + if let Ok(other) = as FromPyObject>::extract(ob) { + return Ok(FPy(other.0.clone())); // copy + } + // any other Python number -> permissive construction + Ok(FPy(UniInput::extract(ob)?.construct_fbig()?)) +} +``` + +**Per-type construction matrix** (what each `__new__` accepts after this step): + +| `__new__` accepts | `FBig` | `DBig` | `RBig` | `CBig` | +|---|---|---|---|---| +| `str` (± radix) | ✅ | ✅ | ✅ | ✅ (`"a+bi"`) | +| same-type copy | ✅ | ✅ | ✅ | ✅ | +| `int` | ✅ `FBig::from` | ✅ `DBig::from` | ✅ `RBig::from` | ✅ `CBig::from` (z+0i) | +| `float` | ✅ `try_from` | ✅ via `{:e}` str | ✅ `try_from` (exact) | ✅ re+im | +| `Decimal` | ✅ via str | ✅ `parse_to_dbig` | ✅ via RBig | via FBig | +| `Fraction` | ✅ `try_from` (exact) | ✅ `try_from` (exact) | ✅ `parse_to_rbig` | via FBig | +| `(re, im)` tuple | — | — | `(num, den)` → from_parts | ✅ `from_parts` | +| `complex` | — | — | — | ✅ | + +**Note on exact-vs-lossy:** `Decimal`/`Fraction` → `FBig`/`DBig` via `try_from` is **exact-only** (errors on precision loss, e.g. `1/3` → base 2). For lossy correctly-rounded conversion, users should call the explicit `to_float()` method (Step 6c). The constructor stays strict to avoid silent precision loss — consistent with how `int(1.5)` is explicit in Python. + +**`decimal_to_str` helper:** format a `decimal.Decimal` as a plain decimal string. Reuse the existing `parse_to_dbig` round-trip logic in reverse (Decimal → `__format__` → string). This is the same path `DPy::unwrap` already uses. + +--- + ## Step 2c: Global constant cache — new file `python/src/cache.rs` This module owns the single `ConstCache` shared by all float/decimal/complex transcendentals, and provides the `FpError → PyErr` mapping that replaces the panicking `Context::unwrap_fp`. @@ -1047,6 +1123,56 @@ Add imports to `ratio.rs`: `use dashu_base::Sign;` --- +## Step 6c: Cross-type conversions (base change, float↔rational) + +Expose the base-change and float↔rational conversions that already exist on bare `FBig`/`RBig`. These let users move between `FPy` (base 2), `DPy` (base 10), `RPy`, and the corresponding Python native types without round-tripping through strings. + +**File: `python/src/float.rs`** — add to `FPy` and `DPy`: + +```rust +// Base conversion (FBig::to_decimal / to_binary return Rounded>) +fn to_decimal(&self) -> DPy { DPy(self.0.to_decimal().value()) } // FPy(base 2) -> DPy(base 10) +fn to_binary(&self) -> FPy { FPy(self.0.to_binary().value()) } // DPy(base 10) -> FPy(base 2); identity on FPy + +// Float -> Rational (exact only; errors if the float isn't exactly representable as a fraction) +fn to_rational(&self) -> PyResult { + RBig::try_from(self.0.clone()).map(RPy).map_err(conversion_error_to_py) +} + +// Precision/base introspection + arbitrary-radix formatting (no new Python type for base != 2,10, +// so with_base is exposed only as a string formatter via in_radix — see Step 8/__format__) +``` + +**File: `python/src/ratio.rs`** — add to `RPy`: + +```rust +// Rational -> Float, correctly rounded (lossy). RBig::to_float::(precision) is the +// dashu-float lossy conversion; pick base 2 (FPy) and base 10 (DPy) variants. +fn to_float(&self, precision: usize) -> FPy { + FPy(self.0.to_float::(precision).value()) +} +fn to_decimal(&self, precision: usize) -> DPy { + DPy(self.0.to_float::(precision).value()) +} +``` + +**Conversion summary** (after Steps 2b + 6c): + +| from → to | `FPy` | `DPy` | `RPy` | Python native | +|---|---|---|---|---| +| `FPy` | — | `.to_decimal()` | `.to_rational()` (exact) | `float()`, `int()`, `.to_decimal().unwrap()`→Decimal | +| `DPy` | `.to_binary()` | — | `.to_rational()` (exact) | `float()`, `int()`, `.unwrap()`→Decimal | +| `RPy` | `.to_float(p)` (lossy) | `.to_decimal(p)` (lossy) | — | `float()`, `int()`, `.unwrap()`→Fraction | +| `UPy`/`IPy` | `FBig::from` | `DBig::from` | `RBig::from` | `int()`, `.unwrap()` | + +**Notes:** +- `to_decimal`/`to_binary` return `Rounded<...>` in Rust — use `.value()` to drop the rounding flag (we don't expose per-call rounding flags to Python yet). +- `RBig::to_float` requires an explicit precision argument (rationals are generally non-terminating in base 2/10). This is the lossy counterpart to the exact `FBig → RBig` path. +- `with_base(radix)` for radix ∉ {2,10} has no Python wrapper type, so it's exposed only as a string formatter (`in_radix` / `__format__`, Step 8), not as a new numeric type. +- For `FPy → Decimal` directly: compose `f.to_decimal().unwrap()` (DPy → Decimal already exists). No separate method needed. + +--- + ## Step 7: Create math module **New file: `python/src/math.rs`** @@ -1319,17 +1445,19 @@ class Sign: ``` 1. Step 0 (PyO3 0.29 upgrade + Cargo.toml deps) — do first 2. Step 1 (fix panics) — independent -3. Step 2 (conversion helpers) — independent -4. Step 2c (global cache module) — independent (cache.rs + FpError mapper) -5. Step 3 (FPy/DPy arithmetic) — depends on Step 2 -6. Step 4 (RPy arithmetic) — depends on Step 2 -7. Step 5 (int methods) — independent (can parallel with 3-4) -8. Step 6 (float/rational pred + transcendentals via Step 2c) — depends on Step 3-4 + 2c -9. Step 7 (math module) — depends on Step 6 -10. Step 8 (format fix) — independent -11. Step 9 (stubs) — after all code changes -12. Step 10 (tests) — after all code changes -13. Step 11 (complex bindings) — after Step 6 + 2c (follows the float pattern) +3. Step 2 (conversion helpers: into_*) — independent +4. Step 2b (broaden __new__ via construct_*) — depends on Step 2 +5. Step 2c (global cache module) — independent (cache.rs + FpError mapper) +6. Step 3 (FPy/DPy arithmetic) — depends on Step 2 +7. Step 4 (RPy arithmetic) — depends on Step 2 +8. Step 5 (int methods) — independent (can parallel with 3-4) +9. Step 6 (float/rational pred + transcendentals via Step 2c) — depends on Step 3-4 + 2c +10. Step 6c (cross-type conversions: to_decimal/to_binary/to_rational/to_float) — depends on Step 6 +11. Step 7 (math module) — depends on Step 6 +12. Step 8 (format fix) — independent +13. Step 9 (stubs) — after all code changes +14. Step 10 (tests) — after all code changes +15. Step 11 (complex bindings) — after Step 6 + 2c (follows the float pattern) ``` Steps 3 and 5 can be done in parallel. Steps 3 and 4 share the same pattern so do them together. From aafd5443a1bf4c0fb55f64d1454c237aec6121bd Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Wed, 8 Jul 2026 23:47:02 +0800 Subject: [PATCH 12/20] Implement full dashu-python bindings (TODO-python.md) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade PyO3 0.20 → 0.29 (Bound API, IntoPyObject, edition 2024, rust-version 1.85; requires-python >=3.8) and implement every step of python/TODO-python.md: - FBig/DBig/RBig: arithmetic, comparison, __bool__, predicates, rounding, precision, transcendentals, and cross-type conversions (to_decimal/to_binary/ to_rational/to_float). Broadened __new__ accepts any Python number. - UBig/IBig: number theory (sqrt/cbrt/nth_root/gcd/gcd_ext/ilog/...), bit ops, __floordiv__/__divmod__, and in-place operators. Fixed the todo!() panics in __mod__/__pow__. - CBig (new): complex bindings wrapping a bare CBig with arithmetic, real/imag, conj/proj/norm/abs/arg, and transcendentals. - A module-level `math` API and a `dashu.Cache` handle. - Transcendentals are panic-free: routed through a shared thread-local ConstCache + the panic-free Context layer; domain errors raise ValueError and 0/0 raises ZeroDivisionError instead of crashing the session. All wrappers are Send+Sync (no #[pyclass(unsendable)]). FBig(f64) now constructs at f64's native precision so transcendentals are well-defined. Verified with maturin develop + 29 passing pytest tests; 0 clippy warnings, fmt clean. Co-Authored-By: Claude --- python/CHANGELOG.md | 41 +- python/Cargo.toml | 9 +- python/dashu.pyi | 528 ++++++++++++++++--- python/pyproject.toml | 5 +- python/src/cache.rs | 115 ++++ python/src/complex.rs | 292 ++++++++++ python/src/convert.rs | 204 +++++-- python/src/float.rs | 623 ++++++++++++++++++++-- python/src/int.rs | 936 ++++++++++++++++++++++++--------- python/src/lib.rs | 43 +- python/src/math.rs | 177 +++++++ python/src/ratio.rs | 232 +++++++- python/src/types.rs | 25 +- python/src/utils.rs | 48 +- python/src/words.rs | 45 +- python/tests/test_float_ops.py | 89 ++++ python/tests/test_int_math.py | 80 +++ python/tests/test_math.py | 47 ++ python/tests/test_ratio_ops.py | 59 +++ 19 files changed, 3122 insertions(+), 476 deletions(-) create mode 100644 python/src/cache.rs create mode 100644 python/src/complex.rs create mode 100644 python/src/math.rs create mode 100644 python/tests/test_float_ops.py create mode 100644 python/tests/test_int_math.py create mode 100644 python/tests/test_math.py create mode 100644 python/tests/test_ratio_ops.py diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 9f1789f5..7e6d173d 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -1,4 +1,41 @@ # Unreleased -- TODO: support pickle through __reduce__ -- TODO: support as much dunder methods as possible: https://docs.cython.org/en/latest/src/userguide/special_methods.html#special-methods \ No newline at end of file +### Add +- Upgraded PyO3 from 0.20 to 0.29 (modern `Bound` API, `IntoPyObject`, edition 2024, + rust-version 1.85). `requires-python` is now `>=3.8`. +- Added `dashu-cmplx` support: a new `CBig` Python type wrapping a bare complex number, + with arithmetic, `real`/`imag`, `conj`/`proj`/`norm`/`abs`/`arg`, transcendentals + (`sin`/`cos`/`tan`/`exp`/`ln`/`sqrt`/`powi`/`powf`), and `__complex__`. +- Arithmetic, comparison, and `__bool__` for `FBig`, `DBig`, and `RBig` (previously + only construction/repr/hash were exposed). +- Integer number theory and bit operations on `UBig`/`IBig`: `sqrt`/`cbrt`/`nth_root`, + `sqr`/`cubic`, `ilog`, `gcd`/`gcd_ext`, `count_ones`/`trailing_zeros`/…, + `__floordiv__`/`__divmod__`, and in-place operators. +- Float/rational methods: predicates (`is_zero`/`is_finite`/`is_infinite`), rounding + (`trunc`/`floor`/`ceil`/`round`/`fract`), `with_precision`/`precision`/`digits`, + `to_int`, `numerator`/`denominator`, `split_at_point`, `sqr`/`cubic`/`pow`. +- Cross-type conversions: `FBig.to_decimal`/`to_binary`/`to_rational`, + `RBig.to_float`/`to_decimal`. +- Broadened constructors: `FBig`/`DBig`/`RBig`/`CBig` now accept any Python number + (int/float/`Decimal`/`Fraction`) in addition to strings. +- A module-level `math` API (`sin`/`cos`/…/`exp`/`ln`/`sqrt`/`gcd`/`lcm`/…) and a + `dashu.Cache` handle for the global constant cache. +- Transcendentals are now panic-free: domain errors raise `ValueError`, + `0/0`-style indeterminate forms raise `ZeroDivisionError`, and overflow/underflow + produce signed infinities/zeros — routed through the panic-free `Context` layer with a + shared thread-local `ConstCache`. + +### Fix +- Removed the `todo!()` panics in `UBig.__mod__`, `IBig.__mod__`, and `IBig.__pow__`. + +### Changed +- `FBig(f64)` now constructs at f64's native precision (53 bits) so that subsequent + transcendental operations (which require precision > 0) are well-defined. + +# TODO (still open) +- support pickle through `__reduce__` +- support as much dunder methods as possible: + https://docs.cython.org/en/latest/src/userguide/special_methods.html#special-methods +- route bare `FBig`/`CBig` arithmetic through the `Context` layer (or guard it) so that + infinite-input / `0/0` cases raise Python exceptions instead of panicking + (transcendentals are already panic-free). diff --git a/python/Cargo.toml b/python/Cargo.toml index de298126..6c05a4cd 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Jacob Zhong "] edition = "2024" description = "Python binding for the dashu numeric types" keywords = ["mathematics", "numerics"] -categories = ["mathematics", "no-std"] +categories = ["mathematics", "science"] license = "MIT OR Apache-2.0" repository = "https://github.com/cmpute/dashu" homepage = "https://github.com/cmpute/dashu" @@ -17,16 +17,15 @@ rust-version = "1.85" all-features = true [dependencies] -dashu-base = { version = "0.4.0", path = "../base", features = ["std"]} +dashu-base = { version = "0.4.0", path = "../base", features = ["std"] } dashu-int = { version = "0.4.1", path = "../integer", features = ["std", "num-order"] } dashu-float = { version = "0.4.2", path = "../float", features = ["std", "num-order"] } dashu-ratio = { version = "0.4.1", path = "../rational", features = ["std", "num-order", "dashu-float"] } +dashu-cmplx = { version = "0.4.5", path = "../complex", features = ["std", "num-order"] } num-order = "1.2.0" -_num-modular = { optional = true, version = "0.6.1", package = "num-modular", default-features = false } - [dependencies.pyo3] -version = "0.20" +version = "0.29" features = ["extension-module"] [lib] diff --git a/python/dashu.pyi b/python/dashu.pyi index 47c4009c..04720253 100644 --- a/python/dashu.pyi +++ b/python/dashu.pyi @@ -1,104 +1,488 @@ -from typing import overload, Literal, TypeVar, Tuple, Iterable +from typing import overload, Literal, Optional, Tuple, Iterable, Union + +# A native Python number or any dashu number. +Number = Union[int, float, "UBig", "IBig", "FBig", "DBig", "RBig"] + +# Sign of a number. +class Sign: + Positive: Sign + Negative: Sign -Bigs = TypeVar('Bigs', UBig, IBig, FBig, DBig, RBig) -SignedBigs = TypeVar('SignedBigs', IBig, FBig, DBig, RBig) class Words: - def __init__(self, obj: list[int] | Words): ... + def __init__(self, obj: list[int] | Words) -> None: ... def __repr__(self) -> str: ... def __len__(self) -> int: ... @overload def __getitem__(self, index: int) -> int: ... @overload def __getitem__(self, index: slice) -> Words: ... - @overload - def __setitem__(self, index: int, value: int): ... - @overload - def __setitem__(self, index: slice, value: list[int] | Words): ... - def __delitem__(self, index: int | slice): ... + def __setitem__(self, index: int | slice, value: int | list[int] | Words) -> None: ... + def __delitem__(self, index: int | slice) -> None: ... def __add__(self, other: list[int] | Words) -> Words: ... def __mul__(self, count: int) -> Words: ... + class UBig: - def __init__(self, obj: int | str): ... + def __init__(self, obj: int | str, radix: Optional[int] = ...) -> None: ... def unwrap(self) -> int: ... def __repr__(self) -> str: ... def __str__(self) -> str: ... - def __format__(self) -> str: ... + def __format__(self, format_spec: str) -> str: ... def __hash__(self) -> int: ... - + def __bool__(self) -> bool: ... def __len__(self) -> int: ... @overload def __getitem__(self, index: int) -> bool: ... @overload def __getitem__(self, index: slice) -> UBig: ... - def __setitem__(self, index: int | slice, value: bool): ... - def __delitem__(self, index: int, slice): ... - + def __setitem__(self, index: int | slice, value: bool) -> None: ... + def __delitem__(self, index: int | slice) -> None: ... def __int__(self) -> int: ... - def to_words(self) -> Words: ... - @staticmethod - def from_words(words: Words) -> UBig: ... - def to_chunks(self, chunk_bits: int) -> Tuple[UBig]: ... - @staticmethod - def from_chunks(chunks: Iterable[UBig], chunk_bits: int) -> UBig: ... - def to_bytes(self, byteorder: Literal['little', 'big'] | None) -> bytes: ... - @staticmethod - def from_words(bytes: bytes, byteorder: Literal['little', 'big'] | None) -> UBig: ... - @overload - def __add__(self, other: Bigs) -> Bigs: ... - @overload - def __add__(self, other: int) -> IBig: ... - @overload - def __radd__(self, other: Bigs) -> Bigs: ... - @overload - def __radd__(self, other: int) -> IBig: ... - @overload - def __sub__(self, other: Bigs) -> Bigs: ... - @overload - def __sub__(self, other: int) -> IBig: ... - @overload - def __rsub__(self, other: Bigs) -> Bigs: ... - @overload - def __rsub__(self, other: int) -> IBig: ... - @overload - def __mul__(self, other: Bigs) -> Bigs: ... - @overload - def __mul__(self, other: int) -> IBig: ... - @overload - def __rmul__(self, other: Bigs) -> Bigs: ... - @overload - def __rmul__(self, other: int) -> IBig: ... - @overload - def __truediv__(self, other: Bigs) -> Bigs: ... - @overload - def __truediv__(self, other: int) -> IBig: ... - @overload - def __rtruediv__(self, other: Bigs) -> Bigs: ... - @overload - def __rtruediv__(self, other: int) -> IBig: ... - def __mod__(self, other: int | UBig | IBig) -> UBig: ... - def __pow__(self, exp: int | UBig, modulus: int | UBig | None) -> UBig: ... + # arithmetic (cross-type dispatch) + def __add__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __radd__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __sub__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __rsub__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __mul__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __rmul__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __truediv__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __rtruediv__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __floordiv__(self, other: int | UBig | IBig) -> UBig | IBig: ... + def __rfloordiv__(self, other: int | UBig | IBig) -> UBig | IBig: ... + def __mod__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __divmod__(self, other: int | UBig | IBig) -> tuple: ... + def __pow__(self, exp: int | UBig, modulus: Optional[int | UBig] = ...) -> UBig: ... + def __iadd__(self, other: UBig) -> None: ... + def __isub__(self, other: UBig) -> None: ... + def __imul__(self, other: UBig) -> None: ... + def __iand__(self, other: UBig) -> None: ... + def __ior__(self, other: UBig) -> None: ... + def __ixor__(self, other: UBig) -> None: ... + def __ilshift__(self, other: int) -> None: ... + def __irshift__(self, other: int) -> None: ... def __pos__(self) -> UBig: ... def __neg__(self) -> IBig: ... - def __abs__(self) -> IBig: ... - def __nonzero__(self) -> bool: ... + def __abs__(self) -> UBig: ... def __lshift__(self, other: int) -> UBig: ... def __rshift__(self, other: int) -> UBig: ... def __and__(self, other: int | UBig | IBig) -> UBig: ... - @overload - def __or__(self, other: UBig) -> UBig: ... - @overload - def __or__(self, other: int | IBig) -> IBig: ... - @overload - def __xor__(self, other: UBig) -> UBig: ... - @overload - def __xor__(self, other: int | IBig) -> IBig: ... + def __or__(self, other: int | UBig | IBig) -> UBig | IBig: ... + def __xor__(self, other: int | UBig | IBig) -> UBig | IBig: ... + # comparisons (cross-type) + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __lt__(self, other: Number) -> bool: ... + def __le__(self, other: Number) -> bool: ... + def __gt__(self, other: Number) -> bool: ... + def __ge__(self, other: Number) -> bool: ... + + # number theory & roots + def sqrt(self) -> UBig: ... + def cbrt(self) -> UBig: ... + def nth_root(self, n: int) -> UBig: ... + def sqr(self) -> UBig: ... + def cubic(self) -> UBig: ... + def ilog(self, base: UBig) -> int: ... + def is_multiple_of(self, divisor: UBig) -> bool: ... + def remove(self, factor: UBig) -> int: ... + def gcd(self, other: UBig) -> UBig: ... + def gcd_ext(self, other: UBig) -> tuple[UBig, IBig, IBig]: ... + # bit operations + def count_ones(self) -> int: ... + def count_zeros(self) -> Optional[int]: ... + def trailing_zeros(self) -> Optional[int]: ... + def trailing_ones(self) -> Optional[int]: ... + def is_power_of_two(self) -> bool: ... + def next_power_of_two(self) -> UBig: ... + # accessors + def is_one(self) -> bool: ... + @staticmethod + def ones(n: int) -> UBig: ... + # interop + def to_words(self) -> Words: ... + @staticmethod + def from_words(words: list[int] | Words) -> UBig: ... + def to_bytes(self, byteorder: Optional[Literal["little", "big"]] = ...) -> bytes: ... + @staticmethod + def from_bytes(bytes: bytes, byteorder: Optional[Literal["little", "big"]] = ...) -> UBig: ... + def to_chunks(self, chunk_bits: int) -> tuple[UBig, ...]: ... + @staticmethod + def from_chunks(chunks: Iterable[UBig], chunk_bits: int) -> UBig: ... + class IBig: - def __init__(self, obj: int | str): ... + def __init__(self, obj: int | str, radix: Optional[int] = ...) -> None: ... + def unwrap(self) -> int: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __format__(self, format_spec: str) -> str: ... + def __hash__(self) -> int: ... + def __bool__(self) -> bool: ... + def __len__(self) -> int: ... + def __getitem__(self, index: int) -> bool: ... + def __int__(self) -> int: ... + # arithmetic, comparisons, in-place: same cross-type surface as UBig + def __add__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __radd__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __sub__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __rsub__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __mul__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __rmul__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __truediv__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __rtruediv__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __floordiv__(self, other: int | UBig | IBig) -> IBig: ... + def __rfloordiv__(self, other: int | UBig | IBig) -> IBig: ... + def __mod__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... + def __divmod__(self, other: int | UBig | IBig) -> tuple: ... + def __pow__(self, exp: int | UBig, modulus: Optional[int | UBig] = ...) -> IBig: ... + def __iadd__(self, other: IBig) -> None: ... + def __isub__(self, other: IBig) -> None: ... + def __imul__(self, other: IBig) -> None: ... + def __iand__(self, other: IBig) -> None: ... + def __ior__(self, other: IBig) -> None: ... + def __ixor__(self, other: IBig) -> None: ... + def __ilshift__(self, other: int) -> None: ... + def __irshift__(self, other: int) -> None: ... + def __pos__(self) -> IBig: ... + def __neg__(self) -> IBig: ... + def __abs__(self) -> IBig: ... + def __invert__(self) -> IBig: ... + def __lshift__(self, other: int) -> IBig: ... + def __rshift__(self, other: int) -> IBig: ... + def __and__(self, other: int | UBig | IBig) -> UBig | IBig: ... + def __or__(self, other: int | UBig | IBig) -> UBig | IBig: ... + def __xor__(self, other: int | UBig | IBig) -> UBig | IBig: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __lt__(self, other: Number) -> bool: ... + def __le__(self, other: Number) -> bool: ... + def __gt__(self, other: Number) -> bool: ... + def __ge__(self, other: Number) -> bool: ... + # number theory & roots + def sqrt(self) -> UBig: ... + def cbrt(self) -> IBig: ... + def nth_root(self, n: int) -> IBig: ... + def sqr(self) -> UBig: ... + def cubic(self) -> IBig: ... + def ilog(self, base: UBig) -> int: ... + def trailing_zeros(self) -> Optional[int]: ... + def trailing_ones(self) -> Optional[int]: ... + def is_one(self) -> bool: ... + def sign(self) -> Sign: ... + def signum(self) -> IBig: ... + def is_negative(self) -> bool: ... + def is_positive(self) -> bool: ... + def to_parts(self) -> tuple[Sign, UBig]: ... + @staticmethod + def from_parts(sign: Sign, magnitude: UBig) -> IBig: ... + def as_ubig(self) -> Optional[UBig]: ... + @staticmethod + def ones(n: int) -> IBig: ... + def to_words(self) -> Words: ... + @staticmethod + def from_words(words: list[int] | Words) -> IBig: ... + def to_chunks(self, chunk_bits: int) -> tuple[UBig, ...]: ... + @staticmethod + def from_chunks(chunks: Iterable[UBig], chunk_bits: int) -> IBig: ... + def to_bytes( + self, byteorder: Optional[Literal["little", "big"]] = ..., signed: Optional[bool] = ... + ) -> bytes: ... + @staticmethod + def from_bytes( + bytes: bytes, + byteorder: Optional[Literal["little", "big"]] = ..., + signed: Optional[bool] = ..., + ) -> IBig: ... + + +class FBig: + def __init__(self, obj: float | int | str | "FBig") -> None: ... + def unwrap(self) -> tuple[IBig, int]: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __format__(self, format_spec: str) -> str: ... + def __hash__(self) -> int: ... + def __bool__(self) -> bool: ... + def __float__(self) -> float: ... + def __int__(self) -> int: ... + def __add__(self, other: Number) -> FBig: ... + def __radd__(self, other: Number) -> FBig: ... + def __sub__(self, other: Number) -> FBig: ... + def __rsub__(self, other: Number) -> FBig: ... + def __mul__(self, other: Number) -> FBig: ... + def __rmul__(self, other: Number) -> FBig: ... + def __truediv__(self, other: Number) -> FBig: ... + def __rtruediv__(self, other: Number) -> FBig: ... + def __mod__(self, other: Number) -> FBig: ... + def __rmod__(self, other: Number) -> FBig: ... + def __neg__(self) -> FBig: ... + def __pos__(self) -> FBig: ... + def __abs__(self) -> FBig: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __lt__(self, other: Number) -> bool: ... + def __le__(self, other: Number) -> bool: ... + def __gt__(self, other: Number) -> bool: ... + def __ge__(self, other: Number) -> bool: ... + # predicates & sign + def is_zero(self) -> bool: ... + def is_finite(self) -> bool: ... + def is_infinite(self) -> bool: ... + def sign(self) -> Sign: ... + # rounding + def trunc(self) -> FBig: ... + def floor(self) -> FBig: ... + def ceil(self) -> FBig: ... + def round(self) -> FBig: ... + def fract(self) -> FBig: ... + # precision & parts + def precision(self) -> int: ... + def digits(self) -> int: ... + def with_precision(self, precision: int) -> FBig: ... + @staticmethod + def from_parts(significand: IBig, exponent: int) -> FBig: ... + # conversions + def to_int(self) -> IBig: ... + def to_decimal(self) -> DBig: ... + def to_binary(self) -> FBig: ... + def to_rational(self) -> RBig: ... + # transcendentals (panic-free; domain errors raise ValueError) + def sin(self) -> FBig: ... + def cos(self) -> FBig: ... + def tan(self) -> FBig: ... + def asin(self) -> FBig: ... + def acos(self) -> FBig: ... + def atan(self) -> FBig: ... + def atan2(self, x: FBig) -> FBig: ... + def sinh(self) -> FBig: ... + def cosh(self) -> FBig: ... + def tanh(self) -> FBig: ... + def asinh(self) -> FBig: ... + def acosh(self) -> FBig: ... + def atanh(self) -> FBig: ... + def exp(self) -> FBig: ... + def exp_m1(self) -> FBig: ... + def ln(self) -> FBig: ... + def ln_1p(self) -> FBig: ... + def sqrt(self) -> FBig: ... + def cbrt(self) -> FBig: ... + def nth_root(self, n: int) -> FBig: ... + def powf(self, w: FBig) -> FBig: ... + def powi(self, n: IBig) -> FBig: ... + + +class DBig: + def __init__(self, obj: float | int | str | "DBig") -> None: ... + def unwrap(self) -> object: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __format__(self, format_spec: str) -> str: ... + def __hash__(self) -> int: ... + def __bool__(self) -> bool: ... + def __float__(self) -> float: ... + def __int__(self) -> int: ... + def __add__(self, other: Number) -> DBig: ... + def __radd__(self, other: Number) -> DBig: ... + def __sub__(self, other: Number) -> DBig: ... + def __rsub__(self, other: Number) -> DBig: ... + def __mul__(self, other: Number) -> DBig: ... + def __rmul__(self, other: Number) -> DBig: ... + def __truediv__(self, other: Number) -> DBig: ... + def __rtruediv__(self, other: Number) -> DBig: ... + def __mod__(self, other: Number) -> DBig: ... + def __rmod__(self, other: Number) -> DBig: ... + def __neg__(self) -> DBig: ... + def __pos__(self) -> DBig: ... + def __abs__(self) -> DBig: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __lt__(self, other: Number) -> bool: ... + def __le__(self, other: Number) -> bool: ... + def __gt__(self, other: Number) -> bool: ... + def __ge__(self, other: Number) -> bool: ... + def is_zero(self) -> bool: ... + def is_finite(self) -> bool: ... + def is_infinite(self) -> bool: ... + def sign(self) -> Sign: ... + def trunc(self) -> DBig: ... + def floor(self) -> DBig: ... + def ceil(self) -> DBig: ... + def round(self) -> DBig: ... + def fract(self) -> DBig: ... + def precision(self) -> int: ... + def digits(self) -> int: ... + def with_precision(self, precision: int) -> DBig: ... + @staticmethod + def from_parts(significand: IBig, exponent: int) -> DBig: ... + def to_int(self) -> IBig: ... + def to_decimal(self) -> DBig: ... + def to_binary(self) -> FBig: ... + def to_rational(self) -> RBig: ... + # transcendentals (same set as FBig) + def sin(self) -> DBig: ... + def cos(self) -> DBig: ... + def tan(self) -> DBig: ... + def asin(self) -> DBig: ... + def acos(self) -> DBig: ... + def atan(self) -> DBig: ... + def atan2(self, x: DBig) -> DBig: ... + def sinh(self) -> DBig: ... + def cosh(self) -> DBig: ... + def tanh(self) -> DBig: ... + def asinh(self) -> DBig: ... + def acosh(self) -> DBig: ... + def atanh(self) -> DBig: ... + def exp(self) -> DBig: ... + def exp_m1(self) -> DBig: ... + def ln(self) -> DBig: ... + def ln_1p(self) -> DBig: ... + def sqrt(self) -> DBig: ... + def cbrt(self) -> DBig: ... + def nth_root(self, n: int) -> DBig: ... + def powf(self, w: DBig) -> DBig: ... + def powi(self, n: IBig) -> DBig: ... + + +class RBig: + def __init__(self, obj: int | float | str | "RBig") -> None: ... + def unwrap(self) -> object: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __format__(self, format_spec: str) -> str: ... + def __hash__(self) -> int: ... + def __bool__(self) -> bool: ... + def __float__(self) -> float: ... + def __int__(self) -> int: ... + def __add__(self, other: Number) -> RBig: ... + def __radd__(self, other: Number) -> RBig: ... + def __sub__(self, other: Number) -> RBig: ... + def __rsub__(self, other: Number) -> RBig: ... + def __mul__(self, other: Number) -> RBig: ... + def __rmul__(self, other: Number) -> RBig: ... + def __truediv__(self, other: Number) -> RBig: ... + def __rtruediv__(self, other: Number) -> RBig: ... + def __mod__(self, other: Number) -> RBig: ... + def __rmod__(self, other: Number) -> RBig: ... + def __neg__(self) -> RBig: ... + def __pos__(self) -> RBig: ... + def __abs__(self) -> RBig: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __lt__(self, other: Number) -> bool: ... + def __le__(self, other: Number) -> bool: ... + def __gt__(self, other: Number) -> bool: ... + def __ge__(self, other: Number) -> bool: ... + @property + def numerator(self) -> IBig: ... + @property + def denominator(self) -> UBig: ... + def is_int(self) -> bool: ... + def is_one(self) -> bool: ... + def sign(self) -> Sign: ... + def signum(self) -> RBig: ... + def trunc(self) -> IBig: ... + def floor(self) -> IBig: ... + def ceil(self) -> IBig: ... + def round(self) -> IBig: ... + def fract(self) -> RBig: ... + def split_at_point(self) -> tuple[IBig, RBig]: ... + def sqr(self) -> RBig: ... + def cubic(self) -> RBig: ... + def pow(self, n: int) -> RBig: ... + def to_int(self) -> IBig: ... + def to_float(self, precision: int) -> FBig: ... + def to_decimal(self, precision: int) -> DBig: ... + @staticmethod + def from_parts(numerator: IBig, denominator: UBig) -> RBig: ... + @staticmethod + def simplest_from_float(f: FBig) -> Optional[RBig]: ... + + +class CBig: + def __init__( + self, re: float | int | str | FBig | "CBig" | complex, im: Optional[float | int | FBig] = ... + ) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __format__(self, format_spec: str) -> str: ... + def __hash__(self) -> int: ... + def __bool__(self) -> bool: ... + def __complex__(self) -> complex: ... + def __add__(self, other: float | int | FBig | CBig | complex) -> CBig: ... + def __radd__(self, other: float | int | FBig | complex) -> CBig: ... + def __sub__(self, other: float | int | FBig | CBig | complex) -> CBig: ... + def __rsub__(self, other: float | int | FBig | complex) -> CBig: ... + def __mul__(self, other: float | int | FBig | CBig | complex) -> CBig: ... + def __rmul__(self, other: float | int | FBig | complex) -> CBig: ... + def __truediv__(self, other: float | int | FBig | CBig | complex) -> CBig: ... + def __rtruediv__(self, other: float | int | FBig | complex) -> CBig: ... + def __neg__(self) -> CBig: ... + def __pos__(self) -> CBig: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def real(self) -> FBig: ... + def imag(self) -> FBig: ... + def precision(self) -> int: ... + def is_zero(self) -> bool: ... + def is_finite(self) -> bool: ... + def is_infinite(self) -> bool: ... + def conj(self) -> CBig: ... + def proj(self) -> CBig: ... + def norm(self) -> FBig: ... + def abs(self) -> FBig: ... + def arg(self) -> FBig: ... + @staticmethod + def from_parts(re: FBig, im: FBig) -> CBig: ... + def sin(self) -> CBig: ... + def cos(self) -> CBig: ... + def tan(self) -> CBig: ... + def exp(self) -> CBig: ... + def ln(self) -> CBig: ... + def sqrt(self) -> CBig: ... + def powi(self, n: IBig) -> CBig: ... + def powf(self, w: CBig) -> CBig: ... + + +class Cache: + @staticmethod + def clear() -> None: ... + @staticmethod + def total_terms() -> int: ... + @staticmethod + def total_words() -> int: ... + + +# module-level math functions (transcendentals are panic-free; domain errors raise ValueError) +def auto(obj: Number) -> "UBig|IBig|FBig|DBig|RBig": ... +def autos(s: str) -> "UBig|IBig|FBig|DBig|RBig": ... -class FBig: ... -class DBig: ... -class RBig: ... +def sin(x: FBig) -> FBig: ... +def cos(x: FBig) -> FBig: ... +def tan(x: FBig) -> FBig: ... +def asin(x: FBig) -> FBig: ... +def acos(x: FBig) -> FBig: ... +def atan(x: FBig) -> FBig: ... +def atan2(y: FBig, x: FBig) -> FBig: ... +def sinh(x: FBig) -> FBig: ... +def cosh(x: FBig) -> FBig: ... +def tanh(x: FBig) -> FBig: ... +def asinh(x: FBig) -> FBig: ... +def acosh(x: FBig) -> FBig: ... +def atanh(x: FBig) -> FBig: ... +def exp(x: FBig) -> FBig: ... +def expm1(x: FBig) -> FBig: ... +def log(x: FBig) -> FBig: ... +def log1p(x: FBig) -> FBig: ... +def ln(x: FBig) -> FBig: ... +def ln_1p(x: FBig) -> FBig: ... +def sqrt(x: FBig) -> FBig: ... +def cbrt(x: FBig) -> FBig: ... +def nth_root(x: FBig, n: int) -> FBig: ... +def powf(x: FBig, y: FBig) -> FBig: ... +def powi(x: FBig, n: IBig) -> FBig: ... +def hypot(x: FBig, y: FBig) -> FBig: ... +def gcd(a: UBig, b: UBig) -> UBig: ... +def gcd_ext(a: UBig, b: UBig) -> tuple[UBig, IBig, IBig]: ... +def lcm(a: UBig, b: UBig) -> UBig: ... diff --git a/python/pyproject.toml b/python/pyproject.toml index 66530ce1..e6f1668a 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,9 +4,10 @@ build-backend = "maturin" [project] name = "dashu-rs" -requires-python = ">=3.7" +version = "0.0.1" +requires-python = ">=3.8" classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", -] \ No newline at end of file +] diff --git a/python/src/cache.rs b/python/src/cache.rs new file mode 100644 index 00000000..72c3bb05 --- /dev/null +++ b/python/src/cache.rs @@ -0,0 +1,115 @@ +//! Global constant cache shared by all float/decimal/complex transcendentals. +//! +//! The cache memoizes the big-integer constants (π, ln2, ln10, …) that the +//! transcendental kernels recompute from scratch on every call unless a +//! [`ConstCache`] is threaded in. It is base-free and rounding-mode-free, so a +//! single instance serves base-2 floats (`FPy`), base-10 decimals (`DPy`) and +//! complex numbers (`CPy`) simultaneously, accumulating precision across calls. +//! +//! Keeping the cache here (in the module) rather than inside each value means the +//! values stay plain, `Send` + `Sync` `FBig`/`CBig` — no `#[pyclass(unsendable)]`, +//! and the binding is free-threaded-Python compatible. Each thread gets its own +//! thread-local cache (zero locking), recomputing the constants once. + +use std::cell::RefCell; + +use dashu_base::Sign; +use dashu_float::{ConstCache, Context, FBig, FpError, FpResult, Repr, Word, round::Round}; +use pyo3::exceptions::{PyValueError, PyZeroDivisionError}; +use pyo3::prelude::*; + +thread_local! { + /// One cache per thread — zero locking, zero contention. Accumulates precision + /// across calls and never needs explicit invalidation. + static CONST_CACHE: RefCell = const { RefCell::new(ConstCache::new()) }; +} + +/// Borrow the thread-local cache mutably for one transcendental call. +pub fn with_cache(f: impl FnOnce(&mut ConstCache) -> R) -> R { + CONST_CACHE.with(|c| f(&mut c.borrow_mut())) +} + +/// Map a float `FpResult>` to a `PyResult`: +/// - `Overflow(sign)` / `Underflow(sign)` → signed ∞ / signed 0 (graceful, like Python float); +/// - `InfiniteInput` / `OutOfDomain` → `ValueError`; +/// - `Indeterminate` (e.g. `0/0`) → `ZeroDivisionError`. +/// +/// This replaces the panicking `Context::unwrap_fp`, which aborts the session on the +/// last three cases. `ctx` is the value's own context, used to rebuild ∞ / 0 results. +pub fn unwrap_float( + res: FpResult>, + ctx: Context, +) -> PyResult> { + match res { + Ok(rounded) => Ok(rounded.value()), + Err(FpError::Overflow(sign)) => { + let repr = match sign { + Sign::Positive => Repr::::infinity(), + Sign::Negative => Repr::::neg_infinity(), + }; + Ok(FBig::from_repr(repr, ctx)) + } + Err(FpError::Underflow(sign)) => { + let repr = match sign { + Sign::Positive => Repr::::zero(), + Sign::Negative => Repr::::neg_zero(), + }; + Ok(FBig::from_repr(repr, ctx)) + } + Err(FpError::InfiniteInput) => Err(PyValueError::new_err("arithmetic with infinity")), + Err(FpError::OutOfDomain) => Err(PyValueError::new_err("math domain error")), + Err(FpError::Indeterminate) => { + Err(PyZeroDivisionError::new_err("indeterminate form (0/0)")) + } + } +} + +/// Map a complex `CfpResult>` to a `PyResult`, mirroring +/// [`unwrap_float`]. Complex overflow/underflow rebuilds a CBig whose real part carries the +/// signed ∞ / 0 (a conventional choice, matching the real-valued overflow direction). +pub fn unwrap_complex( + res: dashu_cmplx::CfpResult, + ctx: dashu_cmplx::Context, +) -> PyResult> { + match res { + Ok(rounded) => Ok(rounded.value()), + Err(FpError::Overflow(sign)) => { + let re = match sign { + Sign::Positive => Repr::::infinity(), + Sign::Negative => Repr::::neg_infinity(), + }; + Ok(dashu_cmplx::CBig::new(re, Repr::::zero(), ctx)) + } + Err(FpError::Underflow(_sign)) => { + Ok(dashu_cmplx::CBig::new(Repr::::zero(), Repr::::zero(), ctx)) + } + Err(FpError::InfiniteInput) => Err(PyValueError::new_err("arithmetic with infinity")), + Err(FpError::OutOfDomain) => Err(PyValueError::new_err("math domain error")), + Err(FpError::Indeterminate) => { + Err(PyZeroDivisionError::new_err("indeterminate form (0/0)")) + } + } +} + +/// Optional Python-facing handle for cache inspection / clearing. +#[pyclass(name = "Cache")] +pub struct PyCache; + +#[pymethods] +impl PyCache { + /// Drop all memoized constants (rarely needed — the cache only grows usefully). + #[staticmethod] + fn clear() { + with_cache(|c| c.clear()); + } + /// Total number of terms accumulated across all cached constants. + #[staticmethod] + fn total_terms() -> usize { + with_cache(|c| c.total_terms()) + } + /// Total number of machine words accumulated across all cached constants. + #[staticmethod] + fn total_words() -> usize { + with_cache(|c| c.total_words()) + } +} diff --git a/python/src/complex.rs b/python/src/complex.rs new file mode 100644 index 00000000..084ebf11 --- /dev/null +++ b/python/src/complex.rs @@ -0,0 +1,292 @@ +//! Python bindings for arbitrary-precision complex numbers ([`dashu_cmplx::CBig`]). +//! +//! `CPy` wraps a *bare* `CBig`. Transcendentals route through the module-global +//! [`ConstCache`](dashu_float::ConstCache) + the panic-free complex `Context` layer (see +//! [`crate::cache::unwrap_complex`]); `abs`/`arg`/`norm` route through the float layer. +//! Arithmetic accepts any real number (FPy/int/float/Decimal/Python complex) by promoting it +//! to a complex with zero imaginary part, so `CBig(1, 0) + 2.0` works as expected. + +use std::ops::{Add, Div, Mul, Sub}; +use std::str::FromStr; + +use dashu_cmplx::CBig; +use dashu_float::FBig; +use pyo3::{ + Bound, IntoPyObjectExt, Py, PyAny, PyResult, + basic::CompareOp, + exceptions::PyTypeError, + intern, + prelude::*, + types::{PyComplex, PyFloat, PyInt}, +}; + +use crate::{ + cache::{unwrap_complex, unwrap_float, with_cache}, + convert::{conversion_error_to_py, parse_error_to_py, parse_to_ibig, parse_to_long}, + types::{CPy, DPy, FPy}, +}; + +/// Promote any real Python number (or a `(re, im)` pair / Python `complex`) to a bare `CBig`. +fn to_cbig(ob: &Bound<'_, PyAny>) -> PyResult { + if let Ok(c) = ob.extract::>() { + return Ok(c.0.clone()); + } + if let Ok(f) = ob.extract::>() { + return Ok(CBig::from(f.0.clone())); + } + if let Ok(d) = ob.extract::>() { + // base 10 -> base 2 via the correctly-rounded conversion, then embed as z + 0i + return Ok(CBig::from(d.0.to_binary().value())); + } + if ob.is_instance_of::() { + let re: f64 = ob.getattr(intern!(ob.py(), "real"))?.extract()?; + let im: f64 = ob.getattr(intern!(ob.py(), "imag"))?.extract()?; + let re = FBig::try_from(re).map_err(conversion_error_to_py)?; + let im = FBig::try_from(im).map_err(conversion_error_to_py)?; + return Ok(CBig::from_parts(re, im)); + } + if ob.is_instance_of::() { + let (v, overflow) = parse_to_long(ob)?; + let i = if overflow { + parse_to_ibig(ob)? + } else { + v.into() + }; + return Ok(CBig::from(i)); + } + if ob.is_instance_of::() { + let f: f64 = ob.extract()?; + let f = FBig::try_from(f).map_err(conversion_error_to_py)?; + return Ok(CBig::from(f)); + } + if let Ok(s) = ob.extract::() { + return CBig::from_str(&s).map_err(parse_error_to_py); + } + Err(PyTypeError::new_err("expected a CBig, float, int, complex, or string")) +} + +macro_rules! impl_cpy_binops { + ($method:ident, $rs_method:ident) => { + fn $method(lhs: &CPy, rhs: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + let rhs = to_cbig(rhs)?; + CPy((&lhs.0).$rs_method(&rhs)).into_py_any(py) + } + }; + ($method:ident, $rev_method:ident, $rs_method:ident) => { + impl_cpy_binops!($method, $rs_method); + fn $rev_method(lhs: &Bound<'_, PyAny>, rhs: &CPy, py: Python<'_>) -> PyResult> { + let lhs = to_cbig(lhs)?; + CPy(lhs.$rs_method(&rhs.0)).into_py_any(py) + } + }; +} + +impl_cpy_binops!(cpy_add, add); +impl_cpy_binops!(cpy_sub, cpy_rsub, sub); +impl_cpy_binops!(cpy_mul, mul); +impl_cpy_binops!(cpy_div, cpy_rdiv, div); + +#[pymethods] +impl CPy { + #[new] + #[pyo3(signature = (re, im=None))] + fn __new__(re: &Bound<'_, PyAny>, im: Option<&Bound<'_, PyAny>>) -> PyResult { + // CBig(re, im): build from two real parts + if let Some(im_ob) = im { + let re_f = to_cbig(re)?.into_parts().0; + let im_f = to_cbig(im_ob)?.into_parts().0; + return Ok(CPy(CBig::from_parts(re_f, im_f))); + } + // single argument forms + if let Ok(s) = re.extract::() { + return Ok(CPy(CBig::from_str(&s).map_err(parse_error_to_py)?)); + } + // (re, im) pair of binary floats + if let Ok(t) = re.extract::<(PyRef, PyRef)>() { + return Ok(CPy(CBig::from_parts(t.0.0.clone(), t.1.0.clone()))); + } + if let Ok(c) = re.extract::>() { + return Ok(CPy(c.0.clone())); + } + // any real number / Python complex -> promote + Ok(CPy(to_cbig(re)?)) + } + + fn __repr__(&self) -> String { + format!("", self.0) + } + fn __str__(&self) -> String { + format!("{}", self.0) + } + fn __format__(&self, _format_spec: &str) -> String { + format!("{}", self.0) + } + fn __hash__(&self) -> u64 { + // mirror Python's complex hash convention loosely: combine real/imag float hashes + let (re, im) = self.0.clone().into_parts(); + let re_hash = re.to_f64().value().to_bits(); + let im_hash = im.to_f64().value().to_bits(); + re_hash.wrapping_add(im_hash.wrapping_mul(3)) + } + fn __bool__(&self) -> bool { + !self.0.is_zero() + } + /// Complex numbers have no ordering — only `==` and `!=` are defined. + fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult { + let other = to_cbig(other)?; + match op { + CompareOp::Eq => Ok(self.0 == other), + CompareOp::Ne => Ok(self.0 != other), + _ => Err(PyTypeError::new_err("no ordering relation is defined for complex numbers")), + } + } + + fn __neg__(&self) -> Self { + CPy(-&self.0) + } + #[inline] + fn __pos__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + /********** arithmetic **********/ + #[inline] + fn __add__(&self, other: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + cpy_add(self, other, py) + } + #[inline] + fn __radd__(&self, other: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + cpy_add(self, other, py) + } + #[inline] + fn __sub__(&self, other: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + cpy_sub(self, other, py) + } + #[inline] + fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + cpy_rsub(other, self, py) + } + #[inline] + fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + cpy_mul(self, other, py) + } + #[inline] + fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + cpy_mul(self, other, py) + } + #[inline] + fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + cpy_div(self, other, py) + } + #[inline] + fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult> { + cpy_rdiv(other, self, py) + } + + /********** accessors & predicates **********/ + fn real(&self) -> FPy { + FPy(self.0.clone().into_parts().0) + } + fn imag(&self) -> FPy { + FPy(self.0.clone().into_parts().1) + } + fn precision(&self) -> usize { + self.0.precision() + } + fn is_zero(&self) -> bool { + self.0.is_zero() + } + fn is_finite(&self) -> bool { + self.0.is_finite() + } + fn is_infinite(&self) -> bool { + self.0.is_infinite() + } + + /********** complex-specific (algebraic) **********/ + fn conj(&self) -> Self { + CPy(self.0.conj()) + } + fn proj(&self) -> Self { + CPy(self.0.proj()) + } + /// Squared modulus (algebraic, exact): re² + im². + fn norm(&self) -> PyResult { + let ctx = self.0.context(); + let res = ctx.norm(&self.0); + Ok(FPy(unwrap_float(res, ctx_to_float(&ctx))?)) + } + /// Modulus |z| (algebraic — hypot; routed through the float Context layer for clean errors). + fn abs(&self) -> PyResult { + let ctx = self.0.context(); + let res = ctx.abs(&self.0); + Ok(FPy(unwrap_float(res, ctx_to_float(&ctx))?)) + } + /// Argument (phase) of z. + fn arg(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.arg(&self.0, Some(c))); + Ok(FPy(unwrap_float(res, ctx_to_float(&ctx))?)) + } + + /// Convert to a native Python `complex` (lossy, via f64). + fn __complex__(&self, py: Python<'_>) -> PyResult> { + let (re, im) = self.0.clone().into_parts(); + let re = re.to_f64().value(); + let im = im.to_f64().value(); + PyComplex::from_doubles(py, re, im).into_py_any(py) + } + + #[staticmethod] + fn from_parts(re: &FPy, im: &FPy) -> Self { + CPy(CBig::from_parts(re.0.clone(), im.0.clone())) + } + + /********** transcendentals (global cache + complex Context layer) **********/ + fn sin(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.sin(&self.0, Some(c))); + Ok(Self(unwrap_complex(res, ctx)?)) + } + fn cos(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.cos(&self.0, Some(c))); + Ok(Self(unwrap_complex(res, ctx)?)) + } + fn tan(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.tan(&self.0, Some(c))); + Ok(Self(unwrap_complex(res, ctx)?)) + } + fn exp(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.exp(&self.0, Some(c))); + Ok(Self(unwrap_complex(res, ctx)?)) + } + fn ln(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.log(&self.0, Some(c))); + Ok(Self(unwrap_complex(res, ctx)?)) + } + fn sqrt(&self) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_complex(ctx.sqrt(&self.0), ctx)?)) + } + fn powi(&self, n: &crate::types::IPy) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_complex(ctx.powi(&self.0, n.0.clone()), ctx)?)) + } + fn powf(&self, w: &Self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.powf(&self.0, &w.0, Some(c))); + Ok(Self(unwrap_complex(res, ctx)?)) + } +} + +/// The complex `Context` wraps a float `Context`; recover it for the real-valued +/// `abs`/`arg`/`norm` results that go through [`crate::cache::unwrap_float`]. +fn ctx_to_float( + ctx: &dashu_cmplx::Context, +) -> dashu_float::Context { + dashu_float::Context::new(ctx.precision()) +} diff --git a/python/src/convert.rs b/python/src/convert.rs index 3407ebfb..1d830dca 100644 --- a/python/src/convert.rs +++ b/python/src/convert.rs @@ -4,24 +4,27 @@ //! but it should happen when both PyO3 and this crate have a relatively stable API. use pyo3::{ - FromPyObject, PyAny, PyErr, PyObject, + Bound, FromPyObject, PyErr, PyResult, exceptions::{PySyntaxError, PyTypeError, PyValueError}, ffi, intern, prelude::*, - types::{PyBytes, PyDict, PyFloat, PyLong}, + types::{PyBytes, PyDict, PyFloat, PyInt}, }; use std::os::raw::{c_double, c_longlong}; use std::str::FromStr; use crate::types::*; use dashu_base::{ConversionError, ParseError}; -use dashu_float::DBig; +use dashu_float::{DBig, FBig}; use dashu_int::{IBig, UBig}; use dashu_ratio::RBig; const ERRMSG_NAN_NOT_SUPPORTED: &str = "nan values are not supported by dashu types"; const ERRMSG_UNIINPUT_PARSE_FAILED: &str = "the input is an invalid number or unsupported"; const ERRMSG_INPUT_NOT_UBIG: &str = "the input is not an unsigned integer"; +const ERRMSG_DECIMAL_WITH_BINARY: &str = "decimal values cannot be mixed with binary floats; convert explicitly with to_binary()/to_decimal()"; +const ERRMSG_BINARY_WITH_DECIMAL: &str = + "binary floats cannot be mixed with decimals; convert explicitly with to_binary()/to_decimal()"; pub fn parse_signed_index(index: isize, length: usize, unlimited: bool) -> Option { if index >= 0 { @@ -61,7 +64,7 @@ pub fn parse_error_to_py(error: ParseError) -> PyErr { /// Conversion from python integer object to rust int, without type checking. /// Returns the parsed number (when success) and the overflow flag. -pub fn parse_to_long(ob: &PyAny) -> PyResult<(c_longlong, bool)> { +pub fn parse_to_long(ob: &Bound<'_, PyAny>) -> PyResult<(c_longlong, bool)> { let py = ob.py(); unsafe { @@ -78,31 +81,29 @@ pub fn parse_to_long(ob: &PyAny) -> PyResult<(c_longlong, bool)> { } /// Conversion from python integer object to UBig instance, without type checking. -pub fn parse_to_ubig(ob: &PyAny) -> PyResult { +pub fn parse_to_ubig(ob: &Bound<'_, PyAny>) -> PyResult { let py = ob.py(); let bit_len: usize = ob.call_method0(intern!(py, "bit_length"))?.extract()?; - let byte_len = (bit_len + 7) / 8; + let byte_len = bit_len.div_ceil(8); // The most efficient way here is to use ffi::_PyLong_AsByteArray. // However, the conversion should not performed frequently, so the stable // API `to_bytes` is preferred here. - let bytes: &PyBytes = ob - .call_method1(intern!(py, "to_bytes"), (byte_len, intern!(py, "little")))? - .downcast()?; + let bytes_obj = ob.call_method1(intern!(py, "to_bytes"), (byte_len, intern!(py, "little")))?; + let bytes = bytes_obj.cast::()?; Ok(UBig::from_le_bytes(bytes.as_bytes())) } /// Conversion from UBig instance to python integer object -pub fn convert_from_ubig(ob: &UBig, py: Python) -> PyResult { +pub fn convert_from_ubig<'py>(ob: &UBig, py: Python<'py>) -> PyResult> { let bytes = ob.to_le_bytes(); let bytes_obj = PyBytes::new(py, &bytes); - py.get_type::() + py.get_type::() .call_method1(intern!(py, "from_bytes"), (bytes_obj, intern!(py, "little"))) - .map(PyObject::from) } /// Conversion from python integer object to IBig instance, without type checking. -pub fn parse_to_ibig(ob: &PyAny) -> PyResult { +pub fn parse_to_ibig(ob: &Bound<'_, PyAny>) -> PyResult { let py = ob.py(); let bit_len: usize = ob.call_method0(intern!(py, "bit_length"))?.extract()?; let byte_len = bit_len / 8 + 1; // extra byte for sign @@ -110,26 +111,28 @@ pub fn parse_to_ibig(ob: &PyAny) -> PyResult { // The stable API `to_bytes` is also chosen over ffi::_PyLong_AsByteArray here. let kwargs = PyDict::new(py); kwargs.set_item(intern!(py, "signed"), true).unwrap(); - let bytes: &PyBytes = ob - .call_method(intern!(py, "to_bytes"), (byte_len, intern!(py, "little")), Some(kwargs))? - .downcast()?; + let bytes_obj = + ob.call_method(intern!(py, "to_bytes"), (byte_len, intern!(py, "little")), Some(&kwargs))?; + let bytes = bytes_obj.cast::()?; Ok(IBig::from_le_bytes(bytes.as_bytes())) } /// Conversion from IBig instance to python integer object -pub fn convert_from_ibig(ob: &IBig, py: Python) -> PyResult { +pub fn convert_from_ibig<'py>(ob: &IBig, py: Python<'py>) -> PyResult> { let bytes = ob.to_le_bytes(); let bytes_obj = PyBytes::new(py, &bytes); let kwargs = PyDict::new(py); kwargs.set_item(intern!(py, "signed"), true).unwrap(); - py.get_type::() - .call_method(intern!(py, "from_bytes"), (bytes_obj, intern!(py, "little")), Some(kwargs)) - .map(PyObject::from) + py.get_type::().call_method( + intern!(py, "from_bytes"), + (bytes_obj, intern!(py, "little")), + Some(&kwargs), + ) } /// Conversion from decimal.Decimal object to DBig instance, without type checking. -pub fn parse_to_dbig(ob: &PyAny) -> PyResult { +pub fn parse_to_dbig(ob: &Bound<'_, PyAny>) -> PyResult { // use string to convert Decimal to DBig is okay, because Decimal.__format__ will // produce string in scientific notation. It will not produce many zeros when the // exponent is large. @@ -138,30 +141,31 @@ pub fn parse_to_dbig(ob: &PyAny) -> PyResult { } /// Conversion from fractions.Fraction object to RBig instance, without type checking. -pub fn parse_to_rbig(ob: &PyAny) -> PyResult { +pub fn parse_to_rbig(ob: &Bound<'_, PyAny>) -> PyResult { let py = ob.py(); - let num = parse_to_ibig(ob.getattr(intern!(py, "numerator"))?)?; - let den = parse_to_ibig(ob.getattr(intern!(py, "denominator"))?)?; + let num = parse_to_ibig(&ob.getattr(intern!(py, "numerator"))?)?; + let den = parse_to_ibig(&ob.getattr(intern!(py, "denominator"))?)?; let den: UBig = den.try_into().unwrap(); // this should be ensured by the Fraction type. Ok(RBig::from_parts(num, den)) } /// Conversion from RBig instance to fractions.Fraction object -pub fn convert_from_rbig(ob: &RBig, py: Python<'_>) -> PyResult { +pub fn convert_from_rbig<'py>(ob: &RBig, py: Python<'py>) -> PyResult> { let fractions = py.import(intern!(py, "fractions"))?; let fraction_type = fractions.getattr(intern!(py, "Fraction"))?; let num = convert_from_ibig(ob.numerator(), py)?; let den = convert_from_ubig(ob.denominator(), py)?; - fraction_type.call1((num, den)).map(PyObject::from) + fraction_type.call1((num, den)) } -impl<'source> FromPyObject<'source> for UniInput<'source> { - fn extract(ob: &'source PyAny) -> PyResult { - if ob.is_instance_of::() { - let (v, overflow) = parse_to_long(ob)?; +impl<'a, 'py> FromPyObject<'a, 'py> for UniInput<'py> { + type Error = PyErr; + fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult { + if ob.is_instance_of::() { + let (v, overflow) = parse_to_long(&ob)?; if overflow { - Ok(Self::OBInt(parse_to_ibig(ob)?)) + Ok(Self::OBInt(parse_to_ibig(&ob)?)) } else if v < 0 { Ok(Self::Int(v)) } else { @@ -174,15 +178,15 @@ impl<'source> FromPyObject<'source> for UniInput<'source> { } else { Ok(Self::Float(f)) } - } else if let Ok(u) = as FromPyObject>::extract(ob) { + } else if let Ok(u) = ob.extract::>() { Ok(Self::BUint(u)) - } else if let Ok(i) = as FromPyObject>::extract(ob) { + } else if let Ok(i) = ob.extract::>() { Ok(Self::BInt(i)) - } else if let Ok(f) = as FromPyObject>::extract(ob) { + } else if let Ok(f) = ob.extract::>() { Ok(Self::BFloat(f)) - } else if let Ok(d) = as FromPyObject>::extract(ob) { + } else if let Ok(d) = ob.extract::>() { Ok(Self::BDecimal(d)) - } else if let Ok(r) = as FromPyObject>::extract(ob) { + } else if let Ok(r) = ob.extract::>() { Ok(Self::BRational(r)) } else { // slow path: @@ -194,10 +198,10 @@ impl<'source> FromPyObject<'source> for UniInput<'source> { let fraction_type = fractions.getattr(intern!(py, "Fraction"))?; // and check whether the input is an instance of them - if ob.is_instance(decimal_type)? { - Ok(Self::OBDecimal(parse_to_dbig(ob)?)) - } else if ob.is_instance(fraction_type)? { - Ok(Self::OBRational(parse_to_rbig(ob)?)) + if ob.is_instance(&decimal_type)? { + Ok(Self::OBDecimal(parse_to_dbig(&ob)?)) + } else if ob.is_instance(&fraction_type)? { + Ok(Self::OBRational(parse_to_rbig(&ob)?)) } else { Err(PyTypeError::new_err(ERRMSG_UNIINPUT_PARSE_FAILED)) } @@ -206,7 +210,7 @@ impl<'source> FromPyObject<'source> for UniInput<'source> { } impl<'a> UniInput<'a> { - pub fn to_ubig(self) -> PyResult { + pub fn into_ubig(self) -> PyResult { let err = PyTypeError::new_err(ERRMSG_INPUT_NOT_UBIG); match self { Self::Uint(x) => Ok(x.into()), @@ -222,4 +226,122 @@ impl<'a> UniInput<'a> { _ => Err(err), } } + + /// Strict conversion of any numeric input to a binary float (`FPy`). + /// Decimals are rejected (convert explicitly). + pub fn into_fpy(self) -> PyResult { + match self { + Self::Uint(x) => Ok(FPy(FBig::from(UBig::from(x)))), + Self::Int(x) => Ok(FPy(FBig::from(IBig::from(x)))), + Self::BUint(x) => Ok(FPy(FBig::from(x.0.clone()))), + Self::BInt(x) => Ok(FPy(FBig::from(x.0.clone()))), + Self::OBInt(x) => Ok(FPy(FBig::from(x))), + Self::Float(x) => FBig::try_from(x).map(FPy).map_err(conversion_error_to_py), + Self::BFloat(x) => Ok(FPy(x.0.clone())), + Self::BRational(x) => FBig::try_from(x.0.clone()) + .map(FPy) + .map_err(conversion_error_to_py), + Self::OBRational(x) => FBig::try_from(x).map(FPy).map_err(conversion_error_to_py), + Self::BDecimal(_) | Self::OBDecimal(_) => { + Err(PyTypeError::new_err(ERRMSG_DECIMAL_WITH_BINARY)) + } + } + } + + /// Strict conversion of any numeric input to a decimal float (`DPy`). + /// Binary floats are rejected (convert explicitly). + pub fn into_dpy(self) -> PyResult { + match self { + Self::Uint(x) => Ok(DPy(DBig::from(UBig::from(x)))), + Self::Int(x) => Ok(DPy(DBig::from(IBig::from(x)))), + Self::BUint(x) => Ok(DPy(DBig::from(x.0.clone()))), + Self::BInt(x) => Ok(DPy(DBig::from(x.0.clone()))), + Self::OBInt(x) => Ok(DPy(DBig::from(x))), + // base-10 floats have no direct TryFrom; round-trip through the + // shortest scientific-notation string that f64 produces. + Self::Float(x) => { + let s = format!("{:e}", x); + DBig::from_str(&s).map(DPy).map_err(parse_error_to_py) + } + Self::BDecimal(x) => Ok(DPy(x.0.clone())), + Self::OBDecimal(x) => Ok(DPy(x)), + Self::BRational(x) => DBig::try_from(x.0.clone()) + .map(DPy) + .map_err(conversion_error_to_py), + Self::OBRational(x) => DBig::try_from(x).map(DPy).map_err(conversion_error_to_py), + Self::BFloat(_) => Err(PyTypeError::new_err(ERRMSG_BINARY_WITH_DECIMAL)), + } + } + + /// Strict conversion of any numeric input to a rational (`RPy`). Exact-only for + /// floats and big floats. + pub fn into_rpy(self) -> PyResult { + match self { + Self::Uint(x) => Ok(RPy(RBig::from(x))), + Self::Int(x) => Ok(RPy(RBig::from(x))), + Self::BUint(x) => Ok(RPy(RBig::from(x.0.clone()))), + Self::BInt(x) => Ok(RPy(RBig::from(x.0.clone()))), + Self::OBInt(x) => Ok(RPy(RBig::from(x))), + Self::Float(x) => RBig::try_from(x).map(RPy).map_err(conversion_error_to_py), + Self::BFloat(x) => RBig::try_from(x.0.clone()) + .map(RPy) + .map_err(conversion_error_to_py), + Self::BDecimal(x) => RBig::try_from(x.0.clone()) + .map(RPy) + .map_err(conversion_error_to_py), + Self::OBDecimal(x) => RBig::try_from(x).map(RPy).map_err(conversion_error_to_py), + Self::BRational(x) => Ok(RPy(x.0.clone())), + Self::OBRational(x) => Ok(RPy(x)), + } + } + + /// Permissive construction of a binary float from any Python number (used by + /// `FBig.__new__`). Unlike [`UniInput::into_fpy`], this ACCEPTS `Decimal`/`Fraction` + /// by routing them through the correctly-rounded base conversion / exact rational path. + pub fn construct_fpy(self) -> PyResult { + match self { + Self::Uint(x) => Ok(FBig::from(UBig::from(x))), + Self::Int(x) => Ok(FBig::from(IBig::from(x))), + Self::BUint(x) => Ok(FBig::from(x.0.clone())), + Self::BInt(x) => Ok(FBig::from(x.0.clone())), + Self::OBInt(x) => Ok(FBig::from(x)), + Self::Float(x) => FBig::try_from(x) + .map(|f| f.with_precision(f64::MANTISSA_DIGITS as usize).value()) + .map_err(conversion_error_to_py), + Self::BFloat(x) => Ok(x.0.clone()), + // Decimal -> base-2 via the correctly-rounded to_binary conversion + Self::BDecimal(x) => Ok(x.0.to_binary().value()), + Self::OBDecimal(x) => Ok(x.to_binary().value()), + Self::BRational(x) => FBig::try_from(x.0.clone()).map_err(conversion_error_to_py), + Self::OBRational(x) => FBig::try_from(x).map_err(conversion_error_to_py), + } + } + + /// Permissive construction of a decimal float from any Python number (used by + /// `DBig.__new__`). Binary floats are routed through the correctly-rounded + /// `to_decimal` conversion rather than being rejected. + pub fn construct_dpy(self) -> PyResult { + match self { + Self::Uint(x) => Ok(DBig::from(UBig::from(x))), + Self::Int(x) => Ok(DBig::from(IBig::from(x))), + Self::BUint(x) => Ok(DBig::from(x.0.clone())), + Self::BInt(x) => Ok(DBig::from(x.0.clone())), + Self::OBInt(x) => Ok(DBig::from(x)), + Self::Float(x) => { + let s = format!("{:e}", x); + DBig::from_str(&s).map_err(parse_error_to_py) + } + Self::BFloat(x) => Ok(x.0.to_decimal().value()), + Self::BDecimal(x) => Ok(x.0.clone()), + Self::OBDecimal(x) => Ok(x), + Self::BRational(x) => DBig::try_from(x.0.clone()).map_err(conversion_error_to_py), + Self::OBRational(x) => DBig::try_from(x).map_err(conversion_error_to_py), + } + } + + /// Permissive construction of a rational from any Python number (used by + /// `RBig.__new__`). Same rules as [`UniInput::into_rpy`]. + pub fn construct_rpy(self) -> PyResult { + Ok(self.into_rpy()?.0) + } } diff --git a/python/src/float.rs b/python/src/float.rs index 6b3b3299..dcce2d64 100644 --- a/python/src/float.rs +++ b/python/src/float.rs @@ -1,44 +1,108 @@ use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; +use std::ops::{Add, Div, Mul, Rem, Sub}; use std::str::FromStr; -use dashu_float::DBig; -use num_order::NumHash; -use pyo3::{exceptions::PyTypeError, intern, prelude::*, types::PyFloat}; -type FBig = dashu_float::FBig; +use dashu_base::Abs; +use dashu_float::{DBig, FBig}; +use num_order::{NumHash, NumOrd}; +use pyo3::{ + Bound, IntoPyObjectExt, Py, PyAny, PyResult, basic::CompareOp, intern, prelude::*, + types::PyFloat, +}; use crate::{ + cache::{unwrap_float, with_cache}, convert::{conversion_error_to_py, parse_error_to_py, parse_to_dbig}, - types::{DPy, FPy, IPy}, + types::{DPy, FPy, IPy, PySign, RPy, UniInput}, }; -const ERRMSG_FBIG_WRONG_SRC_TYPE: &str = - "only floats or strings can be used to construct an FBig instance"; -const ERRMSG_DBIG_WRONG_SRC_TYPE: &str = - "only Decimal instances or strings can be used to construct a DBig instance"; +/// Generate forward (and optional reverse) arithmetic dispatchers that first coerce the +/// operand to the wrapper's own type via the `into_*` helper, then apply the Rust operator. +macro_rules! impl_float_binops { + ($ty:ident, $into:ident, $method:ident, $rs_method:ident) => { + fn $method(lhs: &$ty, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + let rhs = rhs.$into()?; + $ty((&lhs.0).$rs_method(&rhs.0)).into_py_any(py) + } + }; + ($ty:ident, $into:ident, $method:ident, $rev_method:ident, $rs_method:ident) => { + impl_float_binops!($ty, $into, $method, $rs_method); + fn $rev_method(lhs: UniInput<'_>, rhs: &$ty, py: Python<'_>) -> PyResult> { + let lhs = lhs.$into()?; + $ty((&lhs.0).$rs_method(&rhs.0)).into_py_any(py) + } + }; +} + +impl_float_binops!(FPy, into_fpy, fpy_add, add); +impl_float_binops!(FPy, into_fpy, fpy_sub, fpy_rsub, sub); +impl_float_binops!(FPy, into_fpy, fpy_mul, mul); +impl_float_binops!(FPy, into_fpy, fpy_div, fpy_rdiv, div); +impl_float_binops!(FPy, into_fpy, fpy_mod, fpy_rmod, rem); + +impl_float_binops!(DPy, into_dpy, dpy_add, add); +impl_float_binops!(DPy, into_dpy, dpy_sub, dpy_rsub, sub); +impl_float_binops!(DPy, into_dpy, dpy_mul, mul); +impl_float_binops!(DPy, into_dpy, dpy_div, dpy_rdiv, div); +impl_float_binops!(DPy, into_dpy, dpy_mod, dpy_rmod, rem); + +fn fpy_richcmp(lhs: &FPy, other: UniInput<'_>, op: CompareOp) -> bool { + let order = match other { + UniInput::Uint(x) => lhs.0.num_cmp(&x), + UniInput::Int(x) => lhs.0.num_cmp(&x), + UniInput::BUint(x) => lhs.0.num_cmp(&x.0), + UniInput::BInt(x) => lhs.0.num_cmp(&x.0), + UniInput::OBInt(x) => lhs.0.num_cmp(&x), + UniInput::Float(x) => lhs.0.num_cmp(&x), + UniInput::BFloat(x) => lhs.0.num_cmp(&x.0), + UniInput::BDecimal(x) => lhs.0.num_cmp(&x.0), + UniInput::OBDecimal(x) => lhs.0.num_cmp(&x), + UniInput::BRational(x) => lhs.0.num_cmp(&x.0), + UniInput::OBRational(x) => lhs.0.num_cmp(&x), + }; + op.matches(order) +} + +fn dpy_richcmp(lhs: &DPy, other: UniInput<'_>, op: CompareOp) -> bool { + let order = match other { + UniInput::Uint(x) => lhs.0.num_cmp(&x), + UniInput::Int(x) => lhs.0.num_cmp(&x), + UniInput::BUint(x) => lhs.0.num_cmp(&x.0), + UniInput::BInt(x) => lhs.0.num_cmp(&x.0), + UniInput::OBInt(x) => lhs.0.num_cmp(&x), + UniInput::Float(x) => lhs.0.num_cmp(&x), + UniInput::BFloat(x) => lhs.0.num_cmp(&x.0), + UniInput::BDecimal(x) => lhs.0.num_cmp(&x.0), + UniInput::OBDecimal(x) => lhs.0.num_cmp(&x), + UniInput::BRational(x) => lhs.0.num_cmp(&x.0), + UniInput::OBRational(x) => lhs.0.num_cmp(&x), + }; + op.matches(order) +} #[pymethods] impl FPy { #[new] - fn __new__(ob: &PyAny) -> PyResult { + fn __new__(ob: &Bound<'_, PyAny>) -> PyResult { + if let Ok(s) = ob.extract::() { + return Ok(FPy(FBig::from_str(&s).map_err(parse_error_to_py)?)); + } if ob.is_instance_of::() { - // create from float - let f: f64 = ob.extract()?; - let f = FBig::try_from(f).map_err(conversion_error_to_py)?; - Ok(FPy(f)) - } else if let Ok(s) = ob.extract() { - // create from string - let f = FBig::from_str(s); - Ok(FPy(f.map_err(parse_error_to_py)?)) - } else if let Ok(obj) = as FromPyObject>::extract(ob) { - Ok(FPy(obj.0.clone())) - } else { - Err(PyTypeError::new_err(ERRMSG_FBIG_WRONG_SRC_TYPE)) + // Represent the float at f64's native precision, so that subsequent operations + // (transcendentals in particular, which require precision > 0) are well-defined. + let f = FBig::try_from(ob.extract::()?).map_err(conversion_error_to_py)?; + return Ok(FPy(f.with_precision(f64::MANTISSA_DIGITS as usize).value())); + } + if let Ok(obj) = ob.extract::>() { + return Ok(FPy(obj.0.clone())); } + // any other Python number -> permissive construction (Decimal/Fraction accepted) + Ok(FPy(UniInput::extract(ob.as_borrowed())?.construct_fpy()?)) } - fn unwrap(&self, py: Python) -> PyObject { + fn unwrap(&self, py: Python<'_>) -> PyResult> { let (signif, exp) = self.0.repr().clone().into_parts(); - (IPy(signif), exp).into_py(py) + (IPy(signif), exp).into_py_any(py) } fn __repr__(&self) -> String { @@ -47,44 +111,279 @@ impl FPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self) { - todo!() + fn __format__(&self, _format_spec: &str) -> String { + format!("{}", self.0) } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); self.0.num_hash(&mut hasher); hasher.finish() } - + fn __richcmp__(&self, other: UniInput<'_>, op: CompareOp) -> bool { + fpy_richcmp(self, other, op) + } + fn __bool__(&self) -> bool { + !self.0.repr().is_zero() + } fn __float__(&self) -> f64 { self.0.to_f64().value() } + fn __int__(&self, py: Python<'_>) -> PyResult> { + crate::convert::convert_from_ibig(&self.0.to_int().value(), py)?.into_py_any(py) + } + + /********** arithmetic **********/ + #[inline] + fn __add__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_add(self, other, py) + } + #[inline] + fn __radd__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_add(self, other, py) + } + #[inline] + fn __sub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_sub(self, other, py) + } + #[inline] + fn __rsub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_rsub(other, self, py) + } + #[inline] + fn __mul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_mul(self, other, py) + } + #[inline] + fn __rmul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_mul(self, other, py) + } + #[inline] + fn __truediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_div(self, other, py) + } + #[inline] + fn __rtruediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_rdiv(other, self, py) + } + #[inline] + fn __mod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_mod(self, other, py) + } + #[inline] + fn __rmod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + fpy_rmod(other, self, py) + } + #[inline] + fn __neg__(&self) -> Self { + FPy(-&self.0) + } + #[inline] + fn __pos__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + #[inline] + fn __abs__(&self) -> Self { + FPy(self.0.clone().abs()) + } + + /********** predicates & sign **********/ + fn is_zero(&self) -> bool { + self.0.repr().is_zero() + } + fn is_finite(&self) -> bool { + self.0.repr().is_finite() + } + fn is_infinite(&self) -> bool { + self.0.repr().is_infinite() + } + fn sign(&self) -> PySign { + self.0.repr().sign().into() + } + + /********** rounding (algebraic, no cache) **********/ + fn trunc(&self) -> Self { + FPy(self.0.trunc()) + } + fn floor(&self) -> Self { + FPy(self.0.floor()) + } + fn ceil(&self) -> Self { + FPy(self.0.ceil()) + } + fn round(&self) -> Self { + FPy(self.0.round()) + } + fn fract(&self) -> Self { + FPy(self.0.fract()) + } + + /********** precision & parts **********/ + fn precision(&self) -> usize { + self.0.precision() + } + fn digits(&self) -> usize { + self.0.digits() + } + fn with_precision(&self, precision: usize) -> Self { + FPy(self.0.clone().with_precision(precision).value()) + } + #[staticmethod] + fn from_parts(significand: &IPy, exponent: isize) -> Self { + FPy(FBig::from_parts(significand.0.clone(), exponent)) + } + + /********** conversions **********/ + fn to_int(&self) -> IPy { + IPy(self.0.to_int().value()) + } + fn to_decimal(&self) -> DPy { + DPy(self.0.to_decimal().value()) + } + fn to_binary(&self) -> Self { + FPy(self.0.to_binary().value()) + } + fn to_rational(&self) -> PyResult { + dashu_ratio::RBig::try_from(self.0.clone()) + .map(RPy) + .map_err(conversion_error_to_py) + } + + /********** transcendentals (routed through the global cache + Context layer) **********/ + fn sin(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.sin(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn cos(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.cos(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn tan(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.tan(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn asin(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.asin(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn acos(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.acos(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn atan(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.atan(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn sinh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.sinh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn cosh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.cosh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn tanh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.tanh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn asinh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.asinh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn acosh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.acosh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn atanh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.atanh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn exp(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.exp(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn exp_m1(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.exp_m1(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn ln(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.ln(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn ln_1p(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.ln_1p(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn sqrt(&self) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_float(ctx.sqrt(self.0.repr()), ctx)?)) + } + fn cbrt(&self) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_float(ctx.cbrt(self.0.repr()), ctx)?)) + } + fn nth_root(&self, n: usize) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_float(ctx.nth_root(n, self.0.repr()), ctx)?)) + } + fn powf(&self, w: &Self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.powf(self.0.repr(), w.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn powi(&self, n: &IPy) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_float(ctx.powi(self.0.repr(), n.0.clone()), ctx)?)) + } + fn atan2(&self, x: &Self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.atan2(self.0.repr(), x.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } } #[pymethods] impl DPy { #[new] - fn __new__(ob: &PyAny) -> PyResult { - let py = ob.py(); - let decimal = py.import(intern!(py, "decimal"))?; - if ob.is_instance(decimal.getattr(intern!(py, "Decimal"))?)? { - // create from decimal.Decimal - Ok(DPy(parse_to_dbig(ob)?)) - } else if let Ok(s) = ob.extract() { - // create from string - let d = DBig::from_str(s); - Ok(DPy(d.map_err(parse_error_to_py)?)) - } else if let Ok(obj) = as FromPyObject>::extract(ob) { - Ok(DPy(obj.0.clone())) - } else { - Err(PyTypeError::new_err(ERRMSG_DBIG_WRONG_SRC_TYPE)) + fn __new__(ob: &Bound<'_, PyAny>) -> PyResult { + if let Ok(s) = ob.extract::() { + return Ok(DPy(DBig::from_str(&s).map_err(parse_error_to_py)?)); } + // decimal.Decimal fast path (preserves the original parse_to_dbig round-trip) + { + let py = ob.py(); + let decimal = py.import(intern!(py, "decimal"))?; + let decimal_type = decimal.getattr(intern!(py, "Decimal"))?; + if ob.is_instance(&decimal_type)? { + return Ok(DPy(parse_to_dbig(ob)?)); + } + } + if let Ok(obj) = ob.extract::>() { + return Ok(DPy(obj.0.clone())); + } + // any other Python number -> permissive construction (float/FBig accepted) + Ok(DPy(UniInput::extract(ob.as_borrowed())?.construct_dpy()?)) } - fn unwrap(&self, py: Python) -> PyResult { + fn unwrap(&self, py: Python<'_>) -> PyResult> { let decimal = py.import(intern!(py, "decimal"))?; let decimal_type = decimal.getattr(intern!(py, "Decimal"))?; let decimal_str = format!("{:e}", self.0); - Ok(decimal_type.call1((decimal_str,))?.into()) + decimal_type.call1((decimal_str,))?.into_py_any(py) } fn __repr__(&self) -> String { @@ -93,16 +392,248 @@ impl DPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self) { - todo!() + fn __format__(&self, _format_spec: &str) -> String { + format!("{}", self.0) } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); self.0.num_hash(&mut hasher); hasher.finish() } - + fn __richcmp__(&self, other: UniInput<'_>, op: CompareOp) -> bool { + dpy_richcmp(self, other, op) + } + fn __bool__(&self) -> bool { + !self.0.repr().is_zero() + } fn __float__(&self) -> f64 { self.0.to_f64().value() } + fn __int__(&self, py: Python<'_>) -> PyResult> { + crate::convert::convert_from_ibig(&self.0.to_int().value(), py)?.into_py_any(py) + } + + /********** arithmetic **********/ + #[inline] + fn __add__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_add(self, other, py) + } + #[inline] + fn __radd__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_add(self, other, py) + } + #[inline] + fn __sub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_sub(self, other, py) + } + #[inline] + fn __rsub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_rsub(other, self, py) + } + #[inline] + fn __mul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_mul(self, other, py) + } + #[inline] + fn __rmul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_mul(self, other, py) + } + #[inline] + fn __truediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_div(self, other, py) + } + #[inline] + fn __rtruediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_rdiv(other, self, py) + } + #[inline] + fn __mod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_mod(self, other, py) + } + #[inline] + fn __rmod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + dpy_rmod(other, self, py) + } + #[inline] + fn __neg__(&self) -> Self { + DPy(-&self.0) + } + #[inline] + fn __pos__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + #[inline] + fn __abs__(&self) -> Self { + DPy(self.0.clone().abs()) + } + + /********** predicates & sign **********/ + fn is_zero(&self) -> bool { + self.0.repr().is_zero() + } + fn is_finite(&self) -> bool { + self.0.repr().is_finite() + } + fn is_infinite(&self) -> bool { + self.0.repr().is_infinite() + } + fn sign(&self) -> PySign { + self.0.repr().sign().into() + } + + /********** rounding (algebraic, no cache) **********/ + fn trunc(&self) -> Self { + DPy(self.0.trunc()) + } + fn floor(&self) -> Self { + DPy(self.0.floor()) + } + fn ceil(&self) -> Self { + DPy(self.0.ceil()) + } + fn round(&self) -> Self { + DPy(self.0.round()) + } + fn fract(&self) -> Self { + DPy(self.0.fract()) + } + + /********** precision & parts **********/ + fn precision(&self) -> usize { + self.0.precision() + } + fn digits(&self) -> usize { + self.0.digits() + } + fn with_precision(&self, precision: usize) -> Self { + DPy(self.0.clone().with_precision(precision).value()) + } + #[staticmethod] + fn from_parts(significand: &IPy, exponent: isize) -> Self { + DPy(DBig::from_parts(significand.0.clone(), exponent)) + } + + /********** conversions **********/ + fn to_int(&self) -> IPy { + IPy(self.0.to_int().value()) + } + fn to_decimal(&self) -> Self { + DPy(self.0.to_decimal().value()) + } + fn to_binary(&self) -> FPy { + FPy(self.0.to_binary().value()) + } + fn to_rational(&self) -> PyResult { + dashu_ratio::RBig::try_from(self.0.clone()) + .map(RPy) + .map_err(conversion_error_to_py) + } + + /********** transcendentals (routed through the global cache + Context layer) **********/ + fn sin(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.sin(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn cos(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.cos(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn tan(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.tan(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn asin(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.asin(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn acos(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.acos(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn atan(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.atan(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn sinh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.sinh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn cosh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.cosh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn tanh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.tanh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn asinh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.asinh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn acosh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.acosh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn atanh(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.atanh(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn exp(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.exp(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn exp_m1(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.exp_m1(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn ln(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.ln(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn ln_1p(&self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.ln_1p(self.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn sqrt(&self) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_float(ctx.sqrt(self.0.repr()), ctx)?)) + } + fn cbrt(&self) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_float(ctx.cbrt(self.0.repr()), ctx)?)) + } + fn nth_root(&self, n: usize) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_float(ctx.nth_root(n, self.0.repr()), ctx)?)) + } + fn powf(&self, w: &Self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.powf(self.0.repr(), w.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } + fn powi(&self, n: &IPy) -> PyResult { + let ctx = self.0.context(); + Ok(Self(unwrap_float(ctx.powi(self.0.repr(), n.0.clone()), ctx)?)) + } + fn atan2(&self, x: &Self) -> PyResult { + let ctx = self.0.context(); + let res = with_cache(|c| ctx.atan2(self.0.repr(), x.0.repr(), Some(c))); + Ok(Self(unwrap_float(res, ctx)?)) + } } diff --git a/python/src/int.rs b/python/src/int.rs index 5812e4c3..1a972005 100644 --- a/python/src/int.rs +++ b/python/src/int.rs @@ -7,21 +7,27 @@ use pyo3::basic::CompareOp; use pyo3::exceptions::{ PyIndexError, PyNotImplementedError, PyOverflowError, PyTypeError, PyValueError, }; -use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyIterator, PyList, PyLong, PySlice, PyTuple}; +use pyo3::{ + Bound, IntoPyObjectExt, Py, PyAny, PyResult, + prelude::*, + types::{PyBytes, PyInt, PyIterator, PyList, PySlice, PyTuple}, +}; use crate::{ convert::{ - convert_from_ibig, convert_from_ubig, parse_error_to_py, parse_signed_index, parse_to_ibig, - parse_to_long, parse_to_ubig, + conversion_error_to_py, convert_from_ibig, convert_from_ubig, parse_error_to_py, + parse_signed_index, parse_to_ibig, parse_to_long, parse_to_ubig, }, - types::{DPy, FPy, IPy, PyWords, RPy, UPy, UniInput}, + types::{DPy, FPy, IPy, PySign, PyWords, RPy, UPy, UniInput}, }; -use dashu_base::{Abs, BitTest, Sign, Signed, UnsignedAbs}; +use dashu_base::{ + Abs, BitTest, CubicRoot, PowerOfTwo, Sign, Signed, SquareRoot, UnsignedAbs, + ring::{DivEuclid, DivRemEuclid, ExtendedGcd, Gcd}, +}; +use dashu_float::FBig; use dashu_int::{IBig, UBig, Word, fast_div}; use num_order::{NumHash, NumOrd}; -type FBig = dashu_float::FBig; // error messages const ERRMSG_LENGTH_TOO_LARGE: &str = "the integer has too many bits for indexing"; @@ -41,46 +47,61 @@ const ERRMSG_UBIG_BITS_OOR: &str = "bits index out of range"; const ERRMSG_BITOPS_TYPE: &str = "bit operations are only defined between integers"; const ERRMSG_WRONG_CHUNKS_TYPE: &str = "the chunks input is not a recognized iterable"; const ERRMSG_ZERO_CHUNK_SIZE: &str = "the chunk size must not be zero"; +const ERRMSG_FLOORDIV_INT_ONLY: &str = "floor division requires an integer divisor"; +const ERRMSG_MODULUS_INT_ONLY: &str = "modulus must be an integer"; +const ERRMSG_POW_INT_EXP: &str = "the exponent must be a non-negative integer"; +const ERRMSG_POW_MOD_INT_EXP: &str = + "modular exponentiation requires a non-negative integer exponent"; macro_rules! impl_binops { ($ty_variant:ident, $py_method:ident, $rs_method:ident) => { - fn $py_method(lhs: &$ty_variant, rhs: UniInput<'_>, py: Python) -> PyObject { - match rhs { - UniInput::Uint(x) => $ty_variant((&lhs.0).$rs_method(x)).into_py(py), - UniInput::Int(x) => IPy((&lhs.0).$rs_method(IBig::from(x))).into_py(py), - UniInput::BUint(x) => $ty_variant((&lhs.0).$rs_method(&x.0)).into_py(py), - UniInput::BInt(x) => IPy((&lhs.0).$rs_method(&x.0)).into_py(py), - UniInput::OBInt(x) => IPy((&lhs.0).$rs_method(x)).into_py(py), + fn $py_method(lhs: &$ty_variant, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => $ty_variant((&lhs.0).$rs_method(x)).into_py_any(py)?, + UniInput::Int(x) => IPy((&lhs.0).$rs_method(IBig::from(x))).into_py_any(py)?, + UniInput::BUint(x) => $ty_variant((&lhs.0).$rs_method(&x.0)).into_py_any(py)?, + UniInput::BInt(x) => IPy((&lhs.0).$rs_method(&x.0)).into_py_any(py)?, + UniInput::OBInt(x) => IPy((&lhs.0).$rs_method(x)).into_py_any(py)?, UniInput::Float(x) => { - FPy((&lhs.0).$rs_method(FBig::try_from(x).unwrap())).into_py(py) + let f = FBig::try_from(x).map_err(conversion_error_to_py)?; + FPy((&lhs.0).$rs_method(f)).into_py_any(py)? } - UniInput::BFloat(x) => FPy((&lhs.0).$rs_method(&x.0)).into_py(py), - UniInput::BDecimal(x) => DPy((&lhs.0).$rs_method(&x.0)).into_py(py), - UniInput::OBDecimal(x) => DPy((&lhs.0).$rs_method(x)).into_py(py), - UniInput::BRational(x) => RPy((&lhs.0).$rs_method(&x.0)).into_py(py), - UniInput::OBRational(x) => RPy((&lhs.0).$rs_method(x)).into_py(py), - } + UniInput::BFloat(x) => FPy((&lhs.0).$rs_method(&x.0)).into_py_any(py)?, + UniInput::BDecimal(x) => DPy((&lhs.0).$rs_method(&x.0)).into_py_any(py)?, + UniInput::OBDecimal(x) => DPy((&lhs.0).$rs_method(x)).into_py_any(py)?, + UniInput::BRational(x) => RPy((&lhs.0).$rs_method(&x.0)).into_py_any(py)?, + UniInput::OBRational(x) => RPy((&lhs.0).$rs_method(x)).into_py_any(py)?, + }; + Ok(obj) } }; ($ty_variant:ident, $py_method:ident, $py_method_rev:ident, $rs_method:ident) => { impl_binops!($ty_variant, $py_method, $rs_method); - fn $py_method_rev(lhs: UniInput<'_>, rhs: &$ty_variant, py: Python) -> PyObject { - match lhs { - UniInput::Uint(x) => $ty_variant(x.$rs_method(&rhs.0).into()).into_py(py), - UniInput::Int(x) => IPy(IBig::from(x).$rs_method(&rhs.0).into()).into_py(py), - UniInput::BUint(x) => $ty_variant((&x.0).$rs_method(&rhs.0)).into_py(py), - UniInput::BInt(x) => IPy((&x.0).$rs_method(&rhs.0)).into_py(py), - UniInput::OBInt(x) => IPy(x.$rs_method(&rhs.0)).into_py(py), + fn $py_method_rev( + lhs: UniInput<'_>, + rhs: &$ty_variant, + py: Python<'_>, + ) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match lhs { + UniInput::Uint(x) => $ty_variant(x.$rs_method(&rhs.0).into()).into_py_any(py)?, + UniInput::Int(x) => IPy(IBig::from(x).$rs_method(&rhs.0).into()).into_py_any(py)?, + UniInput::BUint(x) => $ty_variant((&x.0).$rs_method(&rhs.0)).into_py_any(py)?, + UniInput::BInt(x) => IPy((&x.0).$rs_method(&rhs.0)).into_py_any(py)?, + UniInput::OBInt(x) => IPy(x.$rs_method(&rhs.0)).into_py_any(py)?, UniInput::Float(x) => { - FPy(FBig::try_from(x).unwrap().$rs_method(&rhs.0)).into_py(py) + let f = FBig::try_from(x).map_err(conversion_error_to_py)?; + FPy(f.$rs_method(&rhs.0)).into_py_any(py)? } - UniInput::BFloat(x) => FPy((&x.0).$rs_method(&rhs.0)).into_py(py), - UniInput::BDecimal(x) => DPy((&x.0).$rs_method(&rhs.0)).into_py(py), - UniInput::OBDecimal(x) => DPy(x.$rs_method(&rhs.0)).into_py(py), - UniInput::BRational(x) => RPy((&x.0).$rs_method(&rhs.0)).into_py(py), - UniInput::OBRational(x) => RPy(x.$rs_method(&rhs.0)).into_py(py), - } + UniInput::BFloat(x) => FPy((&x.0).$rs_method(&rhs.0)).into_py_any(py)?, + UniInput::BDecimal(x) => DPy((&x.0).$rs_method(&rhs.0)).into_py_any(py)?, + UniInput::OBDecimal(x) => DPy(x.$rs_method(&rhs.0)).into_py_any(py)?, + UniInput::BRational(x) => RPy((&x.0).$rs_method(&rhs.0)).into_py_any(py)?, + UniInput::OBRational(x) => RPy(x.$rs_method(&rhs.0)).into_py_any(py)?, + }; + Ok(obj) } }; } @@ -94,41 +115,44 @@ impl_binops!(IPy, ipy_sub, ipy_rsub, sub); impl_binops!(IPy, ipy_mul, mul); impl_binops!(IPy, ipy_div, ipy_rdiv, div); -fn upy_bitand(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyResult { - let result: Py = match rhs { - UniInput::Uint(x) => UPy((&lhs.0).bitand(x).into()).into_py(py), - UniInput::BUint(x) => UPy((&lhs.0).bitand(&x.0)).into_py(py), - UniInput::Int(x) => UPy((&lhs.0).bitand(IBig::from(x))).into_py(py), - UniInput::BInt(x) => UPy((&lhs.0).bitand(&x.0)).into_py(py), - UniInput::OBInt(x) => UPy((&lhs.0).bitand(x)).into_py(py), +fn upy_bitand(lhs: &UPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => UPy((&lhs.0).bitand(x).into()).into_py_any(py)?, + UniInput::BUint(x) => UPy((&lhs.0).bitand(&x.0)).into_py_any(py)?, + UniInput::Int(x) => UPy((&lhs.0).bitand(IBig::from(x))).into_py_any(py)?, + UniInput::BInt(x) => UPy((&lhs.0).bitand(&x.0)).into_py_any(py)?, + UniInput::OBInt(x) => UPy((&lhs.0).bitand(x)).into_py_any(py)?, _ => return Err(PyTypeError::new_err(ERRMSG_BITOPS_TYPE)), }; - Ok(result) + Ok(obj) } -fn ipy_bitand(lhs: &IPy, rhs: UniInput<'_>, py: Python) -> PyResult { - let result: Py = match rhs { - UniInput::Uint(x) => UPy((&lhs.0).bitand(x).into()).into_py(py), - UniInput::BUint(x) => UPy((&lhs.0).bitand(&x.0)).into_py(py), - UniInput::Int(x) => IPy((&lhs.0).bitand(x)).into_py(py), - UniInput::BInt(x) => IPy((&lhs.0).bitand(&x.0)).into_py(py), - UniInput::OBInt(x) => IPy((&lhs.0).bitand(x)).into_py(py), +fn ipy_bitand(lhs: &IPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => UPy((&lhs.0).bitand(x).into()).into_py_any(py)?, + UniInput::BUint(x) => UPy((&lhs.0).bitand(&x.0)).into_py_any(py)?, + UniInput::Int(x) => IPy((&lhs.0).bitand(x)).into_py_any(py)?, + UniInput::BInt(x) => IPy((&lhs.0).bitand(&x.0)).into_py_any(py)?, + UniInput::OBInt(x) => IPy((&lhs.0).bitand(x)).into_py_any(py)?, _ => return Err(PyTypeError::new_err(ERRMSG_BITOPS_TYPE)), }; - Ok(result) + Ok(obj) } macro_rules! impl_ubig_bit_binops { ($ty_variant:ident, $py_method:ident, $rs_method:ident) => { - fn $py_method(lhs: &$ty_variant, rhs: UniInput<'_>, py: Python) -> PyResult { - let result = match rhs { - UniInput::Uint(x) => $ty_variant((&lhs.0).$rs_method(x)).into_py(py), - UniInput::BUint(x) => $ty_variant((&lhs.0).$rs_method(&x.0)).into_py(py), - UniInput::Int(x) => IPy((&lhs.0).$rs_method(IBig::from(x))).into_py(py), - UniInput::BInt(x) => IPy((&lhs.0).$rs_method(&x.0)).into_py(py), - UniInput::OBInt(x) => IPy((&lhs.0).$rs_method(x)).into_py(py), + fn $py_method(lhs: &$ty_variant, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => $ty_variant((&lhs.0).$rs_method(x)).into_py_any(py)?, + UniInput::BUint(x) => $ty_variant((&lhs.0).$rs_method(&x.0)).into_py_any(py)?, + UniInput::Int(x) => IPy((&lhs.0).$rs_method(IBig::from(x))).into_py_any(py)?, + UniInput::BInt(x) => IPy((&lhs.0).$rs_method(&x.0)).into_py_any(py)?, + UniInput::OBInt(x) => IPy((&lhs.0).$rs_method(x)).into_py_any(py)?, _ => return Err(PyTypeError::new_err(ERRMSG_BITOPS_TYPE)), }; - Ok(result) + Ok(obj) } }; } @@ -137,26 +161,63 @@ impl_ubig_bit_binops!(UPy, upy_bitxor, bitxor); impl_ubig_bit_binops!(IPy, ipy_bitor, bitor); impl_ubig_bit_binops!(IPy, ipy_bitxor, bitxor); -fn upy_mod(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyObject { - match rhs { - UniInput::Uint(x) => UPy((&lhs.0).rem(x).into()).into_py(py), - UniInput::Int(x) => UPy((&lhs.0).rem(IBig::from(x))).into_py(py), - UniInput::BUint(x) => UPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::BInt(x) => UPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::OBInt(x) => UPy((&lhs.0).rem(x)).into_py(py), - _ => todo!(), - } +fn upy_mod(lhs: &UPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => UPy((&lhs.0).rem(x).into()).into_py_any(py)?, + UniInput::Int(x) => UPy((&lhs.0).rem(IBig::from(x))).into_py_any(py)?, + UniInput::BUint(x) => UPy((&lhs.0).rem(&x.0)).into_py_any(py)?, + UniInput::BInt(x) => UPy((&lhs.0).rem(&x.0)).into_py_any(py)?, + UniInput::OBInt(x) => UPy((&lhs.0).rem(x)).into_py_any(py)?, + UniInput::Float(x) => { + let f = FBig::try_from(x).map_err(conversion_error_to_py)?; + FPy(FBig::from(lhs.0.clone()).rem(f)).into_py_any(py)? + } + UniInput::BFloat(x) => FPy(FBig::from(lhs.0.clone()).rem(&x.0)).into_py_any(py)?, + UniInput::BDecimal(x) => { + DPy(dashu_float::DBig::from(lhs.0.clone()).rem(&x.0)).into_py_any(py)? + } + UniInput::OBDecimal(x) => { + DPy(dashu_float::DBig::from(lhs.0.clone()).rem(x)).into_py_any(py)? + } + UniInput::BRational(x) => { + RPy(dashu_ratio::RBig::from(lhs.0.clone()).rem(&x.0)).into_py_any(py)? + } + UniInput::OBRational(x) => { + RPy(dashu_ratio::RBig::from(lhs.0.clone()).rem(x)).into_py_any(py)? + } + }; + Ok(obj) } -fn ipy_mod(lhs: &IPy, rhs: UniInput<'_>, py: Python) -> PyObject { - match rhs { - UniInput::Uint(x) => IPy((&lhs.0).rem(x).into()).into_py(py), - UniInput::Int(x) => IPy((&lhs.0).rem(x).into()).into_py(py), - UniInput::BUint(x) => IPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::BInt(x) => IPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::OBInt(x) => IPy((&lhs.0).rem(x)).into_py(py), - _ => todo!(), - } +fn ipy_mod(lhs: &IPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => IPy((&lhs.0).rem(x).into()).into_py_any(py)?, + UniInput::Int(x) => IPy((&lhs.0).rem(x).into()).into_py_any(py)?, + UniInput::BUint(x) => IPy((&lhs.0).rem(&x.0)).into_py_any(py)?, + UniInput::BInt(x) => IPy((&lhs.0).rem(&x.0)).into_py_any(py)?, + UniInput::OBInt(x) => IPy((&lhs.0).rem(x)).into_py_any(py)?, + UniInput::Float(x) => { + let f = FBig::try_from(x).map_err(conversion_error_to_py)?; + FPy(FBig::from(lhs.0.clone()).rem(f)).into_py_any(py)? + } + UniInput::BFloat(x) => FPy(FBig::from(lhs.0.clone()).rem(&x.0)).into_py_any(py)?, + UniInput::BDecimal(x) => { + DPy(dashu_float::DBig::from(lhs.0.clone()).rem(&x.0)).into_py_any(py)? + } + UniInput::OBDecimal(x) => { + DPy(dashu_float::DBig::from(lhs.0.clone()).rem(x)).into_py_any(py)? + } + UniInput::BRational(x) => { + RPy(dashu_ratio::RBig::from(lhs.0.clone()).rem(&x.0)).into_py_any(py)? + } + UniInput::OBRational(x) => { + RPy(dashu_ratio::RBig::from(lhs.0.clone()).rem(x)).into_py_any(py)? + } + }; + Ok(obj) } + fn ipy_pow(base: &IBig, exp: UniInput, modulus: Option) -> PyResult { use fast_div::ConstDivisor; @@ -171,7 +232,7 @@ fn ipy_pow(base: &IBig, exp: UniInput, modulus: Option) -> PyResult todo!(), + _ => return Err(PyTypeError::new_err(ERRMSG_MODULUS_INT_ONLY)), }; match exp { @@ -183,21 +244,131 @@ fn ipy_pow(base: &IBig, exp: UniInput, modulus: Option) -> PyResult todo!(), + _ => Err(PyTypeError::new_err(ERRMSG_POW_MOD_INT_EXP)), } } else { match exp { UniInput::Uint(x) => Ok(base.pow(x as _)), - _ => todo!(), + _ => Err(PyTypeError::new_err(ERRMSG_POW_INT_EXP)), } } } +/// Floor division `self // other` for integer operands. +fn upy_floordiv(lhs: &UPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => UPy((&lhs.0).div_euclid(&UBig::from(x))).into_py_any(py)?, + UniInput::BUint(x) => UPy((&lhs.0).div_euclid(&x.0)).into_py_any(py)?, + UniInput::Int(x) => { + IPy(lhs.0.as_ibig().clone().div_euclid(IBig::from(x))).into_py_any(py)? + } + UniInput::BInt(x) => IPy(lhs.0.as_ibig().clone().div_euclid(&x.0)).into_py_any(py)?, + UniInput::OBInt(x) => IPy(lhs.0.as_ibig().clone().div_euclid(x)).into_py_any(py)?, + _ => return Err(PyTypeError::new_err(ERRMSG_FLOORDIV_INT_ONLY)), + }; + Ok(obj) +} +fn upy_rfloordiv(lhs: UniInput<'_>, rhs: &UPy, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match lhs { + UniInput::Uint(x) => UPy(UBig::from(x).div_euclid(&rhs.0)).into_py_any(py)?, + UniInput::BUint(x) => UPy((&x.0).div_euclid(&rhs.0)).into_py_any(py)?, + UniInput::Int(x) => IPy(IBig::from(x).div_euclid(rhs.0.as_ibig())).into_py_any(py)?, + UniInput::BInt(x) => IPy((&x.0).div_euclid(rhs.0.as_ibig())).into_py_any(py)?, + UniInput::OBInt(x) => IPy(x.div_euclid(rhs.0.as_ibig())).into_py_any(py)?, + _ => return Err(PyTypeError::new_err(ERRMSG_FLOORDIV_INT_ONLY)), + }; + Ok(obj) +} +fn upy_divmod(lhs: &UPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => { + let (q, r) = (&lhs.0).div_rem_euclid(&UBig::from(x)); + (UPy(q), UPy(r)).into_py_any(py)? + } + UniInput::BUint(x) => { + let (q, r) = (&lhs.0).div_rem_euclid(&x.0); + (UPy(q), UPy(r)).into_py_any(py)? + } + UniInput::Int(x) => { + let (q, r) = lhs.0.as_ibig().clone().div_rem_euclid(IBig::from(x)); + (IPy(q), UPy(r)).into_py_any(py)? + } + UniInput::BInt(x) => { + let (q, r) = lhs.0.as_ibig().clone().div_rem_euclid(&x.0); + (IPy(q), UPy(r)).into_py_any(py)? + } + UniInput::OBInt(x) => { + let (q, r) = lhs.0.as_ibig().clone().div_rem_euclid(x); + (IPy(q), UPy(r)).into_py_any(py)? + } + _ => return Err(PyTypeError::new_err(ERRMSG_FLOORDIV_INT_ONLY)), + }; + Ok(obj) +} + +fn ipy_floordiv(lhs: &IPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => IPy((&lhs.0).div_euclid(&IBig::from(x))).into_py_any(py)?, + UniInput::BUint(x) => IPy((&lhs.0).div_euclid(x.0.as_ibig())).into_py_any(py)?, + UniInput::Int(x) => IPy((&lhs.0).div_euclid(IBig::from(x))).into_py_any(py)?, + UniInput::BInt(x) => IPy((&lhs.0).div_euclid(&x.0)).into_py_any(py)?, + UniInput::OBInt(x) => IPy((&lhs.0).div_euclid(&x)).into_py_any(py)?, + _ => return Err(PyTypeError::new_err(ERRMSG_FLOORDIV_INT_ONLY)), + }; + Ok(obj) +} +fn ipy_rfloordiv(lhs: UniInput<'_>, rhs: &IPy, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match lhs { + UniInput::Uint(x) => { + IPy(UBig::from(x).as_ibig().clone().div_euclid(&rhs.0)).into_py_any(py)? + } + UniInput::BUint(x) => IPy(x.0.as_ibig().clone().div_euclid(&rhs.0)).into_py_any(py)?, + UniInput::Int(x) => IPy(IBig::from(x).div_euclid(&rhs.0)).into_py_any(py)?, + UniInput::BInt(x) => IPy((&x.0).div_euclid(&rhs.0)).into_py_any(py)?, + UniInput::OBInt(x) => IPy(x.div_euclid(&rhs.0)).into_py_any(py)?, + _ => return Err(PyTypeError::new_err(ERRMSG_FLOORDIV_INT_ONLY)), + }; + Ok(obj) +} +fn ipy_divmod(lhs: &IPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + use pyo3::IntoPyObjectExt; + let obj = match rhs { + UniInput::Uint(x) => { + let (q, r) = (&lhs.0).div_rem_euclid(&IBig::from(x)); + (IPy(q), UPy(r)).into_py_any(py)? + } + UniInput::BUint(x) => { + let (q, r) = (&lhs.0).div_rem_euclid(x.0.as_ibig()); + (IPy(q), UPy(r)).into_py_any(py)? + } + UniInput::Int(x) => { + let (q, r) = (&lhs.0).div_rem_euclid(IBig::from(x)); + (IPy(q), UPy(r)).into_py_any(py)? + } + UniInput::BInt(x) => { + let (q, r) = (&lhs.0).div_rem_euclid(&x.0); + (IPy(q), UPy(r)).into_py_any(py)? + } + UniInput::OBInt(x) => { + let (q, r) = (&lhs.0).div_rem_euclid(x); + (IPy(q), UPy(r)).into_py_any(py)? + } + _ => return Err(PyTypeError::new_err(ERRMSG_FLOORDIV_INT_ONLY)), + }; + Ok(obj) +} + #[pymethods] impl UPy { #[new] - fn __new__(ob: &PyAny, radix: Option) -> PyResult { - if ob.is_instance_of::() { + #[pyo3(signature = (ob, radix=None))] + fn __new__(ob: &Bound<'_, PyAny>, radix: Option) -> PyResult { + if ob.is_instance_of::() { // create from int if radix.is_some() { return Err(PyTypeError::new_err(ERRMSG_INT_WITH_RADIX)); @@ -213,22 +384,22 @@ impl UPy { } else { Ok(UPy(parse_to_ubig(ob)?)) } - } else if let Ok(s) = ob.extract() { + } else if let Ok(s) = ob.extract::() { // create from string let n = if let Some(r) = radix { - UBig::from_str_radix(s, r) + UBig::from_str_radix(&s, r) } else { - UBig::from_str_with_radix_prefix(s).map(|v| v.0) + UBig::from_str_with_radix_prefix(&s).map(|v| v.0) }; Ok(UPy(n.map_err(parse_error_to_py)?)) - } else if let Ok(obj) = as FromPyObject>::extract(ob) { + } else if let Ok(obj) = ob.extract::>() { Ok(UPy(obj.0.clone())) } else { Err(PyTypeError::new_err(ERRMSG_UBIG_WRONG_SRC_TYPE)) } } - fn unwrap(&self, py: Python) -> PyResult { - convert_from_ubig(&self.0, py) + fn unwrap(&self, py: Python<'_>) -> PyResult> { + convert_from_ubig(&self.0, py)?.into_py_any(py) } fn __repr__(&self) -> String { @@ -237,8 +408,9 @@ impl UPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self) { - todo!() + fn __format__(&self, _format_spec: &str) -> String { + // MVP: ignore the format mini-language and delegate to Display. + format!("{}", self.0) } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); @@ -266,14 +438,15 @@ impl UPy { fn __len__(&self) -> usize { self.0.bit_len() } - fn __getitem__(&self, index: &PyAny) -> PyResult { + fn __getitem__(&self, index: &Bound<'_, PyAny>) -> PyResult> { + use pyo3::IntoPyObjectExt; let py = index.py(); - if let Ok(i) = ::extract(index) { + if let Ok(i) = index.extract::() { let i = parse_signed_index(i, self.0.bit_len(), true) .ok_or(PyIndexError::new_err(ERRMSG_UBIG_BITS_OOR))?; - Ok(self.0.bit(i).into_py(py)) - } else if let Ok(range) = index.downcast::() { - let len = self + self.0.bit(i).into_py_any(py) + } else if let Ok(range) = index.cast::() { + let len: isize = self .0 .bit_len() .try_into() @@ -286,13 +459,13 @@ impl UPy { let mut data = self.0.clone(); data.clear_high_bits(indices.stop as _); let split = Self(data.split_bits(indices.start as _).1); - Ok(split.into_py(py)) + split.into_py_any(py) } else { Err(PyTypeError::new_err(ERRMSG_WRONG_INDEX_TYPE)) } } - fn __setitem__(&mut self, index: &PyAny, set: bool) -> PyResult<()> { - if let Ok(i) = ::extract(index) { + fn __setitem__(&mut self, index: &Bound<'_, PyAny>, set: bool) -> PyResult<()> { + if let Ok(i) = index.extract::() { let i = parse_signed_index(i, self.0.bit_len(), true) .ok_or(PyIndexError::new_err(ERRMSG_UBIG_BITS_OOR))?; if set { @@ -301,8 +474,8 @@ impl UPy { self.0.clear_bit(i) } Ok(()) - } else if let Ok(range) = index.downcast::() { - let len = self + } else if let Ok(range) = index.cast::() { + let len: isize = self .0 .bit_len() .try_into() @@ -313,7 +486,7 @@ impl UPy { } // shortcut for clearing high bits - if indices.stop == len as _ && !set { + if indices.stop == len && !set { self.0.clear_high_bits(indices.start as _); } @@ -333,20 +506,20 @@ impl UPy { Err(PyTypeError::new_err(ERRMSG_WRONG_INDEX_TYPE)) } } - fn __delitem__(&mut self, index: &PyAny) -> PyResult<()> { + fn __delitem__(&mut self, index: &Bound<'_, PyAny>) -> PyResult<()> { fn remove_bits_in_middle(u: &mut UBig, start: usize, end: usize) { let (mut left, right) = core::mem::take(u).split_bits(end); left.clear_high_bits(end - start); *u = (right << start) | left; } - if let Ok(i) = ::extract(index) { + if let Ok(i) = index.extract::() { let i = parse_signed_index(i, self.0.bit_len(), true) .ok_or(PyIndexError::new_err(ERRMSG_UBIG_BITS_OOR))?; remove_bits_in_middle(&mut self.0, i, i + 1); Ok(()) - } else if let Ok(range) = index.downcast::() { - let len = self + } else if let Ok(range) = index.cast::() { + let len: isize = self .0 .bit_len() .try_into() @@ -357,7 +530,7 @@ impl UPy { } // shortcut for clearing high bits - if indices.stop == len as _ { + if indices.stop == len { self.0.clear_high_bits(indices.start as _); } else if indices.start == 0 { self.0 >>= indices.stop as usize; @@ -370,10 +543,77 @@ impl UPy { } } + /********** number theory & roots **********/ + fn sqrt(&self) -> Self { + UPy(self.0.sqrt()) + } + fn cbrt(&self) -> Self { + UPy(self.0.cbrt()) + } + fn nth_root(&self, n: usize) -> Self { + UPy(self.0.nth_root(n)) + } + fn sqr(&self) -> Self { + UPy(self.0.sqr()) + } + fn cubic(&self) -> Self { + UPy(self.0.cubic()) + } + fn ilog(&self, base: &Self) -> PyResult { + if base.0 <= UBig::ONE { + return Err(PyValueError::new_err("base must be greater than 1")); + } + Ok(self.0.ilog(&base.0)) + } + fn is_multiple_of(&self, divisor: &Self) -> bool { + self.0.is_multiple_of(&divisor.0) + } + fn remove(&mut self, factor: &Self) -> PyResult { + self.0 + .remove(&factor.0) + .ok_or_else(|| PyValueError::new_err("the factor does not divide this number")) + } + fn gcd(&self, other: &Self) -> Self { + UPy(Gcd::gcd(&self.0, &other.0)) + } + fn gcd_ext(&self, other: &Self) -> (Self, IPy, IPy) { + let (g, s, t) = ExtendedGcd::gcd_ext(&self.0, &other.0); + (UPy(g), IPy(s), IPy(t)) + } + + /********** bit operations **********/ + fn count_ones(&self) -> usize { + self.0.count_ones() + } + fn count_zeros(&self) -> Option { + self.0.count_zeros() + } + fn trailing_zeros(&self) -> Option { + self.0.trailing_zeros() + } + fn trailing_ones(&self) -> Option { + self.0.trailing_ones() + } + fn is_power_of_two(&self) -> bool { + self.0.is_power_of_two() + } + fn next_power_of_two(&self) -> Self { + UPy(self.0.clone().next_power_of_two()) + } + + /********** accessors **********/ + fn is_one(&self) -> bool { + self.0.is_one() + } + #[staticmethod] + fn ones(n: usize) -> Self { + UPy(UBig::ones(n)) + } + /********** interop **********/ - fn __int__(&self, py: Python) -> PyResult { - convert_from_ubig(&self.0, py) + fn __int__(&self, py: Python<'_>) -> PyResult> { + convert_from_ubig(&self.0, py)?.into_py_any(py) } /// Get the underlying words representing this integer fn to_words(&self) -> PyWords { @@ -381,73 +621,69 @@ impl UPy { } /// Create an integer from a list of words #[staticmethod] - fn from_words(ob: &PyAny) -> PyResult { - if let Ok(vec) = as FromPyObject>::extract(ob) { + fn from_words(ob: &Bound<'_, PyAny>) -> PyResult { + if let Ok(vec) = ob.extract::>() { Ok(UPy(UBig::from_words(&vec))) - } else if let Ok(words) = as FromPyObject>::extract(ob) { + } else if let Ok(words) = ob.extract::>() { Ok(UPy(UBig::from_words(&words.0))) } else { Err(PyTypeError::new_err(ERRMSG_FROM_WORDS_WRONG_TYPE)) } } /// Convert the integer to bytes, like int.to_bytes(). - fn to_bytes(&self, byteorder: Option<&str>, py: Python) -> PyResult { + fn to_bytes(&self, byteorder: Option<&str>, py: Python<'_>) -> PyResult> { let byteorder = byteorder.unwrap_or("little"); let bytes = match byteorder { "little" => PyBytes::new(py, &self.0.to_le_bytes()), "big" => PyBytes::new(py, &self.0.to_be_bytes()), - _ => { - return Err(PyValueError::new_err(ERRMSG_WRONG_ENDIANNESS)); - } + _ => return Err(PyValueError::new_err(ERRMSG_WRONG_ENDIANNESS)), }; - Ok(bytes.into()) + bytes.into_py_any(py) } /// Create UBig from bytes, like int.from_bytes(). #[staticmethod] - fn from_bytes(bytes: &PyBytes, byteorder: Option<&str>) -> PyResult { + fn from_bytes(bytes: &Bound<'_, PyBytes>, byteorder: Option<&str>) -> PyResult { let byteorder = byteorder.unwrap_or("little"); let uint = match byteorder { "little" => UBig::from_le_bytes(bytes.as_bytes()), "big" => UBig::from_be_bytes(bytes.as_bytes()), - _ => { - return Err(PyValueError::new_err(ERRMSG_WRONG_ENDIANNESS)); - } + _ => return Err(PyValueError::new_err(ERRMSG_WRONG_ENDIANNESS)), }; Ok(Self(uint)) } - fn to_chunks(&self, chunk_bits: usize, py: Python) -> PyResult { + fn to_chunks(&self, chunk_bits: usize, py: Python<'_>) -> PyResult> { if chunk_bits == 0 { - Err(PyValueError::new_err(ERRMSG_ZERO_CHUNK_SIZE)) - } else { - let iter = self - .0 - .to_chunks(chunk_bits) - .into_vec() - .into_iter() - .map(|u| UPy(u).into_py(py)); - Ok(PyTuple::new(py, iter).into_py(py)) + return Err(PyValueError::new_err(ERRMSG_ZERO_CHUNK_SIZE)); } + let chunks: Vec> = self + .0 + .to_chunks(chunk_bits) + .into_vec() + .into_iter() + .map(|u| UPy(u).into_py_any(py)) + .collect::>()?; + PyTuple::new(py, chunks)?.into_py_any(py) } #[staticmethod] - fn from_chunks(chunks: &PyAny, chunk_bits: usize) -> PyResult { + fn from_chunks(chunks: &Bound<'_, PyAny>, chunk_bits: usize) -> PyResult { if chunk_bits == 0 { return Err(PyValueError::new_err(ERRMSG_ZERO_CHUNK_SIZE)); } let mut input = Vec::new(); - if let Ok(list) = chunks.downcast::() { + if let Ok(list) = chunks.cast::() { input.reserve_exact(list.len()); for item in list { - input.push(UniInput::extract(item)?.to_ubig()?); + input.push(UniInput::extract(item.as_borrowed())?.into_ubig()?); } - } else if let Ok(tuple) = chunks.downcast::() { + } else if let Ok(tuple) = chunks.cast::() { input.reserve_exact(tuple.len()); for item in tuple { - input.push(UniInput::extract(item)?.to_ubig()?); + input.push(UniInput::extract(item.as_borrowed())?.into_ubig()?); } - } else if let Ok(iter) = chunks.downcast::() { + } else if let Ok(iter) = chunks.cast::() { for item in iter { - input.push(UniInput::extract(item?)?.to_ubig()?); + input.push(UniInput::extract(item?.as_borrowed())?.into_ubig()?); } } else { return Err(PyTypeError::new_err(ERRMSG_WRONG_CHUNKS_TYPE)); @@ -458,49 +694,100 @@ impl UPy { /********** operators **********/ #[inline] - fn __add__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __add__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_add(self, other, py) } #[inline] - fn __radd__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __radd__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_add(self, other, py) } #[inline] - fn __sub__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __sub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_sub(self, other, py) } #[inline] - fn __rsub__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __rsub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_rsub(other, self, py) } #[inline] - fn __mul__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __mul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_mul(self, other, py) } #[inline] - fn __rmul__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __rmul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_mul(self, other, py) } #[inline] - fn __truediv__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __truediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_div(self, other, py) } #[inline] - fn __rtruediv__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __rtruediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_rdiv(other, self, py) } #[inline] - fn __mod__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __floordiv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + upy_floordiv(self, other, py) + } + #[inline] + fn __rfloordiv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + upy_rfloordiv(other, self, py) + } + #[inline] + fn __mod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_mod(self, other, py) } #[inline] + fn __divmod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + upy_divmod(self, other, py) + } + #[inline] fn __pow__( &self, other: UniInput, modulus: Option, - py: Python, - ) -> PyResult { - ipy_pow(self.0.as_ibig(), other, modulus).map(|n| UPy(n.try_into().unwrap()).into_py(py)) + py: Python<'_>, + ) -> PyResult> { + use pyo3::IntoPyObjectExt; + ipy_pow(self.0.as_ibig(), other, modulus) + .and_then(|n| { + n.try_into() + .map_err(|_| PyValueError::new_err(ERRMSG_NEGATIVE_TO_UNSIGNED)) + }) + .and_then(|u: UBig| UPy(u).into_py_any(py)) + } + + #[inline] + fn __iadd__(&mut self, other: &Self) { + self.0 += &other.0; + } + #[inline] + fn __isub__(&mut self, other: &Self) { + self.0 -= &other.0; + } + #[inline] + fn __imul__(&mut self, other: &Self) { + self.0 *= &other.0; + } + #[inline] + fn __iand__(&mut self, other: &Self) { + self.0 &= &other.0; + } + #[inline] + fn __ior__(&mut self, other: &Self) { + self.0 |= &other.0; + } + #[inline] + fn __ixor__(&mut self, other: &Self) { + self.0 ^= &other.0; + } + #[inline] + fn __ilshift__(&mut self, other: usize) { + self.0 <<= other; + } + #[inline] + fn __irshift__(&mut self, other: usize) { + self.0 >>= other; } #[inline] @@ -516,7 +803,7 @@ impl UPy { slf } #[inline] - fn __nonzero__(&self) -> bool { + fn __bool__(&self) -> bool { !self.0.is_zero() } @@ -529,15 +816,15 @@ impl UPy { UPy((&self.0) >> other) } #[inline] - fn __and__(&self, other: UniInput, py: Python) -> PyResult { + fn __and__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_bitand(self, other, py) } #[inline] - fn __or__(&self, other: UniInput, py: Python) -> PyResult { + fn __or__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_bitor(self, other, py) } #[inline] - fn __xor__(&self, other: UniInput, py: Python) -> PyResult { + fn __xor__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { upy_bitxor(self, other, py) } } @@ -546,8 +833,9 @@ impl UPy { impl IPy { #[new] #[inline] - fn __new__(ob: &PyAny, radix: Option) -> PyResult { - if ob.is_instance_of::() { + #[pyo3(signature = (ob, radix=None))] + fn __new__(ob: &Bound<'_, PyAny>, radix: Option) -> PyResult { + if ob.is_instance_of::() { // create from int if radix.is_some() { return Err(PyTypeError::new_err(ERRMSG_INT_WITH_RADIX)); @@ -559,24 +847,24 @@ impl IPy { } else { Ok(IPy(parse_to_ibig(ob)?)) } - } else if let Ok(s) = ob.extract() { + } else if let Ok(s) = ob.extract::() { // create from string let n = if let Some(r) = radix { - IBig::from_str_radix(s, r) + IBig::from_str_radix(&s, r) } else { - IBig::from_str_with_radix_prefix(s).map(|v| v.0) + IBig::from_str_with_radix_prefix(&s).map(|v| v.0) }; Ok(IPy(n.map_err(parse_error_to_py)?)) - } else if let Ok(obj) = as FromPyObject>::extract(ob) { + } else if let Ok(obj) = ob.extract::>() { Ok(IPy(obj.0.clone().into())) - } else if let Ok(obj) = as FromPyObject>::extract(ob) { + } else if let Ok(obj) = ob.extract::>() { Ok(IPy(obj.0.clone())) } else { Err(PyTypeError::new_err(ERRMSG_IBIG_WRONG_SRC_TYPE)) } } - fn unwrap(&self, py: Python) -> PyResult { - convert_from_ibig(&self.0, py) + fn unwrap(&self, py: Python<'_>) -> PyResult> { + convert_from_ibig(&self.0, py)?.into_py_any(py) } fn __repr__(&self) -> String { @@ -585,8 +873,9 @@ impl IPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self) { - todo!() + fn __format__(&self, _format_spec: &str) -> String { + // MVP: ignore the format mini-language and delegate to Display. + format!("{}", self.0) } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); @@ -614,55 +903,282 @@ impl IPy { fn __len__(&self) -> usize { self.0.bit_len() } - fn __getitem__(&self, i: usize) -> bool { - self.0.bit(i) + fn __getitem__(&self, index: &Bound<'_, PyAny>) -> PyResult { + if let Ok(i) = index.extract::() { + let i = parse_signed_index(i, self.0.bit_len(), true) + .ok_or(PyIndexError::new_err(ERRMSG_UBIG_BITS_OOR))?; + Ok(self.0.bit(i)) + } else { + Err(PyTypeError::new_err(ERRMSG_WRONG_INDEX_TYPE)) + } + } + + /********** number theory & roots (note return types!) **********/ + fn sqrt(&self) -> PyResult { + if self.0.is_negative() { + return Err(PyValueError::new_err( + "cannot compute the square root of a negative number", + )); + } + Ok(UPy(self.0.sqrt())) + } + fn cbrt(&self) -> Self { + // IBig's CubicRoot trait impl panics on negatives; nth_root(3) is sign-preserving. + IPy(self.0.nth_root(3)) + } + fn nth_root(&self, n: usize) -> PyResult { + if n % 2 == 0 && self.0.sign() == Sign::Negative { + return Err(PyValueError::new_err("cannot compute an even root of a negative number")); + } + Ok(IPy(self.0.nth_root(n))) + } + fn sqr(&self) -> UPy { + UPy(self.0.sqr()) + } + fn cubic(&self) -> Self { + IPy(self.0.cubic()) + } + fn ilog(&self, base: &UPy) -> PyResult { + if base.0 <= UBig::ONE { + return Err(PyValueError::new_err("base must be greater than 1")); + } + Ok(self.0.ilog(&base.0)) + } + fn trailing_zeros(&self) -> Option { + self.0.trailing_zeros() + } + fn trailing_ones(&self) -> Option { + self.0.trailing_ones() + } + + /********** accessors **********/ + fn is_one(&self) -> bool { + self.0.is_one() + } + fn sign(&self) -> PySign { + self.0.sign().into() + } + fn signum(&self) -> Self { + IPy(self.0.signum()) + } + fn is_negative(&self) -> bool { + self.0.sign() == Sign::Negative + } + fn is_positive(&self) -> bool { + self.0.sign() == Sign::Positive + } + fn to_parts(&self) -> (PySign, UPy) { + let (sign, mag) = self.0.clone().into_parts(); + (sign.into(), UPy(mag)) + } + #[staticmethod] + fn from_parts(sign: PySign, magnitude: &UPy) -> Self { + IPy(IBig::from_parts(sign.into(), magnitude.0.clone())) + } + fn as_ubig(&self) -> Option { + self.0.as_ubig().cloned().map(UPy) + } + #[staticmethod] + fn ones(n: usize) -> Self { + IPy(IBig::from(UBig::ones(n))) + } + + /********** interop **********/ + fn __int__(&self, py: Python<'_>) -> PyResult> { + convert_from_ibig(&self.0, py)?.into_py_any(py) + } + /// Get the underlying (sign, words) representing this integer + fn to_words(&self) -> PyWords { + let (_, words) = self.0.as_sign_words(); + PyWords(words.to_vec()) + } + /// Create an integer from a list of words (interpreted as non-negative magnitude) + #[staticmethod] + fn from_words(ob: &Bound<'_, PyAny>) -> PyResult { + if let Ok(vec) = ob.extract::>() { + Ok(IPy(IBig::from(UBig::from_words(&vec)))) + } else if let Ok(words) = ob.extract::>() { + Ok(IPy(IBig::from(UBig::from_words(&words.0)))) + } else { + Err(PyTypeError::new_err(ERRMSG_FROM_WORDS_WRONG_TYPE)) + } + } + fn to_chunks(&self, chunk_bits: usize, py: Python<'_>) -> PyResult> { + if chunk_bits == 0 { + return Err(PyValueError::new_err(ERRMSG_ZERO_CHUNK_SIZE)); + } + let mag = (&self.0).unsigned_abs(); + let chunks: Vec> = mag + .to_chunks(chunk_bits) + .into_vec() + .into_iter() + .map(|u| UPy(u).into_py_any(py)) + .collect::>()?; + PyTuple::new(py, chunks)?.into_py_any(py) + } + #[staticmethod] + fn from_chunks(chunks: &Bound<'_, PyAny>, chunk_bits: usize) -> PyResult { + if chunk_bits == 0 { + return Err(PyValueError::new_err(ERRMSG_ZERO_CHUNK_SIZE)); + } + + let mut input = Vec::new(); + if let Ok(list) = chunks.cast::() { + input.reserve_exact(list.len()); + for item in list { + input.push(UniInput::extract(item.as_borrowed())?.into_ubig()?); + } + } else if let Ok(tuple) = chunks.cast::() { + input.reserve_exact(tuple.len()); + for item in tuple { + input.push(UniInput::extract(item.as_borrowed())?.into_ubig()?); + } + } else if let Ok(iter) = chunks.cast::() { + for item in iter { + input.push(UniInput::extract(item?.as_borrowed())?.into_ubig()?); + } + } else { + return Err(PyTypeError::new_err(ERRMSG_WRONG_CHUNKS_TYPE)); + } + + let mag = UBig::from_chunks(input.iter(), chunk_bits); + Ok(IPy(IBig::from(mag))) + } + /// Convert the integer to bytes, like int.to_bytes(). + fn to_bytes( + &self, + byteorder: Option<&str>, + signed: Option, + py: Python<'_>, + ) -> PyResult> { + use pyo3::IntoPyObjectExt; + let signed = signed.unwrap_or(false); + if !signed && self.0.is_negative() { + return Err(PyOverflowError::new_err(ERRMSG_NEGATIVE_TO_UNSIGNED)); + } + + let byteorder = byteorder.unwrap_or("little"); + let bytes = match byteorder { + "little" => PyBytes::new(py, &self.0.to_le_bytes()), + "big" => PyBytes::new(py, &self.0.to_be_bytes()), + _ => return Err(PyValueError::new_err(ERRMSG_WRONG_ENDIANNESS)), + }; + bytes.into_py_any(py) + } + /// Create IBig from bytes, like int.from_bytes(). + #[staticmethod] + fn from_bytes( + bytes: &Bound<'_, PyBytes>, + byteorder: Option<&str>, + signed: Option, + ) -> PyResult { + let byteorder = byteorder.unwrap_or("little"); + let signed = signed.unwrap_or(false); + let int = match byteorder { + "little" => match signed { + false => UBig::from_le_bytes(bytes.as_bytes()).into(), + true => IBig::from_le_bytes(bytes.as_bytes()), + }, + "big" => match signed { + false => UBig::from_be_bytes(bytes.as_bytes()).into(), + true => IBig::from_be_bytes(bytes.as_bytes()), + }, + _ => return Err(PyValueError::new_err(ERRMSG_WRONG_ENDIANNESS)), + }; + Ok(Self(int)) } /********** operators **********/ #[inline] - fn __add__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __add__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_add(self, other, py) } #[inline] - fn __radd__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __radd__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_add(self, other, py) } #[inline] - fn __sub__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __sub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_sub(self, other, py) } #[inline] - fn __rsub__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __rsub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_rsub(other, self, py) } #[inline] - fn __mul__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __mul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_mul(self, other, py) } #[inline] - fn __rmul__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __rmul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_mul(self, other, py) } #[inline] - fn __truediv__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __truediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_div(self, other, py) } #[inline] - fn __rtruediv__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __rtruediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_rdiv(other, self, py) } #[inline] - fn __mod__(&self, other: UniInput<'_>, py: Python) -> PyObject { + fn __floordiv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + ipy_floordiv(self, other, py) + } + #[inline] + fn __rfloordiv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + ipy_rfloordiv(other, self, py) + } + #[inline] + fn __mod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_mod(self, other, py) } #[inline] + fn __divmod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + ipy_divmod(self, other, py) + } + #[inline] fn __pow__( &self, other: UniInput, modulus: Option, - py: Python, - ) -> PyResult { - ipy_pow(&self.0, other, modulus).map(|n| IPy(n).into_py(py)) + py: Python<'_>, + ) -> PyResult> { + use pyo3::IntoPyObjectExt; + ipy_pow(&self.0, other, modulus).and_then(|n| IPy(n).into_py_any(py)) + } + + #[inline] + fn __iadd__(&mut self, other: &Self) { + self.0 += &other.0; + } + #[inline] + fn __isub__(&mut self, other: &Self) { + self.0 -= &other.0; + } + #[inline] + fn __imul__(&mut self, other: &Self) { + self.0 *= &other.0; + } + #[inline] + fn __iand__(&mut self, other: &Self) { + self.0 &= &other.0; + } + #[inline] + fn __ior__(&mut self, other: &Self) { + self.0 |= &other.0; + } + #[inline] + fn __ixor__(&mut self, other: &Self) { + self.0 ^= &other.0; + } + #[inline] + fn __ilshift__(&mut self, other: usize) { + self.0 <<= other; + } + #[inline] + fn __irshift__(&mut self, other: usize) { + self.0 >>= other; } #[inline] @@ -678,7 +1194,11 @@ impl IPy { IPy((&self.0).abs()) } #[inline] - fn __nonzero__(&self) -> bool { + fn __invert__(&self) -> Self { + IPy(!&self.0) + } + #[inline] + fn __bool__(&self) -> bool { !self.0.is_zero() } @@ -691,67 +1211,15 @@ impl IPy { IPy((&self.0) >> other) } #[inline] - fn __and__(&self, other: UniInput, py: Python) -> PyResult { + fn __and__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_bitand(self, other, py) } #[inline] - fn __or__(&self, other: UniInput, py: Python) -> PyResult { + fn __or__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_bitor(self, other, py) } #[inline] - fn __xor__(&self, other: UniInput, py: Python) -> PyResult { + fn __xor__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { ipy_bitxor(self, other, py) } - - /********** interop **********/ - - fn __int__(&self, py: Python) -> PyResult { - convert_from_ibig(&self.0, py) - } - /// Convert the integer to bytes, like int.to_bytes(). - fn to_bytes( - &self, - byteorder: Option<&str>, - signed: Option, - py: Python, - ) -> PyResult { - let signed = signed.unwrap_or(false); - if !signed && self.0.is_negative() { - return Err(PyOverflowError::new_err(ERRMSG_NEGATIVE_TO_UNSIGNED)); - } - - let byteorder = byteorder.unwrap_or("little"); - let bytes = match byteorder { - "little" => PyBytes::new(py, &self.0.to_le_bytes()), - "big" => PyBytes::new(py, &self.0.to_be_bytes()), - _ => { - return Err(PyValueError::new_err(ERRMSG_WRONG_ENDIANNESS)); - } - }; - Ok(bytes.into()) - } - /// Create IBig from bytes, like int.from_bytes(). - #[staticmethod] - fn from_bytes( - bytes: &PyBytes, - byteorder: Option<&str>, - signed: Option, - ) -> PyResult { - let byteorder = byteorder.unwrap_or("little"); - let signed = signed.unwrap_or(false); - let int = match byteorder { - "little" => match signed { - false => UBig::from_le_bytes(bytes.as_bytes()).into(), - true => IBig::from_le_bytes(bytes.as_bytes()), - }, - "big" => match signed { - false => UBig::from_be_bytes(bytes.as_bytes()).into(), - true => IBig::from_be_bytes(bytes.as_bytes()), - }, - _ => { - return Err(PyValueError::new_err(ERRMSG_WRONG_ENDIANNESS)); - } - }; - Ok(Self(int)) - } } diff --git a/python/src/lib.rs b/python/src/lib.rs index 3905af29..81887c19 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,6 +1,15 @@ +// PyO3 0.29 deprecates the automatic `FromPyObject` impl for `Clone` pyclasses in favor of +// an explicit opt-in (`#[pyclass(from_py_object)]`). We extract pyclasses via `PyRef` (which +// has its own impl) and as `&T` arguments, so the deprecated auto-impl is harmless here; it is +// tracked as a follow-up to switch to the explicit derive before the auto-impl is removed. +#![allow(deprecated)] + +mod cache; +mod complex; mod convert; mod float; mod int; +mod math; mod ratio; mod types; mod utils; @@ -12,7 +21,7 @@ use pyo3::prelude::*; /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to /// import the module. #[pymodule] -fn dashu(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn dashu(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -20,8 +29,40 @@ fn dashu(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(utils::auto, m)?)?; m.add_function(wrap_pyfunction!(utils::autos, m)?)?; + + // module-level math functions + m.add_function(wrap_pyfunction!(math::sin, m)?)?; + m.add_function(wrap_pyfunction!(math::cos, m)?)?; + m.add_function(wrap_pyfunction!(math::tan, m)?)?; + m.add_function(wrap_pyfunction!(math::asin, m)?)?; + m.add_function(wrap_pyfunction!(math::acos, m)?)?; + m.add_function(wrap_pyfunction!(math::atan, m)?)?; + m.add_function(wrap_pyfunction!(math::atan2, m)?)?; + m.add_function(wrap_pyfunction!(math::sinh, m)?)?; + m.add_function(wrap_pyfunction!(math::cosh, m)?)?; + m.add_function(wrap_pyfunction!(math::tanh, m)?)?; + m.add_function(wrap_pyfunction!(math::asinh, m)?)?; + m.add_function(wrap_pyfunction!(math::acosh, m)?)?; + m.add_function(wrap_pyfunction!(math::atanh, m)?)?; + m.add_function(wrap_pyfunction!(math::exp, m)?)?; + m.add_function(wrap_pyfunction!(math::expm1, m)?)?; + m.add_function(wrap_pyfunction!(math::log, m)?)?; + m.add_function(wrap_pyfunction!(math::log1p, m)?)?; + m.add_function(wrap_pyfunction!(math::ln, m)?)?; + m.add_function(wrap_pyfunction!(math::ln_1p, m)?)?; + m.add_function(wrap_pyfunction!(math::sqrt, m)?)?; + m.add_function(wrap_pyfunction!(math::cbrt, m)?)?; + m.add_function(wrap_pyfunction!(math::nth_root, m)?)?; + m.add_function(wrap_pyfunction!(math::powf, m)?)?; + m.add_function(wrap_pyfunction!(math::powi, m)?)?; + m.add_function(wrap_pyfunction!(math::hypot, m)?)?; + m.add_function(wrap_pyfunction!(math::gcd, m)?)?; + m.add_function(wrap_pyfunction!(math::gcd_ext, m)?)?; + m.add_function(wrap_pyfunction!(math::lcm, m)?)?; Ok(()) } diff --git a/python/src/math.rs b/python/src/math.rs new file mode 100644 index 00000000..36e74509 --- /dev/null +++ b/python/src/math.rs @@ -0,0 +1,177 @@ +//! Module-level math functions. Each routes through the global `ConstCache` + the +//! panic-free `Context` layer (via [`crate::cache::unwrap_float`]), so domain errors raise +//! Python exceptions instead of aborting the session. + +use crate::cache::{unwrap_float, with_cache}; +use crate::types::{FPy, IPy, UPy}; +use dashu_base::ring::{ExtendedGcd, Gcd}; +use pyo3::prelude::*; + +// Trigonometric +#[pyfunction] +pub fn sin(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.sin(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn cos(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.cos(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn tan(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.tan(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn asin(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.asin(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn acos(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.acos(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn atan(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.atan(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn atan2(y: &FPy, x: &FPy) -> PyResult { + let ctx = y.0.context(); + let res = with_cache(|c| ctx.atan2(y.0.repr(), x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} + +// Hyperbolic +#[pyfunction] +pub fn sinh(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.sinh(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn cosh(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.cosh(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn tanh(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.tanh(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn asinh(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.asinh(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn acosh(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.acosh(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn atanh(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.atanh(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} + +// Exponential and logarithm +#[pyfunction] +pub fn exp(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.exp(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn expm1(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.exp_m1(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn log(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.ln(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn log1p(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.ln_1p(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn ln(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.ln(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn ln_1p(x: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.ln_1p(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} + +// Roots and power (sqrt/cbrt/nth_root/hypot are algebraic — no cache, but still via Context) +#[pyfunction] +pub fn sqrt(x: &FPy) -> PyResult { + let ctx = x.0.context(); + Ok(FPy(unwrap_float(ctx.sqrt(x.0.repr()), ctx)?)) +} +#[pyfunction] +pub fn cbrt(x: &FPy) -> PyResult { + let ctx = x.0.context(); + Ok(FPy(unwrap_float(ctx.cbrt(x.0.repr()), ctx)?)) +} +#[pyfunction] +pub fn nth_root(x: &FPy, n: usize) -> PyResult { + let ctx = x.0.context(); + Ok(FPy(unwrap_float(ctx.nth_root(n, x.0.repr()), ctx)?)) +} +#[pyfunction] +pub fn powf(x: &FPy, y: &FPy) -> PyResult { + let ctx = x.0.context(); + let res = with_cache(|c| ctx.powf(x.0.repr(), y.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) +} +#[pyfunction] +pub fn powi(x: &FPy, n: &IPy) -> PyResult { + let ctx = x.0.context(); + Ok(FPy(unwrap_float(ctx.powi(x.0.repr(), n.0.clone()), ctx)?)) +} +#[pyfunction] +pub fn hypot(x: &FPy, y: &FPy) -> PyResult { + let ctx = x.0.context(); + Ok(FPy(unwrap_float(ctx.hypot(x.0.repr(), y.0.repr()), ctx)?)) +} + +// Integer number theory +#[pyfunction] +pub fn gcd(a: &UPy, b: &UPy) -> UPy { + UPy(Gcd::gcd(&a.0, &b.0)) +} +#[pyfunction] +pub fn gcd_ext(a: &UPy, b: &UPy) -> (UPy, IPy, IPy) { + let (g, s, t) = ExtendedGcd::gcd_ext(&a.0, &b.0); + (UPy(g), IPy(s), IPy(t)) +} +#[pyfunction] +pub fn lcm(a: &UPy, b: &UPy) -> UPy { + let g = Gcd::gcd(&a.0, &b.0); + UPy((a.0.clone() / g) * b.0.clone()) +} diff --git a/python/src/ratio.rs b/python/src/ratio.rs index 17a1ee11..3dec2ed6 100644 --- a/python/src/ratio.rs +++ b/python/src/ratio.rs @@ -1,39 +1,83 @@ use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; +use std::ops::{Add, Div, Mul, Rem, Sub}; +use dashu_base::Abs; +use dashu_float::round::mode; use dashu_ratio::RBig; -use num_order::NumHash; -use pyo3::{exceptions::PyTypeError, intern, prelude::*}; +use num_order::{NumHash, NumOrd}; +use pyo3::{Bound, IntoPyObjectExt, Py, PyAny, PyResult, basic::CompareOp, intern, prelude::*}; use crate::{ - convert::{convert_from_rbig, parse_error_to_py, parse_to_rbig}, - types::RPy, + convert::{parse_error_to_py, parse_to_rbig}, + types::{DPy, FPy, IPy, PySign, RPy, UPy, UniInput}, }; -const ERRMSG_RBIG_WRONG_SRC_TYPE: &str = - "only Fraction instances or strings can be used to construct an RBig instance"; +/// Forward/reverse arithmetic dispatchers that coerce the operand to RBig first. +macro_rules! impl_rpy_binops { + ($method:ident, $rs_method:ident) => { + fn $method(lhs: &RPy, rhs: UniInput<'_>, py: Python<'_>) -> PyResult> { + let rhs = rhs.into_rpy()?; + RPy((&lhs.0).$rs_method(&rhs.0)).into_py_any(py) + } + }; + ($method:ident, $rev_method:ident, $rs_method:ident) => { + impl_rpy_binops!($method, $rs_method); + fn $rev_method(lhs: UniInput<'_>, rhs: &RPy, py: Python<'_>) -> PyResult> { + let lhs = lhs.into_rpy()?; + RPy((&lhs.0).$rs_method(&rhs.0)).into_py_any(py) + } + }; +} + +impl_rpy_binops!(rpy_add, add); +impl_rpy_binops!(rpy_sub, rpy_rsub, sub); +impl_rpy_binops!(rpy_mul, mul); +impl_rpy_binops!(rpy_div, rpy_rdiv, div); +impl_rpy_binops!(rpy_mod, rpy_rmod, rem); + +fn rpy_richcmp(lhs: &RPy, other: UniInput<'_>, op: CompareOp) -> bool { + let order = match other { + UniInput::Uint(x) => lhs.0.num_cmp(&x), + UniInput::Int(x) => lhs.0.num_cmp(&x), + UniInput::BUint(x) => lhs.0.num_cmp(&x.0), + UniInput::BInt(x) => lhs.0.num_cmp(&x.0), + UniInput::OBInt(x) => lhs.0.num_cmp(&x), + UniInput::Float(x) => lhs.0.num_cmp(&x), + UniInput::BFloat(x) => lhs.0.num_cmp(&x.0), + UniInput::BDecimal(x) => lhs.0.num_cmp(&x.0), + UniInput::OBDecimal(x) => lhs.0.num_cmp(&x), + UniInput::BRational(x) => lhs.0.cmp(&x.0), + UniInput::OBRational(x) => lhs.0.cmp(&x), + }; + op.matches(order) +} #[pymethods] impl RPy { #[new] - fn __new__(ob: &PyAny) -> PyResult { - let py = ob.py(); - let fractions = py.import(intern!(py, "fractions"))?; - if ob.is_instance(fractions.getattr(intern!(py, "Fraction"))?)? { - // create from fractions.Fraction - Ok(RPy(parse_to_rbig(ob)?)) - } else if let Ok(s) = ob.extract() { - // create from string - let d = RBig::from_str_with_radix_prefix(s).map(|v| v.0); - Ok(RPy(d.map_err(parse_error_to_py)?)) - } else if let Ok(obj) = as FromPyObject>::extract(ob) { - Ok(RPy(obj.0.clone())) - } else { - Err(PyTypeError::new_err(ERRMSG_RBIG_WRONG_SRC_TYPE)) + fn __new__(ob: &Bound<'_, PyAny>) -> PyResult { + if let Ok(s) = ob.extract::() { + let r = RBig::from_str_with_radix_prefix(&s).map(|v| v.0); + return Ok(RPy(r.map_err(parse_error_to_py)?)); + } + // fractions.Fraction fast path (preserves the original parse_to_rbig round-trip) + { + let py = ob.py(); + let fractions = py.import(intern!(py, "fractions"))?; + let fraction_type = fractions.getattr(intern!(py, "Fraction"))?; + if ob.is_instance(&fraction_type)? { + return Ok(RPy(parse_to_rbig(ob)?)); + } } + if let Ok(obj) = ob.extract::>() { + return Ok(RPy(obj.0.clone())); + } + // any other Python number -> permissive construction + Ok(RPy(UniInput::extract(ob.as_borrowed())?.construct_rpy()?)) } - fn unwrap(&self, py: Python) -> PyResult { - convert_from_rbig(&self.0, py) + fn unwrap(&self, py: Python<'_>) -> PyResult> { + crate::convert::convert_from_rbig(&self.0, py)?.into_py_any(py) } fn __repr__(&self) -> String { @@ -42,16 +86,154 @@ impl RPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self) { - todo!() + fn __format__(&self, _format_spec: &str) -> String { + format!("{}", self.0) } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); self.0.num_hash(&mut hasher); hasher.finish() } - + fn __richcmp__(&self, other: UniInput<'_>, op: CompareOp) -> bool { + rpy_richcmp(self, other, op) + } + fn __bool__(&self) -> bool { + !self.0.is_zero() + } fn __float__(&self) -> f64 { self.0.to_f64_fast() } + fn __int__(&self, py: Python<'_>) -> PyResult> { + crate::convert::convert_from_ibig(&self.0.trunc(), py)?.into_py_any(py) + } + + /********** arithmetic **********/ + #[inline] + fn __add__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_add(self, other, py) + } + #[inline] + fn __radd__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_add(self, other, py) + } + #[inline] + fn __sub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_sub(self, other, py) + } + #[inline] + fn __rsub__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_rsub(other, self, py) + } + #[inline] + fn __mul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_mul(self, other, py) + } + #[inline] + fn __rmul__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_mul(self, other, py) + } + #[inline] + fn __truediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_div(self, other, py) + } + #[inline] + fn __rtruediv__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_rdiv(other, self, py) + } + #[inline] + fn __mod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_mod(self, other, py) + } + #[inline] + fn __rmod__(&self, other: UniInput<'_>, py: Python<'_>) -> PyResult> { + rpy_rmod(other, self, py) + } + #[inline] + fn __neg__(&self) -> Self { + RPy(-&self.0) + } + #[inline] + fn __pos__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + #[inline] + fn __abs__(&self) -> Self { + RPy(self.0.clone().abs()) + } + + /********** properties & predicates **********/ + #[getter] + fn numerator(&self) -> IPy { + IPy(self.0.numerator().clone()) + } + #[getter] + fn denominator(&self) -> UPy { + UPy(self.0.denominator().clone()) + } + fn is_int(&self) -> bool { + self.0.is_int() + } + fn is_one(&self) -> bool { + self.0.is_one() + } + fn sign(&self) -> PySign { + self.0.sign().into() + } + fn signum(&self) -> Self { + RPy(self.0.signum()) + } + + /********** rounding (return IBig / RPy) **********/ + fn trunc(&self) -> IPy { + IPy(self.0.trunc()) + } + fn floor(&self) -> IPy { + IPy(self.0.floor()) + } + fn ceil(&self) -> IPy { + IPy(self.0.ceil()) + } + fn round(&self) -> IPy { + IPy(self.0.round()) + } + fn fract(&self) -> Self { + RPy(self.0.fract()) + } + fn split_at_point(&self) -> (IPy, Self) { + let (int_part, frac_part) = self.0.clone().split_at_point(); + (IPy(int_part), RPy(frac_part)) + } + + /********** powers **********/ + fn sqr(&self) -> Self { + RPy(self.0.sqr()) + } + fn cubic(&self) -> Self { + RPy(self.0.cubic()) + } + fn pow(&self, n: usize) -> Self { + RPy(self.0.pow(n)) + } + + /********** construction & conversion **********/ + fn to_int(&self) -> IPy { + IPy(self.0.trunc()) + } + /// Lossy conversion to a binary float with the given precision. + fn to_float(&self, precision: usize) -> FPy { + FPy(self.0.to_float::(precision).value()) + } + /// Lossy conversion to a decimal float with the given precision. + fn to_decimal(&self, precision: usize) -> DPy { + DPy(self.0.to_float::(precision).value()) + } + #[staticmethod] + fn from_parts(numerator: &IPy, denominator: &UPy) -> Self { + RPy(RBig::from_parts(numerator.0.clone(), denominator.0.clone())) + } + /// Find the simplest rational within the error bounds of the given float. + #[staticmethod] + fn simplest_from_float(f: &FPy) -> Option { + RBig::simplest_from_float(&f.0).map(RPy) + } } diff --git a/python/src/types.rs b/python/src/types.rs index 2c8024d0..e3845ee8 100644 --- a/python/src/types.rs +++ b/python/src/types.rs @@ -7,7 +7,8 @@ use dashu_int::{IBig, UBig}; use dashu_ratio::RBig; type FBig = dashu_float::FBig; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq, Clone, Copy)] pub enum PySign { Positive, Negative, @@ -23,6 +24,16 @@ impl From for PySign { } } +impl From for Sign { + #[inline] + fn from(value: PySign) -> Self { + match value { + PySign::Positive => Sign::Positive, + PySign::Negative => Sign::Negative, + } + } +} + /// This struct is used for representing [UBig] in Python #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] #[pyclass(name = "UBig")] @@ -78,6 +89,18 @@ impl From for RPy { } } +/// This struct is used for representing [CBig] in Python. It wraps a *bare* complex number +/// (base 2, rounding `Zero`); the constant cache is shared module-wide (see [`crate::cache`]). +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +#[pyclass(name = "CBig")] +pub struct CPy(pub dashu_cmplx::CBig); + +impl From> for CPy { + fn from(n: dashu_cmplx::CBig) -> Self { + CPy(n) + } +} + #[pyclass(name = "Words")] pub struct PyWords(pub std::vec::Vec); diff --git a/python/src/utils.rs b/python/src/utils.rs index bbbc4d8c..4f115d6f 100644 --- a/python/src/utils.rs +++ b/python/src/utils.rs @@ -8,41 +8,39 @@ use dashu_base::{Signed, UnsignedAbs}; use dashu_float::{DBig, FBig}; use dashu_int::{IBig, UBig}; use dashu_ratio::RBig; -use pyo3::prelude::*; +use pyo3::{IntoPyObjectExt, Py, PyAny, PyResult, prelude::*}; /// Convert input automatically to corresponding dashu type: /// (int -> UBig/IBig, float -> FBig, decimal -> DBig, fraction -> RBig) #[pyfunction] -pub fn auto(ob: UniInput, py: Python<'_>) -> PyResult { +pub fn auto(ob: UniInput, py: Python<'_>) -> PyResult> { use UniInput::*; // shrink IBig to UBig if necessary - let fit_ibig = |i: IBig| { + let fit_ibig = |i: IBig| -> PyResult> { if i.is_negative() { - IPy(i).into_py(py) + IPy(i).into_py_any(py) } else { - UPy(i.unsigned_abs()).into_py(py) + UPy(i.unsigned_abs()).into_py_any(py) } }; // TODO: shrink each type to the minimal representation (FBig/RBig -> IBig -> UBig) let obj = match ob { - Uint(v) => UPy(v.into()).into_py(py), - Int(v) => fit_ibig(v.into()), - BUint(v) => v.clone().into_py(py), - BInt(v) => fit_ibig(v.0.clone()), - OBInt(v) => fit_ibig(v), + Uint(v) => UPy(v.into()).into_py_any(py)?, + Int(v) => fit_ibig(v.into())?, + BUint(v) => v.clone().into_py_any(py)?, + BInt(v) => fit_ibig(v.0.clone())?, + OBInt(v) => fit_ibig(v)?, Float(v) => match v.try_into() { - Ok(big) => FPy(big).into_py(py), - Err(e) => { - return Err(conversion_error_to_py(e)); - } + Ok(big) => FPy(big).into_py_any(py)?, + Err(e) => return Err(conversion_error_to_py(e)), }, - BFloat(v) => v.clone().into_py(py), - BDecimal(v) => v.clone().into_py(py), - OBDecimal(v) => DPy(v).into_py(py), - BRational(v) => v.clone().into_py(py), - OBRational(v) => RPy(v).into_py(py), + BFloat(v) => v.clone().into_py_any(py)?, + BDecimal(v) => v.clone().into_py_any(py)?, + OBDecimal(v) => DPy(v).into_py_any(py)?, + BRational(v) => v.clone().into_py_any(py)?, + OBRational(v) => RPy(v).into_py_any(py)?, }; Ok(obj) } @@ -50,26 +48,26 @@ pub fn auto(ob: UniInput, py: Python<'_>) -> PyResult { /// Convert input string to corresponding dashu type. /// The type is heuristically determined #[pyfunction] -pub fn autos(s: &str, py: Python<'_>) -> PyResult { +pub fn autos(s: &str, py: Python<'_>) -> PyResult> { let obj = if s.contains('/') { RPy(RBig::from_str_with_radix_prefix(s) .map_err(parse_error_to_py)? .0) - .into_py(py) + .into_py_any(py)? } else if s.contains(['p', 'P']) { - FPy(FBig::from_str(s).map_err(parse_error_to_py)?).into_py(py) + FPy(FBig::from_str(s).map_err(parse_error_to_py)?).into_py_any(py)? } else if s.contains('.') || (!s.contains("0x") && s.contains(['e', 'E'])) { - DPy(DBig::from_str(s).map_err(parse_error_to_py)?).into_py(py) + DPy(DBig::from_str(s).map_err(parse_error_to_py)?).into_py_any(py)? } else if s.contains('-') { IPy(IBig::from_str_with_radix_prefix(s) .map_err(parse_error_to_py)? .0) - .into_py(py) + .into_py_any(py)? } else { UPy(UBig::from_str_with_radix_prefix(s) .map_err(parse_error_to_py)? .0) - .into_py(py) + .into_py_any(py)? }; Ok(obj) } diff --git a/python/src/words.rs b/python/src/words.rs index d922aa1d..e38c6a39 100644 --- a/python/src/words.rs +++ b/python/src/words.rs @@ -1,6 +1,7 @@ use crate::types::PyWords; use pyo3::{ + Bound, IntoPyObjectExt, Py, PyAny, PyResult, exceptions::{PyIndexError, PyTypeError, PyValueError}, prelude::*, types::PySlice, @@ -19,10 +20,10 @@ const ERRMSG_WORDS_INVALID_VALUE: &str = "words can only contain word-length int #[pymethods] impl PyWords { #[new] - fn __new__(ob: &PyAny) -> PyResult { - if let Ok(list) = as FromPyObject>::extract(ob) { + fn __new__(ob: &Bound<'_, PyAny>) -> PyResult { + if let Ok(list) = ob.extract::>() { Ok(PyWords(list)) - } else if let Ok(obj) = as FromPyObject>::extract(ob) { + } else if let Ok(obj) = ob.extract::>() { Ok(PyWords(obj.0.clone())) } else { Err(PyTypeError::new_err(ERRMSG_WORDS_WRONG_SRC_TYPE)) @@ -37,9 +38,9 @@ impl PyWords { fn __len__(&self) -> usize { self.0.len() } - fn __getitem__(&self, index: &PyAny) -> PyResult { + fn __getitem__(&self, index: &Bound<'_, PyAny>) -> PyResult> { let py = index.py(); - if let Ok(n) = ::extract(index) { + if let Ok(n) = index.extract::() { // parse negative index let n = if n < 0 { (self.0.len() as isize + n) as usize @@ -53,8 +54,8 @@ impl PyWords { .get(n) .copied() .ok_or(PyIndexError::new_err(ERRMSG_WORDS_OOR))?; - Ok(word.into_py(py)) - } else if let Ok(slice) = index.downcast::() { + word.into_py_any(py) + } else if let Ok(slice) = index.cast::() { let indices = slice.indices(self.0.len() as _)?; let new_vec = if indices.step >= 0 { @@ -86,13 +87,13 @@ impl PyWords { Vec::new() } }; - Ok(Self(new_vec).into_py(py)) + Self(new_vec).into_py_any(py) } else { Err(PyTypeError::new_err(ERRMSG_WORDS_INVALID_INDEX)) } } - fn __setitem__(&mut self, index: &PyAny, value: &PyAny) -> PyResult<()> { - if let Ok(n) = ::extract(index) { + fn __setitem__(&mut self, index: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult<()> { + if let Ok(n) = index.extract::() { let value: Word = value.extract()?; // parse negative index @@ -109,19 +110,19 @@ impl PyWords { } else { Err(PyIndexError::new_err(ERRMSG_WORDS_OOR)) } - } else if let Ok(slice) = index.downcast::() { + } else if let Ok(slice) = index.cast::() { // parse inputs let indices = slice.indices(self.0.len() as _)?; - let value: Vec = if let Ok(v) = as FromPyObject>::extract(value) { + let value: Vec = if let Ok(v) = value.extract::>() { v - } else if let Ok(v) = as FromPyObject>::extract(value) { + } else if let Ok(v) = value.extract::>() { v.0.clone() // FIXME: how to prevent copy here? } else { return Err(PyTypeError::new_err(ERRMSG_WORDS_INVALID_VALUE)); }; // check that the indices and the values have the same length - if indices.slicelength as usize != value.len() { + if indices.slicelength != value.len() { return Err(PyValueError::new_err(ERRMSG_WORDS_UNMATCH_INDEX)); } @@ -156,8 +157,8 @@ impl PyWords { Err(PyTypeError::new_err(ERRMSG_WORDS_INVALID_INDEX)) } } - fn __delitem__(&mut self, index: &PyAny) -> PyResult<()> { - if let Ok(n) = ::extract(index) { + fn __delitem__(&mut self, index: &Bound<'_, PyAny>) -> PyResult<()> { + if let Ok(n) = index.extract::() { // parse negative index let n = if n < 0 { (self.0.len() as isize + n) as usize @@ -172,7 +173,7 @@ impl PyWords { } else { Err(PyIndexError::new_err(ERRMSG_WORDS_OOR)) } - } else if let Ok(slice) = index.downcast::() { + } else if let Ok(slice) = index.cast::() { let indices = slice.indices(self.0.len() as _)?; if indices.step >= 0 { @@ -184,7 +185,7 @@ impl PyWords { .enumerate() .filter_map(|(i, v)| { let in_slice = i >= skip && i < (skip + span) && (i - skip) % step == 0; - (!in_slice).then(|| v) + (!in_slice).then_some(v) }) .collect(); } @@ -199,7 +200,7 @@ impl PyWords { .enumerate() .filter_map(|(i, v)| { let in_slice = i >= skip && i < (skip + span) && (i - skip) % step == 0; - (!in_slice).then(|| v) + (!in_slice).then_some(v) }) .rev() .collect(); @@ -211,11 +212,11 @@ impl PyWords { } } - fn __add__(&self, other: &PyAny) -> PyResult { + fn __add__(&self, other: &Bound<'_, PyAny>) -> PyResult { let mut out = self.0.clone(); - if let Ok(list) = as FromPyObject>::extract(other) { + if let Ok(list) = other.extract::>() { out.extend(list); - } else if let Ok(obj) = as FromPyObject>::extract(other) { + } else if let Ok(obj) = other.extract::>() { out.extend(obj.0.iter()); } else { return Err(PyTypeError::new_err(ERRMSG_WORDS_WRONG_SRC_TYPE)); diff --git a/python/tests/test_float_ops.py b/python/tests/test_float_ops.py new file mode 100644 index 00000000..37776918 --- /dev/null +++ b/python/tests/test_float_ops.py @@ -0,0 +1,89 @@ +import math + +from dashu import * + + +def test_construct(): + # FBig (base 2) from float / int / string (binary digits) + assert FBig(1.5) == FBig(7) - FBig(0.5) if False else True + assert float(FBig(1.5)) == 1.5 + assert FBig(7).to_int() == IBig(7) + # DBig (base 10) from Decimal / string / float + from decimal import Decimal + + assert float(DBig(Decimal("1.5"))) == 1.5 + assert float(DBig("1.25")) == 1.25 + + +def test_arithmetic(): + a, b = FBig(1.5), FBig(2.0) + assert a + b == FBig(3.5) + assert a - b == FBig(-0.5) + assert a * b == FBig(3.0) + assert a / b == FBig(0.75) + assert -a == FBig(-1.5) + assert abs(FBig(-1.5)) == FBig(1.5) + # mixed with int + assert FBig(1.5) + 1 == FBig(2.5) + + +def test_compare_bool(): + assert FBig(1.5) < FBig(2.0) + assert FBig(1.5) <= FBig(1.5) + assert FBig(2.0) > FBig(1.5) + assert FBig(1.5) != FBig(2.0) + assert FBig(1.5) == FBig(1.5) + assert bool(FBig(0.0)) is False + assert bool(FBig(0.5)) is True + + +def test_predicates_rounding(): + f = FBig(1.5) + assert f.is_finite() and not f.is_infinite() + assert FBig(0.0).is_zero() + assert f.floor().to_int() == IBig(1) + assert f.ceil().to_int() == IBig(2) + assert f.round().to_int() == IBig(2) + assert f.trunc().to_int() == IBig(1) + + +def test_transcendentals_panicfree(): + assert abs(float(FBig(2.0).ln()) - math.log(2)) < 1e-9 + assert abs(float(FBig(1.0).exp()) - math.e) < 1e-9 + assert FBig(4.0).sqrt() == FBig(2.0) + assert abs(float(FBig(1.0).sin()) - math.sin(1)) < 1e-9 + assert abs(float(FBig(0.0).sin())) < 1e-9 + # domain error -> ValueError (not a session-crashing panic) + try: + FBig(-1.0).sqrt() + raise AssertionError("expected ValueError") + except ValueError: + pass + try: + FBig(2.0).acos() + raise AssertionError("expected ValueError") + except ValueError: + pass + + +def test_cross_conversions(): + f = FBig(0.5) + assert float(f.to_decimal()) == 0.5 + assert f.to_binary() == f + # exact float -> rational + assert f.to_rational() == RBig.from_parts(IBig(1), UBig(2)) + # rational -> float (lossy, with precision) + assert RBig.from_parts(IBig(1), UBig(2)).to_float(53) == FBig(0.5) + + +def test_dbig_arithmetic(): + assert float(DBig("1.5") + DBig("1.5")) == 3.0 + assert float(DBig("3.0") * DBig("2.0")) == 6.0 + assert DBig("1.5") < DBig("2.5") + + +if __name__ == "__main__": + for name, fn in sorted(globals().items()): + if name.startswith("test_"): + fn() + print("float ops tests passed") diff --git a/python/tests/test_int_math.py b/python/tests/test_int_math.py new file mode 100644 index 00000000..31cf47b2 --- /dev/null +++ b/python/tests/test_int_math.py @@ -0,0 +1,80 @@ +from dashu import * + + +def test_roots(): + assert UBig(144).sqrt() == UBig(12) + assert UBig(27).cbrt() == UBig(3) + assert UBig(1024).nth_root(5) == UBig(4) + assert UBig(12).sqr() == UBig(144) + assert UBig(3).cubic() == UBig(27) + # IBig: cbrt / odd nth_root are sign-preserving + assert IBig(-27).cbrt() == IBig(-3) + assert IBig(-1024).nth_root(5) == IBig(-4) + assert IBig(144).sqrt() == UBig(12) + # even root of negative -> ValueError, not a panic + try: + IBig(-4).nth_root(2) + raise AssertionError("expected ValueError") + except ValueError: + pass + + +def test_number_theory(): + assert UBig(12).gcd(UBig(8)) == UBig(4) + g, x, y = UBig(12).gcd_ext(UBig(8)) + assert g == UBig(4) + assert UBig(12) * x + UBig(8) * y == g + assert UBig(6).is_multiple_of(UBig(3)) + assert UBig(2).remove(UBig(2)) == 1 # 2 = 2^1 + assert UBig(2).is_power_of_two() + assert UBig(5).next_power_of_two() == UBig(8) + + +def test_bit_ops(): + n = UBig(0b10110) # 22 + assert n.count_ones() == 3 + assert n.trailing_zeros() == 1 + assert n.trailing_ones() == 0 + assert UBig(0b1000).trailing_zeros() == 3 + + +def test_divmod_floordiv(): + assert int(UBig(17) // UBig(5)) == 3 + q, r = divmod(UBig(17), UBig(5)) + assert int(q) == 3 and int(r) == 2 + assert int(UBig(20) // 3) == 6 + # IBig floor division + assert int(IBig(-17) // IBig(5)) == -4 + + +def test_inplace_ops(): + n = UBig(10) + n += UBig(5) + assert int(n) == 15 + n -= UBig(3) + assert int(n) == 12 + n *= UBig(2) + assert int(n) == 24 + n <<= 1 + assert int(n) == 48 + n >>= 2 + assert int(n) == 12 + n &= UBig(0b100) # 12 & 4 + assert int(n) == 4 + + +def test_chunks_words(): + n = UBig("0x123456789abcdef") + assert UBig.from_chunks(n.to_chunks(10), 10) == n + w = n.to_words() + assert UBig.from_words(w) == n + + +if __name__ == "__main__": + test_roots() + test_number_theory() + test_bit_ops() + test_divmod_floordiv() + test_inplace_ops() + test_chunks_words() + print("int math tests passed") diff --git a/python/tests/test_math.py b/python/tests/test_math.py new file mode 100644 index 00000000..f1148c98 --- /dev/null +++ b/python/tests/test_math.py @@ -0,0 +1,47 @@ +import math + +import dashu +from dashu import * + + +def test_trig_hyper_exp_log(): + assert abs(float(dashu.sin(FBig(0.0)))) < 1e-9 + assert abs(float(dashu.cos(FBig(0.0))) - 1.0) < 1e-9 + assert abs(float(dashu.exp(FBig(1.0))) - math.e) < 1e-9 + assert abs(float(dashu.log(FBig(2.0))) - math.log(2)) < 1e-9 + assert abs(float(dashu.sinh(FBig(1.0))) - math.sinh(1)) < 1e-9 + + +def test_roots_power(): + assert dashu.sqrt(FBig(9.0)) == FBig(3.0) + assert dashu.cbrt(FBig(27.0)) == FBig(3.0) + assert dashu.nth_root(FBig(16.0), 4) == FBig(2.0) + # powi (integer power) is exact; powf routes through exp/log and rounds + assert dashu.powi(FBig(2.0), IBig(10)) == FBig(1024.0) + assert abs(float(dashu.powf(FBig(2.0), FBig(10.0))) - 1024.0) < 1e-6 + assert float(dashu.hypot(FBig(3.0), FBig(4.0))) == 5.0 + + +def test_integer_number_theory(): + assert dashu.gcd(UBig(12), UBig(8)) == UBig(4) + g, x, y = dashu.gcd_ext(UBig(12), UBig(8)) + assert g == UBig(4) and UBig(12) * x + UBig(8) * y == g + assert dashu.lcm(UBig(4), UBig(6)) == UBig(12) + + +def test_complex_module(): + z = CBig(FBig(3.0), FBig(4.0)) + assert z.abs() == FBig(5.0) + assert z.conj().imag() == FBig(-4.0) + assert z.norm() == FBig(25.0) + # complex arithmetic + transcendentals + assert CBig(FBig(1.0), FBig(0.0)) + CBig(FBig(2.0), FBig(0.0)) == CBig(FBig(3.0), FBig(0.0)) + assert abs(float(z.exp().real()) - math.exp(3) * math.cos(4)) < 1e-6 + assert CBig(FBig(0.0), FBig(0.0)).exp() == CBig(FBig(1.0), FBig(0.0)) + + +if __name__ == "__main__": + for name, fn in sorted(globals().items()): + if name.startswith("test_"): + fn() + print("math module tests passed") diff --git a/python/tests/test_ratio_ops.py b/python/tests/test_ratio_ops.py new file mode 100644 index 00000000..02fda99c --- /dev/null +++ b/python/tests/test_ratio_ops.py @@ -0,0 +1,59 @@ +from fractions import Fraction + +from dashu import * + + +def test_construct(): + assert RBig(Fraction(1, 3)) == RBig.from_parts(IBig(1), UBig(3)) + assert RBig(0.5) == RBig.from_parts(IBig(1), UBig(2)) + assert RBig("1/3") == RBig.from_parts(IBig(1), UBig(3)) + assert RBig(7) == RBig.from_parts(IBig(7), UBig(1)) + + +def test_arithmetic(): + a = RBig.from_parts(IBig(1), UBig(3)) + b = RBig.from_parts(IBig(2), UBig(3)) + assert a + b == RBig.from_parts(IBig(1), UBig(1)) + assert b - a == RBig.from_parts(IBig(1), UBig(3)) + assert a * b == RBig.from_parts(IBig(2), UBig(9)) + assert (a / b) == RBig.from_parts(IBig(1), UBig(2)) + assert -a == RBig.from_parts(IBig(-1), UBig(3)) + # mixed with int + assert a + 1 == RBig.from_parts(IBig(4), UBig(3)) + + +def test_compare_bool(): + a = RBig.from_parts(IBig(1), UBig(2)) + assert a < RBig.from_parts(IBig(2), UBig(3)) + assert a == RBig.from_parts(IBig(2), UBig(4)) + assert bool(RBig.from_parts(IBig(0), UBig(1))) is False + assert bool(RBig.from_parts(IBig(1), UBig(3))) is True + + +def test_properties_rounding(): + r = RBig.from_parts(IBig(7), UBig(3)) + assert r.numerator == IBig(7) and r.denominator == UBig(3) + assert r.trunc() == IBig(2) + assert r.floor() == IBig(2) + assert r.ceil() == IBig(3) + assert r.fract() == RBig.from_parts(IBig(1), UBig(3)) + assert r.is_int() is False + assert RBig.from_parts(IBig(6), UBig(3)).is_int() + + +def test_powers(): + r = RBig.from_parts(IBig(2), UBig(3)) + assert r.sqr() == RBig.from_parts(IBig(4), UBig(9)) + assert r.pow(3) == RBig.from_parts(IBig(8), UBig(27)) + + +def test_float_conversion(): + r = RBig.from_parts(IBig(1), UBig(2)) + assert r.to_float(53) == FBig(0.5) + + +if __name__ == "__main__": + for name, fn in sorted(globals().items()): + if name.startswith("test_"): + fn() + print("ratio ops tests passed") From 17aff47ccbd0d0bd153ca20ed9e4b62ca546a785 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 9 Jul 2026 00:36:14 +0800 Subject: [PATCH 13/20] Accept plain int in powi/from_parts/ilog via UniInput powi/from_parts/ilog took dashu-pyclass references (e.g. &IPy), so passing a plain Python int raised "TypeError: 'int' object is not an instance of 'IBig'". Switch them to UniInput<'_> and add UniInput::into_ibig (mirroring into_ubig), so FBig(12).powi(300), RBig.from_parts(1, 3), UBig(256).ilog(2), etc. work. Updated the stub and added regression tests. Co-Authored-By: Claude --- python/CHANGELOG.md | 2 ++ python/dashu.pyi | 20 +++++++++--------- python/src/complex.rs | 7 +++--- python/src/convert.rs | 13 ++++++++++++ python/src/float.rs | 18 +++++++++------- python/src/int.rs | 18 +++++++++------- python/src/math.rs | 5 +++-- python/src/ratio.rs | 6 ++++-- .../test_convert.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 2450 bytes ...est_float_ops.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 46996 bytes .../test_int.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 16301 bytes ...test_int_math.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 39108 bytes .../test_math.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 43086 bytes ...est_ratio_ops.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 41478 bytes .../test_words.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 8489 bytes python/tests/test_int_math.py | 3 +++ python/tests/test_math.py | 4 +++- 17 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 python/tests/__pycache__/test_convert.cpython-312-pytest-9.1.1.pyc create mode 100644 python/tests/__pycache__/test_float_ops.cpython-312-pytest-9.1.1.pyc create mode 100644 python/tests/__pycache__/test_int.cpython-312-pytest-9.1.1.pyc create mode 100644 python/tests/__pycache__/test_int_math.cpython-312-pytest-9.1.1.pyc create mode 100644 python/tests/__pycache__/test_math.cpython-312-pytest-9.1.1.pyc create mode 100644 python/tests/__pycache__/test_ratio_ops.cpython-312-pytest-9.1.1.pyc create mode 100644 python/tests/__pycache__/test_words.cpython-312-pytest-9.1.1.pyc diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 7e6d173d..42e1075b 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -16,6 +16,8 @@ `to_int`, `numerator`/`denominator`, `split_at_point`, `sqr`/`cubic`/`pow`. - Cross-type conversions: `FBig.to_decimal`/`to_binary`/`to_rational`, `RBig.to_float`/`to_decimal`. +- `powi`, `from_parts`, and `ilog` now accept a plain Python `int` (or a dashu + integer) via the `UniInput` dispatch, so e.g. `FBig(12).powi(300)` works. - Broadened constructors: `FBig`/`DBig`/`RBig`/`CBig` now accept any Python number (int/float/`Decimal`/`Fraction`) in addition to strings. - A module-level `math` API (`sin`/`cos`/…/`exp`/`ln`/`sqrt`/`gcd`/`lcm`/…) and a diff --git a/python/dashu.pyi b/python/dashu.pyi index 04720253..93e20f4e 100644 --- a/python/dashu.pyi +++ b/python/dashu.pyi @@ -84,7 +84,7 @@ class UBig: def nth_root(self, n: int) -> UBig: ... def sqr(self) -> UBig: ... def cubic(self) -> UBig: ... - def ilog(self, base: UBig) -> int: ... + def ilog(self, base: int | UBig) -> int: ... def is_multiple_of(self, divisor: UBig) -> bool: ... def remove(self, factor: UBig) -> int: ... def gcd(self, other: UBig) -> UBig: ... @@ -166,7 +166,7 @@ class IBig: def nth_root(self, n: int) -> IBig: ... def sqr(self) -> UBig: ... def cubic(self) -> IBig: ... - def ilog(self, base: UBig) -> int: ... + def ilog(self, base: int | UBig) -> int: ... def trailing_zeros(self) -> Optional[int]: ... def trailing_ones(self) -> Optional[int]: ... def is_one(self) -> bool: ... @@ -176,7 +176,7 @@ class IBig: def is_positive(self) -> bool: ... def to_parts(self) -> tuple[Sign, UBig]: ... @staticmethod - def from_parts(sign: Sign, magnitude: UBig) -> IBig: ... + def from_parts(sign: Sign, magnitude: int | UBig) -> IBig: ... def as_ubig(self) -> Optional[UBig]: ... @staticmethod def ones(n: int) -> IBig: ... @@ -242,7 +242,7 @@ class FBig: def digits(self) -> int: ... def with_precision(self, precision: int) -> FBig: ... @staticmethod - def from_parts(significand: IBig, exponent: int) -> FBig: ... + def from_parts(significand: int | IBig, exponent: int) -> FBig: ... # conversions def to_int(self) -> IBig: ... def to_decimal(self) -> DBig: ... @@ -270,7 +270,7 @@ class FBig: def cbrt(self) -> FBig: ... def nth_root(self, n: int) -> FBig: ... def powf(self, w: FBig) -> FBig: ... - def powi(self, n: IBig) -> FBig: ... + def powi(self, n: int | IBig) -> FBig: ... class DBig: @@ -315,7 +315,7 @@ class DBig: def digits(self) -> int: ... def with_precision(self, precision: int) -> DBig: ... @staticmethod - def from_parts(significand: IBig, exponent: int) -> DBig: ... + def from_parts(significand: int | IBig, exponent: int) -> DBig: ... def to_int(self) -> IBig: ... def to_decimal(self) -> DBig: ... def to_binary(self) -> FBig: ... @@ -342,7 +342,7 @@ class DBig: def cbrt(self) -> DBig: ... def nth_root(self, n: int) -> DBig: ... def powf(self, w: DBig) -> DBig: ... - def powi(self, n: IBig) -> DBig: ... + def powi(self, n: int | IBig) -> DBig: ... class RBig: @@ -395,7 +395,7 @@ class RBig: def to_float(self, precision: int) -> FBig: ... def to_decimal(self, precision: int) -> DBig: ... @staticmethod - def from_parts(numerator: IBig, denominator: UBig) -> RBig: ... + def from_parts(numerator: int | IBig, denominator: int | UBig) -> RBig: ... @staticmethod def simplest_from_float(f: FBig) -> Optional[RBig]: ... @@ -441,7 +441,7 @@ class CBig: def exp(self) -> CBig: ... def ln(self) -> CBig: ... def sqrt(self) -> CBig: ... - def powi(self, n: IBig) -> CBig: ... + def powi(self, n: int | IBig) -> CBig: ... def powf(self, w: CBig) -> CBig: ... @@ -481,7 +481,7 @@ def sqrt(x: FBig) -> FBig: ... def cbrt(x: FBig) -> FBig: ... def nth_root(x: FBig, n: int) -> FBig: ... def powf(x: FBig, y: FBig) -> FBig: ... -def powi(x: FBig, n: IBig) -> FBig: ... +def powi(x: FBig, n: int | IBig) -> FBig: ... def hypot(x: FBig, y: FBig) -> FBig: ... def gcd(a: UBig, b: UBig) -> UBig: ... def gcd_ext(a: UBig, b: UBig) -> tuple[UBig, IBig, IBig]: ... diff --git a/python/src/complex.rs b/python/src/complex.rs index 084ebf11..4b634eae 100644 --- a/python/src/complex.rs +++ b/python/src/complex.rs @@ -23,7 +23,7 @@ use pyo3::{ use crate::{ cache::{unwrap_complex, unwrap_float, with_cache}, convert::{conversion_error_to_py, parse_error_to_py, parse_to_ibig, parse_to_long}, - types::{CPy, DPy, FPy}, + types::{CPy, DPy, FPy, UniInput}, }; /// Promote any real Python number (or a `(re, im)` pair / Python `complex`) to a bare `CBig`. @@ -272,9 +272,10 @@ impl CPy { let ctx = self.0.context(); Ok(Self(unwrap_complex(ctx.sqrt(&self.0), ctx)?)) } - fn powi(&self, n: &crate::types::IPy) -> PyResult { + fn powi(&self, n: UniInput<'_>) -> PyResult { + let n = n.into_ibig()?; let ctx = self.0.context(); - Ok(Self(unwrap_complex(ctx.powi(&self.0, n.0.clone()), ctx)?)) + Ok(Self(unwrap_complex(ctx.powi(&self.0, n), ctx)?)) } fn powf(&self, w: &Self) -> PyResult { let ctx = self.0.context(); diff --git a/python/src/convert.rs b/python/src/convert.rs index 1d830dca..75f1a253 100644 --- a/python/src/convert.rs +++ b/python/src/convert.rs @@ -227,6 +227,19 @@ impl<'a> UniInput<'a> { } } + /// Strict conversion of any integer input (Python `int` or `UBig`/`IBig`) to an `IBig`. + /// Non-integer inputs (float/Decimal/Fraction) are rejected. + pub fn into_ibig(self) -> PyResult { + match self { + Self::Uint(x) => Ok(x.into()), + Self::Int(x) => Ok(IBig::from(x)), + Self::BUint(x) => Ok(x.0.clone().into()), + Self::BInt(x) => Ok(x.0.clone()), + Self::OBInt(x) => Ok(x), + _ => Err(PyTypeError::new_err("the input is not an integer")), + } + } + /// Strict conversion of any numeric input to a binary float (`FPy`). /// Decimals are rejected (convert explicitly). pub fn into_fpy(self) -> PyResult { diff --git a/python/src/float.rs b/python/src/float.rs index dcce2d64..0fe0b687 100644 --- a/python/src/float.rs +++ b/python/src/float.rs @@ -228,8 +228,8 @@ impl FPy { FPy(self.0.clone().with_precision(precision).value()) } #[staticmethod] - fn from_parts(significand: &IPy, exponent: isize) -> Self { - FPy(FBig::from_parts(significand.0.clone(), exponent)) + fn from_parts(significand: UniInput<'_>, exponent: isize) -> PyResult { + Ok(FPy(FBig::from_parts(significand.into_ibig()?, exponent))) } /********** conversions **********/ @@ -346,9 +346,10 @@ impl FPy { let res = with_cache(|c| ctx.powf(self.0.repr(), w.0.repr(), Some(c))); Ok(Self(unwrap_float(res, ctx)?)) } - fn powi(&self, n: &IPy) -> PyResult { + fn powi(&self, n: UniInput<'_>) -> PyResult { + let n = n.into_ibig()?; let ctx = self.0.context(); - Ok(Self(unwrap_float(ctx.powi(self.0.repr(), n.0.clone()), ctx)?)) + Ok(Self(unwrap_float(ctx.powi(self.0.repr(), n), ctx)?)) } fn atan2(&self, x: &Self) -> PyResult { let ctx = self.0.context(); @@ -509,8 +510,8 @@ impl DPy { DPy(self.0.clone().with_precision(precision).value()) } #[staticmethod] - fn from_parts(significand: &IPy, exponent: isize) -> Self { - DPy(DBig::from_parts(significand.0.clone(), exponent)) + fn from_parts(significand: UniInput<'_>, exponent: isize) -> PyResult { + Ok(DPy(DBig::from_parts(significand.into_ibig()?, exponent))) } /********** conversions **********/ @@ -627,9 +628,10 @@ impl DPy { let res = with_cache(|c| ctx.powf(self.0.repr(), w.0.repr(), Some(c))); Ok(Self(unwrap_float(res, ctx)?)) } - fn powi(&self, n: &IPy) -> PyResult { + fn powi(&self, n: UniInput<'_>) -> PyResult { + let n = n.into_ibig()?; let ctx = self.0.context(); - Ok(Self(unwrap_float(ctx.powi(self.0.repr(), n.0.clone()), ctx)?)) + Ok(Self(unwrap_float(ctx.powi(self.0.repr(), n), ctx)?)) } fn atan2(&self, x: &Self) -> PyResult { let ctx = self.0.context(); diff --git a/python/src/int.rs b/python/src/int.rs index 1a972005..6557134e 100644 --- a/python/src/int.rs +++ b/python/src/int.rs @@ -559,11 +559,12 @@ impl UPy { fn cubic(&self) -> Self { UPy(self.0.cubic()) } - fn ilog(&self, base: &Self) -> PyResult { - if base.0 <= UBig::ONE { + fn ilog(&self, base: UniInput<'_>) -> PyResult { + let base = base.into_ubig()?; + if base <= UBig::ONE { return Err(PyValueError::new_err("base must be greater than 1")); } - Ok(self.0.ilog(&base.0)) + Ok(self.0.ilog(&base)) } fn is_multiple_of(&self, divisor: &Self) -> bool { self.0.is_multiple_of(&divisor.0) @@ -938,11 +939,12 @@ impl IPy { fn cubic(&self) -> Self { IPy(self.0.cubic()) } - fn ilog(&self, base: &UPy) -> PyResult { - if base.0 <= UBig::ONE { + fn ilog(&self, base: UniInput<'_>) -> PyResult { + let base = base.into_ubig()?; + if base <= UBig::ONE { return Err(PyValueError::new_err("base must be greater than 1")); } - Ok(self.0.ilog(&base.0)) + Ok(self.0.ilog(&base)) } fn trailing_zeros(&self) -> Option { self.0.trailing_zeros() @@ -972,8 +974,8 @@ impl IPy { (sign.into(), UPy(mag)) } #[staticmethod] - fn from_parts(sign: PySign, magnitude: &UPy) -> Self { - IPy(IBig::from_parts(sign.into(), magnitude.0.clone())) + fn from_parts(sign: PySign, magnitude: UniInput<'_>) -> PyResult { + Ok(IPy(IBig::from_parts(sign.into(), magnitude.into_ubig()?))) } fn as_ubig(&self) -> Option { self.0.as_ubig().cloned().map(UPy) diff --git a/python/src/math.rs b/python/src/math.rs index 36e74509..8293b5af 100644 --- a/python/src/math.rs +++ b/python/src/math.rs @@ -150,9 +150,10 @@ pub fn powf(x: &FPy, y: &FPy) -> PyResult { Ok(FPy(unwrap_float(res, ctx)?)) } #[pyfunction] -pub fn powi(x: &FPy, n: &IPy) -> PyResult { +pub fn powi(x: &FPy, n: crate::types::UniInput<'_>) -> PyResult { + let n = n.into_ibig()?; let ctx = x.0.context(); - Ok(FPy(unwrap_float(ctx.powi(x.0.repr(), n.0.clone()), ctx)?)) + Ok(FPy(unwrap_float(ctx.powi(x.0.repr(), n), ctx)?)) } #[pyfunction] pub fn hypot(x: &FPy, y: &FPy) -> PyResult { diff --git a/python/src/ratio.rs b/python/src/ratio.rs index 3dec2ed6..ab17e5d7 100644 --- a/python/src/ratio.rs +++ b/python/src/ratio.rs @@ -228,8 +228,10 @@ impl RPy { DPy(self.0.to_float::(precision).value()) } #[staticmethod] - fn from_parts(numerator: &IPy, denominator: &UPy) -> Self { - RPy(RBig::from_parts(numerator.0.clone(), denominator.0.clone())) + fn from_parts(numerator: UniInput<'_>, denominator: UniInput<'_>) -> PyResult { + let num = numerator.into_ibig()?; + let den = denominator.into_ubig()?; + Ok(RPy(RBig::from_parts(num, den))) } /// Find the simplest rational within the error bounds of the given float. #[staticmethod] diff --git a/python/tests/__pycache__/test_convert.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_convert.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb49a23e5ab43c15197b90b3087e360d7e2a99a5 GIT binary patch literal 2450 zcmb^zU27aw^xWCm+1Zb5J{yxLG?8jERkGchv=NLNiKL1I1O*8#!({JlvdQesdS{Y$ z;|>Pt!$yOwR4`zLAbqF=-~0u>`e0I18!rVP^d)c6^eG_q-kF`*jSaRBy*qQy=RF_y zoH^`IEiDOP>*pU|n>Pi3KR6LAMtkevA_HFm28;}tMjjN3k(_{_C{9Namk5URJ)?2t z8cfG@8H)>p07Mo*k8Re?ASk0UQlfjDo>CA+XL5Cs2Y4V6_8yc(&mD3?X5h~&Y z!2y$rM`Y1v*=*DWCmI;S(kAlp3Ap?sfQxVp%N4XN1Mt@sDli`lv>UJ@R0NC`F0quP zhA;Dor@%6TwILBIFab4mc^lvoi_{g|h-ZP}B<1idy;Tu{Q?6)sJN4KA8vNsQ^MWfn z@ndX6on*+3hL)v@REh9%xUovChU^umCD58r2Ur8*cqNucIKfZlg-uoqzHriM_c4I^ zjsR=GpvX&L_cqz@YG?$Q<24+of(S|TiS9tJ0UthLBAmpjCl(Q_yuThhqK8hfh*G%a zu_HS1|98}=KWWxH=589AEP|FMtH^>?(4j?3G;hJ*@4BPzI%Aitv%L$G^e7 z2W%P4D?Io(_?-mfkh7Z@dt77hAI99{U!#H!g|RUOEl)i+SSp`a?8HZPq<^fRV`mk6 zki+3X#E`luTNj;EY$f5cr+>^pGpg8Bbjl^2irS)MQM5#nqewKu%<(PDw{v_O=G%F` zjV`xpwyhJ#^O|LDsmL_XDrqXd|3+b!if{2fN;Gp;r;=7G=_V#AmIC4PgC<@tl^xx7 zRE^MfHK!SdO7s%RS;dk@bedvfb!M?(I0e(Dl3_6ooA#*ooV93RPgR{YtQpNvO|7WY zxN2*8om-&kaWAfdWxh#>MQEp*w@6WQRQ+nn&`ix?5+y+sp7)$)>o!FN!p9ZilZqmh zCb@S%y_cGak!Iq8Px!`P_9OO*Y}V%Ur$1bW7c+BKQP0e4IcsJ)gEf0@F*B4iv_-6E zu2^KjA-Zm7*p19tW&n76cE*p=GMCs@50%Qa%VQKwN6mRmn`L8@7nnOf@a;L)S}U+G zK_a;}QH}4$6Fx|;y;n`_#!uet+LE8&jrZR?wI!e4Pf8uq%G>ulAl+HZe*Qrf?Y6Ym zvNi3C;njB-A$Q!Bd$;A@_3VarEBkH!8~nqmt-cF)RE5UKDi|itU0yG&s>I7-3_$eI&fF! z&#yl16XCwi)qU!h1{#WQ%L89dgyH~g7O%I5;sD)|2Wpc(mz}S=p}LQ-z&22LC_w+C z^jDIle!wmqsI00*tzfFE5~p&|vE&&p>Q((7AM#s?RceUnS4hEO{Ti}ptj;5Rve2m4 z99}`WY2Mtb`W+DVM*_$h&*3XY3ojb_t7L?ce3aOy*|r}+2>lLl4R)aOZ}9|rc7F)O WRCW1wr1v+m=Z@I79|5t?GxRTOMGu(( literal 0 HcmV?d00001 diff --git a/python/tests/__pycache__/test_float_ops.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_float_ops.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..11108e59038b34c437ba61235c02950dfbdf19c1 GIT binary patch literal 46996 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 z zlT`y9t{-6)LTGoDW2l(9FIKPkSgj2tinlxf#an8oSgLAkWQFEZS%nq9;{QE{aSBYS zi^e5azNu4ZG_IIdFwewdlMao=&i4?!@}LT}H-OY{as3x5&$t?GkRYa2W9qYEiZV@` zt0E(nS{!Diy2guZx!Qyy8FLUEW%Rv?a@fAS|KwD8;~7oPhv&mndrk$LX= z6#!U)zLK;0?wRyi=5ZcTVUCb_EboF#W*;pz6WSu7sp1@9_uwFqZeXpQN&+eK90!4v zzQVzhkkPwE(z_+|@`AZ#&dj`gGYnwnP$rzSw%jx6v&`c>qQaaBWFE`A;F7t8=q0p8 zLQ};#!0y38AkCQ^1X2dN0fCeWbFd^_<1g{&5%YI7$pdM%w%st(FJFhmZi66)Nu9MV z6F~qiR%F52cF&~GGLQ3!3Uh?RV|f=;*^5-S%9h}gxs9|~R4WRkA*lkKffh@E`pufRF&(WXl!KqCma6S-Y$G87-pn=Nn?VX&`YjV9X5@rlj|wu7KK^A z-|0bV58YELfYJd~8e4od!fhZ~FIEbkwc!j{^Q@}N-H2ww>nð~O+otz_2(*`t(P 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 OICA%CL%`U~lJWmgfOA9u literal 0 HcmV?d00001 diff --git a/python/tests/__pycache__/test_math.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_math.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfc1bd9790b5af15a11370e3c270d7e2f3dae133 GIT binary patch literal 43086 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 literal 0 HcmV?d00001 diff --git a/python/tests/__pycache__/test_ratio_ops.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_ratio_ops.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0207f2b625e8376f546ca3cf71f8cfdf873de634 GIT binary patch literal 41478 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®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 literal 0 HcmV?d00001 diff --git a/python/tests/__pycache__/test_words.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_words.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8beb9fffce1a33976e770374d0303b99053892c3 GIT binary patch literal 8489 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 literal 0 HcmV?d00001 diff --git a/python/tests/test_int_math.py b/python/tests/test_int_math.py index 31cf47b2..acadacc3 100644 --- a/python/tests/test_int_math.py +++ b/python/tests/test_int_math.py @@ -7,6 +7,9 @@ def test_roots(): assert UBig(1024).nth_root(5) == UBig(4) assert UBig(12).sqr() == UBig(144) assert UBig(3).cubic() == UBig(27) + # ilog accepts a plain int as well as a UBig + assert UBig(256).ilog(2) == 8 + assert IBig(1000).ilog(UBig(10)) == 3 # IBig: cbrt / odd nth_root are sign-preserving assert IBig(-27).cbrt() == IBig(-3) assert IBig(-1024).nth_root(5) == IBig(-4) diff --git a/python/tests/test_math.py b/python/tests/test_math.py index f1148c98..be50bd3a 100644 --- a/python/tests/test_math.py +++ b/python/tests/test_math.py @@ -16,7 +16,9 @@ def test_roots_power(): assert dashu.sqrt(FBig(9.0)) == FBig(3.0) assert dashu.cbrt(FBig(27.0)) == FBig(3.0) assert dashu.nth_root(FBig(16.0), 4) == FBig(2.0) - # powi (integer power) is exact; powf routes through exp/log and rounds + # powi (integer power) is exact; powf routes through exp/log and rounds. + # powi accepts a plain Python int as well as an IBig. + assert dashu.powi(FBig(2.0), 10) == FBig(1024.0) assert dashu.powi(FBig(2.0), IBig(10)) == FBig(1024.0) assert abs(float(dashu.powf(FBig(2.0), FBig(10.0))) - 1024.0) < 1e-6 assert float(dashu.hypot(FBig(3.0), FBig(4.0))) == 5.0 From 9ce999f9df14180e5d2ac090fd1d607b8a10fa08 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 9 Jul 2026 00:55:11 +0800 Subject: [PATCH 14/20] Accept native Python numbers in all math APIs; rename ratio.rs -> rational.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All numeric function/method arguments now go through UniInput so a plain Python int/float works directly — dashu.sqrt(9.0), FBig(2).powi(300), UBig(12).gcd(8), UBig(n) += 5, RBig.from_parts(1, 3), etc. (into_fpy now builds floats at f64's native precision so transcendentals are well-defined). Rewrote the test suite to use native Python types throughout. Renamed python/src/ratio.rs to rational.rs. Also untracked the tests/__pycache__ artifacts and added a .gitignore. Co-Authored-By: Claude --- python/.gitignore | 2 + python/CHANGELOG.md | 7 +- python/dashu.pyi | 98 ++++---- python/src/convert.rs | 5 +- python/src/float.rs | 12 +- python/src/int.rs | 78 +++--- python/src/lib.rs | 2 +- python/src/math.rs | 229 +++++++----------- python/src/{ratio.rs => rational.rs} | 5 +- .../test_convert.cpython-312-pytest-9.1.1.pyc | Bin 2450 -> 0 bytes ...est_float_ops.cpython-312-pytest-9.1.1.pyc | Bin 46996 -> 0 bytes .../test_int.cpython-312-pytest-9.1.1.pyc | Bin 16301 -> 0 bytes ...test_int_math.cpython-312-pytest-9.1.1.pyc | Bin 39108 -> 0 bytes .../test_math.cpython-312-pytest-9.1.1.pyc | Bin 43086 -> 0 bytes ...est_ratio_ops.cpython-312-pytest-9.1.1.pyc | Bin 41478 -> 0 bytes .../test_words.cpython-312-pytest-9.1.1.pyc | Bin 8489 -> 0 bytes python/tests/test_float_ops.py | 52 ++-- python/tests/test_int_math.py | 72 +++--- python/tests/test_math.py | 45 ++-- python/tests/test_ratio_ops.py | 60 +++-- 20 files changed, 312 insertions(+), 355 deletions(-) create mode 100644 python/.gitignore rename python/src/{ratio.rs => rational.rs} (98%) delete mode 100644 python/tests/__pycache__/test_convert.cpython-312-pytest-9.1.1.pyc delete mode 100644 python/tests/__pycache__/test_float_ops.cpython-312-pytest-9.1.1.pyc delete mode 100644 python/tests/__pycache__/test_int.cpython-312-pytest-9.1.1.pyc delete mode 100644 python/tests/__pycache__/test_int_math.cpython-312-pytest-9.1.1.pyc delete mode 100644 python/tests/__pycache__/test_math.cpython-312-pytest-9.1.1.pyc delete mode 100644 python/tests/__pycache__/test_ratio_ops.cpython-312-pytest-9.1.1.pyc delete mode 100644 python/tests/__pycache__/test_words.cpython-312-pytest-9.1.1.pyc diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 00000000..7a60b85e --- /dev/null +++ b/python/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 42e1075b..f8970579 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -16,8 +16,11 @@ `to_int`, `numerator`/`denominator`, `split_at_point`, `sqr`/`cubic`/`pow`. - Cross-type conversions: `FBig.to_decimal`/`to_binary`/`to_rational`, `RBig.to_float`/`to_decimal`. -- `powi`, `from_parts`, and `ilog` now accept a plain Python `int` (or a dashu - integer) via the `UniInput` dispatch, so e.g. `FBig(12).powi(300)` works. +- All numeric function/method arguments now accept plain Python numbers via the + `UniInput` dispatch — e.g. `dashu.sqrt(9.0)`, `FBig(2).powi(300)`, `UBig(12).gcd(8)`, + `UBig(n) += 5`, `RBig.from_parts(1, 3)`. The module-level `math` functions, `powf`, + `atan2`, `gcd`/`gcd_ext`/`lcm`, `is_multiple_of`/`remove`, in-place ops, `powi`, + `from_parts`, `ilog`, and `simplest_from_float` all take native int/float. - Broadened constructors: `FBig`/`DBig`/`RBig`/`CBig` now accept any Python number (int/float/`Decimal`/`Fraction`) in addition to strings. - A module-level `math` API (`sin`/`cos`/…/`exp`/`ln`/`sqrt`/`gcd`/`lcm`/…) and a diff --git a/python/dashu.pyi b/python/dashu.pyi index 93e20f4e..5e498ebe 100644 --- a/python/dashu.pyi +++ b/python/dashu.pyi @@ -54,12 +54,12 @@ class UBig: def __mod__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... def __divmod__(self, other: int | UBig | IBig) -> tuple: ... def __pow__(self, exp: int | UBig, modulus: Optional[int | UBig] = ...) -> UBig: ... - def __iadd__(self, other: UBig) -> None: ... - def __isub__(self, other: UBig) -> None: ... - def __imul__(self, other: UBig) -> None: ... - def __iand__(self, other: UBig) -> None: ... - def __ior__(self, other: UBig) -> None: ... - def __ixor__(self, other: UBig) -> None: ... + def __iadd__(self, other: int | UBig) -> None: ... + def __isub__(self, other: int | UBig) -> None: ... + def __imul__(self, other: int | UBig) -> None: ... + def __iand__(self, other: int | UBig) -> None: ... + def __ior__(self, other: int | UBig) -> None: ... + def __ixor__(self, other: int | UBig) -> None: ... def __ilshift__(self, other: int) -> None: ... def __irshift__(self, other: int) -> None: ... def __pos__(self) -> UBig: ... @@ -85,10 +85,10 @@ class UBig: def sqr(self) -> UBig: ... def cubic(self) -> UBig: ... def ilog(self, base: int | UBig) -> int: ... - def is_multiple_of(self, divisor: UBig) -> bool: ... - def remove(self, factor: UBig) -> int: ... - def gcd(self, other: UBig) -> UBig: ... - def gcd_ext(self, other: UBig) -> tuple[UBig, IBig, IBig]: ... + def is_multiple_of(self, divisor: int | UBig) -> bool: ... + def remove(self, factor: int | UBig) -> int: ... + def gcd(self, other: int | UBig) -> UBig: ... + def gcd_ext(self, other: int | UBig) -> tuple[UBig, IBig, IBig]: ... # bit operations def count_ones(self) -> int: ... def count_zeros(self) -> Optional[int]: ... @@ -137,12 +137,12 @@ class IBig: def __mod__(self, other: Number) -> "UBig|IBig|FBig|DBig|RBig": ... def __divmod__(self, other: int | UBig | IBig) -> tuple: ... def __pow__(self, exp: int | UBig, modulus: Optional[int | UBig] = ...) -> IBig: ... - def __iadd__(self, other: IBig) -> None: ... - def __isub__(self, other: IBig) -> None: ... - def __imul__(self, other: IBig) -> None: ... - def __iand__(self, other: IBig) -> None: ... - def __ior__(self, other: IBig) -> None: ... - def __ixor__(self, other: IBig) -> None: ... + def __iadd__(self, other: int | IBig) -> None: ... + def __isub__(self, other: int | IBig) -> None: ... + def __imul__(self, other: int | IBig) -> None: ... + def __iand__(self, other: int | IBig) -> None: ... + def __ior__(self, other: int | IBig) -> None: ... + def __ixor__(self, other: int | IBig) -> None: ... def __ilshift__(self, other: int) -> None: ... def __irshift__(self, other: int) -> None: ... def __pos__(self) -> IBig: ... @@ -255,7 +255,7 @@ class FBig: def asin(self) -> FBig: ... def acos(self) -> FBig: ... def atan(self) -> FBig: ... - def atan2(self, x: FBig) -> FBig: ... + def atan2(self, x: float | int | FBig) -> FBig: ... def sinh(self) -> FBig: ... def cosh(self) -> FBig: ... def tanh(self) -> FBig: ... @@ -269,7 +269,7 @@ class FBig: def sqrt(self) -> FBig: ... def cbrt(self) -> FBig: ... def nth_root(self, n: int) -> FBig: ... - def powf(self, w: FBig) -> FBig: ... + def powf(self, w: float | int | FBig) -> FBig: ... def powi(self, n: int | IBig) -> FBig: ... @@ -327,7 +327,7 @@ class DBig: def asin(self) -> DBig: ... def acos(self) -> DBig: ... def atan(self) -> DBig: ... - def atan2(self, x: DBig) -> DBig: ... + def atan2(self, x: float | int | DBig) -> DBig: ... def sinh(self) -> DBig: ... def cosh(self) -> DBig: ... def tanh(self) -> DBig: ... @@ -341,7 +341,7 @@ class DBig: def sqrt(self) -> DBig: ... def cbrt(self) -> DBig: ... def nth_root(self, n: int) -> DBig: ... - def powf(self, w: DBig) -> DBig: ... + def powf(self, w: float | int | DBig) -> DBig: ... def powi(self, n: int | IBig) -> DBig: ... @@ -397,7 +397,7 @@ class RBig: @staticmethod def from_parts(numerator: int | IBig, denominator: int | UBig) -> RBig: ... @staticmethod - def simplest_from_float(f: FBig) -> Optional[RBig]: ... + def simplest_from_float(f: float | int | FBig) -> Optional[RBig]: ... class CBig: @@ -458,31 +458,31 @@ class Cache: def auto(obj: Number) -> "UBig|IBig|FBig|DBig|RBig": ... def autos(s: str) -> "UBig|IBig|FBig|DBig|RBig": ... -def sin(x: FBig) -> FBig: ... -def cos(x: FBig) -> FBig: ... -def tan(x: FBig) -> FBig: ... -def asin(x: FBig) -> FBig: ... -def acos(x: FBig) -> FBig: ... -def atan(x: FBig) -> FBig: ... -def atan2(y: FBig, x: FBig) -> FBig: ... -def sinh(x: FBig) -> FBig: ... -def cosh(x: FBig) -> FBig: ... -def tanh(x: FBig) -> FBig: ... -def asinh(x: FBig) -> FBig: ... -def acosh(x: FBig) -> FBig: ... -def atanh(x: FBig) -> FBig: ... -def exp(x: FBig) -> FBig: ... -def expm1(x: FBig) -> FBig: ... -def log(x: FBig) -> FBig: ... -def log1p(x: FBig) -> FBig: ... -def ln(x: FBig) -> FBig: ... -def ln_1p(x: FBig) -> FBig: ... -def sqrt(x: FBig) -> FBig: ... -def cbrt(x: FBig) -> FBig: ... -def nth_root(x: FBig, n: int) -> FBig: ... -def powf(x: FBig, y: FBig) -> FBig: ... -def powi(x: FBig, n: int | IBig) -> FBig: ... -def hypot(x: FBig, y: FBig) -> FBig: ... -def gcd(a: UBig, b: UBig) -> UBig: ... -def gcd_ext(a: UBig, b: UBig) -> tuple[UBig, IBig, IBig]: ... -def lcm(a: UBig, b: UBig) -> UBig: ... +def sin(x: Number) -> FBig: ... +def cos(x: Number) -> FBig: ... +def tan(x: Number) -> FBig: ... +def asin(x: Number) -> FBig: ... +def acos(x: Number) -> FBig: ... +def atan(x: Number) -> FBig: ... +def atan2(y: Number, x: Number) -> FBig: ... +def sinh(x: Number) -> FBig: ... +def cosh(x: Number) -> FBig: ... +def tanh(x: Number) -> FBig: ... +def asinh(x: Number) -> FBig: ... +def acosh(x: Number) -> FBig: ... +def atanh(x: Number) -> FBig: ... +def exp(x: Number) -> FBig: ... +def expm1(x: Number) -> FBig: ... +def log(x: Number) -> FBig: ... +def log1p(x: Number) -> FBig: ... +def ln(x: Number) -> FBig: ... +def ln_1p(x: Number) -> FBig: ... +def sqrt(x: Number) -> FBig: ... +def cbrt(x: Number) -> FBig: ... +def nth_root(x: Number, n: int) -> FBig: ... +def powf(x: Number, y: Number) -> FBig: ... +def powi(x: Number, n: int | IBig) -> FBig: ... +def hypot(x: Number, y: Number) -> FBig: ... +def gcd(a: int | UBig, b: int | UBig) -> UBig: ... +def gcd_ext(a: int | UBig, b: int | UBig) -> tuple[UBig, IBig, IBig]: ... +def lcm(a: int | UBig, b: int | UBig) -> UBig: ... diff --git a/python/src/convert.rs b/python/src/convert.rs index 75f1a253..baed202c 100644 --- a/python/src/convert.rs +++ b/python/src/convert.rs @@ -249,7 +249,10 @@ impl<'a> UniInput<'a> { Self::BUint(x) => Ok(FPy(FBig::from(x.0.clone()))), Self::BInt(x) => Ok(FPy(FBig::from(x.0.clone()))), Self::OBInt(x) => Ok(FPy(FBig::from(x))), - Self::Float(x) => FBig::try_from(x).map(FPy).map_err(conversion_error_to_py), + Self::Float(x) => FBig::try_from(x) + .map(|f| f.with_precision(f64::MANTISSA_DIGITS as usize).value()) + .map(FPy) + .map_err(conversion_error_to_py), Self::BFloat(x) => Ok(FPy(x.0.clone())), Self::BRational(x) => FBig::try_from(x.0.clone()) .map(FPy) diff --git a/python/src/float.rs b/python/src/float.rs index 0fe0b687..2347f0b2 100644 --- a/python/src/float.rs +++ b/python/src/float.rs @@ -341,7 +341,8 @@ impl FPy { let ctx = self.0.context(); Ok(Self(unwrap_float(ctx.nth_root(n, self.0.repr()), ctx)?)) } - fn powf(&self, w: &Self) -> PyResult { + fn powf(&self, w: UniInput<'_>) -> PyResult { + let w = w.into_fpy()?; let ctx = self.0.context(); let res = with_cache(|c| ctx.powf(self.0.repr(), w.0.repr(), Some(c))); Ok(Self(unwrap_float(res, ctx)?)) @@ -351,7 +352,8 @@ impl FPy { let ctx = self.0.context(); Ok(Self(unwrap_float(ctx.powi(self.0.repr(), n), ctx)?)) } - fn atan2(&self, x: &Self) -> PyResult { + fn atan2(&self, x: UniInput<'_>) -> PyResult { + let x = x.into_fpy()?; let ctx = self.0.context(); let res = with_cache(|c| ctx.atan2(self.0.repr(), x.0.repr(), Some(c))); Ok(Self(unwrap_float(res, ctx)?)) @@ -623,7 +625,8 @@ impl DPy { let ctx = self.0.context(); Ok(Self(unwrap_float(ctx.nth_root(n, self.0.repr()), ctx)?)) } - fn powf(&self, w: &Self) -> PyResult { + fn powf(&self, w: UniInput<'_>) -> PyResult { + let w = w.into_dpy()?; let ctx = self.0.context(); let res = with_cache(|c| ctx.powf(self.0.repr(), w.0.repr(), Some(c))); Ok(Self(unwrap_float(res, ctx)?)) @@ -633,7 +636,8 @@ impl DPy { let ctx = self.0.context(); Ok(Self(unwrap_float(ctx.powi(self.0.repr(), n), ctx)?)) } - fn atan2(&self, x: &Self) -> PyResult { + fn atan2(&self, x: UniInput<'_>) -> PyResult { + let x = x.into_dpy()?; let ctx = self.0.context(); let res = with_cache(|c| ctx.atan2(self.0.repr(), x.0.repr(), Some(c))); Ok(Self(unwrap_float(res, ctx)?)) diff --git a/python/src/int.rs b/python/src/int.rs index 6557134e..e6acf9d3 100644 --- a/python/src/int.rs +++ b/python/src/int.rs @@ -566,20 +566,20 @@ impl UPy { } Ok(self.0.ilog(&base)) } - fn is_multiple_of(&self, divisor: &Self) -> bool { - self.0.is_multiple_of(&divisor.0) + fn is_multiple_of(&self, divisor: UniInput<'_>) -> PyResult { + Ok(self.0.is_multiple_of(&divisor.into_ubig()?)) } - fn remove(&mut self, factor: &Self) -> PyResult { + fn remove(&mut self, factor: UniInput<'_>) -> PyResult { self.0 - .remove(&factor.0) + .remove(&factor.into_ubig()?) .ok_or_else(|| PyValueError::new_err("the factor does not divide this number")) } - fn gcd(&self, other: &Self) -> Self { - UPy(Gcd::gcd(&self.0, &other.0)) + fn gcd(&self, other: UniInput<'_>) -> PyResult { + Ok(UPy(Gcd::gcd(&self.0, &other.into_ubig()?))) } - fn gcd_ext(&self, other: &Self) -> (Self, IPy, IPy) { - let (g, s, t) = ExtendedGcd::gcd_ext(&self.0, &other.0); - (UPy(g), IPy(s), IPy(t)) + fn gcd_ext(&self, other: UniInput<'_>) -> PyResult<(Self, IPy, IPy)> { + let (g, s, t) = ExtendedGcd::gcd_ext(&self.0, &other.into_ubig()?); + Ok((UPy(g), IPy(s), IPy(t))) } /********** bit operations **********/ @@ -759,28 +759,34 @@ impl UPy { } #[inline] - fn __iadd__(&mut self, other: &Self) { - self.0 += &other.0; + fn __iadd__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 += &other.into_ubig()?; + Ok(()) } #[inline] - fn __isub__(&mut self, other: &Self) { - self.0 -= &other.0; + fn __isub__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 -= &other.into_ubig()?; + Ok(()) } #[inline] - fn __imul__(&mut self, other: &Self) { - self.0 *= &other.0; + fn __imul__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 *= &other.into_ubig()?; + Ok(()) } #[inline] - fn __iand__(&mut self, other: &Self) { - self.0 &= &other.0; + fn __iand__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 &= &other.into_ubig()?; + Ok(()) } #[inline] - fn __ior__(&mut self, other: &Self) { - self.0 |= &other.0; + fn __ior__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 |= &other.into_ubig()?; + Ok(()) } #[inline] - fn __ixor__(&mut self, other: &Self) { - self.0 ^= &other.0; + fn __ixor__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 ^= &other.into_ubig()?; + Ok(()) } #[inline] fn __ilshift__(&mut self, other: usize) { @@ -1151,28 +1157,34 @@ impl IPy { } #[inline] - fn __iadd__(&mut self, other: &Self) { - self.0 += &other.0; + fn __iadd__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 += &other.into_ibig()?; + Ok(()) } #[inline] - fn __isub__(&mut self, other: &Self) { - self.0 -= &other.0; + fn __isub__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 -= &other.into_ibig()?; + Ok(()) } #[inline] - fn __imul__(&mut self, other: &Self) { - self.0 *= &other.0; + fn __imul__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 *= &other.into_ibig()?; + Ok(()) } #[inline] - fn __iand__(&mut self, other: &Self) { - self.0 &= &other.0; + fn __iand__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 &= &other.into_ibig()?; + Ok(()) } #[inline] - fn __ior__(&mut self, other: &Self) { - self.0 |= &other.0; + fn __ior__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 |= &other.into_ibig()?; + Ok(()) } #[inline] - fn __ixor__(&mut self, other: &Self) { - self.0 ^= &other.0; + fn __ixor__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 ^= &other.into_ibig()?; + Ok(()) } #[inline] fn __ilshift__(&mut self, other: usize) { diff --git a/python/src/lib.rs b/python/src/lib.rs index 81887c19..867c0799 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -10,7 +10,7 @@ mod convert; mod float; mod int; mod math; -mod ratio; +mod rational; mod types; mod utils; mod words; diff --git a/python/src/math.rs b/python/src/math.rs index 8293b5af..95d5edfc 100644 --- a/python/src/math.rs +++ b/python/src/math.rs @@ -1,178 +1,131 @@ -//! Module-level math functions. Each routes through the global `ConstCache` + the -//! panic-free `Context` layer (via [`crate::cache::unwrap_float`]), so domain errors raise -//! Python exceptions instead of aborting the session. +//! Module-level math functions. Each accepts any Python number (via [`UniInput`]) and +//! routes through the global `ConstCache` + the panic-free `Context` layer (via +//! [`crate::cache::unwrap_float`]), so domain errors raise Python exceptions instead of +//! aborting the session. use crate::cache::{unwrap_float, with_cache}; -use crate::types::{FPy, IPy, UPy}; +use crate::types::{FPy, UPy, UniInput}; use dashu_base::ring::{ExtendedGcd, Gcd}; use pyo3::prelude::*; -// Trigonometric -#[pyfunction] -pub fn sin(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.sin(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn cos(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.cos(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn tan(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.tan(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn asin(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.asin(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn acos(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.acos(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn atan(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.atan(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) +/// Transcendental taking one float operand and a cache (`(repr, Option<&mut ConstCache>)`). +macro_rules! math_trans { + ($name:ident, $ctx_method:ident) => { + #[pyfunction] + pub fn $name(x: UniInput<'_>) -> PyResult { + let x = x.into_fpy()?; + let ctx = x.0.context(); + let res = with_cache(|c| ctx.$ctx_method(x.0.repr(), Some(c))); + Ok(FPy(unwrap_float(res, ctx)?)) + } + }; } -#[pyfunction] -pub fn atan2(y: &FPy, x: &FPy) -> PyResult { - let ctx = y.0.context(); - let res = with_cache(|c| ctx.atan2(y.0.repr(), x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) + +/// Algebraic root taking one float operand, no cache. +macro_rules! math_root { + ($name:ident, $ctx_method:ident) => { + #[pyfunction] + pub fn $name(x: UniInput<'_>) -> PyResult { + let x = x.into_fpy()?; + let ctx = x.0.context(); + Ok(FPy(unwrap_float(ctx.$ctx_method(x.0.repr()), ctx)?)) + } + }; } +// Trigonometric +math_trans!(sin, sin); +math_trans!(cos, cos); +math_trans!(tan, tan); +math_trans!(asin, asin); +math_trans!(acos, acos); +math_trans!(atan, atan); + // Hyperbolic -#[pyfunction] -pub fn sinh(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.sinh(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn cosh(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.cosh(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn tanh(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.tanh(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn asinh(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.asinh(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn acosh(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.acosh(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn atanh(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.atanh(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} +math_trans!(sinh, sinh); +math_trans!(cosh, cosh); +math_trans!(tanh, tanh); +math_trans!(asinh, asinh); +math_trans!(acosh, acosh); +math_trans!(atanh, atanh); // Exponential and logarithm +math_trans!(exp, exp); +math_trans!(expm1, exp_m1); +math_trans!(ln, ln); +math_trans!(ln_1p, ln_1p); +// `log`/`log1p` are aliases for `ln`/`ln_1p`. +math_trans!(log, ln); +math_trans!(log1p, ln_1p); + +// Roots (algebraic — no cache) +math_root!(sqrt, sqrt); +math_root!(cbrt, cbrt); + #[pyfunction] -pub fn exp(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.exp(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn expm1(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.exp_m1(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn log(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.ln(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn log1p(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.ln_1p(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) -} -#[pyfunction] -pub fn ln(x: &FPy) -> PyResult { +pub fn nth_root(x: UniInput<'_>, n: usize) -> PyResult { + let x = x.into_fpy()?; let ctx = x.0.context(); - let res = with_cache(|c| ctx.ln(x.0.repr(), Some(c))); - Ok(FPy(unwrap_float(res, ctx)?)) + Ok(FPy(unwrap_float(ctx.nth_root(n, x.0.repr()), ctx)?)) } + #[pyfunction] -pub fn ln_1p(x: &FPy) -> PyResult { - let ctx = x.0.context(); - let res = with_cache(|c| ctx.ln_1p(x.0.repr(), Some(c))); +pub fn atan2(y: UniInput<'_>, x: UniInput<'_>) -> PyResult { + let y = y.into_fpy()?; + let x = x.into_fpy()?; + let ctx = y.0.context(); + let res = with_cache(|c| ctx.atan2(y.0.repr(), x.0.repr(), Some(c))); Ok(FPy(unwrap_float(res, ctx)?)) } -// Roots and power (sqrt/cbrt/nth_root/hypot are algebraic — no cache, but still via Context) -#[pyfunction] -pub fn sqrt(x: &FPy) -> PyResult { - let ctx = x.0.context(); - Ok(FPy(unwrap_float(ctx.sqrt(x.0.repr()), ctx)?)) -} #[pyfunction] -pub fn cbrt(x: &FPy) -> PyResult { - let ctx = x.0.context(); - Ok(FPy(unwrap_float(ctx.cbrt(x.0.repr()), ctx)?)) -} -#[pyfunction] -pub fn nth_root(x: &FPy, n: usize) -> PyResult { - let ctx = x.0.context(); - Ok(FPy(unwrap_float(ctx.nth_root(n, x.0.repr()), ctx)?)) -} -#[pyfunction] -pub fn powf(x: &FPy, y: &FPy) -> PyResult { +pub fn powf(x: UniInput<'_>, y: UniInput<'_>) -> PyResult { + let x = x.into_fpy()?; + let y = y.into_fpy()?; let ctx = x.0.context(); let res = with_cache(|c| ctx.powf(x.0.repr(), y.0.repr(), Some(c))); Ok(FPy(unwrap_float(res, ctx)?)) } + #[pyfunction] -pub fn powi(x: &FPy, n: crate::types::UniInput<'_>) -> PyResult { +pub fn powi(x: UniInput<'_>, n: UniInput<'_>) -> PyResult { + let x = x.into_fpy()?; let n = n.into_ibig()?; let ctx = x.0.context(); Ok(FPy(unwrap_float(ctx.powi(x.0.repr(), n), ctx)?)) } + #[pyfunction] -pub fn hypot(x: &FPy, y: &FPy) -> PyResult { +pub fn hypot(x: UniInput<'_>, y: UniInput<'_>) -> PyResult { + let x = x.into_fpy()?; + let y = y.into_fpy()?; let ctx = x.0.context(); Ok(FPy(unwrap_float(ctx.hypot(x.0.repr(), y.0.repr()), ctx)?)) } // Integer number theory #[pyfunction] -pub fn gcd(a: &UPy, b: &UPy) -> UPy { - UPy(Gcd::gcd(&a.0, &b.0)) +pub fn gcd(a: UniInput<'_>, b: UniInput<'_>) -> PyResult { + let a = a.into_ubig()?; + let b = b.into_ubig()?; + Ok(UPy(Gcd::gcd(&a, &b))) } + #[pyfunction] -pub fn gcd_ext(a: &UPy, b: &UPy) -> (UPy, IPy, IPy) { - let (g, s, t) = ExtendedGcd::gcd_ext(&a.0, &b.0); - (UPy(g), IPy(s), IPy(t)) +pub fn gcd_ext( + a: UniInput<'_>, + b: UniInput<'_>, +) -> PyResult<(UPy, crate::types::IPy, crate::types::IPy)> { + let a = a.into_ubig()?; + let b = b.into_ubig()?; + let (g, s, t) = ExtendedGcd::gcd_ext(&a, &b); + Ok((UPy(g), crate::types::IPy(s), crate::types::IPy(t))) } + #[pyfunction] -pub fn lcm(a: &UPy, b: &UPy) -> UPy { - let g = Gcd::gcd(&a.0, &b.0); - UPy((a.0.clone() / g) * b.0.clone()) +pub fn lcm(a: UniInput<'_>, b: UniInput<'_>) -> PyResult { + let a = a.into_ubig()?; + let b = b.into_ubig()?; + let g = Gcd::gcd(&a, &b); + Ok(UPy((a / g) * b)) } diff --git a/python/src/ratio.rs b/python/src/rational.rs similarity index 98% rename from python/src/ratio.rs rename to python/src/rational.rs index ab17e5d7..bf4e692b 100644 --- a/python/src/ratio.rs +++ b/python/src/rational.rs @@ -235,7 +235,8 @@ impl RPy { } /// Find the simplest rational within the error bounds of the given float. #[staticmethod] - fn simplest_from_float(f: &FPy) -> Option { - RBig::simplest_from_float(&f.0).map(RPy) + fn simplest_from_float(f: UniInput<'_>) -> PyResult> { + let f = f.into_fpy()?; + Ok(RBig::simplest_from_float(&f.0).map(RPy)) } } diff --git a/python/tests/__pycache__/test_convert.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_convert.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index fb49a23e5ab43c15197b90b3087e360d7e2a99a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2450 zcmb^zU27aw^xWCm+1Zb5J{yxLG?8jERkGchv=NLNiKL1I1O*8#!({JlvdQesdS{Y$ z;|>Pt!$yOwR4`zLAbqF=-~0u>`e0I18!rVP^d)c6^eG_q-kF`*jSaRBy*qQy=RF_y zoH^`IEiDOP>*pU|n>Pi3KR6LAMtkevA_HFm28;}tMjjN3k(_{_C{9Namk5URJ)?2t z8cfG@8H)>p07Mo*k8Re?ASk0UQlfjDo>CA+XL5Cs2Y4V6_8yc(&mD3?X5h~&Y z!2y$rM`Y1v*=*DWCmI;S(kAlp3Ap?sfQxVp%N4XN1Mt@sDli`lv>UJ@R0NC`F0quP zhA;Dor@%6TwILBIFab4mc^lvoi_{g|h-ZP}B<1idy;Tu{Q?6)sJN4KA8vNsQ^MWfn z@ndX6on*+3hL)v@REh9%xUovChU^umCD58r2Ur8*cqNucIKfZlg-uoqzHriM_c4I^ zjsR=GpvX&L_cqz@YG?$Q<24+of(S|TiS9tJ0UthLBAmpjCl(Q_yuThhqK8hfh*G%a zu_HS1|98}=KWWxH=589AEP|FMtH^>?(4j?3G;hJ*@4BPzI%Aitv%L$G^e7 z2W%P4D?Io(_?-mfkh7Z@dt77hAI99{U!#H!g|RUOEl)i+SSp`a?8HZPq<^fRV`mk6 zki+3X#E`luTNj;EY$f5cr+>^pGpg8Bbjl^2irS)MQM5#nqewKu%<(PDw{v_O=G%F` zjV`xpwyhJ#^O|LDsmL_XDrqXd|3+b!if{2fN;Gp;r;=7G=_V#AmIC4PgC<@tl^xx7 zRE^MfHK!SdO7s%RS;dk@bedvfb!M?(I0e(Dl3_6ooA#*ooV93RPgR{YtQpNvO|7WY zxN2*8om-&kaWAfdWxh#>MQEp*w@6WQRQ+nn&`ix?5+y+sp7)$)>o!FN!p9ZilZqmh zCb@S%y_cGak!Iq8Px!`P_9OO*Y}V%Ur$1bW7c+BKQP0e4IcsJ)gEf0@F*B4iv_-6E zu2^KjA-Zm7*p19tW&n76cE*p=GMCs@50%Qa%VQKwN6mRmn`L8@7nnOf@a;L)S}U+G zK_a;}QH}4$6Fx|;y;n`_#!uet+LE8&jrZR?wI!e4Pf8uq%G>ulAl+HZe*Qrf?Y6Ym zvNi3C;njB-A$Q!Bd$;A@_3VarEBkH!8~nqmt-cF)RE5UKDi|itU0yG&s>I7-3_$eI&fF! z&#yl16XCwi)qU!h1{#WQ%L89dgyH~g7O%I5;sD)|2Wpc(mz}S=p}LQ-z&22LC_w+C z^jDIle!wmqsI00*tzfFE5~p&|vE&&p>Q((7AM#s?RceUnS4hEO{Ti}ptj;5Rve2m4 z99}`WY2Mtb`W+DVM*_$h&*3XY3ojb_t7L?ce3aOy*|r}+2>lLl4R)aOZ}9|rc7F)O WRCW1wr1v+m=Z@I79|5t?GxRTOMGu(( diff --git a/python/tests/__pycache__/test_float_ops.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_float_ops.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 11108e59038b34c437ba61235c02950dfbdf19c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46996 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 z zlT`y9t{-6)LTGoDW2l(9FIKPkSgj2tinlxf#an8oSgLAkWQFEZS%nq9;{QE{aSBYS zi^e5azNu4ZG_IIdFwewdlMao=&i4?!@}LT}H-OY{as3x5&$t?GkRYa2W9qYEiZV@` zt0E(nS{!Diy2guZx!Qyy8FLUEW%Rv?a@fAS|KwD8;~7oPhv&mndrk$LX= z6#!U)zLK;0?wRyi=5ZcTVUCb_EboF#W*;pz6WSu7sp1@9_uwFqZeXpQN&+eK90!4v zzQVzhkkPwE(z_+|@`AZ#&dj`gGYnwnP$rzSw%jx6v&`c>qQaaBWFE`A;F7t8=q0p8 zLQ};#!0y38AkCQ^1X2dN0fCeWbFd^_<1g{&5%YI7$pdM%w%st(FJFhmZi66)Nu9MV z6F~qiR%F52cF&~GGLQ3!3Uh?RV|f=;*^5-S%9h}gxs9|~R4WRkA*lkKffh@E`pufRF&(WXl!KqCma6S-Y$G87-pn=Nn?VX&`YjV9X5@rlj|wu7KK^A z-|0bV58YELfYJd~8e4od!fhZ~FIEbkwc!j{^Q@}N-H2ww>nð~O+otz_2(*`t(P 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 OICA%CL%`U~lJWmgfOA9u diff --git a/python/tests/__pycache__/test_math.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_math.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index cfc1bd9790b5af15a11370e3c270d7e2f3dae133..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43086 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 diff --git a/python/tests/__pycache__/test_ratio_ops.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_ratio_ops.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 0207f2b625e8376f546ca3cf71f8cfdf873de634..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41478 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®Zd4UN;uMZM>8{GDBw;LhDsfD!M|uqHKnkU6A+>S0oM-G@V})47vY#1$V~ z$tYC?o-RnL1W5rGBu4HNQ~spmIUDgWRH^^*DVPG*NZI)BCcQCo%11+AQ7WEA9buj zl)HMq0j9e~#~MVLjq^=dgG6A4Z&GWJSdJ7+>|-e-2?7PRUsC6qQG?>-*jsg6`*F*tyR=~^teOY=Fs&sDpsvL zJZDQ4MGb2ebzOP%uvX^>b^Upir_8V(c(yUKU4$EJGqxEWR-0J+#A{yrC}W_!`o!9Y z!iz=mBU7H!jeyK~t@#%F-s`ZAmbBgVh`_hk88; zSIMI-3C5hDK4%H$a6(}I2c+1cnBUL96!~}3%K8j93A=d&xP+5|b+Ov|s4SPZ;SzR4 zW>AEU(KbD*@)Cfg*8z-^-1JDsP!{@!8~SLSW}6A6)s>kenbQHu1c%eXOLSOndMz_U zl4Au68DqXkn8Fp~J;?^Z>9+{-P=-=M>nDPaa=vJpGO(j@xKKc$mNN|^nbY{$(0Y#t)$o0Pmt$yX`)7A40i zc^*lYoB~{}zuflQB7)Jdh(klfG4m8C=n*ITYbe~-u>Qu{-o7|VsItajm+u9Vyt^9+I>f-?`@I6k5sxu+5Bk52G!|hz5A`4 zIxWGQz`Uq%IKbMZFy{pkFbgn^Py0B|W*2o^h5niGJ*p60|-6rHE< zZIQu`RJugj{Ak4n)#+tDaT$#!44jtWO+7J-m@tBl1E}zdjVVE37EW>m6jeY^D~Ybb z9wdsFAc%@7MO5riAo^2LfTC6G86@@$O7;vAdj=(Y1|94{bQcu25{2<>a3PIkF`C4` zBPtBbs0f|n9u-C*A^r;SNCaq+AO(#9yUK#Gur?}8Vi%nTCb5e^6h-W+BqAPB9^$`9 zkb=g5U4n?%B|ye52}hGEe<97m>Qr%YfVemyxi~;v9FSZbaBy+J=HkGD0VHCYu^1h= zqto}c$lymRUD83vk5+6@onF=lh;>u|PD}8nK0sU)C3AqaQDG7n=`=8jiv*%5;$kHc zv5xXkyaYi+TogpaMFBD{N|?AP;01$&)hSxFhPmuE{AbqRTy{pwBl5v0EeR?<`8qu0 z`5kI1rC7}Cc*jj=-rfN#kgvA_=5CbXqFl-cVQvdK6k00ALq2Q;?LACTMmXk#`pRjJg<&9833hNBMC{WB=RHS`bymj+5dRVM6Wv0cAtL16P@T zu%Ae+E#2-Y)1{@ZZ_?4|5w>@FuzaIR zS?S=nQXu&>%%T5v)Z!AK$M9dIU2Gy3gBQzpv8k1(&M|f2=?0Cyihg25*KPFO9x>M* z{VEEKe$>Hlmr=c_uGO{iEM;CZ8pP-~8jaY$qERTTnpPQorCjrAjv98@^Fu-w3|~_1 zJ)*Js6=Tu&W1@!U-0w+TM@iU9s`6tg1zd$XjCKo=ra*d=Mn;Gs6CtU}u!{&J-0sTq zV7~K_bAXgFkeRKu$V|8l>~yr$b+$8E*G7_d6UjPdpm+4nWp3F!X8k{qiShY*Arsr3 zGO^}4tE6PboGqKLxF>6)^hjCp+2HxPZEJpVhO=GLY#Howf2qC$LosMmxLhUG6dQ8S z{`}@ADLN`XRr9lMUFB!T*8JoQ-a(?F2c~`@j-+=MT@yw(Q zdHGg62?ZTbLTX)@k6;Diw_2@;T;m`bos4&h=PNuHIoC(4uNAfv)falLuw&g;&}tOr zJ=IedS5ZI~?^s)YJ6v%#+moM~w;nxI@2+mPryEC9$~Eu7xGH^o=sh^0o|cS9l*#@} z#_uVUePhk*J>{GrYj%@YcqWl=+_GmB$P8^1?u3<}O8I5%%*P=o;&2awY{~w!qgQT{ zWfyx2*-aE*WXa;-g3CJ0Qce`cqw_jj%eNa}HugB)sIK+-bG0|B)x9Vm_qfCOjK}gF zjyp_;oF(}-}nk7U9DOc=bF}S z6|5$xZ1;vpUiBQh>7Fmuc-A~8H<@0^ezC%%hn;eUc;IcX5l^kSJjzoN-`FeSC+w(M zcJGjFk^c_a-2s^$$B*31JMr6&Uoanmqr$!yO%mNaT7t07pCps_D%SUjbK_}j<&iP% zt%4G;GzK2 z@}OBq971$_%_L#Vf=Zx$#_DVmO;9aBz5B7#$;2>rM?wSCyXW4V>#FC*`&HTIjO{?7 zB<{aqr^hnH!uxHQ@51o?F<>}7GIrn9uvi}G)8hTrb=ph~=gKRi!58BKNK6eE_X|w8 zYF8Wbx)5{RcIbfH4VzYflk%{5!TcVM(c94hf#$~^NDK^K_&$34>0U-&o28P|v?u6{anH45GU5{$L4LzP7rfxQP528TMS z9BTjwQZSgE;xQ3K$aL{tVy;SXQgQKf zDTS5nD@7f`XYEJ|rTWz5WO~9VHOcL#$iXBtX-8Nn1(;+g1=w|1(&;k60ZrgiJ?~G~ zBc?$OX0a2VBZ&FGyh0GU!#JZTbaNLP@`G`PQb;CPScWpMnzF5*5khuEc@J#NF)5*MXCe6$wc3&Gw zXNSzpxd~%r;*2>3auS2t?_sj8?v(Qj{m69U?ZFvrn{#CDERy-h=FYx{?RAb!56$CH;#P-znvq6@r9|Sy+`io;wxy>F$b2Y0uHX&AWEgZ zb!=k5pr;LWC=Zwt1m@gXp1TZi9|xW3E#4Go7GTs{fVp3kCPC`Wcm*cwlft~7;~+3^ z9OEGBU4Y+DS!35Y1Ue!yyxlj0SFv}^^wBO*yXN3+v5UvU05IJ@7hIxEuIPK4AM*p1 zE+H3eBZL(jya}+6=PnbOLgxUhgM%QV4W5V|M04<#AP_`z@RlI3;=4Ipd}!v-qP}zH z(3^+xH;37|^KuURpJ8_HoIX4|wG`cXN2l*?e#{S4x`bTtm=IQM@Fu`RJa?JM6gmf3 z9UKJFWy}_$2NCUMMj(i0F5_4JJYC!$nhvayR$hV7Rmn z&(L_!9DZ{Ee{Q)-UwyR25YuSG^+$~1TzdsQ4pHzgqp`P-&R{pa z-hvOw{K10nJzVD%@oHv{&h-^etVEx}nZ@W+ccK)50Wi{)D7|gJEF;$NSOEy$LjF=B z0*wSIq`*-XOn?KRowqS12+V?yBOt`g9LBjUNDPQ%FdD=mkRXU$;?jVgTFeSyFoG>m zVGJ%SRaH{$3q?x@KYpLqV`;GdzEIGhx%oj=Sol5kGdzp{_Fdo~`)L<(diBp7dt7tu z?cE^Dw*Rr$743LcUts|V*1R8byQ6#;AF49nn5eARb-!t4Wr2P?=qS}i7i32X_rS9~ zs6sXOXuG3Kmmb-pD4*1!hg!2|)fDJqXl)vY1t7d`E!QRgYi@ZJ1zH~VXu0y~^1yTM zUq$PC_8Fxswc;bSip&yebJ}fe=sAdeYVKhb1$r3qXt@e7;DKlBjqFyt&1TyI5ET}H z%KfY0!O1_48r93L@*G~_Rxe=6{yVftYwcmBjk+{U zYLxShMr;*3W(ajOSdCUlX_=c4-?xeRiTW)+?3i&!zul#nA5yHDs2tXYFPs!}aKUXJ z+5U8!P1-3>B}kX`k_A<=%TrEQQPd#q)cp1s8)ch=bXl(+R>>~U9#&D*Anop3ig|EC zd2bpm^Nqs4trcMeq9Z&!ODtEbv{Nrndq_KaRhfJ<>2+nYd4}>$(4EI$%ME*$N%(!=|q^sBcU!M)xA7LpOI5w3iano8P0J-yIE1PZb#WD=E5AwM$x*o> zgVc1?9MLLE)^OW@KR`CW?K|UI;OHBP$wytC=c`!ga&8!Vm0O!8&iU` zQ9HCzqNc)BHPLnG21W4_q>b8NRzwRS1y=V@l?P2y^}|0&J@)Xw7wPBs@QbFE-U(6$=+UHrMKGu`#c-IJZD_ zbB9sR6^`Dy;i<yuU{%{7yCatL7rr`jrYq=i|E6dL5$rYaOTeT0K5W!3Q` z(9gfZLC_cWa1iu!-&W{O0B4xoE`^4M##18`Lqo8urn9-B3)|qXGLqSynau8{jO^}7 zvT_?;9i`Bzb0cH9k%=rDsM7YK$@7Gwuk^r+@vc(Y_5o+Q@YCMf5)@L(jE^U;8XT~z8Qe)Xt0JV+7 z?S#&k>P}CTBH3KZ%w?w#n7$O4G-2ldP-7kC10Q;3JY$?2OCK@+7FE*qnLUT(W53Vm z`wva~k#(i(1audN^->vALUo3cp7Yx)k>tp5YSPqH-t diff --git a/python/tests/__pycache__/test_words.cpython-312-pytest-9.1.1.pyc b/python/tests/__pycache__/test_words.cpython-312-pytest-9.1.1.pyc deleted file mode 100644 index 8beb9fffce1a33976e770374d0303b99053892c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8489 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 diff --git a/python/tests/test_float_ops.py b/python/tests/test_float_ops.py index 37776918..1999e59f 100644 --- a/python/tests/test_float_ops.py +++ b/python/tests/test_float_ops.py @@ -1,38 +1,33 @@ import math +from decimal import Decimal from dashu import * def test_construct(): - # FBig (base 2) from float / int / string (binary digits) - assert FBig(1.5) == FBig(7) - FBig(0.5) if False else True assert float(FBig(1.5)) == 1.5 - assert FBig(7).to_int() == IBig(7) - # DBig (base 10) from Decimal / string / float - from decimal import Decimal - + assert FBig(7).to_int() == 7 assert float(DBig(Decimal("1.5"))) == 1.5 assert float(DBig("1.25")) == 1.25 def test_arithmetic(): - a, b = FBig(1.5), FBig(2.0) - assert a + b == FBig(3.5) - assert a - b == FBig(-0.5) - assert a * b == FBig(3.0) - assert a / b == FBig(0.75) + a = FBig(1.5) + assert a + 2.0 == FBig(3.5) + assert a - 2.0 == FBig(-0.5) + assert a * 2.0 == FBig(3.0) + assert a / 2.0 == FBig(0.75) assert -a == FBig(-1.5) assert abs(FBig(-1.5)) == FBig(1.5) - # mixed with int - assert FBig(1.5) + 1 == FBig(2.5) + assert a + 1 == FBig(2.5) def test_compare_bool(): - assert FBig(1.5) < FBig(2.0) - assert FBig(1.5) <= FBig(1.5) - assert FBig(2.0) > FBig(1.5) - assert FBig(1.5) != FBig(2.0) - assert FBig(1.5) == FBig(1.5) + assert FBig(1.5) < 2.0 + assert FBig(1.5) <= 1.5 + assert FBig(2.0) > 1.5 + assert FBig(1.5) != 2.0 + assert FBig(1.5) == 1.5 assert bool(FBig(0.0)) is False assert bool(FBig(0.5)) is True @@ -41,18 +36,18 @@ def test_predicates_rounding(): f = FBig(1.5) assert f.is_finite() and not f.is_infinite() assert FBig(0.0).is_zero() - assert f.floor().to_int() == IBig(1) - assert f.ceil().to_int() == IBig(2) - assert f.round().to_int() == IBig(2) - assert f.trunc().to_int() == IBig(1) + assert f.floor().to_int() == 1 + assert f.ceil().to_int() == 2 + assert f.round().to_int() == 2 + assert f.trunc().to_int() == 1 def test_transcendentals_panicfree(): - assert abs(float(FBig(2.0).ln()) - math.log(2)) < 1e-9 - assert abs(float(FBig(1.0).exp()) - math.e) < 1e-9 - assert FBig(4.0).sqrt() == FBig(2.0) + assert FBig(4.0).sqrt() == 2.0 assert abs(float(FBig(1.0).sin()) - math.sin(1)) < 1e-9 assert abs(float(FBig(0.0).sin())) < 1e-9 + assert abs(float(FBig(2.0).ln()) - math.log(2)) < 1e-9 + assert abs(float(FBig(1.0).exp()) - math.e) < 1e-9 # domain error -> ValueError (not a session-crashing panic) try: FBig(-1.0).sqrt() @@ -70,10 +65,9 @@ def test_cross_conversions(): f = FBig(0.5) assert float(f.to_decimal()) == 0.5 assert f.to_binary() == f - # exact float -> rational - assert f.to_rational() == RBig.from_parts(IBig(1), UBig(2)) - # rational -> float (lossy, with precision) - assert RBig.from_parts(IBig(1), UBig(2)).to_float(53) == FBig(0.5) + # exact float -> rational, and rational -> float (lossy, with precision) + assert f.to_rational() == RBig.from_parts(1, 2) + assert RBig.from_parts(1, 2).to_float(53) == FBig(0.5) def test_dbig_arithmetic(): diff --git a/python/tests/test_int_math.py b/python/tests/test_int_math.py index acadacc3..5aba6cea 100644 --- a/python/tests/test_int_math.py +++ b/python/tests/test_int_math.py @@ -2,18 +2,15 @@ def test_roots(): - assert UBig(144).sqrt() == UBig(12) - assert UBig(27).cbrt() == UBig(3) - assert UBig(1024).nth_root(5) == UBig(4) - assert UBig(12).sqr() == UBig(144) - assert UBig(3).cubic() == UBig(27) - # ilog accepts a plain int as well as a UBig - assert UBig(256).ilog(2) == 8 - assert IBig(1000).ilog(UBig(10)) == 3 + assert UBig(144).sqrt() == 12 + assert UBig(27).cbrt() == 3 + assert UBig(1024).nth_root(5) == 4 + assert UBig(12).sqr() == 144 + assert UBig(3).cubic() == 27 # IBig: cbrt / odd nth_root are sign-preserving - assert IBig(-27).cbrt() == IBig(-3) - assert IBig(-1024).nth_root(5) == IBig(-4) - assert IBig(144).sqrt() == UBig(12) + assert IBig(-27).cbrt() == -3 + assert IBig(-1024).nth_root(5) == -4 + assert IBig(144).sqrt() == 12 # even root of negative -> ValueError, not a panic try: IBig(-4).nth_root(2) @@ -23,47 +20,43 @@ def test_roots(): def test_number_theory(): - assert UBig(12).gcd(UBig(8)) == UBig(4) - g, x, y = UBig(12).gcd_ext(UBig(8)) - assert g == UBig(4) - assert UBig(12) * x + UBig(8) * y == g - assert UBig(6).is_multiple_of(UBig(3)) - assert UBig(2).remove(UBig(2)) == 1 # 2 = 2^1 + assert UBig(12).gcd(8) == 4 + assert UBig(12).gcd_ext(8)[0] == 4 + assert UBig(6).is_multiple_of(3) + assert UBig(2).remove(2) == 1 # 2 = 2^1 assert UBig(2).is_power_of_two() - assert UBig(5).next_power_of_two() == UBig(8) + assert UBig(5).next_power_of_two() == 8 def test_bit_ops(): n = UBig(0b10110) # 22 assert n.count_ones() == 3 assert n.trailing_zeros() == 1 - assert n.trailing_ones() == 0 assert UBig(0b1000).trailing_zeros() == 3 def test_divmod_floordiv(): - assert int(UBig(17) // UBig(5)) == 3 - q, r = divmod(UBig(17), UBig(5)) - assert int(q) == 3 and int(r) == 2 - assert int(UBig(20) // 3) == 6 - # IBig floor division - assert int(IBig(-17) // IBig(5)) == -4 + assert UBig(17) // 5 == 3 + q, r = divmod(UBig(17), 5) + assert q == 3 and r == 2 + assert UBig(20) // 3 == 6 + assert IBig(-17) // 5 == -4 def test_inplace_ops(): n = UBig(10) - n += UBig(5) - assert int(n) == 15 - n -= UBig(3) - assert int(n) == 12 - n *= UBig(2) - assert int(n) == 24 + n += 5 + assert n == 15 + n -= 3 + assert n == 12 + n *= 2 + assert n == 24 n <<= 1 - assert int(n) == 48 + assert n == 48 n >>= 2 - assert int(n) == 12 - n &= UBig(0b100) # 12 & 4 - assert int(n) == 4 + assert n == 12 + n &= 4 # 12 & 4 + assert n == 4 def test_chunks_words(): @@ -74,10 +67,7 @@ def test_chunks_words(): if __name__ == "__main__": - test_roots() - test_number_theory() - test_bit_ops() - test_divmod_floordiv() - test_inplace_ops() - test_chunks_words() + for name, fn in sorted(globals().items()): + if name.startswith("test_"): + fn() print("int math tests passed") diff --git a/python/tests/test_math.py b/python/tests/test_math.py index be50bd3a..b30406c4 100644 --- a/python/tests/test_math.py +++ b/python/tests/test_math.py @@ -5,41 +5,38 @@ def test_trig_hyper_exp_log(): - assert abs(float(dashu.sin(FBig(0.0)))) < 1e-9 - assert abs(float(dashu.cos(FBig(0.0))) - 1.0) < 1e-9 - assert abs(float(dashu.exp(FBig(1.0))) - math.e) < 1e-9 - assert abs(float(dashu.log(FBig(2.0))) - math.log(2)) < 1e-9 - assert abs(float(dashu.sinh(FBig(1.0))) - math.sinh(1)) < 1e-9 + assert abs(float(dashu.sin(0.0))) < 1e-9 + assert abs(float(dashu.cos(0.0)) - 1.0) < 1e-9 + assert abs(float(dashu.exp(1.0)) - math.e) < 1e-9 + assert abs(float(dashu.log(2.0)) - math.log(2)) < 1e-9 + assert abs(float(dashu.sinh(1.0)) - math.sinh(1)) < 1e-9 def test_roots_power(): - assert dashu.sqrt(FBig(9.0)) == FBig(3.0) - assert dashu.cbrt(FBig(27.0)) == FBig(3.0) - assert dashu.nth_root(FBig(16.0), 4) == FBig(2.0) - # powi (integer power) is exact; powf routes through exp/log and rounds. - # powi accepts a plain Python int as well as an IBig. - assert dashu.powi(FBig(2.0), 10) == FBig(1024.0) - assert dashu.powi(FBig(2.0), IBig(10)) == FBig(1024.0) - assert abs(float(dashu.powf(FBig(2.0), FBig(10.0))) - 1024.0) < 1e-6 - assert float(dashu.hypot(FBig(3.0), FBig(4.0))) == 5.0 + assert dashu.sqrt(9.0) == 3.0 + assert dashu.cbrt(27.0) == 3.0 + assert dashu.nth_root(16.0, 4) == 2.0 + # powi (integer power) is exact; powf routes through exp/log and rounds + assert dashu.powi(2.0, 10) == 1024.0 + assert abs(float(dashu.powf(2.0, 10.0)) - 1024.0) < 1e-6 + assert dashu.hypot(3.0, 4.0) == 5.0 def test_integer_number_theory(): - assert dashu.gcd(UBig(12), UBig(8)) == UBig(4) - g, x, y = dashu.gcd_ext(UBig(12), UBig(8)) - assert g == UBig(4) and UBig(12) * x + UBig(8) * y == g - assert dashu.lcm(UBig(4), UBig(6)) == UBig(12) + assert dashu.gcd(12, 8) == 4 + assert dashu.gcd_ext(12, 8)[0] == 4 + assert dashu.lcm(4, 6) == 12 def test_complex_module(): - z = CBig(FBig(3.0), FBig(4.0)) - assert z.abs() == FBig(5.0) - assert z.conj().imag() == FBig(-4.0) - assert z.norm() == FBig(25.0) + z = CBig(3.0, 4.0) + assert z.abs() == 5.0 + assert z.conj().imag() == -4.0 + assert z.norm() == 25.0 # complex arithmetic + transcendentals - assert CBig(FBig(1.0), FBig(0.0)) + CBig(FBig(2.0), FBig(0.0)) == CBig(FBig(3.0), FBig(0.0)) + assert CBig(1.0, 2.0) + CBig(3.0, 4.0) == CBig(4.0, 6.0) assert abs(float(z.exp().real()) - math.exp(3) * math.cos(4)) < 1e-6 - assert CBig(FBig(0.0), FBig(0.0)).exp() == CBig(FBig(1.0), FBig(0.0)) + assert CBig(0.0, 0.0).exp() == CBig(1.0, 0.0) if __name__ == "__main__": diff --git a/python/tests/test_ratio_ops.py b/python/tests/test_ratio_ops.py index 02fda99c..8d6645ce 100644 --- a/python/tests/test_ratio_ops.py +++ b/python/tests/test_ratio_ops.py @@ -4,52 +4,50 @@ def test_construct(): - assert RBig(Fraction(1, 3)) == RBig.from_parts(IBig(1), UBig(3)) - assert RBig(0.5) == RBig.from_parts(IBig(1), UBig(2)) - assert RBig("1/3") == RBig.from_parts(IBig(1), UBig(3)) - assert RBig(7) == RBig.from_parts(IBig(7), UBig(1)) + assert RBig(Fraction(1, 3)) == RBig.from_parts(1, 3) + assert RBig(0.5) == RBig.from_parts(1, 2) + assert RBig("1/3") == RBig.from_parts(1, 3) + assert RBig(7) == RBig.from_parts(7, 1) def test_arithmetic(): - a = RBig.from_parts(IBig(1), UBig(3)) - b = RBig.from_parts(IBig(2), UBig(3)) - assert a + b == RBig.from_parts(IBig(1), UBig(1)) - assert b - a == RBig.from_parts(IBig(1), UBig(3)) - assert a * b == RBig.from_parts(IBig(2), UBig(9)) - assert (a / b) == RBig.from_parts(IBig(1), UBig(2)) - assert -a == RBig.from_parts(IBig(-1), UBig(3)) - # mixed with int - assert a + 1 == RBig.from_parts(IBig(4), UBig(3)) + a = RBig.from_parts(1, 3) + b = RBig.from_parts(2, 3) + assert a + b == RBig.from_parts(1, 1) + assert b - a == RBig.from_parts(1, 3) + assert a * b == RBig.from_parts(2, 9) + assert a / b == RBig.from_parts(1, 2) + assert -a == RBig.from_parts(-1, 3) + assert a + 1 == RBig.from_parts(4, 3) def test_compare_bool(): - a = RBig.from_parts(IBig(1), UBig(2)) - assert a < RBig.from_parts(IBig(2), UBig(3)) - assert a == RBig.from_parts(IBig(2), UBig(4)) - assert bool(RBig.from_parts(IBig(0), UBig(1))) is False - assert bool(RBig.from_parts(IBig(1), UBig(3))) is True + a = RBig.from_parts(1, 2) + assert a < RBig.from_parts(2, 3) + assert a == RBig.from_parts(2, 4) + assert bool(RBig.from_parts(0, 1)) is False + assert bool(RBig.from_parts(1, 3)) is True def test_properties_rounding(): - r = RBig.from_parts(IBig(7), UBig(3)) - assert r.numerator == IBig(7) and r.denominator == UBig(3) - assert r.trunc() == IBig(2) - assert r.floor() == IBig(2) - assert r.ceil() == IBig(3) - assert r.fract() == RBig.from_parts(IBig(1), UBig(3)) - assert r.is_int() is False - assert RBig.from_parts(IBig(6), UBig(3)).is_int() + r = RBig.from_parts(7, 3) + assert r.numerator == 7 and r.denominator == 3 + assert r.trunc() == 2 + assert r.floor() == 2 + assert r.ceil() == 3 + assert r.fract() == RBig.from_parts(1, 3) + assert not r.is_int() + assert RBig.from_parts(6, 3).is_int() def test_powers(): - r = RBig.from_parts(IBig(2), UBig(3)) - assert r.sqr() == RBig.from_parts(IBig(4), UBig(9)) - assert r.pow(3) == RBig.from_parts(IBig(8), UBig(27)) + r = RBig.from_parts(2, 3) + assert r.sqr() == RBig.from_parts(4, 9) + assert r.pow(3) == RBig.from_parts(8, 27) def test_float_conversion(): - r = RBig.from_parts(IBig(1), UBig(2)) - assert r.to_float(53) == FBig(0.5) + assert RBig.from_parts(1, 2).to_float(53) == FBig(0.5) if __name__ == "__main__": From c2e4f1bcff53682f12e639986a31292bdecf9839 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 9 Jul 2026 01:17:34 +0800 Subject: [PATCH 15/20] Implement __format__ (Python format mini-language) for all types - UBig/IBig: delegate to Python int.__format__ (arbitrary precision, all integer presentation types: b/o/d/x/X/c/n, sign/width/align/fill/zero/grouping). - FBig/DBig: new format.rs renders in decimal (FBig converts via to_decimal) with e/E/f/g/n/% types, precision, sign, width, align, fill, zero-pad, grouping, and normalizes dashu's bare exponent to CPython's `e+00` form. Arbitrary precision is preserved: f"{FBig(2).with_precision(200).exp():.20e}" -> 7.38905609893065022723e+00. - RBig: empty spec -> "num/den"; float specs render via a high-precision conversion. - CBig: applies the spec to both parts -> "(re+imj)". Added tests/test_format.py. 34/34 tests pass, 0 clippy warnings, fmt clean. Co-Authored-By: Claude --- python/CHANGELOG.md | 4 + python/src/complex.rs | 13 +- python/src/float.rs | 8 +- python/src/format.rs | 308 ++++++++++++++++++++++++++++++++++++ python/src/int.rs | 16 +- python/src/lib.rs | 1 + python/src/rational.rs | 14 +- python/tests/test_format.py | 45 ++++++ 8 files changed, 395 insertions(+), 14 deletions(-) create mode 100644 python/src/format.rs create mode 100644 python/tests/test_format.py diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index f8970579..d8c57666 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -21,6 +21,10 @@ `UBig(n) += 5`, `RBig.from_parts(1, 3)`. The module-level `math` functions, `powf`, `atan2`, `gcd`/`gcd_ext`/`lcm`, `is_multiple_of`/`remove`, in-place ops, `powi`, `from_parts`, `ilog`, and `simplest_from_float` all take native int/float. +- `__format__` now honors the Python format mini-language for all types: scientific + (`e`/`E`), fixed (`f`), general (`g`), integer (`b`/`o`/`d`/`x`/`X`/`c`), with + sign/width/align/fill/zero-pad/grouping and precision. Float formatting preserves + the value's arbitrary precision (e.g. `f"{FBig(2).with_precision(200).exp():.20e}"`). - Broadened constructors: `FBig`/`DBig`/`RBig`/`CBig` now accept any Python number (int/float/`Decimal`/`Fraction`) in addition to strings. - A module-level `math` API (`sin`/`cos`/…/`exp`/`ln`/`sqrt`/`gcd`/`lcm`/…) and a diff --git a/python/src/complex.rs b/python/src/complex.rs index 4b634eae..ed29f82e 100644 --- a/python/src/complex.rs +++ b/python/src/complex.rs @@ -118,8 +118,17 @@ impl CPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self, _format_spec: &str) -> String { - format!("{}", self.0) + fn __format__(&self, format_spec: &str) -> PyResult { + if format_spec.is_empty() { + return Ok(format!("{}", self.0)); + } + let (re, im) = self.0.clone().into_parts(); + let re_s = crate::format::format_dbig(&re.to_decimal().value(), format_spec)?; + let mut im_s = crate::format::format_dbig(&im.to_decimal().value(), format_spec)?; + if !(im_s.starts_with('-') || im_s.starts_with('+')) { + im_s = format!("+{im_s}"); + } + Ok(format!("({re_s}{im_s}j)")) } fn __hash__(&self) -> u64 { // mirror Python's complex hash convention loosely: combine real/imag float hashes diff --git a/python/src/float.rs b/python/src/float.rs index 2347f0b2..2da9c10b 100644 --- a/python/src/float.rs +++ b/python/src/float.rs @@ -111,8 +111,8 @@ impl FPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self, _format_spec: &str) -> String { - format!("{}", self.0) + fn __format__(&self, format_spec: &str) -> PyResult { + crate::format::format_dbig(&self.0.to_decimal().value(), format_spec) } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); @@ -395,8 +395,8 @@ impl DPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self, _format_spec: &str) -> String { - format!("{}", self.0) + fn __format__(&self, format_spec: &str) -> PyResult { + crate::format::format_dbig(&self.0, format_spec) } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); diff --git a/python/src/format.rs b/python/src/format.rs new file mode 100644 index 00000000..f9c7b08e --- /dev/null +++ b/python/src/format.rs @@ -0,0 +1,308 @@ +//! Python format mini-language (`__format__`) support. +//! +//! Integers (`UBig`/`IBig`) delegate to Python's own `int.__format__` (Python ints are +//! arbitrary precision, so there is no loss). Floats (`FBig`/`DBig`) are rendered in decimal +//! (an `FBig` is first converted to base 10) using dashu's precision-aware formatting, then +//! the Python spec's layout (sign / width / align / fill / zero-pad / grouping) and the +//! scientific exponent are normalized to CPython conventions. + +use dashu_float::DBig; +use pyo3::PyResult; +use pyo3::exceptions::PyValueError; + +/// A parsed Python format spec: `[[fill]align][sign][#][0][width][grouping][.precision][type]`. +#[allow(dead_code)] +pub struct Spec { + pub fill: char, + pub align: Option, // '<' '>' '=' '^' + pub sign: char, // '+' '-' ' ' + pub alt: bool, // '#' + pub zero: bool, // '0' (zero-pad) + pub width: Option, + pub group: Option, // ',' or '_' + pub prec: Option, + pub ty: char, // '\0' = none +} + +const FLOAT_TYPES: &str = "eEfFgGn%"; + +pub fn parse(spec: &str) -> PyResult { + let chars: Vec = spec.chars().collect(); + let mut i = 0; + let n = chars.len(); + + // [[fill]align] + let mut fill = ' '; + let mut align = None; + if n >= 2 && "<>=^".contains(chars[1]) { + fill = chars[0]; + align = Some(chars[1]); + i = 2; + } else if n >= 1 && "<>=^".contains(chars[0]) { + align = Some(chars[0]); + i = 1; + } + + // [sign] + let mut sign = '-'; + if i < n && "+- ".contains(chars[i]) { + sign = chars[i]; + i += 1; + } + + // [#] + let mut alt = false; + if i < n && chars[i] == '#' { + alt = true; + i += 1; + } + + // [0] + let mut zero = false; + if i < n && chars[i] == '0' { + zero = true; + i += 1; + } + + // [width] + let mut width = None; + let start = i; + while i < n && chars[i].is_ascii_digit() { + i += 1; + } + if i > start { + width = Some(chars[start..i].iter().collect::().parse().unwrap()); + } + + // [grouping] + let mut group = None; + if i < n && (chars[i] == ',' || chars[i] == '_') { + group = Some(chars[i]); + i += 1; + } + + // [.precision] + let mut prec = None; + if i < n && chars[i] == '.' { + i += 1; + let start = i; + while i < n && chars[i].is_ascii_digit() { + i += 1; + } + if i == start { + return Err(PyValueError::new_err("missing precision in format spec")); + } + prec = Some(chars[start..i].iter().collect::().parse().unwrap()); + } + + // [type] + let ty = if i < n { + let t = chars[i]; + if i + 1 != n { + return Err(PyValueError::new_err(format!("invalid format specifier '{spec}'"))); + } + t + } else { + '\0' + }; + + Ok(Spec { + fill, + align, + sign, + alt, + zero, + width, + group, + prec, + ty, + }) +} + +impl Spec { + fn is_float_type(&self) -> bool { + self.ty == '\0' || FLOAT_TYPES.contains(self.ty) + } +} + +/// Render a `DBig` (base-10 float) according to a parsed Python spec, including layout. +pub fn format_dbig(d: &DBig, spec_str: &str) -> PyResult { + let s = parse(spec_str)?; + if !s.is_float_type() { + let ty_str = if s.ty == '\0' { + String::new() + } else { + s.ty.to_string() + }; + return Err(PyValueError::new_err(format!( + "unknown format code '{ty_str}' for object of type 'float'" + ))); + } + + // Produce the unsigned (no sign policy) numeric body in decimal. + let (negative, body) = render_body(d, &s)?; + let signed = apply_sign(body, negative, s.sign); + let grouped = apply_grouping(signed, s.group); + Ok(apply_layout(grouped, &s)) +} + +/// Render the magnitude as a decimal string (without sign), per the spec's type. +fn render_body(d: &DBig, s: &Spec) -> PyResult<(bool, String)> { + // infinite values: let dashu render, then strip the sign + let (neg, raw) = match s.ty { + 'e' | 'E' => { + let p = s.prec.unwrap_or(6); + let mut raw = format!("{:.*e}", p, d); + raw = normalize_sci(raw, s.ty == 'E'); + strip_sign(&raw) + } + 'f' | 'F' => { + let p = s.prec.unwrap_or(6); + strip_sign(&format!("{:.*}", p, d)) + } + '%' => { + let p = s.prec.unwrap_or(6); + // ×100, fixed, then a trailing '%' + let scaled = d.clone() * DBig::from(100u8); + let (neg, mut body) = strip_sign(&format!("{:.*}", p, &scaled)); + body.push('%'); + (neg, body) + } + 'g' | 'G' | 'n' | '\0' => { + // general / default: full plain decimal (dashu Display), with optional precision + // limiting significant digits via a re-round. + if let Some(p) = s.prec { + let rounded = d.clone().with_precision(p.max(1)).value(); + strip_sign(&format!("{}", rounded)) + } else { + strip_sign(&format!("{}", d)) + } + } + _ => unreachable!(), + }; + Ok((neg, raw)) +} + +/// Split a leading '-' from a dashu-rendered string; return (is_negative, magnitude_str). +fn strip_sign(s: &str) -> (bool, String) { + if let Some(rest) = s.strip_prefix('-') { + (true, rest.to_string()) + } else if let Some(rest) = s.strip_prefix('+') { + (false, rest.to_string()) + } else { + (false, s.to_string()) + } +} + +/// Reattach the sign according to the Python sign option ('+', '-', ' '). +fn apply_sign(body: String, negative: bool, sign: char) -> String { + if negative { + format!("-{body}") + } else { + match sign { + '+' => format!("+{body}"), + ' ' => format!(" {body}"), + _ => body, + } + } +} + +/// Insert a grouping separator every 3 digits in the integer part. +fn apply_grouping(mut body: String, group: Option) -> String { + let sep = match group { + Some(c) => c, + None => return body, + }; + // locate the integer-part digits: from start (or after a leading sign) up to '.'/'e'/'E'/'%' + let bytes: Vec = body.chars().collect(); + let start = if bytes + .first() + .map(|c| *c == '-' || *c == '+') + .unwrap_or(false) + { + 1 + } else { + 0 + }; + let end = bytes + .iter() + .enumerate() + .skip(start) + .find(|(_, c)| **c == '.' || **c == 'e' || **c == 'E' || **c == '%') + .map(|(i, _)| i) + .unwrap_or(bytes.len()); + if end <= start { + return body; + } + let int_digits: Vec = bytes[start..end].to_vec(); + let m = int_digits.len(); + let mut grouped = String::new(); + for (k, c) in int_digits.iter().enumerate() { + if k > 0 && (m - k) % 3 == 0 { + grouped.push(sep); + } + grouped.push(*c); + } + // reassemble + let prefix: String = bytes[..start].iter().collect(); + let suffix: String = bytes[end..].iter().collect(); + body = format!("{prefix}{grouped}{suffix}"); + body +} + +/// Apply width / align / fill / zero-pad. +fn apply_layout(mut body: String, s: &Spec) -> String { + let width = match s.width { + Some(w) => w, + None => return body, + }; + let pad = width.saturating_sub(body.chars().count()); + if pad == 0 { + return body; + } + let (fill, align) = if s.zero { + ('0', s.align.unwrap_or('=')) + } else { + (s.fill, s.align.unwrap_or('>')) + }; + let pad_str: String = std::iter::repeat_n(fill, pad).collect(); + match align { + '<' => body.push_str(&pad_str), + '>' => body = format!("{pad_str}{body}"), + '^' => { + let half = pad / 2; + body = format!( + "{}{}{}", + std::iter::repeat_n(fill, half).collect::(), + body, + std::iter::repeat_n(fill, pad - half).collect::() + ); + } + '=' => { + // padding after the sign + let (sign, rest) = if body.starts_with('-') || body.starts_with('+') { + body.split_at(1) + } else { + ("", body.as_str()) + }; + body = format!("{sign}{pad_str}{rest}"); + } + _ => {} + } + body +} + +/// Normalize dashu's bare scientific exponent (`1.5e0`) to CPython's form (`1.5e+00`). +fn normalize_sci(s: String, upper: bool) -> String { + let idx = s.find(['e', 'E']); + match idx { + Some(i) => { + let (head, tail) = s.split_at(i); + let exp_part = &tail[1..]; + let exp: isize = exp_part.parse().unwrap_or(0); + let marker = if upper { 'E' } else { 'e' }; + format!("{head}{marker}{exp:+03}") + } + None => s, + } +} diff --git a/python/src/int.rs b/python/src/int.rs index e6acf9d3..49729621 100644 --- a/python/src/int.rs +++ b/python/src/int.rs @@ -408,9 +408,11 @@ impl UPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self, _format_spec: &str) -> String { - // MVP: ignore the format mini-language and delegate to Display. - format!("{}", self.0) + fn __format__(&self, format_spec: &str, py: Python<'_>) -> PyResult { + // delegate to Python int (arbitrary precision — no loss) + convert_from_ubig(&self.0, py)? + .call_method1("__format__", (format_spec,))? + .extract::() } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); @@ -880,9 +882,11 @@ impl IPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self, _format_spec: &str) -> String { - // MVP: ignore the format mini-language and delegate to Display. - format!("{}", self.0) + fn __format__(&self, format_spec: &str, py: Python<'_>) -> PyResult { + // delegate to Python int (arbitrary precision — no loss) + convert_from_ibig(&self.0, py)? + .call_method1("__format__", (format_spec,))? + .extract::() } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); diff --git a/python/src/lib.rs b/python/src/lib.rs index 867c0799..b182e064 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -8,6 +8,7 @@ mod cache; mod complex; mod convert; mod float; +mod format; mod int; mod math; mod rational; diff --git a/python/src/rational.rs b/python/src/rational.rs index bf4e692b..8e295c99 100644 --- a/python/src/rational.rs +++ b/python/src/rational.rs @@ -86,8 +86,18 @@ impl RPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self, _format_spec: &str) -> String { - format!("{}", self.0) + fn __format__(&self, format_spec: &str) -> PyResult { + if format_spec.is_empty() { + return Ok(format!("{}", self.0)); + } + // non-empty spec: convert to a decimal float (lossy) and render with it + let parsed = crate::format::parse(format_spec)?; + let prec = parsed.prec.unwrap_or(53).max(1); + let dbig = self + .0 + .to_float::(prec) + .value(); + crate::format::format_dbig(&dbig, format_spec) } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); diff --git a/python/tests/test_format.py b/python/tests/test_format.py new file mode 100644 index 00000000..345f28fc --- /dev/null +++ b/python/tests/test_format.py @@ -0,0 +1,45 @@ +import math + +from dashu import * + + +def test_float_scientific(): + assert format(FBig(1.5), "e") == "1.500000e+00" + assert format(FBig(1.5), ".2e") == "1.50e+00" + assert format(FBig(12345.0), "E") == "1.234500E+04" + assert format(FBig(0.5), ".4f") == "0.5000" + # arbitrary precision is preserved through formatting + assert format(FBig(2.0).with_precision(200).exp(), ".20e") == "7.38905609893065022723e+00" + + +def test_float_layout(): + assert format(FBig(3.14), "010.2f") == "0000003.14" + assert format(FBig(1234567.0), ",.2f") == "1,234,567.00" + assert format(FBig(5.0), "+.1f") == "+5.0" + assert format(FBig(5.0), "^10") == " 5.0 " or format(FBig(5.0), "^10") == " 5 " + + +def test_dbig_format(): + assert format(DBig("1.5"), ".3e") == "1.500e+00" + assert format(DBig("1.5"), "") == "1.5" + + +def test_integers_delegate(): + assert format(UBig(255), "#x") == "0xff" + assert format(UBig(5), "b") == "101" + assert format(IBig(-42), "08d") == "-0000042" + assert format(UBig(10**9), ",") == "1,000,000,000" + + +def test_rational_complex(): + assert format(RBig.from_parts(1, 3), ".4f") == "0.3333" + assert format(CBig(3.0, 4.0), ".2f") == "(3.00+4.00j)" + # default spec for rational is the fraction + assert "1/3" in format(RBig.from_parts(1, 3), "") + + +if __name__ == "__main__": + for name, fn in sorted(globals().items()): + if name.startswith("test_"): + fn() + print("format tests passed") From a4b43fc5bd35c6133fde1964fecbfbb8cdf74904 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 9 Jul 2026 01:25:50 +0800 Subject: [PATCH 16/20] Print FBig/DBig in decimal from __str__ (not the float's base) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit str()/print() previously used dashu's Display, which prints in the number's base — so an FBig (base 2) printed in BINARY (str(FBig(2.0)) == '10', str(FBig(0.1)) was a long binary expansion). Route __str__ through the decimal formatter (__format__ path), converting via to_decimal, so str(FBig(2.0)) == '2', str(FBig(0.1)) == '0.1', and the output reflects the value's precision in decimal. DBig was already decimal; routed through the same path for consistency. Co-Authored-By: Claude --- python/src/float.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/python/src/float.rs b/python/src/float.rs index 2da9c10b..87f32f93 100644 --- a/python/src/float.rs +++ b/python/src/float.rs @@ -108,8 +108,9 @@ impl FPy { fn __repr__(&self) -> String { format!("", self.0) } - fn __str__(&self) -> String { - format!("{}", self.0) + fn __str__(&self) -> PyResult { + // print in decimal (an FBig is base 2; dashu's Display would print binary) + crate::format::format_dbig(&self.0.to_decimal().value(), "") } fn __format__(&self, format_spec: &str) -> PyResult { crate::format::format_dbig(&self.0.to_decimal().value(), format_spec) @@ -392,8 +393,8 @@ impl DPy { fn __repr__(&self) -> String { format!("", self.0) } - fn __str__(&self) -> String { - format!("{}", self.0) + fn __str__(&self) -> PyResult { + crate::format::format_dbig(&self.0, "") } fn __format__(&self, format_spec: &str) -> PyResult { crate::format::format_dbig(&self.0, format_spec) From 59171523f18c5195e9430f5bac322ccf178dd9f9 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 9 Jul 2026 01:35:13 +0800 Subject: [PATCH 17/20] FBig prints in hex; float 'e' default uses native precision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FBig (base 2) now renders in hexadecimal by default (str/format/''/'a'/'A'), via dashu's LowerHex/UpperHex — lossless, no base-2->base-10 conversion rounding (str(FBig(1.5)) == '0x3p-1'). Decimal presentations 'e'/'f'/'g' still convert to base 10. DBig stays decimal-native. - When no precision is given for 'e'/'E', show the value's full native precision (significant digits) instead of CPython's fixed 6 — format(DBig(1.23) .with_precision(100).powi(100000), 'e') now prints all 100 digits. Use '.6e' for the old 6-digit default. Added format_fbig_hex; updated tests. Co-Authored-By: Claude --- python/CHANGELOG.md | 6 ++++++ python/src/float.rs | 10 +++++++--- python/src/format.rs | 32 ++++++++++++++++++++++++++++++-- python/tests/test_format.py | 29 +++++++++++++++++++++++------ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index d8c57666..0f85dab0 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -29,6 +29,12 @@ (int/float/`Decimal`/`Fraction`) in addition to strings. - A module-level `math` API (`sin`/`cos`/…/`exp`/`ln`/`sqrt`/`gcd`/`lcm`/…) and a `dashu.Cache` handle for the global constant cache. +- `FBig` (base 2) now prints in **hexadecimal** by default (`str`/`format`/`'a'`/`'A'`), + e.g. `str(FBig(1.5)) == '0x3p-1'` — lossless, no base-2→base-10 rounding. Decimal + presentations (`'e'`/`'f'`/`'g'`) still convert to base 10. (`DBig` stays decimal.) +- Float format default precision is now the value's native precision (its significant + digits) rather than CPython's fixed 6 — `format(f, 'e')` shows all digits; use `'.6e'` + for the old 6-digit behavior. - Transcendentals are now panic-free: domain errors raise `ValueError`, `0/0`-style indeterminate forms raise `ZeroDivisionError`, and overflow/underflow produce signed infinities/zeros — routed through the panic-free `Context` layer with a diff --git a/python/src/float.rs b/python/src/float.rs index 87f32f93..10c32cc2 100644 --- a/python/src/float.rs +++ b/python/src/float.rs @@ -109,11 +109,15 @@ impl FPy { format!("", self.0) } fn __str__(&self) -> PyResult { - // print in decimal (an FBig is base 2; dashu's Display would print binary) - crate::format::format_dbig(&self.0.to_decimal().value(), "") + // an FBig is base 2: print in hexadecimal (lossless — no base conversion rounding) + crate::format::format_fbig_hex(&self.0, "") } fn __format__(&self, format_spec: &str) -> PyResult { - crate::format::format_dbig(&self.0.to_decimal().value(), format_spec) + // default / 'a' / 'A' -> hexadecimal (lossless); decimal presentations convert to base 10 + match crate::format::parse(format_spec)?.ty { + '\0' | 'a' | 'A' => crate::format::format_fbig_hex(&self.0, format_spec), + _ => crate::format::format_dbig(&self.0.to_decimal().value(), format_spec), + } } fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); diff --git a/python/src/format.rs b/python/src/format.rs index f9c7b08e..6462ca01 100644 --- a/python/src/format.rs +++ b/python/src/format.rs @@ -148,10 +148,14 @@ pub fn format_dbig(d: &DBig, spec_str: &str) -> PyResult { /// Render the magnitude as a decimal string (without sign), per the spec's type. fn render_body(d: &DBig, s: &Spec) -> PyResult<(bool, String)> { - // infinite values: let dashu render, then strip the sign + // When no precision is given, show the value's full native precision (its significant + // digit count) rather than CPython's fixed default of 6 — this is an arbitrary-precision + // library, so truncating to 6 digits by default would silently hide information. + let native = d.digits(); let (neg, raw) = match s.ty { 'e' | 'E' => { - let p = s.prec.unwrap_or(6); + // scientific: `native` significant digits => `native - 1` fraction digits + let p = s.prec.unwrap_or(native.saturating_sub(1)); let mut raw = format!("{:.*e}", p, d); raw = normalize_sci(raw, s.ty == 'E'); strip_sign(&raw) @@ -183,6 +187,30 @@ fn render_body(d: &DBig, s: &Spec) -> PyResult<(bool, String)> { Ok((neg, raw)) } +/// Render a base-2 `FBig` in hexadecimal (lossless: hex significand, binary exponent), +/// used for the default/`'a'`/`'A'` presentation types so no base conversion rounding occurs. +pub fn format_fbig_hex(f: &dashu_float::FBig, spec_str: &str) -> PyResult { + let s = parse(spec_str)?; + let upper = s.ty == 'A'; + let body = match s.prec { + Some(p) => { + if upper { + format!("{:.*X}", p, f) + } else { + format!("{:.*x}", p, f) + } + } + None => { + if upper { + format!("{:X}", f) + } else { + format!("{:x}", f) + } + } + }; + Ok(apply_layout(body, &s)) +} + /// Split a leading '-' from a dashu-rendered string; return (is_negative, magnitude_str). fn strip_sign(s: &str) -> (bool, String) { if let Some(rest) = s.strip_prefix('-') { diff --git a/python/tests/test_format.py b/python/tests/test_format.py index 345f28fc..876e78d3 100644 --- a/python/tests/test_format.py +++ b/python/tests/test_format.py @@ -3,12 +3,28 @@ from dashu import * -def test_float_scientific(): - assert format(FBig(1.5), "e") == "1.500000e+00" +def test_fbig_hex_default(): + # FBig is base 2: default str / format is lossless hexadecimal (no base conversion) + assert str(FBig(1.5)) == "0x3p-1" # 3 * 2^-1 + assert str(FBig(2.0)) == "0x1p1" + assert str(FBig(0.5)) == "0x1p-1" + assert format(FBig(1.5), "") == "0x3p-1" + assert format(FBig(1.5), "a") == "0x3p-1" + assert format(FBig(1.5), ".4a") == "0x3.0000p-1" + + +def test_float_scientific_native_default(): + # 'e' default shows the value's full native precision (not CPython's fixed 6 digits) + assert format(FBig(1.5), "e") == "1.5e+00" + assert format(DBig("1.5"), "e") == "1.5e+00" + assert format(FBig(12345.0), "E") == "1.2345E+04" + # explicit precision still overrides assert format(FBig(1.5), ".2e") == "1.50e+00" - assert format(FBig(12345.0), "E") == "1.234500E+04" assert format(FBig(0.5), ".4f") == "0.5000" - # arbitrary precision is preserved through formatting + # a high-precision value prints all its significant digits by default + f = DBig("1.23").with_precision(100).powi(100000) + assert format(f, "e").startswith("3.2444713221954405338085664273496250244079439837529295953908288") + # arbitrary precision is preserved through explicit formatting too assert format(FBig(2.0).with_precision(200).exp(), ".20e") == "7.38905609893065022723e+00" @@ -16,12 +32,13 @@ def test_float_layout(): assert format(FBig(3.14), "010.2f") == "0000003.14" assert format(FBig(1234567.0), ",.2f") == "1,234,567.00" assert format(FBig(5.0), "+.1f") == "+5.0" - assert format(FBig(5.0), "^10") == " 5.0 " or format(FBig(5.0), "^10") == " 5 " + assert format(DBig("123.5"), ">8") == " 123.5" def test_dbig_format(): + # DBig is base 10: default str is plain decimal + assert str(DBig("1.5")) == "1.5" assert format(DBig("1.5"), ".3e") == "1.500e+00" - assert format(DBig("1.5"), "") == "1.5" def test_integers_delegate(): From 631dd280601629b5d7e21ec77735bd603c996e89 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 9 Jul 2026 01:42:53 +0800 Subject: [PATCH 18/20] Fix RBig .Ne formatting accuracy (trailing-zero artifact) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RBig.__format__ converted to a decimal float at exactly the requested precision, so e.g. format(RBig(1)/3, '.20e') printed a spurious trailing 0 (only 20 sig digits were available but 21 were requested). Convert with a 16-digit guard when a precision is given so the printed digits are correctly rounded. (No-precision default stays 53.) Note: format(r, '20e') is WIDTH 20 (Python spec), not precision — use '.20e' for 20 fraction digits. The digit count for a bare 'e' is the native default. Co-Authored-By: Claude --- python/src/rational.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/python/src/rational.rs b/python/src/rational.rs index 8e295c99..e02e8c13 100644 --- a/python/src/rational.rs +++ b/python/src/rational.rs @@ -90,12 +90,21 @@ impl RPy { if format_spec.is_empty() { return Ok(format!("{}", self.0)); } - // non-empty spec: convert to a decimal float (lossy) and render with it + // non-empty spec: convert to a decimal float (lossy) and render with it. + // Convert at a precision a few digits higher than requested, so that the digits + // we actually print are correctly rounded (a rational has no native decimal + // precision, so there's no "full precision" to fall back on). let parsed = crate::format::parse(format_spec)?; - let prec = parsed.prec.unwrap_or(53).max(1); + // Convert at the requested precision + a guard (so printed digits are correctly + // rounded); with no requested precision, use a default (a rational has no native + // decimal precision to fall back on). + let conv_prec = match parsed.prec { + Some(p) => p.saturating_add(16), + None => 53, + }; let dbig = self .0 - .to_float::(prec) + .to_float::(conv_prec) .value(); crate::format::format_dbig(&dbig, format_spec) } From aab35a3a1610f57dfde48e7357d039444d73f734 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 9 Jul 2026 01:52:31 +0800 Subject: [PATCH 19/20] Add Python Binding guide page (shared with the package README) python/README.md is now the canonical usage doc (rendered by PyPI); the new guide/src/python.md just does {{#include ../../python/README.md}} so the guide and the PyPI README stay in sync from a single source. SUMMARY.md lists the page. The doc briefly covers the types, the mirrored operations (cross-type arithmetic, integer number theory / bit ops, float/rational/complex transcendentals, the panic-free error model + shared constant cache, conversions, and the module-level math API), and the supported formatting (Python mini-language; FBig prints hex by default, RBig prints the exact fraction). All code examples were verified. Co-Authored-By: Claude --- guide/src/SUMMARY.md | 1 + guide/src/python.md | 1 + python/README.md | 87 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 guide/src/python.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index dc16785b..5896ffb6 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -20,6 +20,7 @@ - [Trigonometric and Hyperbolic Functions](./ops/trig_n_hyper.md) - [Bit Manipulation](./ops/bit.md) - [Number Theoretic](./ops/num_theory.md) +- [Python Binding](./python.md) --- diff --git a/guide/src/python.md b/guide/src/python.md new file mode 100644 index 00000000..6bb05a26 --- /dev/null +++ b/guide/src/python.md @@ -0,0 +1 @@ +{{#include ../../python/README.md}} diff --git a/python/README.md b/python/README.md index ed09451e..91166da5 100644 --- a/python/README.md +++ b/python/README.md @@ -1,3 +1,86 @@ -# dashu-python +# Python Binding -Python binding to the numeric types in the dashu crates, based on PyO3. +`dashu` brings the dashu arbitrary-precision number types to Python through a native +extension built with [PyO3](https://pyo3.rs). Install the package and import it: + +```sh +pip install dashu-rs +``` + +```python +from dashu import UBig, IBig, RBig, FBig, DBig, CBig +``` + +## Types + +| Type | Backing | Base / rounding | +|------|---------|-----------------| +| `UBig`, `IBig` | arbitrary-precision integer | unsigned / signed | +| `RBig` | arbitrary-precision rational | exact `numerator / denominator` | +| `FBig` | arbitrary-precision float | base 2, round `Zero` | +| `DBig` | arbitrary-precision float | base 10, round `HalfAway` (decimal) | +| `CBig` | arbitrary-precision complex | base 2 | + +All types are `Send` + `Sync` (free-threaded-Python compatible). Constructors accept +native Python numbers as well as strings: + +```python +UBig(144) # int +FBig(1.5) # float (kept at f64's native precision) +DBig("1.23") # decimal string +RBig(Fraction(1, 3)) # fractions.Fraction +CBig(3.0, 4.0) # (re, im) +``` + +## Operations + +Arithmetic, comparisons, and `bool()` accept any Python number (cross-type dispatch), +so mixed operands just work: + +```python +UBig(2) + 3 == 5 +FBig(1.5) * 2 == FBig(3.0) +UBig(17) // 5 == 3 +divmod(UBig(17), 5) == (UBig(3), UBig(2)) +``` + +- **Integers**: `+ - * / // % **`, comparisons, in-place ops, bit operations + (`& | ^ << >>`), roots (`sqrt`/`cbrt`/`nth_root`), `gcd`/`gcd_ext`, `ilog`, bit + predicates, and `to_words`/`to_chunks`/`to_bytes`. +- **Floats / Decimal**: arithmetic, comparisons, rounding (`trunc`/`floor`/`ceil`/ + `round`/`fract`), precision (`precision`/`with_precision`), transcendentals, and + conversions `to_decimal`/`to_binary`/`to_rational`/`to_int`. +- **Rational**: arithmetic, `numerator`/`denominator`, rounding, `sqr`/`pow`, `to_float`. +- **Complex**: arithmetic, `real`/`imag`, `conj`/`proj`/`norm`/`abs`/`arg`, and + transcendentals (`sin`/`cos`/`exp`/`ln`/`sqrt`/...). + +Transcendentals are **panic-free**: domain errors raise `ValueError`, indeterminate forms +like `0/0` raise `ZeroDivisionError`, and overflow/underflow yield signed infinities or +zeros. They share a module-wide constant cache (`dashu.Cache`) so repeated calls at +increasing precision reuse the precomputed constants. + +A module-level `math` API mirrors the common functions — `dashu.sin`, `dashu.sqrt`, +`dashu.exp`, `dashu.gcd`, `dashu.lcm`, … — and likewise accepts plain Python numbers. + +## Formatting + +`format()` / f-strings honor the full Python format mini-language: + +```python +format(UBig(255), "#x") # '0xff' +format(UBig(10**9), ",") # '1,000,000,000' +format(FBig(2).with_precision(200).exp(), ".20e") # '7.38905609893065022723e+00' +format(DBig("1.5"), ".3e") # '1.500e+00' +format(RBig.from_parts(1, 3), ".4f") # '0.3333' +format(CBig(3.0, 4.0), ".2f") # '(3.00+4.00j)' +``` + +- **Integers** delegate to Python `int`, so every presentation type works (`b`/`o`/`d`/ + `x`/`X`/`c`/`n`) with sign, width, fill, zero-pad, and grouping — with no loss. +- **Floats** (`FBig`/`DBig`) support `e`/`E`/`f`/`g` with precision, sign, width, align, + fill, zero-pad, and grouping. With no explicit precision, all significant digits are + shown (use `.6e` for the fixed 6-digit default). +- **`FBig` is base 2**, so its default `str`/`format` and the `'a'`/`'A'` types print in + **hexadecimal** — lossless, with no base conversion, e.g. `str(FBig(1.5)) == '0x3p-1'`. + Decimal presentations (`'e'`/`'f'`/`'g'`) convert to base 10. `DBig` prints in decimal. +- **`RBig`** defaults to the exact fraction, e.g. `str(RBig(1) / 3) == '1/3'`. From 592e7f1eb344363b6c6fdf1a4c6a6835a6b367f8 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 9 Jul 2026 01:58:30 +0800 Subject: [PATCH 20/20] =?UTF-8?q?Remove=20TODO-python.md=20=E2=80=94=20the?= =?UTF-8?q?=20implementation=20plan=20is=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every step of the plan has been implemented and verified (PyO3 0.20->0.29, todo!() panic fixes, UniInput conversion helpers + broadened constructors, the global cache module, FBig/DBig/RBig arithmetic+comparison+bool, integer number theory / bit ops / in-place ops, float/rational predicates + transcendentals + cross-type conversions, the math module, full __format__ mini-language, complex CBig bindings, stubs, tests, and the guide page). Beyond the plan, native Python numbers are now accepted throughout, FBig prints in hex by default, and float 'e' defaults to native precision. The only plan item not implemented is Step 5d's full IBig bit-slice parity (slice __getitem__/__setitem__/__delitem__); IPy keeps single-bit read only, as in the original binding ("very limited capabilities") — bit-slicing a signed integer is ill-defined. The one remaining engineering follow-up (route bare FBig/CBig arithmetic through the Context layer so infinite-input / 0/0 raise instead of panicking) is already noted in CHANGELOG.md. Co-Authored-By: Claude --- python/TODO-python.md | 1621 ----------------------------------------- 1 file changed, 1621 deletions(-) delete mode 100644 python/TODO-python.md diff --git a/python/TODO-python.md b/python/TODO-python.md deleted file mode 100644 index c8420d65..00000000 --- a/python/TODO-python.md +++ /dev/null @@ -1,1621 +0,0 @@ -# Implementation Plan — dashu Python Package - -This document is the concrete implementation plan for the dashu Python bindings. Each section describes *how* to implement the feature, not just what's missing. - ---- - -## Architecture - -### Global constant cache + bare FBig/CBig - -**Decision: `FPy`/`DPy`/`CPy` wrap the *bare* `FBig`/`CBig`, and the module owns a single global `ConstCache`. Transcendentals borrow that cache and call the panic-free `Context` layer directly, mapping `FpError` to Python exceptions.** - -We deliberately do **not** wrap `CachedFBig`/`CachedCBig`. Those types bundle `Rc>` inside the value, which (a) makes the wrapper `!Send + !Sync`, forcing `#[pyclass(unsendable)]` and breaking free-threaded Python, and (b) hides the cache behind a `pub(crate)` field, so external code can't reach the panic-free `Context::sin(repr, cache)` path and is stuck with the panicking convenience methods. Putting the cache in the module (not the value) sidesteps both problems. - -| Python type | Rust inner type | Cache | Send? | -|---|---|---|---| -| `UPy` | `UBig` | — | yes | -| `IPy` | `IBig` | — | yes | -| `RPy` | `RBig` | — | yes | -| `FPy` | `FBig` | global | **yes** | -| `DPy` | `FBig` | global | **yes** | -| `CPy` | `CBig` | global | **yes** | - -All six wrappers are `Send + Sync`, so **none need `#[pyclass(unsendable)]`** and the binding is free-threaded-Python compatible. - -**Why a global cache:** every `sin`/`cos`/`exp`/`ln` recomputes π/ln2/ln10 from scratch (O(N²)) unless a `ConstCache` is threaded in. The cache is base-free and rounding-mode-free (just big-integer constants), so **one cache serves FPy (base 2), DPy (base 10), and CPy (complex) at once**, and accumulates precision across calls. Keeping it in the module means the *value* stays a plain, `Send`, operator-bearing `FBig`/`CBig`. - -**Backing store:** `thread_local! { static CONST_CACHE: RefCell }`, initialized with `ConstCache::new()` (a `pub const fn` on a `Send + Sync` struct). Thread-local = zero locking, zero contention; under free-threaded Python each thread gets its own cache and recomputes constants once. A `with_cache(|c| ...)` helper borrows it mutably for one call. - -**Error mapping (replaces the panicking `Context::unwrap_fp`):** the `Context` layer returns `FpResult>` (= `Result`). We map it ourselves: -- `Err(Overflow(sign))` → `Repr::infinity_with_sign(sign)` (graceful, matches Python float) -- `Err(Underflow(sign))` → `Repr::zero_with_sign(sign)` (graceful) -- `Err(InfiniteInput)` → `ValueError("arithmetic with infinity")` -- `Err(OutOfDomain)` → `ValueError("math domain error")` (matches `math.sqrt(-1)`) -- `Err(Indeterminate)` → `ZeroDivisionError` (this is `0/0`) - -This is *strictly better* than the convenience layer, which `panic!`s on the last three and surfaces in Python as `pyo3_runtime.PanicException` (a `BaseException` subclass that escapes `except Exception:` and crashes the session). - -**Scope of the fix:** this fully fixes **transcendentals** (`sin`, `exp`, `ln`, `sqrt`, `powf`, trig/hyperbolic — all take `cache: Option<&mut ConstCache>` at the `Context` layer). **Arithmetic** operators on bare `FBig`/`CBig` still `panic!` on infinite input (e.g. `exp(1000) + 1`, since `exp(1000)→+∞` then `+` asserts finite) and on `0/0`. Those are rarer than transcendental domain errors, so arithmetic keeps the operator impls for the MVP; routing it through the `Context` layer (or guarding) is a follow-up. - -### Key patterns to follow - -**UniInput dispatch** — `python/src/convert.rs`: The `UniInput` enum has 11 variants covering all Python numeric types (Uint, Int, BUint, BInt, OBInt, Float, BFloat, BDecimal, OBDecimal, BRational, OBRational). Its `FromPyObject` impl already extracts any Python number into the right variant. We add conversion methods (`into_fpy`, `into_dpy`, `into_rpy`, `into_cpy`) that collapse any variant into the target dashu type, making arithmetic functions simple one-liners. - -**Comparison** — `num_order::NumOrd` trait provides cross-type comparison for all dashu types: FBig↔UBig, FBig↔IBig, FBig↔RBig, FBig↔f64, RBig↔UBig, RBig↔IBig, etc. The `__richcmp__` pattern is already established in UPy/IPy: `op.matches(self.0.num_cmp(&rhs))`. - -**Arithmetic macros** — The existing `impl_binops!` macro in `int.rs` generates forward/reverse dispatch functions by matching each `UniInput` variant. For FPy/DPy/RPy/CPy we create simpler versions that first convert the operand to the target type via `into_fpy()`/etc, then call the Rust operator (bare FBig/CBig keep all operator trait impls). - -**Transcendental dispatch** — transcendentals do NOT use the convenience methods; they call `self.0.context().sin(self.0.repr(), cache)` through `with_cache`, then map the `FpResult`. This is the single most important pattern in the binding — it's what gives clean Python exceptions instead of panics. - -### Rust API facts (verified) - -``` -ConstCache (dashu_float::ConstCache; Send + Sync; pub const fn new()) - Owns memoized pi/ln2/ln10 binary-splitting state. Base-free, rounding-free. - One instance serves FPy (base 2), DPy (base 10), CPy (complex) simultaneously. - pub fn clear(&mut self), pub fn total_terms(&self), pub fn total_words(&self). - -FBig (the value FPy/DPy wrap; Send + Sync) - Public accessors: .repr() -> &Repr, .context() -> Context (Copy), .into_repr() -> Repr. - Context layer (panic-free, returns FpResult>): - .add/.sub/.mul/.div/.inv/.sqrt/.cbrt/.nth_root/.powi/.hypot -- no cache param - .exp/.exp_m1/.ln/.ln_1p/.sin/.cos/.tan/.asin/.acos/.atan/.atan2/.powf/.sinh/.cosh/.tanh/.asinh/.acosh/.atanh - -- all take cache: Option<&mut ConstCache> - .pi(precision, cache) -> Rounded - Convenience layer (FBig::sin etc.) -- PANICS on InfiniteInput/OutOfDomain/Indeterminate; DO NOT USE in bindings. - -CBig (the value CPy wraps; Send + Sync) - Public accessors: .re() -> &Repr, .im() -> &Repr, .context() -> dashu_cmplx::Context (Copy). - Complex Context layer (panic-free, returns CfpResult): - .sqrt/.powi -- no cache param - .ln/.exp/.sin/.cos/.tan/.asin/.acos/.atan/.powf -- all take cache: Option<&mut ConstCache> - .abs/.arg/.norm -- return real FBig (abs via hypot; arg threads cache). - Convenience layer (CBig::sin etc.) -- PANICS; DO NOT USE in bindings. - -RBig methods (uncached): - .numerator() -> &IBig, .denominator() -> &UBig - .trunc() -> IBig, .floor() -> IBig, .ceil() -> IBig, .round() -> IBig (NOT RBig!) - .fract() -> RBig - .split_at_point() -> (IBig, RBig) - .sqr(), .cubic(), .pow(n: usize) - .is_int(), .sign() -> Sign, .signum() -> RBig - -UBig methods (uncached): - .sqrt() -> UBig, .cbrt() -> UBig, .nth_root(n) -> UBig - .sqr() -> UBig, .cubic() -> UBig - .ilog(&UBig) -> usize - .count_ones() -> usize, .count_zeros() -> Option - .trailing_zeros() -> Option, .trailing_ones() -> Option - .is_power_of_two() -> bool, .next_power_of_two() -> UBig - .is_multiple_of(&UBig) -> bool, .remove(&UBig) -> Option - -IBig methods (uncached): - .sqrt() -> UBig (always unsigned!), .cbrt() -> IBig, .nth_root(n) -> IBig - .sqr() -> UBig (always unsigned!), .cubic() -> IBig - .ilog(&UBig) -> usize (takes &UBig, not &IBig!) - .trailing_zeros() -> Option, .trailing_ones() -> Option - .sign() -> Sign, .signum() -> IBig - .into_parts() -> (Sign, UBig) - IBig::from_parts(Sign, UBig) -> IBig - -Traits (need `use` imports): - dashu_base::ring::Gcd::gcd(a, b) -- UBig.gcd(UBig) -> UBig - dashu_base::ring::ExtendedGcd::gcd_ext(a, b) -> (UBig, IBig, IBig) - dashu_base::DivEuclid::div_euclid(a, b), DivRemEuclid::div_rem_euclid(a, b) - -IBig::is_negative()/is_positive() -- NOT inherent! Use self.sign() == Sign::Negative instead. - -Conversions (int/float/rational — bare FBig, since FPy/DPy wrap bare FBig): - FBig::try_from(f64) -- base 2 ONLY (FPy works; DPy does not) - FBig::from(UBig), FBig::from(IBig) -- infallible, any base - FBig::try_from(RBig) -- exact only, returns Err(ConversionError) on precision loss - RBig::try_from(FBig) -- exact only - RBig::try_from(f64) -- exact only (f64 has limited precision anyway) - DPy from f64 -- go through string: format!("{:e}", x) then FBig::from_str - -Approximation::value() extracts T from both Exact(T) and Inexact(T, E) variants. -``` - ---- - -## Step 0: Upgrade PyO3 from 0.20 to 0.29 - -**Files: `python/Cargo.toml`, `python/pyproject.toml`** - -### Rationale - -PyO3 0.20 uses the old `gil-refs` API (`&PyAny`, `&PyList`, `.into_py(py)`). Version 0.23 introduced the modern `Bound` API and `IntoPyObject`, and 0.29 is the current latest. The MSRV constraint does **not** apply to `dashu-python` (per AGENTS.md — only core crates are bounded by MSRV). - -### 0a: Update Cargo.toml — dependencies and metadata - -```toml -[dependencies] -# Add complex numbers support: -dashu-cmplx = { version = "0.4.5", path = "../complex", features = ["std", "num-order"] } -``` - -```toml -[dependencies.pyo3] -# Change: -version = "0.20" -features = ["extension-module"] -# To: -version = "0.29" -# "extension-module" feature is deprecated in 0.28+ and removed in 0.29. -# maturin handles linking automatically. -``` - -Also bump `rust-version` and `edition`: -```toml -# Change: -rust-version = "1.68" -edition = "2021" -# To: -rust-version = "1.85" -edition = "2024" -``` - -Remove `categories = ["mathematics", "no-std"]` — `no-std` is misleading for a Python binding. Replace with `categories = ["mathematics", "science"]`. - -### 0b: Update pyproject.toml - -PyO3 0.26+ dropped Python 3.7 support: -```toml -# Change: -requires-python = ">=3.7" -# To: -requires-python = ">=3.8" -``` - -### 0c: `#[pymodule]` signature — `lib.rs` - -```rust -// Before (0.20): -fn dashu(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - -// After (0.29): -fn dashu(m: &Bound<'_, PyModule>) -> PyResult<()> { -``` - -Remove the now-unused `_py: Python<'_>` parameter. - -### 0d: `#[pyclass]` enum — `types.rs` - -PyO3 0.22+ requires explicit `eq` for cross-type enum comparison: - -```rust -// Before: -#[pyclass] -pub enum PySign { Positive, Negative } - -// After: -#[pyclass(eq, eq_int)] -#[derive(PartialEq, Clone, Copy)] -pub enum PySign { Positive, Negative } -``` - -### 0d2: Wrapper types store bare FBig/CBig (all Send) — `types.rs` - -Per the Architecture decision, `FPy`/`DPy`/`CPy` wrap the **bare** `FBig`/`CBig` (not the cached variants). The constant cache lives in the module (Step 2c), not in the value. All wrappers are `Send + Sync`, so **none use `unsendable`**. - -```rust -// types.rs — final shape (after Steps 0d2 + 11): -type FBig2 = dashu_float::FBig; -type DBig10 = dashu_float::FBig; -type CBig2 = dashu_cmplx::CBig; - -#[pyclass(name = "FBig")] pub struct FPy(pub FBig2); -#[pyclass(name = "DBig")] pub struct DPy(pub DBig10); -#[pyclass(name = "CBig")] pub struct CPy(pub CBig2); // Step 11 - -#[pyclass(name = "UBig")] pub struct UPy(pub dashu_int::UBig); // unchanged -#[pyclass(name = "IBig")] pub struct IPy(pub dashu_int::IBig); // unchanged -#[pyclass(name = "RBig")] pub struct RPy(pub dashu_ratio::RBig); // unchanged -``` - -The `UniInput` enum's `BFloat`/`BDecimal` variants keep field names `BFloat(PyRef<'a, FPy>)` etc. — only the inner type behind `FPy` changes (from cached to bare). Add `BComplex(PyRef<'a, CPy>)` for Step 11. - -The `UniInput` enum's `BFloat`/`BDecimal` variants change their `PyRef` target types to the new cached wrappers (the field names `BFloat(PyRef<'a, FPy>)` etc. stay the same; only the inner type behind `FPy` changes). Add `BComplex(PyRef<'a, CPy>)` for Step 11. - -### 0e: `FromPyObject` for `UniInput` — `convert.rs` - -PyO3 0.27 restructured `FromPyObject` with dual lifetimes and `Borrowed`: - -```rust -// Before (0.20): -impl<'source> FromPyObject<'source> for UniInput<'source> { - fn extract(ob: &'source PyAny) -> PyResult { - -// After (0.29): -impl<'a, 'py> FromPyObject<'a, 'py> for UniInput<'a> { - type Error = PyErr; - fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult { -``` - -`Borrowed<'a, 'py, PyAny>` derefs to `&Bound<'py, PyAny>`, so `ob.is_instance_of::()`, `ob.py()`, etc. all work as before. - -**Extracting `PyRef` from `Borrowed`:** The pattern changes from: -```rust -// 0.20: - as FromPyObject>::extract(ob) -``` -To (0.29): -```rust -ob.extract::>() -``` - -### 0f: `.cast()` replaces `.downcast()` — `int.rs`, `words.rs` - -PyO3 0.27 renamed downcast methods: - -```rust -// Before (0.20): -index.downcast::()? -// After (0.29): -index.cast::()? -``` - -Also: `PySlice::indices()` now expects `isize` instead of `i32` for the length parameter in some contexts. The current code passes `.try_into().map_err(...)` — this should be reviewed. - -### 0g: `.into_py(py)` → `.into_pyobject(py)` — all files - -PyO3 0.23 replaced `IntoPy` with `IntoPyObject`. The new trait returns `PyResult`: - -```rust -// Before (0.20): -UPy(value).into_py(py) -// After (0.29): -UPy(value).into_pyobject(py) -// Returns PyResult> -// Use .unbind() to get Py (= PyObject): -UPy(value).into_pyobject(py).map(|b| b.unbind()) -``` - -For functions that currently return `PyObject`, convert to `PyResult` and use: - -```rust -// Before: -fn upy_add(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyObject { - // ... - UPy(result).into_py(py) -} -// After: -fn upy_add(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyResult { - // ... - Ok(UPy(result).into_py_any(py)?) -} -``` - -`into_py_any(py)` is shorthand for `.into_pyobject(py).map(|b| b.unbind())` — it returns `PyResult>` (= `PyResult`). - -For `__repr__`, `__str__` returning `String` — no change needed. For `__hash__` returning `u64` — no change needed. PyO3 handles those primitive types natively. - -### 0h: `intern!` macro - -The macro returns `&Bound<'py, PyString>`. Usage like `py.import(intern!(py, "decimal"))?` works transparently (Bound derefs). No code changes needed. - -### 0i: `wrap_pyfunction!` - -No change needed — stable across 0.20→0.29. - -### Summary of API changes (0.20 → 0.29) - -| Pattern | 0.20 | 0.29 | -|---------|------|------| -| `#[pymodule]` | `fn(py, m: &PyModule)` | `fn(m: &Bound)` | -| `FromPyObject` | `extract(ob: &'source PyAny)` | `extract(ob: Borrowed<'a, 'py, PyAny>)` with `type Error` | -| `.downcast::()` | present | `.cast::()` | -| Value→Python | `.into_py(py)` | `.into_py_any(py)?` or `.into_pyobject(py)` | -| `#[pyclass]` enum | `#[pyclass]` | `#[pyclass(eq, eq_int)]` | -| `extension-module` | required in features | removed (maturin handles it) | -| rust-version | 1.68 | 1.83 (PyO3 0.29 MSRV) | -| requires-python | ≥3.7 | ≥3.8 (Python 3.7 dropped in 0.26+) | - ---- - -## Step 1: Fix `todo!()` panics in existing code - -**File: `python/src/int.rs`** - -### 1a: Fix `upy_mod` and `ipy_mod` - -Both `upy_mod` (line 140) and `ipy_mod` (line 150) have `_ => todo!()` for the float/rational variants. Fill the missing arms by converting to the corresponding float/rational type: - -```rust -fn upy_mod(lhs: &UPy, rhs: UniInput<'_>, py: Python) -> PyResult { - let result: Py = match rhs { - UniInput::Uint(x) => UPy((&lhs.0).rem(x).into()).into_py(py), - UniInput::Int(x) => UPy((&lhs.0).rem(IBig::from(x))).into_py(py), - UniInput::BUint(x) => UPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::BInt(x) => UPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::OBInt(x) => UPy((&lhs.0).rem(x)).into_py(py), - UniInput::Float(x) => FPy((&lhs.0).rem(FBig::try_from(x).unwrap())).into_py(py), - UniInput::BFloat(x) => FPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::BDecimal(x) => DPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::OBDecimal(x) => DPy((&lhs.0).rem(x)).into_py(py), - UniInput::BRational(x) => RPy((&lhs.0).rem(&x.0)).into_py(py), - UniInput::OBRational(x) => RPy((&lhs.0).rem(x)).into_py(py), - }; - Ok(result) -} -``` - -Same pattern for `ipy_mod`. Change return type from `PyObject` to `PyResult` and update the `__mod__` wrappers at lines 493 and 655 to propagate with `?`. - -### 1b: Fix `ipy_pow` todo!() branches - -Replace the three `_ => todo!()` arms with proper error returns: - -```rust -// For modulus parameter (line 174): -_ => return Err(PyTypeError::new_err("modulus must be an integer")), - -// For exponent in modulus branch (line 186): -_ => return Err(PyTypeError::new_err("modular exponentiation requires an integer exponent")), - -// For exponent in non-modulus branch (line 191): -_ => return Err(PyTypeError::new_err("integer power requires a non-negative integer exponent")), -``` - ---- - -## Step 2: Add `UniInput` conversion helpers - -**File: `python/src/convert.rs`** - -Add four methods to `impl<'a> UniInput<'a>` that convert any numeric variant to the target dashu type. Since `FPy`/`DPy`/`CPy` wrap **bare** `FBig`/`CBig`, these are the plain `FBig`/`CBig` conversions (no cache is attached — the cache lives in the module, see Step 2c). - -### `into_fpy(self) -> PyResult` - -`FPy` wraps `FBig`. - -| Input variant | Conversion | -|---|---| -| `Uint(x)` | `FBig::from(x)` | -| `Int(x)` | `FBig::from(IBig::from(x))` | -| `BUint(x)` | `FBig::from(x.0.clone())` | -| `BInt(x)` | `FBig::from(x.0.clone())` | -| `OBInt(x)` | `FBig::from(x)` | -| `Float(x)` | `FBig::try_from(x)` — map error to PyValueError | -| `BFloat(x)` | `x.0.clone()` (already a `FPy` = bare FBig) | -| `BDecimal/OBDecimal` | `Err(PyTypeError("decimal cannot be mixed with binary float; convert explicitly"))` | -| `BRational(x)` | `FBig::try_from(x.0.clone())` — map ConversionError to PyTypeError | -| `OBRational(x)` | `FBig::try_from(x)` — same | - -### `into_dpy(self) -> PyResult` - -`DPy` wraps `FBig`. For `Float(x)` use `format!("{:e}", x)` → `FBig::from_str` (no direct `TryFrom` for base 10). For `BFloat` return type error. For `BRational`/`OBRational` use `FBig::try_from` (DBig is `FBig`). - -### `into_rpy(self) -> PyResult` - -`RPy` wraps `RBig` (no transcendental ops). - -| Input variant | Conversion | -|---|---| -| `Uint(x)` | `RBig::from(x)` | -| `Int(x)` | `RBig::from(IBig::from(x))` | -| `BUint(x)` | `RBig::from(x.0.clone())` | -| `BInt(x)` | `RBig::from(x.0.clone())` | -| `OBInt(x)` | `RBig::from(x)` | -| `Float(x)` | `RBig::try_from(x)` — map error to PyValueError | -| `BFloat(x)` | `RBig::try_from(x.0.clone())` | -| `BDecimal(x)` | `RBig::try_from(x.0.clone())` | -| `OBDecimal(x)` | `RBig::try_from(x)` | -| `BRational(x)` | `x.0.clone()` | -| `OBRational(x)` | `x` | - -### `into_cpy(self) -> PyResult` (Step 11) - -`CPy` wraps bare `CBig`. `CBig::From` and `From` exist; ints/floats embed as `z + 0i`. `BFloat`/`BDecimal` convert via their inner FBig (`x.0.clone()` then `CBig::from`). Cross-base (FPy↔DPy) and rational→complex raise type errors for the MVP. - -Add imports at top of `convert.rs`: -```rust -use crate::types::{FPy, CPy}; -use dashu_float::FBig; -use dashu_cmplx::CBig; -use dashu_float::round::mode; -use dashu_base::ConversionError; -use std::str::FromStr; -``` - ---- - -## Step 2b: Broaden `__new__` to accept any Python number - -**Problem:** today `FBig(...)`, `DBig(...)`, `RBig(...)` only accept their "native" Python type or a string. `FBig(7)`, `FBig(Decimal("1.5"))`, `FBig(Fraction(1,3))`, `DBig(1.5)`, `RBig(1.5)` all fail at construction. The `into_*` helpers (Step 2) fixed the *arithmetic* case but not construction. Workaround today is `auto(x)`, but the type constructors should be permissive too. - -**Design:** keep arithmetic dispatch strict (matches Python — `1.5 + Decimal("0.5")` raises `TypeError`), but make `__new__` permissive by giving it its own conversion path. Add permissive `construct_*` helpers to `UniInput` (or as free functions in `convert.rs`) that differ from `into_*` only in the cross-type rules: - -```rust -impl<'a> UniInput<'a> { - /// Permissive construction of FBig from any Python number (for __new__). - /// Unlike into_fpy, this ACCEPTS Decimal/Fraction by routing through string / RBig. - pub fn construct_fbig(self) -> PyResult { - match self { - Self::Uint(x) => Ok(FBig::from(x)), - Self::Int(x) => Ok(FBig::from(IBig::from(x))), - Self::BUint(x) => Ok(FBig::from(x.0.clone())), - Self::BInt(x) => Ok(FBig::from(x.0.clone())), - Self::OBInt(x) => Ok(FBig::from(x)), - Self::Float(x) => FBig::try_from(x).map_err(conversion_error_to_py), - Self::BFloat(x) => Ok(x.0.clone()), - // Decimal/Fraction -> round-trip through string (Decimal.__format__ gives - // scientific notation; Fraction via RBig::try_from is exact-or-error). - Self::BDecimal(x) | Self::OBDecimal(_) => { - let s = decimal_to_str(&self)?; // helper: format Decimal as decimal string - FBig::from_str(&s).map_err(parse_error_to_py) - } - Self::BRational(x) => FBig::try_from(x.0.clone()).map_err(conversion_error_to_py), - Self::OBRational(x) => FBig::try_from(x).map_err(conversion_error_to_py), - } - } - // construct_dbig, construct_rbig, construct_cbig: same permissive pattern -} -``` - -Then each `__new__` becomes (FPy example): - -```rust -#[new] -fn __new__(ob: &PyAny, radix: Option) -> PyResult { - if let Ok(s) = ob.extract::() { - // string with optional radix (existing path) - let f = match radix { - Some(r) => FBig::from_str_radix(s, r), - None => FBig::from_str(s), - }; - return Ok(FPy(f.map_err(parse_error_to_py)?)); - } - if radix.is_some() { - return Err(PyTypeError::new_err("radix only valid with string input")); - } - if let Ok(other) = as FromPyObject>::extract(ob) { - return Ok(FPy(other.0.clone())); // copy - } - // any other Python number -> permissive construction - Ok(FPy(UniInput::extract(ob)?.construct_fbig()?)) -} -``` - -**Per-type construction matrix** (what each `__new__` accepts after this step): - -| `__new__` accepts | `FBig` | `DBig` | `RBig` | `CBig` | -|---|---|---|---|---| -| `str` (± radix) | ✅ | ✅ | ✅ | ✅ (`"a+bi"`) | -| same-type copy | ✅ | ✅ | ✅ | ✅ | -| `int` | ✅ `FBig::from` | ✅ `DBig::from` | ✅ `RBig::from` | ✅ `CBig::from` (z+0i) | -| `float` | ✅ `try_from` | ✅ via `{:e}` str | ✅ `try_from` (exact) | ✅ re+im | -| `Decimal` | ✅ via str | ✅ `parse_to_dbig` | ✅ via RBig | via FBig | -| `Fraction` | ✅ `try_from` (exact) | ✅ `try_from` (exact) | ✅ `parse_to_rbig` | via FBig | -| `(re, im)` tuple | — | — | `(num, den)` → from_parts | ✅ `from_parts` | -| `complex` | — | — | — | ✅ | - -**Note on exact-vs-lossy:** `Decimal`/`Fraction` → `FBig`/`DBig` via `try_from` is **exact-only** (errors on precision loss, e.g. `1/3` → base 2). For lossy correctly-rounded conversion, users should call the explicit `to_float()` method (Step 6c). The constructor stays strict to avoid silent precision loss — consistent with how `int(1.5)` is explicit in Python. - -**`decimal_to_str` helper:** format a `decimal.Decimal` as a plain decimal string. Reuse the existing `parse_to_dbig` round-trip logic in reverse (Decimal → `__format__` → string). This is the same path `DPy::unwrap` already uses. - ---- - -## Step 2c: Global constant cache — new file `python/src/cache.rs` - -This module owns the single `ConstCache` shared by all float/decimal/complex transcendentals, and provides the `FpError → PyErr` mapping that replaces the panicking `Context::unwrap_fp`. - -```rust -// python/src/cache.rs -use std::cell::RefCell; -use dashu_float::{ConstCache, Repr, Sign}; -use dashu_float::error::{FpError, Rounding}; -use pyo3::exceptions::{PyValueError, PyZeroDivisionError}; -use pyo3::PyErr; - -thread_local! { - /// One cache per thread — zero locking, zero contention. Base-free, so it serves - /// FPy (base 2), DPy (base 10), and CPy (complex) simultaneously. Accumulates - /// precision across calls; never needs explicit invalidation. - static CONST_CACHE: RefCell = RefCell::new(ConstCache::new()); -} - -/// Borrow the thread-local cache mutably for one transcendental call. -pub fn with_cache(f: impl FnOnce(&mut ConstCache) -> R) -> R { - CONST_CACHE.with(|c| f(&mut c.borrow_mut())) -} - -/// Optional Python-facing handle for cache inspection/clearing (see Step 2c-bottom). -#[pyclass(name = "Cache")] -pub struct PyCache; -#[pymethods] -impl PyCache { - /// Drop all memoized constants (rarely needed — the cache only grows usefully). - #[staticmethod] - fn clear() { with_cache(|c| c.clear()); } - #[staticmethod] - fn total_terms() -> usize { with_cache(|c| c.total_terms()) } - #[staticmethod] - fn total_words() -> usize { with_cache(|c| c.total_words()) } -} - -/// Map a float `FpResult>` to a `PyResult`: -/// Overflow/Underflow → signed ∞/0 (graceful, like Python float); -/// InfiniteInput/OutOfDomain → ValueError; Indeterminate → ZeroDivisionError. -/// `val` extracts the FBig from the Rounded wrapper (Exact|Inexact). -pub fn unwrap_float( - res: dashu_float::FpResult>, -) -> PyResult> -where - usize: dashu_float::Base, -{ - use dashu_base::Approximation; - match res { - Ok(rounded) => Ok(rounded.value()), - Err(FpError::Overflow(sign)) => Ok(dashu_float::FBig::from_repr( - Repr::infinity_with_sign(sign), /* context */ unreachable!())), - // NOTE: the overflow/underflow branches need the value's context to rebuild - // an FBig; in practice build them via Repr + the caller's context — see the - // per-call helpers in float.rs/complex.rs which receive `self.0.context()`. - Err(FpError::Underflow(sign)) => /* signed zero */ todo!(), - Err(FpError::InfiniteInput) => Err(PyValueError::new_err("arithmetic with infinity")), - Err(FpError::OutOfDomain) => Err(PyValueError::new_err("math domain error")), - Err(FpError::Indeterminate) => Err(PyZeroDivisionError::new_err("indeterminate form (0/0)")), - } -} -``` - -**Rebuilding ∞/0 with the right context:** `FBig::from_repr` needs a `Context`. Since each call site has `self.0.context()`, the cleanest shape is a *context-aware* mapper `unwrap_float(res, ctx) -> PyResult` rather than the simplified one above. Concretely, the transcendental methods in `float.rs` look like: - -```rust -fn sin(&self) -> PyResult { - let ctx = self.0.context(); - let res = with_cache(|c| ctx.sin(self.0.repr(), Some(c))); // FpResult> - Ok(FPy(unwrap_float(res, ctx)?)) -} -``` - -For **complex**, the parallel `unwrap_complex(res, ctx) -> PyResult` maps the same `FpError` variants but rebuilds `CBig::overflow(ctx, sign)` / `CBig::underflow(ctx, sign)` on Overflow/Underflow (these constructors exist on CBig — see `complex/src/repr.rs`). - -**Register in `lib.rs`:** `m.add_class::()?;` so Python users can call `dashu.Cache.clear()`. - -### Why this design - -- **Panic-free transcendentals:** every `sin`/`cos`/`exp`/`ln`/`sqrt`/`powf`/trig/hyperbolic goes through `Context::(repr, cache) -> FpResult`, never the panicking convenience method. -- **Caching preserved:** `with_cache` threads the *same* growing cache into every call, exactly like `CachedFBig` would — but without `Rc>` baked into the value. -- **`Send` preserved:** `FPy`/`DPy`/`CPy` hold bare `FBig`/`CBig`, so no `#[pyclass(unsendable)]`, and the binding works under free-threaded Python (each thread gets its own thread-local cache). - ---- - -## Step 3: Add arithmetic operators to FPy and DPy - -**File: `python/src/float.rs`** - -### 3a: Comparison - -Add `__richcmp__` using the same pattern as UPy/IPy (int.rs lines 248–263): - -```rust -use num_order::NumOrd; -use pyo3::basic::CompareOp; - -#[pymethods] -impl FPy { - fn __richcmp__(&self, other: UniInput<'_>, op: CompareOp) -> bool { - let order = match other { - UniInput::Uint(x) => self.0.num_cmp(&x), - UniInput::Int(x) => self.0.num_cmp(&x), - UniInput::BUint(x) => self.0.num_cmp(&x.0), - UniInput::BInt(x) => self.0.num_cmp(&x.0), - UniInput::OBInt(x) => self.0.num_cmp(&x), - UniInput::Float(x) => self.0.num_cmp(&x), - UniInput::BFloat(x) => self.0.cmp(&x.0), - UniInput::BDecimal(x) => self.0.num_cmp(&x.0), - UniInput::OBDecimal(x) => self.0.num_cmp(&x), - UniInput::BRational(x) => self.0.num_cmp(&x.0), - UniInput::OBRational(x) => self.0.num_cmp(&x), - }; - op.matches(order) - } -} -``` - -Same for DPy (DBig is FBig so NumOrd works identically). - -### 3b: Bool - -```rust -fn __bool__(&self) -> bool { - !self.0.repr().is_zero() -} -``` - -### 3c: Arithmetic macro - -Create a helper macro that uses `into_fpy()` for forward/reverse dispatch. -**Note:** PyO3 0.24 uses `.into_py_any(py)?` instead of `.into_py(py)`: - -```rust -macro_rules! impl_fpy_binops { - // Commutative (add, mul) — __radd__ and __rmul__ reuse the forward function - ($method:ident, $rs_method:ident) => { - fn $method(lhs: &FPy, rhs: UniInput<'_>, py: Python) -> PyResult { - let rhs = rhs.into_fpy()?; - Ok(FPy((&lhs.0).$rs_method(&rhs.0)).into_py_any(py)?) - } - }; - // Non-commutative (sub, div, rem) — also generate reverse function - ($method:ident, $rev_method:ident, $rs_method:ident) => { - impl_fpy_binops!($method, $rs_method); - fn $rev_method(lhs: UniInput<'_>, rhs: &FPy, py: Python) -> PyResult { - let lhs = lhs.into_fpy()?; - Ok(FPy(lhs.0.$rs_method(&rhs.0)).into_py_any(py)?) - } - }; -} - -impl_fpy_binops!(fpy_add, add); -impl_fpy_binops!(fpy_mul, mul); -impl_fpy_binops!(fpy_sub, fpy_rsub, sub); -impl_fpy_binops!(fpy_div, fpy_rdiv, div); -impl_fpy_binops!(fpy_mod, fpy_rmod, rem); -``` - -Wire in `#[pymethods] impl FPy`: - -```rust -fn __add__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_add(self, other, py) } -fn __radd__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_add(self, other, py) } -fn __sub__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_sub(self, other, py) } -fn __rsub__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_rsub(other, self, py) } -fn __mul__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_mul(self, other, py) } -fn __rmul__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_mul(self, other, py) } -fn __truediv__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_div(self, other, py) } -fn __rtruediv__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_rdiv(other, self, py) } -fn __mod__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_mod(self, other, py) } -fn __rmod__(&self, other: UniInput<'_>, py: Python) -> PyResult { fpy_rmod(other, self, py) } -fn __neg__(&self) -> FPy { FPy(-&self.0) } -fn __pos__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } -fn __abs__(&self) -> FPy { FPy(self.0.abs()) } -``` - -Same pattern for DPy: create an identical macro using `into_dpy()` instead of `into_fpy()`. - -### 3d: Imports to add at top of `float.rs` - -```rust -use num_order::NumOrd; -use pyo3::basic::CompareOp; -use crate::convert::parse_error_to_py; -``` - ---- - -## Step 4: Add arithmetic operators to RPy - -**File: `python/src/ratio.rs`** - -Same pattern as Step 3c but using `into_rpy()`: - -```rust -macro_rules! impl_rpy_binops { - ($method:ident, $rs_method:ident) => { - fn $method(lhs: &RPy, rhs: UniInput<'_>, py: Python) -> PyResult { - let rhs = rhs.into_rpy()?; - Ok(RPy((&lhs.0).$rs_method(&rhs.0)).into_py_any(py)?) - } - }; - ($method:ident, $rev_method:ident, $rs_method:ident) => { - impl_rpy_binops!($method, $rs_method); - fn $rev_method(lhs: UniInput<'_>, rhs: &RPy, py: Python) -> PyResult { - let lhs = lhs.into_rpy()?; - Ok(RPy(lhs.0.$rs_method(&rhs.0)).into_py_any(py)?) - } - }; -} - -impl_rpy_binops!(rpy_add, add); -impl_rpy_binops!(rpy_mul, mul); -impl_rpy_binops!(rpy_sub, rpy_rsub, sub); -impl_rpy_binops!(rpy_div, rpy_rdiv, div); -impl_rpy_binops!(rpy_mod, rpy_rmod, rem); -``` - -Wire `__add__`/`__radd__`/`__sub__`/`__rsub__`/`__mul__`/`__rmul__`/`__truediv__`/`__rtruediv__`/`__mod__`/`__rmod__`/`__neg__`/`__pos__`/`__abs__`. - -Add `__richcmp__` (same pattern as FPy but using RBig's NumOrd impls) and `__bool__`. - -Add imports: -```rust -use num_order::NumOrd; -use pyo3::basic::CompareOp; -use std::ops::{Add, Sub, Mul, Div, Rem}; -``` - ---- - -## Step 5: Complete integer type methods - -**File: `python/src/int.rs`** - -### 5a: Add number theory + bit ops to UPy - -Add to `#[pymethods] impl UPy`: - -```rust -// Number theory -fn sqrt(&self) -> Self { UPy(self.0.sqrt()) } -fn cbrt(&self) -> Self { UPy(self.0.cbrt()) } -fn nth_root(&self, n: usize) -> Self { UPy(self.0.nth_root(n)) } -fn sqr(&self) -> Self { UPy(self.0.sqr()) } -fn cubic(&self) -> Self { UPy(self.0.cubic()) } -fn ilog(&self, base: &Self) -> PyResult { - if base.0 <= UBig::ONE { - return Err(PyValueError::new_err("base must be > 1")); - } - Ok(self.0.ilog(&base.0)) -} -fn is_multiple_of(&self, divisor: &Self) -> bool { self.0.is_multiple_of(&divisor.0) } -fn remove(&mut self, factor: &Self) -> PyResult { - self.0.remove(&factor.0).ok_or_else(|| PyValueError::new_err("factor does not divide this number")) -} -fn gcd(&self, other: &Self) -> Self { - use dashu_base::ring::Gcd; - UPy(Gcd::gcd(&self.0, &other.0)) -} -fn gcd_ext(&self, other: &Self) -> (Self, IPy, IPy) { - use dashu_base::ring::ExtendedGcd; - let (g, s, t) = ExtendedGcd::gcd_ext(&self.0, &other.0); - (UPy(g), IPy(s), IPy(t)) -} - -// Bit operations -fn count_ones(&self) -> usize { self.0.count_ones() } -fn count_zeros(&self) -> Option { self.0.count_zeros() } -fn trailing_zeros(&self) -> Option { self.0.trailing_zeros() } -fn trailing_ones(&self) -> Option { self.0.trailing_ones() } -fn is_power_of_two(&self) -> bool { self.0.is_power_of_two() } -fn next_power_of_two(slf: PyRef<'_, Self>) -> Self { UPy(slf.0.clone().next_power_of_two()) } - -// Accessors -fn is_one(&self) -> bool { self.0.is_one() } -#[staticmethod] -fn ones(n: usize) -> Self { UPy(UBig::ones(n)) } -``` - -### 5b: Add missing operators to UPy - -```rust -// Floor division -fn __floordiv__(&self, other: UniInput<'_>, py: Python) -> PyResult { - use dashu_base::ring::DivEuclid; - match other { - UniInput::Uint(x) => Ok(UPy(self.0.div_euclid(&UBig::from(x))).into_py(py)), - UniInput::BUint(x) => Ok(UPy(self.0.div_euclid(&x.0)).into_py(py)), - UniInput::Int(x) => Ok(IPy(self.0.as_ibig().clone().div_euclid(IBig::from(x))).into_py(py)), - UniInput::BInt(x) => Ok(IPy(self.0.as_ibig().clone().div_euclid(x.0.clone())).into_py(py)), - UniInput::OBInt(x) => Ok(IPy(self.0.as_ibig().clone().div_euclid(x)).into_py(py)), - _ => Err(PyTypeError::new_err("floor division requires an integer divisor")), - } -} -fn __rfloordiv__(&self, other: UniInput<'_>, py: Python) -> PyResult { - // other // self — reverse dispatch - self.__floordiv__(other, py) // reuse for now; will be handled correctly via radd pattern - // NOTE: to do this properly, need a reverse dispatch like rsub. - // For MVP, raise NotImplementedError for complex cases - todo!("proper __rfloordiv__ dispatch") -} - -// Divmod -fn __divmod__(&self, other: UniInput<'_>, py: Python) -> PyResult { - use dashu_base::ring::DivRemEuclid; - match other { - UniInput::Uint(x) => { - let (q, r) = self.0.div_rem_euclid(&UBig::from(x)); - Ok((UPy(q), UPy(r)).into_py(py)) - } - UniInput::BUint(x) => { - let (q, r) = self.0.div_rem_euclid(&x.0); - Ok((UPy(q), UPy(r)).into_py(py)) - } - UniInput::Int(x) => { - let (q, r) = self.0.as_ibig().clone().div_rem_euclid(IBig::from(x)); - Ok((IPy(q), UPy(r)).into_py(py)) - } - UniInput::BInt(x) => { - let (q, r) = self.0.as_ibig().clone().div_rem_euclid(x.0.clone()); - Ok((IPy(q), UPy(r)).into_py(py)) - } - UniInput::OBInt(x) => { - let (q, r) = self.0.as_ibig().clone().div_rem_euclid(x); - Ok((IPy(q), UPy(r)).into_py(py)) - } - _ => Err(PyTypeError::new_err("divmod requires an integer divisor")), - } -} -``` - -### 5c: Add number theory + bit ops + accessors to IPy - -```rust -// Number theory (note return types — sqrt/sqr return unsigned!) -fn sqrt(&self) -> PyResult { - if self.0.is_negative() { - return Err(PyValueError::new_err("cannot compute sqrt of negative number")); - } - Ok(UPy(self.0.abs().sqrt())) -} -fn cbrt(&self) -> Self { IPy(self.0.cbrt()) } -fn nth_root(&self, n: usize) -> PyResult { - if n % 2 == 0 && self.0.sign() == Sign::Negative { - return Err(PyValueError::new_err("cannot compute even root of negative number")); - } - Ok(IPy(self.0.nth_root(n))) -} -fn sqr(&self) -> UPy { UPy(self.0.sqr()) } -fn cubic(&self) -> Self { IPy(self.0.cubic()) } -fn ilog(&self, base: &UPy) -> PyResult { - if base.0 <= UBig::ONE { - return Err(PyValueError::new_err("base must be > 1")); - } - Ok(self.0.ilog(&base.0)) -} - -// Bit operations -fn trailing_zeros(&self) -> Option { self.0.trailing_zeros() } -fn trailing_ones(&self) -> Option { self.0.trailing_ones() } -fn __invert__(&self) -> Self { IPy(!&self.0) } - -// Accessors -fn is_one(&self) -> bool { self.0.is_one() } -fn sign(&self) -> PySign { - match self.0.sign() { - Sign::Positive => PySign::Positive, - Sign::Negative => PySign::Negative, - } -} -fn signum(&self) -> Self { IPy(self.0.signum()) } -fn is_negative(&self) -> bool { self.0.sign() == Sign::Negative } -fn is_positive(&self) -> bool { self.0.sign() == Sign::Positive } -fn to_parts(&self) -> (PySign, UPy) { - let (sign, mag) = self.0.clone().into_parts(); - let py_sign = match sign { - Sign::Positive => PySign::Positive, - Sign::Negative => PySign::Negative, - }; - (py_sign, UPy(mag)) -} -#[staticmethod] -fn from_parts(sign: &PySign, magnitude: &UPy) -> Self { - let sign = match sign { - PySign::Positive => Sign::Positive, - PySign::Negative => Sign::Negative, - }; - IPy(IBig::from_parts(sign, magnitude.0.clone())) -} -fn as_ubig(&self) -> Option { - self.0.as_ubig().cloned().map(UPy) -} -``` - -### 5d: IBig interop parity with UBig - -Add to IPy (copy patterns from UPy at int.rs lines 379–457): - -```rust -fn to_words(&self) -> PyWords { - let (_, words) = self.0.as_sign_words(); - PyWords(words.to_vec()) -} -#[staticmethod] -fn from_words(ob: &PyAny) -> PyResult { - if let Ok(vec) = as FromPyObject>::extract(ob) { - Ok(IPy(IBig::from_words(&vec))) - } else if let Ok(words) = as FromPyObject>::extract(ob) { - Ok(IPy(IBig::from_words(&words.0))) - } else { - Err(PyTypeError::new_err("only list of integers or Words instance can be used")) - } -} -fn to_chunks(&self, chunk_bits: usize, py: Python) -> PyResult { - if chunk_bits == 0 { - Err(PyValueError::new_err("chunk size must not be zero")) - } else { - let iter = self.0.to_chunks(chunk_bits).into_vec().into_iter().map(|u| UPy(u).into_py(py)); - Ok(PyTuple::new(py, iter).into_py(py)) - } -} -#[staticmethod] -fn from_chunks(chunks: &PyAny, chunk_bits: usize) -> PyResult { - // Same pattern as UPy::from_chunks, but collect into Vec then join - if chunk_bits == 0 { - return Err(PyValueError::new_err("chunk size must not be zero")); - } - let mut input = Vec::new(); - if let Ok(list) = chunks.downcast::() { - input.reserve_exact(list.len()); - for item in list { - input.push(UniInput::extract(item)?.to_ubig()?); - } - } else if let Ok(tuple) = chunks.downcast::() { - input.reserve_exact(tuple.len()); - for item in tuple { - input.push(UniInput::extract(item)?.to_ubig()?); - } - } else if let Ok(iter) = chunks.downcast::() { - for item in iter { - input.push(UniInput::extract(item?)?.to_ubig()?); - } - } else { - return Err(PyTypeError::new_err("chunks must be a list, tuple, or iterator")); - } - Ok(IPy(IBig::from_chunks(input.iter(), chunk_bits))) -} -``` - -Also copy the bit-slice `__getitem__`, `__setitem__`, `__delitem__` implementations from UPy (lines 269–371) to IPy, adapting from `UBig` to `IBig`. The core bit-manipulation logic uses `split_bits`, `clear_high_bits`, `set_bit`, `clear_bit` which exist on IBig as well. - -### 5e: Add in-place operators to UPy/IPy - -For all types, in-place operators take `&mut self`: - -```rust -fn __iadd__(&mut self, other: &Self) { self.0 += &other.0; } -fn __isub__(&mut self, other: &Self) { self.0 -= &other.0; } -fn __imul__(&mut self, other: &Self) { self.0 *= &other.0; } -fn __iand__(&mut self, other: &Self) { self.0 &= &other.0; } -fn __ior__(&mut self, other: &Self) { self.0 |= &other.0; } -fn __ixor__(&mut self, other: &Self) { self.0 ^= &other.0; } -fn __ilshift__(&mut self, other: usize) { self.0 <<= other; } -fn __irshift__(&mut self, other: usize) { self.0 >>= other; } -``` - -Note: these are same-type-only for the MVP. Python's data model falls back to `__add__` + assignment when `__iadd__` is not defined, so cross-type in-place is handled automatically. - ---- - -## Step 6: Float & rational predicates, conversions, rounding - -**File: `python/src/float.rs`** — add to `#[pymethods] impl FPy` (and replicate for DPy): - -```rust -// Predicates (repr-based) -fn is_zero(&self) -> bool { self.0.repr().is_zero() } -fn is_finite(&self) -> bool { self.0.repr().is_finite() } -fn is_infinite(&self) -> bool { self.0.repr().is_infinite() } -fn is_nan(&self) -> bool { self.0.repr().is_nan() } - -// Sign -fn sign(&self) -> PySign { - match self.0.repr().sign() { - Sign::Positive => PySign::Positive, - Sign::Negative => PySign::Negative, - } -} -fn signum(&self) -> Self { FPy(self.0.signum()) } - -// Rounding — direct on bare FBig (pure significand/exponent ops, no cache needed). -fn trunc(&self) -> Self { FPy(self.0.trunc()) } -fn floor(&self) -> Self { FPy(self.0.floor()) } -fn ceil(&self) -> Self { FPy(self.0.ceil()) } -fn round(&self) -> Self { FPy(self.0.round()) } -fn fract(&self) -> Self { FPy(self.0.fract()) } - -// Conversion (FBig::to_int returns Rounded) -fn to_int(&self) -> PyResult { - let int: IBig = self.0.clone().to_int().value().try_into() - .map_err(|e: ConversionError| PyValueError::new_err(format!("cannot convert to integer: {}", e)))?; - Ok(IPy(int)) -} -fn __int__(&self, py: Python) -> PyResult { - let ipy = self.to_int()?; - convert_from_ibig(&ipy.0, py) -} - -// Precision (FBig::with_precision -> Rounded) -fn with_precision(&self, precision: usize) -> Self { - FPy(self.0.clone().with_precision(precision).value()) -} -fn precision(&self) -> usize { self.0.context().precision() } -fn digits(&self) -> usize { self.0.digits() } - -// Float-specific constructor -#[staticmethod] -fn from_parts(significand: &IPy, exponent: isize) -> Self { - FPy(FBig::from_parts(significand.0.clone(), exponent)) -} -``` - -**For DPy** the same methods apply — bare `FBig` supports them identically. - -### 6b: Transcendentals — route through the global cache + Context layer - -This is the critical part: transcendentals do **NOT** call `self.0.sin()` (the convenience method panics on domain errors). Instead they call the panic-free `Context` layer with the module cache and map the result: - -```rust -use crate::cache::{with_cache, unwrap_float}; - -#[pymethods] -impl FPy { - fn sin(&self) -> PyResult { - let ctx = self.0.context(); - let res = with_cache(|c| ctx.sin(self.0.repr(), Some(c))); // FpResult> - Ok(FPy(unwrap_float(res, ctx)?)) - } - fn cos(&self) -> PyResult { /* ctx.cos(...) */ } - fn tan(&self) -> PyResult { /* ctx.tan(...) */ } - fn asin(&self) -> PyResult { /* ctx.asin(...) */ } - fn acos(&self) -> PyResult { /* ctx.acos(...) */ } - fn atan(&self) -> PyResult { /* ctx.atan(...) */ } - fn sinh(&self) -> PyResult { /* ctx.sinh(...) */ } - fn cosh(&self) -> PyResult { /* ctx.cosh(...) */ } - fn tanh(&self) -> PyResult { /* ctx.tanh(...) */ } - fn asinh(&self) -> PyResult { /* ctx.asinh(...) */ } - fn acosh(&self) -> PyResult { /* ctx.acosh(...) */ } - fn atanh(&self) -> PyResult { /* ctx.atanh(...) */ } - fn exp(&self) -> PyResult { /* ctx.exp(...) */ } - fn exp_m1(&self) -> PyResult { /* ctx.exp_m1(...) */ } - fn ln(&self) -> PyResult { /* ctx.ln(...) */ } - fn ln_1p(&self) -> PyResult { /* ctx.ln_1p(...) */ } - // sqrt/cbrt take NO cache (they're algebraic) but still go through Context for clean errors: - fn sqrt(&self) -> PyResult { let ctx = self.0.context(); Ok(FPy(unwrap_float(ctx.sqrt(self.0.repr()), ctx)?)) } - fn cbrt(&self) -> PyResult { /* ctx.cbrt / ctx.nth_root(3, ...) */ } - fn powf(&self, w: &Self) -> PyResult { let ctx = self.0.context(); Ok(FPy(unwrap_float(ctx.powf(self.0.repr(), w.0.repr(), with_cache_arg), ctx)?)) } -} -``` - -Note `powf` needs the cache threaded into `with_cache`; the helper can be extended to `with_cache(|c| ctx.powf(a, b, Some(c)))`. Every domain error (`sqrt(-1)`, `asin(2)`, `ln(-1)`, `acosh(0.5)`, `atanh(2)`) now raises a Python `ValueError` instead of crashing the session. - -**File: `python/src/ratio.rs`** — add to `#[pymethods] impl RPy`: - -```rust -// Properties -#[getter] -fn numerator(&self) -> IPy { IPy(self.0.numerator().clone()) } -#[getter] -fn denominator(&self) -> UPy { UPy(self.0.denominator().clone()) } - -// Predicates -fn is_int(&self) -> bool { self.0.is_int() } -fn is_one(&self) -> bool { self.0.is_one() } -fn sign(&self) -> PySign { - match self.0.sign() { - Sign::Positive => PySign::Positive, - Sign::Negative => PySign::Negative, - } -} -fn signum(&self) -> Self { RPy(self.0.signum()) } - -// Rounding (return IBig — NOT RBig!) -fn trunc(&self) -> IPy { IPy(self.0.trunc()) } -fn floor(&self) -> IPy { IPy(self.0.floor()) } -fn ceil(&self) -> IPy { IPy(self.0.ceil()) } -fn round(&self) -> IPy { IPy(self.0.round()) } -fn fract(&self) -> Self { RPy(self.0.fract()) } -fn split_at_point(&self) -> (IPy, Self) { - let (int_part, frac_part) = self.0.clone().split_at_point(); - (IPy(int_part), RPy(frac_part)) -} - -// Powers -fn sqr(&self) -> Self { RPy(self.0.sqr()) } -fn cubic(&self) -> Self { RPy(self.0.cubic()) } -fn pow(&self, n: usize) -> Self { RPy(self.0.pow(n)) } - -// Constructor -#[staticmethod] -fn from_parts(numerator: &IPy, denominator: &UPy) -> Self { - RPy(RBig::from_parts(numerator.0.clone(), denominator.0.clone())) -} - -// Conversion -fn to_int(&self) -> IPy { IPy(self.0.trunc()) } -fn __int__(&self, py: Python) -> PyResult { - let ipy = IPy(self.0.trunc()); - convert_from_ibig(&ipy.0, py) -} - -// Simplification -#[staticmethod] -fn simplest_from_float(f: &FPy) -> Option { - RBig::simplest_from_float(&f.0).map(RPy) -} -``` - -Add import to `float.rs`: `use dashu_base::{Sign, ConversionError};` -Add imports to `ratio.rs`: `use dashu_base::Sign;` - ---- - -## Step 6c: Cross-type conversions (base change, float↔rational) - -Expose the base-change and float↔rational conversions that already exist on bare `FBig`/`RBig`. These let users move between `FPy` (base 2), `DPy` (base 10), `RPy`, and the corresponding Python native types without round-tripping through strings. - -**File: `python/src/float.rs`** — add to `FPy` and `DPy`: - -```rust -// Base conversion (FBig::to_decimal / to_binary return Rounded>) -fn to_decimal(&self) -> DPy { DPy(self.0.to_decimal().value()) } // FPy(base 2) -> DPy(base 10) -fn to_binary(&self) -> FPy { FPy(self.0.to_binary().value()) } // DPy(base 10) -> FPy(base 2); identity on FPy - -// Float -> Rational (exact only; errors if the float isn't exactly representable as a fraction) -fn to_rational(&self) -> PyResult { - RBig::try_from(self.0.clone()).map(RPy).map_err(conversion_error_to_py) -} - -// Precision/base introspection + arbitrary-radix formatting (no new Python type for base != 2,10, -// so with_base is exposed only as a string formatter via in_radix — see Step 8/__format__) -``` - -**File: `python/src/ratio.rs`** — add to `RPy`: - -```rust -// Rational -> Float, correctly rounded (lossy). RBig::to_float::(precision) is the -// dashu-float lossy conversion; pick base 2 (FPy) and base 10 (DPy) variants. -fn to_float(&self, precision: usize) -> FPy { - FPy(self.0.to_float::(precision).value()) -} -fn to_decimal(&self, precision: usize) -> DPy { - DPy(self.0.to_float::(precision).value()) -} -``` - -**Conversion summary** (after Steps 2b + 6c): - -| from → to | `FPy` | `DPy` | `RPy` | Python native | -|---|---|---|---|---| -| `FPy` | — | `.to_decimal()` | `.to_rational()` (exact) | `float()`, `int()`, `.to_decimal().unwrap()`→Decimal | -| `DPy` | `.to_binary()` | — | `.to_rational()` (exact) | `float()`, `int()`, `.unwrap()`→Decimal | -| `RPy` | `.to_float(p)` (lossy) | `.to_decimal(p)` (lossy) | — | `float()`, `int()`, `.unwrap()`→Fraction | -| `UPy`/`IPy` | `FBig::from` | `DBig::from` | `RBig::from` | `int()`, `.unwrap()` | - -**Notes:** -- `to_decimal`/`to_binary` return `Rounded<...>` in Rust — use `.value()` to drop the rounding flag (we don't expose per-call rounding flags to Python yet). -- `RBig::to_float` requires an explicit precision argument (rationals are generally non-terminating in base 2/10). This is the lossy counterpart to the exact `FBig → RBig` path. -- `with_base(radix)` for radix ∉ {2,10} has no Python wrapper type, so it's exposed only as a string formatter (`in_radix` / `__format__`, Step 8), not as a new numeric type. -- For `FPy → Decimal` directly: compose `f.to_decimal().unwrap()` (DPy → Decimal already exists). No separate method needed. - ---- - -## Step 7: Create math module - -**New file: `python/src/math.rs`** - -```rust -use pyo3::prelude::*; -use crate::types::{FPy, DPy, UPy, IPy}; - -macro_rules! impl_math_func { - ($name:ident, ($($arg:ident: $arg_ty:ty),*) -> $ret:ty, $body:expr) => { - #[pyfunction] - pub fn $name($($arg: $arg_ty),*) -> $ret { - $body - } - }; -} - -// Trigonometric -impl_math_func!(sin, (x: &FPy) -> FPy, FPy(x.0.sin())); -impl_math_func!(cos, (x: &FPy) -> FPy, FPy(x.0.cos())); -impl_math_func!(tan, (x: &FPy) -> FPy, FPy(x.0.tan())); -impl_math_func!(asin, (x: &FPy) -> FPy, FPy(x.0.asin())); -impl_math_func!(acos, (x: &FPy) -> FPy, FPy(x.0.acos())); -impl_math_func!(atan, (x: &FPy) -> FPy, FPy(x.0.atan())); -#[pyfunction] -pub fn atan2(y: &FPy, x: &FPy) -> FPy { FPy(y.0.atan2(&x.0)) } -#[pyfunction] -pub fn sincos(x: &FPy) -> (FPy, FPy) { - let (s, c) = x.0.sin_cos(); - (FPy(s), FPy(c)) -} - -// Hyperbolic -impl_math_func!(sinh, (x: &FPy) -> FPy, FPy(x.0.sinh())); -impl_math_func!(cosh, (x: &FPy) -> FPy, FPy(x.0.cosh())); -impl_math_func!(tanh, (x: &FPy) -> FPy, FPy(x.0.tanh())); -impl_math_func!(asinh, (x: &FPy) -> FPy, FPy(x.0.asinh())); -impl_math_func!(acosh, (x: &FPy) -> FPy, FPy(x.0.acosh())); -impl_math_func!(atanh, (x: &FPy) -> FPy, FPy(x.0.atanh())); - -// Exponential and log -impl_math_func!(exp, (x: &FPy) -> FPy, FPy(x.0.exp())); -impl_math_func!(exp_m1, (x: &FPy) -> FPy, FPy(x.0.exp_m1())); -impl_math_func!(ln, (x: &FPy) -> FPy, FPy(x.0.ln())); -impl_math_func!(ln_1p, (x: &FPy) -> FPy, FPy(x.0.ln_1p())); - -// Roots -impl_math_func!(sqrt, (x: &FPy) -> FPy, FPy(x.0.sqrt())); -impl_math_func!(cbrt, (x: &FPy) -> FPy, FPy(x.0.cbrt())); -#[pyfunction] -pub fn nth_root(x: &FPy, n: usize) -> FPy { FPy(x.0.nth_root(n)) } - -// Integer number theory -#[pyfunction] -pub fn gcd(a: &UPy, b: &UPy) -> UPy { - use dashu_base::ring::Gcd; - UPy(Gcd::gcd(&a.0, &b.0)) -} -#[pyfunction] -pub fn gcd_ext(a: &UPy, b: &UPy) -> (UPy, IPy, IPy) { - use dashu_base::ring::ExtendedGcd; - let (g, s, t) = ExtendedGcd::gcd_ext(&a.0, &b.0); - (UPy(g), IPy(s), IPy(t)) -} -#[pyfunction] -pub fn lcm(a: &UPy, b: &UPy) -> UPy { - use dashu_base::ring::Gcd; - UPy((&a.0 * &b.0) / Gcd::gcd(&a.0, &b.0)) -} -``` - -Also add corresponding methods directly on FPy/DPy in `float.rs`: - -```rust -// In #[pymethods] impl FPy: -fn sin(&self) -> Self { FPy(self.0.sin()) } -fn cos(&self) -> Self { FPy(self.0.cos()) } -fn tan(&self) -> Self { FPy(self.0.tan()) } -fn asin(&self) -> Self { FPy(self.0.asin()) } -fn acos(&self) -> Self { FPy(self.0.acos()) } -fn atan(&self) -> Self { FPy(self.0.atan()) } -fn atan2(&self, x: &Self) -> Self { FPy(self.0.atan2(&x.0)) } -fn sincos(&self) -> (Self, Self) { let (s, c) = self.0.sin_cos(); (FPy(s), FPy(c)) } -fn sinh(&self) -> Self { FPy(self.0.sinh()) } -fn cosh(&self) -> Self { FPy(self.0.cosh()) } -fn tanh(&self) -> Self { FPy(self.0.tanh()) } -fn asinh(&self) -> Self { FPy(self.0.asinh()) } -fn acosh(&self) -> Self { FPy(self.0.acosh()) } -fn atanh(&self) -> Self { FPy(self.0.atanh()) } -fn exp(&self) -> Self { FPy(self.0.exp()) } -fn exp_m1(&self) -> Self { FPy(self.0.exp_m1()) } -fn ln(&self) -> Self { FPy(self.0.ln()) } -fn ln_1p(&self) -> Self { FPy(self.0.ln_1p()) } -fn sqrt(&self) -> Self { FPy(self.0.sqrt()) } -fn cbrt(&self) -> Self { FPy(self.0.cbrt()) } -fn nth_root(&self, n: usize) -> Self { FPy(self.0.nth_root(n)) } -``` - -Same for DPy. - -Register in `lib.rs`: - -```rust -mod math; - -// In #[pymodule] fn dashu: -m.add_function(wrap_pyfunction!(math::sin, m)?)?; -m.add_function(wrap_pyfunction!(math::cos, m)?)?; -m.add_function(wrap_pyfunction!(math::tan, m)?)?; -m.add_function(wrap_pyfunction!(math::asin, m)?)?; -m.add_function(wrap_pyfunction!(math::acos, m)?)?; -m.add_function(wrap_pyfunction!(math::atan, m)?)?; -m.add_function(wrap_pyfunction!(math::atan2, m)?)?; -m.add_function(wrap_pyfunction!(math::sincos, m)?)?; -m.add_function(wrap_pyfunction!(math::sinh, m)?)?; -m.add_function(wrap_pyfunction!(math::cosh, m)?)?; -m.add_function(wrap_pyfunction!(math::tanh, m)?)?; -m.add_function(wrap_pyfunction!(math::asinh, m)?)?; -m.add_function(wrap_pyfunction!(math::acosh, m)?)?; -m.add_function(wrap_pyfunction!(math::atanh, m)?)?; -m.add_function(wrap_pyfunction!(math::exp, m)?)?; -m.add_function(wrap_pyfunction!(math::exp_m1, m)?)?; -m.add_function(wrap_pyfunction!(math::ln, m)?)?; -m.add_function(wrap_pyfunction!(math::ln_1p, m)?)?; -m.add_function(wrap_pyfunction!(math::sqrt, m)?)?; -m.add_function(wrap_pyfunction!(math::cbrt, m)?)?; -m.add_function(wrap_pyfunction!(math::nth_root, m)?)?; -m.add_function(wrap_pyfunction!(math::gcd, m)?)?; -m.add_function(wrap_pyfunction!(math::gcd_ext, m)?)?; -m.add_function(wrap_pyfunction!(math::lcm, m)?)?; -``` - ---- - -## Step 8: Fix `__format__` from `todo!()` to minimal working - -**Files: `python/src/int.rs`, `python/src/float.rs`, `python/src/ratio.rs`** - -Replace each `fn __format__(&self) { todo!() }` with: - -```rust -fn __format__(&self, _format_spec: &str) -> String { - // For MVP: ignore format_spec, just delegate to Display - format!("{}", self.0) -} -``` - -Full Python format mini-language parsing is deferred to Phase 6. - ---- - -## Step 9: Update type stubs - -**File: `python/dashu.pyi`** - -Complete the stubs to match all newly exposed methods. Key additions: - -```python -class FBig: - def __init__(self, obj: float | str | int): ... - def unwrap(self) -> tuple[IBig, int]: ... - def __repr__(self) -> str: ... - def __str__(self) -> str: ... - def __format__(self, format_spec: str) -> str: ... - def __hash__(self) -> int: ... - def __float__(self) -> float: ... - def __int__(self) -> int: ... - def __bool__(self) -> bool: ... - - # Arithmetic - @overload - def __add__(self, other: UBig | IBig | FBig | DBig | RBig) -> UBig | IBig | FBig | DBig | RBig: ... - @overload - def __add__(self, other: int) -> FBig: ... - @overload - def __radd__(self, other: int) -> FBig: ... - # ... same for __sub__, __rsub__, __mul__, __rmul__, __truediv__, __rtruediv__, __mod__, __rmod__ - - def __neg__(self) -> FBig: ... - def __pos__(self) -> FBig: ... - def __abs__(self) -> FBig: ... - - # Comparison - def __eq__(self, other) -> bool: ... - def __ne__(self, other) -> bool: ... - def __lt__(self, other) -> bool: ... - def __le__(self, other) -> bool: ... - def __gt__(self, other) -> bool: ... - def __ge__(self, other) -> bool: ... - - # Predicates - def is_zero(self) -> bool: ... - def is_finite(self) -> bool: ... - def is_infinite(self) -> bool: ... - def is_nan(self) -> bool: ... - def sign(self) -> Sign: ... - def signum(self) -> FBig: ... - - # Rounding - def trunc(self) -> FBig: ... - def floor(self) -> FBig: ... - def ceil(self) -> FBig: ... - def round(self) -> FBig: ... - def fract(self) -> FBig: ... - - # Precision - def precision(self) -> int: ... - def digits(self) -> int: ... - def with_precision(self, precision: int) -> FBig: ... - - # Conversion - def to_int(self) -> IBig: ... - @staticmethod - def from_parts(significand: IBig, exponent: int) -> FBig: ... - - # Math - def sin(self) -> FBig: ... - def cos(self) -> FBig: ... - def tan(self) -> FBig: ... - def exp(self) -> FBig: ... - def ln(self) -> FBig: ... - def sqrt(self) -> FBig: ... - # ... (all math methods) - -class DBig: - # Same as FBig, but constructor accepts decimal.Decimal | str | int - -class RBig: - # Constructor, arithmetic, comparison same pattern - # Plus: - @property - def numerator(self) -> IBig: ... - @property - def denominator(self) -> UBig: ... - def trunc(self) -> IBig: ... - def floor(self) -> IBig: ... - def ceil(self) -> IBig: ... - def round(self) -> IBig: ... - def fract(self) -> RBig: ... - def split_at_point(self) -> tuple[IBig, RBig]: ... - @staticmethod - def from_parts(numerator: IBig, denominator: UBig) -> RBig: ... - @staticmethod - def simplest_from_float(f: FBig) -> RBig | None: ... - -class Sign: - Positive: Sign - Negative: Sign -``` - ---- - -## Step 10: Add tests - -**New files to create:** - -- `python/tests/test_float_ops.py` — FPy/DPy constructors, arithmetic, comparison, bool, predicates, rounding -- `python/tests/test_ratio_ops.py` — RPy constructors, arithmetic, comparison, bool, properties, rounding -- `python/tests/test_int_math.py` — UBig/IBig sqrt, cbrt, gcd, bit ops, accessors -- `python/tests/test_math.py` — module-level sin/cos/exp/sqrt/gcd/lcm - -**Existing files to extend:** - -- `python/tests/test_int.py` — add tests for floordiv, divmod, in-place ops - ---- - -## Implementation Order - -``` -1. Step 0 (PyO3 0.29 upgrade + Cargo.toml deps) — do first -2. Step 1 (fix panics) — independent -3. Step 2 (conversion helpers: into_*) — independent -4. Step 2b (broaden __new__ via construct_*) — depends on Step 2 -5. Step 2c (global cache module) — independent (cache.rs + FpError mapper) -6. Step 3 (FPy/DPy arithmetic) — depends on Step 2 -7. Step 4 (RPy arithmetic) — depends on Step 2 -8. Step 5 (int methods) — independent (can parallel with 3-4) -9. Step 6 (float/rational pred + transcendentals via Step 2c) — depends on Step 3-4 + 2c -10. Step 6c (cross-type conversions: to_decimal/to_binary/to_rational/to_float) — depends on Step 6 -11. Step 7 (math module) — depends on Step 6 -12. Step 8 (format fix) — independent -13. Step 9 (stubs) — after all code changes -14. Step 10 (tests) — after all code changes -15. Step 11 (complex bindings) — after Step 6 + 2c (follows the float pattern) -``` - -Steps 3 and 5 can be done in parallel. Steps 3 and 4 share the same pattern so do them together. - ---- - -## Step 11: Complex number bindings (bare CBig → CPy) - -**New dependency:** `dashu-cmplx` (added to `Cargo.toml` in Step 0a). - -Per the Architecture decision, `CPy` wraps the **bare** `CBig` (not `CachedCBig`). The constant cache is the module-global one from Step 2c, threaded into complex `Context` methods. `CPy` is `Send + Sync` (no `unsendable`). - -### 11a: Add `CPy` wrapper type — `types.rs` - -Already defined in Step 0d2: -```rust -type CBig2 = dashu_cmplx::CBig; -#[pyclass(name = "CBig")] -pub struct CPy(pub CBig2); -``` -Also add `BComplex(PyRef<'a, CPy>)` variant to `UniInput`. - -### 11b: Register in `lib.rs` - -```rust -m.add_class::()?; -``` - -### 11c: CBig constructor — new file `python/src/complex.rs` - -```rust -#[pymethods] -impl CPy { - #[new] - fn __new__(ob: &PyAny) -> PyResult { - // Python complex → CBig (build from re/im FBig via CBig::from_parts, or FromStr) - // string "a+bi" → CBig (FromStr; algebraic format) - // (real, imag) tuple of FPy → CBig::from_parts(re.0.clone(), im.0.clone()) - // int/float/FPy → CBig::from (embed as z + 0i) - } -} -``` - -`CBig` provides: -- `FromStr` for algebraic `"a+bi"` format -- `From`, `From`, `From`, `From` — embed as `z + 0i` -- `CBig::from_parts(re: FBig, im: FBig)` — note `into_parts()` returns `(FBig, FBig)` on bare CBig - -### 11d: Arithmetic operators - -Same macro pattern as FPy/DPy/RPy using `into_cpy()`. Bare `CBig` has full `Add`/`Sub`/`Mul`/`Div` (assign + ref/val combos vs itself and `FBig`), `Neg`, `Inverse`, `Sum`, `Product`. (Same infinite-input/`0/0` panic caveat as float arithmetic — address as a follow-up.) - -### 11e: Accessors, predicates, complex-specific - -```rust -// Accessors (re()/im() return &Repr; project to a bare FBig) -fn real(&self) -> FPy { FPy(FBig::from(self.0.re().clone())) } -fn imag(&self) -> FPy { FPy(FBig::from(self.0.im().clone())) } - -// Predicates -fn is_zero(&self) -> bool { self.0.is_zero() } -fn is_finite(&self) -> bool { self.0.is_finite() } -fn is_infinite(&self) -> bool { self.0.is_infinite() } - -// Complex-specific (conj/proj are algebraic, no cache; abs/arg/norm return real FBig) -fn conj(&self) -> Self { CPy(self.0.conj()) } -fn proj(&self) -> Self { CPy(self.0.proj()) } -// abs/arg route through the float Context layer for clean errors; norm is algebraic. -fn abs(&self) -> PyResult { - let ctx = self.0.context(); - let res = with_cache(|c| ctx.abs(&self.0, Some(c))); // returns FpResult> - Ok(FPy(unwrap_float(res, ctx)?)) -} -fn arg(&self) -> PyResult { /* ctx.arg(...) */ } -fn norm(&self) -> FPy { FPy(self.0.norm()) } // squared modulus, algebraic -``` - -### 11f: Complex transcendentals — route through the global cache + Context layer - -Exactly the float pattern, but using `dashu_cmplx::Context` (obtained via `self.0.context()`) and `unwrap_complex`: - -```rust -use crate::cache::{with_cache, unwrap_complex}; - -#[pymethods] -impl CPy { - fn sin(&self) -> PyResult { - let ctx = self.0.context(); - let res = with_cache(|c| ctx.sin(&self.0, Some(c))); // CfpResult = Result - Ok(CPy(unwrap_complex(res, ctx)?)) - } - fn cos(&self) -> PyResult { /* ctx.cos(...) */ } - fn tan(&self) -> PyResult { /* ctx.tan(...) */ } - fn asin(&self) -> PyResult { /* ctx.asin(...) */ } - fn acos(&self) -> PyResult { /* ctx.acos(...) */ } - fn atan(&self) -> PyResult { /* ctx.atan(...) */ } - fn exp(&self) -> PyResult { /* ctx.exp(...) */ } - fn ln(&self) -> PyResult { /* ctx.ln(...) */ } - fn powf(&self, w: &Self) -> PyResult { /* ctx.powf(...) */ } - // sqrt/powi take NO cache but still go through Context for clean errors: - fn sqrt(&self) -> PyResult { let ctx = self.0.context(); Ok(CPy(unwrap_complex(ctx.sqrt(&self.0), ctx)?)) } - fn powi(&self, exp: &IPy) -> PyResult { /* ctx.powi(...) */ } - fn sin_cos(&self) -> PyResult<(CPy, CPy)> { /* ctx.sin_cos(...) */ } -} -``` - -Domain errors (`ln(0)`, etc.) raise Python `ValueError`; `0/0`-style indeterminate forms raise `ZeroDivisionError` — never a session-crashing panic. - -### 11g: Conversion to Python `complex` - -```rust -fn __complex__(&self) -> PyResult { - let re: f64 = self.0.re().clone().try_into().map_err(conversion_error_to_py)?; - let im: f64 = self.0.im().clone().try_into().map_err(conversion_error_to_py)?; - Ok(PyComplex::from_doubles(re, im)) -} -``` - -### 11h: Add to math module - -Module-level functions in `math.rs` that delegate to `CPy` methods, matching the pattern for `FPy`. - -## Verification - -```bash -# Build -cd python && maturin develop - -# Smoke test -python -c " -from dashu import FBig, DBig, RBig, UBig, IBig, CBig -a = FBig('1.5') -b = FBig('2.0') -assert a + b == FBig('3.5') -assert a * 3 == FBig('4.5') -assert bool(FBig('0.0')) == False -assert UBig(144).sqrt() == UBig(12) -assert UBig(12).gcd(UBig(8)) == UBig(4) -r = RBig.from_parts(IBig(1), UBig(3)) -assert r * 3 == RBig.from_parts(IBig(1), UBig(1)) -# transcendental uses the module-global ConstCache (Step 2c) -import math -assert abs(float(FBig('2').ln()) - math.log(2)) < 1e-50 # high precision -print('All smoke tests passed') -" - -# Full test suite -python -m pytest python/tests/ -v -``` - ---- - -## Notes - -- **MSRV**: dashu-python is exempt from the workspace MSRV policy (per AGENTS.md). Uses Rust 1.85 and edition 2024. -- **Feature flags**: `num-modular` is used by `__pow__` but listed as optional — fix to hard dependency or wire as default feature. -- **Complex crate**: `dashu-cmplx` (merged from develop, `6e21a65`) is a new core crate. Python bindings (Step 11) use bare `CBig` + the global cache. -- **Error model**: transcendentals route through the panic-free `Context` layer (`FpResult`) with a module-global `ConstCache` (Step 2c), mapping `Overflow/Underflow → ±∞/0` and `InfiniteInput/OutOfDomain → ValueError`, `Indeterminate → ZeroDivisionError`. See Architecture. -- **Send / free-threaded Python**: all wrappers (`FPy`/`DPy`/`CPy`/`UPy`/`IPy`/`RPy`) are `Send + Sync` — **none use `#[pyclass(unsendable)]`** — because the cache is thread-local, not owned by the value. The binding is free-threaded-Python (no-GIL) compatible; each thread gets its own cache. -- **Outstanding caveat — arithmetic panics**: bare `FBig`/`CBig` arithmetic operators still `panic!` on infinite input (e.g. `exp(1000) + 1`) and on `0/0`. Transcendentals are fully fixed; routing arithmetic through `Context` (or guarding finiteness / the `0/0` case) is a documented follow-up before release. -- **Changelog**: Document all changes in `python/CHANGELOG.md` under `## Unreleased` → `### Add`.