Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,26 @@ env:

jobs:
build:
name: Build and test
name: Build and test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [
ubuntu-latest,
windows-latest,
macos-latest,
]
os: [ubuntu-latest, windows-latest, macos-latest]
rust: [stable]
include:
- os: ubuntu-latest
features: complex
- os: windows-latest
features: complex
- os: macos-latest
features: complex,mul_add
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
check-latest: true
python-version: "3.14"
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
Expand All @@ -37,9 +39,9 @@ jobs:
override: true
- name: Build
run: cargo build --verbose
- name: Run tests
if: matrix.os != 'macos-latest'
run: cargo test --verbose && cargo test --release --verbose
- name: Run tests with FMA (macOS)
if: matrix.os == 'macos-latest'
run: cargo test --verbose --features mul_add && cargo test --release --verbose --features mul_add
- name: Run tests with num-bigint
run: cargo test --verbose --features ${{ matrix.features }},num-bigint
- name: Run tests with num-bigint (Release)
run: cargo test --verbose --features ${{ matrix.features }},num-bigint --release
- name: Run tests with malachite-bigint
run: cargo test --verbose --features ${{ matrix.features }},malachite-bigint
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.14
195 changes: 195 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# pymath Agent Guidelines

This project is a strict port of CPython's math and cmath modules to Rust.

## Core Principle

**Every function must match CPython exactly** - same logic, same special case handling, same error conditions.

- `math` module → `Modules/mathmodule.c`
- `cmath` module → `Modules/cmathmodule.c`

## Porting Rules

### 1. Use Existing Helpers

CPython uses helpers like `math_1`, `math_2`, `FUNC1`, `FUNC2`, etc. We have Rust equivalents:

| CPython | Rust |
|---------|------|
| `FUNC1(name, func, can_overflow, ...)` | `math_1(x, func, can_overflow)` |
| `FUNC2(name, func, ...)` | `math_2(x, y, func)` |
| `m_log`, `m_log10`, `m_log2` | Same names in `exponential.rs` |

If a function uses a helper in CPython, use the corresponding helper here.

### 2. Create Missing Helpers

If CPython has a helper we don't have yet, implement it. Examples:
- `math_1_fn` for Rust function pointers (vs C function pointers)
- Special case handlers for specific functions

### 3. Error Handling

CPython sets `errno` and calls `is_error()`. We return `Result` directly:

```rust
// CPython:
errno = EDOM;
if (errno && is_error(r, 1)) return NULL;

// Rust:
return Err(crate::Error::EDOM);
```

**Never use `set_errno(libc::EDOM)` or similar** - just return `Err()` directly.
The only valid `set_errno` call is `set_errno(0)` to clear errno before libm calls.

### 4. Special Cases

CPython has explicit special case handling for IEEE specials (NaN, Inf, etc.). Copy this logic exactly:

```rust
// Example from pow():
if !x.is_finite() || !y.is_finite() {
if x.is_nan() {
return Ok(if y == 0.0 { 1.0 } else { x }); // NaN**0 = 1
}
// ... more special cases
}
```

### 5. Reference CPython Source

Always check CPython source in the `cpython/` directory:

**For math module** (`Modules/mathmodule.c`):
- Function implementations
- Helper macros (`FUNC1`, `FUNC1D`, `FUNC2`)
- Special case comments
- Error conditions

**For cmath module** (`Modules/cmathmodule.c`):
- `special_type()` enum and function
- 7x7 special value tables (e.g., `tanh_special_values`)
- `SPECIAL_VALUE` macro usage
- Complex-specific error handling

### 6. Fused Multiply-Add (mul_add)

For bit-exact matching with CPython, use `crate::mul_add(a, b, c)` instead of `a * b + c` in specific cases.

**Why this matters**: CPython compiled with clang on macOS may use FMA (fused multiply-add) instructions for expressions like `1.0 + x * x`. FMA computes `a * b + c` in a single operation without intermediate rounding, which can produce results that differ by 1-2 ULP from separate multiply and add operations.

