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/AGENTS.md b/AGENTS.md index a1aec0b4..3de8cb5e 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`, `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. ## Workspace structure @@ -13,6 +15,7 @@ dashu is a library set of arbitrary precision numbers implemented in pure Rust, | `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 | @@ -94,5 +97,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/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/.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 9f1789f5..0f85dab0 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -1,4 +1,56 @@ # 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`. +- 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. +- `__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 + `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 + 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 b9bbd5dc..6c05a4cd 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -2,31 +2,30 @@ 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"] +categories = ["mathematics", "science"] license = "MIT OR Apache-2.0" 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 [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/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'`. diff --git a/python/dashu.pyi b/python/dashu.pyi index 47c4009c..5e498ebe 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: 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: ... 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: int | UBig) -> int: ... + 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]: ... + 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: 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: ... + 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: int | 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: int | 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: int | 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: float | int | 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: float | int | FBig) -> FBig: ... + def powi(self, n: int | 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: int | 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: float | int | 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: float | int | DBig) -> DBig: ... + def powi(self, n: int | 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: int | IBig, denominator: int | UBig) -> RBig: ... + @staticmethod + def simplest_from_float(f: float | int | 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: int | 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: 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/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..ed29f82e --- /dev/null +++ b/python/src/complex.rs @@ -0,0 +1,302 @@ +//! 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, UniInput}, +}; + +/// 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) -> 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 + 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: UniInput<'_>) -> PyResult { + let n = n.into_ibig()?; + let ctx = self.0.context(); + Ok(Self(unwrap_complex(ctx.powi(&self.0, n), 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 99626312..baed202c 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::{ + Bound, FromPyObject, PyErr, PyResult, exceptions::{PySyntaxError, PyTypeError, PyValueError}, ffi, intern, prelude::*, - types::{PyBytes, PyDict, PyFloat, PyLong}, - FromPyObject, PyAny, PyErr, PyObject, + 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 { @@ -33,11 +36,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 } } } @@ -65,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 { @@ -82,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 @@ -114,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. @@ -142,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 { @@ -178,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: @@ -198,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)) } @@ -210,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()), @@ -226,4 +226,138 @@ impl<'a> UniInput<'a> { _ => Err(err), } } + + /// 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 { + 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(|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) + .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..10c32cc2 100644 --- a/python/src/float.rs +++ b/python/src/float.rs @@ -1,108 +1,650 @@ 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 { format!("", self.0) } - fn __str__(&self) -> String { - format!("{}", self.0) + fn __str__(&self) -> PyResult { + // an FBig is base 2: print in hexadecimal (lossless — no base conversion rounding) + crate::format::format_fbig_hex(&self.0, "") } - fn __format__(&self) { - todo!() + fn __format__(&self, format_spec: &str) -> PyResult { + // 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(); 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: UniInput<'_>, exponent: isize) -> PyResult { + Ok(FPy(FBig::from_parts(significand.into_ibig()?, 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: 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)?)) + } + 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), ctx)?)) + } + 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)?)) + } } #[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 { format!("", self.0) } - fn __str__(&self) -> String { - format!("{}", self.0) + fn __str__(&self) -> PyResult { + crate::format::format_dbig(&self.0, "") } - fn __format__(&self) { - todo!() + fn __format__(&self, format_spec: &str) -> PyResult { + crate::format::format_dbig(&self.0, format_spec) } 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: UniInput<'_>, exponent: isize) -> PyResult { + Ok(DPy(DBig::from_parts(significand.into_ibig()?, 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: 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)?)) + } + 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), ctx)?)) + } + 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/format.rs b/python/src/format.rs new file mode 100644 index 00000000..6462ca01 --- /dev/null +++ b/python/src/format.rs @@ -0,0 +1,336 @@ +//! 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)> { + // 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' => { + // 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) + } + '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)) +} + +/// 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('-') { + (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 be0c97a7..49729621 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_int::{fast_div, IBig, UBig, Word}; +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,11 @@ impl UPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self) { - todo!() + 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(); @@ -266,14 +440,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 +461,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 +476,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 +488,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 +508,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 +532,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 +545,78 @@ 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: 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)) + } + fn is_multiple_of(&self, divisor: UniInput<'_>) -> PyResult { + Ok(self.0.is_multiple_of(&divisor.into_ubig()?)) + } + fn remove(&mut self, factor: UniInput<'_>) -> PyResult { + self.0 + .remove(&factor.into_ubig()?) + .ok_or_else(|| PyValueError::new_err("the factor does not divide this number")) + } + fn gcd(&self, other: UniInput<'_>) -> PyResult { + Ok(UPy(Gcd::gcd(&self.0, &other.into_ubig()?))) + } + 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 **********/ + 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 +624,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 +697,106 @@ 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: UniInput<'_>) -> PyResult<()> { + self.0 += &other.into_ubig()?; + Ok(()) + } + #[inline] + fn __isub__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 -= &other.into_ubig()?; + Ok(()) + } + #[inline] + fn __imul__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 *= &other.into_ubig()?; + Ok(()) + } + #[inline] + fn __iand__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 &= &other.into_ubig()?; + Ok(()) + } + #[inline] + fn __ior__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 |= &other.into_ubig()?; + Ok(()) + } + #[inline] + fn __ixor__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 ^= &other.into_ubig()?; + Ok(()) + } + #[inline] + fn __ilshift__(&mut self, other: usize) { + self.0 <<= other; + } + #[inline] + fn __irshift__(&mut self, other: usize) { + self.0 >>= other; } #[inline] @@ -516,7 +812,7 @@ impl UPy { slf } #[inline] - fn __nonzero__(&self) -> bool { + fn __bool__(&self) -> bool { !self.0.is_zero() } @@ -529,15 +825,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 +842,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 +856,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 +882,11 @@ impl IPy { fn __str__(&self) -> String { format!("{}", self.0) } - fn __format__(&self) { - todo!() + 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(); @@ -614,55 +914,289 @@ 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: 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)) + } + 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: UniInput<'_>) -> PyResult { + Ok(IPy(IBig::from_parts(sign.into(), magnitude.into_ubig()?))) + } + 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: UniInput<'_>) -> PyResult<()> { + self.0 += &other.into_ibig()?; + Ok(()) + } + #[inline] + fn __isub__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 -= &other.into_ibig()?; + Ok(()) + } + #[inline] + fn __imul__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 *= &other.into_ibig()?; + Ok(()) + } + #[inline] + fn __iand__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 &= &other.into_ibig()?; + Ok(()) + } + #[inline] + fn __ior__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 |= &other.into_ibig()?; + Ok(()) + } + #[inline] + fn __ixor__(&mut self, other: UniInput<'_>) -> PyResult<()> { + self.0 ^= &other.into_ibig()?; + Ok(()) + } + #[inline] + fn __ilshift__(&mut self, other: usize) { + self.0 <<= other; + } + #[inline] + fn __irshift__(&mut self, other: usize) { + self.0 >>= other; } #[inline] @@ -678,7 +1212,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 +1229,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..b182e064 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,7 +1,17 @@ +// 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 format; mod int; -mod ratio; +mod math; +mod rational; mod types; mod utils; mod words; @@ -12,7 +22,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 +30,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..95d5edfc --- /dev/null +++ b/python/src/math.rs @@ -0,0 +1,131 @@ +//! 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, UPy, UniInput}; +use dashu_base::ring::{ExtendedGcd, Gcd}; +use pyo3::prelude::*; + +/// 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)?)) + } + }; +} + +/// 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 +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 nth_root(x: UniInput<'_>, n: usize) -> PyResult { + let x = x.into_fpy()?; + let ctx = x.0.context(); + Ok(FPy(unwrap_float(ctx.nth_root(n, x.0.repr()), ctx)?)) +} + +#[pyfunction] +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)?)) +} + +#[pyfunction] +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: 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: 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: 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: 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: 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/ratio.rs deleted file mode 100644 index 17a1ee11..00000000 --- a/python/src/ratio.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::collections::hash_map::DefaultHasher; -use std::hash::Hasher; - -use dashu_ratio::RBig; -use num_order::NumHash; -use pyo3::{exceptions::PyTypeError, intern, prelude::*}; - -use crate::{ - convert::{convert_from_rbig, parse_error_to_py, parse_to_rbig}, - types::RPy, -}; - -const ERRMSG_RBIG_WRONG_SRC_TYPE: &str = - "only Fraction instances or strings can be used to construct an RBig instance"; - -#[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 unwrap(&self, py: Python) -> PyResult { - convert_from_rbig(&self.0, py) - } - - fn __repr__(&self) -> String { - format!("", self.0) - } - fn __str__(&self) -> String { - format!("{}", self.0) - } - fn __format__(&self) { - todo!() - } - fn __hash__(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.0.num_hash(&mut hasher); - hasher.finish() - } - - fn __float__(&self) -> f64 { - self.0.to_f64_fast() - } -} diff --git a/python/src/rational.rs b/python/src/rational.rs new file mode 100644 index 00000000..e02e8c13 --- /dev/null +++ b/python/src/rational.rs @@ -0,0 +1,261 @@ +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, NumOrd}; +use pyo3::{Bound, IntoPyObjectExt, Py, PyAny, PyResult, basic::CompareOp, intern, prelude::*}; + +use crate::{ + convert::{parse_error_to_py, parse_to_rbig}, + types::{DPy, FPy, IPy, PySign, RPy, UPy, UniInput}, +}; + +/// 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: &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> { + crate::convert::convert_from_rbig(&self.0, py)?.into_py_any(py) + } + + fn __repr__(&self) -> String { + format!("", self.0) + } + fn __str__(&self) -> 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. + // 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)?; + // 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::(conv_prec) + .value(); + crate::format::format_dbig(&dbig, format_spec) + } + 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: 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] + 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/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..1999e59f --- /dev/null +++ b/python/tests/test_float_ops.py @@ -0,0 +1,83 @@ +import math +from decimal import Decimal + +from dashu import * + + +def test_construct(): + assert float(FBig(1.5)) == 1.5 + 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 = 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) + assert a + 1 == FBig(2.5) + + +def test_compare_bool(): + 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 + + +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() == 1 + assert f.ceil().to_int() == 2 + assert f.round().to_int() == 2 + assert f.trunc().to_int() == 1 + + +def test_transcendentals_panicfree(): + 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() + 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, 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(): + 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_format.py b/python/tests/test_format.py new file mode 100644 index 00000000..876e78d3 --- /dev/null +++ b/python/tests/test_format.py @@ -0,0 +1,62 @@ +import math + +from dashu import * + + +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(0.5), ".4f") == "0.5000" + # 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" + + +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(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" + + +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") diff --git a/python/tests/test_int_math.py b/python/tests/test_int_math.py new file mode 100644 index 00000000..5aba6cea --- /dev/null +++ b/python/tests/test_int_math.py @@ -0,0 +1,73 @@ +from dashu import * + + +def test_roots(): + 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() == -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) + raise AssertionError("expected ValueError") + except ValueError: + pass + + +def test_number_theory(): + 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() == 8 + + +def test_bit_ops(): + n = UBig(0b10110) # 22 + assert n.count_ones() == 3 + assert n.trailing_zeros() == 1 + assert UBig(0b1000).trailing_zeros() == 3 + + +def test_divmod_floordiv(): + 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 += 5 + assert n == 15 + n -= 3 + assert n == 12 + n *= 2 + assert n == 24 + n <<= 1 + assert n == 48 + n >>= 2 + assert n == 12 + n &= 4 # 12 & 4 + assert 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__": + 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 new file mode 100644 index 00000000..b30406c4 --- /dev/null +++ b/python/tests/test_math.py @@ -0,0 +1,46 @@ +import math + +import dashu +from dashu import * + + +def test_trig_hyper_exp_log(): + 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(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(12, 8) == 4 + assert dashu.gcd_ext(12, 8)[0] == 4 + assert dashu.lcm(4, 6) == 12 + + +def test_complex_module(): + 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(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(0.0, 0.0).exp() == CBig(1.0, 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..8d6645ce --- /dev/null +++ b/python/tests/test_ratio_ops.py @@ -0,0 +1,57 @@ +from fractions import Fraction + +from dashu import * + + +def test_construct(): + 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(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(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(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(2, 3) + assert r.sqr() == RBig.from_parts(4, 9) + assert r.pow(3) == RBig.from_parts(8, 27) + + +def test_float_conversion(): + assert RBig.from_parts(1, 2).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")