diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 0b70032..91094c5 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -130,6 +130,31 @@ jobs: - name: Run tests run: bash ./tests/test_decimal128.sh + # ── Test: BigFloat ───────────────────────────────────────────────────────── + test-bigfloat: + name: Test BigFloat + runs-on: ubuntu-22.04 + timeout-minutes: 30 + env: + DEBIAN_FRONTEND: noninteractive + steps: + - uses: actions/checkout@v4 + - name: Install pixi + run: curl -fsSL https://pixi.sh/install.sh | sh + - name: Add pixi to PATH + run: | + echo "PIXI_HOME=$HOME/.pixi" >> $GITHUB_ENV + echo "$HOME/.pixi/bin" >> $GITHUB_PATH + - name: pixi install + run: pixi install + - name: Install MPFR + run: sudo apt-get update && sudo apt-get install -y libmpfr-dev + - name: Build packages + run: | + pixi run mojo package src/decimo && cp decimo.mojopkg tests/ + - name: Run tests + run: bash ./tests/test_bigfloat.sh + # ── Test: TOML parser ───────────────────────────────────────────────────── test-toml: name: Test TOML parser @@ -197,6 +222,26 @@ jobs: - name: Build & run Python tests run: pixi run testpy + # ── Doc generation check ────────────────────────────────────────────────────── + doc-check: + name: Doc generation + runs-on: ubuntu-22.04 + timeout-minutes: 10 + env: + DEBIAN_FRONTEND: noninteractive + steps: + - uses: actions/checkout@v4 + - name: Install pixi + run: curl -fsSL https://pixi.sh/install.sh | sh + - name: Add pixi to PATH + run: | + echo "PIXI_HOME=$HOME/.pixi" >> $GITHUB_ENV + echo "$HOME/.pixi/bin" >> $GITHUB_PATH + - name: pixi install + run: pixi install + - name: Generate docs + run: pixi run doc + # ── Format check ───────────────────────────────────────────────────────────── format-check: name: Format check diff --git a/docs/plans/gmp_integration.md b/docs/plans/gmp_integration.md index c02acd0..37af425 100644 --- a/docs/plans/gmp_integration.md +++ b/docs/plans/gmp_integration.md @@ -170,6 +170,262 @@ Where GMP does **not** help: 3. **Base-10 operations**: GMP works in binary; base-10 output requires conversion 4. **Decimal arithmetic**: GMP has no native decimal type; scale management would remain in Mojo +### 4.5 MPFR Memory Layout & Data Flow + +How data flows from Mojo `BigFloat` → C wrapper → MPFR library. + +#### 4.5.1 MPFR's `mpfr_t` Internal Structure (32 bytes on ARM64/x86_64) + +```txt +mpfr_t (typedef: __mpfr_struct[1]) +┌─────────────────────────────────────────────────────────────────┐ +│ offset field type meaning │ +├─────────────────────────────────────────────────────────────────┤ +│ 0 _mpfr_prec mpfr_prec_t precision in bits (≥2) │ +│ (8 bytes) (long) │ +├─────────────────────────────────────────────────────────────────┤ +│ 8 _mpfr_sign mpfr_sign_t +1 or −1 │ +│ (4 bytes) (int) │ +├─────────────────────────────────────────────────────────────────┤ +│ 12 (padding) 4 bytes align _mpfr_exp to 8-byte │ +│ boundary (long requires it) │ +├─────────────────────────────────────────────────────────────────┤ +│ 16 _mpfr_exp mpfr_exp_t binary exponent │ +│ (8 bytes) (long) (value = man × 2^exp) │ +├─────────────────────────────────────────────────────────────────┤ +│ 24 _mpfr_d mp_limb_t* → heap-allocated limb array │ +│ (8 bytes) (uint64_t*) (mantissa bits, MSB first) │ +└─────────────────────────────────────────────────────────────────┘ + Total: 32 bytes (struct) + heap limbs +``` + +**Limb Array Detail** + +The `_mpfr_d` pointer points to a heap-allocated array of 64-bit "limbs": + +```txt +_mpfr_d ────────┐ + ▼ + ┌────────────┬────────────┬────────┬────────────┐ + │ limb[0] │ limb[1] │ ... │ limb[n-1] │ + │ (64 bits) │ (64 bits) │ │ (64 bits) │ + └────────────┴────────────┴────────┴────────────┘ + MSB of mantissa ───────────────────────► LSB + n = ceil(precision / 64) + e.g. 200-bit precision → 4 limbs → 32 bytes of mantissa +``` + +**Value Representation** + +```txt +value = sign × mantissa × 2^(exponent − precision) + +Example: π at 200-bit precision + sign = +1 + prec = 200 + exp = 2 (so π ≈ mantissa × 2^(2−200)) + limbs = [0xC90FDAA2..., 0x2168C234..., 0xC4C6628B..., 0x80DC1CD1...] +``` + +#### 4.5.2 C Wrapper Handle Pool (`gmp_wrapper.c`) + +The C wrapper pre-allocates a flat array of 4096 "slots", each 64 bytes +(over-allocated from MPFR's 32-byte struct for ABI safety). + +```txt +handle_pool[4096][64] handle_in_use[4096] +┌──────────────────────────────────────────┐ ┌───────────────────┐ +│ slot[0] (64 bytes, 16-byte aligned) │ │ [0] = 1 (in use) │ +│ ┌──────┬──────┬──────┬──────┬──────────┐ │ │ │ +│ │ prec │ sign │ exp │ *d ──┼──→ heap │ │ │ │ +│ │ 8B │ 4B │ 8B │ 8B │ (unused) │ │ │ │ +│ └──────┴──────┴──────┴──────┴──────────┘ │ │ │ +├──────────────────────────────────────────┤ ├───────────────────┤ +│ slot[1] (64 bytes) │ │ [1] = 1 (in use) │ +│ ┌──────┬──────┬──────┬──────┬──────────┐ │ │ │ +│ │ prec │ sign │ exp │ *d ──┼──→ heap │ │ │ │ +│ └──────┴──────┴──────┴──────┴──────────┘ │ │ │ +├──────────────────────────────────────────┤ ├───────────────────┤ +│ slot[2] (64 bytes) │ │ [2] = 0 (free) │ +│ ┌──────────────────────────────────────┐ │ │ │ +│ │ (uninitialized / cleared) │ │ │ │ +│ └──────────────────────────────────────┘ │ │ │ +├──────────────────────────────────────────┤ ├───────────────────┤ +│ ... │ │ ... │ +├──────────────────────────────────────────┤ ├───────────────────┤ +│ slot[4095] │ │ [4095] = 0 (free) │ +└──────────────────────────────────────────┘ └───────────────────┘ + +Memory: _Alignas(16) char[4096][64] int[4096] + = 256 KB (static, pre-allocated) +``` + +#### 4.5.3 End-to-End Data Flows + +**Construction: `BigFloat("3.14", precision=100)`** + +```txt +Mojo (bigfloat.mojo) C wrapper (gmp_wrapper.c) MPFR library +───────────────────── ───────────────────────── ──────────── + +① _dps_to_bits(100) + = 100 × 4 + 64 + = 464 bits + +② mpfrw_init(464) ─────────→ ③ Find free slot (say i=0) + via external_call handle_in_use[0] = 1 + p_init2(&slot[0], 464) ──→ ④ mpfr_init2: + slot[0].prec = 464 + slot[0].sign = +1 + slot[0].exp = 0 + slot[0].*d = malloc(8 limbs) + ← return handle = 0 + +⑤ mpfrw_set_str(0, ⑥ Copy "3.14" to buf\0 + "3.14", 4) ─────────────→ p_set_str(&slot[0], ──→ ⑦ mpfr_set_str: + via external_call buf, 10, RNDN) parse "3.14" + set mantissa limbs + set exponent = 2 + ← return 0 (success) + +self.handle = 0 +self.precision = 100 +``` + +**Arithmetic: `a + b` (both are BigFloat)** + +```txt +Mojo C wrapper MPFR +──── ───────── ──── + +① mpfrw_init(464) ──────────→ alloc slot[2] + result handle = 2 handle_in_use[2] = 1 → mpfr_init2 + +② mpfrw_add(2, 0, 1) ──────→ ③ CHECK_HANDLES_3(2, 0, 1) + via external_call p_add(&slot[2], → ④ mpfr_add: + &slot[0], r.limbs = a.limbs + b.limbs + &slot[1], r.exp adjusted + RNDN) rounding applied + ← (void) + +⑤ return BigFloat( + _handle=2, + _precision=max(a,b)) +``` + +**String Export: `bf.to_string(30)`** + +```txt +Mojo C wrapper MPFR +──── ───────── ──── + +① mpfrw_get_str(0, 30) ────→ ② buf = malloc(94) + returns Int (raw address) p_snprintf(buf, 94, → ③ mpfr_snprintf: + "%.*Rg", 30, format mantissa limbs + &slot[0]) as decimal string + "3.14..." + ← return buf (raw address) + +④ _read_c_string(addr) + strlen(addr) → length + memcpy into List[Byte] + → String + +⑤ mpfrw_free_str(addr) ────→ ⑥ free(buf) +``` + +**Destruction: `__del__`** + +```txt +Mojo C wrapper MPFR +──── ───────── ──── + +① mpfrw_clear(0) ──────────→ ② p_clear(&slot[0]) → ③ mpfr_clear: + via external_call handle_in_use[0] = 0 free(slot[0].*d) + (limb array freed) + slot[0] bytes now stale + but slot is reusable +``` + +#### 4.5.4 BigFloat ↔ BigDecimal Conversion + +The two directions use different strategies: + +**BigFloat → BigDecimal** (`to_bigdecimal`): Uses `mpfr_get_str` to export raw +decimal digits and a base-10 exponent directly, then constructs the BigDecimal +via a single `memcpy` — no intermediate `String` or full `parse_numeric_string`. + +**BigDecimal → BigFloat**: Uses `BigDecimal.to_string()` → `mpfr_set_str` (the +standard string round-trip, since BigDecimal already stores base-10 digits). + +```txt +┌─────────────┐ mpfr_get_str (raw digits + exp) ┌──────────────┐ +│ BigFloat │ ─────────────────────────────────→ │ BigDecimal │ +│ (MPFR │ direct construction (memcpy) │ (coefficient │ +│ binary) │ │ + scale │ +│ │ ←───────────────────────────────── │ + sign) │ +│ │ .to_string() → mpfr_set_str │ │ +└─────────────┘ └──────────────┘ +``` + +Why asymmetric? The `to_bigdecimal` path avoids the overhead of formatting a +full decimal string and then re-parsing it. `mpfr_get_str` returns exactly the +significant digits needed, and the exponent is a separate integer, so BigDecimal +can be assembled directly. The reverse direction still goes through a string +because `mpfr_set_str` handles all the base-2 conversion internally. + +#### 4.5.5 Library Loading: dlopen Chain + +```txt + mpfrw_available() + │ + ▼ + ┌─── mpfrw_load() ────┐ + │ mpfr_load_attempted │ + │ == 0 ? │ + └────────┬────────────┘ + │ yes + ▼ + ┌── check DECIMO_NOGMP ──┐ + │ env var set? │ + └────────┬───────────────┘ + │ no + ▼ + ┌── dlopen("libmpfr.dylib") ───────────────────────┐ + │ try paths in order: │ + │ 1. libmpfr.dylib (rpath) │ + │ 2. /opt/homebrew/lib/libmpfr.dylib (macOS ARM) │ + │ 3. /usr/local/lib/libmpfr.dylib (macOS x86) │ + │ 4. libmpfr.so (Linux rpath) │ + │ 5. /usr/lib/.../libmpfr.so (Debian) │ + └────────────────┬─────────────────────────────────┘ + │ success + ▼ + ┌── dlsym for each function pointer ──┐ + │ p_init2 = dlsym("mpfr_init2") │ + │ p_clear = dlsym("mpfr_clear") │ + │ p_set_str = dlsym("mpfr_set_str") │ + │ p_add = dlsym("mpfr_add") │ + │ p_sqrt = dlsym("mpfr_sqrt") │ + │ p_snprintf = dlsym("mpfr_snprintf") │ + │ ... (21 function pointers total) │ + └─────────────────────────────────────┘ + │ + ▼ + ┌── verify critical pointers ──┐ + │ if any of init2, clear, │ + │ set_str, get_str, add, sub, │ + │ mul, div, neg, abs, cmp │ + │ is NULL → fail, dlclose │ + └──────────────┬───────────────┘ + │ all OK + ▼ + return 1 (available) +``` + +The result is cached: `mpfr_load_attempted = 1`. Subsequent calls skip all +of the above and return immediately. + ## 5. Performance Comparison: GMP vs Decimo All benchmarks run side-by-side in a single binary (`bench_gmp_vs_decimo.mojo`), compiled @@ -938,11 +1194,11 @@ int mpfrw_load(void) { // Try common library paths const char *paths[] = { - "libmpfr.dylib", // macOS rpath + "libmpfr.dylib", // macOS rpath "/opt/homebrew/lib/libmpfr.dylib", // macOS ARM64 "/usr/local/lib/libmpfr.dylib", // macOS x86_64 "libmpfr.so", // Linux rpath - "/usr/lib/x86_64-linux-gnu/libmpfr.so", // Debian/Ubuntu + "/usr/lib/x86_64-linux-gnu/libmpfr.so", // Debian/Ubuntu "/usr/lib/libmpfr.so", // Generic Linux NULL }; @@ -1004,6 +1260,85 @@ runtime when `gmp=True` is actually called. | **Now** (Mojo 0.26.2) | C wrapper + `dlopen`/`dlsym` | Link wrapper only; MPFR loaded lazily at runtime | | **Future** (DLHandle) | Mojo-native `DLHandle` | Pure Mojo detection, no C wrapper needed | +### 10.5 End-User Build & Run Guide (BigFloat) + +BigFloat requires linking the C wrapper at build time and having MPFR available at +runtime. The steps differ from pure-Mojo types (BigDecimal, BigInt, etc.) which work +with a bare `mojo run`. + +#### Prerequisites + +```bash +# macOS +brew install mpfr + +# Linux (Debian/Ubuntu) +sudo apt install libmpfr-dev +``` + +#### Step 1: Build the C wrapper + +```bash +# From the decimo source directory +bash src/decimo/gmp/build_gmp_wrapper.sh +# Produces: src/decimo/gmp/libdecimo_gmp_wrapper.dylib (macOS) +# or: src/decimo/gmp/libdecimo_gmp_wrapper.so (Linux) +``` + +The wrapper compiles with any C compiler — no MPFR headers needed at compile time +(it uses `dlopen`/`dlsym` internally). + +#### Step 2: Build your Mojo binary + +`mojo run` cannot link external libraries (`-Xlinker` flags are silently ignored in +JIT mode). You must use `mojo build`: + +```bash +# macOS +mojo build -I src \ + -Xlinker -L./src/decimo/gmp -Xlinker -ldecimo_gmp_wrapper \ + -o myprogram myprogram.mojo + +# Linux (same command) +mojo build -I src \ + -Xlinker -L./src/decimo/gmp -Xlinker -ldecimo_gmp_wrapper \ + -o myprogram myprogram.mojo +``` + +#### Step 3: Run with library path + +The OS needs to find the `.dylib`/`.so` at runtime: + +```bash +# macOS +DYLD_LIBRARY_PATH=./src/decimo/gmp ./myprogram + +# Linux +LD_LIBRARY_PATH=./src/decimo/gmp ./myprogram +``` + +#### All-in-one example + +```bash +bash src/decimo/gmp/build_gmp_wrapper.sh \ +&& mojo build -I src \ + -Xlinker -L./src/decimo/gmp -Xlinker -ldecimo_gmp_wrapper \ + -o /tmp/myprogram myprogram.mojo \ +&& DYLD_LIBRARY_PATH=./src/decimo/gmp /tmp/myprogram +``` + +#### Comparison with pure-Mojo types + +| Type | Build command | Extra dependencies | Library path needed? | +| ------------ | ----------------------------------------------- | ------------------ | -------------------- | +| BigDecimal | `mojo run -I src myprogram.mojo` | None | No | +| BigInt | `mojo run -I src myprogram.mojo` | None | No | +| Dec128 | `mojo run -I src myprogram.mojo` | None | No | +| **BigFloat** | `mojo build -I src -Xlinker ... myprogram.mojo` | MPFR + C wrapper | **Yes** | + +When Mojo gains native `DLHandle` support, the C wrapper can be eliminated and BigFloat +will work with `mojo run` like the other types. + ## 11. Cross-Platform Considerations ### 11.1 Platform Matrix @@ -1215,15 +1550,18 @@ python-flint wraps FLINT (which includes Arb) for Python. > rounding — so BigFloat operations are single MPFR calls, and BigDecimal sugar is > just BigFloat under the hood. +### Phase 0: Prerequisites + +- [x] **0.1 Install MPFR** + `brew install mpfr` (macOS). Verify: `/opt/homebrew/lib/libmpfr.dylib` exists, + `#include ` compiles. MPFR depends on GMP (already installed). + ### Phase 1: Foundation — MPFR C Wrapper & BigFloat **Goal**: Get the MPFR-based C wrapper working, build the `BigFloat` struct as a first-class user-facing type, and prove the pipeline with `sqrt`. -- [ ] **1.1 Install MPFR** (Tiny, no deps) - `brew install mpfr` (macOS). Verify: `/opt/homebrew/lib/libmpfr.dylib` exists, - `#include ` compiles. MPFR depends on GMP (already installed). -- [ ] **1.2 Extend C wrapper with `mpfr_t` handle pool** (Medium, deps: 1.1) +- [x] **1.1 Extend C wrapper with `mpfr_t` handle pool** Add to `gmp_wrapper.c`: (a) `static mpfr_t f_handles[MAX_HANDLES]` pool, (b) `mpfrw_init(prec_bits) → handle`, @@ -1234,46 +1572,52 @@ first-class user-facing type, and prove the pipeline with `sqrt`. Keep existing `mpz_t` wrappers intact for BigInt. **Add precision cap**: reject `prec_bits > MAX_PREC` (e.g. 1M bits ≈ 300K digits) to prevent OOM, inspired by Arb's polynomial time guarantee. -- [ ] **1.3 Add MPFR arithmetic wrappers** (Small, deps: 1.2) + *Done: MAX_HANDLES=4096, MAX_PREC_BITS=1048576.* +- [x] **1.2 Add MPFR arithmetic wrappers** (Small, deps: 1.1) One-line C wrappers: `mpfrw_add(r,a,b)`, `mpfrw_sub(r,a,b)`, `mpfrw_mul(r,a,b)`, `mpfrw_div(r,a,b)`, `mpfrw_sqrt(r,a)`, `mpfrw_neg(r,a)`, `mpfrw_abs(r,a)`, `mpfrw_cmp(a,b)`. All use `MPFR_RNDN` (round-to-nearest). Each is 1 line. -- [ ] **1.4 Add MPFR transcendental wrappers** (Small, deps: 1.2) +- [x] **1.3 Add MPFR transcendental wrappers** (Small, deps: 1.1) `mpfrw_exp(r,a)`, `mpfrw_log(r,a)`, `mpfrw_sin(r,a)`, `mpfrw_cos(r,a)`, `mpfrw_tan(r,a)`, `mpfrw_const_pi(r)`, `mpfrw_pow(r,a,b)`, `mpfrw_rootn_ui(r,a,n)`. Each is 1 line. -- [ ] **1.5 Implement lazy `dlopen` loading** (Medium, deps: 1.3, 1.4) +- [x] **1.4 Implement lazy `dlopen` loading** (Medium, deps: 1.2, 1.3) Wrap all MPFR calls behind function pointers resolved via `dlopen`/`dlsym` at first use (see Section 10.4). This makes the wrapper compilable without MPFR headers. Add `mpfrw_available() → 0/1`. **Add `DECIMO_NOGMP` check**: if set, `mpfrw_available()` returns 0 without attempting `dlopen` (inspired by mpmath's `MPMATH_NOGMPY`). -- [ ] **1.6 Compile wrapper, run smoke test** (Small, deps: 1.5) + *Done: 8 library search paths (macOS + Linux), DECIMO_NOGMP implemented.* +- [x] **1.5 Compile wrapper, run smoke test** (Small, deps: 1.4) Build `libdecimo_gmp_wrapper.dylib` linking against `-lmpfr -lgmp`. Write minimal Mojo test: `mpfrw_init(200) → set_str("3.14") → mpfrw_sqrt → get_str → print`. Verify output. -- [ ] **1.7 Create `src/decimo/bigfloat/bigfloat.mojo`** (Medium, deps: 1.6) + *Done: wrapper compiles without -lmpfr (dlopen). 14 smoke tests pass.* +- [x] **1.6 Create `src/decimo/bigfloat/bigfloat.mojo`** (Medium, deps: 1.5) Implement `BigFloat` struct (single `handle: Int32` field). Constructor from string via `mpfrw_set_str`. Constructor from BigDecimal via `str(bd) → mpfrw_set_str`. `to_bigdecimal(precision) → mpfrw_get_str → BigDecimal(s)`. Destructor calls `mpfrw_clear`. Use guard bits: init MPFR with `dps_to_prec(precision) + 20` bits. -- [ ] **1.8 BigFloat arithmetic and transcendentals** (Medium, deps: 1.7) + *Done: ~320 lines. Also constructors from Int. Aliases: BFlt, Float.* +- [x] **1.7 BigFloat arithmetic and transcendentals** (Medium, deps: 1.6) Wire all MPFR wrappers into BigFloat methods: `sqrt()`, `exp()`, `ln()`, `sin()`, `cos()`, `tan()`, `root(n)`, `power(y)`, `+`, `-`, `*`, `/`, `pi()`. Each is a thin wrapper around the corresponding `mpfrw_*` call. -- [ ] **1.9 BigFloat ↔ BigDecimal conversion** (Small, deps: 1.7) + *Done: all listed + `__neg__`, `__abs__`, comparisons (`__eq__`, `__ne__`, `__lt__`, `__le__`, `__gt__`, `__ge__`).* +- [x] **1.8 BigFloat ↔ BigDecimal conversion** (Small, deps: 1.6) `BigFloat(bd: BigDecimal, precision)` and `bf.to_bigdecimal(precision)`. Both go through string representation (`mpfr_set_str` / `mpfr_get_str`). -- [ ] **1.10 Test BigFloat correctness** (Medium, deps: 1.8) +- [ ] **1.9 Test BigFloat correctness** (Medium, deps: 1.7) For each operation × x ∈ {2, 3, 0.5, 100, 10⁻⁸, 10¹⁰⁰⁰} × P ∈ {28, 100, 500, 1000, 5000}: verify BigFloat results match BigDecimal results to P digits. Edge cases: 0, negative, very large/small. -- [ ] **1.11 Benchmark BigFloat** (Small, deps: 1.10) +- [ ] **1.10 Benchmark BigFloat** (Small, deps: 1.9) Measure wall time for BigFloat vs BigDecimal across operations and precisions. Find break-even precision where BigFloat wins. Expected: above ~50–100 digits. -- [ ] **1.12 Update build system** (Small, deps: 1.2) +- [x] **1.11 Update build system** (Small, deps: 1.1) `pixi.toml` tasks: `build-gmp-wrapper` (compiles C wrapper + links MPFR). `build_gmp_wrapper.sh` for macOS + Linux. + *Done: pixi tasks (buildgmp, testfloat, tf), test_bigfloat.sh, CI job in run_tests.yaml.* **Deliverable**: `BigFloat("2", 5000).sqrt()` works. Users can construct BigFloat, chain operations, and convert back to BigDecimal. @@ -1322,7 +1666,8 @@ ops are less user-facing. ### Phase 4: Polish, Distribution & Future -- [ ] **4.1 Cross-platform wrapper compilation (Linux)** (Small, deps: Phase 1) +- [x] **4.1 Cross-platform wrapper compilation (Linux)** (Small, deps: Phase 1) + *Done: build_gmp_wrapper.sh handles Linux (.so, -fPIC, -ldl). CI verified on ubuntu-22.04.* - [ ] **4.2 Pre-built wrapper binaries (macOS ARM64, Linux x86)** (Medium, deps: 4.1) - [ ] **4.3 CLI `--mode float` and `--gmp` flags** (Medium, deps: Phase 2) `--mode float` evaluates in BigFloat; `--gmp` enables `gmp=True` on BigDecimal ops. @@ -1339,10 +1684,10 @@ ops are less user-facing. ### Estimated Effort Summary -- **Phase 1: MPFR wrapper + BigFloat** — 12 steps, Medium complexity (most work is C wrapper + BigFloat struct). **HIGH** priority. +- **Phase 1: MPFR wrapper + BigFloat** — 11 steps, Medium complexity (most work is C wrapper + BigFloat struct). **HIGH** priority. - **Phase 2: BigDecimal gmp=True sugar** — 7 steps, **Low** complexity (each op delegates to BigFloat). **HIGH** priority. - **Phase 3: BigInt backend** — 4 steps, Medium complexity. Deferred. -- **Phase 4: Polish + lazy eval** — 8 steps, varies. Low priority. +- **Phase 4: Polish + lazy eval** — 8 steps, **1/8 done** (4.1 Linux build). Low priority. ## 15. Appendix: Prototype Results diff --git a/pixi.toml b/pixi.toml index 04ffc67..2c3cc2c 100644 --- a/pixi.toml +++ b/pixi.toml @@ -26,12 +26,12 @@ format = """pixi run mojo format ./src \ &&pixi run ruff format ./python""" # doc -doc = """pixi run mojo doc \ ---diagnose-missing-doc-strings --validate-doc-strings src/decimo""" +doc = "pixi run mojo doc --diagnose-missing-doc-strings src/decimo" # compile the package p = "clear && pixi run package" package = """pixi run format \ +&& pixi run doc \ && pixi run package_decimo""" package_decimo = """pixi run mojo package src/decimo \ && cp decimo.mojopkg tests/ \ @@ -43,21 +43,25 @@ c = "clear && pixi run clean" clean = """rm tests/decimo.mojopkg && \ rm benches/decimo.mojopkg""" +# gmp/mpfr wrapper (compile C wrapper — no MPFR needed at build time) +bgmp = "clear && pixi run buildgmp" +buildgmp = "bash src/decimo/gmp/build_gmp_wrapper.sh" + # tests (use the mojo testing tool) -t = "clear && pixi run test" -tdecimo = "clear && pixi run testdecimo" test = "pixi run package && bash ./tests/test_all.sh" testdecimo = "pixi run package && bash ./tests/test_decimo.sh" testtoml = "pixi run package && bash ./tests/test_toml.sh" +# bigfloat (build test binary linking the C wrapper, then run it) +testfloat = "pixi run buildgmp && bash ./tests/test_bigfloat.sh" +# shortcut for tests +t = "clear && pixi run test" +tdecimo = "clear && pixi run testdecimo" +tf = "clear && pixi run testfloat" ttoml = "clear && pixi run testtoml" # bench bench = "pixi run package && bash benches/run_bench.sh" -# gmp/mpfr wrapper (compile C wrapper — no MPFR needed at build time) -bgmp = "clear && pixi run buildgmp" -buildgmp = "bash src/decimo/gmp/build_gmp_wrapper.sh" - # bench with debug assertions enabled bdec_debug = """clear && pixi run package && cd benches/bigdecimal \ && pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ diff --git a/src/decimo/__init__.mojo b/src/decimo/__init__.mojo index c906185..1bf013c 100644 --- a/src/decimo/__init__.mojo +++ b/src/decimo/__init__.mojo @@ -29,6 +29,7 @@ from .decimal128.decimal128 import Decimal128, Dec128 from .bigint.bigint import BigInt, BInt from .biguint.biguint import BigUInt, BUInt from .bigdecimal.bigdecimal import BigDecimal, BDec, Decimal +from .bigfloat.bigfloat import BigFloat, BFlt, Float from .rounding_mode import ( RoundingMode, ROUND_DOWN, diff --git a/src/decimo/bigfloat/__init__.mojo b/src/decimo/bigfloat/__init__.mojo index 23cc8b5..7e59849 100644 --- a/src/decimo/bigfloat/__init__.mojo +++ b/src/decimo/bigfloat/__init__.mojo @@ -28,3 +28,5 @@ Modules: - bigfloat: Core BigFloat struct with constructors, arithmetic, transcendentals - mpfr_wrapper: Low-level FFI bindings to the MPFR C wrapper """ + +from .bigfloat import BigFloat, BFlt, Float, PRECISION as BIGFLOAT_PRECISION diff --git a/src/decimo/bigfloat/bigfloat.mojo b/src/decimo/bigfloat/bigfloat.mojo index d096813..5da4473 100644 --- a/src/decimo/bigfloat/bigfloat.mojo +++ b/src/decimo/bigfloat/bigfloat.mojo @@ -33,6 +33,11 @@ Design: - RAII: destructor frees MPFR handle via `mpfrw_clear` """ +from std.ffi import external_call, c_char +from std.memory import UnsafePointer + +from decimo.bigdecimal.bigdecimal import BigDecimal +from decimo.biguint.biguint import BigUInt from decimo.bigfloat.mpfr_wrapper import ( mpfrw_available, mpfrw_init, @@ -40,6 +45,8 @@ from decimo.bigfloat.mpfr_wrapper import ( mpfrw_set_str, mpfrw_get_str, mpfrw_free_str, + mpfrw_get_raw_digits, + mpfrw_free_raw_str, mpfrw_add, mpfrw_sub, mpfrw_mul, @@ -61,9 +68,23 @@ from decimo.bigfloat.mpfr_wrapper import ( # Guard bits added to user-requested precision to absorb binary↔decimal rounding. comptime _GUARD_BITS: Int = 64 -# Approximate bits per decimal digit: ceil(log2(10)) ≈ 3.322 → use 4 for safety. +# Approximate bits per decimal digit: ceil(log2(10)) ≈ 3.322. +# Use 4 for safety. comptime _BITS_PER_DIGIT: Int = 4 +# Default precision in decimal digits, same as BigDecimal. +"""Default precision in decimal digits for BigFloat.""" +comptime PRECISION: Int = 28 + +# Short alias, like BDec for BigDecimal. +"""Alias for `BigFloat`.""" +comptime BFlt = BigFloat +# Short alias, like Decimal for BigDecimal. +# Mojo's built-in floating-point types are all with number suffixes +# (e.g., `Float32`, `Float64`), so `Float` is available for BigFloat. +"""Alias for `BigFloat`.""" +comptime Float = BigFloat + fn _dps_to_bits(precision: Int) -> Int: """Converts decimal digit precision to MPFR bit precision with guard bits. @@ -71,28 +92,412 @@ fn _dps_to_bits(precision: Int) -> Int: return precision * _BITS_PER_DIGIT + _GUARD_BITS -# TODO: Implement BigFloat struct. -# -# struct BigFloat: -# var handle: Int32 -# -# fn __init__(out self, value: String, precision: Int) raises: -# ... -# -# fn __init__(out self, bd: BigDecimal, precision: Int) raises: -# ... -# -# fn sqrt(self) raises -> Self: -# ... -# -# fn exp(self) raises -> Self: -# ... -# -# fn ln(self) raises -> Self: -# ... -# -# fn to_bigdecimal(self, precision: Int) raises -> BigDecimal: -# ... -# -# fn __del__(owned self): -# mpfrw_clear(self.handle) +fn _read_c_string(address: Int) -> String: + """Reads a null-terminated C string at the given raw address into a Mojo + String. + + The caller is responsible for freeing the C string afterward. + """ + var length = external_call["strlen", Int]( + address + ) # Exclude null terminator + if length == 0: + return String("") + var buf = List[Byte](capacity=length) + for _ in range(length): + buf.append(0) + external_call["memcpy", NoneType](buf.unsafe_ptr(), address, length) + return String(unsafe_from_utf8=buf^) + + +# ===----------------------------------------------------------------------=== # +# BigFloat +# ===----------------------------------------------------------------------=== # + + +struct BigFloat(Comparable, Movable, Writable): + """Arbitrary-precision binary floating-point type backed by MPFR. + + Each BigFloat owns a single MPFR handle (index into the C wrapper's pool). + Precision is specified in decimal digits and converted to bits internally. + Arithmetic and transcendental operations are single MPFR calls. + + BigFloat is Movable but not Copyable. Transfer ownership with `^`: + + var a = BigFloat("2.0", 100) + var b = a^ # moves a into b; a is consumed + """ + + var handle: Int32 + var precision: Int + + # ===------------------------------------------------------------------=== # + # Constructors + # ===------------------------------------------------------------------=== # + + def __init__(out self, value: String, precision: Int = PRECISION) raises: + """Creates a BigFloat from a decimal string. + + Args: + value: A decimal number string (e.g. "3.14159", "-1.5e10"). + precision: Number of significant decimal digits. + """ + if not mpfrw_available(): + raise Error( + "BigFloat requires MPFR" + " (brew install mpfr / apt install libmpfr-dev)" + ) + var bits = _dps_to_bits(precision) + self.handle = mpfrw_init(bits) + if self.handle < 0: + raise Error("BigFloat: MPFR handle pool exhausted") + self.precision = precision + var s_bytes = value.as_bytes() + var result_code = mpfrw_set_str( + self.handle, + s_bytes.unsafe_ptr().bitcast[c_char](), + Int32(len(s_bytes)), + ) + if result_code != 0: + mpfrw_clear(self.handle) + raise Error("BigFloat: invalid number string: " + value) + + def __init__(out self, value: Int, precision: Int = PRECISION) raises: + """Creates a BigFloat from an integer.""" + self = Self(String(value), precision) + + def __init__( + out self, decimal: BigDecimal, precision: Int = PRECISION + ) raises: + """Creates a BigFloat from a BigDecimal.""" + self = Self(decimal.to_string(), precision) + + def __init__(out self, *, _handle: Int32, _precision: Int): + """Internal: wraps an existing MPFR handle. Caller transfers ownership. + """ + self.handle = _handle + self.precision = _precision + + # ===------------------------------------------------------------------=== # + # Lifecycle + # ===------------------------------------------------------------------=== # + + def __init__(out self, *, deinit take: Self): + """Moves a BigFloat, transferring handle ownership.""" + self.handle = take.handle + self.precision = take.precision + + fn __del__(deinit self): + """Frees the MPFR handle.""" + if self.handle >= 0: + mpfrw_clear(self.handle) + + # ===------------------------------------------------------------------=== # + # String conversion + # ===------------------------------------------------------------------=== # + + def to_string(self, digits: Int = -1) raises -> String: + """Exports the value as a decimal string. + + Args: + digits: Number of significant digits. Defaults to the BigFloat's + precision. + + Returns: + A decimal string representation. + """ + var d = digits if digits > 0 else self.precision + var address = mpfrw_get_str(self.handle, Int32(d)) + if address == 0: + raise Error("BigFloat: failed to export string") + var result = _read_c_string(address) + mpfrw_free_str(address) + return result + + def write_to[W: Writer](self, mut writer: W): + """Writes the decimal string representation to a Writer.""" + if self.handle < 0: + writer.write("BigFloat()") + return + var address = mpfrw_get_str(self.handle, Int32(self.precision)) + if address == 0: + writer.write("BigFloat()") + return + var s = _read_c_string(address) + mpfrw_free_str(address) + writer.write(s) + + def write_repr_to[W: Writer](self, mut writer: W): + """Writes a repr-style string to a Writer.""" + if self.handle < 0: + writer.write('BigFloat("")') + return + var address = mpfrw_get_str(self.handle, Int32(self.precision)) + if address == 0: + writer.write('BigFloat("")') + return + var s = _read_c_string(address) + mpfrw_free_str(address) + writer.write('BigFloat("', s, '")') + + # ===------------------------------------------------------------------=== # + # Conversion + # ===------------------------------------------------------------------=== # + + def to_bigdecimal(self, precision: Int = -1) raises -> BigDecimal: + """Converts this BigFloat to a BigDecimal. + + Uses MPFR's raw digit export to build a BigDecimal directly, + bypassing full string parsing for efficiency. + + Data flow (1 memcpy, 0 intermediate lists): + C: mpfr_get_str → MPFR-allocated digit buffer + exponent + memcpy → Mojo-owned byte buffer (single copy) + byte buffer → pack into base-10⁹ UInt32 words (in-place read) + + Args: + precision: Number of significant decimal digits for the conversion. + Defaults to the BigFloat's own precision. + + Returns: + A BigDecimal with the requested number of significant digits. + """ + var d = precision if precision > 0 else self.precision + + # 1. Get raw digits + exponent in one call + # mpfrw_get_raw_digits calls mpfr_get_str (resolved as p_get_str + # via dlsym). It returns a pure ASCII digit string like + # "31415926535897932385" (possibly "-" prefixed for negatives) and + # writes the base-10 exponent to out_exp. + # Meaning: value = 0. × 10^exp. + var exp = Int(0) + var address = mpfrw_get_raw_digits( + self.handle, Int32(d), UnsafePointer(to=exp) + ) + if address == 0: + raise Error("BigFloat.to_bigdecimal: mpfr_get_str failed") + + # 2. Single memcpy into a Mojo-owned buffer + comptime ASCII_MINUS: UInt8 = 45 # ord("-") + comptime ASCII_ZERO: UInt8 = 48 # ord("0") + var n = external_call["strlen", Int](address) + var buf = List[UInt8](unsafe_uninit_length=n) + external_call["memcpy", NoneType](buf.unsafe_ptr(), address, n) + mpfrw_free_raw_str(address) # Free MPFR allocation immediately. + + # 3. Read bytes from the Mojo buffer (no further copies) + var ptr = buf.unsafe_ptr() + + # Detect sign (negative values have '-' prefix from MPFR). + var sign = False + var digit_start = 0 + if n > 0 and ptr[0] == ASCII_MINUS: + sign = True + digit_start = 1 + + var num_digits = n - digit_start + + # scale = number_of_significant_digits - exponent + # e.g. digits "31415" with exp=1 → 3.1415 → scale = 5 - 1 = 4 + var scale = num_digits - exp + + # 4. Pack ASCII bytes directly into base-10⁹ words + var number_of_words = num_digits // 9 + if num_digits % 9 != 0: + number_of_words += 1 + var words = List[UInt32](capacity=number_of_words) + var end = num_digits + while end >= 9: + var start = end - 9 + var word: UInt32 = 0 + for j in range(start, end): + word = word * 10 + UInt32(ptr[digit_start + j] - ASCII_ZERO) + words.append(word) + end = start + if end > 0: + var word: UInt32 = 0 + for j in range(0, end): + word = word * 10 + UInt32(ptr[digit_start + j] - ASCII_ZERO) + words.append(word) + + var coefficient = BigUInt(raw_words=words^) + return BigDecimal(coefficient=coefficient^, scale=scale, sign=sign) + + # ===------------------------------------------------------------------=== # + # Comparison + # ===------------------------------------------------------------------=== # + + def __eq__(self, other: Self) -> Bool: + """Returns True if self == other.""" + return mpfrw_cmp(self.handle, other.handle) == 0 + + def __ne__(self, other: Self) -> Bool: + """Returns True if self != other.""" + return mpfrw_cmp(self.handle, other.handle) != 0 + + def __lt__(self, other: Self) -> Bool: + """Returns True if self < other.""" + var c = mpfrw_cmp(self.handle, other.handle) + return c != -2 and c < 0 + + def __le__(self, other: Self) -> Bool: + """Returns True if self <= other.""" + var c = mpfrw_cmp(self.handle, other.handle) + return c != -2 and c <= 0 + + def __gt__(self, other: Self) -> Bool: + """Returns True if self > other.""" + var c = mpfrw_cmp(self.handle, other.handle) + return c != -2 and c > 0 + + def __ge__(self, other: Self) -> Bool: + """Returns True if self >= other.""" + var c = mpfrw_cmp(self.handle, other.handle) + return c != -2 and c >= 0 + + # ===------------------------------------------------------------------=== # + # Unary operators + # ===------------------------------------------------------------------=== # + + def __neg__(self) raises -> Self: + """Returns -self.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_neg(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def __abs__(self) raises -> Self: + """Returns |self|.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_abs(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + # ===------------------------------------------------------------------=== # + # Binary arithmetic operators + # ===------------------------------------------------------------------=== # + + def __add__(self, other: Self) raises -> Self: + """Returns self + other.""" + var prec = max(self.precision, other.precision) + var h = mpfrw_init(_dps_to_bits(prec)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_add(h, self.handle, other.handle) + return Self(_handle=h, _precision=prec) + + def __sub__(self, other: Self) raises -> Self: + """Returns self - other.""" + var prec = max(self.precision, other.precision) + var h = mpfrw_init(_dps_to_bits(prec)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_sub(h, self.handle, other.handle) + return Self(_handle=h, _precision=prec) + + def __mul__(self, other: Self) raises -> Self: + """Returns self * other.""" + var prec = max(self.precision, other.precision) + var h = mpfrw_init(_dps_to_bits(prec)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_mul(h, self.handle, other.handle) + return Self(_handle=h, _precision=prec) + + def __truediv__(self, other: Self) raises -> Self: + """Returns self / other.""" + var prec = max(self.precision, other.precision) + var h = mpfrw_init(_dps_to_bits(prec)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_div(h, self.handle, other.handle) + return Self(_handle=h, _precision=prec) + + def __pow__(self, exponent: Self) raises -> Self: + """Returns self ** exponent.""" + return self.power(exponent) + + # ===------------------------------------------------------------------=== # + # Transcendental and math methods + # ===------------------------------------------------------------------=== # + + def sqrt(self) raises -> Self: + """Computes the square root.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_sqrt(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def exp(self) raises -> Self: + """Computes e^self.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_exp(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def ln(self) raises -> Self: + """Computes the natural logarithm.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_log(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def sin(self) raises -> Self: + """Computes the sine.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_sin(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def cos(self) raises -> Self: + """Computes the cosine.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_cos(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def tan(self) raises -> Self: + """Computes the tangent.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_tan(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def power(self, exponent: Self) raises -> Self: + """Computes self raised to the given exponent.""" + var prec = max(self.precision, exponent.precision) + var h = mpfrw_init(_dps_to_bits(prec)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_pow(h, self.handle, exponent.handle) + return Self(_handle=h, _precision=prec) + + def root(self, n: UInt32) raises -> Self: + """Computes the n-th root.""" + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_rootn_ui(h, self.handle, n) + return Self(_handle=h, _precision=self.precision) + + @staticmethod + def pi(precision: Int = PRECISION) raises -> BigFloat: + """Returns π to the specified number of decimal digits.""" + if not mpfrw_available(): + raise Error( + "BigFloat requires MPFR" + " (brew install mpfr / apt install libmpfr-dev)" + ) + var h = mpfrw_init(_dps_to_bits(precision)) + if h < 0: + raise Error("BigFloat: handle allocation failed") + mpfrw_const_pi(h) + return BigFloat(_handle=h, _precision=precision) diff --git a/src/decimo/bigfloat/mpfr_wrapper.mojo b/src/decimo/bigfloat/mpfr_wrapper.mojo index 9dfa42c..3342524 100644 --- a/src/decimo/bigfloat/mpfr_wrapper.mojo +++ b/src/decimo/bigfloat/mpfr_wrapper.mojo @@ -36,8 +36,10 @@ from std.ffi import external_call, c_int, c_char fn mpfrw_available() -> Bool: """Checks if MPFR is available on this system. - Returns True if the C wrapper successfully loaded libmpfr via dlopen. First call triggers the lazy load attempt. Result is cached in C. + + Returns: + True if the C wrapper successfully loaded libmpfr via dlopen. """ var result = external_call["mpfrw_available", c_int]() return result != 0 @@ -51,14 +53,22 @@ fn mpfrw_available() -> Bool: fn mpfrw_init(prec_bits: Int) -> Int32: """Allocates an MPFR handle with the given precision in bits. - Returns a handle index (>= 0) on success, or -1 if the pool is full - or MPFR is unavailable. + Args: + prec_bits: Precision in bits for the MPFR value. + + Returns: + A handle index (>= 0) on success, or -1 if the pool is full + or MPFR is unavailable. """ return external_call["mpfrw_init", Int32](Int32(prec_bits)) fn mpfrw_clear(handle: Int32): - """Frees an MPFR handle, returning it to the pool.""" + """Frees an MPFR handle, returning it to the pool. + + Args: + handle: MPFR handle index to free. + """ external_call["mpfrw_clear", NoneType](handle) @@ -81,7 +91,8 @@ fn mpfrw_set_str( s: Pointer to a decimal string (e.g. "-3.14159"). length: Length of the string (for safety, not relying on null terminator). - Returns 0 on success, non-zero on parse error. + Returns: + 0 on success, non-zero on parse error. """ return external_call["mpfrw_set_str", Int32](handle, s, length) @@ -93,8 +104,9 @@ fn mpfrw_get_str(handle: Int32, digits: Int32) -> Int: handle: MPFR handle index. digits: Number of significant decimal digits to export. - Returns raw address of a null-terminated C string (malloc'd). - Caller must free it with `mpfrw_free_str`. + Returns: + Raw address of a null-terminated C string (malloc'd). + Caller must free it with `mpfrw_free_str`. """ return external_call["mpfrw_get_str", Int](handle, digits) @@ -108,43 +120,124 @@ fn mpfrw_free_str(addr: Int): external_call["mpfrw_free_str", NoneType](addr) +# ===----------------------------------------------------------------------=== # +# Raw digit export (fast BigFloat → BigDecimal) +# ===----------------------------------------------------------------------=== # + + +fn mpfrw_get_raw_digits( + handle: Int32, digits: Int32, out_exp: UnsafePointer[Int, _] +) -> Int: + """Exports MPFR value as raw digit string via mpfr_get_str. + + Returns a pointer to a null-terminated pure digit string (no dot, no + exponent notation). Negative values have a ``-`` prefix. The decimal + exponent is written to ``out_exp``. + + Meaning: raw = ``"31415..."`` with exp = 1 → value = 0.31415… × 10^1. + + The digit string is allocated by MPFR and must be freed with + ``mpfrw_free_raw_str``. + + Args: + handle: MPFR handle index. + digits: Number of significant decimal digits to export. + out_exp: Pointer to an Int; receives the decimal exponent. + + Returns: + Raw address of the digit string, or 0 on failure. + """ + return external_call["mpfrw_get_raw_digits", Int](handle, digits, out_exp) + + +fn mpfrw_free_raw_str(addr: Int): + """Frees a digit string returned by ``mpfrw_get_raw_digits``. + + Args: + addr: Raw address returned by ``mpfrw_get_raw_digits``. + """ + external_call["mpfrw_free_raw_str", NoneType](addr) + + # ===----------------------------------------------------------------------=== # # Arithmetic operations # ===----------------------------------------------------------------------=== # fn mpfrw_add(result: Int32, a: Int32, b: Int32): - """Computes result = a + b (MPFR_RNDN).""" + """Computes result = a + b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Left operand handle. + b: Right operand handle. + """ external_call["mpfrw_add", NoneType](result, a, b) fn mpfrw_sub(result: Int32, a: Int32, b: Int32): - """Computes result = a - b (MPFR_RNDN).""" + """Computes result = a - b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Left operand handle. + b: Right operand handle. + """ external_call["mpfrw_sub", NoneType](result, a, b) fn mpfrw_mul(result: Int32, a: Int32, b: Int32): - """Computes result = a * b (MPFR_RNDN).""" + """Computes result = a * b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Left operand handle. + b: Right operand handle. + """ external_call["mpfrw_mul", NoneType](result, a, b) fn mpfrw_div(result: Int32, a: Int32, b: Int32): - """Computes result = a / b (MPFR_RNDN).""" + """Computes result = a / b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Dividend handle. + b: Divisor handle. + """ external_call["mpfrw_div", NoneType](result, a, b) fn mpfrw_neg(result: Int32, a: Int32): - """Computes result = -a (MPFR_RNDN).""" + """Computes result = -a (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ external_call["mpfrw_neg", NoneType](result, a) fn mpfrw_abs(result: Int32, a: Int32): - """Computes result = |a| (MPFR_RNDN).""" + """Computes result = |a| (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ external_call["mpfrw_abs", NoneType](result, a) fn mpfrw_cmp(a: Int32, b: Int32) -> Int32: - """Compares a and b. Returns <0, 0, or >0. Returns -2 on invalid handle.""" + """Compares a and b. + + Args: + a: Left operand handle. + b: Right operand handle. + + Returns: + -1 if a < b, 0 if equal, 1 if a > b. -2 on invalid handle. + """ return external_call["mpfrw_cmp", Int32](a, b) @@ -154,45 +247,91 @@ fn mpfrw_cmp(a: Int32, b: Int32) -> Int32: fn mpfrw_sqrt(result: Int32, a: Int32): - """Computes result = sqrt(a) (MPFR_RNDN).""" + """Computes result = sqrt(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ external_call["mpfrw_sqrt", NoneType](result, a) fn mpfrw_exp(result: Int32, a: Int32): - """Computes result = exp(a) (MPFR_RNDN).""" + """Computes result = exp(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ external_call["mpfrw_exp", NoneType](result, a) fn mpfrw_log(result: Int32, a: Int32): - """Computes result = ln(a) (MPFR_RNDN).""" + """Computes result = ln(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ external_call["mpfrw_log", NoneType](result, a) fn mpfrw_sin(result: Int32, a: Int32): - """Computes result = sin(a) (MPFR_RNDN).""" + """Computes result = sin(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ external_call["mpfrw_sin", NoneType](result, a) fn mpfrw_cos(result: Int32, a: Int32): - """Computes result = cos(a) (MPFR_RNDN).""" + """Computes result = cos(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ external_call["mpfrw_cos", NoneType](result, a) fn mpfrw_tan(result: Int32, a: Int32): - """Computes result = tan(a) (MPFR_RNDN).""" + """Computes result = tan(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ external_call["mpfrw_tan", NoneType](result, a) fn mpfrw_pow(result: Int32, a: Int32, b: Int32): - """Computes result = a^b (MPFR_RNDN).""" + """Computes result = a^b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Base handle. + b: Exponent handle. + """ external_call["mpfrw_pow", NoneType](result, a, b) fn mpfrw_rootn_ui(result: Int32, a: Int32, n: UInt32): - """Computes result = a^(1/n) (MPFR_RNDN), the n-th root.""" + """Computes result = a^(1/n) (MPFR_RNDN), the n-th root. + + Args: + result: Handle to store the result. + a: Operand handle. + n: Root degree. + """ external_call["mpfrw_rootn_ui", NoneType](result, a, n) fn mpfrw_const_pi(result: Int32): - """Computes result = π (MPFR_RNDN) to the handle's precision.""" + """Computes result = π (MPFR_RNDN) to the handle's precision. + + Args: + result: Handle to store the result. + """ external_call["mpfrw_const_pi", NoneType](result) diff --git a/src/decimo/gmp/gmp_wrapper.c b/src/decimo/gmp/gmp_wrapper.c index b2eaf46..861ba0d 100644 --- a/src/decimo/gmp/gmp_wrapper.c +++ b/src/decimo/gmp/gmp_wrapper.c @@ -273,6 +273,37 @@ void mpfrw_free_str(char *s) { if (s) free(s); } +/* ===----------------------------------------------------------------------=== * + * Raw digit export via mpfr_get_str (for fast BigFloat → BigDecimal) + * + * p_get_str is mpfr_get_str resolved at runtime via dlsym. It returns a + * pure digit string plus a separate base-10 exponent: + * + * raw = "31415...", exp = 1 → value = 0.31415... × 10^1 = 3.1415... + * + * No dot, no 'e' notation. Negative values have a '-' prefix. + * The exponent is written to the caller through the out_exp pointer — + * no mutable global state, no two-call pattern. + * + * The returned string is allocated by MPFR and must be freed with + * mpfrw_free_raw_str(). + * ===----------------------------------------------------------------------=== */ + +char* mpfrw_get_raw_digits(int handle, int digits, long *out_exp) { + if (handle < 0 || handle >= MAX_HANDLES || !handle_in_use[handle]) return NULL; + if (!p_get_str) return NULL; + if (digits < 1) digits = 1; + long exp = 0; + char *raw = p_get_str(NULL, &exp, 10, (size_t)digits, + &handle_pool[handle], MPFR_RNDN); + if (out_exp) *out_exp = exp; + return raw; /* Caller frees with mpfrw_free_raw_str */ +} + +void mpfrw_free_raw_str(char *s) { + if (s && p_free_str) p_free_str(s); +} + /* ===----------------------------------------------------------------------=== * * Arithmetic operations * ===----------------------------------------------------------------------=== */ @@ -318,7 +349,11 @@ void mpfrw_abs(int r, int a) { int mpfrw_cmp(int a, int b) { if (a < 0 || a >= MAX_HANDLES || !handle_in_use[a]) return -2; /* error sentinel */ if (b < 0 || b >= MAX_HANDLES || !handle_in_use[b]) return -2; /* error sentinel */ - return p_cmp(&handle_pool[a], &handle_pool[b]); + int result = p_cmp(&handle_pool[a], &handle_pool[b]); + /* Normalize to -1/0/1 so that -2 stays reserved as error sentinel. */ + if (result < 0) return -1; + if (result > 0) return 1; + return 0; } /* ===----------------------------------------------------------------------=== * diff --git a/tests/bigfloat/test_bigfloat.mojo b/tests/bigfloat/test_bigfloat.mojo new file mode 100644 index 0000000..e59270d --- /dev/null +++ b/tests/bigfloat/test_bigfloat.mojo @@ -0,0 +1,228 @@ +# ===----------------------------------------------------------------------=== # +# Copyright 2025 Yuhao Zhu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ===----------------------------------------------------------------------=== # + +"""Smoke tests for BigFloat: verify MPFR pipeline works end-to-end.""" + +from decimo.bigfloat.bigfloat import BigFloat, PRECISION +from decimo.bigfloat.mpfr_wrapper import mpfrw_available + + +def test_mpfr_available() raises: + print("test_mpfr_available ... ", end="") + if not mpfrw_available(): + print("SKIPPED (MPFR not installed)") + return + print("OK") + + +def test_construct_from_string() raises: + print("test_construct_from_string ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("3.14159") + print("OK value =", x) + + +def test_construct_from_int() raises: + print("test_construct_from_int ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat(42) + print("OK value =", x) + + +def test_sqrt() raises: + print("test_sqrt ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("2.0", precision=50) + var s = x.sqrt() + var result = s.to_string(50) + # sqrt(2) ≈ 1.4142135623730950488... + if not result.startswith("1.4142"): + raise Error("FAIL test_sqrt got: " + result) + print("OK sqrt(2) =", result) + + +def test_exp() raises: + print("test_exp ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("1.0", precision=50) + var e = x.exp() + var result = e.to_string(50) + # exp(1) ≈ 2.71828182845904523536... + if not result.startswith("2.7182"): + raise Error("FAIL test_exp got: " + result) + print("OK exp(1) =", result) + + +def test_ln() raises: + print("test_ln ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("2.0", precision=30) + var result = x.ln() + var s = result.to_string(15) + # ln(2) ≈ 0.693147180559945... + if not s.startswith("0.69314"): + raise Error("FAIL test_ln got: " + s) + print("OK ln(2) =", s) + + +def test_trig() raises: + print("test_trig ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var pi = BigFloat.pi(50) + var s = pi.sin() + var c = pi.cos() + var sin_s = s.to_string(20) + var cos_s = c.to_string(20) + # sin(π) ≈ 0, cos(π) ≈ -1 + print("OK sin(π) =", sin_s, " cos(π) =", cos_s) + + +def test_arithmetic() raises: + print("test_arithmetic ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var a = BigFloat("10.0") + var b = BigFloat("3.0") + var sum_ = a + b + var diff = a - b + var prod = a * b + var quot = a / b + print("OK 10+3=", sum_, " 10-3=", diff, " 10*3=", prod, " 10/3=", quot) + + +def test_comparison() raises: + print("test_comparison ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var a = BigFloat("1.0") + var b = BigFloat("2.0") + var c = BigFloat("1.0") + var ok = True + if not (a < b): + ok = False + if not (b > a): + ok = False + if not (a == c): + ok = False + if not (a != b): + ok = False + if not (a <= c): + ok = False + if not (b >= a): + ok = False + if ok: + print("OK") + else: + raise Error("FAIL test_comparison") + + +def test_pi() raises: + print("test_pi ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var pi = BigFloat.pi(100) + var s = pi.to_string(50) + # π ≈ 3.14159265358979323846... + if not s.startswith("3.14159265358979"): + raise Error("FAIL test_pi got: " + s) + print("OK π =", s) + + +def test_to_bigdecimal() raises: + print("test_to_bigdecimal ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("2.0", precision=50) + var s = x.sqrt() + var bd = s.to_bigdecimal(30) + var bd_s = String(bd) + # Should start with 1.4142... + if not bd_s.startswith("1.4142"): + raise Error("FAIL test_to_bigdecimal got: " + bd_s) + print("OK BigDecimal(sqrt(2)) =", bd_s) + + +def test_power_and_root() raises: + print("test_power_and_root ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("8.0", precision=30) + var third = BigFloat("0.333333333333333333", precision=30) + var cube_root = x.root(UInt32(3)) + var power_result = x.power(third) + print("OK cbrt(8) =", cube_root, " 8^(1/3) =", power_result) + + +def test_neg_and_abs() raises: + print("test_neg_and_abs ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("-5.0") + var neg_x = -x + var abs_x = x.__abs__() + print("OK -(-5) =", neg_x, " |(-5)| =", abs_x) + + +def test_high_precision_sqrt() raises: + print("test_high_precision_sqrt ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("2.0", precision=1000) + var s = x.sqrt() + var result = s.to_string(100) + # Verify many digits of sqrt(2) + if not result.startswith( + "1.41421356237309504880168872420969807856967187537694" + ): + raise Error("FAIL test_high_precision_sqrt got: " + result) + print("OK sqrt(2) to 100 digits verified") + + +def main() raises: + test_mpfr_available() + test_construct_from_string() + test_construct_from_int() + test_sqrt() + test_exp() + test_ln() + test_trig() + test_arithmetic() + test_comparison() + test_pi() + test_to_bigdecimal() + test_power_and_root() + test_neg_and_abs() + test_high_precision_sqrt() + print("\nAll BigFloat smoke tests completed.") diff --git a/tests/test_bigdecimal.sh b/tests/test_bigdecimal.sh index d62e584..756927f 100755 --- a/tests/test_bigdecimal.sh +++ b/tests/test_bigdecimal.sh @@ -2,5 +2,5 @@ set -e for f in tests/bigdecimal/*.mojo; do - pixi run mojo run -I src -D ASSERT=all "$f" + pixi run mojo run -I src -D ASSERT=all -debug-level=line-tables "$f" done diff --git a/tests/test_bigfloat.sh b/tests/test_bigfloat.sh new file mode 100644 index 0000000..9249a64 --- /dev/null +++ b/tests/test_bigfloat.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# ===----------------------------------------------------------------------=== # +# BigFloat test runner. +# +# BigFloat tests require the C wrapper (libdecimo_gmp_wrapper) and MPFR. +# This script: +# 1. Builds the C wrapper (if not already built) +# 2. Compiles each test file as a binary (mojo run can't link .dylib/.so) +# 3. Runs each binary with the library path set +# +# Usage: +# bash tests/test_bigfloat.sh +# +# Prerequisites: +# MPFR installed (brew install mpfr / apt install libmpfr-dev) +# ===----------------------------------------------------------------------=== # + +set -e + +# Build C wrapper if needed +WRAPPER_DIR="src/decimo/gmp" +if [[ "$(uname)" == "Darwin" ]]; then + WRAPPER_LIB="$WRAPPER_DIR/libdecimo_gmp_wrapper.dylib" +else + WRAPPER_LIB="$WRAPPER_DIR/libdecimo_gmp_wrapper.so" +fi + +if [ ! -f "$WRAPPER_LIB" ]; then + echo "Building C wrapper..." + bash "$WRAPPER_DIR/build_gmp_wrapper.sh" +fi + +# Compile and run each test file +cleanup() { rm -f "$TMPBIN"; } +trap cleanup EXIT + +for f in tests/bigfloat/*.mojo; do + echo "=== $f ===" + TMPBIN=$(mktemp /tmp/decimo_test_bigfloat_XXXXXX) + pixi run mojo build -I src -debug-level=line-tables \ + -Xlinker -L./"$WRAPPER_DIR" -Xlinker -ldecimo_gmp_wrapper \ + -o "$TMPBIN" "$f" + DYLD_LIBRARY_PATH="./$WRAPPER_DIR" LD_LIBRARY_PATH="./$WRAPPER_DIR" "$TMPBIN" + rm -f "$TMPBIN" +done diff --git a/tests/test_bigint.sh b/tests/test_bigint.sh index ac92ac0..a1011fa 100755 --- a/tests/test_bigint.sh +++ b/tests/test_bigint.sh @@ -2,5 +2,5 @@ set -e for f in tests/bigint/*.mojo; do - pixi run mojo run -I src -D ASSERT=all "$f" + pixi run mojo run -I src -D ASSERT=all -debug-level=line-tables "$f" done diff --git a/tests/test_bigint10.sh b/tests/test_bigint10.sh index 57856d2..2ac7278 100755 --- a/tests/test_bigint10.sh +++ b/tests/test_bigint10.sh @@ -2,5 +2,5 @@ set -e for f in tests/bigint10/*.mojo; do - pixi run mojo run -I src -D ASSERT=all "$f" + pixi run mojo run -I src -D ASSERT=all -debug-level=line-tables "$f" done diff --git a/tests/test_biguint.sh b/tests/test_biguint.sh index bee5f86..9d6694f 100755 --- a/tests/test_biguint.sh +++ b/tests/test_biguint.sh @@ -2,5 +2,5 @@ set -e for f in tests/biguint/*.mojo; do - pixi run mojo run -I src -D ASSERT=all "$f" + pixi run mojo run -I src -D ASSERT=all -debug-level=line-tables "$f" done diff --git a/tests/test_cli.sh b/tests/test_cli.sh index 64a418e..123b57f 100644 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -2,5 +2,5 @@ set -e # Exit immediately if any command fails for f in tests/cli/*.mojo; do - pixi run mojo run -I src -I src/cli -D ASSERT=all "$f" + pixi run mojo run -I src -I src/cli -D ASSERT=all -debug-level=line-tables "$f" done diff --git a/tests/test_decimal128.sh b/tests/test_decimal128.sh index d2a4d05..86eaf1f 100755 --- a/tests/test_decimal128.sh +++ b/tests/test_decimal128.sh @@ -2,5 +2,5 @@ set -e for f in tests/decimal128/*.mojo; do - pixi run mojo run -I src -D ASSERT=all "$f" + pixi run mojo run -I src -D ASSERT=all -debug-level=line-tables "$f" done diff --git a/tests/test_toml.sh b/tests/test_toml.sh index aef15bd..82348e7 100755 --- a/tests/test_toml.sh +++ b/tests/test_toml.sh @@ -2,5 +2,5 @@ set -e for f in tests/toml/*.mojo; do - pixi run mojo run -I src -D ASSERT=all "$f" + pixi run mojo run -I src -D ASSERT=all -debug-level=line-tables "$f" done