**When to use `mul_add`**:
- Expressions of the form `a * b + c` or `c + a * b` in complex math functions
- Especially in formulas like `1.0 + x * x` → `mul_add(x, x, 1.0)`

**Example** (from `c_tanh`):
```rust
// Wrong - may differ from CPython by 1-2 ULP
let denom = 1.0 + txty * txty;
let r_re = tx * (1.0 + ty * ty) / denom;

// Correct - matches CPython exactly
let denom = mul_add(txty, txty, 1.0);
let r_re = tx * mul_add(ty, ty, 1.0) / denom;
```

**Example** (from `c_asinh`):
```rust
// mul_add for cross-product calculations
let r_re = m::asinh(mul_add(s1.re, s2.im, -(s2.re * s1.im)));
let r_im = m::atan2(z.im, mul_add(s1.re, s2.re, -(s1.im * s2.im)));
```

**Example** (from `c_atanh`):
```rust
// mul_add for squared terms
let one_minus_re = 1.0 - z.re;
let r_re = m::log1p(4.0 * z.re / mul_add(one_minus_re, one_minus_re, ay * ay)) / 4.0;
let r_im = -m::atan2(-2.0 * z.im, mul_add(one_minus_re, 1.0 + z.re, -(ay * ay))) / 2.0;
```

**Feature flag**: The `mul_add` feature controls whether hardware FMA is used:

- `mul_add` enabled: Uses `f64::mul_add()` (hardware FMA instruction)
- `mul_add` disabled (default): Falls back to `a * b + c` (separate operations)

Note: macOS CI always enables `mul_add` because CPython on macOS uses FMA.

**How to identify missing mul_add usage**:
1. If a test fails with 1-2 ULP difference
2. Look for `a * b + c` or `c + a * b` patterns in the failing function
3. Replace with `mul_add(a, b, c)` and re-test

### 7. Platform-specific sincos (macOS, cmath only)

On macOS, Python's cmath module uses Apple's `__sincos_stret` function, which computes sin and cos together with slightly different results than calling them separately (up to 1 ULP difference).

For bit-exact matching on macOS, use `m::sincos(x)` which returns `(sin, cos)` tuple:

```rust
// Instead of:
let sin_x = m::sin(x);
let cos_x = m::cos(x);

// Use:
let (sin_x, cos_x) = m::sincos(x);
```

Required in cmath functions that use both sin and cos of the same angle:

- `cosh`, `sinh` - for the imaginary argument
- `exp` - for the imaginary argument
- `rect` - for the phi angle

On non-macOS platforms, `m::sincos(x)` falls back to calling sin and cos separately.

## Testing

### EDGE_VALUES

All float functions must be tested with `crate::test::EDGE_VALUES` which includes:
- Zeros: `0.0`, `-0.0`
- Infinities: `INFINITY`, `NEG_INFINITY`
- NaNs: `NAN`, `-NAN`, and NaN with different payload
- Subnormals
- Boundary values: `MIN_POSITIVE`, `MAX`, `MIN`
- Large values near infinity
- Trigonometric special values: `PI`, `PI/2`, `PI/4`, `TAU`

### Error Type Verification

Tests must verify both:
1. Correct values for Ok results
2. Correct error types (EDOM vs ERANGE) for Err results

Python `ValueError` → `Error::EDOM`
Python `OverflowError` → `Error::ERANGE`

## File Structure

### Core
- `src/lib.rs` - Root module, `mul_add` function
- `src/err.rs` - Error types (EDOM, ERANGE)
- `src/test.rs` - Test helpers, `EDGE_VALUES`, `EDGE_INTS`

### System libm bindings
- `src/m_sys.rs` - Raw FFI declarations (`extern "C"`)
- `src/m.rs` - Safe wrappers, platform-specific `sincos`

### math module
- `src/math.rs` - Main module, `math_1`, `math_2` helpers, `hypot`, constants
- `src/math/exponential.rs` - exp, log, pow, sqrt, cbrt, etc.
- `src/math/trigonometric.rs` - sin, cos, tan, asin, acos, atan, etc.
- `src/math/misc.rs` - frexp, ldexp, modf, fmod, copysign, isclose, ulp, etc.
- `src/math/gamma.rs` - gamma, lgamma, erf, erfc
- `src/math/aggregate.rs` - fsum, prod, sumprod, dist (vector operations)
- `src/math/integer.rs` - gcd, lcm, isqrt, comb, perm, factorial (requires `_bigint` feature)

### cmath module (requires `complex` feature)
- `src/cmath.rs` - Main module, `special_type`, `special_value!` macro, shared constants
- `src/cmath/exponential.rs` - sqrt, exp, log, log10
- `src/cmath/trigonometric.rs` - sin, cos, tan, sinh, cosh, tanh, asin, acos, atan, asinh, acosh, atanh
- `src/cmath/misc.rs` - phase, polar, rect, abs, isfinite, isnan, isinf, isclose
15 changes: 13 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
[package]
name = "pymath"
version = "0.0.2"
version = "0.0.3"
edition = "2024"
description = "A binary representation compatible Rust implementation of Python's math library."
license = "PSF-2.0"


[features]
default = ["complex"]
complex = ["dep:num-complex"]
num-bigint = ["_bigint", "dep:num-bigint"]
malachite-bigint = ["_bigint", "dep:malachite-bigint"]
_bigint = ["dep:num-traits", "dep:num-integer"] # Internal feature. User must use num-bigint or malachite-bigint instead.

# Do not enable this feature unless you really need it.
# CPython didn't intend to use FMA for its math library.
# This project uses this feature in CI to verify the code doesn't have additional bugs on aarch64-apple-darwin.
Expand All @@ -17,7 +23,12 @@ mul_add = []

[dependencies]
libc = "0.2"
num-complex = { version = "0.4", optional = true }
num-bigint = { version = "0.4", optional = true }
num-traits = { version = "0.2", optional = true }
num-integer = { version = "0.1", optional = true }
malachite-bigint = { version = "0.2", optional = true }

[dev-dependencies]
proptest = "1.6.0"
pyo3 = { version = "0.24", features = ["abi3"] }
pyo3 = { version = "0.27", features = ["abi3", "auto-initialize"] }
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ A binary representation compatible Rust implementation of Python's math library.

Each function has been carefully translated from CPython's C implementation to Rust, preserving the same algorithms, constants, and corner case handling. The code maintains the same numerical properties, but in Rust!

## Module Structure

- `pymath::math` - Real number math functions (Python's `math` module)
- `pymath::cmath` - Complex number functions (Python's `cmath` module, requires `complex` feature)
- `pymath::m` - Direct libm bindings

## Usage
Comment on lines +14 to 20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Resolve markdownlint MD003 heading-style warnings (consistency).
Either switch these headings to setext style or update the markdownlint config to accept ATX headings.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

14-14: Heading style
Expected: setext; Actual: atx

(MD003, heading-style)


20-20: Heading style
Expected: setext; Actual: atx

(MD003, heading-style)

🤖 Prompt for AI Agents
In @README.md around lines 14 - 20, The README uses mixed heading styles causing
markdownlint MD003 warnings; make heading styles consistent by converting the
ATX headings like "## Module Structure" and "## Usage" to setext style (e.g.,
"Module Structure" followed by "-----------------" and "Usage" followed by
"-----") or alternatively update the project's markdownlint config to permit ATX
headings; pick one approach and apply it consistently to the "Module Structure",
"Usage" and any other headings in README.md so all headings follow the same
style.


```rust
use pymath::{gamma, lgamma};
use pymath::math::{gamma, lgamma};

fn main() {
// Get the same results as Python's math.gamma and math.lgamma
Expand Down
10 changes: 10 additions & 0 deletions proptest-regressions/cmath.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc d9f364ff419553f1fcf92be2834534eb8bd5faacc285ed6587b5a3c827b235bd # shrinks to re = 1.00214445281558e32, im = -3.0523230669839245e-65
cc 26832b65255697171026596e8fbeee37e5a2bf82133ca5cff83c21b95add285f # shrinks to re = 5.946781139174558e-217, im = 3.4143760786656616e281
cc 78224345cd95a0451f2f83872fb7ca6f9462c7eed58bf5490d6b9b717c5b8e02 # shrinks to re = -9.234931944778561e90, im = 1.0662656145085839e88
cc a1b5e651ca7e81b1cf0fee4c7fb4a982982d24a10b4d0b27ae38559b70f0e9db # shrinks to re = -0.6032998606032244, im = -4.778999871811813e-156
7 changes: 7 additions & 0 deletions proptest-regressions/cmath/misc.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 6b3810fffb8caa48de63b8c593267f33b68ed7c82057a67ee94dd2077e1d1813 # shrinks to re = 4.861109893051668e77, im = -4.975947432969132e-264
11 changes: 11 additions & 0 deletions proptest-regressions/cmath/trigonometric.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc a0c58bc19132b1fba3a2edc94204f7582b5dbea7b87139f582da75f58aefa06e # shrinks to re = 2.4494096702311403e-49, im = -1.2388169541772939e300
cc 409f359905a7e77eacdbd4643c3a1483cc128e46a1c06795c246f3d57a2e61b9 # shrinks to re = -4.17454178893395e-69, im = 7.373554801445074e178
cc 5acbcf76c3bcbaf0b2b89a987e38c75265434f96fdc11598830e2221df72fc53 # shrinks to re = 0.00010260965539526095, im = 4.984721877290597e-19
cc ac04191f916633de0408e071f5f6ada8f7fe7a1be02caca7a32f37ecb148a5ac # shrinks to re = 3.049837651806167e74, im = -2.222842222335753e-166
cc 77ec3c08d2e057fb9467eb13c3b953e4e30f2800259d3653f7bcdfcbaf53614f # shrinks to re = 7.812038268590211e52, im = -2.623972069152808e-109
13 changes: 0 additions & 13 deletions proptest-regressions/gamma.txt

This file was deleted.

7 changes: 7 additions & 0 deletions proptest-regressions/math.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 6f6becc96f663a83d20def559f516c7c5ce1a90b87c373d6c025dd3ab8f1fc39 # shrinks to x = 5.868849392888587e-309, y = 1.985586796867676e-308
7 changes: 7 additions & 0 deletions proptest-regressions/math/aggregate.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc f58d401e380fa9d3a8dd5b137a668be6bd437776f47186a9479aee07c0aa28b8 # shrinks to p1 = 0.0, p2 = 0.0, q1 = -1.156587418587806e301, q2 = -1.315804087909368e-150
10 changes: 10 additions & 0 deletions proptest-regressions/math/exponential.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc c756e015f8e09d21d37e7f805e54f9270cac137e4570d10ecd5302d752e2559f # shrinks to x = -2.9225692145627912e-248, y = 1.565985096226121e-308
cc 2f4b02dd2c2019dee35dcf980b7b4f077899289a7005bba21f849776768eda7a # shrinks to x = 6.095938323843682e143
cc b52b190a89a473a7e5269f9a3efa83735318ae3bd52f3340c32bc4a40f4cda88 # shrinks to x = -0.0, y = -9.787367203123051e54
cc a34a3387ec2547faa88352529f9730d47c6202f9b418a0b4474c9ebb850081e7 # shrinks to x = -1.3916042894622981e-207
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 531a136f9fcde9d1da1ba5d173e62eee8ec8f7c877eb34abbc6d47611a641bc7 # shrinks to x = 0.0
cc ac5c11a6ec8450aef2b03644e8b46a066f81c689b55f9657fcd0ae73d5829bc2 # shrinks to x = -1.8815128643365582
Loading
Loading