From e3141b8692c922a7d01822e3ab3487cb9604f1b4 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 4 Apr 2026 01:15:10 +0200 Subject: [PATCH 01/17] [float][gmp] Add planning for GMP integration + Initialize folders and files (#190) This PR introduces initial scaffolding and documentation for optional MPFR/GMP integration, including a C shared-library wrapper (loaded via `dlopen`/`dlsym`) and Mojo-side FFI bindings intended to support a future MPFR-backed `BigFloat` type. **Changes:** - Added a C-based MPFR wrapper with lazy runtime loading plus a build script and pixi tasks to compile it. - Added Mojo FFI bindings (`mpfr_wrapper.mojo`) and a placeholder `bigfloat.mojo` for the upcoming `BigFloat` API. - Updated repository documentation/licensing notes (NOTICE + planning doc) and ignore rules for generated shared libraries. --- .gitignore | 3 + NOTICE | 26 + README.md | 4 +- docs/plans/gmp_integration.md | 1610 +++++++++++++++++++++++++ docs/readme_cli.md | 0 docs/readme_unreleased.md | 22 +- pixi.toml | 4 + src/decimo/bigfloat/__init__.mojo | 30 + src/decimo/bigfloat/bigfloat.mojo | 98 ++ src/decimo/bigfloat/mpfr_wrapper.mojo | 198 +++ src/decimo/gmp/README.md | 49 + src/decimo/gmp/build_gmp_wrapper.sh | 41 + src/decimo/gmp/gmp_wrapper.c | 383 ++++++ 13 files changed, 2449 insertions(+), 19 deletions(-) create mode 100644 NOTICE create mode 100644 docs/plans/gmp_integration.md create mode 100644 docs/readme_cli.md create mode 100644 src/decimo/bigfloat/__init__.mojo create mode 100644 src/decimo/bigfloat/bigfloat.mojo create mode 100644 src/decimo/bigfloat/mpfr_wrapper.mojo create mode 100644 src/decimo/gmp/README.md create mode 100755 src/decimo/gmp/build_gmp_wrapper.sh create mode 100644 src/decimo/gmp/gmp_wrapper.c diff --git a/.gitignore b/.gitignore index a05a2e40..3b4a4852 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ kgen.trace.json* local # CLI binary /decimo +# Compiled GMP/MPFR wrapper (built from source) +*.dylib +*.so # Python build artifacts *.so __pycache__/ diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..fdcee941 --- /dev/null +++ b/NOTICE @@ -0,0 +1,26 @@ +Decimo +Copyright 2025 Yuhao Zhu + +Licensed under the Apache License, Version 2.0. + + +Third-Party Dependencies +======================== + +Decimo optionally uses the following libraries at runtime, if installed by +the user. Decimo does not include, copy, or distribute any source code or +binaries from these projects. + +GNU MPFR Library (https://www.mpfr.org/) + Copyright (C) Free Software Foundation, Inc. + Licensed under the GNU Lesser General Public License v3.0 or later (LGPLv3+). + +GNU Multiple Precision Arithmetic Library (GMP) (https://gmplib.org/) + Copyright (C) Free Software Foundation, Inc. + Licensed under the GNU Lesser General Public License v3.0 or later (LGPLv3+), + or the GNU General Public License v2.0 or later (GPLv2+). + +MPFR and GMP are loaded at runtime via dlopen/dlsym when the user has +independently installed them (e.g. `brew install mpfr` on macOS or +`apt install libmpfr-dev` on Linux). The BigFloat type requires MPFR; +all other Decimo types work without any external dependencies. diff --git a/README.md b/README.md index 6020858b..a9cd6a2a 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,9 @@ The core types are[^auxiliary]: | `Decimal` | `BDec`, `BigDecimal` | Equivalent to Python's `decimal.Decimal` | Base-10^9 | | `Dec128` | `Decimal128` | 128-bit fixed-precision decimal type | Triple 32-bit words | ---- - **Decimo** combines "**Deci**mal" and "**Mo**jo" - reflecting its purpose and implementation language. "Decimo" is also a Latin word meaning "tenth" and is the root of the word "decimal". ---- +A CLI calculator tool, built on top of the Decimo library and powered by [ArgMojo](https://github.com/forfudan/argmojo) (a command-line argument parser library), is also available in this repository. It provides a convenient way to perform high-precision calculations directly from the command line. This repository includes a built-in [TOML parser](./docs/readme_toml.md) (`decimo.toml`), a lightweight pure-Mojo implementation supporting TOML v1.0. It parses configuration files and test data, supporting basic types, arrays, and nested tables. While created for Decimo's testing framework, it offers general-purpose structured data parsing with a clean, simple API. diff --git a/docs/plans/gmp_integration.md b/docs/plans/gmp_integration.md new file mode 100644 index 00000000..c02acd0e --- /dev/null +++ b/docs/plans/gmp_integration.md @@ -0,0 +1,1610 @@ +# GMP Integration Plan for Decimo + +> **Date**: 2026-04-02 +> **Target**: decimo >=0.9.0 +> **Mojo Version**: >=0.26.2 +> **GMP Version**: 6.3.0 (tested on macOS ARM64, Apple Silicon) +> +> 子曰工欲善其事必先利其器 +> The mechanic, who wishes to do his work well, must first sharpen his tools -- Confucius + +## 1. Executive Summary + +### Verdict: FEASIBLE ✓ + +GMP integration into Decimo is technically feasible — I've proven it with a working prototype. A C wrapper (`gmp_wrapper.c`) bridges Mojo's FFI layer to GMP, and all core arithmetic operations pass. + +### Key Findings + +| Aspect | Status | Notes | +| ------------------------ | ----------- | ------------------------------------------------------------------------------- | +| Mojo → C FFI | ✓ Works | Via `external_call` with link-time binding | +| GMP arithmetic ops | ✓ All work | Add, sub, mul, div, mod, pow, sqrt, gcd, bitwise | +| Build pipeline | ✓ Works | `mojo build` with `-Xlinker` flags | +| Runtime detection | ! Partial | Build-time detection feasible; true runtime dlopen not available in Mojo 0.26.2 | +| BigInt ↔ GMP conversion | ✓ Efficient | Both use base-2^32, near-trivial O(n) conversion | +| BigUInt/BigDecimal ↔ GMP | ! Costly | Requires base-10^9 → binary conversion (O(n²) naïve, O(n·log²n) D&C) | +| `mojo run` (JIT) | ✗ Broken | `-Xlinker` flags ignored in JIT mode; only `mojo build` works | +| Cross-platform | ! Varies | macOS/Linux straightforward; Windows needs MPIR or MinGW GMP | + +### Recommendation + +Two-type architecture: + +1. **BigFloat** — a new first-class MPFR-backed binary float type (like mpmath's `mpf` + but faster). Requires MPFR. Every operation is a single MPFR call. This is the fast + path for scientific computing at high precision. +2. **BigDecimal** — keeps its native base-10⁹ implementation. Gains an optional + `gmp=True` parameter for one-off acceleration of expensive operations (sqrt, exp, ln, + etc.) without changing types. No MPFR dependency. +3. **BigInt** — GMP backend deferred to Phase 3. Cheap O(n) word conversion, 7–18× + speedup measured. + +BigFloat and BigDecimal coexist because they solve different problems: BigFloat is fast +binary arithmetic (like mpmath); BigDecimal is exact decimal arithmetic (like Python +`decimal`). Both in one library, user picks the right tool. + +## 2. Technical Feasibility Assessment + +### 2.1 What Was Tested + +A complete prototype was built in `temp/gmp/`: + +1. **C wrapper** (`gmp_wrapper.c`): Handle-based API wrapping GMP's `mpz_t` operations +2. **Shared library** (`libgmp_wrapper.dylib`): Compiled, linked against GMP +3. **Mojo FFI test** (`test_gmp_ffi.mojo`): 18 tests covering all operation categories +4. **Mojo benchmark** (`bench_gmp_vs_decimo.mojo`): Performance comparison across digit sizes + +### 2.2 Prototype Results + +| Test Category | Result | +| -------------------------- | ------------------------------------- | +| Initialization & cleanup | ✓ Pass | +| String import/export | ✓ Pass | +| Addition, subtraction | ✓ Pass | +| Multiplication | ✓ Pass | +| Floor/truncate division | ✓ Pass | +| Modulo | ✓ Pass | +| Power, modular power | ✓ Pass | +| Square root | ✓ Pass | +| GCD | ✓ Pass | +| Negation, absolute value | ✓ Pass | +| Comparison, sign | ✓ Pass | +| Bitwise AND, OR, XOR | ✓ Pass | +| Left/right shift | ✓ Pass | +| Availability/version check | ✓ Pass | +| UInt32 word import/export | ! Partial (parameter alignment issue) | + +### 2.3 Why C Wrapper Is Required + +Direct FFI to GMP doesn't work in Mojo 0.26.2 because: + +1. **No `DLHandle`**: Mojo 0.26.2 does not expose `DLHandle` for runtime dynamic library loading. Only `external_call` (compile-time/link-time binding) is available. +2. **`mpz_t` is a struct**: GMP's `mpz_t` is `__mpz_struct[1]` (a 1-element array used as a pass-by-reference trick). Mojo cannot directly represent or pass this. +3. **Pointer origin tracking**: `UnsafePointer` in Mojo 0.26.2 has strict origin tracking that prevents constructing pointers from raw integer addresses in non-`fn` contexts. +4. **GMP macros**: Many GMP operations (e.g., `mpz_init`, `mpz_clear`) are preprocessor macros, not true function symbols. + +The C wrapper solves all of this by exposing a flat, handle-based C API where Mojo +passes and receives only primitive types (`Int32`, `Int`). + +## 3. Mojo FFI Capabilities and Constraints + +### 3.1 Available FFI Mechanism + +```mojo +from std.ffi import external_call + +# Example: calling a C function +var result = external_call["gmpw_add", NoneType, Int32, Int32, Int32](a, b, c) +``` + +- `external_call` resolves symbols at **link time** (not runtime) +- Function signature must be known at compile time +- Supports: `Int`, `Int32`, `UInt32`, `Float64`, `NoneType`, pointer-as-`Int` +- Does **not** support: variadic args, struct-by-value, function pointers + +### 3.2 Constraints Discovered + +| Constraint | Impact | Workaround | +| -------------------------------- | ------------------------------------------------- | ------------------------------------------------------- | +| No `DLHandle` (runtime dlopen) | Cannot conditionally load GMP at runtime | Compile-time feature flag or separate build targets | +| `-Xlinker` ignored in `mojo run` | JIT mode cannot link external libs | Must use `mojo build` + execute binary | +| `UnsafePointer` origin tracking | Cannot construct pointers from addresses in `def` | Pass pointers as `Int` type | +| No struct-by-value in FFI | Cannot pass/return C structs | Handle-based API (C wrapper manages structs internally) | +| `UnsafePointer.alloc()` removed | Cannot allocate raw memory directly | Use `List[T](length=n, fill=v)` + `.unsafe_ptr()` | +| Same-pointer aliasing forbidden | Cannot pass same buffer twice to `external_call` | Use separate buffer variables | + +### 3.3 Future Mojo Improvements That Would Help + +- **`DLHandle` support**: Would enable true runtime detection and conditional loading +- **Stable `UnsafePointer` API**: Would simplify pointer handling +- **Global variables**: Would allow module-level GMP backend state +- **Conditional compilation (`@static_if`)**: Would enable clean feature toggling + +## 4. Architecture Analysis + +### 4.1 Current Decimo Architecture + +```txt +Decimo Types Internal Representation +───────────── ─────────────────────── +BigInt (BInt) → List[UInt32] in base-2^32 + Bool sign +BigUInt → List[UInt32] in base-10^9 (unsigned) +BigDecimal (Decimal) → BigUInt coefficient + Int scale + Bool sign +Dec128 → 3×UInt32 coefficient + UInt32 flags (fixed 128-bit) +``` + +### 4.2 GMP Architecture + +```txt +GMP Types Internal Representation +───────────── ─────────────────────── +mpz_t → mp_limb_t* (base-2^64 on ARM64) + int size + int alloc +mpq_t → mpz_t numerator + mpz_t denominator +mpf_t → Binary floating-point with configurable precision +``` + +### 4.3 Type Mapping + +| Decimo Type | GMP Equivalent | Conversion Cost | Integration Priority | +| -------------------- | ------------------------------- | ----------------------------------------------------- | -------------------- | +| **BigFloat** *(new)* | `mpfr_t` | **None** — direct MPFR handle | **Phase 1** (HIGH) | +| **BigInt** | `mpz_t` | **Low** — same binary base (2^32 → 2^64 word packing) | **Phase 3** (MEDIUM) | +| **BigUInt** | `mpz_t` (unsigned subset) | **High** — base-10^9 → binary conversion | Skip | +| **BigDecimal** | via BigFloat (`gmp=True` sugar) | **High** — string conversion, but single round-trip | **Phase 2** (HIGH) | +| **Dec128** | Not applicable | N/A — fixed precision, already fast | Skip | + +### 4.4 Where GMP Helps + +GMP's advantages over my native implementation: + +1. **Highly optimized assembly**: GMP includes hand-tuned ARM64 assembly for core operations +2. **Mature algorithm suite**: Decades of optimization (Schönhage-Strassen for huge multiplications, sub-quadratic GCD, etc.) +3. **Large-number regime**: GMP's algorithmic cutoffs are well-tuned for every platform +4. **Additional algorithms**: FFT-based multiplication, extended GCD, primality testing, etc. + +Where GMP does **not** help: + +1. **Small numbers** (<50 digits): FFI overhead dominates +2. **Dec128**: Fixed 128-bit operations are already optimal with native code +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 + +## 5. Performance Comparison: GMP vs Decimo + +All benchmarks run side-by-side in a single binary (`bench_gmp_vs_decimo.mojo`), compiled +with `mojo build -O3` (release mode, assertions off). Each data point is the **median of +7 runs**. Platform: Apple M2, macOS, GMP 6.3.0, Mojo 0.26.2. + +### 5.1 BigInt (base-2³²) vs GMP `mpz` + +These compare Decimo's `BigInt` (Karatsuba multiply, Burnikel-Ziegler divide) against +GMP's `mpz_*` functions. Both operate in binary representation; no base conversion needed. + +#### Multiplication (n-digit × n-digit) + +| Digits | GMP | Decimo | Decimo / GMP | +| ------ | -------- | --------- | ------------ | +| 50 | 12 ns | 106 ns | 8.8× | +| 100 | 21 ns | 156 ns | 7.4× | +| 500 | 223 ns | 2.20 µs | 9.9× | +| 1,000 | 733 ns | 7.45 µs | 10.2× | +| 5,000 | 10.40 µs | 127.44 µs | 12.3× | +| 10,000 | 24.45 µs | 380.81 µs | 15.6× | + +GMP is **8–16× faster** for multiplication, with the gap widening at larger sizes due to +GMP's hand-tuned assembly (Karatsuba / Toom-3 / FFT) versus Decimo's pure-Mojo Karatsuba. + +#### Addition (n-digit + n-digit) + +| Digits | GMP | Decimo | Decimo / GMP | +| ------ | ------ | ------- | ------------ | +| 50 | 5 ns | 51 ns | 10.2× | +| 100 | 5 ns | 56 ns | 11.2× | +| 500 | 8 ns | 111 ns | 13.9× | +| 1,000 | 12 ns | 181 ns | 15.1× | +| 5,000 | 69 ns | 667 ns | 9.7× | +| 10,000 | 150 ns | 1.24 µs | 8.2× | + +GMP is **8–15× faster** for addition. GMP uses assembly-optimized `mpn_add_n`; Decimo +uses SIMD vectorization but cannot match hand-written assembly. + +#### Floor Division (n-digit / (n/2)-digit) + +| Digits | GMP | Decimo | Decimo / GMP | +| ------ | -------- | --------- | ------------ | +| 100 | 44 ns | 510 ns | 11.6× | +| 500 | 218 ns | 1.76 µs | 8.1× | +| 1,000 | 593 ns | 5.47 µs | 9.2× | +| 5,000 | 7.16 µs | 81.73 µs | 11.4× | +| 10,000 | 22.62 µs | 254.62 µs | 11.3× | + +GMP is **8–12× faster** for division. Both use sub-quadratic algorithms (Burnikel-Ziegler), +but GMP's multiply subroutine is faster, and its lower-level code is more optimized. + +#### Integer Square Root + +| Digits | GMP | Decimo | Decimo / GMP | +| ------ | -------- | --------- | ------------ | +| 50 | 44 ns | 205 ns | 4.7× | +| 100 | 81 ns | 709 ns | 8.8× | +| 500 | 273 ns | 3.55 µs | 13.0× | +| 1,000 | 569 ns | 8.41 µs | 14.8× | +| 5,000 | 5.90 µs | 104.67 µs | 17.7× | +| 10,000 | 17.52 µs | 322.26 µs | 18.4× | + +GMP is **5–18× faster** for sqrt. Both use Newton's method, but GMP's inner multiply/divide +is much faster, and the gap accumulates over Newton iterations. + +#### GCD + +| Digits | GMP | Decimo | Decimo / GMP | +| ------ | ------- | ------- | ------------ | +| 50 | 53 ns | 216 ns | 4.1× | +| 100 | 45 ns | 247 ns | 5.5× | +| 500 | 93 ns | 543 ns | 5.8× | +| 1,000 | 142 ns | 955 ns | 6.7× | +| 5,000 | 646 ns | 4.21 µs | 6.5× | +| 10,000 | 1.95 µs | 8.02 µs | 4.1× | + +GMP is **4–7× faster** for GCD. GMP uses a sub-quadratic half-GCD algorithm (Lehmer/​Schönhage); +Decimo currently uses the Euclidean algorithm. This is the smallest gap of all operations. + +### 5.2 BigDecimal (base-10⁹) vs GMP Integer Equivalents + +GMP has no native decimal type. To compare fairly, I benchmark GMP performing the +**equivalent integer computation** that a GMP-backed BigDecimal would perform: + +- **Multiply**: GMP integer multiply on same-digit-count coefficients +- **Sqrt(2) at precision P**: GMP `isqrt(2 × 10^(2P))` vs Decimo `BigDecimal.sqrt(precision=P)` +- **Division (1/3) at precision P**: GMP `(1 × 10^P) / 3` vs Decimo `BigDecimal.true_divide(…, precision=P)` + +These ratios represent the **best-case speedup** from a GMP backend, before +accounting for the base-10⁹ ↔ binary conversion overhead. + +#### BigDecimal Multiplication (n sig-digits × n sig-digits) + +| Digits | GMP (int mul) | Decimo (BigDecimal) | Decimo / GMP | +| ------ | ------------- | ------------------- | ------------ | +| 28 | 9 ns | 137 ns | 15.2× | +| 100 | 21 ns | 327 ns | 15.6× | +| 500 | 226 ns | 7.92 µs | 35.1× | +| 1,000 | 725 ns | 25.75 µs | 35.5× | +| 5,000 | 10.72 µs | 309.71 µs | 28.9× | +| 10,000 | 24.63 µs | 923.19 µs | 37.5× | + +BigDecimal multiply is **15–38× slower** than GMP integer multiply. This large gap comes +from two factors: (1) BigDecimal uses base-10⁹ `BigUInt` internally (not the faster base-2³² +`BigInt`), and (2) GMP's assembly is faster than either Decimo representation. + +#### BigDecimal Sqrt(2) at Various Precisions + +| Precision | GMP (isqrt) | Decimo (BigDecimal.sqrt) | Decimo / GMP | +| --------- | ----------- | ------------------------ | ------------ | +| 28 | 32 ns | 3.48 µs | 109× | +| 100 | 112 ns | 9.58 µs | 86× | +| 500 | 430 ns | 93.41 µs | 217× | +| 1,000 | 1.05 µs | 258.40 µs | 246× | +| 2,000 | 2.75 µs | 869.55 µs | 316× | +| 5,000 | 11.10 µs | 3.92 ms | 353× | + +BigDecimal sqrt is **86–353× slower** than GMP's integer sqrt equivalent. This extreme gap +reflects the compounding cost of Newton iteration: every iteration requires multiple +BigDecimal multiplies and divides, each of which is individually 15–35× slower than GMP. + +#### BigDecimal Division (1/3) at Various Precisions + +| Precision | GMP (int div) | Decimo (BigDecimal.true_divide) | Decimo / GMP | +| --------- | ------------- | ------------------------------- | ------------ | +| 28 | 10 ns | 426 ns | 42.6× | +| 100 | 18 ns | 496 ns | 27.6× | +| 500 | 70 ns | 830 ns | 11.9× | +| 1,000 | 150 ns | 1.30 µs | 8.7× | +| 2,000 | 300 ns | 2.30 µs | 7.7× | +| 5,000 | 800 ns | 4.80 µs | 6.0× | + +BigDecimal division is **6–43× slower**. The ratio decreases at higher precision because +Decimo's division cost is dominated by the final long-division step (linear), while at +low precision the constant overhead of BigDecimal setup is proportionally larger. + +### 5.3 Summary of Speedup Ratios + +| Operation | Small (≤100d) | Medium (~1,000d) | Large (≥5,000d) | Key Driver | +| ------------------- | ------------- | ---------------- | --------------- | --------------------------------------- | +| BigInt Multiply | 7–9× | 10× | 12–16× | GMP asm multiply (Karatsuba/Toom/FFT) | +| BigInt Add | 10–11× | 15× | 8–10× | GMP asm `mpn_add_n` | +| BigInt Floor Divide | 12× | 9× | 11× | GMP asm multiply in Burnikel-Ziegler | +| BigInt Sqrt | 5–9× | 15× | 18× | Multiply gap compounds in Newton iters | +| BigInt GCD | 4–6× | 7× | 4–7× | GMP sub-quadratic half-GCD | +| BigDecimal Multiply | 15–16× | 36× | 29–38× | base-10⁹ overhead + GMP asm | +| BigDecimal Sqrt | 86–109× | 246× | 316–353× | Newton iteration compounds the gap | +| BigDecimal Divide | 28–43× | 9× | 6–8× | Gap narrows as division dominates setup | + +### 5.4 FFI Overhead Consideration + +The C wrapper call path has fixed overhead: + +1. `external_call` dispatch: ~1–2 ns +2. Handle array lookup: ~1–2 ns +3. Total fixed overhead: **~3–5 ns per call** + +In practice: + +- At **10 digits**: overhead is 30–100% of operation time → **GMP not beneficial** +- At **100 digits**: overhead is 5–10% → **GMP moderately beneficial** +- At **1,000+ digits**: overhead is <0.5% → **GMP clearly beneficial** + +**Recommended crossover threshold**: Use GMP backend for numbers with **≥64 words** (~600 +digits for BigInt). Below this, Decimo's native implementation avoids FFI overhead. + +### 5.5 Mojo String FFI Safety Issue + +During benchmarking, I hit a Mojo compiler issue where the optimizer may drop a +`String` parameter's backing buffer before the `external_call` using its `unsafe_ptr()` +completes. Under heavy allocation pressure, the freed buffer gets reused immediately, +causing GMP's `mpz_set_str` to read corrupted data. + +**Workaround**: I added a length-aware C function `gmpw_set_str_n(handle, ptr, len)` that +copies exactly `len` bytes into a null-terminated buffer before calling GMP, making the +FFI call safe regardless of the caller's buffer lifetime. Any production integration must +use the length-based variant (`gmpw_set_str_n`) or ensure String buffers remain live +through explicit variable binding. + +## 6. Operation Coverage Mapping + +### 6.1 BigInt Operations → GMP Functions + +| Decimo BigInt Operation | GMP Function (via wrapper) | Status | +| ------------------------ | ----------------------------------- | -------------- | +| `__add__` | `mpz_add` → `gmpw_add` | ✓ Tested | +| `__sub__` | `mpz_sub` → `gmpw_sub` | ✓ Tested | +| `__mul__` | `mpz_mul` → `gmpw_mul` | ✓ Tested | +| `__floordiv__` | `mpz_fdiv_q` → `gmpw_fdiv_q` | ✓ Tested | +| `__truediv__` | `mpz_tdiv_q` → `gmpw_tdiv_q` | ✓ Tested | +| `__mod__` | `mpz_mod` → `gmpw_mod` | ✓ Tested | +| `__pow__` | `mpz_pow_ui` → `gmpw_pow_ui` | ✓ Tested | +| `mod_pow` | `mpz_powm` → `gmpw_powm` | ✓ Tested | +| `sqrt` / `isqrt` | `mpz_sqrt` → `gmpw_sqrt` | ✓ Tested | +| `gcd` | `mpz_gcd` → `gmpw_gcd` | ✓ Tested | +| `__neg__` | `mpz_neg` → `gmpw_neg` | ✓ Tested | +| `__abs__` | `mpz_abs` → `gmpw_abs` | ✓ Tested | +| `__eq__`, `__lt__`, etc. | `mpz_cmp` → `gmpw_cmp` | ✓ Tested | +| `__and__` | `mpz_and` → `gmpw_and` | ✓ Tested | +| `__or__` | `mpz_ior` → `gmpw_or` | ✓ Tested | +| `__xor__` | `mpz_xor` → `gmpw_xor` | ✓ Tested | +| `__lshift__` | `mpz_mul_2exp` → `gmpw_lshift` | ✓ Tested | +| `__rshift__` | `mpz_fdiv_q_2exp` → `gmpw_rshift` | ✓ Tested | +| `lcm` | `mpz_lcm` | Wrapper needed | +| `extended_gcd` | `mpz_gcdext` | Wrapper needed | +| `mod_inverse` | `mpz_invert` | Wrapper needed | +| `from_string` | `mpz_set_str` → `gmpw_set_str` | ✓ Tested | +| `__str__` | `mpz_get_str` → `gmpw_get_str_buf` | ✓ Tested | +| Word import | `mpz_import` → `gmpw_import_u32_le` | ! Partial | +| Word export | `mpz_export` → `gmpw_export_u32_le` | ! Partial | + +### 6.2 BigDecimal Operations → GMP Coverage + +BigDecimal operations would use GMP indirectly by converting the coefficient (BigUInt) to/from GMP: + +| Operation | GMP Applicability | Notes | +| ----------- | ----------------- | ---------------------------------------------------------------------- | +| Add/Sub | ! Low value | Scale alignment dominates; coefficient add is cheap | +| Multiply | ✓ High value | Large coefficient multiplication benefits from GMP | +| Divide | ✓ High value | Newton iteration on large coefficients benefits | +| Sqrt | ✓ High value | Large-precision sqrt benefits from GMP's optimized Newton | +| Exp/Ln/Trig | ! Mixed | Series evaluation is Mojo-managed; inner multiplications could use GMP | +| Round | ✗ No value | Pure digit manipulation, no heavy arithmetic | + +### 6.3 Operations NOT in GMP + +These must remain native Mojo: + +- Decimal rounding (all modes) +- Scale/exponent management +- Base-10 digit manipulation +- Trigonometric series evaluation logic +- TOML parsing +- CLI calculator evaluation +- Dec128 operations (fixed-precision) + +## 7. Data Conversion Between Decimo and GMP + +### 7.1 BigInt ↔ GMP (Efficient) + +BigInt uses base-2^32 `List[UInt32]` in little-endian order — basically the same thing as GMP's `mpz_t` but with 32-bit limbs instead of 64-bit. + +**Conversion approach**: + +```txt +BigInt.words: [w0, w1, w2, w3, ...] (UInt32, little-endian) +GMP limbs: [w0|w1, w2|w3, ...] (UInt64, little-endian) +``` + +Using `mpz_import`: + +```c +// Import BigInt words into GMP +mpz_import(z, word_count, -1, sizeof(uint32_t), 0, 0, words_ptr); +// -1 = least significant word first (little-endian word order) +// sizeof(uint32_t) = 4 bytes per word +// 0 = native byte order within word +// 0 = no nail bits +``` + +Using `mpz_export`: + +```c +// Export GMP value to BigInt words +size_t count; +mpz_export(words_ptr, &count, -1, sizeof(uint32_t), 0, 0, z); +``` + +**Cost**: O(n) memory copy — essentially free compared to the arithmetic operations. + +**Sign handling**: BigInt stores sign as separate `Bool`; GMP stores sign in `_mp_size` (negative = negative). Trivial to map. + +**Implementation in C wrapper** (already prototyped): + +```c +void gmpw_import_u32_le(int handle, const uint32_t* words, int count); +int gmpw_export_u32_le(int handle, uint32_t* out_buf, int max_count); +``` + +### 7.2 BigUInt ↔ GMP (Expensive) + +BigUInt uses base-10^9 — a completely different number base from GMP's binary format. + +**Conversion options**: + +| Method | Complexity | Description | +| ------------------ | ---------- | -------------------------------------------------------------- | +| String round-trip | O(n²) | `BigUInt → String → mpz_set_str` — simple but slow | +| Horner's method | O(n²) | Evaluate polynomial: `((w[n-1] × 10^9 + w[n-2]) × 10^9 + ...)` | +| Divide-and-conquer | O(n·log²n) | Split at midpoint, convert halves, combine with shift+add | + +**Recommended approach**: + +- For <1000 words: Horner's method (simpler, O(n²) acceptable) +- For ≥1000 words: Divide-and-conquer using GMP's own multiply for combining + +**Break-even analysis**: Assuming base conversion costs ~O(n²) for n words: + +- At 100 words (~900 digits): conversion ~10 µs, GMP multiply saves ~4 µs → **NOT worth it** +- At 1000 words (~9000 digits): conversion ~1 ms, GMP multiply saves ~150 µs → **NOT worth it for single operation** +- **Conclusion**: BigUInt→GMP conversion is only justified when amortized across many operations (e.g., performing multiple root/exp iterations on the same number). + +### 7.3 Recommended Strategy + +```txt + ┌──────────────────────────────────────┐ + BigInt operations │ Direct word import/export (O(n)) │ → Phase 1 + │ Cheap, always beneficial > 64 words │ + └──────────────────────────────────────┘ + + ┌──────────────────────────────────────┐ + BigDecimal ops │ Convert once, compute many times │ → Phase 2 + (iterative: sqrt, │ Cache GMP representation during │ + exp, ln, etc.) │ multi-step computations │ + └──────────────────────────────────────┘ + + ┌──────────────────────────────────────┐ + BigDecimal ops │ Skip GMP — conversion cost │ → Phase 3 (or skip) + (single-shot: │ exceeds arithmetic savings │ + add, sub, round) │ │ + └──────────────────────────────────────┘ +``` + +## 8. GMP Detection and Fallback Strategy + +### 8.1 Detection Approaches + +Since Mojo 0.26.2 doesn't have `DLHandle` for runtime library probing, detection has to be **build-time** or **semi-static**: + +#### Option A: Compile-Time Feature Flag (Recommended for Now) + +```toml +# pixi.toml +[feature.gmp] +tasks.package-gmp = "mojo build -DGMP_ENABLED -Xlinker -lgmp_wrapper ..." +``` + +```mojo +# In Decimo source (when Mojo supports comptime parameters) +@parameter +if GMP_ENABLED: + alias _engine = GMPEngine +else: + alias _engine = NativeEngine +``` + +**Pros**: Zero overhead, no runtime branching +**Cons**: Requires separate builds for GMP and non-GMP versions + +#### Option B: Probe Function at Module Load + +```mojo +# Use the C wrapper's availability check +fn _check_gmp_available() -> Bool: + """Call gmpw_available() which returns 1 if GMP is linked.""" + try: + var result = external_call["gmpw_available", Int32]() + return result == 1 + except: + return False + +# Cache at first use (workaround for no global variables) +struct _GMPState: + var checked: Bool + var available: Bool +``` + +**Pros**: Single binary works with or without GMP +**Cons**: Requires the wrapper to be linked (even if GMP is absent) + +#### Option C: Two-Library Approach + +Build two versions of the wrapper: + +1. `libgmp_wrapper.dylib` — full GMP wrapper (requires GMP) +2. `libgmp_stub.dylib` — stub that returns "unavailable" for all functions + +Link against whichever is present. The stub library lets Decimo compile and run without GMP. + +**Pros**: Clean separation, single Mojo codebase +**Cons**: Requires distributing two library variants + +### 8.2 Recommended Detection Strategy + +**Phase 1 (immediate)**: Use **Option A** — compile-time feature flag. Build two package variants: + +- `decimo.mojopkg` — native only (default) +- `decimo_gmp.mojopkg` — with GMP backend + +**Phase 2 (when Mojo matures)**: Migrate to **Option B** with `DLHandle` when available, enabling true runtime detection. + +### 8.3 Fallback Behavior + +```txt +User calls BigInt.multiply(a, b): + 1. Check engine preference (from parameter or default) + 2. If engine == GMP and gmp_available: + a. Check size threshold (≥64 words?) + b. If yes: convert words → GMP, compute, convert back + c. If no: use native Mojo implementation + 3. If engine == Native or !gmp_available: + → Use native Mojo implementation (current code) +``` + +## 9. Type Design: BigFloat and BigDecimal + +Decimo gets two arbitrary-precision types with different backends and semantics. + +### 9.1 Two Types, Two Number Systems + +| | **BigDecimal** | **BigFloat** | +| ------------------ | -------------------------------- | -------------------------------------------------- | +| Internal base | 10⁹ (decimal) | binary (MPFR `mpfr_t`) | +| Python analogue | `decimal.Decimal` | `mpmath.mpf` | +| `sqrt(0.01)` | `0.1` (exact) | `0.1000...0` (padded) | +| Dependency | None | MPFR required | +| Speed at high prec | Native Mojo | 10–400× faster (MPFR) | +| Precision unit | Decimal digits | Bits (user specifies digits, converted internally) | +| Rounding | Decimal-exact where possible | Correctly rounded per IEEE binary semantics | +| Primary use case | Finance, human-friendly decimals | Scientific computing, transcendentals | + +They coexist in the same library. User picks the right tool for the job. + +### 9.2 Why Two Types Instead of a Backend Swap + +`mpmath` pulls off a transparent backend swap (`MPZ = gmpy.mpz or int`) because: + +- Python is dynamically typed — the swap is invisible at runtime +- `mpmath` already uses binary representation internally — swapping `int` for `mpz_t` + doesn't change semantics + +I can't do this in Decimo because: + +1. **Mojo is statically typed** — users must choose their type at compile time. +2. **BigDecimal uses base-10⁹** — swapping BigUInt for `mpz_t` would change decimal + semantics (e.g., `sqrt(0.01)` behavior). +3. **They solve different problems** — BigDecimal is for exact decimal arithmetic; + BigFloat is for fast scientific computation. + +Python itself acknowledges this split: `decimal.Decimal` and `mpmath.mpf` are separate +packages. I'm combining both under one import. + +### 9.3 BigFloat: MPFR-Backed Binary Float + +```mojo +struct BigFloat: + """Arbitrary-precision binary floating-point, backed by MPFR. + + Like mpmath's mpf but faster — every operation is a single MPFR call. + Requires MPFR to be installed. If MPFR is unavailable, construction raises. + """ + var handle: Int32 # MPFR handle (via C wrapper handle pool) +``` + +**BigFloat requires MPFR. No fallback.** + +I considered a pure-Mojo BigInt-based fallback (like mpmath's pure-Python mode), but: + +- mpmath's fallback arithmetic is ~30K lines of Python for the math functions alone + (Taylor series, argument reduction, binary splitting, etc.) +- Reimplementing all that in Mojo defeats the purpose — BigFloat's value proposition + IS speed via MPFR +- If MPFR isn't installed, users have BigDecimal — it already does sqrt, exp, ln, sin, + cos, tan, pi, all in pure Mojo. Just slower at high precision. +- A half-working BigFloat (add/sub/mul work but sqrt/exp/ln don't) would be confusing + +So: BigFloat without MPFR is like numpy without its C core. Users without MPFR use +BigDecimal. This is an honest, clean boundary. + +### 9.4 Why MPFR, Not `mpz_t` with Manual Scale + +GMP has three numeric types: + +| Type | What it stores | Mojo analogue | Use for BigFloat? | +| -------- | --------------------------------------------------- | ------------- | ------------------------------------------------------------------ | +| `mpz_t` | Arbitrary-precision **integer** | BigInt | ✗ Poor — must manually track scale, sign, implement all algorithms | +| `mpf_t` | Arbitrary-precision **binary float** (GMP built-in) | — | △ Works but legacy, no correct rounding | +| `mpfr_t` | Arbitrary-precision **binary float** (MPFR library) | **BigFloat** | **✓ Best** — correctly rounded, built-in exp/ln/sin/cos/π | + +With `mpz_t`, I'd have to: + +- Store `(sign, mantissa: mpz_t, exponent: Int)` and manage scale manually +- Scale up numerators by 10^N before integer division +- Implement Newton iterations for sqrt, Taylor series for exp/ln/trig — **myself** +- Handle scale alignment for addition/subtraction +- That's basically reimplementing mpmath in Mojo (~30K lines) + +With **MPFR**, all of this is handled internally: + +- Division: `mpfr_div(r, a, b, MPFR_RNDN)` — one call +- Sqrt: `mpfr_sqrt(r, a, MPFR_RNDN)` — one call +- Exp/Ln: `mpfr_exp`, `mpfr_log` — one call each, correctly rounded +- Sin/Cos/Tan: `mpfr_sin`, `mpfr_cos`, `mpfr_tan` — one call each +- π: `mpfr_const_pi(r, MPFR_RNDN)` — one call, arbitrary precision + +MPFR sits on top of GMP and is the industry standard for arbitrary-precision +floating-point. Easy to get: `brew install mpfr` (macOS), `apt install libmpfr-dev` +(Linux). + +### 9.5 BigFloat API + +```mojo +# Construction +var x = BigFloat("3.14159", precision=1000) # from string +var y = BigFloat(bd, precision=1000) # from BigDecimal +var z = BigFloat.pi(precision=5000) # constant + +# Arithmetic (returns BigFloat) +var r = x + y +var r = x * y +var r = x / y + +# Transcendentals (single MPFR call each) +var r = x.sqrt() +var r = x.exp() +var r = x.ln() +var r = x.sin() +var r = x.cos() +var r = x.tan() +var r = x.root(n) +var r = x.power(y) + +# Conversion back to BigDecimal +var bd = r.to_bigdecimal(precision=1000) + +# Chained computation (stays in MPFR the whole time) +var result = ( + BigFloat("23", P).sqrt() + .multiply(BigFloat("2.1", P).exp()) + .divide(BigFloat.pi(P)) + .to_bigdecimal(P) +) +# Cost: 2 string→MPFR conversions in, 5 MPFR calls, 1 MPFR→string out +# No intermediate decimal conversions +``` + +Precision is specified in decimal digits but converted to bits internally: +`bits = ceil(digits × 3.322) + guard_bits`. Guard bits (64 extra) ensure the +requested decimal digits are all correct after binary→decimal conversion. + +### 9.6 BigDecimal `gmp=True` Convenience + +BigDecimal keeps an optional `gmp=True` parameter for **one-off acceleration** +without changing types. Internally it does `BigDecimal → BigFloat → compute → BigDecimal`: + +```mojo +# Single-call sugar (user stays in BigDecimal world) +var r = sqrt(x, precision=5000, gmp=True) +var r = exp(x, precision=5000, gmp=True) +var r = pi(precision=10000, gmp=True) + +# Equivalent to: +var r = BigFloat(x, 5010).sqrt().to_bigdecimal(5000) +``` + +Only **iterative compute-heavy** operations get the `gmp` parameter: + +| Function | Gets `gmp`? | Reason | +| ------------------------------- | ----------- | --------------------------------------- | +| `sqrt`, `root` | ✓ Yes | Newton iteration → single MPFR call | +| `exp`, `ln` | ✓ Yes | Taylor/atanh series → single MPFR call | +| `sin`, `cos`, `tan` | ✓ Yes | Taylor series → single MPFR call | +| `pi` | ✓ Yes | Chudnovsky → `mpfr_const_pi` | +| `true_divide` | ✓ Yes | Long division at high precision | +| `__add__`, `__sub__`, `__mul__` | ✗ No | Too cheap — FFI overhead not worth it | +| `round`, `__floordiv__` | ✗ No | Digit manipulation, no heavy arithmetic | + +**When to use `gmp=True` vs BigFloat directly:** + +- Single operation on BigDecimal inputs? → `gmp=True` (one liner, stays in BigDecimal) +- Chain of operations? → Use BigFloat directly (avoids repeated conversion) + +### 9.7 Behavioral Differences: Binary vs. Decimal + +MPFR is a **binary** float internally. What this means: + +1. **Last 1–2 decimal digits may differ** between `gmp=True` and `gmp=False`. The + numerical value agrees to the requested precision, but the least significant digits + can differ due to binary↔decimal rounding. + +2. **Guard digits handle this** — I compute with `precision + 10` extra digits, then + truncate. Standard practice (Python's `decimal` module does the same). + +3. **MPFR guarantees correct rounding** — every operation is correctly rounded to the + working binary precision. Actually *stronger* than Decimo's native implementation + which may accumulate rounding errors across Newton iterations. + +4. **Semantic difference with BigFloat** — `BigFloat("0.01").sqrt()` returns + `0.1000000...0` (binary representation of 0.1). `BigDecimal("0.01").sqrt()` returns + exactly `0.1`. For scientific computing this doesn't matter. For financial/display + work, use BigDecimal. + +### 9.8 When to Use Which + +| Scenario | Use | Why | +| ---------------------------------------- | ------------------------ | ------------------------------------------------- | +| Financial calculations | BigDecimal | Exact decimal representation, no binary surprises | +| Display-friendly output | BigDecimal | `0.1` stays `0.1`, not `0.10000...00001` | +| Fast transcendentals (exp, ln, sin, cos) | BigFloat | Single MPFR call, 10–353× faster | +| Scientific computing | BigFloat | MPFR's correctly-rounded arithmetic | +| Chained high-precision computation | BigFloat | Stays in MPFR, no intermediate conversions | +| One-off speedup without changing types | `gmp=True` on BigDecimal | Drop-in acceleration, backward compatible | +| No MPFR installed | BigDecimal | Works everywhere, pure Mojo | +| Quick calculation under ~100 digits | BigDecimal | FFI overhead not worth it at low precision | + +### 9.9 Future: Lazy Evaluation for BigDecimal + +The `gmp=True` per-call approach has a weakness: chained operations convert back and +forth between BigDecimal and MPFR at every step. A future `lazy()` API fixes this: + +```mojo +# Future API +var result = ( + BigDecimal("23").lazy() + .sqrt(5000) + .multiply(BigDecimal("2.1").lazy().exp(5000)) + .divide(BigDecimal.pi(5000)) + .collect() # executes the whole chain +) +``` + +How it would work: + +1. `.lazy()` returns a `LazyDecimal` that **records operations** instead of evaluating +2. `.collect()` analyzes the expression tree and picks the best execution strategy +3. If MPFR available: convert leaf inputs to MPFR **once**, execute entire DAG in MPFR, + convert root output back **once** — zero intermediate conversions +4. If MPFR unavailable (or `.collect(gmp=False)`): execute natively in BigDecimal + +This is the same pattern as: + +- **Polars** — lazy DataFrames, `.collect()` triggers execution +- **Spark** — query planning, transformations are lazy until action +- **C++ Eigen** — expression templates, deferred evaluation until assignment +- **TensorFlow 1.x** — computation graphs, `session.run()` triggers execution + +The key insight: users write BigDecimal code (familiar API), the engine figures out the +fastest execution path. This goes beyond the roadmap below but is architecturally clean +because BigFloat and the MPFR wrapper already provide the fast backend. + +### 9.10 CLI Integration + +With BigFloat as a first-class type, the CLI can offer: + +```bash +decimo "sqrt(2)" -p 1000 # default: BigDecimal +decimo "sqrt(2)" -p 1000 --mode float # use BigFloat (requires MPFR) +decimo "sqrt(2)" -p 1000 --gmp # BigDecimal with gmp=True +``` + +`--mode float` tells the evaluator to parse all literals as BigFloat and evaluate the +entire expression in MPFR. `--gmp` keeps BigDecimal but enables `gmp=True` on each +operation (current plan, deferred to Phase 4). + +### 9.11 Future: Global Variable or Context + +When Mojo gets global variables, a module-level default can supplement the per-call +parameter: + +```mojo +# Future (when Mojo supports global variables) +BigDecimal.set_default_engine("gmp") +var result = sqrt(x, 1000) # automatically uses GMP +``` + +The `gmp` parameter stays as an explicit override for fine-grained control. + +## 10. Build System Integration + +### 10.1 C Wrapper Compilation + +The C wrapper gets compiled as a shared library, shipped alongside the Mojo package. + +**Build script** (`build_gmp_wrapper.sh`): + +```bash +#!/bin/bash +# Detect GMP installation +GMP_PREFIX=$(brew --prefix gmp 2>/dev/null || echo "/usr/local") + +if [ ! -f "$GMP_PREFIX/include/gmp.h" ]; then + echo "GMP not found. Building without GMP support." + exit 0 +fi + +# Compile wrapper +cc -shared -O2 -o libgmp_wrapper.dylib gmp_wrapper.c \ + -I"$GMP_PREFIX/include" -L"$GMP_PREFIX/lib" -lgmp + +# Fix install name (macOS) +if [[ "$(uname)" == "Darwin" ]]; then + install_name_tool -id @rpath/libgmp_wrapper.dylib libgmp_wrapper.dylib +fi +``` + +### 10.2 pixi.toml Integration + +```toml +[tasks] +build-gmp-wrapper = """ + bash src/decimo/gmp/build_gmp_wrapper.sh +""" + +package-with-gmp = { depends-on = ["build-gmp-wrapper"] } +package-with-gmp.cmd = """ + mojo package src/decimo -o decimo.mojopkg && \ + cp src/decimo/gmp/libgmp_wrapper.dylib . +""" +``` + +### 10.3 Mojo Build Flags + +For building executables that use Decimo with GMP: + +```bash +# macOS (Homebrew) +mojo build \ + -Xlinker -L./path/to/wrapper -Xlinker -lgmp_wrapper \ + -Xlinker -L/opt/homebrew/lib -Xlinker -lgmp \ + -Xlinker -rpath -Xlinker ./path/to/wrapper \ + -Xlinker -rpath -Xlinker /opt/homebrew/lib \ + -o myprogram myprogram.mojo + +# Linux (system GMP) +mojo build \ + -Xlinker -L./path/to/wrapper -Xlinker -lgmp_wrapper \ + -Xlinker -lgmp \ + -Xlinker -rpath -Xlinker ./path/to/wrapper \ + -o myprogram myprogram.mojo +``` + +### 10.4 Runtime Detection: Single Binary Without Link-Time Dependencies + +**Goal**: One compiled binary that works whether or not MPFR/GMP is on the +machine. If `gmp=True` is requested and MPFR is found, use it. If not, raise +a helpful error. If nobody calls `gmp=True`, zero cost. + +**Verified**: `dlopen` works from Mojo 0.26.2 via `external_call["dlopen", ...]`. +I tested it — GMP loaded at runtime without any `-Xlinker` flags at compile time. + +**Architecture**: The C wrapper does lazy loading internally using `dlopen`/`dlsym`, +so it compiles and links **without** needing MPFR/GMP headers or libraries present: + +```c +// gmp_wrapper.c — always compilable, no link-time dependency on libmpfr +#include + +static void *mpfr_lib = NULL; +static int mpfr_loaded = 0; + +// Function pointer types +typedef int (*mpfr_init2_t)(void*, long); +typedef void (*mpfr_clear_t)(void*); +typedef int (*mpfr_set_str_t)(void*, const char*, int, int); +// ... one typedef per MPFR function used + +static mpfr_init2_t p_mpfr_init2 = NULL; +static mpfr_clear_t p_mpfr_clear = NULL; +static mpfr_set_str_t p_mpfr_set_str = NULL; +// ... etc. + +int mpfrw_load(void) { + if (mpfr_loaded) return mpfr_lib != NULL; + mpfr_loaded = 1; + + // Try common library paths + const char *paths[] = { + "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/libmpfr.so", // Generic Linux + NULL + }; + + for (int i = 0; paths[i]; i++) { + mpfr_lib = dlopen(paths[i], RTLD_LAZY); + if (mpfr_lib) break; + } + if (!mpfr_lib) return 0; + + // Resolve all needed symbols + p_mpfr_init2 = dlsym(mpfr_lib, "mpfr_init2"); + p_mpfr_clear = dlsym(mpfr_lib, "mpfr_clear"); + p_mpfr_set_str = dlsym(mpfr_lib, "mpfr_set_str"); + // ... resolve all function pointers + + return (p_mpfr_init2 && p_mpfr_clear && p_mpfr_set_str /* && ... */); +} + +int mpfrw_available(void) { + return mpfrw_load(); +} +``` + +**On the Mojo side**, the wrapper is the only native dependency: + +```mojo +def sqrt(x: BigDecimal, precision: Int, gmp: Bool = False) raises -> BigDecimal: + if not gmp: + return _sqrt_native(x, precision) + + # Check MPFR availability at runtime + var available = external_call["mpfrw_available", c_int]() + if available == 0: + raise Error( + "gmp=True requires MPFR. Install: brew install mpfr (macOS) " + "or apt install libmpfr-dev (Linux)" + ) + return _sqrt_mpfr(x, precision) +``` + +**Build simplification**: With lazy loading, the build command for users becomes: + +```bash +# Only need to link the small wrapper — no -lmpfr or -lgmp needed +mojo build -Xlinker -L./path/to/wrapper -Xlinker -ldecimo_gmp_wrapper \ + -Xlinker -rpath -Xlinker ./path/to/wrapper \ + -o myprogram myprogram.mojo +``` + +The wrapper is a tiny C file (~200 lines) that compiles on any system +with just a C compiler — no GMP/MPFR headers needed. MPFR only gets loaded at +runtime when `gmp=True` is actually called. + +**Summary of approach by timeline**: + +| Timeline | Detection Method | Binary Requirements | +| --------------------- | ---------------------------- | ------------------------------------------------ | +| **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 | + +## 11. Cross-Platform Considerations + +### 11.1 Platform Matrix + +| Platform | GMP Availability | Wrapper Format | Install Method | Notes | +| ---------------- | ----------------- | -------------- | ------------------------ | --------------------------- | +| **macOS ARM64** | ✓ Homebrew | `.dylib` | `brew install gmp` | **Tested ✓** | +| **macOS x86_64** | ✓ Homebrew | `.dylib` | `brew install gmp` | Should work (untested) | +| **Linux x86_64** | ✓ System packages | `.so` | `apt install libgmp-dev` | Expected to work | +| **Linux ARM64** | ✓ System packages | `.so` | `apt install libgmp-dev` | Expected to work | +| **Windows** | ! Complex | `.dll` | MSYS2/vcpkg/MPIR | Requires MinGW or MPIR fork | + +### 11.2 GMP Detection by Platform + +```bash +# macOS +GMP_PREFIX=$(brew --prefix gmp 2>/dev/null) +# → /opt/homebrew/Cellar/gmp/6.3.0 + +# Linux (Debian/Ubuntu) +dpkg -l libgmp-dev 2>/dev/null | grep -q '^ii' +# Headers: /usr/include/gmp.h +# Library: /usr/lib/x86_64-linux-gnu/libgmp.so + +# Linux (generic) +pkg-config --exists gmp && pkg-config --cflags --libs gmp +``` + +### 11.3 Platform-Specific Considerations + +**macOS**: + +- Homebrew puts GMP in `/opt/homebrew/` (ARM64) or `/usr/local/` (x86_64) +- Must set `@rpath` for `.dylib` loading +- SIP may restrict library search paths + +**Linux**: + +- System GMP is typically in standard paths (`/usr/lib/`, `/usr/include/`) +- No rpath issues with standard paths +- May need `-lgmp` at end of link flags for some linkers + +**Windows** (future): + +- GMP does not natively support MSVC +- Options: MPIR (Windows-friendly GMP fork), MinGW-compiled GMP, or Cygwin +- Different shared library format (`.dll` + import lib) +- Mojo on Windows is still early — defer this concern + +## 12. Risk Analysis + +### 12.1 Technical Risks + +| Risk | Likelihood | Impact | Mitigation | +| --------------------------------------- | ---------- | ------ | ---------------------------------------------------------- | +| Mojo FFI API changes in future versions | HIGH | MEDIUM | Isolate FFI calls in single module; version-gate if needed | +| GMP version incompatibility | LOW | LOW | Target GMP ≥6.0 (stable ABI); C wrapper abstracts details | +| Handle exhaustion (4096 limit) | LOW | MEDIUM | Add handle recycling; expand pool; document limit | +| Memory leaks from uncleared handles | MEDIUM | MEDIUM | Add `__del__` integration; RAII pattern in Mojo wrapper | +| Base conversion overhead for BigDecimal | HIGH | MEDIUM | Only use GMP for iterative operations; cache conversions | +| Build complexity increase | MEDIUM | MEDIUM | Provide clear build scripts; make GMP optional | + +### 12.2 Ecosystem Risks + +| Risk | Likelihood | Impact | Mitigation | +| ------------------------------- | ---------- | ------ | --------------------------------------------------------- | +| Users don't have GMP installed | HIGH | LOW | GMP is optional; native Mojo is always available | +| Package distribution complexity | MEDIUM | MEDIUM | Document build steps; consider bundling prebuilt wrappers | +| Mojo package system changes | MEDIUM | MEDIUM | Keep wrapper as separate build artifact | + +### 12.3 Performance Risks + +| Risk | Description | Mitigation | +| ---------------------------------------- | ------------------------------------ | ------------------------------------------------ | +| FFI overhead dominates for small numbers | Measured: ~3–5 ns overhead per call | Size threshold (≥64 words) before using GMP | +| String-based conversion bottleneck | O(n²) for string round-trip | Use binary word import/export instead | +| Unnecessary conversions | Converting back and forth repeatedly | Cache GMP handles during multi-step computations | +| GMP slower for specific operations | Possible edge cases | Benchmark-driven thresholds per operation type | + +## 13. Lessons from Other Libraries + +Before diving into the roadmap, I looked at how major open-source libraries integrate GMP/MPFR to see what I can learn from them. + +### 13.1 mpmath (Python) — Backend Abstraction + +mpmath is *the* arbitrary-precision math library for Python (~1M+ monthly downloads). + +**How mpmath does it**: + +- **Backend swap**: `backend.py` defines `MPZ` (integer type) and `BACKEND` (string). + At startup it tries `import gmpy2`, then `import gmp`, falling back to Python `int`. + All mantissa arithmetic goes through `MPZ` — a single point of substitution. +- **Environment override**: `MPMATH_NOGMPY` env var disables GMP entirely, + useful for debugging and pure-Python deployment. +- **gmpy acceleration hooks**: When gmpy is available, mpmath replaces its + `_normalize` and `from_man_exp` hot-path functions with optimized C versions + from gmpy (`gmpy._mpmath_normalize`, `gmpy._mpmath_create`), and also + `mpf_mul_int` is swapped to a gmpy-accelerated version. This gives 2–5× + speedup on core arithmetic with zero API change. +- **Internal representation**: Binary floating-point tuples `(sign, man, exp, bc)`. + All precision is in **bits**, converted to/from decimal digits via + `dps_to_prec(n) = max(1, int(round((n+1)*3.3219...)))` and + `prec_to_dps(n) = max(1, int(round(n/3.3219...))-1)`. +- **Guard bits**: Extra precision is added internally. Individual functions use + `extraprec` (add N bits) or `workprec` (set to N bits). The `autoprec` decorator + doubles precision until results converge — a "guess-and-verify" paradigm. + +**Takeaway**: The single-variable backend swap (`MPZ = gmpy.mpz`) is clean. +My C wrapper approach is analogous but at a lower level. I should make sure +that all GMP-dependent code stays isolated in a single module. + +### 13.2 gmpy2 (Python ↔ MPFR) — Context-Based Precision + +gmpy2 is the standard Python binding for GMP + MPFR + MPC. + +**How gmpy2 does it**: + +- **Context object**: `gmpy2.context()` controls precision, rounding mode, exception + traps, and exponent range. `get_context()` returns the active context; + `local_context()` provides temporary overrides (similar to Python's `decimal.localcontext()`). +- **5 rounding modes**: RoundToNearest, RoundToZero, RoundUp, RoundDown, RoundAwayZero. + These map directly to MPFR's rounding modes. +- **Thread safety**: MPFR data (flags, emin/emax, default precision) uses + thread-local storage when built with `--enable-thread-safe`. +- **Exception model**: Flags for overflow, underflow, inexact, invalid, divzero, + erange. Each has a corresponding `trap_*` bool — if True, raises a Python + exception; if False, sets the flag silently and returns a special value (NaN, ±Inf). +- **Precision tracking**: Each `mpfr` value carries its own precision. When combining + values of different precisions, the context precision governs the result. +- **Implicit conversion**: `mpz → mpfr` is automatic. `float → mpfr` preserves + all 53 bits. `str → mpfr` uses full specified precision. + +**Takeaway**: My `gmp: Bool = False` parameter approach is simpler but +sufficient. If I later want a richer API, a context object could control precision, +rounding mode, and GMP-enable globally. For now, per-call `gmp=True` is fine. + +### 13.3 Arb/FLINT — Ball Arithmetic & Guard Bits + +Arb (now merged into FLINT) implements rigorous arbitrary-precision ball arithmetic. + +**How Arb works**: + +- **Ball semantics**: Each number is `[midpoint ± radius]`. Operations propagate + error bounds automatically. The user doesn't manually track rounding errors. +- **Binary ↔ decimal conversion**: Arb explicitly warns that this conversion is + lossy. `arb_set_str("2.3", prec)` correctly creates a ball containing 23/10, + unlike a naive `double` assignment. `arb_printn()` only prints digits that are + guaranteed correct. +- **Guess-and-verify paradigm**: Start with modest precision, check if the result + is accurate enough, double precision and retry if not. This is the recommended + approach for complex computations. +- **Guard bits heuristic**: For accuracy of p bits, use working precision O(1)+p. + The O(1) term depends on the expression. "Adding a few guard bits" (10–20 for + basic operations) is usually enough. +- **Polynomial time guarantee**: Arb caps internal work parameters by polynomial + functions of `prec` to prevent runaway computation. E.g., `arb_sin(huge_x)` returns + `[±1]` instead of attempting expensive argument reduction. +- **Handle lifecycle**: GMP-style `arb_init(x)` / `arb_clear(x)`. Output parameters + come first, then inputs, then precision. + +**Takeaway**: The guard bits pattern confirms my approach of using +`prec + 10` in MPFR calls. I should also add a precision cap for safety. +The polynomial time guarantee is worth stealing for my iterative operations. + +### 13.4 python-flint — Direct FLINT/Arb Bindings + +python-flint wraps FLINT (which includes Arb) for Python. + +**How python-flint works**: + +- **Type hierarchy**: Separate types for each number ring: `fmpz` (integers), + `fmpq` (rationals), `arb` (real balls), `acb` (complex balls), plus polynomial + and matrix variants. +- **Global context**: Precision is set globally (`ctx.prec = 333` for ~100 decimal + digits). All operations use the current context precision. + +### 13.5 Consistency Check: My Plan vs. Best Practices + +| Best Practice (from survey) | Status in My Plan | Notes | +| ------------------------------------------ | ------------------- | ----------------------------------------------------------------- | +| Graceful fallback (GMP optional) | ✓ Designed | BigDecimal works without MPFR; BigFloat requires it | +| First-class user-facing type | ✓ BigFloat | Like mpmath's `mpf` — user constructs and chains operations | +| Convenience sugar on existing type | ✓ `gmp=True` | BigDecimal ops accept `gmp=True` for one-off acceleration | +| Isolated backend module | ✓ `bigfloat.mojo` | All MPFR calls in one file | +| Guard bits in MPFR calls | ✓ `prec + 10` | Confirmed adequate by Arb docs | +| Decimal ↔ binary via string (not raw bits) | ✓ Planned | `mpfr_set_str` / `mpfr_get_str`, base 10 | +| Environment variable to disable GMP | ✗ **Not yet** | **TODO**: `DECIMO_NOGMP` env check (like mpmath `MPMATH_NOGMPY`) | +| Handle pool with lifecycle management | ✓ Planned | C wrapper handle pool | +| Rounding mode control | △ Partial | RNDN only. Consider exposing rounding mode later | +| Precision cap for safety | ✗ **Not yet** | **TODO**: Cap internal MPFR precision to prevent OOM | +| Conversion overhead awareness | ✓ Documented | String round-trip mitigated by BigFloat chaining + lazy future | +| Thread safety considerations | ✗ **Not yet** | **TODO**: MPFR handle pool is not thread-safe; document this | +| Benchmark-driven thresholds | ✓ Planned (Phase 1) | Find break-even precision for each operation | +| Lazy evaluation for chained ops | △ Future | `BigDecimal.lazy()...collect()` — deferred, architecturally clean | + +**Gaps I found** (incorporated into the roadmap below): + +1. **`DECIMO_NOGMP` environment variable** (Phase 1) — lets users disable GMP at + runtime, like mpmath's `MPMATH_NOGMPY`. +2. **Precision cap** (Phase 1) — prevent MPFR from allocating unbounded memory + for extreme precision values. +3. **Thread safety docs** (Phase 4) — the C handle pool is not thread-safe; + need to document this. + +## 14. Implementation Roadmap + +> BigFloat first (Phase 1), then BigDecimal `gmp=True` sugar (Phase 2), then BigInt +> backend (Phase 3 or never). MPFR has built-in sqrt, exp, ln, sin, cos, tan, π with correct +> rounding — so BigFloat operations are single MPFR calls, and BigDecimal sugar is +> just BigFloat under the hood. + +### 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) + Add to `gmp_wrapper.c`: + (a) `static mpfr_t f_handles[MAX_HANDLES]` pool, + (b) `mpfrw_init(prec_bits) → handle`, + (c) `mpfrw_clear(h)`, + (d) `mpfrw_set_str(h, str, len)` (length-safe, base-10), + (e) `mpfrw_get_str(h, digits) → char*`, + (f) `mpfrw_available()`. + 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) + 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) + `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) + 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) + 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) + 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) + 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) + `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) + 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) + 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) + `pixi.toml` tasks: `build-gmp-wrapper` (compiles C wrapper + links MPFR). + `build_gmp_wrapper.sh` for macOS + Linux. + +**Deliverable**: `BigFloat("2", 5000).sqrt()` works. Users can construct BigFloat, +chain operations, and convert back to BigDecimal. + +### Phase 2: BigDecimal `gmp=True` Convenience Sugar + +**Goal**: Wire `gmp=True` into BigDecimal compute-heavy operations. Internally each +uses BigFloat (Phase 1), so this is just plumbing. + +- [ ] **2.1 Wire `sqrt(x, P, gmp=True)`** (Small, deps: Phase 1) + If `gmp=True`: `BigFloat(x, P+10).sqrt().to_bigdecimal(P)`. Three lines. +- [ ] **2.2 Wire `true_divide(x, y, P, gmp=True)`** (Small, deps: Phase 1) + `BigFloat(x, P+10) / BigFloat(y, P+10) → to_bigdecimal(P)`. +- [ ] **2.3 Wire `root`, `exp`, `ln`, `sin`, `cos`, `tan`** (Small, deps: Phase 1) + Each is a one-liner delegating to BigFloat. Single MPFR call per operation. +- [ ] **2.4 Wire `pi(P, gmp=True)`** (Small, deps: Phase 1) + `BigFloat.pi(P+10).to_bigdecimal(P)`. No Chudnovsky needed. +- [ ] **2.5 Instance method wrappers** (Small, deps: 2.4) + Update `self.sqrt()`, `self.exp()`, etc. to accept `gmp: Bool = False`. +- [ ] **2.6 Comprehensive test suite** (Large, deps: 2.5) + For each function × {gmp=True, gmp=False}: verify agreement to requested + precision. Edge cases: 0, negative, very large/small. +- [ ] **2.7 Benchmark gmp=True vs gmp=False** (Medium, deps: 2.6) + Table: operation × precision (28, 100, 500, 1000, 5000) × gmp. Include + conversion overhead. + +**Deliverable**: All iterative BigDecimal functions support `gmp=True`. Existing +BigDecimal API unchanged for `gmp=False`. + +**Checkpoint test**: `pi(precision=10000, gmp=True)` matches known π digits and runs +significantly faster than `pi(precision=10000)`. + +### Phase 3: BigInt GMP Backend (Deferred, maybe never) + +**Goal**: GMP backend for BigInt (base-2³²) using `mpz_t`. Deferred — integer +ops are less user-facing. + +- [ ] **3.1 Word import/export in C wrapper** (Small, deps: Phase 1) + Complete `gmpw_import_u32_le` / `gmpw_export_u32_le`. Test round-trip. +- [ ] **3.2 Create `src/decimo/bigint/gmp_backend.mojo`** (Medium, deps: 3.1) + GMPBigInt struct. Convert BigInt ↔ GMP via O(n) word import/export. +- [ ] **3.3 Add `gmp: Bool = False` to BigInt ops** (Medium, deps: 3.2) + On `multiply`, `floor_divide`, `sqrt`, `gcd`, `power` free functions. +- [ ] **3.4 Tests and benchmarks** (Medium, deps: 3.3) + Verify correctness. Confirm 7–18× speedup. + +### Phase 4: Polish, Distribution & Future + +- [ ] **4.1 Cross-platform wrapper compilation (Linux)** (Small, deps: Phase 1) +- [ ] **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. +- [ ] **4.4 Documentation: user guide for BigFloat and GMP integration** (Small, deps: Phase 2) +- [ ] **4.5 Pure-Mojo runtime detection (when DLHandle available)** (Medium, deps: Mojo improvement) +- [ ] **4.6 GMP rational numbers (`mpq_t`) for exact division** (Medium, future) +- [ ] **4.7 Document thread safety** (Small, deps: Phase 1) + The C handle pool is not thread-safe; MPFR requires `--enable-thread-safe` for + concurrent use. Document this limitation. +- [ ] **4.8 Lazy evaluation: `BigDecimal.lazy()...collect()`** (Large, future) + Record a DAG of operations, execute entire chain in MPFR with zero intermediate + conversions. See Section 9.9 for design. Architecturally clean because BigFloat + and the MPFR wrapper (Phase 1) already provide the backend. + +### Estimated Effort Summary + +- **Phase 1: MPFR wrapper + BigFloat** — 12 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. + +## 15. Appendix: Prototype Results + +### 15.1 Files Created + +| File | Purpose | Status | +| ----------------------------------- | ------------------------- | -------------------------------------- | +| `temp/gmp/gmp_wrapper.c` | C wrapper library source | ✓ Complete | +| `temp/gmp/libgmp_wrapper.dylib` | Compiled shared library | ✓ Built | +| `temp/gmp/test_gmp_ffi.mojo` | FFI test suite (18 tests) | ✓ 14/18 pass (4 wrong expected values) | +| `temp/gmp/bench_gmp_vs_decimo.mojo` | GMP benchmark suite | ✓ Complete | +| `temp/gmp/build_and_test.sh` | Build and test automation | ✓ Complete | + +### 15.2 C Wrapper API Summary + +```c +// Lifecycle +int gmpw_init(void); // → handle +void gmpw_clear(int handle); + +// String I/O +int gmpw_set_str(int handle, const char* str, int base); +int gmpw_get_str_buf(int handle, char* buf, int bufsize, int base); + +// Arithmetic +void gmpw_add(int r, int a, int b); +void gmpw_sub(int r, int a, int b); +void gmpw_mul(int r, int a, int b); +void gmpw_fdiv_q(int r, int a, int b); // Floor division +void gmpw_tdiv_q(int r, int a, int b); // Truncate division +void gmpw_mod(int r, int a, int b); +void gmpw_pow_ui(int r, int base, unsigned long exp); +void gmpw_powm(int r, int base, int exp, int mod); // Modular exponentiation +void gmpw_sqrt(int r, int a); +void gmpw_gcd(int r, int a, int b); +void gmpw_neg(int r, int a); +void gmpw_abs(int r, int a); + +// Comparison +int gmpw_cmp(int a, int b); +int gmpw_sign(int handle); +int gmpw_sizeinbase10(int handle); + +// Bitwise +void gmpw_and(int r, int a, int b); +void gmpw_or(int r, int a, int b); +void gmpw_xor(int r, int a, int b); +void gmpw_lshift(int r, int a, unsigned long bits); +void gmpw_rshift(int r, int a, unsigned long bits); + +// Binary word I/O +void gmpw_import_u32_le(int handle, const uint32_t* words, int count); +int gmpw_export_u32_le(int handle, uint32_t* out, int max_words); +int gmpw_export_u32_count(int handle); + +// Meta +int gmpw_available(void); +const char* gmpw_version(void); +``` + +### 15.3 Sample Mojo FFI Calls + +```mojo +from std.ffi import external_call, c_int + +# ⚠️ SAFE pattern: pass length explicitly to avoid use-after-free (see Section 16.1) +def gmp_set_str(h: c_int, s: String): + var slen = c_int(len(s)) + var ptr = Int(s.unsafe_ptr()) + _ = external_call["gmpw_set_str_n", c_int, c_int, Int, c_int](h, ptr, slen) + +# ❌ UNSAFE pattern: DO NOT USE — String may be freed before external_call executes +# def gmp_set_str(h: c_int, s: String): +# _ = external_call["gmpw_set_str", c_int, c_int, Int](h, Int(s.unsafe_ptr())) + +def gmp_init() -> c_int: + return external_call["gmpw_init", c_int]() + +def gmp_clear(h: c_int): + external_call["gmpw_clear", NoneType, c_int](h) + +def gmp_add(r: c_int, a: c_int, b: c_int): + external_call["gmpw_add", NoneType, c_int, c_int, c_int](r, a, b) +``` + +### 15.4 Benchmark Data Table + +**GMP absolute timings** (per operation, macOS ARM64, Apple M-series): + +| Digits | Multiply | Add | Divide | Sqrt | GCD | +| ------- | -------- | ------ | ------ | ------ | ------ | +| 10 | 5 ns | 7 ns | — | 8 ns | 8 ns | +| 100 | 23 ns | 8 ns | 67 ns | 114 ns | 50 ns | +| 1,000 | 1 µs | 20 ns | 818 ns | 722 ns | 169 ns | +| 10,000 | 50 µs | 226 ns | 30 µs | 21 µs | 2 µs | +| 100,000 | 939 µs | 2 µs | 740 µs | 642 µs | 324 µs | + +## 16. Lessons Learned from Prototype Development + +This section documents gotchas from the GMP prototype build-test cycle +and how I fixed them. **Read this before starting implementation** — every item +cost me real debugging time and will bite again if not handled. + +### 16.1 CRITICAL: Mojo String Use-After-Free in FFI + +**Symptom**: GMP's `mpz_set_str()` silently fails (returns -1), leaving the mpz as 0. Subsequent operations on zero values trigger `__gmp_overflow_in_mpz` in division +or crash with corrupt data. + +**Root cause**: When you write: + +```mojo +def gmp_set_str(h: c_int, s: String): + _ = external_call["gmpw_set_str", c_int, c_int, Int](h, Int(s.unsafe_ptr())) +``` + +Mojo's optimizer may drop the `String` parameter `s` (freeing its buffer) before +`external_call` reads the pointer. Under heavy allocation pressure (millions of +BigInt/BigDecimal ops), the freed buffer gets reused immediately, +overwriting the null terminator. GMP reads past the string into adjacent +memory, hits non-digit bytes, returns -1. + +**How I found it**: The crash only showed up in the full benchmark binary after +millions of prior allocations — never in small test files. Adding `fprintf(stderr)` +logging to the C wrapper revealed: + +- `strlen()` on the pointer showed 109 instead of expected 100 +- `mpz_set_str` returned -1 (failure), not 0 (success) +- The extra bytes were from adjacent String buffers reusing freed memory + +**Fix**: Pass the string length explicitly and copy in C: + +```c +// In gmp_wrapper.c +int gmpw_set_str_n(int h, const char *str, int len) { + char *buf = (char *)malloc(len + 1); + memcpy(buf, str, len); + buf[len] = '\0'; + int rc = mpz_set_str(handles[h], buf, 10); + free(buf); + return rc; +} +``` + +```mojo +# In Mojo +def gmp_set_str(h: c_int, s: String): + var slen = c_int(len(s)) # Extract length BEFORE ptr + var ptr = Int(s.unsafe_ptr()) + _ = external_call["gmpw_set_str_n", c_int, c_int, Int, c_int](h, ptr, slen) +``` + +**Rule**: **Always use `gmpw_set_str_n` with explicit length for any String → C FFI +call.** Don't rely on null-termination of Mojo `String.unsafe_ptr()` in `external_call`. + +**Broader implication**: This affects ALL FFI calls passing Mojo `String` pointers. +Every such call must either: + +1. Use the length-aware C variant (recommended), or +2. Store the String in a local variable AND ensure no other allocation occurs between + `unsafe_ptr()` extraction and the `external_call` (fragile, not recommended) + +### 16.2 `mojo run` Does Not Support `-Xlinker` Flags + +**Symptom**: `mojo run file.mojo -Xlinker -lgmp_wrapper` ignores the linker flags, +causing "undefined symbol" errors at runtime. + +**Cause**: The JIT (`mojo run`) does not process `-Xlinker` flags. Only `mojo build` +(AOT compilation) supports them. + +**Fix**: Always use `mojo build` → run the executable. Never use `mojo run` for +GMP-linked code. + +```bash +# ✓ Correct +pixi run mojo build -Xlinker -L... -Xlinker -lgmp_wrapper -o program program.mojo +./program + +# ✗ Wrong — linker flags silently ignored +pixi run mojo run -Xlinker -L... program.mojo +``` + +### 16.3 Mojo 0.26.2 Syntax Differences + +Several Mojo syntax changes from earlier versions caused compile errors: + +| Feature | Old Syntax | Mojo 0.26.2 Syntax | +| ------------------ | ---------------------------------- | ---------------------------------------------- | +| Compile-time const | `alias X = 7` | `comptime X = 7` | +| String slicing | `s[:10]` | `s[byte=:10]` | +| UInt → Float64 | `Float64(some_uint)` | `Float64(Int(some_uint))` | +| UnsafePointer | `UnsafePointer[Byte](address=ptr)` | `UnsafePointer[Byte](unsafe_from_address=ptr)` | + +### 16.4 GMP `__gmp_overflow_in_mpz` Means Bad Input, Not Overflow + +Despite the name, `__gmp_overflow_in_mpz` usually means the `mpz_t` struct has +corrupted metadata (alloc=0, size=0, or bad limb pointer), NOT that a result was too large. If you see this: + +1. **First check**: Did `mpz_set_str` fail? (Return value -1 means the string was invalid.) +2. **Second check**: Was the handle cleared and reused? +3. **Third check**: Was the handle ever initialized? + +In my case it was always (1) — the string pointer was dangling. + +### 16.5 Handle Pool Sizing + +The C wrapper uses a static array of `MAX_HANDLES = 4096`. For Phase 1 (sqrt), +each Newton iteration typically uses 3–5 temporary handles, so 4096 is plenty. But for +Phase 2 (complex expression trees), monitor handle usage. Consider: + +- Adding `gmpw_handles_in_use()` diagnostic function +- Expanding to dynamic allocation if 4096 is insufficient +- Ensuring every code path calls `gmpw_clear()` (RAII via `BigFloat.__del__`) + +### 16.6 Build Command Template + +The full build command for any GMP-linked Mojo file on macOS: + +```bash +pixi run mojo build \ + -Xlinker -L/path/to/temp/gmp -Xlinker -lgmp_wrapper \ + -Xlinker -L/opt/homebrew/lib -Xlinker -lgmp \ + -Xlinker -rpath -Xlinker /path/to/temp/gmp \ + -Xlinker -rpath -Xlinker /opt/homebrew/lib \ + -I src \ + -o output_binary input_file.mojo +``` + +Key points: + +- `-I src` is needed for `from decimo.xxx import ...` to resolve +- Both `-L` (link-time) and `-rpath` (run-time) paths needed for each library +- Build must be run from the project root directory for module resolution +- The binary must be run from the directory containing `libgmp_wrapper.dylib` + (or use absolute rpath) + +### 16.7 Debugging Tip: Add `fprintf(stderr)` to C Wrapper + +When debugging FFI issues, temporarily add `fprintf(stderr)` logging to the C wrapper +functions. This is invaluable because Mojo's error reporting for FFI issues is limited. +The C wrapper can inspect raw pointer values, string contents, and GMP internal state +(`handles[h]->_mp_alloc`, `_mp_size`, `_mp_d`) that are invisible from the Mojo side. + +Always remove debug logging before benchmarking — `fprintf` has measurable overhead. + +## Summary + +GMP/MPFR integration is **proven and worth doing** for Decimo. Two-type architecture: + +1. **BigFloat** — new first-class MPFR-backed binary float type. Like mpmath's `mpf` + but faster. Every operation (sqrt, exp, ln, sin, cos, tan, π, divide) is a **single + MPFR call**. Struct is one field (`handle: Int32`). Requires MPFR — no fallback, + no apologies. Users without MPFR use BigDecimal. +2. **BigDecimal `gmp=True`** — convenience sugar for one-off acceleration. Internally + delegates to BigFloat. Backward-compatible: defaults to `gmp=False`, existing code + unchanged. Guard digits handle binary↔decimal rounding. +3. **Two types, two use cases** — BigFloat for fast scientific computing (binary + semantics). BigDecimal for exact decimal arithmetic (finance, human-friendly output). + Both in one library. +4. **Single binary, runtime detection** — C wrapper uses `dlopen`/`dlsym` to load + MPFR lazily (verified on Mojo 0.26.2). If MPFR isn't installed, BigDecimal works + fine without it; BigFloat raises a clear error. +5. **BigInt** deferred to Phase 3 (`mpz_t` with word import/export, ~10× speedup). +6. **Future: lazy evaluation** — `BigDecimal.lazy()...collect()` records an operation + DAG and executes the whole chain in MPFR with zero intermediate conversions. + Deferred to Phase 4, but architecturally clean because BigFloat provides the backend. diff --git a/docs/readme_cli.md b/docs/readme_cli.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/readme_unreleased.md b/docs/readme_unreleased.md index 59895438..9cf0ec58 100644 --- a/docs/readme_unreleased.md +++ b/docs/readme_unreleased.md @@ -3,16 +3,16 @@ An arbitrary-precision integer and decimal library for [Mojo](https://www.modular.com/mojo), inspired by Python's `int` and `Decimal`. [![Version](https://img.shields.io/github/v/tag/forfudan/decimo?label=version&color=blue)](https://github.com/forfudan/decimo/releases) +[![Mojo](https://img.shields.io/badge/mojo-0.26.2-orange)](https://docs.modular.com/mojo/manual/) [![pixi](https://img.shields.io/badge/pixi%20add-decimo-purple)](https://prefix.dev/channels/modular-community/packages/decimo) [![CI](https://img.shields.io/github/actions/workflow/status/forfudan/decimo/run_tests.yaml?branch=main&label=tests)](https://github.com/forfudan/decimo/actions/workflows/run_tests.yaml) -[![Last Commit](https://img.shields.io/github/last-commit/forfudan/decimo?color=red)](https://github.com/forfudan/decimo/commits/main) ") - writer.write(self.function) - if self.message is None: - writer.write("\n") - else: - writer.write("\n\n") - writer.write(Self.error_type) + writer.write(_RESET) + writer.write('"') + writer.write(", line ") + writer.write(_CLR_LINE_NUM) + writer.write(String(self.line)) + writer.write(_RESET) + if self.function is not None: + writer.write(", in ") + writer.write(_CLR_FUNC_NAME) + writer.write(self.function.value()) + writer.write(_RESET) + writer.write("\n") + + # "ValueError: description of what went wrong" + writer.write(_CLR_ERROR_TYPE) + writer.write(Self.error_type) + writer.write(_RESET) + if self.message is not None: writer.write(": ") + writer.write(_CLR_MSG_TEXT) writer.write(self.message.value()) - writer.write("\n") - if self.previous_error is not None: - writer.write("\n") - writer.write(self.previous_error.value()) + writer.write(_RESET) + writer.write("\n") diff --git a/src/decimo/str.mojo b/src/decimo/str.mojo index 8da18506..a399794a 100644 --- a/src/decimo/str.mojo +++ b/src/decimo/str.mojo @@ -18,6 +18,8 @@ from std.algorithm import vectorize +from decimo.errors import ValueError + def rjust(s: String, width: Int, fillchar: String = " ") -> String: """Right-justifies a string by padding with a fill character on the left. @@ -124,7 +126,12 @@ def parse_numeric_string( var n = len(value_bytes) if n == 0: - raise Error("Error in `parse_numeric_string`: Empty string.") + raise Error( + ValueError( + message="Empty string.", + function="parse_numeric_string()", + ) + ) var ptr = value_bytes.unsafe_ptr() @@ -188,9 +195,19 @@ def parse_numeric_string( elif c == 46: last_was_separator = False if in_exponent: - raise Error("Decimal point cannot appear in the exponent part.") + raise Error( + ValueError( + message=( + "Decimal point cannot appear in the exponent part." + ), + ) + ) if decimal_point_pos != -1: - raise Error("Decimal point can only appear once.") + raise Error( + ValueError( + message="Decimal point can only appear once.", + ) + ) decimal_point_pos = i sign_read = True @@ -198,9 +215,17 @@ def parse_numeric_string( elif c == 101 or c == 69: last_was_separator = True if in_exponent: - raise Error("Exponential notation can only appear once.") + raise Error( + ValueError( + message="Exponential notation can only appear once.", + ) + ) if total_mantissa_digits == 0: - raise Error("Exponential notation must follow a number.") + raise Error( + ValueError( + message="Exponential notation must follow a number.", + ) + ) exponent_pos = i in_exponent = True @@ -210,14 +235,23 @@ def parse_numeric_string( if in_exponent: if exponent_sign_read: raise Error( - "Exponent sign can only appear once," - " before exponent digits." + ValueError( + message=( + "Exponent sign can only appear once," + " before exponent digits." + ), + ) ) exponent_sign_read = True else: if sign_read: raise Error( - "Minus sign can only appear once at the beginning." + ValueError( + message=( + "Minus sign can only appear once at the" + " beginning." + ), + ) ) sign = True sign_read = True @@ -228,29 +262,48 @@ def parse_numeric_string( if in_exponent: if exponent_sign_read: raise Error( - "Exponent sign can only appear once," - " before exponent digits." + ValueError( + message=( + "Exponent sign can only appear once," + " before exponent digits." + ), + ) ) exponent_sign_read = True else: if sign_read: raise Error( - "Plus sign can only appear once at the beginning." + ValueError( + message=( + "Plus sign can only appear once at the" + " beginning." + ), + ) ) sign_read = True else: raise Error( - String( - "Invalid character in the string of the number: {}" - ).format(chr(Int(c))) + ValueError( + message=String( + "Invalid character in the string of the number: {}" + ).format(chr(Int(c))), + ) ) if last_was_separator: - raise Error("Unexpected end character in the string of the number.") + raise Error( + ValueError( + message="Unexpected end character in the string of the number.", + ) + ) if total_mantissa_digits == 0: - raise Error("No digits found in the string of the number.") + raise Error( + ValueError( + message="No digits found in the string of the number.", + ) + ) # ================================================================== # Parse exponent value (separate from pass 1 to keep the main loop diff --git a/src/decimo/tests.mojo b/src/decimo/tests.mojo index 061faa49..3258e5f1 100644 --- a/src/decimo/tests.mojo +++ b/src/decimo/tests.mojo @@ -52,6 +52,7 @@ Pattern expansion in string values: from .toml import parse_file as parse_toml_file from .toml.parser import TOMLDocument +from .errors import DecimoError from std.python import Python, PythonObject from std.collections import List from std import os @@ -303,10 +304,11 @@ def parse_file(file_path: String) raises -> TOMLDocument: return parse_toml_file(file_path) except e: raise Error( - "tests.parse_file(): Failed to parse TOML file:", - file_path, - "\nTraceback:", - e, + DecimoError( + message="Failed to parse TOML file: " + file_path, + function="parse_file()", + previous_error=e^, + ) ) diff --git a/src/decimo/toml/parser.mojo b/src/decimo/toml/parser.mojo index f85443f0..785b79b0 100644 --- a/src/decimo/toml/parser.mojo +++ b/src/decimo/toml/parser.mojo @@ -30,6 +30,7 @@ Supports: """ from std.collections import Dict +from decimo.errors import ValueError from .tokenizer import Token, TokenType, Tokenizer @@ -471,11 +472,21 @@ def _set_value( var existing = root[key].table_values.copy() for entry in value.table_values.items(): if entry.key in existing: - raise Error("Duplicate key: " + key + "." + entry.key) + raise Error( + ValueError( + message="Duplicate key: " + + key + + "." + + entry.key, + function="parse()", + ) + ) existing[entry.key] = entry.value.copy() root[key] = _make_table(existing^) return - raise Error("Duplicate key: " + key) + raise Error( + ValueError(message="Duplicate key: " + key, function="parse()") + ) root[key] = value^ return @@ -496,7 +507,12 @@ def _set_value( arr[len(arr) - 1] = _make_table(last_tbl^) root[first].array_values = arr^ return - raise Error("Key exists but is not a table: " + first) + raise Error( + ValueError( + message="Key exists but is not a table: " + first, + function="parse()", + ) + ) var table = root[first].table_values.copy() var remaining = List[String]() @@ -533,7 +549,12 @@ def _ensure_table_path( root[first].array_values = arr^ return elif root[first].type != TOMLValueType.TABLE: - raise Error("Key exists but is not a table: " + first) + raise Error( + ValueError( + message="Key exists but is not a table: " + first, + function="parse()", + ) + ) if len(path) > 1: var table = root[first].table_values.copy() @@ -553,7 +574,12 @@ def _append_array_of_tables( ) raises: """Append a new empty table to the array-of-tables at `path`.""" if len(path) == 0: - raise Error("Array of tables path cannot be empty") + raise Error( + ValueError( + message="Array of tables path cannot be empty", + function="parse()", + ) + ) if len(path) == 1: var key = path[0] @@ -569,7 +595,12 @@ def _append_array_of_tables( _make_table(Dict[String, TOMLValue]()) ) else: - raise Error("Cannot redefine as array of tables: " + key) + raise Error( + ValueError( + message="Cannot redefine as array of tables: " + key, + function="parse()", + ) + ) return # Multi-part path: navigate to the parent, then handle the last key @@ -596,7 +627,12 @@ def _append_array_of_tables( arr[len(arr) - 1] = _make_table(last_tbl^) root[first].array_values = arr^ else: - raise Error("Key exists but is not a table or array: " + first) + raise Error( + ValueError( + message="Key exists but is not a table or array: " + first, + function="parse()", + ) + ) struct TOMLParser: @@ -669,7 +705,7 @@ struct TOMLParser: var parts = List[String]() if not self._is_key_token(): - raise Error("Expected key") + raise Error(ValueError(message="Expected key", function="parse()")) parts.append(self._tok().value) self._advance() @@ -678,7 +714,11 @@ struct TOMLParser: while self._tok().type == TokenType.DOT: self._advance() # skip dot if not self._is_key_token(): - raise Error("Expected key after dot") + raise Error( + ValueError( + message="Expected key after dot", function="parse()" + ) + ) parts.append(self._tok().value) self._advance() @@ -816,7 +856,12 @@ struct TOMLParser: # Expect equals if self._tok().type != TokenType.EQUAL: - raise Error("Expected '=' in inline table") + raise Error( + ValueError( + message="Expected '=' in inline table", + function="parse()", + ) + ) self._advance() # Parse value @@ -844,7 +889,12 @@ struct TOMLParser: self._advance() break else: - raise Error("Expected ',' or '}' in inline table") + raise Error( + ValueError( + message="Expected ',' or '}' in inline table", + function="parse()", + ) + ) return _make_table(table^) @@ -858,7 +908,12 @@ struct TOMLParser: if self._tok().type == TokenType.ARRAY_END: self._advance() else: - raise Error("Expected ']' after table header") + raise Error( + ValueError( + message="Expected ']' after table header", + function="parse()", + ) + ) return path^ @@ -870,11 +925,21 @@ struct TOMLParser: if self._tok().type == TokenType.ARRAY_END: self._advance() else: - raise Error("Expected ']]' after array of tables header") + raise Error( + ValueError( + message="Expected ']]' after array of tables header", + function="parse()", + ) + ) if self._tok().type == TokenType.ARRAY_END: self._advance() else: - raise Error("Expected ']]' after array of tables header") + raise Error( + ValueError( + message="Expected ']]' after array of tables header", + function="parse()", + ) + ) return path^ From 78b0c9c7a27ebb996264580724948e11f68ae522 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Thu, 9 Apr 2026 19:55:08 +0200 Subject: [PATCH 07/17] [error] Make `message` and `function` fields mandatory for `DecimoError` type (#196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates Decimo’s structured error type so that `DecimoError` always includes a `message` and `function`, ensuring tracebacks consistently show both fields (alongside auto-captured file/line). **Changes:** - Make `DecimoError.message` and `DecimoError.function` mandatory (non-optional) and require them in `__init__`. - Update error construction call sites across parsing, numeric parsing, and numeric types to pass `message` + `function`. - Convert a couple of previously string-raised errors to structured `ValueError` instances to comply with the new contract. --- src/decimo/bigfloat/bigfloat.mojo | 111 +++++++++++++++++++++---- src/decimo/bigint/bigint.mojo | 3 + src/decimo/bigint10/arithmetics.mojo | 6 ++ src/decimo/bigint10/bigint10.mojo | 4 + src/decimo/biguint/arithmetics.mojo | 4 + src/decimo/biguint/biguint.mojo | 11 +++ src/decimo/decimal128/decimal128.mojo | 21 ++++- src/decimo/decimal128/exponential.mojo | 3 + src/decimo/errors.mojo | 40 +++++---- src/decimo/str.mojo | 11 +++ src/decimo/toml/parser.mojo | 6 +- 11 files changed, 177 insertions(+), 43 deletions(-) diff --git a/src/decimo/bigfloat/bigfloat.mojo b/src/decimo/bigfloat/bigfloat.mojo index 9b03360a..76fee07e 100644 --- a/src/decimo/bigfloat/bigfloat.mojo +++ b/src/decimo/bigfloat/bigfloat.mojo @@ -151,7 +151,8 @@ struct BigFloat(Comparable, Movable, Writable): message=( "BigFloat requires MPFR (brew install mpfr / apt" " install libmpfr-dev)" - ) + ), + function="BigFloat.__init__()", ) ) var bits = _dps_to_bits(precision) @@ -468,7 +469,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.__neg__()", + ) + ) mpfrw_neg(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -480,7 +486,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.__abs__()", + ) + ) mpfrw_abs(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -500,7 +511,12 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.__add__()", + ) + ) mpfrw_add(h, self.handle, other.handle) return Self(_handle=h, _precision=prec) @@ -516,7 +532,12 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.__sub__()", + ) + ) mpfrw_sub(h, self.handle, other.handle) return Self(_handle=h, _precision=prec) @@ -532,7 +553,12 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.__mul__()", + ) + ) mpfrw_mul(h, self.handle, other.handle) return Self(_handle=h, _precision=prec) @@ -548,7 +574,12 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.__truediv__()", + ) + ) mpfrw_div(h, self.handle, other.handle) return Self(_handle=h, _precision=prec) @@ -575,7 +606,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.sqrt()", + ) + ) mpfrw_sqrt(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -587,7 +623,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.exp()", + ) + ) mpfrw_exp(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -599,7 +640,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.ln()", + ) + ) mpfrw_log(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -611,7 +657,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.sin()", + ) + ) mpfrw_sin(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -623,7 +674,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.cos()", + ) + ) mpfrw_cos(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -635,7 +691,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.tan()", + ) + ) mpfrw_tan(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -651,7 +712,12 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, exponent.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.power()", + ) + ) mpfrw_pow(h, self.handle, exponent.handle) return Self(_handle=h, _precision=prec) @@ -666,7 +732,12 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.root()", + ) + ) mpfrw_rootn_ui(h, self.handle, n) return Self(_handle=h, _precision=self.precision) @@ -686,11 +757,17 @@ struct BigFloat(Comparable, Movable, Writable): message=( "BigFloat requires MPFR (brew install mpfr / apt" " install libmpfr-dev)" - ) + ), + function="BigFloat.pi()", ) ) var h = mpfrw_init(_dps_to_bits(precision)) if h < 0: - raise Error(DecimoError(message="Handle allocation failed")) + raise Error( + DecimoError( + message="Handle allocation failed.", + function="BigFloat.pi()", + ) + ) mpfrw_const_pi(h) return BigFloat(_handle=h, _precision=precision) diff --git a/src/decimo/bigint/bigint.mojo b/src/decimo/bigint/bigint.mojo index 9e5b175a..efc4ab7c 100644 --- a/src/decimo/bigint/bigint.mojo +++ b/src/decimo/bigint/bigint.mojo @@ -986,6 +986,7 @@ struct BigInt( except e: raise Error( DecimoError( + message="See the above exception.", function="BigInt.__floordiv__()", previous_error=e^, ) @@ -1006,6 +1007,7 @@ struct BigInt( except e: raise Error( DecimoError( + message="See the above exception.", function="BigInt.__mod__()", previous_error=e^, ) @@ -1026,6 +1028,7 @@ struct BigInt( except e: raise Error( DecimoError( + message="See the above exception.", function="BigInt.__divmod__()", previous_error=e^, ) diff --git a/src/decimo/bigint10/arithmetics.mojo b/src/decimo/bigint10/arithmetics.mojo index 8774f4e5..0446a292 100644 --- a/src/decimo/bigint10/arithmetics.mojo +++ b/src/decimo/bigint10/arithmetics.mojo @@ -227,6 +227,7 @@ def floor_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: except e: raise Error( DecimoError( + message="See the above exception.", function="floor_divide()", previous_error=e^, ), @@ -242,6 +243,7 @@ def floor_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: except e: raise Error( DecimoError( + message="See the above exception.", function="floor_divide()", previous_error=e^, ), @@ -272,6 +274,7 @@ def truncate_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: except e: raise Error( DecimoError( + message="See the above exception.", function="truncate_divide()", previous_error=e^, ), @@ -307,6 +310,7 @@ def floor_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: except e: raise Error( DecimoError( + message="See the above exception.", function="floor_modulo()", previous_error=e^, ), @@ -322,6 +326,7 @@ def floor_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: except e: raise Error( DecimoError( + message="See the above exception.", function="floor_modulo()", previous_error=e^, ), @@ -352,6 +357,7 @@ def truncate_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: except e: raise Error( DecimoError( + message="See the above exception.", function="truncate_modulo()", previous_error=e^, ), diff --git a/src/decimo/bigint10/bigint10.mojo b/src/decimo/bigint10/bigint10.mojo index 00d5c89b..c8dc3e2a 100644 --- a/src/decimo/bigint10/bigint10.mojo +++ b/src/decimo/bigint10/bigint10.mojo @@ -119,6 +119,7 @@ struct BigInt10( except e: raise Error( DecimoError( + message="See the above exception.", function=( "BigInt10.__init__(var words: List[UInt32], sign: Bool)" ), @@ -231,6 +232,7 @@ struct BigInt10( except e: raise Error( DecimoError( + message="See the above exception.", function=( "BigInt10.from_list(var words: List[UInt32], sign:" " Bool)" @@ -636,6 +638,7 @@ struct BigInt10( except e: raise Error( DecimoError( + message="See the above exception.", function="BigInt10.__floordiv__()", previous_error=e^, ) @@ -656,6 +659,7 @@ struct BigInt10( except e: raise Error( DecimoError( + message="See the above exception.", function="BigInt10.__mod__()", previous_error=e^, ) diff --git a/src/decimo/biguint/arithmetics.mojo b/src/decimo/biguint/arithmetics.mojo index b3f9fe34..715c7f94 100644 --- a/src/decimo/biguint/arithmetics.mojo +++ b/src/decimo/biguint/arithmetics.mojo @@ -3345,6 +3345,7 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: except e: raise Error( DecimoError( + message="See the above exception.", function="floor_modulo()", previous_error=e^, ) @@ -3357,6 +3358,7 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: except e: raise Error( DecimoError( + message="See the above exception.", function="floor_modulo()", previous_error=e^, ) @@ -3386,6 +3388,7 @@ def truncate_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: except e: raise Error( DecimoError( + message="See the above exception.", function="truncate_modulo()", previous_error=e^, ) @@ -3474,6 +3477,7 @@ def floor_divide_modulo( except e: raise Error( DecimoError( + message="See the above exception.", function="floor_divide_modulo()", previous_error=e^, ) diff --git a/src/decimo/biguint/biguint.mojo b/src/decimo/biguint/biguint.mojo index 6ae8b004..195fcd48 100644 --- a/src/decimo/biguint/biguint.mojo +++ b/src/decimo/biguint/biguint.mojo @@ -205,6 +205,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__init__(var words: List[UInt32])", previous_error=e^, ) @@ -250,6 +251,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__init__(value: Int)", previous_error=e^, ) @@ -285,6 +287,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__init__(value: String)", previous_error=e^, ) @@ -788,6 +791,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__int__()", previous_error=e^, ) @@ -1150,6 +1154,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__sub__(other: Self)", previous_error=e^, ) @@ -1182,6 +1187,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__floordiv__(other: Self)", previous_error=e^, ) @@ -1202,6 +1208,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__ceildiv__(other: Self)", previous_error=e^, ) @@ -1222,6 +1229,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__mod__(other: Self)", previous_error=e^, ) @@ -1242,6 +1250,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__divmod__(other: Self)", previous_error=e^, ) @@ -1262,6 +1271,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__pow__(exponent: Self)", previous_error=e^, ) @@ -1282,6 +1292,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): except e: raise Error( DecimoError( + message="See the above exception.", function="BigUInt.__pow__(exponent: Int)", previous_error=e^, ) diff --git a/src/decimo/decimal128/decimal128.mojo b/src/decimo/decimal128/decimal128.mojo index 60fca5f2..168d907d 100644 --- a/src/decimo/decimal128/decimal128.mojo +++ b/src/decimo/decimal128/decimal128.mojo @@ -400,10 +400,12 @@ struct Decimal128( if scale > UInt32(Self.MAX_SCALE): raise Error( - String( - "Error in Decimal128 constructor with five components:" - " Scale must be between 0 and 28, but got {}" - ).format(scale) + ValueError( + message=String( + "Scale must be between 0 and 28, but got {}." + ).format(scale), + function="Decimal128.from_components()", + ) ) var flags: UInt32 = 0 @@ -635,6 +637,7 @@ struct Decimal128( message=String( "Invalid characters in decimal128 string: {}" ).format(value), + function="Decimal128.from_string()", ) ) @@ -672,6 +675,7 @@ struct Decimal128( message=( "Minus sign cannot appear twice in exponent." ), + function="Decimal128.from_string()", ) ) elif exponent_notation_read: @@ -684,6 +688,7 @@ struct Decimal128( "Minus sign can only appear once at the" " beginning." ), + function="Decimal128.from_string()", ) ) else: @@ -698,6 +703,7 @@ struct Decimal128( message=( "Plus sign cannot appear twice in exponent." ), + function="Decimal128.from_string()", ) ) elif exponent_notation_read: @@ -709,6 +715,7 @@ struct Decimal128( "Plus sign can only appear once at the" " beginning." ), + function="Decimal128.from_string()", ) ) else: @@ -720,6 +727,7 @@ struct Decimal128( raise Error( ValueError( message="Decimal point can only appear once.", + function="Decimal128.from_string()", ) ) else: @@ -734,6 +742,7 @@ struct Decimal128( message=( "Exponential notation can only appear once." ), + function="Decimal128.from_string()", ) ) if not mantissa_start: @@ -742,6 +751,7 @@ struct Decimal128( message=( "Exponential notation must follow a number." ), + function="Decimal128.from_string()", ) ) else: @@ -789,6 +799,7 @@ struct Decimal128( message=String( "Exponent part is too large: {}" ).format(raw_exponent), + function="Decimal128.from_string()", ) ) @@ -825,6 +836,7 @@ struct Decimal128( message=String( "Invalid character in decimal128 string: {}" ).format(chr(Int(code))), + function="Decimal128.from_string()", ) ) @@ -832,6 +844,7 @@ struct Decimal128( raise Error( ValueError( message="Unexpected end character in decimal128 string.", + function="Decimal128.from_string()", ) ) diff --git a/src/decimo/decimal128/exponential.mojo b/src/decimo/decimal128/exponential.mojo index 98bdb6e9..f95e890d 100644 --- a/src/decimo/decimal128/exponential.mojo +++ b/src/decimo/decimal128/exponential.mojo @@ -84,6 +84,7 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: except e: raise Error( DecimoError( + message="See the above exception.", function="power()", previous_error=e^, ) @@ -95,6 +96,7 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: except e: raise Error( DecimoError( + message="See the above exception.", function="power()", previous_error=e^, ) @@ -109,6 +111,7 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: except e: raise Error( DecimoError( + message="See the above exception.", function="power()", previous_error=e^, ) diff --git a/src/decimo/errors.mojo b/src/decimo/errors.mojo index 083d2f75..57fabb6e 100644 --- a/src/decimo/errors.mojo +++ b/src/decimo/errors.mojo @@ -26,8 +26,8 @@ ValueError: description of what went wrong ``` File name and line number are automatically captured at the raise site using -`call_location()`. Function name is an optional argument -- Mojo does not have -a built-in way to get the current function name at runtime. +`call_location()`. Function name must be provided manually since Mojo does not +have a built-in way to get the current function name at runtime. """ from std.reflection import call_location @@ -88,7 +88,7 @@ struct DecimoError[error_type: String = "DecimoError"](Writable): ``` File name and line number are automatically captured at the raise site. - Function name is an optional argument since Mojo does not yet support + Function name must be provided manually since Mojo does not yet support runtime introspection of the current function name. Parameters: @@ -99,10 +99,10 @@ struct DecimoError[error_type: String = "DecimoError"](Writable): """The source file where the error occurred (auto-captured).""" var line: Int """The line number where the error occurred (auto-captured).""" - var function: Optional[String] - """An optional function name where the error occurred.""" - var message: Optional[String] - """An optional message describing the error.""" + var function: String + """The function name where the error occurred.""" + var message: String + """A message describing the error.""" var previous_error: Optional[String] """An optional formatted string of a previous error that caused this one.""" @@ -110,8 +110,8 @@ struct DecimoError[error_type: String = "DecimoError"](Writable): def __init__( out self, *, - message: Optional[String] = None, - function: Optional[String] = None, + message: String, + function: String, previous_error: Optional[Error] = None, ): """Creates a new `DecimoError` with auto-captured file and line. @@ -119,8 +119,8 @@ struct DecimoError[error_type: String = "DecimoError"](Writable): File name and line number are automatically captured from the call site. Args: - message: An optional message describing the error. - function: An optional function name where the error occurred. + message: A message describing the error. + function: The function name where the error occurred. previous_error: An optional previous error that caused this one. """ var loc = call_location() @@ -193,20 +193,18 @@ struct DecimoError[error_type: String = "DecimoError"](Writable): writer.write(_CLR_LINE_NUM) writer.write(String(self.line)) writer.write(_RESET) - if self.function is not None: - writer.write(", in ") - writer.write(_CLR_FUNC_NAME) - writer.write(self.function.value()) - writer.write(_RESET) + writer.write(", in ") + writer.write(_CLR_FUNC_NAME) + writer.write(self.function) + writer.write(_RESET) writer.write("\n") # "ValueError: description of what went wrong" writer.write(_CLR_ERROR_TYPE) writer.write(Self.error_type) writer.write(_RESET) - if self.message is not None: - writer.write(": ") - writer.write(_CLR_MSG_TEXT) - writer.write(self.message.value()) - writer.write(_RESET) + writer.write(": ") + writer.write(_CLR_MSG_TEXT) + writer.write(self.message) + writer.write(_RESET) writer.write("\n") diff --git a/src/decimo/str.mojo b/src/decimo/str.mojo index a399794a..877ef6c3 100644 --- a/src/decimo/str.mojo +++ b/src/decimo/str.mojo @@ -200,12 +200,14 @@ def parse_numeric_string( message=( "Decimal point cannot appear in the exponent part." ), + function="parse_numeric_string()", ) ) if decimal_point_pos != -1: raise Error( ValueError( message="Decimal point can only appear once.", + function="parse_numeric_string()", ) ) decimal_point_pos = i @@ -218,12 +220,14 @@ def parse_numeric_string( raise Error( ValueError( message="Exponential notation can only appear once.", + function="parse_numeric_string()", ) ) if total_mantissa_digits == 0: raise Error( ValueError( message="Exponential notation must follow a number.", + function="parse_numeric_string()", ) ) exponent_pos = i @@ -240,6 +244,7 @@ def parse_numeric_string( "Exponent sign can only appear once," " before exponent digits." ), + function="parse_numeric_string()", ) ) exponent_sign_read = True @@ -251,6 +256,7 @@ def parse_numeric_string( "Minus sign can only appear once at the" " beginning." ), + function="parse_numeric_string()", ) ) sign = True @@ -267,6 +273,7 @@ def parse_numeric_string( "Exponent sign can only appear once," " before exponent digits." ), + function="parse_numeric_string()", ) ) exponent_sign_read = True @@ -278,6 +285,7 @@ def parse_numeric_string( "Plus sign can only appear once at the" " beginning." ), + function="parse_numeric_string()", ) ) sign_read = True @@ -288,6 +296,7 @@ def parse_numeric_string( message=String( "Invalid character in the string of the number: {}" ).format(chr(Int(c))), + function="parse_numeric_string()", ) ) @@ -295,6 +304,7 @@ def parse_numeric_string( raise Error( ValueError( message="Unexpected end character in the string of the number.", + function="parse_numeric_string()", ) ) @@ -302,6 +312,7 @@ def parse_numeric_string( raise Error( ValueError( message="No digits found in the string of the number.", + function="parse_numeric_string()", ) ) diff --git a/src/decimo/toml/parser.mojo b/src/decimo/toml/parser.mojo index 785b79b0..27f2e14e 100644 --- a/src/decimo/toml/parser.mojo +++ b/src/decimo/toml/parser.mojo @@ -871,7 +871,11 @@ struct TOMLParser: if len(key_parts) == 1: if key_parts[0] in table: raise Error( - "Duplicate key in inline table: " + key_parts[0] + ValueError( + message="Duplicate key in inline table: " + + key_parts[0], + function="TOMLParser._parse_inline_table()", + ) ) table[key_parts[0]] = value^ else: From 2cf5e0fd528c002b7da20443b3795ac7cefa47ec Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Thu, 9 Apr 2026 22:22:10 +0200 Subject: [PATCH 08/17] [error] Remove redundant `Error` wrappers + Add `RuntimeError` type + Replace `DecimoError` with specific error types (#198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR refactors Decimo’s error-handling to raise the library’s typed `DecimoError[...]` aliases directly (e.g., `ValueError`, `OverflowError`) instead of wrapping them in `Error(...)`, introduces a `RuntimeError` type, and updates call sites/tests accordingly. **Changes:** - Remove redundant `Error(...)` wrappers across TOML parsing, numeric parsing, and arithmetic modules. - Add `RuntimeError` as a `DecimoError[...]` alias and update runtime-infrastructure failure sites (e.g., MPFR availability/allocation). - Replace uses of the generic `DecimoError` wrapper with more specific error types (and update tests to match). --- docs/plans/gmp_integration.md | 2 +- src/decimo/bigdecimal/arithmetics.mojo | 32 +- src/decimo/bigdecimal/bigdecimal.mojo | 30 +- src/decimo/bigdecimal/constants.mojo | 6 +- src/decimo/bigdecimal/exponential.mojo | 179 +++++------- src/decimo/bigdecimal/trigonometric.mojo | 8 +- src/decimo/bigfloat/bigfloat.mojo | 182 +++++------- src/decimo/bigint/arithmetics.mojo | 54 ++-- src/decimo/bigint/bigint.mojo | 104 +++---- src/decimo/bigint/exponential.mojo | 10 +- src/decimo/bigint/number_theory.mojo | 34 +-- src/decimo/bigint10/arithmetics.mojo | 62 ++-- src/decimo/bigint10/bigint10.mojo | 105 +++---- src/decimo/biguint/arithmetics.mojo | 183 +++++------- src/decimo/biguint/biguint.mojo | 354 ++++++++++------------- src/decimo/decimal128/arithmetics.mojo | 123 +++----- src/decimo/decimal128/constants.mojo | 8 +- src/decimo/decimal128/decimal128.mojo | 265 +++++++---------- src/decimo/decimal128/exponential.mojo | 148 ++++------ src/decimo/decimal128/rounding.mojo | 36 +-- src/decimo/decimal128/special.mojo | 30 +- src/decimo/errors.mojo | 6 +- src/decimo/str.mojo | 124 +++----- src/decimo/tests.mojo | 12 +- src/decimo/toml/parser.mojo | 113 +++----- 25 files changed, 858 insertions(+), 1352 deletions(-) diff --git a/docs/plans/gmp_integration.md b/docs/plans/gmp_integration.md index 37af4251..a0c32452 100644 --- a/docs/plans/gmp_integration.md +++ b/docs/plans/gmp_integration.md @@ -1321,7 +1321,7 @@ LD_LIBRARY_PATH=./src/decimo/gmp ./myprogram ```bash bash src/decimo/gmp/build_gmp_wrapper.sh \ -&& mojo build -I src \ +&& pixi run 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 diff --git a/src/decimo/bigdecimal/arithmetics.mojo b/src/decimo/bigdecimal/arithmetics.mojo index e3463fb2..055e3100 100644 --- a/src/decimo/bigdecimal/arithmetics.mojo +++ b/src/decimo/bigdecimal/arithmetics.mojo @@ -320,11 +320,9 @@ def true_divide( """ # Check for division by zero if y.coefficient.is_zero(): - raise Error( - ZeroDivisionError( - message="Division by zero.", - function="true_divide()", - ) + raise ZeroDivisionError( + message="Division by zero.", + function="true_divide()", ) # Handle dividend of zero @@ -698,11 +696,9 @@ def true_divide_inexact( # Check for division by zero if x2.coefficient.is_zero(): - raise Error( - ZeroDivisionError( - message="Division by zero.", - function="true_divide_inexact()", - ) + raise ZeroDivisionError( + message="Division by zero.", + function="true_divide_inexact()", ) # Handle dividend of zero @@ -916,11 +912,9 @@ def truncate_divide(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: """ # Check for division by zero if x2.coefficient.is_zero(): - raise Error( - ZeroDivisionError( - message="Division by zero.", - function="truncate_divide()", - ) + raise ZeroDivisionError( + message="Division by zero.", + function="truncate_divide()", ) # Handle dividend of zero @@ -961,11 +955,9 @@ def truncate_modulo( """ # Check for division by zero if x2.coefficient.is_zero(): - raise Error( - ZeroDivisionError( - message="Division by zero.", - function="truncate_modulo()", - ) + raise ZeroDivisionError( + message="Division by zero.", + function="truncate_modulo()", ) return subtract( diff --git a/src/decimo/bigdecimal/bigdecimal.mojo b/src/decimo/bigdecimal/bigdecimal.mojo index 2da23b7a..250773f5 100644 --- a/src/decimo/bigdecimal/bigdecimal.mojo +++ b/src/decimo/bigdecimal/bigdecimal.mojo @@ -27,7 +27,7 @@ from std.memory import UnsafePointer from std.python import PythonObject from std import testing -from decimo.errors import DecimoError, ConversionError, ValueError +from decimo.errors import ConversionError, ValueError from decimo.rounding_mode import RoundingMode from decimo.bigdecimal.rounding import round_to_precision from decimo.bigint10.bigint10 import BigInt10 @@ -362,22 +362,18 @@ struct BigDecimal( return Self(coefficient=BigUInt.zero(), scale=0, sign=False) if value != value: # Check for NaN - raise Error( - ValueError( - message="Cannot convert NaN to BigDecimal.", - function="BigDecimal.from_scalar()", - ) + raise ValueError( + message="Cannot convert NaN to BigDecimal.", + function="BigDecimal.from_scalar()", ) # Convert to string with full precision try: return Self.from_string(String(value)) except e: - raise Error( - ConversionError( - message="Cannot convert scalar to BigDecimal.", - function="BigDecimal.from_scalar()", - previous_error=e^, - ) + raise ConversionError( + message="Cannot convert scalar to BigDecimal.", + function="BigDecimal.from_scalar()", + previous_error=e^, ) @staticmethod @@ -544,12 +540,10 @@ struct BigDecimal( return Self(coefficient=coefficient^, scale=scale, sign=sign) except e: - raise Error( - ConversionError( - message="Failed to convert Python Decimal to BigDecimal.", - function="BigDecimal.from_python_decimal()", - previous_error=e^, - ), + raise ConversionError( + message="Failed to convert Python Decimal to BigDecimal.", + function="BigDecimal.from_python_decimal()", + previous_error=e^, ) # ===------------------------------------------------------------------=== # diff --git a/src/decimo/bigdecimal/constants.mojo b/src/decimo/bigdecimal/constants.mojo index f2b5536b..ac67c48a 100644 --- a/src/decimo/bigdecimal/constants.mojo +++ b/src/decimo/bigdecimal/constants.mojo @@ -165,10 +165,8 @@ def pi(precision: Int) raises -> BigDecimal: """ if precision < 0: - raise Error( - ValueError( - message="Precision must be non-negative", function="pi()" - ) + raise ValueError( + message="Precision must be non-negative", function="pi()" ) # TODO: When global variables are supported, diff --git a/src/decimo/bigdecimal/exponential.mojo b/src/decimo/bigdecimal/exponential.mojo index b63de430..44777d5e 100644 --- a/src/decimo/bigdecimal/exponential.mojo +++ b/src/decimo/bigdecimal/exponential.mojo @@ -236,20 +236,14 @@ def power( # Special cases if base.coefficient.is_zero(): if exponent.coefficient.is_zero(): - raise Error( - ValueError( - message="0^0 is undefined.", - function="power()", - ) + raise ValueError( + message="0^0 is undefined.", + function="power()", ) elif exponent.sign: - raise Error( - ZeroDivisionError( - message=( - "Division by zero (negative exponent with zero base)." - ), - function="power()", - ) + raise ZeroDivisionError( + message="Division by zero (negative exponent with zero base).", + function="power()", ) else: return BigDecimal(BigUInt.zero(), 0, False) @@ -273,14 +267,12 @@ def power( # Check for negative base with non-integer exponent if base.sign and not exponent.is_integer(): - raise Error( - ValueError( - message=( - "Negative base with non-integer exponent would produce" - " a complex result." - ), - function="power()", - ) + raise ValueError( + message=( + "Negative base with non-integer exponent would produce" + " a complex result." + ), + function="power()", ) # Optimization for integer exponents @@ -449,11 +441,9 @@ def root(x: BigDecimal, n: BigDecimal, precision: Int) raises -> BigDecimal: # Check for n = 0 if n.coefficient.is_zero(): - raise Error( - ValueError( - message="Cannot compute zeroth root.", - function="root()", - ) + raise ValueError( + message="Cannot compute zeroth root.", + function="root()", ) # Special case for integer roots - use more efficient implementation @@ -512,14 +502,11 @@ def root(x: BigDecimal, n: BigDecimal, precision: Int) raises -> BigDecimal: var n_is_integer = n.is_integer() var n_is_odd_reciprocal = is_odd_reciprocal(n) if not n_is_integer and not n_is_odd_reciprocal: - raise Error( - ValueError( - message=( - "Cannot compute non-odd-integer root of a negative" - " number." - ), - function="root()", - ) + raise ValueError( + message=( + "Cannot compute non-odd-integer root of a negative number." + ), + function="root()", ) elif n_is_integer: var result = integer_root(x, n, precision) @@ -574,27 +561,21 @@ def integer_root( # Handle special case: n must be a positive integer if n.sign: - raise Error( - ValueError( - message="Root value must be positive.", - function="integer_root()", - ) + raise ValueError( + message="Root value must be positive.", + function="integer_root()", ) if not n.is_integer(): - raise Error( - ValueError( - message="Root value must be an integer.", - function="integer_root()", - ) + raise ValueError( + message="Root value must be an integer.", + function="integer_root()", ) if n.coefficient.is_zero(): - raise Error( - ValueError( - message="Cannot compute zeroth root.", - function="integer_root()", - ) + raise ValueError( + message="Cannot compute zeroth root.", + function="integer_root()", ) # Special case: n = 1 (1st root is just the number itself) @@ -633,11 +614,9 @@ def integer_root( if n_uint.words[0] % 2 == 1: # Odd root result_sign = True else: # n_uint.words[0] % 2 == 0: # Even root - raise Error( - ValueError( - message="Cannot compute even root of a negative number.", - function="integer_root()", - ) + raise ValueError( + message="Cannot compute even root of a negative number.", + function="integer_root()", ) # Extract n as Int for Newton's method @@ -1225,11 +1204,9 @@ def sqrt_exact(x: BigDecimal, precision: Int) raises -> BigDecimal: # Handle special cases if x.sign: - raise Error( - ValueError( - message="Cannot compute square root of a negative number.", - function="sqrt_exact()", - ) + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt_exact()", ) if x.coefficient.is_zero(): @@ -1361,11 +1338,9 @@ def sqrt_reciprocal(x: BigDecimal, precision: Int) raises -> BigDecimal: # Handle special cases if x.sign: - raise Error( - ValueError( - message="Cannot compute square root of a negative number.", - function="sqrt_reciprocal()", - ) + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt_reciprocal()", ) if x.coefficient.is_zero(): @@ -1546,11 +1521,9 @@ def sqrt_newton(x: BigDecimal, precision: Int) raises -> BigDecimal: # Handle special cases if x.sign: - raise Error( - ValueError( - message="Cannot compute square root of a negative number.", - function="sqrt_newton()", - ) + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt_newton()", ) if x.coefficient.is_zero(): @@ -1634,11 +1607,9 @@ def sqrt_decimal_approach(x: BigDecimal, precision: Int) raises -> BigDecimal: # Handle special cases if x.sign: - raise Error( - ValueError( - message="Cannot compute square root of a negative number.", - function="sqrt_decimal_approach()", - ) + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt_decimal_approach()", ) if x.coefficient.is_zero(): @@ -1820,10 +1791,8 @@ def exp(x: BigDecimal, precision: Int) raises -> BigDecimal: # For very large positive values, result will overflow BigDecimal capacity # TODO: Use BigInt10 as scale can avoid overflow in this case if not x.sign and x.adjusted() >= 20: # x > 10^20 - raise Error( - OverflowError( - message="Result too large to represent", function="exp()" - ) + raise OverflowError( + message="Result too large to represent", function="exp()" ) # For very large negative values, result will be effectively zero @@ -2031,17 +2000,13 @@ def ln( # Handle special cases if x.sign: - raise Error( - ValueError( - message="Cannot compute logarithm of negative number", - function="ln()", - ) + raise ValueError( + message="Cannot compute logarithm of negative number", + function="ln()", ) if x.coefficient.is_zero(): - raise Error( - ValueError( - message="Cannot compute logarithm of zero", function="ln()" - ) + raise ValueError( + message="Cannot compute logarithm of zero", function="ln()" ) if x == BigDecimal(BigUInt.one(), 0, False): return BigDecimal(BigUInt.zero(), 0, False) # ln(1) = 0 @@ -2131,36 +2096,26 @@ def log(x: BigDecimal, base: BigDecimal, precision: Int) raises -> BigDecimal: # Special cases if x.sign: - raise Error( - ValueError( - message="Cannot compute logarithm of a negative number", - function="log()", - ) + raise ValueError( + message="Cannot compute logarithm of a negative number", + function="log()", ) if x.coefficient.is_zero(): - raise Error( - ValueError( - message="Cannot compute logarithm of zero", function="log()" - ) + raise ValueError( + message="Cannot compute logarithm of zero", function="log()" ) # Base validation if base.sign: - raise Error( - ValueError(message="Cannot use a negative base", function="log()") - ) + raise ValueError(message="Cannot use a negative base", function="log()") if base.coefficient.is_zero(): - raise Error( - ValueError(message="Cannot use zero as a base", function="log()") - ) + raise ValueError(message="Cannot use zero as a base", function="log()") if ( base.coefficient.number_of_digits() == base.scale + 1 and base.coefficient.words[-1] == 1 ): - raise Error( - ValueError( - message="Cannot use base 1 for logarithm", function="log()" - ) + raise ValueError( + message="Cannot use base 1 for logarithm", function="log()" ) # Special cases @@ -2209,17 +2164,13 @@ def log10(x: BigDecimal, precision: Int) raises -> BigDecimal: # Special cases if x.sign: - raise Error( - ValueError( - message="Cannot compute logarithm of a negative number", - function="log10()", - ) + raise ValueError( + message="Cannot compute logarithm of a negative number", + function="log10()", ) if x.coefficient.is_zero(): - raise Error( - ValueError( - message="Cannot compute logarithm of zero", function="log10()" - ) + raise ValueError( + message="Cannot compute logarithm of zero", function="log10()" ) # Fast path: Powers of 10 are handled directly diff --git a/src/decimo/bigdecimal/trigonometric.mojo b/src/decimo/bigdecimal/trigonometric.mojo index 10f1f79d..eb2166b5 100644 --- a/src/decimo/bigdecimal/trigonometric.mojo +++ b/src/decimo/bigdecimal/trigonometric.mojo @@ -364,8 +364,8 @@ def tan_cot(x: BigDecimal, precision: Int, is_tan: Bool) raises -> BigDecimal: # since tan(0) is defined as 0. # This is a design choice, not a mathematical one. # In practice, cot(0) should raise an error. - raise Error( - ValueError(message="cot(nπ) is undefined", function="tan_cot()") + raise ValueError( + message="cot(nπ) is undefined", function="tan_cot()" ) var pi = decimo.bigdecimal.constants.pi(precision=working_precision_pi) @@ -430,9 +430,7 @@ def csc(x: BigDecimal, precision: Int) raises -> BigDecimal: This function calculates csc(x) = 1 / sin(x). """ if x.is_zero(): - raise Error( - ValueError(message="csc(nπ) is undefined", function="csc()") - ) + raise ValueError(message="csc(nπ) is undefined", function="csc()") comptime BUFFER_DIGITS = 9 var working_precision = precision + BUFFER_DIGITS diff --git a/src/decimo/bigfloat/bigfloat.mojo b/src/decimo/bigfloat/bigfloat.mojo index 76fee07e..d3d468cd 100644 --- a/src/decimo/bigfloat/bigfloat.mojo +++ b/src/decimo/bigfloat/bigfloat.mojo @@ -38,7 +38,7 @@ from std.memory import UnsafePointer from decimo.bigdecimal.bigdecimal import BigDecimal from decimo.biguint.biguint import BigUInt -from decimo.errors import DecimoError, ConversionError +from decimo.errors import ConversionError, RuntimeError from decimo.bigfloat.mpfr_wrapper import ( mpfrw_available, mpfrw_init, @@ -146,23 +146,19 @@ struct BigFloat(Comparable, Movable, Writable): precision: Number of significant decimal digits. """ if not mpfrw_available(): - raise Error( - DecimoError( - message=( - "BigFloat requires MPFR (brew install mpfr / apt" - " install libmpfr-dev)" - ), - function="BigFloat.__init__()", - ) + raise RuntimeError( + message=( + "BigFloat requires MPFR (brew install mpfr / apt" + " install libmpfr-dev)" + ), + function="BigFloat.__init__()", ) var bits = _dps_to_bits(precision) self.handle = mpfrw_init(bits) if self.handle < 0: - raise Error( - DecimoError( - message="MPFR handle pool exhausted", - function="BigFloat.__init__()", - ) + raise RuntimeError( + message="MPFR handle pool exhausted", + function="BigFloat.__init__()", ) self.precision = precision var s_bytes = value.as_bytes() @@ -173,11 +169,9 @@ struct BigFloat(Comparable, Movable, Writable): ) if result_code != 0: mpfrw_clear(self.handle) - raise Error( - ConversionError( - message="Invalid number string: " + value, - function="BigFloat.__init__()", - ) + raise ConversionError( + message="Invalid number string: " + value, + function="BigFloat.__init__()", ) def __init__(out self, value: Int, precision: Int = PRECISION) raises: @@ -245,11 +239,9 @@ struct BigFloat(Comparable, Movable, Writable): var d = digits if digits > 0 else self.precision var address = mpfrw_get_str(self.handle, Int32(d)) if address == 0: - raise Error( - ConversionError( - message="Failed to export string", - function="BigFloat.to_string()", - ) + raise ConversionError( + message="Failed to export string", + function="BigFloat.to_string()", ) var result = _read_c_string(address) mpfrw_free_str(address) @@ -330,11 +322,9 @@ struct BigFloat(Comparable, Movable, Writable): self.handle, Int32(d), UnsafePointer(to=exp) ) if address == 0: - raise Error( - ConversionError( - message="mpfr_get_str failed", - function="BigFloat.to_bigdecimal()", - ) + raise ConversionError( + message="mpfr_get_str failed", + function="BigFloat.to_bigdecimal()", ) # 2. Single memcpy into a Mojo-owned buffer @@ -469,11 +459,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.__neg__()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__neg__()", ) mpfrw_neg(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -486,11 +474,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.__abs__()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__abs__()", ) mpfrw_abs(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -511,11 +497,9 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.__add__()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__add__()", ) mpfrw_add(h, self.handle, other.handle) return Self(_handle=h, _precision=prec) @@ -532,11 +516,9 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.__sub__()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__sub__()", ) mpfrw_sub(h, self.handle, other.handle) return Self(_handle=h, _precision=prec) @@ -553,11 +535,9 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.__mul__()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__mul__()", ) mpfrw_mul(h, self.handle, other.handle) return Self(_handle=h, _precision=prec) @@ -574,11 +554,9 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.__truediv__()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__truediv__()", ) mpfrw_div(h, self.handle, other.handle) return Self(_handle=h, _precision=prec) @@ -606,11 +584,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.sqrt()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.sqrt()", ) mpfrw_sqrt(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -623,11 +599,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.exp()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.exp()", ) mpfrw_exp(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -640,11 +614,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.ln()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.ln()", ) mpfrw_log(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -657,11 +629,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.sin()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.sin()", ) mpfrw_sin(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -674,11 +644,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.cos()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.cos()", ) mpfrw_cos(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -691,11 +659,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.tan()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.tan()", ) mpfrw_tan(h, self.handle) return Self(_handle=h, _precision=self.precision) @@ -712,11 +678,9 @@ struct BigFloat(Comparable, Movable, Writable): var prec = max(self.precision, exponent.precision) var h = mpfrw_init(_dps_to_bits(prec)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.power()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.power()", ) mpfrw_pow(h, self.handle, exponent.handle) return Self(_handle=h, _precision=prec) @@ -732,11 +696,9 @@ struct BigFloat(Comparable, Movable, Writable): """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.root()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.root()", ) mpfrw_rootn_ui(h, self.handle, n) return Self(_handle=h, _precision=self.precision) @@ -752,22 +714,18 @@ struct BigFloat(Comparable, Movable, Writable): A `BigFloat` containing π at the requested precision. """ if not mpfrw_available(): - raise Error( - DecimoError( - message=( - "BigFloat requires MPFR (brew install mpfr / apt" - " install libmpfr-dev)" - ), - function="BigFloat.pi()", - ) + raise RuntimeError( + message=( + "BigFloat requires MPFR (brew install mpfr / apt" + " install libmpfr-dev)" + ), + function="BigFloat.pi()", ) var h = mpfrw_init(_dps_to_bits(precision)) if h < 0: - raise Error( - DecimoError( - message="Handle allocation failed.", - function="BigFloat.pi()", - ) + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.pi()", ) mpfrw_const_pi(h) return BigFloat(_handle=h, _precision=precision) diff --git a/src/decimo/bigint/arithmetics.mojo b/src/decimo/bigint/arithmetics.mojo index 06515c70..6536d4ec 100644 --- a/src/decimo/bigint/arithmetics.mojo +++ b/src/decimo/bigint/arithmetics.mojo @@ -37,7 +37,7 @@ from std.memory import memcpy, memset_zero from decimo.bigint.bigint import BigInt from decimo.bigint.comparison import compare_magnitudes -from decimo.errors import DecimoError, ZeroDivisionError +from decimo.errors import ValueError, ZeroDivisionError # Karatsuba cutoff: operands with this many words or fewer use schoolbook. @@ -715,11 +715,9 @@ def _divmod_magnitudes( divisor_is_zero = False break if divisor_is_zero: - raise Error( - ZeroDivisionError( - function="_divmod_magnitudes()", - message="Division by zero", - ) + raise ZeroDivisionError( + function="_divmod_magnitudes()", + message="Division by zero.", ) # Compare magnitudes to handle trivial cases @@ -1151,11 +1149,9 @@ def _divmod_knuth_d_from_slices( if len_a_eff <= 0: return ([UInt32(0)], [UInt32(0)]) if len_b_eff <= 0: - raise Error( - DecimoError( - function="_divmod_knuth_d_from_slices()", - message="Division by zero in B-Z base case", - ) + raise ZeroDivisionError( + function="_divmod_knuth_d_from_slices()", + message="Division by zero in B-Z base case", ) # Single-word divisor fast path @@ -2337,32 +2333,28 @@ def power(base: BigInt, exponent: Int) raises -> BigInt: Error: If the exponent is too large (>= 1_000_000_000). """ if exponent < 0: - raise Error( - DecimoError( - function="power()", - message=( - "The exponent " - + String(exponent) - + " is negative.\n" - + "Consider using a non-negative exponent." - ), - ) + raise ValueError( + function="power()", + message=( + "The exponent " + + String(exponent) + + " is negative.\n" + + "Consider using a non-negative exponent." + ), ) if exponent == 0: return BigInt(1) if exponent >= 1_000_000_000: - raise Error( - DecimoError( - function="power()", - message=( - "The exponent " - + String(exponent) - + " is too large.\n" - + "Consider using an exponent below 1_000_000_000." - ), - ) + raise ValueError( + function="power()", + message=( + "The exponent " + + String(exponent) + + " is too large.\n" + + "Consider using an exponent below 1_000_000_000." + ), ) if base.is_zero(): diff --git a/src/decimo/bigint/bigint.mojo b/src/decimo/bigint/bigint.mojo index efc4ab7c..d5e050e6 100644 --- a/src/decimo/bigint/bigint.mojo +++ b/src/decimo/bigint/bigint.mojo @@ -39,10 +39,10 @@ import decimo.str from decimo.bigint10.bigint10 import BigInt10 from decimo.biguint.biguint import BigUInt from decimo.errors import ( - DecimoError, ConversionError, OverflowError, ValueError, + ZeroDivisionError, ) # Type aliases @@ -468,31 +468,27 @@ struct BigInt( # For BigInt (integer type), the fractional part must be zero. if scale > 0: if scale >= len(coef): - raise Error( - ConversionError( + raise ConversionError( + function="BigInt.from_string(value: String)", + message=( + 'The input value "' + + value + + '" is not an integer.\n' + + "The scale is larger than the number of digits." + ), + ) + # Check that the fractional digits are all zero + for i in range(1, scale + 1): + if coef[-i] != 0: + raise ConversionError( function="BigInt.from_string(value: String)", message=( 'The input value "' + value + '" is not an integer.\n' - + "The scale is larger than the number of digits." + + "The fractional part is not zero." ), ) - ) - # Check that the fractional digits are all zero - for i in range(1, scale + 1): - if coef[-i] != 0: - raise Error( - ConversionError( - function="BigInt.from_string(value: String)", - message=( - 'The input value "' - + value - + '" is not an integer.\n' - + "The fractional part is not zero." - ), - ) - ) # Remove fractional zeros from coefficient coef.resize(len(coef) - scale, UInt8(0)) @@ -629,11 +625,9 @@ struct BigInt( # Int is 64-bit, so we need at most 2 words to represent it. # Int.MAX = 9_223_372_036_854_775_807 = 0x7FFF_FFFF_FFFF_FFFF if len(self.words) > 2: - raise Error( - OverflowError( - message="The number exceeds the size of Int", - function="BigInt.to_int()", - ) + raise OverflowError( + message="The number exceeds the size of Int", + function="BigInt.to_int()", ) var magnitude: UInt64 = UInt64(self.words[0]) @@ -643,11 +637,9 @@ struct BigInt( if self.sign: # Negative: check against Int.MIN magnitude (2^63) if magnitude > UInt64(9_223_372_036_854_775_808): - raise Error( - OverflowError( - message="The number exceeds the size of Int", - function="BigInt.to_int()", - ) + raise OverflowError( + message="The number exceeds the size of Int", + function="BigInt.to_int()", ) if magnitude == UInt64(9_223_372_036_854_775_808): return Int.MIN @@ -655,11 +647,9 @@ struct BigInt( else: # Positive: check against Int.MAX (2^63 - 1) if magnitude > UInt64(9_223_372_036_854_775_807): - raise Error( - OverflowError( - message="The number exceeds the size of Int", - function="BigInt.to_int()", - ) + raise OverflowError( + message="The number exceeds the size of Int", + function="BigInt.to_int()", ) return Int(magnitude) @@ -984,12 +974,10 @@ struct BigInt( try: return decimo.bigint.arithmetics.floor_divide(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigInt.__floordiv__()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt.__floordiv__()", + previous_error=e^, ) @always_inline @@ -1005,12 +993,10 @@ struct BigInt( try: return decimo.bigint.arithmetics.floor_modulo(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigInt.__mod__()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt.__mod__()", + previous_error=e^, ) @always_inline @@ -1026,12 +1012,10 @@ struct BigInt( try: return decimo.bigint.arithmetics.floor_divmod(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigInt.__divmod__()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt.__divmod__()", + previous_error=e^, ) @always_inline @@ -1468,21 +1452,17 @@ struct BigInt( Error: If the exponent is negative or too large. """ if exponent.is_negative(): - raise Error( - ValueError( - message="Exponent must be non-negative", - function="BigInt.power()", - ) + raise ValueError( + message="Exponent must be non-negative", + function="BigInt.power()", ) var exp_int: Int try: exp_int = exponent.to_int() except e: - raise Error( - OverflowError( - message="Exponent too large to fit in Int", - function="BigInt.power()", - ) + raise OverflowError( + message="Exponent too large to fit in Int", + function="BigInt.power()", ) return self.power(exp_int) diff --git a/src/decimo/bigint/exponential.mojo b/src/decimo/bigint/exponential.mojo index ab50c511..7c04cd30 100644 --- a/src/decimo/bigint/exponential.mojo +++ b/src/decimo/bigint/exponential.mojo @@ -25,7 +25,7 @@ from std import math from decimo.bigint.bigint import BigInt import decimo.bigint.arithmetics -from decimo.errors import DecimoError +from decimo.errors import ValueError # ===----------------------------------------------------------------------=== # @@ -281,11 +281,9 @@ def sqrt(x: BigInt) raises -> BigInt: native 64-bit arithmetic (no heap allocation, O(1) per iteration). """ if x.is_negative(): - raise Error( - DecimoError( - function="sqrt()", - message="Cannot compute square root of a negative number", - ) + raise ValueError( + function="sqrt()", + message="Cannot compute square root of a negative number", ) if x.is_zero(): diff --git a/src/decimo/bigint/number_theory.mojo b/src/decimo/bigint/number_theory.mojo index a2115677..2c8c184d 100644 --- a/src/decimo/bigint/number_theory.mojo +++ b/src/decimo/bigint/number_theory.mojo @@ -34,7 +34,7 @@ from decimo.bigint.arithmetics import ( subtract_inplace, right_shift_inplace, ) -from decimo.errors import DecimoError +from decimo.errors import ValueError # ===----------------------------------------------------------------------=== # @@ -245,19 +245,15 @@ def mod_pow(base: BigInt, exponent: BigInt, modulus: BigInt) raises -> BigInt: If exponent < 0 or modulus <= 0. """ if exponent.is_negative(): - raise Error( - DecimoError( - function="mod_pow()", - message="Exponent must be non-negative", - ) + raise ValueError( + function="mod_pow()", + message="Exponent must be non-negative", ) if not modulus.is_positive(): - raise Error( - DecimoError( - function="mod_pow()", - message="Modulus must be positive", - ) + raise ValueError( + function="mod_pow()", + message="Modulus must be positive", ) # x mod 1 = 0 for all x @@ -326,11 +322,9 @@ def mod_inverse(a: BigInt, modulus: BigInt) raises -> BigInt: If modulus <= 0 or the inverse does not exist (gcd(a, modulus) != 1). """ if not modulus.is_positive(): - raise Error( - DecimoError( - function="mod_inverse()", - message="Modulus must be positive", - ) + raise ValueError( + function="mod_inverse()", + message="Modulus must be positive", ) var result = extended_gcd(a, modulus) @@ -338,11 +332,9 @@ def mod_inverse(a: BigInt, modulus: BigInt) raises -> BigInt: var x = result[1].copy() if not g.is_one(): - raise Error( - DecimoError( - function="mod_inverse()", - message="Modular inverse does not exist (gcd != 1)", - ) + raise ValueError( + function="mod_inverse()", + message="Modular inverse does not exist (gcd != 1)", ) # Ensure result is in [0, modulus) diff --git a/src/decimo/bigint10/arithmetics.mojo b/src/decimo/bigint10/arithmetics.mojo index 0446a292..ed69548b 100644 --- a/src/decimo/bigint10/arithmetics.mojo +++ b/src/decimo/bigint10/arithmetics.mojo @@ -20,7 +20,7 @@ Implements basic arithmetic functions for the BigInt10 type. from decimo.bigint10.bigint10 import BigInt10 from decimo.biguint.biguint import BigUInt -from decimo.errors import DecimoError +from decimo.errors import ZeroDivisionError from decimo.rounding_mode import RoundingMode @@ -225,12 +225,10 @@ def floor_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="floor_divide()", - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="floor_divide()", + previous_error=e^, ) return BigInt10(magnitude^, sign=False) @@ -241,12 +239,10 @@ def floor_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="floor_divide()", - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="floor_divide()", + previous_error=e^, ) return BigInt10(magnitude^, sign=True) @@ -272,12 +268,10 @@ def truncate_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="truncate_divide()", - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="truncate_divide()", + previous_error=e^, ) return BigInt10(magnitude^, sign=x1.sign != x2.sign) @@ -308,12 +302,10 @@ def floor_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="floor_modulo()", - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="floor_modulo()", + previous_error=e^, ) return BigInt10(magnitude^, sign=x2.sign) @@ -324,12 +316,10 @@ def floor_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="floor_modulo()", - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="floor_modulo()", + previous_error=e^, ) return BigInt10(magnitude^, sign=x2.sign) @@ -355,11 +345,9 @@ def truncate_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="truncate_modulo()", - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="truncate_modulo()", + previous_error=e^, ) return BigInt10(magnitude^, sign=x1.sign) diff --git a/src/decimo/bigint10/bigint10.mojo b/src/decimo/bigint10/bigint10.mojo index c8dc3e2a..89d45572 100644 --- a/src/decimo/bigint10/bigint10.mojo +++ b/src/decimo/bigint10/bigint10.mojo @@ -31,10 +31,10 @@ import decimo.bigint10.comparison from decimo.bigdecimal.bigdecimal import BigDecimal from decimo.biguint.biguint import BigUInt from decimo.errors import ( - DecimoError, ValueError, OverflowError, ConversionError, + ZeroDivisionError, ) import decimo.str @@ -117,14 +117,12 @@ struct BigInt10( try: self = Self.from_list(words^, sign=sign) except e: - raise Error( - DecimoError( - message="See the above exception.", - function=( - "BigInt10.__init__(var words: List[UInt32], sign: Bool)" - ), - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function=( + "BigInt10.__init__(var words: List[UInt32], sign: Bool)" + ), + previous_error=e^, ) def __init__(out self, *, var raw_words: List[UInt32], sign: Bool): @@ -159,12 +157,10 @@ struct BigInt10( try: self = Self.from_string(value) except e: - raise Error( - ConversionError( - message="Cannot initialize BigInt10 from String.", - function="BigInt10.__init__()", - previous_error=e^, - ) + raise ConversionError( + message="Cannot initialize BigInt10 from String.", + function="BigInt10.__init__()", + previous_error=e^, ) # TODO: If Mojo makes Int type an alias of SIMD[DType.index, 1], @@ -230,15 +226,12 @@ struct BigInt10( try: return Self(BigUInt.from_list(words^), sign) except e: - raise Error( - DecimoError( - message="See the above exception.", - function=( - "BigInt10.from_list(var words: List[UInt32], sign:" - " Bool)" - ), - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function=( + "BigInt10.from_list(var words: List[UInt32], sign: Bool)" + ), + previous_error=e^, ) @staticmethod @@ -264,13 +257,9 @@ struct BigInt10( # Check if the words are valid for word in words: if word > UInt32(999_999_999): - raise Error( - ValueError( - message=( - "Word value exceeds maximum value of 999_999_999" - ), - function="BigInt10.__init__()", - ) + raise ValueError( + message="Word value exceeds maximum value of 999_999_999", + function="BigInt10.__init__()", ) else: list_of_words.append(word) @@ -408,12 +397,10 @@ struct BigInt10( # Use the existing from_string() method to parse the string return Self.from_string(py_str) except e: - raise Error( - ConversionError( - message="Failed to convert Python int to BigInt10.", - function="BigInt10.from_python_int()", - previous_error=e^, - ) + raise ConversionError( + message="Failed to convert Python int to BigInt10.", + function="BigInt10.from_python_int()", + previous_error=e^, ) # ===------------------------------------------------------------------=== # @@ -470,11 +457,9 @@ struct BigInt10( # is larger than 10^18 -1 but smaller than 10^27 - 1 if len(self.magnitude.words) > 3: - raise Error( - OverflowError( - message="The number exceeds the size of Int.", - function="BigInt10.to_int()", - ) + raise OverflowError( + message="The number exceeds the size of Int.", + function="BigInt10.to_int()", ) var value: Int128 = 0 @@ -491,11 +476,9 @@ struct BigInt10( var int_min = Int.MIN var int_max = Int.MAX if value < Int128(int_min) or value > Int128(int_max): - raise Error( - OverflowError( - message="The number exceeds the size of Int.", - function="BigInt10.to_int()", - ) + raise OverflowError( + message="The number exceeds the size of Int.", + function="BigInt10.to_int()", ) return Int(value) @@ -636,12 +619,10 @@ struct BigInt10( try: return decimo.bigint10.arithmetics.floor_divide(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigInt10.__floordiv__()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt10.__floordiv__()", + previous_error=e^, ) @always_inline @@ -657,12 +638,10 @@ struct BigInt10( try: return decimo.bigint10.arithmetics.floor_modulo(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigInt10.__mod__()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt10.__mod__()", + previous_error=e^, ) @always_inline @@ -1071,11 +1050,9 @@ struct BigInt10( The result of raising to the given power. """ if exponent > Self(BigUInt(raw_words=[0, 1]), sign=False): - raise Error( - OverflowError( - message="The exponent is too large.", - function="BigInt10.power()", - ) + raise OverflowError( + message="The exponent is too large.", + function="BigInt10.power()", ) var exponent_as_int = exponent.to_int() return self.power(exponent_as_int) diff --git a/src/decimo/biguint/arithmetics.mojo b/src/decimo/biguint/arithmetics.mojo index 715c7f94..9005502a 100644 --- a/src/decimo/biguint/arithmetics.mojo +++ b/src/decimo/biguint/arithmetics.mojo @@ -25,7 +25,6 @@ from std.memory import memcpy, memset_zero from decimo.biguint.biguint import BigUInt import decimo.biguint.comparison from decimo.errors import ( - DecimoError, OverflowError, ValueError, ZeroDivisionError, @@ -108,11 +107,9 @@ def negative(x: BigUInt) raises -> BigUInt: debug_assert[assert_mode="none"]( len(x.words) == 1, "negative(): leading zero words" ) - raise Error( - OverflowError( - function="negative()", - message="Negative of non-zero unsigned integer is undefined", - ) + raise OverflowError( + function="negative()", + message="Negative of non-zero unsigned integer is undefined", ) return BigUInt.zero() # Return zero @@ -538,14 +535,12 @@ def subtract_school(x: BigUInt, y: BigUInt) raises -> BigUInt: # |x| = |y| return BigUInt.zero() # Return zero if comparison_result < 0: - raise Error( - OverflowError( - function="subtract_school()", - message=( - "biguint.arithmetics.subtract(): Result is negative due to" - " x < y" - ), - ) + raise OverflowError( + function="subtract_school()", + message=( + "biguint.arithmetics.subtract(): Result is negative due to" + " x < y" + ), ) # Now it is safe to subtract the smaller number from the larger one @@ -632,14 +627,12 @@ def subtract_simd(x: BigUInt, y: BigUInt) raises -> BigUInt: # |x| = |y| return BigUInt.zero() # Return zero if comparison_result < 0: - raise Error( - OverflowError( - function="subtract()", - message=( - "biguint.arithmetics.subtract(): Result is negative due to" - " x < y" - ), - ) + raise OverflowError( + function="subtract()", + message=( + "biguint.arithmetics.subtract(): Result is negative due to" + " x < y" + ), ) # Now it is safe to subtract the smaller number from the larger one @@ -703,14 +696,12 @@ def subtract_inplace(mut x: BigUInt, y: BigUInt) raises -> None: x.words.resize(unsafe_uninit_length=1) x.words[0] = UInt32(0) # Result is zero elif comparison_result < 0: - raise Error( - OverflowError( - function="subtract_inplace()", - message=( - "biguint.arithmetics.subtract(): Result is negative due to" - " x < y" - ), - ) + raise OverflowError( + function="subtract_inplace()", + message=( + "biguint.arithmetics.subtract(): Result is negative due to" + " x < y" + ), ) # Now it is safe to subtract the smaller number from the larger one @@ -1977,11 +1968,9 @@ def floor_divide(x: BigUInt, y: BigUInt) raises -> BigUInt: # CASE: y is zero if y.is_zero(): - raise Error( - ZeroDivisionError( - function="floor_divide()", - message="Division by zero", - ) + raise ZeroDivisionError( + function="floor_divide()", + message="Division by zero", ) # CASE: Dividend is zero @@ -2078,10 +2067,8 @@ def floor_divide_school(x: BigUInt, y: BigUInt) raises -> BigUInt: # handled properly to improve performance. # CASE: y is zero if y.is_zero(): - raise Error( - ZeroDivisionError( - message="Division by zero", function="floor_divide()" - ) + raise ZeroDivisionError( + message="Division by zero", function="floor_divide()" ) # CASE: Dividend is zero @@ -3142,11 +3129,9 @@ def floor_divide_three_by_two_uint32( b = b1 * BASE + b0. """ if b1 < 500_000_000: - raise Error( - ValueError( - message="b1 must be at least 500_000_000", - function="floor_divide_three_by_two_uint32()", - ) + raise ValueError( + message="b1 must be at least 500_000_000", + function="floor_divide_three_by_two_uint32()", ) var a2a1 = UInt64(a2) * 1_000_000_000 + UInt64(a1) @@ -3198,42 +3183,32 @@ def floor_divide_four_by_two_uint32( """ if b1 < 500_000_000: - raise Error( - ValueError( - message="b1 must be at least 500_000_000", - function="floor_divide_four_by_two_uint32()", - ) + raise ValueError( + message="b1 must be at least 500_000_000", + function="floor_divide_four_by_two_uint32()", ) if a3 > b1: - raise Error( - ValueError( - message="a must be less than b * 10^18", - function="floor_divide_four_by_two_uint32()", - ) + raise ValueError( + message="a must be less than b * 10^18", + function="floor_divide_four_by_two_uint32()", ) elif a3 == b1: if a2 > b0: - raise Error( - ValueError( - message="a must be less than b * 10^18", - function="floor_divide_four_by_two_uint32()", - ) + raise ValueError( + message="a must be less than b * 10^18", + function="floor_divide_four_by_two_uint32()", ) elif a2 == b0: if a1 > 0: - raise Error( - ValueError( - message="a must be less than b * 10^18", - function="floor_divide_four_by_two_uint32()", - ) + raise ValueError( + message="a must be less than b * 10^18", + function="floor_divide_four_by_two_uint32()", ) elif a1 == 0: if a0 >= 0: - raise Error( - ValueError( - message="a must be less than b * 10^18", - function="floor_divide_four_by_two_uint32()", - ) + raise ValueError( + message="a must be less than b * 10^18", + function="floor_divide_four_by_two_uint32()", ) var q1, r1, r0 = floor_divide_three_by_two_uint32(a3, a2, a1, b1, b0) @@ -3277,10 +3252,8 @@ def ceil_divide(x1: BigUInt, x2: BigUInt) raises -> BigUInt: len(x2.words) == 1, "ceil_divide(): leading zero words", ) - raise Error( - ZeroDivisionError( - message="Division by zero", function="ceil_divide()" - ) + raise ZeroDivisionError( + message="Division by zero", function="ceil_divide()" ) # Apply floor division and check if there is a remainder @@ -3316,11 +3289,9 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: len(x2.words) == 1, "truncate_modulo(): leading zero words", ) - raise Error( - ZeroDivisionError( - function="floor_modulo()", - message="Division by zero", - ) + raise ZeroDivisionError( + function="floor_modulo()", + message="Division by zero", ) # CASE: Dividend is zero @@ -3343,12 +3314,10 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: try: quotient = floor_divide(x1, x2) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="floor_modulo()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="floor_modulo()", + previous_error=e^, ) # Calculate remainder: dividend - (divisor * quotient) @@ -3356,12 +3325,10 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: try: remainder = subtract(x1, multiply(x2, quotient)) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="floor_modulo()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="floor_modulo()", + previous_error=e^, ) return remainder^ @@ -3381,18 +3348,12 @@ def truncate_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: The remainder of x1 being divided by x2. Raises: - Error: If `floor_modulo()` raises an OverflowError. + ZeroDivisionError: If the divisor is zero. """ try: return floor_modulo(x1, x2) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="truncate_modulo()", - previous_error=e^, - ) - ) + raise e^ def ceil_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: @@ -3408,17 +3369,15 @@ def ceil_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: The remainder of x1 being ceil-divided by x2. Raises: - ValueError: If the divisor is zero. + ZeroDivisionError: If the divisor is zero. """ # CASE: Division by zero if x2.is_zero(): debug_assert[assert_mode="none"]( len(x2.words) == 1, "ceil_modulo(): leading zero words" ) - raise Error( - ZeroDivisionError( - message="Division by zero", function="ceil_modulo()" - ) + raise ZeroDivisionError( + message="Division by zero", function="ceil_modulo()" ) # CASE: Dividend is zero @@ -3475,12 +3434,10 @@ def floor_divide_modulo( var remainder = subtract(x1, multiply(x2, quotient)) return (quotient^, remainder^) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="floor_divide_modulo()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="floor_divide_modulo()", + previous_error=e^, ) @@ -3661,11 +3618,9 @@ def power_of_10(n: Int) raises -> BigUInt: DecimoError: If n is negative. """ if n < 0: - raise Error( - DecimoError( - function="power_of_10()", - message="Negative exponent not supported", - ) + raise ValueError( + function="power_of_10()", + message="Negative exponent not supported", ) if n == 0: diff --git a/src/decimo/biguint/biguint.mojo b/src/decimo/biguint/biguint.mojo index 195fcd48..d74c3ee1 100644 --- a/src/decimo/biguint/biguint.mojo +++ b/src/decimo/biguint/biguint.mojo @@ -29,11 +29,11 @@ from decimo.bigint10.bigint10 import BigInt10 import decimo.biguint.arithmetics import decimo.biguint.comparison from decimo.errors import ( - DecimoError, ConversionError, ValueError, IndexError, OverflowError, + ZeroDivisionError, ) import decimo.str @@ -203,12 +203,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: self = Self.from_list(words^) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__init__(var words: List[UInt32])", - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function="BigUInt.__init__(var words: List[UInt32])", + previous_error=e^, ) def __init__(out self, *, var raw_words: List[UInt32]): @@ -249,12 +247,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: self = Self.from_int(value) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__init__(value: Int)", - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function="BigUInt.__init__(value: Int)", + previous_error=e^, ) @implicit @@ -285,12 +281,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: self = Self.from_string(value, ignore_sign=ignore_sign) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__init__(value: String)", - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function="BigUInt.__init__(value: String)", + previous_error=e^, ) # ===------------------------------------------------------------------=== # @@ -328,15 +322,13 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): # Check if the words are valid for word in words: if word > UInt32(999_999_999): - raise Error( - OverflowError( - message=( - "Word value " - + String(word) - + " exceeds maximum value of 999_999_999" - ), - function="BigUInt.from_list()", - ) + raise OverflowError( + message=( + "Word value " + + String(word) + + " exceeds maximum value of 999_999_999" + ), + function="BigUInt.from_list()", ) var res = Self(raw_words=words^) @@ -394,15 +386,13 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): # Check if the words are valid for word in words: if word > UInt32(999_999_999): - raise Error( - OverflowError( - message=( - "Word value " - + String(word) - + " exceeds maximum value of 999_999_999" - ), - function="BigUInt.from_words()", - ) + raise OverflowError( + message=( + "Word value " + + String(word) + + " exceeds maximum value of 999_999_999" + ), + function="BigUInt.from_words()", ) else: list_of_words.append(word) @@ -468,15 +458,13 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): return Self() if value < 0: - raise Error( - OverflowError( - function="BigUInt.from_int(value: Int)", - message=( - "The input value " - + String(value) - + " is negative and is not compatible with BigUInt." - ), - ) + raise OverflowError( + function="BigUInt.from_int(value: Int)", + message=( + "The input value " + + String(value) + + " is negative and is not compatible with BigUInt." + ), ) var list_of_words = List[UInt32]() @@ -668,17 +656,14 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): var sign: Bool = _tuple[2] if (not ignore_sign) and sign: - raise Error( - OverflowError( - function="BigUInt.from_string(value: String)", - message=( - 'The input value "' - + value - + '" is negative but `ignore_sign` is False.\n' - + "Consider using `ignore_sign=True` to ignore the" - " sign." - ), - ) + raise OverflowError( + function="BigUInt.from_string(value: String)", + message=( + 'The input value "' + + value + + '" is negative but `ignore_sign` is False.\n' + + "Consider using `ignore_sign=True` to ignore the sign." + ), ) # Check if the number is zero @@ -690,30 +675,26 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): # If the fractional part is zero, remove the fractional part if scale > 0: if scale >= len(coef): - raise Error( - ConversionError( + raise ConversionError( + function="BigUInt.from_string(value: String)", + message=( + 'The input value "' + + value + + '" is not an integer.\n' + + "The scale is larger than the number of digits." + ), + ) + for i in range(1, scale + 1): + if coef[-i] != 0: + raise ConversionError( function="BigUInt.from_string(value: String)", message=( 'The input value "' + value + '" is not an integer.\n' - + "The scale is larger than the number of digits." + + "The fractional part is not zero." ), ) - ) - for i in range(1, scale + 1): - if coef[-i] != 0: - raise Error( - ConversionError( - function="BigUInt.from_string(value: String)", - message=( - 'The input value "' - + value - + '" is not an integer.\n' - + "The fractional part is not zero." - ), - ) - ) coef.resize(len(coef) - scale, UInt8(0)) scale = 0 @@ -789,12 +770,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: return self.to_int() except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__int__()", - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function="BigUInt.__int__()", + previous_error=e^, ) def write_repr_to[W: Writer](self, mut writer: W): @@ -837,23 +816,24 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): # 2^63-1 = 9_223_372_036_854_775_807 # is larger than 10^18 -1 but smaller than 10^27 - 1 - var overflow_error: Error = Error( - OverflowError( - function="BigUInt.to_int()", - message="The number exceeds the size of Int (" - + String(Int.MAX) - + ")", - ) + var overflow_msg = ( + "The number exceeds the size of Int (" + String(Int.MAX) + ")" ) if len(self.words) > 3: - raise overflow_error^ + raise OverflowError( + function="BigUInt.to_int()", + message=overflow_msg, + ) var value: Int128 = 0 for i in range(len(self.words)): value += Int128(self.words[i]) * Int128(1_000_000_000) ** i if value > Int128(Int.MAX): - raise overflow_error^ + raise OverflowError( + function="BigUInt.to_int()", + message=overflow_msg, + ) return Int(value) @@ -867,15 +847,13 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Error: If the number exceeds the size of UInt64. """ if self.is_uint64_overflow(): - raise Error( - OverflowError( - function="BigUInt.to_uint64()", - message=( - "The number exceeds the size of UInt64 (" - + String(UInt64.MAX) - + ")" - ), - ) + raise OverflowError( + function="BigUInt.to_uint64()", + message=( + "The number exceeds the size of UInt64 (" + + String(UInt64.MAX) + + ")" + ), ) if len(self.words) == 1: @@ -1152,13 +1130,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: return decimo.biguint.arithmetics.subtract(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__sub__(other: Self)", - previous_error=e^, - ) - ) + raise e^ @always_inline def __mul__(self, other: Self) -> Self: @@ -1185,12 +1157,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: return decimo.biguint.arithmetics.floor_divide(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__floordiv__(other: Self)", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigUInt.__floordiv__(other: Self)", + previous_error=e^, ) @always_inline @@ -1206,12 +1176,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: return decimo.biguint.arithmetics.ceil_divide(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__ceildiv__(other: Self)", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigUInt.__ceildiv__(other: Self)", + previous_error=e^, ) @always_inline @@ -1227,12 +1195,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: return decimo.biguint.arithmetics.floor_modulo(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__mod__(other: Self)", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigUInt.__mod__(other: Self)", + previous_error=e^, ) @always_inline @@ -1248,13 +1214,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: return decimo.biguint.arithmetics.floor_divide_modulo(self, other) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__divmod__(other: Self)", - previous_error=e^, - ) - ) + raise e^ @always_inline def __pow__(self, exponent: Self) raises -> Self: @@ -1269,12 +1229,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: return self.power(exponent) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__pow__(exponent: Self)", - previous_error=e^, - ) + raise ValueError( + message="See the above exception.", + function="BigUInt.__pow__(exponent: Self)", + previous_error=e^, ) @always_inline @@ -1290,12 +1248,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: return self.power(exponent) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="BigUInt.__pow__(exponent: Int)", - previous_error=e^, - ) + raise ValueError( + message="See the above exception.", + function="BigUInt.__pow__(exponent: Int)", + previous_error=e^, ) # ===------------------------------------------------------------------=== # @@ -1721,32 +1677,28 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Error: If the exponent is too large, e.g., larger than 1_000_000_000. """ if exponent < 0: - raise Error( - ValueError( - function="BigUInt.power(exponent: Int)", - message=( - "The exponent " - + String(exponent) - + " is negative.\n" - + "Consider using a non-negative exponent." - ), - ) + raise ValueError( + function="BigUInt.power(exponent: Int)", + message=( + "The exponent " + + String(exponent) + + " is negative.\n" + + "Consider using a non-negative exponent." + ), ) if exponent == 0: return Self(raw_words=[1]) if exponent >= 1_000_000_000: - raise Error( - ValueError( - function="BigUInt.power(exponent: Int)", - message=( - "The exponent " - + String(exponent) - + " is too large.\n" - + "Consider using an exponent below 1_000_000_000." - ), - ) + raise ValueError( + function="BigUInt.power(exponent: Int)", + message=( + "The exponent " + + String(exponent) + + " is too large.\n" + + "Consider using an exponent below 1_000_000_000." + ), ) var result = Self(raw_words=[1]) @@ -1773,16 +1725,14 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): The result of raising this number to the power of `exponent`. """ if len(exponent.words) > 1: - raise Error( - ValueError( - function="BigUInt.power(exponent: BigUInt)", - message=( - "The exponent " - + String(exponent) - + " is too large.\n" - + "Consider using an exponent below 1_000_000_000." - ), - ) + raise ValueError( + function="BigUInt.power(exponent: BigUInt)", + message=( + "The exponent " + + String(exponent) + + " is too large.\n" + + "Consider using an exponent below 1_000_000_000." + ), ) var exponent_as_int = exponent.to_int() return self.power(exponent_as_int) @@ -2062,16 +2012,14 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): IndexError: If the index is negative. """ if i < 0: - raise Error( - IndexError( - function="BigUInt.ith_digit(i: Int)", - message=( - "The index " - + String(i) - + " is negative.\n" - + "Consider using a non-negative index." - ), - ) + raise IndexError( + function="BigUInt.ith_digit(i: Int)", + message=( + "The index " + + String(i) + + " is negative.\n" + + "Consider using a non-negative index." + ), ) if i >= len(self.words) * 9: return 0 @@ -2202,29 +2150,25 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): `remove_extra_digit_due_to_rounding` is True, the result will be 10. """ if ndigits < 0: - raise Error( - ValueError( - function="BigUInt.remove_trailing_digits_with_rounding()", - message=( - "The number of digits to remove is negative: " - + String(ndigits) - ), - ) + raise ValueError( + function="BigUInt.remove_trailing_digits_with_rounding()", + message=( + "The number of digits to remove is negative: " + + String(ndigits) + ), ) if ndigits == 0: return self.copy() if ndigits > self.number_of_digits(): - raise Error( - ValueError( - function="BigUInt.remove_trailing_digits_with_rounding()", - message=( - "The number of digits to remove is larger than the " - "number of digits in the BigUInt: " - + String(ndigits) - + " > " - + String(self.number_of_digits()) - ), - ) + raise ValueError( + function="BigUInt.remove_trailing_digits_with_rounding()", + message=( + "The number of digits to remove is larger than the " + "number of digits in the BigUInt: " + + String(ndigits) + + " > " + + String(self.number_of_digits()) + ), ) # floor_divide_by_power_of_ten is the same as removing the last n digits @@ -2276,11 +2220,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): # TODO: Remove this fallback once Mojo has proper enum support, # which will make exhaustive matching a compile-time guarantee. else: - raise Error( - ValueError( - function="BigUInt.remove_trailing_digits_with_rounding()", - message=("Unknown rounding mode: " + String(rounding_mode)), - ) + raise ValueError( + function="BigUInt.remove_trailing_digits_with_rounding()", + message=("Unknown rounding mode: " + String(rounding_mode)), ) if round_up: diff --git a/src/decimo/decimal128/arithmetics.mojo b/src/decimo/decimal128/arithmetics.mojo index 9c0cabaa..de3e2fb3 100644 --- a/src/decimo/decimal128/arithmetics.mojo +++ b/src/decimo/decimal128/arithmetics.mojo @@ -37,7 +37,6 @@ from std import testing from decimo.decimal128.decimal128 import Decimal128 from decimo.rounding_mode import RoundingMode from decimo.errors import ( - DecimoError, OverflowError, ZeroDivisionError, ) @@ -129,11 +128,9 @@ def add(x1: Decimal128, x2: Decimal128) raises -> Decimal128: # Check for overflow (UInt128 can store values beyond our 96-bit limit) # We need to make sure the sum fits in 96 bits (our Decimal128 capacity) if summation > Decimal128.MAX_AS_UINT128: # 2^96-1 - raise Error( - OverflowError( - message="Decimal128 overflow in addition.", - function="add()", - ) + raise OverflowError( + message="Decimal128 overflow in addition.", + function="add()", ) return Decimal128.from_uint128(summation, 0, x1.is_negative()) @@ -164,11 +161,9 @@ def add(x1: Decimal128, x2: Decimal128) raises -> Decimal128: # Check for overflow (UInt128 can store values beyond our 96-bit limit) # We need to make sure the sum fits in 96 bits (our Decimal128 capacity) if summation > Decimal128.MAX_AS_UINT128: # 2^96-1 - raise Error( - OverflowError( - message="Decimal128 overflow in addition.", - function="add()", - ) + raise OverflowError( + message="Decimal128 overflow in addition.", + function="add()", ) # Determine the scale for the result @@ -389,12 +384,10 @@ def subtract(x1: Decimal128, x2: Decimal128) raises -> Decimal128: try: return x1 + (-x2) except e: - raise Error( - DecimoError( - message="Subtraction failed.", - function="subtract()", - previous_error=e^, - ) + raise OverflowError( + message="Subtraction failed.", + function="subtract()", + previous_error=e^, ) @@ -565,13 +558,11 @@ def multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: elif combined_num_bits <= 128: var prod: UInt128 = UInt128(x1_coef) * UInt128(x2_coef) if prod > Decimal128.MAX_AS_UINT128: - raise Error( - OverflowError( - message=String( - "The product {} exceeds Decimal128 capacity." - ).format(prod), - function="multiply()", - ) + raise OverflowError( + message=String( + "The product {} exceeds Decimal128 capacity." + ).format(prod), + function="multiply()", ) else: return Decimal128.from_uint128(prod, 0, is_negative) @@ -579,13 +570,11 @@ def multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: # Large integers, it will definitely overflow else: var prod: UInt256 = UInt256(x1_coef) * UInt256(x2_coef) - raise Error( - OverflowError( - message=String( - "The product {} exceeds Decimal128 capacity." - ).format(prod), - function="multiply()", - ) + raise OverflowError( + message=String( + "The product {} exceeds Decimal128 capacity." + ).format(prod), + function="multiply()", ) # SPECIAL CASE: Both operands are integers but with scales @@ -601,11 +590,9 @@ def multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: x2_integral_part ) if prod > Decimal128.MAX_AS_UINT256: - raise Error( - OverflowError( - message="Decimal128 overflow in multiplication.", - function="multiply()", - ) + raise OverflowError( + message="Decimal128 overflow in multiplication.", + function="multiply()", ) else: var num_digits = decimo.decimal128.utility.number_of_digits(prod) @@ -698,11 +685,9 @@ def multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if (num_digits_of_integral_part >= Decimal128.MAX_NUM_DIGITS) & ( truncated_prod_at_max_length > Decimal128.MAX_AS_UINT128 ): - raise Error( - OverflowError( - message="Decimal128 overflow in multiplication.", - function="multiply()", - ) + raise OverflowError( + message="Decimal128 overflow in multiplication.", + function="multiply()", ) # Otherwise, the value will not overflow even after rounding @@ -764,11 +749,9 @@ def multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if (num_digits_of_integral_part >= Decimal128.MAX_NUM_DIGITS) & ( truncated_prod_at_max_length > Decimal128.MAX_AS_UINT256 ): - raise Error( - OverflowError( - message="Decimal128 overflow in multiplication.", - function="multiply()", - ) + raise OverflowError( + message="Decimal128 overflow in multiplication.", + function="multiply()", ) # Otherwise, the value will not overflow even after rounding @@ -831,11 +814,9 @@ def divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: # 特例: 除數爲零 # Check for division by zero if x2.is_zero(): - raise Error( - ZeroDivisionError( - message="Division by zero.", - function="divide()", - ) + raise ZeroDivisionError( + message="Division by zero.", + function="divide()", ) # SPECIAL CASE: zero dividend @@ -893,11 +874,9 @@ def divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: else: var quot = UInt256(x1_coef) * UInt256(10) ** (-diff_scale) if quot > Decimal128.MAX_AS_UINT256: - raise Error( - OverflowError( - message="Decimal128 overflow in division.", - function="divide()", - ) + raise OverflowError( + message="Decimal128 overflow in division.", + function="divide()", ) else: var low = UInt32(quot & 0xFFFFFFFF) @@ -963,11 +942,9 @@ def divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: else: var quot = UInt256(quot) * UInt256(10) ** (-diff_scale) if quot > Decimal128.MAX_AS_UINT256: - raise Error( - OverflowError( - message="Decimal128 overflow in division.", - function="divide()", - ) + raise OverflowError( + message="Decimal128 overflow in division.", + function="divide()", ) else: var low = UInt32(quot & 0xFFFFFFFF) @@ -1245,11 +1222,9 @@ def divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: (ndigits_quot_int_part == Decimal128.MAX_NUM_DIGITS) and (truncated_quot > Decimal128.MAX_AS_UINT256) ): - raise Error( - OverflowError( - message="Decimal128 overflow in division.", - function="divide()", - ) + raise OverflowError( + message="Decimal128 overflow in division.", + function="divide()", ) var scale_of_truncated_quot = ( @@ -1301,13 +1276,7 @@ def truncate_divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: try: return divide(x1, x2).round(0, RoundingMode.down()) except e: - raise Error( - DecimoError( - message="Division failed.", - function="truncate_divide()", - previous_error=e^, - ) - ) + raise e^ def modulo(x1: Decimal128, x2: Decimal128) raises -> Decimal128: @@ -1324,10 +1293,4 @@ def modulo(x1: Decimal128, x2: Decimal128) raises -> Decimal128: try: return x1 - (truncate_divide(x1, x2) * x2) except e: - raise Error( - DecimoError( - message="Modulo failed.", - function="modulo()", - previous_error=e^, - ) - ) + raise e^ diff --git a/src/decimo/decimal128/constants.mojo b/src/decimo/decimal128/constants.mojo index c634d94e..a8e2cbca 100644 --- a/src/decimo/decimal128/constants.mojo +++ b/src/decimo/decimal128/constants.mojo @@ -462,11 +462,9 @@ def N_DIVIDE_NEXT(n: Int) raises -> Decimal128: # 20/21 = 0.95238095238095238095238095238095... return Decimal128(0x33CF3CF4, 0xCD78948D, 0x1EC5E91C, 0x1C0000) else: - raise Error( - ValueError( - message="n must be between 1 and 20, inclusive.", - function="N_DIVIDE_NEXT()", - ) + raise ValueError( + message="n must be between 1 and 20, inclusive.", + function="N_DIVIDE_NEXT()", ) diff --git a/src/decimo/decimal128/decimal128.mojo b/src/decimo/decimal128/decimal128.mojo index 168d907d..5154693f 100644 --- a/src/decimo/decimal128/decimal128.mojo +++ b/src/decimo/decimal128/decimal128.mojo @@ -33,7 +33,6 @@ import decimo.decimal128.exponential import decimo.decimal128.rounding from decimo.rounding_mode import RoundingMode from decimo.errors import ( - DecimoError, ValueError, OverflowError, ConversionError, @@ -297,12 +296,10 @@ struct Decimal128( try: self = Decimal128.from_components(low, mid, high, scale, sign) except e: - raise Error( - DecimoError( - message="Cannot initialize with five components.", - function="Decimal128.__init__()", - previous_error=e^, - ) + raise ConversionError( + message="Cannot initialize with five components.", + function="Decimal128.__init__()", + previous_error=e^, ) def __init__(out self, value: Int): @@ -325,12 +322,10 @@ struct Decimal128( try: self = Decimal128.from_int(value, scale) except e: - raise Error( - ConversionError( - message="Cannot initialize Decimal128 from Int.", - function="Decimal128.__init__()", - previous_error=e^, - ) + raise ConversionError( + message="Cannot initialize Decimal128 from Int.", + function="Decimal128.__init__()", + previous_error=e^, ) def __init__(out self, value: String) raises: @@ -343,12 +338,10 @@ struct Decimal128( try: self = Decimal128.from_string(value) except e: - raise Error( - ConversionError( - message="Cannot initialize Decimal128 from String.", - function="Decimal128.__init__()", - previous_error=e^, - ) + raise ConversionError( + message="Cannot initialize Decimal128 from String.", + function="Decimal128.__init__()", + previous_error=e^, ) def __init__(out self, value: Float64) raises: @@ -362,12 +355,10 @@ struct Decimal128( try: self = Decimal128.from_float(value) except e: - raise Error( - ConversionError( - message="Cannot initialize Decimal128 from Float64.", - function="Decimal128.__init__()", - previous_error=e^, - ) + raise ConversionError( + message="Cannot initialize Decimal128 from Float64.", + function="Decimal128.__init__()", + previous_error=e^, ) # ===------------------------------------------------------------------=== # @@ -399,13 +390,11 @@ struct Decimal128( """ if scale > UInt32(Self.MAX_SCALE): - raise Error( - ValueError( - message=String( - "Scale must be between 0 and 28, but got {}." - ).format(scale), - function="Decimal128.from_components()", - ) + raise ValueError( + message=String( + "Scale must be between 0 and 28, but got {}." + ).format(scale), + function="Decimal128.from_components()", ) var flags: UInt32 = 0 @@ -525,13 +514,11 @@ struct Decimal128( var flags: UInt32 if scale > UInt32(Self.MAX_SCALE): - raise Error( - ValueError( - message=String( - "Scale must be between 0 and 28, but got {}" - ).format(scale), - function="Decimal128.from_int()", - ) + raise ValueError( + message=String( + "Scale must be between 0 and 28, but got {}" + ).format(scale), + function="Decimal128.from_int()", ) if value >= 0: @@ -569,23 +556,19 @@ struct Decimal128( """ if value >> 96 != 0: - raise Error( - ValueError( - message=String( - "Value must fit in 96 bits, but got {}" - ).format(value), - function="Decimal128.from_uint128()", - ) + raise ValueError( + message=String("Value must fit in 96 bits, but got {}").format( + value + ), + function="Decimal128.from_uint128()", ) if scale > UInt32(Self.MAX_SCALE): - raise Error( - ValueError( - message=String( - "Scale must be between 0 and 28, but got {}" - ).format(scale), - function="Decimal128.from_uint128()", - ) + raise ValueError( + message=String( + "Scale must be between 0 and 28, but got {}" + ).format(scale), + function="Decimal128.from_uint128()", ) var result = UnsafePointer(to=value).bitcast[Decimal128]()[] @@ -632,13 +615,11 @@ struct Decimal128( # Check for non-ASCII characters (each non-ASCII char is multi-byte) for byte in value_bytes: if byte > 127: - raise Error( - ValueError( - message=String( - "Invalid characters in decimal128 string: {}" - ).format(value), - function="Decimal128.from_string()", - ) + raise ValueError( + message=String( + "Invalid characters in decimal128 string: {}" + ).format(value), + function="Decimal128.from_string()", ) # Yuhao's notes: @@ -670,26 +651,19 @@ struct Decimal128( elif code == 45: unexpected_end_char = True if exponent_sign_read: - raise Error( - ValueError( - message=( - "Minus sign cannot appear twice in exponent." - ), - function="Decimal128.from_string()", - ) + raise ValueError( + message="Minus sign cannot appear twice in exponent.", + function="Decimal128.from_string()", ) elif exponent_notation_read: exponent_sign = True exponent_sign_read = True elif mantissa_sign_read: - raise Error( - ValueError( - message=( - "Minus sign can only appear once at the" - " beginning." - ), - function="Decimal128.from_string()", - ) + raise ValueError( + message=( + "Minus sign can only appear once at the beginning." + ), + function="Decimal128.from_string()", ) else: mantissa_sign = True @@ -698,25 +672,18 @@ struct Decimal128( elif code == 43: unexpected_end_char = True if exponent_sign_read: - raise Error( - ValueError( - message=( - "Plus sign cannot appear twice in exponent." - ), - function="Decimal128.from_string()", - ) + raise ValueError( + message="Plus sign cannot appear twice in exponent.", + function="Decimal128.from_string()", ) elif exponent_notation_read: exponent_sign_read = True elif mantissa_sign_read: - raise Error( - ValueError( - message=( - "Plus sign can only appear once at the" - " beginning." - ), - function="Decimal128.from_string()", - ) + raise ValueError( + message=( + "Plus sign can only appear once at the beginning." + ), + function="Decimal128.from_string()", ) else: mantissa_sign_read = True @@ -724,11 +691,9 @@ struct Decimal128( elif code == 46: unexpected_end_char = False if decimal_point_read: - raise Error( - ValueError( - message="Decimal point can only appear once.", - function="Decimal128.from_string()", - ) + raise ValueError( + message="Decimal point can only appear once.", + function="Decimal128.from_string()", ) else: decimal_point_read = True @@ -737,22 +702,14 @@ struct Decimal128( elif code == 101 or code == 69: unexpected_end_char = True if exponent_notation_read: - raise Error( - ValueError( - message=( - "Exponential notation can only appear once." - ), - function="Decimal128.from_string()", - ) + raise ValueError( + message="Exponential notation can only appear once.", + function="Decimal128.from_string()", ) if not mantissa_start: - raise Error( - ValueError( - message=( - "Exponential notation must follow a number." - ), - function="Decimal128.from_string()", - ) + raise ValueError( + message="Exponential notation must follow a number.", + function="Decimal128.from_string()", ) else: exponent_notation_read = True @@ -794,13 +751,11 @@ struct Decimal128( if (not exponent_sign) and ( raw_exponent > Decimal128.MAX_NUM_DIGITS * 2 ): - raise Error( - OverflowError( - message=String( - "Exponent part is too large: {}" - ).format(raw_exponent), - function="Decimal128.from_string()", - ) + raise OverflowError( + message=String( + "Exponent part is too large: {}" + ).format(raw_exponent), + function="Decimal128.from_string()", ) # Skip the digit if exponent is negatively too large @@ -831,21 +786,17 @@ struct Decimal128( scale += 1 else: - raise Error( - ValueError( - message=String( - "Invalid character in decimal128 string: {}" - ).format(chr(Int(code))), - function="Decimal128.from_string()", - ) + raise ValueError( + message=String( + "Invalid character in decimal128 string: {}" + ).format(chr(Int(code))), + function="Decimal128.from_string()", ) if unexpected_end_char: - raise Error( - ValueError( - message="Unexpected end character in decimal128 string.", - function="Decimal128.from_string()", - ) + raise ValueError( + message="Unexpected end character in decimal128 string.", + function="Decimal128.from_string()", ) # print("DEBUG: coef = ", coef) @@ -967,14 +918,12 @@ struct Decimal128( # Early exit if the value is too large if UInt128(abs_value) > Decimal128.MAX_AS_UINT128: - raise Error( - OverflowError( - message=String( - "The float value {} is too large (>=2^96) to be" - " transformed into Decimal128." - ).format(value), - function="Decimal128.from_float()", - ) + raise OverflowError( + message=String( + "The float value {} is too large (>=2^96) to be" + " transformed into Decimal128." + ).format(value), + function="Decimal128.from_float()", ) # Extract binary exponent using IEEE 754 bit manipulation @@ -989,11 +938,9 @@ struct Decimal128( # CASE: Infinity or NaN if biased_exponent == 0x7FF: - raise Error( - ValueError( - message="Cannot convert infinity or NaN to Decimal128.", - function="Decimal128.from_float()", - ) + raise ValueError( + message="Cannot convert infinity or NaN to Decimal128.", + function="Decimal128.from_float()", ) # Get unbias exponent @@ -1175,12 +1122,10 @@ struct Decimal128( try: return Int(self.to_int64()) except e: - raise Error( - ConversionError( - message="Cannot convert Decimal128 to Int.", - function="Decimal128.to_int()", - previous_error=e^, - ) + raise ConversionError( + message="Cannot convert Decimal128 to Int.", + function="Decimal128.to_int()", + previous_error=e^, ) def to_int64(self) raises -> Int64: @@ -1196,19 +1141,15 @@ struct Decimal128( var result = self.to_int128() if result > Int128(Int64.MAX): - raise Error( - OverflowError( - message="Decimal128 is too large to fit in Int64.", - function="Decimal128.to_int64()", - ) + raise OverflowError( + message="Decimal128 is too large to fit in Int64.", + function="Decimal128.to_int64()", ) if result < Int128(Int64.MIN): - raise Error( - OverflowError( - message="Decimal128 is too small to fit in Int64.", - function="Decimal128.to_int64()", - ) + raise OverflowError( + message="Decimal128 is too small to fit in Int64.", + function="Decimal128.to_int64()", ) return Int64(result & 0xFFFF_FFFF_FFFF_FFFF) @@ -2064,13 +2005,9 @@ struct Decimal128( End of examples. """ if precision_diff < 0: - raise Error( - ValueError( - message=( - "precision_diff must be greater than or equal to 0." - ), - function="Decimal128.extend_precision()", - ) + raise ValueError( + message="precision_diff must be greater than or equal to 0.", + function="Decimal128.extend_precision()", ) if precision_diff == 0: diff --git a/src/decimo/decimal128/exponential.mojo b/src/decimo/decimal128/exponential.mojo index f95e890d..a8dbf482 100644 --- a/src/decimo/decimal128/exponential.mojo +++ b/src/decimo/decimal128/exponential.mojo @@ -20,7 +20,7 @@ import std.math from std import testing from std import time -from decimo.errors import DecimoError, ValueError, OverflowError +from decimo.errors import ValueError, OverflowError, ZeroDivisionError import decimo.decimal128.constants import decimo.decimal128.special import decimo.decimal128.utility @@ -56,24 +56,16 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: try: return power(base, Int(exponent)) except e: - raise Error( - DecimoError( - message="Failed to compute power with Decimal128 exponent.", - function="power()", - previous_error=e^, - ) - ) + raise e^ # CASE: For negative bases, only integer exponents are supported if base.is_negative(): - raise Error( - ValueError( - message=( - "Negative base with non-integer exponent results in a" - " complex number." - ), - function="power()", - ) + raise ValueError( + message=( + "Negative base with non-integer exponent results in a" + " complex number." + ), + function="power()", ) # CASE: If the exponent is simple fractions @@ -82,24 +74,20 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: try: return sqrt(base) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="power()", - previous_error=e^, - ) + raise ValueError( + message="See the above exception.", + function="power()", + previous_error=e^, ) # -0.5 if exponent == Decimal128(5, 0, 0, 0x80010000): try: return Decimal128.ONE() / sqrt(base) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="power()", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="power()", + previous_error=e^, ) # GENERAL CASE @@ -109,13 +97,7 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: var product = exponent * ln_base return exp(product) except e: - raise Error( - DecimoError( - message="See the above exception.", - function="power()", - previous_error=e^, - ) - ) + raise e^ def power(base: Decimal128, exponent: Int) raises -> Decimal128: @@ -144,11 +126,9 @@ def power(base: Decimal128, exponent: Int) raises -> Decimal128: return Decimal128.ZERO() else: # 0^n is undefined for n < 0 - raise Error( - ValueError( - message="Zero cannot be raised to a negative power.", - function="power()", - ) + raise ValueError( + message="Zero cannot be raised to a negative power.", + function="power()", ) if base.coefficient() == 1 and base.scale() == 0: @@ -200,11 +180,9 @@ def root(x: Decimal128, n: Int) raises -> Decimal128: # Special cases for n if n <= 0: - raise Error( - ValueError( - message="Cannot compute non-positive root.", - function="root()", - ) + raise ValueError( + message="Cannot compute non-positive root.", + function="root()", ) if n == 1: return x @@ -218,11 +196,9 @@ def root(x: Decimal128, n: Int) raises -> Decimal128: return Decimal128.ONE() if x.is_negative(): if n % 2 == 0: - raise Error( - ValueError( - message="Cannot compute even root of a negative number.", - function="root()", - ) + raise ValueError( + message="Cannot compute even root of a negative number.", + function="root()", ) # For odd roots of negative numbers, compute |x|^(1/n) and negate return -root(-x, n) @@ -235,12 +211,10 @@ def root(x: Decimal128, n: Int) raises -> Decimal128: # Direct calculation: x^n = e^(ln(x)/n) return exp(ln(x) / Decimal128(n)) except e: - raise Error( - DecimoError( - message="Root computation failed.", - function="root()", - previous_error=e^, - ) + raise ValueError( + message="Root computation failed.", + function="root()", + previous_error=e^, ) # Initial guess @@ -382,11 +356,9 @@ def sqrt(x: Decimal128) raises -> Decimal128: """ # Special cases if x.is_negative(): - raise Error( - ValueError( - message="Cannot compute square root of a negative number.", - function="sqrt()", - ) + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt()", ) if x.is_zero(): @@ -514,14 +486,12 @@ def exp(x: Decimal128) raises -> Decimal128: """ if x > Decimal128.from_int(value=6654, scale=UInt32(2)): - raise Error( - OverflowError( - message=( - "x is too large (must be <= 66.54). Consider using" - " BigDecimal type." - ), - function="exp()", - ) + raise OverflowError( + message=( + "x is too large (must be <= 66.54). Consider using" + " BigDecimal type." + ), + function="exp()", ) # Handle special cases @@ -728,11 +698,9 @@ def ln(x: Decimal128) raises -> Decimal128: # Handle special cases if x.is_negative() or x.is_zero(): - raise Error( - ValueError( - message="Cannot compute logarithm of a non-positive number.", - function="ln()", - ) + raise ValueError( + message="Cannot compute logarithm of a non-positive number.", + function="ln()", ) if x.is_one(): @@ -994,29 +962,23 @@ def log(x: Decimal128, base: Decimal128) raises -> Decimal128: """ # Special cases: x <= 0 if x.is_negative() or x.is_zero(): - raise Error( - ValueError( - message="Cannot compute logarithm of a non-positive number.", - function="log()", - ) + raise ValueError( + message="Cannot compute logarithm of a non-positive number.", + function="log()", ) # Special cases: base <= 0 if base.is_negative() or base.is_zero(): - raise Error( - ValueError( - message="Cannot use non-positive base for logarithm.", - function="log()", - ) + raise ValueError( + message="Cannot use non-positive base for logarithm.", + function="log()", ) # Special case: base = 1 if base.is_one(): - raise Error( - ValueError( - message="Cannot use base 1 for logarithm.", - function="log()", - ) + raise ValueError( + message="Cannot use base 1 for logarithm.", + function="log()", ) # Special case: x = 1 @@ -1057,11 +1019,9 @@ def log10(x: Decimal128) raises -> Decimal128: """ # Special cases: x <= 0 if x.is_negative() or x.is_zero(): - raise Error( - ValueError( - message="Cannot compute logarithm of a non-positive number.", - function="log10()", - ) + raise ValueError( + message="Cannot compute logarithm of a non-positive number.", + function="log10()", ) var x_scale = x.scale() diff --git a/src/decimo/decimal128/rounding.mojo b/src/decimo/decimal128/rounding.mojo index 4ed4cba7..61a5dba1 100644 --- a/src/decimo/decimal128/rounding.mojo +++ b/src/decimo/decimal128/rounding.mojo @@ -95,18 +95,16 @@ def round( if scale_diff > 0: # If the digits of result > 29, directly raise an error if ndigits_of_x + scale_diff > Decimal128.MAX_NUM_DIGITS: - raise Error( - OverflowError( - message=String( - "ndigits={} causes the number of significant figures" - " ({}) to exceed the maximum capacity ({})." - ).format( - ndigits, - ndigits_of_x + scale_diff, - Decimal128.MAX_NUM_DIGITS, - ), - function="round()", - ) + raise OverflowError( + message=String( + "ndigits={} causes the number of significant figures" + " ({}) to exceed the maximum capacity ({})." + ).format( + ndigits, + ndigits_of_x + scale_diff, + Decimal128.MAX_NUM_DIGITS, + ), + function="round()", ) # If the digits of result <= 29, calculate the result by scaling up @@ -117,14 +115,12 @@ def round( if (ndigits_of_x + scale_diff == Decimal128.MAX_NUM_DIGITS) and ( res_coef > Decimal128.MAX_AS_UINT128 ): - raise Error( - OverflowError( - message=String( - "ndigits={} causes the significant digits ({})" - " to exceed the maximum capacity ({})." - ).format(ndigits, res_coef, Decimal128.MAX_AS_UINT128), - function="round()", - ) + raise OverflowError( + message=String( + "ndigits={} causes the significant digits ({})" + " to exceed the maximum capacity ({})." + ).format(ndigits, res_coef, Decimal128.MAX_AS_UINT128), + function="round()", ) # In other cases, return the result diff --git a/src/decimo/decimal128/special.mojo b/src/decimo/decimal128/special.mojo index 8a7c6675..0c6556bf 100644 --- a/src/decimo/decimal128/special.mojo +++ b/src/decimo/decimal128/special.mojo @@ -39,21 +39,17 @@ def factorial(n: Int) raises -> Decimal128: """ if n < 0: - raise Error( - ValueError( - message="Factorial is not defined for negative numbers.", - function="factorial()", - ) + raise ValueError( + message="Factorial is not defined for negative numbers.", + function="factorial()", ) if n > 27: - raise Error( - OverflowError( - message=String( - "{}! is too large to be represented by Decimal128." - ).format(n), - function="factorial()", - ) + raise OverflowError( + message=String( + "{}! is too large to be represented by Decimal128." + ).format(n), + function="factorial()", ) # Directly return the factorial for n = 0 to 27 @@ -178,13 +174,9 @@ def factorial_reciprocal(n: Int) raises -> Decimal128: # 1/27! = 0.0000000000000000000000000001, Decimal128.from_words(0x1, 0x0, 0x0, 0x1c0000) if n < 0: - raise Error( - ValueError( - message=( - "Factorial reciprocal is not defined for negative numbers." - ), - function="factorial_reciprocal()", - ) + raise ValueError( + message="Factorial reciprocal is not defined for negative numbers.", + function="factorial_reciprocal()", ) # For n > 27, 1/n! is essentially 0 at Decimal128 precision diff --git a/src/decimo/errors.mojo b/src/decimo/errors.mojo index 57fabb6e..0ba4c596 100644 --- a/src/decimo/errors.mojo +++ b/src/decimo/errors.mojo @@ -75,8 +75,12 @@ comptime ZeroDivisionError = DecimoError[error_type="ZeroDivisionError"] comptime ConversionError = DecimoError[error_type="ConversionError"] """Type for conversion errors in Decimo.""" +comptime RuntimeError = DecimoError[error_type="RuntimeError"] +"""Type for runtime infrastructure errors in Decimo (e.g., resource allocation +failures, missing native libraries).""" -struct DecimoError[error_type: String = "DecimoError"](Writable): + +struct DecimoError[error_type: StringLiteral = "DecimoError"](Writable): """Base type for all Decimo errors. The error message format mimics Python's traceback: diff --git a/src/decimo/str.mojo b/src/decimo/str.mojo index 877ef6c3..a00a3421 100644 --- a/src/decimo/str.mojo +++ b/src/decimo/str.mojo @@ -126,11 +126,9 @@ def parse_numeric_string( var n = len(value_bytes) if n == 0: - raise Error( - ValueError( - message="Empty string.", - function="parse_numeric_string()", - ) + raise ValueError( + message="Empty string.", + function="parse_numeric_string()", ) var ptr = value_bytes.unsafe_ptr() @@ -195,20 +193,14 @@ def parse_numeric_string( elif c == 46: last_was_separator = False if in_exponent: - raise Error( - ValueError( - message=( - "Decimal point cannot appear in the exponent part." - ), - function="parse_numeric_string()", - ) + raise ValueError( + message="Decimal point cannot appear in the exponent part.", + function="parse_numeric_string()", ) if decimal_point_pos != -1: - raise Error( - ValueError( - message="Decimal point can only appear once.", - function="parse_numeric_string()", - ) + raise ValueError( + message="Decimal point can only appear once.", + function="parse_numeric_string()", ) decimal_point_pos = i sign_read = True @@ -217,18 +209,14 @@ def parse_numeric_string( elif c == 101 or c == 69: last_was_separator = True if in_exponent: - raise Error( - ValueError( - message="Exponential notation can only appear once.", - function="parse_numeric_string()", - ) + raise ValueError( + message="Exponential notation can only appear once.", + function="parse_numeric_string()", ) if total_mantissa_digits == 0: - raise Error( - ValueError( - message="Exponential notation must follow a number.", - function="parse_numeric_string()", - ) + raise ValueError( + message="Exponential notation must follow a number.", + function="parse_numeric_string()", ) exponent_pos = i in_exponent = True @@ -238,26 +226,21 @@ def parse_numeric_string( last_was_separator = True if in_exponent: if exponent_sign_read: - raise Error( - ValueError( - message=( - "Exponent sign can only appear once," - " before exponent digits." - ), - function="parse_numeric_string()", - ) + raise ValueError( + message=( + "Exponent sign can only appear once," + " before exponent digits." + ), + function="parse_numeric_string()", ) exponent_sign_read = True else: if sign_read: - raise Error( - ValueError( - message=( - "Minus sign can only appear once at the" - " beginning." - ), - function="parse_numeric_string()", - ) + raise ValueError( + message=( + "Minus sign can only appear once at the beginning." + ), + function="parse_numeric_string()", ) sign = True sign_read = True @@ -267,53 +250,42 @@ def parse_numeric_string( last_was_separator = True if in_exponent: if exponent_sign_read: - raise Error( - ValueError( - message=( - "Exponent sign can only appear once," - " before exponent digits." - ), - function="parse_numeric_string()", - ) + raise ValueError( + message=( + "Exponent sign can only appear once," + " before exponent digits." + ), + function="parse_numeric_string()", ) exponent_sign_read = True else: if sign_read: - raise Error( - ValueError( - message=( - "Plus sign can only appear once at the" - " beginning." - ), - function="parse_numeric_string()", - ) + raise ValueError( + message=( + "Plus sign can only appear once at the beginning." + ), + function="parse_numeric_string()", ) sign_read = True else: - raise Error( - ValueError( - message=String( - "Invalid character in the string of the number: {}" - ).format(chr(Int(c))), - function="parse_numeric_string()", - ) + raise ValueError( + message=String( + "Invalid character in the string of the number: {}" + ).format(chr(Int(c))), + function="parse_numeric_string()", ) if last_was_separator: - raise Error( - ValueError( - message="Unexpected end character in the string of the number.", - function="parse_numeric_string()", - ) + raise ValueError( + message="Unexpected end character in the string of the number.", + function="parse_numeric_string()", ) if total_mantissa_digits == 0: - raise Error( - ValueError( - message="No digits found in the string of the number.", - function="parse_numeric_string()", - ) + raise ValueError( + message="No digits found in the string of the number.", + function="parse_numeric_string()", ) # ================================================================== diff --git a/src/decimo/tests.mojo b/src/decimo/tests.mojo index 3258e5f1..7dc17478 100644 --- a/src/decimo/tests.mojo +++ b/src/decimo/tests.mojo @@ -52,7 +52,7 @@ Pattern expansion in string values: from .toml import parse_file as parse_toml_file from .toml.parser import TOMLDocument -from .errors import DecimoError +from .errors import ValueError from std.python import Python, PythonObject from std.collections import List from std import os @@ -303,12 +303,10 @@ def parse_file(file_path: String) raises -> TOMLDocument: try: return parse_toml_file(file_path) except e: - raise Error( - DecimoError( - message="Failed to parse TOML file: " + file_path, - function="parse_file()", - previous_error=e^, - ) + raise ValueError( + message="Failed to parse TOML file: " + file_path, + function="parse_file()", + previous_error=e^, ) diff --git a/src/decimo/toml/parser.mojo b/src/decimo/toml/parser.mojo index 27f2e14e..5bc99c1c 100644 --- a/src/decimo/toml/parser.mojo +++ b/src/decimo/toml/parser.mojo @@ -472,20 +472,15 @@ def _set_value( var existing = root[key].table_values.copy() for entry in value.table_values.items(): if entry.key in existing: - raise Error( - ValueError( - message="Duplicate key: " - + key - + "." - + entry.key, - function="parse()", - ) + raise ValueError( + message="Duplicate key: " + key + "." + entry.key, + function="parse()", ) existing[entry.key] = entry.value.copy() root[key] = _make_table(existing^) return - raise Error( - ValueError(message="Duplicate key: " + key, function="parse()") + raise ValueError( + message="Duplicate key: " + key, function="parse()" ) root[key] = value^ return @@ -507,11 +502,9 @@ def _set_value( arr[len(arr) - 1] = _make_table(last_tbl^) root[first].array_values = arr^ return - raise Error( - ValueError( - message="Key exists but is not a table: " + first, - function="parse()", - ) + raise ValueError( + message="Key exists but is not a table: " + first, + function="parse()", ) var table = root[first].table_values.copy() @@ -549,11 +542,9 @@ def _ensure_table_path( root[first].array_values = arr^ return elif root[first].type != TOMLValueType.TABLE: - raise Error( - ValueError( - message="Key exists but is not a table: " + first, - function="parse()", - ) + raise ValueError( + message="Key exists but is not a table: " + first, + function="parse()", ) if len(path) > 1: @@ -574,11 +565,9 @@ def _append_array_of_tables( ) raises: """Append a new empty table to the array-of-tables at `path`.""" if len(path) == 0: - raise Error( - ValueError( - message="Array of tables path cannot be empty", - function="parse()", - ) + raise ValueError( + message="Array of tables path cannot be empty", + function="parse()", ) if len(path) == 1: @@ -595,11 +584,9 @@ def _append_array_of_tables( _make_table(Dict[String, TOMLValue]()) ) else: - raise Error( - ValueError( - message="Cannot redefine as array of tables: " + key, - function="parse()", - ) + raise ValueError( + message="Cannot redefine as array of tables: " + key, + function="parse()", ) return @@ -627,11 +614,9 @@ def _append_array_of_tables( arr[len(arr) - 1] = _make_table(last_tbl^) root[first].array_values = arr^ else: - raise Error( - ValueError( - message="Key exists but is not a table or array: " + first, - function="parse()", - ) + raise ValueError( + message="Key exists but is not a table or array: " + first, + function="parse()", ) @@ -705,7 +690,7 @@ struct TOMLParser: var parts = List[String]() if not self._is_key_token(): - raise Error(ValueError(message="Expected key", function="parse()")) + raise ValueError(message="Expected key", function="parse()") parts.append(self._tok().value) self._advance() @@ -714,10 +699,8 @@ struct TOMLParser: while self._tok().type == TokenType.DOT: self._advance() # skip dot if not self._is_key_token(): - raise Error( - ValueError( - message="Expected key after dot", function="parse()" - ) + raise ValueError( + message="Expected key after dot", function="parse()" ) parts.append(self._tok().value) self._advance() @@ -856,11 +839,9 @@ struct TOMLParser: # Expect equals if self._tok().type != TokenType.EQUAL: - raise Error( - ValueError( - message="Expected '=' in inline table", - function="parse()", - ) + raise ValueError( + message="Expected '=' in inline table", + function="parse()", ) self._advance() @@ -870,12 +851,10 @@ struct TOMLParser: # Set value at potentially nested path if len(key_parts) == 1: if key_parts[0] in table: - raise Error( - ValueError( - message="Duplicate key in inline table: " - + key_parts[0], - function="TOMLParser._parse_inline_table()", - ) + raise ValueError( + message="Duplicate key in inline table: " + + key_parts[0], + function="TOMLParser._parse_inline_table()", ) table[key_parts[0]] = value^ else: @@ -893,11 +872,9 @@ struct TOMLParser: self._advance() break else: - raise Error( - ValueError( - message="Expected ',' or '}' in inline table", - function="parse()", - ) + raise ValueError( + message="Expected ',' or '}' in inline table", + function="parse()", ) return _make_table(table^) @@ -912,11 +889,9 @@ struct TOMLParser: if self._tok().type == TokenType.ARRAY_END: self._advance() else: - raise Error( - ValueError( - message="Expected ']' after table header", - function="parse()", - ) + raise ValueError( + message="Expected ']' after table header", + function="parse()", ) return path^ @@ -929,20 +904,16 @@ struct TOMLParser: if self._tok().type == TokenType.ARRAY_END: self._advance() else: - raise Error( - ValueError( - message="Expected ']]' after array of tables header", - function="parse()", - ) + raise ValueError( + message="Expected ']]' after array of tables header", + function="parse()", ) if self._tok().type == TokenType.ARRAY_END: self._advance() else: - raise Error( - ValueError( - message="Expected ']]' after array of tables header", - function="parse()", - ) + raise ValueError( + message="Expected ']]' after array of tables header", + function="parse()", ) return path^ From b5567d9cf4929f42a02531566c8a5aa1f15fc060 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Fri, 10 Apr 2026 00:47:55 +0200 Subject: [PATCH 09/17] [docstring][error] Fixes `raises:` sections in docstrings across codebase to show correct error types (#199) This PR updates docstring `Raises:` sections across Decimo to replace generic `Error` entries with specific error types (e.g., `ValueError`, `OverflowError`, `ZeroDivisionError`, `ConversionError`) so API documentation more accurately reflects runtime behavior. **Changes:** - Standardized and expanded docstring `Raises:` sections across TOML parsing, numeric parsing, and multiple numeric types (Decimal128/Big*). - Replaced generic `Error`/`DecimoError` docstring entries with concrete error types used by the implementation. - Added missing `Raises:` sections to several functions that previously had none. --- src/decimo/bigdecimal/arithmetics.mojo | 10 +++- src/decimo/bigdecimal/bigdecimal.mojo | 6 ++- src/decimo/bigdecimal/constants.mojo | 3 ++ src/decimo/bigdecimal/exponential.mojo | 41 +++++++------- src/decimo/bigdecimal/trigonometric.mojo | 6 +++ src/decimo/bigfloat/bigfloat.mojo | 55 +++++++++++++++++++ src/decimo/bigint/arithmetics.mojo | 20 ++++--- src/decimo/bigint/bigint.mojo | 64 +++++++++++++++++++--- src/decimo/bigint/exponential.mojo | 4 +- src/decimo/bigint/number_theory.mojo | 10 +++- src/decimo/bigint10/arithmetics.mojo | 10 ++-- src/decimo/bigint10/bigint10.mojo | 54 +++++++++++++++++-- src/decimo/biguint/arithmetics.mojo | 29 ++++++---- src/decimo/biguint/biguint.mojo | 69 ++++++++++++++++++++---- src/decimo/decimal128/arithmetics.mojo | 19 ++++++- src/decimo/decimal128/constants.mojo | 2 +- src/decimo/decimal128/decimal128.mojo | 36 +++++++++---- src/decimo/decimal128/exponential.mojo | 26 ++++----- src/decimo/decimal128/rounding.mojo | 3 ++ src/decimo/decimal128/special.mojo | 7 +++ src/decimo/str.mojo | 4 ++ src/decimo/tests.mojo | 3 ++ src/decimo/toml/parser.mojo | 36 +++++++++++-- 23 files changed, 416 insertions(+), 101 deletions(-) diff --git a/src/decimo/bigdecimal/arithmetics.mojo b/src/decimo/bigdecimal/arithmetics.mojo index 055e3100..41d9c553 100644 --- a/src/decimo/bigdecimal/arithmetics.mojo +++ b/src/decimo/bigdecimal/arithmetics.mojo @@ -308,6 +308,9 @@ def true_divide( The quotient of x and y, with precision up to `precision` significant digits. + Raises: + ZeroDivisionError: If the divisor is zero. + Notes: - If the coefficients can be divided exactly, the number of digits after @@ -692,6 +695,9 @@ def true_divide_inexact( Returns: The quotient of x1 and x2. + + Raises: + ZeroDivisionError: If the divisor is zero. """ # Check for division by zero @@ -904,7 +910,7 @@ def truncate_divide(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: The quotient of x1 and x2, truncated to zeros. Raises: - Error: If division by zero is attempted. + ZeroDivisionError: If division by zero is attempted. Notes: This function performs integer division that truncates toward zero. @@ -951,7 +957,7 @@ def truncate_modulo( The truncated modulo of x1 and x2. Raises: - Error: If division by zero is attempted. + ZeroDivisionError: If division by zero is attempted. """ # Check for division by zero if x2.coefficient.is_zero(): diff --git a/src/decimo/bigdecimal/bigdecimal.mojo b/src/decimo/bigdecimal/bigdecimal.mojo index 250773f5..e4dbcdc5 100644 --- a/src/decimo/bigdecimal/bigdecimal.mojo +++ b/src/decimo/bigdecimal/bigdecimal.mojo @@ -345,6 +345,10 @@ struct BigDecimal( Returns: The BigDecimal representation of the Scalar value. + Raises: + ValueError: If the value is NaN. + ConversionError: If the conversion from scalar to BigDecimal fails. + Notes: If the value is a floating-point number, it is converted to a string @@ -430,7 +434,7 @@ struct BigDecimal( The BigDecimal representation of the Python Decimal. Raises: - Error: If the conversion from Python Decimal fails, or if + ConversionError: If the conversion from Python Decimal fails, or if the as_tuple() method returns invalid data. Examples: diff --git a/src/decimo/bigdecimal/constants.mojo b/src/decimo/bigdecimal/constants.mojo index ac67c48a..802759b2 100644 --- a/src/decimo/bigdecimal/constants.mojo +++ b/src/decimo/bigdecimal/constants.mojo @@ -162,6 +162,9 @@ def pi(precision: Int) raises -> BigDecimal: Returns: The value of π to the specified precision. + + Raises: + ValueError: If the precision is negative. """ if precision < 0: diff --git a/src/decimo/bigdecimal/exponential.mojo b/src/decimo/bigdecimal/exponential.mojo index 44777d5e..9540ee7d 100644 --- a/src/decimo/bigdecimal/exponential.mojo +++ b/src/decimo/bigdecimal/exponential.mojo @@ -221,8 +221,9 @@ def power( The result of base^exponent. Raises: - Error: If base is negative and exponent is not an integer. - Error: If base is zero and exponent is negative or zero. + ValueError: If base is negative and exponent is not an integer. + ValueError: If base is zero and exponent is zero. + ZeroDivisionError: If base is zero and exponent is negative. Notes: @@ -425,8 +426,8 @@ def root(x: BigDecimal, n: BigDecimal, precision: Int) raises -> BigDecimal: The nth root of x with the specified precision. Raises: - Error: If x is negative and n is not an odd integer. - Error: If n is zero. + ValueError: If x is negative and n is not an odd integer. + ValueError: If n is zero. Notes: Uses the identity x^(1/n) = exp(ln(|x|)/n) for calculation. @@ -552,9 +553,9 @@ def integer_root( The nth root of x with the specified precision. Raises: - Error: If x is negative and n is even. - Error: If n is not a positive integer. - Error: If n is zero. + ValueError: If x is negative and n is even. + ValueError: If n is not a positive integer. + ValueError: If n is zero. """ comptime BUFFER_DIGITS = 9 var working_precision = precision + BUFFER_DIGITS @@ -1016,7 +1017,7 @@ def sqrt(x: BigDecimal, precision: Int) raises -> BigDecimal: The square root of x with the specified precision. Raises: - Error: If x is negative. + ValueError: If x is negative. """ return sqrt_exact(x, precision) @@ -1199,7 +1200,7 @@ def sqrt_exact(x: BigDecimal, precision: Int) raises -> BigDecimal: The square root of x with the specified precision. Raises: - Error: If x is negative. + ValueError: If x is negative. """ # Handle special cases @@ -1333,7 +1334,7 @@ def sqrt_reciprocal(x: BigDecimal, precision: Int) raises -> BigDecimal: The square root of x with the specified precision. Raises: - Error: If x is negative. + ValueError: If x is negative. """ # Handle special cases @@ -1496,7 +1497,7 @@ def sqrt_newton(x: BigDecimal, precision: Int) raises -> BigDecimal: The square root of x with the specified precision. Raises: - Error: If x is negative. + ValueError: If x is negative. Notes: @@ -1593,7 +1594,7 @@ def sqrt_decimal_approach(x: BigDecimal, precision: Int) raises -> BigDecimal: The square root of x with the specified precision. Raises: - Error: If x is negative. + ValueError: If x is negative. Notes: @@ -1746,9 +1747,6 @@ def cbrt(x: BigDecimal, precision: Int) raises -> BigDecimal: Returns: The cube root of x with the specified precision. - - Raises: - Error: If x is negative. """ result = integer_root( @@ -1774,6 +1772,9 @@ def exp(x: BigDecimal, precision: Int) raises -> BigDecimal: Returns: The natural exponential of x (e^x) to the specified precision. + Raises: + OverflowError: If the result is too large to represent. + Notes: Uses aggressive range reduction for optimal performance: 1. Divide x by 2^M where M ≈ √(3.322·precision) to make x tiny @@ -1970,7 +1971,7 @@ def ln(x: BigDecimal, precision: Int) raises -> BigDecimal: The natural logarithm of x to the specified precision. Raises: - Error: If x is negative or zero. + ValueError: If x is negative or zero. """ var cache = MathCache() return ln(x, precision, cache) @@ -1993,7 +1994,7 @@ def ln( The natural logarithm of x to the specified precision. Raises: - Error: If x is negative or zero. + ValueError: If x is negative or zero. """ comptime BUFFER_DIGITS = 9 # word-length, easy to append and trim var working_precision = precision + BUFFER_DIGITS @@ -2088,8 +2089,8 @@ def log(x: BigDecimal, base: BigDecimal, precision: Int) raises -> BigDecimal: The logarithm of x with respect to base. Raises: - Error: If x is negative or zero. - Error: If base is negative, zero, or one. + ValueError: If x is negative or zero. + ValueError: If base is negative, zero, or one. """ comptime BUFFER_DIGITS = 9 # word-length, easy to append and trim var working_precision = precision + BUFFER_DIGITS @@ -2157,7 +2158,7 @@ def log10(x: BigDecimal, precision: Int) raises -> BigDecimal: The base-10 logarithm of x. Raises: - Error: If x is negative or zero. + ValueError: If x is negative or zero. """ comptime BUFFER_DIGITS = 9 # word-length, easy to append and trim var working_precision = precision + BUFFER_DIGITS diff --git a/src/decimo/bigdecimal/trigonometric.mojo b/src/decimo/bigdecimal/trigonometric.mojo index eb2166b5..5ea652c3 100644 --- a/src/decimo/bigdecimal/trigonometric.mojo +++ b/src/decimo/bigdecimal/trigonometric.mojo @@ -346,6 +346,9 @@ def tan_cot(x: BigDecimal, precision: Int, is_tan: Bool) raises -> BigDecimal: Returns: The cotangent of x with the specified precision. + Raises: + ValueError: If computing cot(nπ) which is undefined. + Notes: This function calculates tan(x) = cos(x) / sin(x) or @@ -425,6 +428,9 @@ def csc(x: BigDecimal, precision: Int) raises -> BigDecimal: Returns: The cosecant of x with the specified precision. + Raises: + ValueError: If x is zero (csc(nπ) is undefined). + Notes: This function calculates csc(x) = 1 / sin(x). diff --git a/src/decimo/bigfloat/bigfloat.mojo b/src/decimo/bigfloat/bigfloat.mojo index d3d468cd..0c1fb43e 100644 --- a/src/decimo/bigfloat/bigfloat.mojo +++ b/src/decimo/bigfloat/bigfloat.mojo @@ -144,6 +144,10 @@ struct BigFloat(Comparable, Movable, Writable): Args: value: A decimal number string (e.g. "3.14159", "-1.5e10"). precision: Number of significant decimal digits. + + Raises: + RuntimeError: If MPFR is not available or handle pool is exhausted. + ConversionError: If the string is not a valid number. """ if not mpfrw_available(): raise RuntimeError( @@ -235,6 +239,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: A decimal string representation. + + Raises: + ConversionError: If string export fails. """ var d = digits if digits > 0 else self.precision var address = mpfrw_get_str(self.handle, Int32(d)) @@ -308,6 +315,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: A BigDecimal with the requested number of significant digits. + + Raises: + ConversionError: If raw digit export fails. """ var d = precision if precision > 0 else self.precision @@ -456,6 +466,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The negated value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -471,6 +484,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The absolute value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -493,6 +509,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The sum. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) @@ -512,6 +531,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The difference. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) @@ -531,6 +553,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The product. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) @@ -550,6 +575,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The quotient. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var prec = max(self.precision, other.precision) var h = mpfrw_init(_dps_to_bits(prec)) @@ -581,6 +609,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The square root of this value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -596,6 +627,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The exponential of this value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -611,6 +645,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The natural logarithm of this value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -626,6 +663,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The sine of this value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -641,6 +681,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The cosine of this value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -656,6 +699,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The tangent of this value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -674,6 +720,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The result of `self` raised to `exponent`. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var prec = max(self.precision, exponent.precision) var h = mpfrw_init(_dps_to_bits(prec)) @@ -693,6 +742,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: The n-th root of this value. + + Raises: + RuntimeError: If MPFR handle allocation fails. """ var h = mpfrw_init(_dps_to_bits(self.precision)) if h < 0: @@ -712,6 +764,9 @@ struct BigFloat(Comparable, Movable, Writable): Returns: A `BigFloat` containing π at the requested precision. + + Raises: + RuntimeError: If MPFR is not available or handle allocation fails. """ if not mpfrw_available(): raise RuntimeError( diff --git a/src/decimo/bigint/arithmetics.mojo b/src/decimo/bigint/arithmetics.mojo index 6536d4ec..013cbbc3 100644 --- a/src/decimo/bigint/arithmetics.mojo +++ b/src/decimo/bigint/arithmetics.mojo @@ -2032,6 +2032,9 @@ def floor_divide_inplace(mut x: BigInt, read other: BigInt) raises: Args: x: The dividend (modified in-place to hold the quotient). other: The divisor. + + Raises: + ZeroDivisionError: If the divisor is zero. """ var result = _divmod_magnitudes(x.words, other.words) var q_words = result[0].copy() @@ -2075,6 +2078,9 @@ def floor_modulo_inplace(mut x: BigInt, read other: BigInt) raises: Args: x: The dividend (modified in-place to hold the remainder). other: The divisor. + + Raises: + ZeroDivisionError: If the divisor is zero. """ var result = _divmod_magnitudes(x.words, other.words) _ = result[0] @@ -2124,7 +2130,7 @@ def floor_divide(x1: BigInt, x2: BigInt) raises -> BigInt: The quotient of x1 / x2, rounded toward negative infinity. Raises: - Error: If x2 is zero. + ZeroDivisionError: If the divisor is zero. """ var result = _divmod_magnitudes(x1.words, x2.words) var q_words = result[0].copy() @@ -2171,7 +2177,7 @@ def truncate_divide(x1: BigInt, x2: BigInt) raises -> BigInt: The quotient of x1 / x2, truncated toward zero. Raises: - Error: If x2 is zero. + ZeroDivisionError: If the divisor is zero. """ var result = _divmod_magnitudes(x1.words, x2.words) var q_words = result[0].copy() @@ -2203,7 +2209,7 @@ def floor_modulo(x1: BigInt, x2: BigInt) raises -> BigInt: The remainder with the same sign as x2. Raises: - Error: If x2 is zero. + ZeroDivisionError: If the divisor is zero. """ var result = _divmod_magnitudes(x1.words, x2.words) _ = result[0] @@ -2243,7 +2249,7 @@ def truncate_modulo(x1: BigInt, x2: BigInt) raises -> BigInt: The remainder with the same sign as x1. Raises: - Error: If x2 is zero. + ZeroDivisionError: If the divisor is zero. """ var result = _divmod_magnitudes(x1.words, x2.words) _ = result[0] @@ -2276,7 +2282,7 @@ def floor_divmod(x1: BigInt, x2: BigInt) raises -> Tuple[BigInt, BigInt]: A tuple of (quotient, remainder). Raises: - Error: If x2 is zero. + ZeroDivisionError: If the divisor is zero. """ var result = _divmod_magnitudes(x1.words, x2.words) var q_words = result[0].copy() @@ -2329,8 +2335,8 @@ def power(base: BigInt, exponent: Int) raises -> BigInt: The result of base raised to the given exponent. Raises: - Error: If the exponent is negative. - Error: If the exponent is too large (>= 1_000_000_000). + ValueError: If the exponent is negative. + ValueError: If the exponent is too large (>= 1_000_000_000). """ if exponent < 0: raise ValueError( diff --git a/src/decimo/bigint/bigint.mojo b/src/decimo/bigint/bigint.mojo index d5e050e6..55408738 100644 --- a/src/decimo/bigint/bigint.mojo +++ b/src/decimo/bigint/bigint.mojo @@ -193,6 +193,9 @@ struct BigInt( Args: value: The string representation of the integer. + + Raises: + ConversionError: If the string cannot be converted to a BigInt. """ self = Self.from_string(value) @@ -451,11 +454,21 @@ struct BigInt( The BigInt representation. Raises: - Error: If the string is empty, contains invalid characters, - or represents a non-integer value. + ConversionError: If the string cannot be parsed as an integer. """ # Use the shared string parser for format handling - _tuple = decimo.str.parse_numeric_string(value) + try: + _tuple = decimo.str.parse_numeric_string(value) + except e: + raise ConversionError( + function="BigInt.from_string(value: String)", + message=( + 'The input value "' + + value + + '" cannot be parsed as an integer.\n' + + String(e) + ), + ) var ref coef: List[UInt8] = _tuple[0] var scale: Int = _tuple[1] var sign: Bool = _tuple[2] @@ -572,6 +585,9 @@ struct BigInt( Returns: The `Int` representation. + + Raises: + OverflowError: If the number exceeds the size of Int. """ return self.to_int() @@ -620,7 +636,7 @@ struct BigInt( The number as Int. Raises: - Error: If the number is too large or too small to fit in Int. + OverflowError: If the number exceeds the size of Int. """ # Int is 64-bit, so we need at most 2 words to represent it. # Int.MAX = 9_223_372_036_854_775_807 = 0x7FFF_FFFF_FFFF_FFFF @@ -970,6 +986,9 @@ struct BigInt( Returns: The quotient, rounded toward negative infinity. + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.bigint.arithmetics.floor_divide(self, other) @@ -989,6 +1008,9 @@ struct BigInt( Returns: The floor remainder with the same sign as the divisor. + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.bigint.arithmetics.floor_modulo(self, other) @@ -1008,6 +1030,9 @@ struct BigInt( Returns: A tuple of (quotient, remainder) using floor division. + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.bigint.arithmetics.floor_divmod(self, other) @@ -1027,6 +1052,10 @@ struct BigInt( Returns: The result of raising to the given power. + + Raises: + ValueError: If the exponent is negative. + OverflowError: If the exponent is too large to fit in Int. """ return self.power(exponent) @@ -1039,6 +1068,9 @@ struct BigInt( Returns: The result of raising to the given power. + + Raises: + ValueError: If the exponent is negative or too large. """ return self.power(exponent) @@ -1115,6 +1147,9 @@ struct BigInt( Returns: The quotient, rounded toward negative infinity. + + Raises: + ZeroDivisionError: If the divisor is zero. """ return decimo.bigint.arithmetics.floor_divide(other, self) @@ -1127,6 +1162,9 @@ struct BigInt( Returns: The floor remainder with the same sign as the divisor. + + Raises: + ZeroDivisionError: If the divisor is zero. """ return decimo.bigint.arithmetics.floor_modulo(other, self) @@ -1139,6 +1177,9 @@ struct BigInt( Returns: A tuple of (quotient, remainder) using floor division. + + Raises: + ZeroDivisionError: If the divisor is zero. """ return decimo.bigint.arithmetics.floor_divmod(other, self) @@ -1151,6 +1192,9 @@ struct BigInt( Returns: The result of raising the base to this power. + + Raises: + ValueError: If the exponent is negative. """ return base.power(self) @@ -1201,6 +1245,9 @@ struct BigInt( Args: other: The right-hand side operand. + + Raises: + ZeroDivisionError: If the divisor is zero. """ decimo.bigint.arithmetics.floor_divide_inplace(self, other) @@ -1210,6 +1257,9 @@ struct BigInt( Args: other: The right-hand side operand. + + Raises: + ZeroDivisionError: If the divisor is zero. """ decimo.bigint.arithmetics.floor_modulo_inplace(self, other) @@ -1435,7 +1485,8 @@ struct BigInt( The result of self raised to the given exponent. Raises: - Error: If the exponent is negative. + ValueError: If the exponent is negative. + ValueError: If the exponent is too large (>= 1_000_000_000). """ return decimo.bigint.arithmetics.power(self, exponent) @@ -1449,7 +1500,8 @@ struct BigInt( The result of self raised to the given exponent. Raises: - Error: If the exponent is negative or too large. + ValueError: If the exponent is negative. + OverflowError: If the exponent is too large to fit in Int. """ if exponent.is_negative(): raise ValueError( diff --git a/src/decimo/bigint/exponential.mojo b/src/decimo/bigint/exponential.mojo index 7c04cd30..b3fd618c 100644 --- a/src/decimo/bigint/exponential.mojo +++ b/src/decimo/bigint/exponential.mojo @@ -261,7 +261,7 @@ def sqrt(x: BigInt) raises -> BigInt: The integer square root of x. Raises: - Error: If x is negative. + ValueError: If x is negative. Notes: @@ -494,6 +494,6 @@ def isqrt(x: BigInt) raises -> BigInt: The integer square root of x. Raises: - Error: If x is negative. + ValueError: If x is negative. """ return sqrt(x) diff --git a/src/decimo/bigint/number_theory.mojo b/src/decimo/bigint/number_theory.mojo index 2c8c184d..b089e66d 100644 --- a/src/decimo/bigint/number_theory.mojo +++ b/src/decimo/bigint/number_theory.mojo @@ -242,7 +242,8 @@ def mod_pow(base: BigInt, exponent: BigInt, modulus: BigInt) raises -> BigInt: A BigInt in the range [0, modulus). Raises: - If exponent < 0 or modulus <= 0. + ValueError: If the exponent is negative. + ValueError: If the modulus is not positive. """ if exponent.is_negative(): raise ValueError( @@ -295,6 +296,10 @@ def mod_pow(base: BigInt, exponent: Int, modulus: BigInt) raises -> BigInt: Returns: A BigInt in the range [0, modulus). + + Raises: + ValueError: If the exponent is negative. + ValueError: If the modulus is not positive. """ return mod_pow(base, BigInt(exponent), modulus) @@ -319,7 +324,8 @@ def mod_inverse(a: BigInt, modulus: BigInt) raises -> BigInt: The modular inverse, in [0, modulus). Raises: - If modulus <= 0 or the inverse does not exist (gcd(a, modulus) != 1). + ValueError: If the modulus is not positive. + ValueError: If the modular inverse does not exist (gcd != 1). """ if not modulus.is_positive(): raise ValueError( diff --git a/src/decimo/bigint10/arithmetics.mojo b/src/decimo/bigint10/arithmetics.mojo index ed69548b..0da10b6a 100644 --- a/src/decimo/bigint10/arithmetics.mojo +++ b/src/decimo/bigint10/arithmetics.mojo @@ -208,8 +208,7 @@ def floor_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: The quotient of x1 / x2, rounded toward negative infinity. Raises: - DecimoError: If `decimo.biguint.arithmetics.floor_divide()` fails. - DecimoError: If `decimo.biguint.arithmetics.ceil_divide()` fails. + ZeroDivisionError: If the divisor is zero. """ # For floor division, the sign rules are: @@ -260,7 +259,7 @@ def truncate_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: The quotient of x1 / x2, truncated toward zero. Raises: - DecimoError: If `decimo.biguint.arithmetics.floor_divide()` fails. + ZeroDivisionError: If the divisor is zero. """ var magnitude: BigUInt try: @@ -289,8 +288,7 @@ def floor_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: The remainder of x1 being divided by x2, with the same sign as x2. Raises: - DecimoError: If `decimo.biguint.arithmetics.floor_modulo()` fails. - DecimoError: If `decimo.biguint.arithmetics.ceil_modulo()` fails. + ZeroDivisionError: If the divisor is zero. """ var magnitude: BigUInt @@ -337,7 +335,7 @@ def truncate_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: The remainder of x1 being divided by x2, with the same sign as x1. Raises: - DecimoError: If `decimo.biguint.arithmetics.floor_modulo()` fails. + ZeroDivisionError: If the divisor is zero. """ var magnitude: BigUInt try: diff --git a/src/decimo/bigint10/bigint10.mojo b/src/decimo/bigint10/bigint10.mojo index 89d45572..11e1f3d3 100644 --- a/src/decimo/bigint10/bigint10.mojo +++ b/src/decimo/bigint10/bigint10.mojo @@ -111,6 +111,9 @@ struct BigInt10( The words are stored in little-endian order. sign: The sign of the BigInt10. + Raises: + ConversionError: If any word exceeds 999_999_999. + Notes: This is equal to `BigInt10.from_list()`. """ @@ -153,6 +156,9 @@ struct BigInt10( Args: value: The string representation of the integer. + + Raises: + ConversionError: If the string cannot be parsed. """ try: self = Self.from_string(value) @@ -193,6 +199,9 @@ struct BigInt10( Args: py: The Python integer object to convert from. + + Raises: + ConversionError: If the Python object cannot be converted. """ self = Self.from_python_int(py) @@ -218,7 +227,7 @@ struct BigInt10( sign: The sign of the BigInt10. Raises: - Error: If any word is larger than `999_999_999`. + ConversionError: If any word exceeds 999_999_999. Returns: The BigInt10 representation of the list of UInt32 words. @@ -250,6 +259,9 @@ struct BigInt10( Returns: A new `BigInt10` from the given words. + + Raises: + ValueError: If any word exceeds 999_999_999. """ var list_of_words = List[UInt32](capacity=len(words)) @@ -345,8 +357,22 @@ struct BigInt10( Returns: The BigInt10 representation of the string. + + Raises: + ConversionError: If the string cannot be parsed. """ - _tuple = decimo.str.parse_numeric_string(value) + try: + _tuple = decimo.str.parse_numeric_string(value) + except e: + raise ConversionError( + function="BigInt10.from_string(value: String)", + message=( + 'The input value "' + + value + + '" cannot be parsed as an integer.\n' + + String(e) + ), + ) var ref coef: List[UInt8] = _tuple[0] var sign: Bool = _tuple[2] @@ -369,8 +395,7 @@ struct BigInt10( The BigInt10 representation of the Python integer. Raises: - Error: If the conversion from Python int to string fails, or if - the string cannot be parsed as a valid integer. + ConversionError: If the conversion from Python int fails. Examples: ```mojo @@ -413,6 +438,9 @@ struct BigInt10( Returns: The `Int` representation of this value. + + Raises: + OverflowError: If the number exceeds the size of Int. """ return self.to_int() @@ -450,7 +478,7 @@ struct BigInt10( The number as Int. Raises: - Error: If the number is too large or too small to fit in Int. + OverflowError: If the number exceeds the size of Int. """ # 2^63-1 = 9_223_372_036_854_775_807 @@ -615,6 +643,9 @@ struct BigInt10( Returns: The floor division quotient. + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.bigint10.arithmetics.floor_divide(self, other) @@ -634,6 +665,9 @@ struct BigInt10( Returns: The remainder of the floor division. + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.bigint10.arithmetics.floor_modulo(self, other) @@ -653,6 +687,9 @@ struct BigInt10( Returns: The result of raising to the given power. + + Raises: + OverflowError: If the exponent is too large. """ return self.power(exponent) @@ -1032,6 +1069,9 @@ struct BigInt10( Returns: The result of raising to the given power. + + Raises: + ValueError: If the exponent is negative or too large. """ var magnitude = self.magnitude.power(exponent) var sign = False @@ -1048,6 +1088,10 @@ struct BigInt10( Returns: The result of raising to the given power. + + Raises: + OverflowError: If the exponent is too large. + ValueError: If the exponent is negative or too large. """ if exponent > Self(BigUInt(raw_words=[0, 1]), sign=False): raise OverflowError( diff --git a/src/decimo/biguint/arithmetics.mojo b/src/decimo/biguint/arithmetics.mojo index 9005502a..3055e5fd 100644 --- a/src/decimo/biguint/arithmetics.mojo +++ b/src/decimo/biguint/arithmetics.mojo @@ -98,7 +98,7 @@ def negative(x: BigUInt) raises -> BigUInt: x: The BigUInt value to compute the negative of. Raises: - Error: If x is not zero, as negative of non-zero unsigned integer is undefined. + OverflowError: If the number is non-zero. Returns: A new BigUInt containing the negative of x. @@ -486,7 +486,7 @@ def subtract(x: BigUInt, y: BigUInt) raises -> BigUInt: y: The second unsigned integer (subtrahend). Raises: - Error: If y is greater than x, resulting in an underflow. + OverflowError: If x < y (result would be negative). Returns: The result of subtracting y from x. @@ -681,6 +681,9 @@ def subtract_inplace(mut x: BigUInt, y: BigUInt) raises -> None: Args: x: The `BigUInt` minuend, modified in place. y: The `BigUInt` subtrahend. + + Raises: + OverflowError: If x < y (result would be negative). """ # If the subtrahend is zero, return the minuend @@ -1938,7 +1941,7 @@ def floor_divide(x: BigUInt, y: BigUInt) raises -> BigUInt: The quotient of x / y, truncated toward zero. Raises: - ValueError: If the divisor is zero. + ZeroDivisionError: If the divisor is zero. Notes: It is equal to truncated division for positive numbers. @@ -2059,7 +2062,7 @@ def floor_divide_school(x: BigUInt, y: BigUInt) raises -> BigUInt: The quotient of x // y. Raises: - Error: If the y is zero. + ZeroDivisionError: If the divisor is zero. """ # Because the Burnikel-Ziegler division algorithm will fall back to this @@ -3123,6 +3126,9 @@ def floor_divide_three_by_two_uint32( (2) the most significant word of the remainder (as UInt32) (3) the least significant word of the remainder (as UInt32). + Raises: + ValueError: If b1 < 500_000_000. + Notes: a = a2 * BASE^2 + a1 * BASE + a0. @@ -3180,6 +3186,9 @@ def floor_divide_four_by_two_uint32( (2) the least significant word of the quotient (as UInt32) (3) the most significant word of the remainder (as UInt32) (4) the least significant word of the remainder (as UInt32). + + Raises: + ValueError: If b1 < 500_000_000 or a >= b * 10^18. """ if b1 < 500_000_000: @@ -3228,6 +3237,9 @@ def truncate_divide(x1: BigUInt, x2: BigUInt) raises -> BigUInt: Returns: The quotient of `x1` divided by `x2`. + + Raises: + ZeroDivisionError: If the divisor is zero. """ return floor_divide(x1, x2) @@ -3243,7 +3255,7 @@ def ceil_divide(x1: BigUInt, x2: BigUInt) raises -> BigUInt: The quotient of x1 / x2, rounded up. Raises: - ValueError: If the divisor is zero. + ZeroDivisionError: If the divisor is zero. """ # CASE: Division by zero @@ -3277,8 +3289,6 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: Raises: ZeroDivisionError: If the divisor is zero. - Error: If `floor_divide()` raises an error. - Error: If `subtract()` raises an error. Notes: It is equal to floored modulo for positive numbers. @@ -3422,8 +3432,7 @@ def floor_divide_modulo( The quotient of x1 / x2, truncated toward zero and the remainder. Raises: - Error: If `floor_divide()` raises an error. - Error: If `subtract()` raises an error. + ZeroDivisionError: If the divisor is zero. Notes: It is equal to truncated division for positive numbers. @@ -3615,7 +3624,7 @@ def power_of_10(n: Int) raises -> BigUInt: A BigUInt representing 10 raised to the power of n. Raises: - DecimoError: If n is negative. + ValueError: If n is negative. """ if n < 0: raise ValueError( diff --git a/src/decimo/biguint/biguint.mojo b/src/decimo/biguint/biguint.mojo index d74c3ee1..e6b6efaf 100644 --- a/src/decimo/biguint/biguint.mojo +++ b/src/decimo/biguint/biguint.mojo @@ -196,6 +196,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Each UInt32 word represents digits ranging from 0 to 10^9 - 1. The words are stored in little-endian order. + Raises: + ConversionError: If any word exceeds 999_999_999. + Notes: This is equal to `BigUInt.from_list()`. @@ -242,7 +245,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): value: The integer to initialize the `BigUInt` from. Raises: - Error: Calling `BigUInt.from_int()`. + ConversionError: If the value is negative. """ try: self = Self.from_int(value) @@ -273,7 +276,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): If False, the sign is considered. Raises: - Error: If an error occurs in `from_string()`. + ConversionError: If the string cannot be parsed. Notes: This is equal to `BigUInt.from_string()`. @@ -310,7 +313,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): The words are stored in little-endian order. Raises: - Error: If any word is larger than `999_999_999`. + OverflowError: If any word exceeds 999_999_999. Returns: The BigUInt representation of the list of UInt32 words. @@ -364,7 +367,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): The words are stored in little-endian order. Raises: - Error: If any word is larger than `999_999_999`. + OverflowError: If any word exceeds 999_999_999. Notes: @@ -452,7 +455,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): The BigUInt representation of the integer value. Raises: - Error: If the input value is negative. + OverflowError: If the value is negative. """ if value == 0: return Self() @@ -765,7 +768,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): The number as Int. Raises: - Error: If to_int() raises an error. + ConversionError: If the number exceeds the size of Int. """ try: return self.to_int() @@ -844,7 +847,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): The number as UInt64. Raises: - Error: If the number exceeds the size of UInt64. + OverflowError: If the number exceeds the size of UInt64. """ if self.is_uint64_overflow(): raise OverflowError( @@ -1081,6 +1084,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The negated value. + + Raises: + OverflowError: If the number is non-zero (negative of unsigned is undefined). """ return decimo.biguint.arithmetics.negative(self) @@ -1126,6 +1132,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The difference. + + Raises: + OverflowError: If the result would be negative. """ try: return decimo.biguint.arithmetics.subtract(self, other) @@ -1153,6 +1162,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The quotient. + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.biguint.arithmetics.floor_divide(self, other) @@ -1172,6 +1184,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The quotient rounded up. + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.biguint.arithmetics.ceil_divide(self, other) @@ -1191,6 +1206,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The remainder. + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.biguint.arithmetics.floor_modulo(self, other) @@ -1210,6 +1228,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: A tuple of (quotient, remainder). + + Raises: + ZeroDivisionError: If the divisor is zero. """ try: return decimo.biguint.arithmetics.floor_divide_modulo(self, other) @@ -1225,6 +1246,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The power `self` raised to `exponent`. + + Raises: + ValueError: If the exponent is too large. """ try: return self.power(exponent) @@ -1244,6 +1268,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The power `self` raised to `exponent`. + + Raises: + ValueError: If the exponent is negative or too large. """ try: return self.power(exponent) @@ -1281,6 +1308,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The difference. + + Raises: + OverflowError: If the result would be negative. """ return decimo.biguint.arithmetics.subtract(other, self) @@ -1305,6 +1335,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The quotient. + + Raises: + ZeroDivisionError: If the divisor is zero. """ return decimo.biguint.arithmetics.floor_divide(other, self) @@ -1317,6 +1350,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The remainder. + + Raises: + ZeroDivisionError: If the divisor is zero. """ return decimo.biguint.arithmetics.floor_modulo(other, self) @@ -1329,6 +1365,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: A tuple of (quotient, remainder). + + Raises: + ZeroDivisionError: If the divisor is zero. """ return decimo.biguint.arithmetics.floor_divide_modulo(other, self) @@ -1341,6 +1380,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The power `base` raised to `self`. + + Raises: + ValueError: If the exponent is too large. """ return base.power(self) @@ -1368,6 +1410,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Args: other: The operand to subtract. + + Raises: + OverflowError: If the result would be negative. """ decimo.biguint.arithmetics.subtract_inplace(self, other) @@ -1386,6 +1431,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Args: other: The divisor. + + Raises: + ZeroDivisionError: If the divisor is zero. """ self = decimo.biguint.arithmetics.floor_divide(self, other) @@ -1395,6 +1443,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Args: other: The divisor. + + Raises: + ZeroDivisionError: If the divisor is zero. """ self = decimo.biguint.arithmetics.floor_modulo(self, other) @@ -1673,8 +1724,8 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): A `BigUInt` representing `self` raised to the power of `exponent`. Raises: - Error: If the exponent is negative. - Error: If the exponent is too large, e.g., larger than 1_000_000_000. + ValueError: If the exponent is negative. + ValueError: If the exponent is too large, e.g., larger than 1_000_000_000. """ if exponent < 0: raise ValueError( diff --git a/src/decimo/decimal128/arithmetics.mojo b/src/decimo/decimal128/arithmetics.mojo index de3e2fb3..b8b5182d 100644 --- a/src/decimo/decimal128/arithmetics.mojo +++ b/src/decimo/decimal128/arithmetics.mojo @@ -57,7 +57,7 @@ def add(x1: Decimal128, x2: Decimal128) raises -> Decimal128: A new Decimal128 containing the sum of x1 and x2. Raises: - Error: If the operation would overflow. + OverflowError: If the result overflows Decimal128 capacity. """ var x1_coef = x1.coefficient() var x2_coef = x2.coefficient() @@ -367,6 +367,9 @@ def subtract(x1: Decimal128, x2: Decimal128) raises -> Decimal128: Returns: A new Decimal128 containing the difference. + Raises: + OverflowError: If the result overflows Decimal128 capacity. + Notes: ------ This method is implemented using the existing `__add__()` and `__neg__()` methods. @@ -439,6 +442,9 @@ def multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: Returns: A new Decimal128 containing the product of x1 and x2. + + Raises: + OverflowError: If the product overflows Decimal128 capacity. """ var x1_coef = x1.coefficient() @@ -804,7 +810,8 @@ def divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: A new Decimal128 containing the result of x1 / x2. Raises: - Error: If x2 is zero. + ZeroDivisionError: If the divisor is zero. + OverflowError: If the result overflows Decimal128 capacity. """ # Treatment for special cases @@ -1272,6 +1279,10 @@ def truncate_divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: Returns: A new Decimal128 containing the integral part of x1 / x2. + + Raises: + ZeroDivisionError: If the divisor is zero. + OverflowError: If the result overflows. """ try: return divide(x1, x2).round(0, RoundingMode.down()) @@ -1289,6 +1300,10 @@ def modulo(x1: Decimal128, x2: Decimal128) raises -> Decimal128: Returns: A new Decimal128 containing the remainder of x1 / x2. + + Raises: + ZeroDivisionError: If the divisor is zero. + OverflowError: If the result overflows. """ try: return x1 - (truncate_divide(x1, x2) * x2) diff --git a/src/decimo/decimal128/constants.mojo b/src/decimo/decimal128/constants.mojo index a8e2cbca..97d5ad11 100644 --- a/src/decimo/decimal128/constants.mojo +++ b/src/decimo/decimal128/constants.mojo @@ -399,7 +399,7 @@ def N_DIVIDE_NEXT(n: Int) raises -> Decimal128: A Decimal128 representing the value of n/(n+1). Raises: - Error: If n is outside the range [1, 20]. + ValueError: If n is not between 1 and 20. """ if n == 1: # 1/2 = 0.5 diff --git a/src/decimo/decimal128/decimal128.mojo b/src/decimo/decimal128/decimal128.mojo index 5154693f..69406d7a 100644 --- a/src/decimo/decimal128/decimal128.mojo +++ b/src/decimo/decimal128/decimal128.mojo @@ -291,6 +291,9 @@ struct Decimal128( high: The most significant 32 bits of the coefficient. scale: The number of decimal places (0-28). sign: `True` if the number is negative, `False` otherwise. + + Raises: + ConversionError: If the initialization fails. """ try: @@ -318,6 +321,9 @@ struct Decimal128( Args: value: The integer value to convert. scale: The number of decimal places (0-28). + + Raises: + ConversionError: If the value cannot be represented. """ try: self = Decimal128.from_int(value, scale) @@ -334,6 +340,9 @@ struct Decimal128( Args: value: The string representation of the decimal number. + + Raises: + ConversionError: If the string cannot be parsed. """ try: self = Decimal128.from_string(value) @@ -350,6 +359,9 @@ struct Decimal128( Args: value: The floating-point value to convert. + + Raises: + ConversionError: If the float cannot be converted. """ try: @@ -386,7 +398,7 @@ struct Decimal128( A Decimal128 instance with the given components. Raises: - Error: If the scale is greater than MAX_SCALE. + ValueError: If the scale exceeds 28. """ if scale > UInt32(Self.MAX_SCALE): @@ -494,7 +506,7 @@ struct Decimal128( The Decimal128 representation of the integer. Raises: - Error: If the scale is greater than MAX_SCALE. + ValueError: If the scale exceeds 28. Notes: @@ -551,8 +563,8 @@ struct Decimal128( The Decimal128 representation of the UInt128 value. Raises: - Error: If the most significant word of the UInt128 is not zero. - Error: If the scale is greater than MAX_SCALE. + ValueError: If the value does not fit in 96 bits. + ValueError: If the scale exceeds 28. """ if value >> 96 != 0: @@ -588,7 +600,8 @@ struct Decimal128( The Decimal128 representation of the string. Raises: - Error: If an error occurs during the conversion, forward the error. + ValueError: If the string contains invalid characters. + OverflowError: If the exponent is too large. Notes: @@ -890,8 +903,8 @@ struct Decimal128( The Decimal128 representation of the floating-point value. Raises: - Error: If the input is too large to be transformed into Decimal128. - Error: If the input is infinity or NaN. + OverflowError: If the value is too large for Decimal128. + ValueError: If the value is infinity or NaN. Example: ```mojo @@ -1037,6 +1050,9 @@ struct Decimal128( Returns: The `Int` representation. + + Raises: + ConversionError: If the conversion fails. """ return self.to_int() @@ -1117,7 +1133,7 @@ struct Decimal128( The signed integral part of the Decimal128. Raises: - Error: If the Decimal128 is too large to fit in Int. + ConversionError: If the conversion fails. """ try: return Int(self.to_int64()) @@ -1136,7 +1152,7 @@ struct Decimal128( The signed integral part of the Decimal128. Raises: - Error: If the Decimal128 is too large to fit in Int64. + OverflowError: If the value exceeds the Int64 range. """ var result = self.to_int128() @@ -1985,7 +2001,7 @@ struct Decimal128( A new Decimal128 with increased precision. Raises: - Error: If the level is less than 0. + ValueError: If precision_diff is negative. Examples: ```mojo diff --git a/src/decimo/decimal128/exponential.mojo b/src/decimo/decimal128/exponential.mojo index a8dbf482..8a0380f9 100644 --- a/src/decimo/decimal128/exponential.mojo +++ b/src/decimo/decimal128/exponential.mojo @@ -44,11 +44,10 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: A new Decimal128 containing the result of base^exponent. Raises: - Error: If the base is negative or the exponent is negative and not an integer. - Error: If an error occurs in calling the power() function with an integer exponent. - Error: If an error occurs in calling the sqrt() function with a Decimal128 exponent. - Error: If an error occurs in calling the ln() function with a Decimal128 base. - Error: If an error occurs in calling the exp() function with a Decimal128 exponent. + ValueError: If the base is negative with a non-integer exponent. + ZeroDivisionError: If a reciprocal power is undefined, such as + when evaluating `0^-0.5`. + OverflowError: If the result overflows. """ # CASE: If the exponent is integer @@ -109,6 +108,9 @@ def power(base: Decimal128, exponent: Int) raises -> Decimal128: Returns: A new Decimal128 containing the result. + + Raises: + ValueError: If the base is zero and exponent is negative. """ # Special cases @@ -173,8 +175,7 @@ def root(x: Decimal128, n: Int) raises -> Decimal128: A new Decimal128 containing the n-th root of x. Raises: - Error: If x is negative and n is even. - Error: If n is zero or negative. + ValueError: If n is non-positive or the input is invalid. """ # var t0 = time.perf_counter_ns() @@ -352,7 +353,7 @@ def sqrt(x: Decimal128) raises -> Decimal128: A new Decimal128 containing the square root of x. Raises: - Error: If x is negative. + ValueError: If x is negative. """ # Special cases if x.is_negative(): @@ -478,7 +479,7 @@ def exp(x: Decimal128) raises -> Decimal128: A Decimal128 approximation of e^x. Raises: - Error: If x is greater than 66.54. + OverflowError: If x is too large (> 66.54). Notes: Because ln(2^96-1) ~= 66.54212933375474970405428366, @@ -688,7 +689,7 @@ def ln(x: Decimal128) raises -> Decimal128: A Decimal128 approximation of ln(x). Raises: - Error: If x is less than or equal to zero. + ValueError: If x is non-positive. Notes: This implementation uses range reduction to improve accuracy and performance. @@ -953,8 +954,7 @@ def log(x: Decimal128, base: Decimal128) raises -> Decimal128: A Decimal128 approximation of log_base(x). Raises: - Error: If x is less than or equal to zero. - Error: If base is less than or equal to zero or equal to 1. + ValueError: If x is non-positive, base is non-positive, or base is 1. Notes: @@ -1012,7 +1012,7 @@ def log10(x: Decimal128) raises -> Decimal128: A Decimal128 approximation of log10(x). Raises: - Error: If x is less than or equal to zero. + ValueError: If x is non-positive. Notes: This implementation uses the identity log10(x) = ln(x) / ln(10). diff --git a/src/decimo/decimal128/rounding.mojo b/src/decimo/decimal128/rounding.mojo index 61a5dba1..fe37968d 100644 --- a/src/decimo/decimal128/rounding.mojo +++ b/src/decimo/decimal128/rounding.mojo @@ -65,6 +65,9 @@ def round( Returns: A new Decimal128 rounded to the specified number of decimal places. + + Raises: + OverflowError: If rounding causes the result to exceed Decimal128 capacity. """ # Number of decimal places of the number is equal to the scale of the number diff --git a/src/decimo/decimal128/special.mojo b/src/decimo/decimal128/special.mojo index 0c6556bf..4ba89e10 100644 --- a/src/decimo/decimal128/special.mojo +++ b/src/decimo/decimal128/special.mojo @@ -32,6 +32,10 @@ def factorial(n: Int) raises -> Decimal128: Returns: The factorial of n. + Raises: + ValueError: If n is negative. + OverflowError: If n > 27. + Notes: 27! is the largest factorial that can be represented by Decimal128. @@ -138,6 +142,9 @@ def factorial_reciprocal(n: Int) raises -> Decimal128: Returns: The reciprocal of factorial of n (1/n!). + Raises: + ValueError: If n is negative. + Notes: This function is optimized for Taylor series calculations. The function uses pre-computed values for speed. diff --git a/src/decimo/str.mojo b/src/decimo/str.mojo index a00a3421..d43f38a6 100644 --- a/src/decimo/str.mojo +++ b/src/decimo/str.mojo @@ -105,6 +105,10 @@ def parse_numeric_string( parse_numeric_string("-123") -> ([1,2,3], 0, True) ``` End of examples. + + Raises: + ValueError: If the string is empty, contains invalid characters, or + has malformed numeric syntax. """ # [Mojo Miji] diff --git a/src/decimo/tests.mojo b/src/decimo/tests.mojo index 7dc17478..76f553c7 100644 --- a/src/decimo/tests.mojo +++ b/src/decimo/tests.mojo @@ -299,6 +299,9 @@ def parse_file(file_path: String) raises -> TOMLDocument: Returns: A `TOMLDocument` containing the parsed TOML data. + + Raises: + ValueError: If the TOML file cannot be parsed. """ try: return parse_toml_file(file_path) diff --git a/src/decimo/toml/parser.mojo b/src/decimo/toml/parser.mojo index 5bc99c1c..5392190e 100644 --- a/src/decimo/toml/parser.mojo +++ b/src/decimo/toml/parser.mojo @@ -460,6 +460,9 @@ def _set_value( Navigates through `path` (creating intermediate tables as needed), then sets `key = value` in the target table. Raises on duplicate non-table keys. + + Raises: + ValueError: If a duplicate key is found or a key exists but is not a table. """ if len(path) == 0: # Set directly in root @@ -522,7 +525,11 @@ def _set_value( def _ensure_table_path( mut root: Dict[String, TOMLValue], path: List[String] ) raises: - """Ensure all tables along `path` exist in `root`.""" + """Ensure all tables along `path` exist in `root`. + + Raises: + ValueError: If a key exists but is not a table. + """ if len(path) == 0: return @@ -563,7 +570,11 @@ def _ensure_table_path( def _append_array_of_tables( mut root: Dict[String, TOMLValue], path: List[String] ) raises: - """Append a new empty table to the array-of-tables at `path`.""" + """Append a new empty table to the array-of-tables at `path`. + + Raises: + ValueError: If the path is empty or a key cannot be redefined as array of tables. + """ if len(path) == 0: raise ValueError( message="Array of tables path cannot be empty", @@ -686,6 +697,9 @@ struct TOMLParser: Returns a list of key parts. Accepts both KEY and STRING tokens as key components. + + Raises: + ValueError: If a key token is expected but not found. """ var parts = List[String]() @@ -826,7 +840,11 @@ struct TOMLParser: # ---- inline tables --------------------------------------------------- def _parse_inline_table(mut self) raises -> TOMLValue: - """Parse an inline table { key = value, ... } (opening { consumed).""" + """Parse an inline table { key = value, ... } (opening { consumed). + + Raises: + ValueError: If a syntax error or duplicate key is found. + """ var table = Dict[String, TOMLValue]() if self._tok().type == TokenType.INLINE_TABLE_END: @@ -882,7 +900,11 @@ struct TOMLParser: # ---- table header parsing -------------------------------------------- def _parse_table_header(mut self) raises -> List[String]: - """Parse [a.b.c] and return the path. Opening [ already consumed.""" + """Parse [a.b.c] and return the path. Opening [ already consumed. + + Raises: + ValueError: If the closing ']' is missing. + """ var path = self._parse_key_path() # Expect closing ] @@ -897,7 +919,11 @@ struct TOMLParser: return path^ def _parse_array_of_tables_header(mut self) raises -> List[String]: - """Parse [[a.b.c]] and return the path. Opening [[ already consumed.""" + """Parse [[a.b.c]] and return the path. Opening [[ already consumed. + + Raises: + ValueError: If the closing ']]' is missing. + """ var path = self._parse_key_path() # Expect closing ]] From 5aa3550200947f71b1989f13d116dd37faaa1ade Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Fri, 10 Apr 2026 21:30:51 +0200 Subject: [PATCH 10/17] [error] Use shortened and relative paths for files that raise errors in error messages to ensure privacy at compilation (#200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates Decimo’s error reporting to avoid exposing absolute file system paths in error messages by shortening file paths captured via `call_location()`. **Changes:** - Added a `_shorten_path()` helper to convert absolute paths into `./src/...`, `./tests/...`, `./benches/...`, or filename-only paths. - Updated `DecimoError.__init__` to store the shortened path instead of the raw `call_location()` path. --- src/decimo/errors.mojo | 61 +++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/src/decimo/errors.mojo b/src/decimo/errors.mojo index 0ba4c596..d73ccc3a 100644 --- a/src/decimo/errors.mojo +++ b/src/decimo/errors.mojo @@ -21,13 +21,15 @@ The error messages follow the Python traceback format as closely as possible: ``` Traceback (most recent call last): - File "/path/to/file.mojo", line 42, in my_function + File "./src/decimo/bigint/bigint.mojo", line 42, in my_function ValueError: description of what went wrong ``` File name and line number are automatically captured at the raise site using -`call_location()`. Function name must be provided manually since Mojo does not -have a built-in way to get the current function name at runtime. +`call_location()`. The absolute path is automatically shortened to a relative +path (e.g. ``./src/...``, ``./tests/...``) for readability and privacy. +Function name must be provided manually since Mojo does not have a built-in way +to get the current function name at runtime. """ from std.reflection import call_location @@ -80,6 +82,46 @@ comptime RuntimeError = DecimoError[error_type="RuntimeError"] failures, missing native libraries).""" +def _shorten_path(full_path: String) -> String: + """Shorten an absolute file path to a relative path. + + Looks for known directory markers (``src/``, ``tests/``, ``benches/``) and + returns a ``./``-prefixed relative path from the rightmost marker found. + If no marker is found, returns just the filename. + + Uses ``rfind`` (reverse search) to handle paths that contain a marker more + than once, e.g. ``/home/user/src/projects/decimo/src/decimo/bigint.mojo`` + correctly shortens to ``./src/decimo/bigint.mojo``. When more than one + marker type appears, the rightmost position wins to produce the shortest + possible relative path. + + Args: + full_path: The absolute file path to shorten. + + Returns: + A shortened relative path string. + """ + var src_idx = full_path.rfind("src/") + var tests_idx = full_path.rfind("tests/") + var benches_idx = full_path.rfind("benches/") + + # We need to handle the cases like the following paths: + # .../tests/.../src/... + # .../benches/.../src/... + var idx = src_idx + if tests_idx > idx: + idx = tests_idx + if benches_idx > idx: + idx = benches_idx + + if idx >= 0: + return "./" + String(full_path[byte=idx:]) + var last_slash = full_path.rfind("/") + if last_slash >= 0: + return String(full_path[byte = last_slash + 1 :]) + return full_path + + struct DecimoError[error_type: StringLiteral = "DecimoError"](Writable): """Base type for all Decimo errors. @@ -87,11 +129,12 @@ struct DecimoError[error_type: StringLiteral = "DecimoError"](Writable): ``` Traceback (most recent call last): - File "/path/to/file.mojo", line 42, in my_function + File "./src/decimo/bigint/bigint.mojo", line 42, in my_function ValueError: description of what went wrong ``` File name and line number are automatically captured at the raise site. + The absolute path is shortened to a relative path for readability. Function name must be provided manually since Mojo does not yet support runtime introspection of the current function name. @@ -127,8 +170,8 @@ struct DecimoError[error_type: StringLiteral = "DecimoError"](Writable): function: The function name where the error occurred. previous_error: An optional previous error that caused this one. """ - var loc = call_location() - self.file = String(loc.file_name) + var loc = call_location() # Comptime evaluated + self.file = _shorten_path(String(loc.file_name)) self.line = loc.line self.function = function self.message = message @@ -144,7 +187,7 @@ struct DecimoError[error_type: StringLiteral = "DecimoError"](Writable): ``` Traceback (most recent call last): - File "/path/to/file.mojo", line 42, in my_function + File "./src/decimo/bigint/bigint.mojo", line 42, in my_function ValueError: description of what went wrong ``` @@ -152,13 +195,13 @@ struct DecimoError[error_type: StringLiteral = "DecimoError"](Writable): ``` Traceback (most recent call last): - File "/path/to/inner.mojo", line 10 + File "./src/decimo/bigint/bigint.mojo", line 10 ValueError: inner error message The above exception was the direct cause of the following exception: Traceback (most recent call last): - File "/path/to/outer.mojo", line 20, in outer_function + File "./src/decimo/bigint/bigint.mojo", line 20, in outer_function DecimoError: outer error message ``` From 8ee0b4ce1402b311dce064ea6a3bd4382faea61f Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Fri, 10 Apr 2026 22:51:13 +0200 Subject: [PATCH 11/17] [cli] Allow negative numbers + add range, value name, argument group validation + custom usage line (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR enhances the `decimo` CLI calculator’s argument parsing and help/usage UX by leveraging newer ArgMojo features, and updates project documentation/plans accordingly. **Changes:** - Add ArgMojo metadata to CLI options/flags (value names, numeric range validation, help groups) and set a custom usage line. - Enable ArgMojo negative-number passthrough for positional expressions. - Update docs/plans to reflect ArgMojo v0.5.0 integration and adjust pixi tasks for fetching ArgMojo source. --- docs/plans/cli_calculator.md | 193 +++++++++++++++-------------------- docs/readme_unreleased.md | 6 +- pixi.toml | 4 +- src/cli/main.mojo | 14 +++ 4 files changed, 106 insertions(+), 111 deletions(-) diff --git a/docs/plans/cli_calculator.md b/docs/plans/cli_calculator.md index 129d6a76..82240f54 100644 --- a/docs/plans/cli_calculator.md +++ b/docs/plans/cli_calculator.md @@ -22,18 +22,19 @@ Rows are sorted by implementation priority for `decimo` (top = implement first). | 3 | Large integers (arbitrary) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | 1 | | 4 | Pipeline/Batch scripting | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 1 | | 5 | Built-in math functions | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | 2 | -| 6 | Interactive REPL | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | 3 | -| 7 | Variables/State | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | 3 | -| 8 | Unit conversion | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | 4 | -| 9 | Matrix/Linear algebra | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ | 4 | -| 10 | Symbolic computation | ✗ | ✗ | ✗ | △ | ✗ | ✗ | ✗ | ✗ | 4 | +| 6 | Interactive REPL | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | 4 | +| 7 | Variables/State | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | 4 | +| 8 | Unit conversion | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | 5 | +| 9 | Matrix/Linear algebra | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ | 5 | +| 10 | Symbolic computation | ✗ | ✗ | ✗ | △ | ✗ | ✗ | ✗ | ✗ | 5 | **Priority rationale:** 1. **Basic arithmetic + High-precision + Large integers + Pipeline** (Phase 1) — These are the raison d'être of `decimo`. Decimo already provides arbitrary-precision `BigDecimal`; wiring up tokenizer → parser → evaluator gives immediate value. Pipeline/batch is nearly free once one-shot works (just loop over stdin lines). 2. **Built-in math functions** (Phase 2) — `sqrt`, `ln`, `exp`, `sin`, `cos`, `tan`, `root` already exist in the Decimo API. Adding them mostly means extending the tokenizer/parser to recognize function names. -3. **Interactive REPL + Variables/State** (Phase 3) — Valuable for exploration, but requires a read-eval-print loop, `ans` tracking, named variable storage and session-level precision management. More engineering effort, less urgency. -4. **Unit conversion / Matrix / Symbolic** (Phase 4) — Out of scope. `decimo` is a numerical calculator, not a CAS or unit library. These can be revisited if there is demand. +3. **Polish & ArgMojo integration** (Phase 3) — Error diagnostics, edge-case handling, and exploiting ArgMojo v0.5.0 features (shell completions, argument groups, numeric range validation, etc.). Mostly CLI UX refinement. +4. **Interactive REPL + Subcommands** (Phase 4) — Requires a read-eval-print loop, `ans` tracking, named variable storage, session-level precision management, and CLI restructuring with subcommands. More engineering effort, less urgency. +5. **Future enhancements** (Phase 5) — CJK full-width detection, response files, unit conversion, matrix, symbolic. Out of scope for now. ## Usage Design @@ -161,15 +162,23 @@ Best for: interactive exploration, multi-step calculations, experimenting with p ### Layer 1: ArgMojo — CLI Argument Parsing -ArgMojo handles the outer CLI structure. No modifications to ArgMojo are needed. +ArgMojo handles the outer CLI structure via its struct-based declarative API (`Parsable` trait). ```mojo -var cmd = Command("decimo", "Arbitrary-precision CLI calculator.", version="0.1.0") -cmd.add_arg(Arg("expr", help="Math expression").positional().required()) -cmd.add_arg(Arg("precision", help="Decimal precision").long("precision").short("p").default("50")) -cmd.add_arg(Arg("sci", help="Scientific notation").long("sci").flag()) -cmd.add_arg(Arg("eng", help="Engineering notation").long("eng").flag()) -cmd.add_arg(Arg("pad", help="Pad trailing zeros to precision").long("pad-to-precision").flag()) +from argmojo import Parsable, Option, Flag, Positional, Command + +struct DecimoArgs(Parsable): + var expr: Positional[String, help="Math expression to evaluate", required=True] + var precision: Option[Int, long="precision", short="p", help="Number of significant digits", + default="50", value_name="N", has_range=True, range_min=1, range_max=1000000000, group="Computation"] + var scientific: Flag[long="scientific", short="s", help="Output in scientific notation", group="Formatting"] + var engineering: Flag[long="engineering", short="e", help="Output in engineering notation", group="Formatting"] + var pad: Flag[long="pad", short="P", help="Pad trailing zeros to the specified precision", group="Formatting"] + var delimiter: Option[String, long="delimiter", short="d", help="Digit-group separator", + default="", value_name="CHAR", group="Formatting"] + var rounding_mode: Option[String, long="rounding-mode", short="r", help="Rounding mode", + default="half-even", choices="half-even,half-up,half-down,up,down,ceiling,floor", + value_name="MODE", group="Computation"] ``` ### Layer 2: Tokenizer — Lexical Analysis @@ -290,34 +299,50 @@ Format the final `BigDecimal` result based on CLI flags: | 2.9 | `--delimiter` / `-d` (digit grouping) | ✓ | Extra feature beyond original plan | | 2.10 | `--rounding-mode` / `-r` | ✓ | 7 modes; extra feature beyond original plan | -### Phase 3: Polish +### Phase 3: Polish & ArgMojo Deep Integration + +> ArgMojo v0.5.0 is installed via pixi. Reference: · [User Manual](https://github.com/forfudan/argmojo/wiki) 1. Error messages: clear diagnostics for malformed expressions (e.g., "Unexpected token '*' at position 5"). 2. Edge cases: division by zero, negative sqrt, overflow, empty expression. -3. Upgrade to ArgMojo v0.2.0 (once available in pixi). See [ArgMojo v0.2.0 Upgrade Tasks](#argmojo-v020-upgrade-tasks) below. -4. Performance: ensure the tokenizer/parser overhead is negligible compared to BigDecimal computation. -5. Documentation and examples in README. -6. Build and distribute as a single binary. - -| # | Task | Status | Notes | -| --- | ---------------------------------------------------------------------- | :----: | --------------------------------------------- | -| 3.1 | Error messages with position info + caret display | ✓ | Colored stderr: red `Error:`, green `^` caret | -| 3.2 | Edge cases (div-by-zero, negative sqrt, empty expression, etc.) | ✓ | 27 error-handling tests | -| 3.3 | ArgMojo v0.2.0 upgrade (`add_tip()`, `allow_negative_numbers()`, etc.) | ? | Blocked: waiting for ArgMojo v0.2.0 in pixi | -| 3.4 | Performance validation | ✗ | No CLI-level benchmarks yet | -| 3.5 | Documentation (user manual for CLI) | ✗ | Will be `docs/user_manual_cli.md` | -| 3.6 | Build and distribute as single binary | ✗ | | - -### Phase 4: Interactive REPL - -1. Read-eval-print loop: read a line from stdin, evaluate, print result, repeat. -2. Custom prompt (`decimo>`). -3. `ans` variable to reference the previous result. -4. Variable assignment: `x = sqrt(2)`, usable in subsequent expressions. -5. Session-level precision: settable via `decimo -p 100` at launch or `:precision 100` command mid-session. -6. Graceful exit: `exit`, `quit`, `Ctrl-D`. -7. Clear error messages without crashing the session (e.g., "Error: division by zero", then continue). -8. History (if Mojo gets readline-like support). +3. Upgrade to ArgMojo v0.5.0 and adopt declarative API. +4. Exploit ArgMojo v0.5.0 features: shell completions, numeric validation, help readability, argument groups. +5. Performance: ensure the tokenizer/parser overhead is negligible compared to BigDecimal computation. +6. Documentation and examples in README (include shell completion setup). +7. Build and distribute as a single binary. + +| # | Task | Status | Notes | +| ---- | --------------------------------------------------------------- | :----: | ------------------------------------------------------------------------------------------------------------------ | +| 3.1 | Error messages with position info + caret display | ✓ | Colored stderr: red `Error:`, green `^` caret | +| 3.2 | Edge cases (div-by-zero, negative sqrt, empty expression, etc.) | ✓ | 27 error-handling tests | +| 3.3 | ArgMojo v0.5.0 declarative API migration | ✓ | `Parsable` struct, `add_tip()`, `mutually_exclusive()`, `choices`, `--version` all working | +| 3.4 | Built-in features (free with ArgMojo v0.5.0) | ✓ | Typo suggestions, `NO_COLOR`, CJK full-width correction, prefix matching, `--` stop marker | +| 3.5 | Shell completion (`--completions bash\|zsh\|fish`) | ✓ | Built-in — zero code; needs documentation in user manual and README | +| 3.6 | `allow_negative_numbers()` to allow pure negative numbers | ✓ | Explicit opt-in in hybrid bridge; `decimo "-3"` works, expressions need quoting or `--` | +| 3.7 | Numeric range on `precision` | ✓ | `has_range=True, range_min=1, range_max=1000000000`; rejects `--precision 0` or `-5` | +| 3.8 | Value names for help readability | ✓ | `--precision `, `--delimiter `, `--rounding-mode ` | +| 3.9 | Argument groups in help output | ✓ | `Computation` and `Formatting` groups in `--help` | +| 3.10 | Custom usage line | ✓ | `Usage: decimo [OPTIONS] ` | +| 3.11 | `Parsable.run()` override | ✗ | Move eval logic into `DecimoArgs.run()` for cleaner separation | +| 3.12 | Performance validation | ✗ | No CLI-level benchmarks yet | +| 3.13 | Documentation (user manual for CLI) | ✗ | `docs/user_manual_cli.md`; include shell completion setup | +| 3.14 | Build and distribute as single binary | ✗ | | +| 3.15 | Allow negative expressions | ✗ | This needs ArgMojo to regard arguments with a hyphen and followed by more than one letter as a positional argument | + +### Phase 4: Interactive REPL & Subcommands + +1. Restructure CLI with subcommands: `decimo eval "expr"` (default), `decimo repl`, `decimo help functions`. +2. Persistent flags (`--precision`, `--scientific`, etc.) across subcommands. +3. Subcommand dispatch via `parse_full()`. +4. No-args + TTY detection → launch REPL directly. +5. Read-eval-print loop: read a line from stdin, evaluate, print result, repeat. +6. Custom prompt (`decimo>`). +7. `ans` variable to reference the previous result. +8. Variable assignment: `x = sqrt(2)`, usable in subsequent expressions. +9. Session-level precision: settable via `decimo -p 100` at launch or `:precision 100` command mid-session. +10. Graceful exit: `exit`, `quit`, `Ctrl-D`. +11. Clear error messages without crashing the session (e.g., "Error: division by zero", then continue). +12. History (if Mojo gets readline-like support). ```bash $ decimo @@ -334,80 +359,32 @@ Error: division by zero decimo> exit ``` -| # | Task | Status | Notes | -| --- | ---------------------------------------- | :----: | ----- | -| 4.1 | Read-eval-print loop | ✗ | | -| 4.2 | Custom prompt (`decimo>`) | ✗ | | -| 4.3 | `ans` variable (previous result) | ✗ | | -| 4.4 | Variable assignment (`x = expr`) | ✗ | | -| 4.5 | Session-level precision (`:precision N`) | ✗ | | -| 4.6 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✗ | | -| 4.7 | Error recovery (don't crash session) | ✗ | | +| # | Task | Status | Notes | +| ---- | ---------------------------------------- | :----: | --------------------------------------------------------------------------------------------------------------- | +| 4.1 | Subcommand restructure | ✗ | `decimo eval "expr"` (default), `decimo repl`, `decimo help functions`; use `subcommands()` hook | +| 4.2 | Persistent flags across subcommands | ✗ | `precision`, `--scientific`, etc. as `persistent=True`; both `decimo repl -p 100` and `decimo -p 100 repl` work | +| 4.3 | `parse_full()` for subcommand dispatch | ✗ | Typed struct + `ParseResult.subcommand` for dispatching to eval/repl/help handlers | +| 4.4 | No-args + TTY → launch REPL directly | ✗ | Replace `help_on_no_arguments()` with REPL auto-launch when terminal detected | +| 4.5 | Read-eval-print loop | ✗ | | +| 4.6 | Custom prompt (`decimo>`) | ✗ | | +| 4.7 | `ans` variable (previous result) | ✗ | | +| 4.8 | Variable assignment (`x = expr`) | ✗ | | +| 4.9 | Session-level precision (`:precision N`) | ✗ | | +| 4.10 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✗ | | +| 4.11 | Error recovery (don't crash session) | ✗ | | +| 4.12 | Interactive prompting for missing values | ✗ | Use `.prompt()` on subcommand args for interactive precision input, etc. | +| 4.13 | Subcommand aliases | ✗ | `command_aliases(["e"])` for `eval`, `command_aliases(["r"])` for `repl` | +| 4.14 | Hidden subcommands | ✗ | Hide `debug` / internal subcommands from help | ### Phase 5: Future Enhancements 1. Detect full-width digits/operators for CJK users while parsing. +2. Response files (`@expressions.txt`) — when Mojo compiler bug is fixed, use ArgMojo's `cmd.response_file_prefix("@")`. -### ArgMojo v0.2.0 Upgrade Tasks - -> **Prerequisite:** ArgMojo ≥ v0.2.0 is available as a pixi package. -> -> Reference: - -Once ArgMojo v0.2.0 lands in pixi, apply the following changes to `decimo`: - -#### 1. Auto-show help when no positional arg is given - -ArgMojo v0.2.0 automatically displays help when a required positional argument is missing — no code change needed on our side. Remove the `.required()` guard if it interferes, or verify the behaviour works out of the box. - -**Current (v0.1.x):** missing `expr` prints a raw error. -**After:** missing `expr` prints the full help text. - -#### 2. Shell-quoting tips via `add_tip()` - -Replace the inline description workaround with ArgMojo's dedicated `add_tip()` API. Tips render as a separate section at the bottom of `--help` output. - -```mojo -cmd.add_tip('If your expression contains *, ( or ), wrap it in quotes:') -cmd.add_tip(' decimo "2 * (3 + 4)"') -cmd.add_tip('Or use noglob: noglob decimo 2*(3+4)') -cmd.add_tip("Or add to ~/.zshrc: alias decimo='noglob decimo'") -``` - -Remove the corresponding note that is currently embedded in the `Command` description string. - -#### 3. Negative number passthrough - -Enable `allow_negative_numbers()` so that expressions like `decimo -3+4` or `decimo -3.14` are treated as math, not as unknown CLI flags. - -```mojo -cmd.allow_negative_numbers() -``` - -#### 4. Rename `Arg` → `Argument` - -`Arg` is kept as an alias in v0.2.0, so this is optional but recommended for consistency with the new API naming. - -```mojo -# Before -from argmojo import Arg, Command -# After -from argmojo import Argument, Command -``` - -#### 5. Colored error messages from ArgMojo - -ArgMojo v0.2.0 produces ANSI-colored stderr errors for its own parse errors (e.g., unknown flags). Our custom `display.mojo` colors still handle calculator-level errors. Verify that both layers look consistent (same RED styling). - -#### 6. Subcommands (Phase 4 REPL prep) - -Although not needed immediately, the new `add_subcommand()` API could later support: - -- `decimo repl` — launch interactive REPL -- `decimo eval "expr"` — explicit one-shot evaluation (current default) -- `decimo help ` — extended help on functions, constants, etc. - -This is deferred to Phase 4 planning. +| # | Task | Status | Notes | +| --- | ------------------------------------------- | :----: | ------------------------------------------------------------------------------ | +| 5.1 | Full-width digit/operator detection for CJK | ✗ | Tokenizer-level handling for CJK users | +| 5.2 | Response files (`@expressions.txt`) | ✗ | Blocked on Mojo compiler bug; `cmd.response_file_prefix("@")` ready when fixed | ## Design Decisions diff --git a/docs/readme_unreleased.md b/docs/readme_unreleased.md index 9cf0ec58..7ab047dc 100644 --- a/docs/readme_unreleased.md +++ b/docs/readme_unreleased.md @@ -2,6 +2,8 @@ An arbitrary-precision integer and decimal library for [Mojo](https://www.modular.com/mojo), inspired by Python's `int` and `Decimal`. +A CLI calculator tool, built on top of Decimo and powered by [ArgMojo](https://github.com/forfudan/argmojo) (a feature-rich command-line argument parser library for Mojo). + [![Version](https://img.shields.io/github/v/tag/forfudan/decimo?label=version&color=blue)](https://github.com/forfudan/decimo/releases) [![Mojo](https://img.shields.io/badge/mojo-0.26.2-orange)](https://docs.modular.com/mojo/manual/) [![pixi](https://img.shields.io/badge/pixi%20add-decimo-purple)](https://prefix.dev/channels/modular-community/packages/decimo) @@ -33,16 +35,18 @@ The core types are[^auxiliary]: - An arbitrary-precision signed integer type `BInt`[^bigint], which is a Mojo-native equivalent of Python's `int`. - An arbitrary-precision decimal implementation (`Decimal`) allowing for calculations with unlimited digits and decimal places[^arbitrary], which is a Mojo-native equivalent of Python's `decimal.Decimal`. - A 128-bit fixed-point decimal implementation (`Dec128`) supporting up to 29 significant digits with a maximum of 28 decimal places[^fixed]. +- An arbitrary-precision floating-point implementation (`Float`) backed by the GNU MPFR library, supporting computations with configurable precision and a wide exponent range. Unlike `Decimal`, which uses base-10 arithmetic, `Float` uses binary floating-point internally. This type is optional and requires MPFR/GMP to be installed on the user's system. | Type | Other names | Information | Internal representation | | --------- | -------------------- | ---------------------------------------- | ----------------------- | | `BInt` | `BigInt` | Equivalent to Python's `int` | Base-2^32 | | `Decimal` | `BDec`, `BigDecimal` | Equivalent to Python's `decimal.Decimal` | Base-10^9 | | `Dec128` | `Decimal128` | 128-bit fixed-precision decimal type | Triple 32-bit words | +| `Float` | `BigFloat` | Arbitrary-precision floating-point type | MPFR/GMP | **Decimo** combines "**Deci**mal" and "**Mo**jo" - reflecting its purpose and implementation language. "Decimo" is also a Latin word meaning "tenth" and is the root of the word "decimal". -A CLI calculator tool, built on top of the Decimo library and powered by [ArgMojo](https://github.com/forfudan/argmojo) (a command-line argument parser library), is also available in this repository. It provides a convenient way to perform high-precision calculations directly from the command line. +A CLI calculator tool, built on top of the Decimo library and powered by [ArgMojo](https://github.com/forfudan/argmojo) (a feature-rich command-line argument parser library for Mojo, with both builder and struct-based declarative APIs), is also available in this repository. It provides a convenient way to perform high-precision calculations directly from the command line. This repository includes a built-in [TOML parser](./docs/readme_toml.md) (`decimo.toml`), a lightweight pure-Mojo implementation supporting TOML v1.0. It parses configuration files and test data, supporting basic types, arrays, and nested tables. While created for Decimo's testing framework, it offers general-purpose structured data parsing with a clean, simple API. diff --git a/pixi.toml b/pixi.toml index 95315a54..f11e72a5 100644 --- a/pixi.toml +++ b/pixi.toml @@ -80,8 +80,8 @@ dec_debug = """clear && pixi run package && cd benches/decimal128 \ # Fetch argmojo source code for CLI calculator # Only do this when necessary # (argmojo is not compatible with the latest mojo version) -# fetch = """git clone https://github.com/forfudan/argmojo.git temp/argmojo 2>/dev/null || true \ -# && cd temp/argmojo && git checkout 29b6f54545f850e19d9a9ccfd1185d87f54e92b2 2>/dev/null""" +fetch = """git clone https://github.com/forfudan/argmojo.git temp/argmojo 2>/dev/null || true \ +&& cd temp/argmojo && git checkout 29b6f54545f850e19d9a9ccfd1185d87f54e92b2 2>/dev/null""" # cli calculator bcli = "clear && pixi run buildcli" diff --git a/src/cli/main.mojo b/src/cli/main.mojo index 5704be9a..af75f72d 100644 --- a/src/cli/main.mojo +++ b/src/cli/main.mojo @@ -31,21 +31,29 @@ struct DecimoArgs(Parsable): short="p", help="Number of significant digits", default="50", + value_name="N", + has_range=True, + range_min=1, + range_max=1_000_000_000, # One billion digits is more than sufficient + group="Computation", ] var scientific: Flag[ long="scientific", short="s", help="Output in scientific notation (e.g. 1.23E+10)", + group="Formatting", ] var engineering: Flag[ long="engineering", short="e", help="Output in engineering notation (exponent multiple of 3)", + group="Formatting", ] var pad: Flag[ long="pad", short="P", help="Pad trailing zeros to the specified precision", + group="Formatting", ] var delimiter: Option[ String, @@ -53,6 +61,8 @@ struct DecimoArgs(Parsable): short="d", help="Digit-group separator inserted every 3 digits (e.g. '_' gives 1_234.567_89)", default="", + value_name="CHAR", + group="Formatting", ] var rounding_mode: Option[ String, @@ -61,6 +71,8 @@ struct DecimoArgs(Parsable): help="Rounding mode for the final result", default="half-even", choices="half-even,half-up,half-down,up,down,ceiling,floor", + value_name="MODE", + group="Computation", ] @staticmethod @@ -89,6 +101,8 @@ def main(): def _run() raises: var cmd = DecimoArgs.to_command() + cmd.usage("decimo [OPTIONS] ") + cmd.allow_negative_numbers() cmd.mutually_exclusive(["scientific", "engineering"]) cmd.add_tip( 'If your expression contains *, ( or ), quote it: decimo "2 * (3 + 4)"' From 7f5185498db7e9c939a0d3dc4d06536a16dadced Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 11 Apr 2026 00:00:44 +0200 Subject: [PATCH 12/17] [cli] Enable negative expressions as positional arguments (#202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates the Decimo CLI argument parsing so expressions that start with `-` (e.g., negative numbers / negative expressions) can be passed as the positional `` more naturally, and documents/tests the behavior. **Changes:** - Enable hyphen-prefixed values for the `expr` positional argument via ArgMojo (`allow_hyphen=True`) and remove the older command-level negative-number allowance. - Expand CLI integration tests to cover negative expressions, mixed option/positional ordering, and `--` separator cases. - Add user-manual documentation for “Negative Expressions” and update the CLI calculator plan checklist accordingly. --- docs/plans/cli_calculator.md | 36 ++++++++++++++++--------------- docs/user_manual_cli.md | 36 +++++++++++++++++++++++++++++++ src/cli/main.mojo | 7 +++++- tests/test_cli.sh | 41 ++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 18 deletions(-) diff --git a/docs/plans/cli_calculator.md b/docs/plans/cli_calculator.md index 82240f54..433faf6c 100644 --- a/docs/plans/cli_calculator.md +++ b/docs/plans/cli_calculator.md @@ -311,23 +311,25 @@ Format the final `BigDecimal` result based on CLI flags: 6. Documentation and examples in README (include shell completion setup). 7. Build and distribute as a single binary. -| # | Task | Status | Notes | -| ---- | --------------------------------------------------------------- | :----: | ------------------------------------------------------------------------------------------------------------------ | -| 3.1 | Error messages with position info + caret display | ✓ | Colored stderr: red `Error:`, green `^` caret | -| 3.2 | Edge cases (div-by-zero, negative sqrt, empty expression, etc.) | ✓ | 27 error-handling tests | -| 3.3 | ArgMojo v0.5.0 declarative API migration | ✓ | `Parsable` struct, `add_tip()`, `mutually_exclusive()`, `choices`, `--version` all working | -| 3.4 | Built-in features (free with ArgMojo v0.5.0) | ✓ | Typo suggestions, `NO_COLOR`, CJK full-width correction, prefix matching, `--` stop marker | -| 3.5 | Shell completion (`--completions bash\|zsh\|fish`) | ✓ | Built-in — zero code; needs documentation in user manual and README | -| 3.6 | `allow_negative_numbers()` to allow pure negative numbers | ✓ | Explicit opt-in in hybrid bridge; `decimo "-3"` works, expressions need quoting or `--` | -| 3.7 | Numeric range on `precision` | ✓ | `has_range=True, range_min=1, range_max=1000000000`; rejects `--precision 0` or `-5` | -| 3.8 | Value names for help readability | ✓ | `--precision `, `--delimiter `, `--rounding-mode ` | -| 3.9 | Argument groups in help output | ✓ | `Computation` and `Formatting` groups in `--help` | -| 3.10 | Custom usage line | ✓ | `Usage: decimo [OPTIONS] ` | -| 3.11 | `Parsable.run()` override | ✗ | Move eval logic into `DecimoArgs.run()` for cleaner separation | -| 3.12 | Performance validation | ✗ | No CLI-level benchmarks yet | -| 3.13 | Documentation (user manual for CLI) | ✗ | `docs/user_manual_cli.md`; include shell completion setup | -| 3.14 | Build and distribute as single binary | ✗ | | -| 3.15 | Allow negative expressions | ✗ | This needs ArgMojo to regard arguments with a hyphen and followed by more than one letter as a positional argument | +| # | Task | Status | Notes | +| ---- | --------------------------------------------------------------- | :----: | ------------------------------------------------------------------------------------------ | +| 3.1 | Error messages with position info + caret display | ✓ | Colored stderr: red `Error:`, green `^` caret | +| 3.2 | Edge cases (div-by-zero, negative sqrt, empty expression, etc.) | ✓ | 27 error-handling tests | +| 3.3 | ArgMojo v0.5.0 declarative API migration | ✓ | `Parsable` struct, `add_tip()`, `mutually_exclusive()`, `choices`, `--version` all working | +| 3.4 | Built-in features (free with ArgMojo v0.5.0) | ✓ | Typo suggestions, `NO_COLOR`, CJK full-width correction, prefix matching, `--` stop marker | +| 3.5 | Shell completion (`--completions bash\|zsh\|fish`) | ✓ | Built-in — zero code; needs documentation in user manual and README | +| 3.6 | `allow_negative_numbers()` to allow pure negative numbers | ✓ | Superseded by 3.15 `allow_hyphen=True`; removed in favour of the more general approach | +| 3.7 | Numeric range on `precision` | ✓ | `has_range=True, range_min=1, range_max=1000000000`; rejects `--precision 0` or `-5` | +| 3.8 | Value names for help readability | ✓ | `--precision `, `--delimiter `, `--rounding-mode ` | +| 3.9 | Argument groups in help output | ✓ | `Computation` and `Formatting` groups in `--help` | +| 3.10 | Custom usage line | ✓ | `Usage: decimo [OPTIONS] ` | +| 3.11 | `Parsable.run()` override | ✗ | Move eval logic into `DecimoArgs.run()` for cleaner separation | +| 3.12 | Performance validation | ✗ | No CLI-level benchmarks yet | +| 3.13 | Documentation (user manual for CLI) | ✗ | `docs/user_manual_cli.md`; include shell completion setup | +| 3.14 | Build and distribute as single binary | ✗ | | +| 3.15 | Allow negative expressions | ✓ | `allow_hyphen=True` on `Positional`; `decimo "-3*pi*(sin(1))"` works | +| 3.16 | Make short names upper cases to avoid expression collisions | ✗ | `-sin(1)` clashes with `-s` (scientific), `-e` clashes with `--engineering` | +| 3.17 | Define `allow_hyphen_values` in declarative API | ✗ | When argmojo supports it | ### Phase 4: Interactive REPL & Subcommands diff --git a/docs/user_manual_cli.md b/docs/user_manual_cli.md index 3d3467be..1ad8fbe7 100644 --- a/docs/user_manual_cli.md +++ b/docs/user_manual_cli.md @@ -20,6 +20,7 @@ - [Rounding Mode (`--rounding-mode`, `-r`)](#rounding-mode---rounding-mode--r) - [Shell Integration](#shell-integration) - [Quoting Expressions](#quoting-expressions) + - [Negative Expressions](#negative-expressions) - [Using noglob](#using-noglob) - [Examples](#examples) - [Basic Arithmetic](#basic-arithmetic) @@ -87,8 +88,11 @@ decimo "2^256" - **Integers:** `42`, `-7`, `1000000` - **Decimals:** `3.14`, `0.001`, `.5` - **Negative numbers:** `-3`, `-3.14`, `(-5 + 2)` +- **Negative expressions:** `-3*pi`, `-3*pi*(sin(1))` - **Unary minus:** `2 * -3`, `sqrt(-1 + 2)` +Negative numbers and many expressions starting with `-` can be passed directly as the positional argument. However, if the expression token looks like a CLI flag (e.g., `-e`), use `--` to force positional parsing. See [Negative Expressions](#negative-expressions) for details. + ### Operators | Operator | Description | Example | Result | @@ -255,6 +259,38 @@ decimo "2 * (3 + 4)" decimo 2 * (3 + 4) ``` +### Negative Expressions + +Most expressions starting with a hyphen (`-`) are treated as positional arguments, not as option flags: + +```bash +# Negative number +decimo "-3.14" +# → -3.14 + +# Negative expression +decimo "-3*2" +# → -6 + +# Complex negative expression +decimo "-3*pi*(sin(1))" +# → -7.930677192244368536658197969… + +# Options can appear before or after the expression +decimo -p 10 "-3*pi" +decimo "-3*pi" -p 10 +``` + +**Caveat:** If the expression looks like an existing CLI flag (e.g., `-e` matches `--engineering`), ArgMojo will consume it as an option. Use `--` to force positional parsing in those cases: + +```bash +# -e is the engineering flag, so use -- to treat it as an expression +decimo -- "-e" +# → -2.71828… +``` + +> **Note:** A future release will rename short flags to uppercase (`-S`, `-E`) to eliminate these collisions entirely. + ### Using noglob On zsh, you can use `noglob` to prevent shell interpretation: diff --git a/src/cli/main.mojo b/src/cli/main.mojo index af75f72d..f1555da8 100644 --- a/src/cli/main.mojo +++ b/src/cli/main.mojo @@ -102,8 +102,13 @@ def main(): def _run() raises: var cmd = DecimoArgs.to_command() cmd.usage("decimo [OPTIONS] ") - cmd.allow_negative_numbers() cmd.mutually_exclusive(["scientific", "engineering"]) + # Allow expressions starting with '-' (e.g. "-3*pi*(sin(1))") to be + # treated as positional values rather than option flags. + for i in range(len(cmd.args)): + if cmd.args[i].name == "expr": + cmd.args[i]._allow_hyphen_values = True + break cmd.add_tip( 'If your expression contains *, ( or ), quote it: decimo "2 * (3 + 4)"' ) diff --git a/tests/test_cli.sh b/tests/test_cli.sh index 5ab2b56b..8ed0a87c 100644 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -55,6 +55,47 @@ assert_output "rounding mode ceiling" "0.33334" "$BINARY" "1/3" -p 5 -r ceiling # Pad flag (--pad / -P) assert_output "pad trailing zeros" "0.33333" "$BINARY" "1/3" -p 5 --pad +# ── Negative expressions (allow_hyphen) ─────────────────────────────────── +assert_output "negative number" "-3.14" "$BINARY" "-3.14" +assert_output "negative integer" "-42" "$BINARY" "-42" +assert_output "negative expression" "-6" "$BINARY" "-3*2" +assert_output "negative expression with pi" "-9.424777961" "$BINARY" "-3*pi" -p 10 +assert_output "negative expr with abs()" "-3" "$BINARY" "-abs(3)" -p 10 +assert_output "negative expression complex" "17.32428719" "$BINARY" "-3*pi/sin(10)" -p 10 +assert_output "negative expression with parens" "-7.9306771922443685366581979690091558499739419154171" "$BINARY" "-3*pi*(sin(1))" -p 50 +assert_output "negative zero" "-0" "$BINARY" "-0" +assert_output "negative minus negative" "1" "$BINARY" "-1*-1" +assert_output "negative power" "-8" "$BINARY" "-2^3" +assert_output "negative cancel out" "0" "$BINARY" "-1+1" +assert_output "negative subtraction" "-50" "$BINARY" "-100+50" + +# ── Option/positional ordering ──────────────────────────────────────────── +assert_output "options before expr" "0.3333333333" "$BINARY" -p 10 "1/3" +assert_output "options after expr" "0.3333333333" "$BINARY" "1/3" -p 10 +assert_output "multiple options before expr" "1.732428719E+1" "$BINARY" -s -p 10 "-3*pi/sin(10)" +assert_output "mixed order: flag expr option" "1.732428719E+1" "$BINARY" -s "-3*pi/sin(10)" -p 10 +assert_output "mixed order: option expr flag" "1.732428719E+1" "$BINARY" -p 10 "-3*pi/sin(10)" -s +assert_output "engineering before expr" "-12.345678E+3" "$BINARY" -e "-12345.678" +assert_output "engineering after expr" "-12.345678E+3" "$BINARY" "-12345.678" -e +assert_output "delimiter before expr" "3.141_592_654" "$BINARY" -d _ -p 10 "pi" +assert_output "delimiter after expr" "3.141_592_654" "$BINARY" "pi" -p 10 -d _ +assert_output "rounding before expr" "0.33334" "$BINARY" -p 5 -r ceiling "1/3" +assert_output "all options before expr" "0.33334" "$BINARY" -p 5 -r ceiling --pad "1/3" +assert_output "all options after expr" "0.33334" "$BINARY" "1/3" -p 5 -r ceiling --pad + +# ── Double-dash separator ───────────────────────────────────────────────── +assert_output "-- with negative expr" "-6" "$BINARY" -- "-3*2" +assert_output "-- with negative number" "-3.14" "$BINARY" -- "-3.14" +assert_output "-- with -e as expr" "-2.7182818284590452353602874713526624977572470937000" "$BINARY" -- "-e" + +# ── Bare hyphen rejection ───────────────────────────────────────────────── +if "$BINARY" -- - >/dev/null 2>&1; then + echo "FAIL: bare hyphen should be rejected" + FAIL=$((FAIL + 1)) +else + PASS=$((PASS + 1)) +fi + echo "" echo "CLI integration tests: $PASS passed, $FAIL failed" From 2c484dea0b5e8d34de21b7a1e738c59b129f2eba Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 11 Apr 2026 20:24:44 +0200 Subject: [PATCH 13/17] [cli] Make all short names upper cases + Enable pipe mode and file mode (#203) This PR updates the Decimo CLI to avoid short-flag collisions with negative expressions by uppercasing short options, and adds new stdin (pipe) + file-based input modes for evaluating multiple expressions. **Changes:** - Uppercase short flags for formatting/computation options (`-P/-S/-E/-D/-R`) and remove the `--pad` short flag to prevent collisions with expressions like `-e`, `-pi`, `-sin(1)`. - Add pipe mode (read/evaluate expressions from stdin, one per non-empty non-comment line) and file mode (`-F/--file`). - Extend integration/unit tests and update docs/changelog/plans for the new CLI UX. --- docs/changelog.md | 11 ++ docs/plans/cli_calculator.md | 28 +-- docs/user_manual_cli.md | 232 +++++++++++++++++------- src/cli/calculator/__init__.mojo | 12 ++ src/cli/calculator/io.mojo | 302 +++++++++++++++++++++++++++++++ src/cli/main.mojo | 231 ++++++++++++++++++++--- tests/cli/test_io.mojo | 252 ++++++++++++++++++++++++++ tests/test_cli.sh | 133 +++++++++++--- 8 files changed, 1069 insertions(+), 132 deletions(-) create mode 100644 src/cli/calculator/io.mojo create mode 100644 tests/cli/test_io.mojo diff --git a/docs/changelog.md b/docs/changelog.md index 0e1b1fe4..0fba57c4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,17 @@ This is a list of changes for the Decimo package (formerly DeciMojo). +## Unreleased - under development + +### ⭐️ New in v0.10.0 + +**CLI Calculator:** + +1. Add **pipe/stdin mode**: read expressions from standard input, one per line, when no positional argument is given and stdin is piped (e.g. `echo "1+2" | decimo`, `printf "pi\nsqrt(2)" | decimo -P 100`). Blank lines and comment lines (starting with `#`) are automatically skipped. +1. Add **file mode**: use `--file` / `-F` flag to evaluate expressions from a file, one per line (e.g. `decimo -F expressions.dm -P 50`). Comments (`#`), inline comments, and blank lines are skipped. All CLI flags (precision, formatting, rounding) apply to every expression. + +### 🦋 Changed in v0.10.0 + ## 20260323 (v0.9.0) Decimo v0.9.0 updates the codebase to **Mojo v0.26.2** and marks the **"make it useful"** phase. This release introduces three major additions: diff --git a/docs/plans/cli_calculator.md b/docs/plans/cli_calculator.md index 433faf6c..cb19c516 100644 --- a/docs/plans/cli_calculator.md +++ b/docs/plans/cli_calculator.md @@ -95,7 +95,7 @@ cat expressions.txt | decimo decimo < expressions.txt # Evaluate a script file (.dm) -decimo file.dm +decimo -F file.dm ``` Example `expressions.dm`: @@ -266,17 +266,17 @@ Format the final `BigDecimal` result based on CLI flags: 6. Handle unary minus. 7. Test with basic expressions. -| # | Task | Status | Notes | -| --- | ------------------------------------------------ | :----: | ----------------------------- | -| 1.1 | Project structure (src/cli/, calculator module) | ✓ | | -| 1.2 | Tokenizer (numbers, `+ - * /`, parens) | ✓ | | -| 1.3 | Shunting-yard parser (infix → RPN) | ✓ | | -| 1.4 | RPN evaluator using `BigDecimal` | ✓ | | -| 1.5 | ArgMojo wiring (`expr`, `--precision`, `--help`) | ✓ | | -| 1.6 | Unary minus | ✓ | | -| 1.7 | Basic expression tests | ✓ | 4 test files, 118 tests total | -| 1.8 | Pipeline / stdin input (`echo "1+2" \| decimo`) | ✗ | Not yet implemented | -| 1.9 | File input (`decimo file.dm`) | ✗ | Not yet implemented | +| # | Task | Status | Notes | +| --- | ------------------------------------------------ | :----: | -------------------------------------------------------------------------------------------- | +| 1.1 | Project structure (src/cli/, calculator module) | ✓ | | +| 1.2 | Tokenizer (numbers, `+ - * /`, parens) | ✓ | | +| 1.3 | Shunting-yard parser (infix → RPN) | ✓ | | +| 1.4 | RPN evaluator using `BigDecimal` | ✓ | | +| 1.5 | ArgMojo wiring (`expr`, `--precision`, `--help`) | ✓ | | +| 1.6 | Unary minus | ✓ | | +| 1.7 | Basic expression tests | ✓ | 4 test files | +| 1.8 | Pipeline / stdin input (`echo "1+2" \| decimo`) | ✓ | Pipe mode: reads stdin when no positional arg and stdin is not a TTY | +| 1.9 | File input (`decimo -F file.dm`) | ✓ | File mode via `-F/--file` flag: reads files line by line, skips comments (#) and blank lines | ### Phase 2: Power and Functions @@ -328,7 +328,7 @@ Format the final `BigDecimal` result based on CLI flags: | 3.13 | Documentation (user manual for CLI) | ✗ | `docs/user_manual_cli.md`; include shell completion setup | | 3.14 | Build and distribute as single binary | ✗ | | | 3.15 | Allow negative expressions | ✓ | `allow_hyphen=True` on `Positional`; `decimo "-3*pi*(sin(1))"` works | -| 3.16 | Make short names upper cases to avoid expression collisions | ✗ | `-sin(1)` clashes with `-s` (scientific), `-e` clashes with `--engineering` | +| 3.16 | Make short names upper cases to avoid expression collisions | ✓ | `-P`, `-S`, `-E`, `-D`, `-R`; `--pad` has no short name; `-e`, `-pi`, `-sin(1)` all work | | 3.17 | Define `allow_hyphen_values` in declarative API | ✗ | When argmojo supports it | ### Phase 4: Interactive REPL & Subcommands @@ -408,7 +408,7 @@ This is the natural choice for a calculator: users expect `7 / 2` to be `3.5`, n | ------------------------------------- | ------------------------------------- | | `decimo "expr"` | One-shot: evaluate and exit | | `echo "expr" \| decimo` | Pipe: read stdin line by line | -| `decimo file.dm` | File: read and evaluate each line | +| `decimo -F file.dm` | File: read and evaluate each line | | `decimo` (no args, terminal is a TTY) | REPL: interactive session | | `decimo -i` | REPL: force interactive even if piped | diff --git a/docs/user_manual_cli.md b/docs/user_manual_cli.md index 1ad8fbe7..ebbef539 100644 --- a/docs/user_manual_cli.md +++ b/docs/user_manual_cli.md @@ -12,12 +12,16 @@ - [Functions](#functions) - [Constants](#constants) - [CLI Options](#cli-options) - - [Precision (`--precision`, `-p`)](#precision---precision--p) - - [Scientific Notation (`--scientific`, `-s`)](#scientific-notation---scientific--s) - - [Engineering Notation (`--engineering`, `-e`)](#engineering-notation---engineering--e) - - [Pad to Precision (`--pad`, `-P`)](#pad-to-precision---pad--p) - - [Digit Separator (`--delimiter`, `-d`)](#digit-separator---delimiter--d) - - [Rounding Mode (`--rounding-mode`, `-r`)](#rounding-mode---rounding-mode--r) + - [Precision (`--precision`, `-P`)](#precision---precision--p) + - [Scientific Notation (`--scientific`, `-S`)](#scientific-notation---scientific--s) + - [Engineering Notation (`--engineering`, `-E`)](#engineering-notation---engineering--e) + - [Pad to Precision (`--pad`)](#pad-to-precision---pad) + - [Digit Separator (`--delimiter`, `-D`)](#digit-separator---delimiter--d) + - [Rounding Mode (`--rounding-mode`, `-R`)](#rounding-mode---rounding-mode--r) +- [Input Modes](#input-modes) + - [Expression Mode (Default)](#expression-mode-default) + - [Pipe Mode (stdin)](#pipe-mode-stdin) + - [File Mode (`-F`)](#file-mode--f) - [Shell Integration](#shell-integration) - [Quoting Expressions](#quoting-expressions) - [Negative Expressions](#negative-expressions) @@ -66,15 +70,15 @@ decimo "1 + 2 * 3" # → 7 # High-precision division -decimo "1/3" -p 100 +decimo "1/3" -P 100 # → 0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333 # Square root of 2 to 50 digits -decimo "sqrt(2)" -p 50 +decimo "sqrt(2)" -P 50 # → 1.4142135623730950488016887242096980785696718753770 # 1000 digits of pi -decimo "pi" -p 1000 +decimo "pi" -P 1000 # Large integer exponentiation decimo "2^256" @@ -91,7 +95,7 @@ decimo "2^256" - **Negative expressions:** `-3*pi`, `-3*pi*(sin(1))` - **Unary minus:** `2 * -3`, `sqrt(-1 + 2)` -Negative numbers and many expressions starting with `-` can be passed directly as the positional argument. However, if the expression token looks like a CLI flag (e.g., `-e`), use `--` to force positional parsing. See [Negative Expressions](#negative-expressions) for details. +Negative numbers and many expressions starting with `-` can be passed directly as the positional argument. See [Negative Expressions](#negative-expressions) for details. ### Operators @@ -122,7 +126,7 @@ Right-associativity of `^` means `2^3^2` = `2^(3^2)` = `2^9` = `512`, not `(2^3) ### Functions -All functions use the CLI's precision setting (default 50, configurable with `-p`). +All functions use the CLI's precision setting (default 50, configurable with `-P`). **Single-argument functions:** @@ -164,67 +168,67 @@ decimo "ln(exp(1))" # → 1 Constants are computed to the requested precision: ```bash -decimo "pi" -p 100 # 100 digits of π -decimo "e" -p 500 # 500 digits of e +decimo "pi" -P 100 # 100 digits of π +decimo "e" -P 500 # 500 digits of e decimo "2 * pi * 6371" # Circumference using π ``` ## CLI Options -### Precision (`--precision`, `-p`) +### Precision (`--precision`, `-P`) Number of significant digits in the result. Default: **50**. ```bash -decimo "1/7" -p 10 # 0.1428571429 -decimo "1/7" -p 100 # 100 significant digits -decimo "1/7" -p 200 # 200 significant digits +decimo "1/7" -P 10 # 0.1428571429 +decimo "1/7" -P 100 # 100 significant digits +decimo "1/7" -P 200 # 200 significant digits ``` -### Scientific Notation (`--scientific`, `-s`) +### Scientific Notation (`--scientific`, `-S`) Output in scientific notation (e.g., `1.23E+10`). ```bash -decimo "123456789 * 987654321" -s +decimo "123456789 * 987654321" -S # → 1.21932631112635269E+17 ``` -### Engineering Notation (`--engineering`, `-e`) +### Engineering Notation (`--engineering`, `-E`) Output in engineering notation (exponent is always a multiple of 3). ```bash -decimo "123456789 * 987654321" -e +decimo "123456789 * 987654321" -E # → 121.932631112635269E+15 ``` > `--scientific` and `--engineering` are mutually exclusive. -### Pad to Precision (`--pad`, `-P`) +### Pad to Precision (`--pad`) Pad trailing zeros so the fractional part has exactly `precision` digits after the decimal point. When used together with `--precision`, the `precision` value is treated as the number of fractional digits for padding purposes, not as a strict limit on total significant digits. As a result, the formatted number can have more than `precision` significant digits. ```bash -decimo "1.5" -P -p 10 +decimo "1.5" --pad -P 10 # → 1.5000000000 (10 fractional digits, 11 significant digits) ``` -### Digit Separator (`--delimiter`, `-d`) +### Digit Separator (`--delimiter`, `-D`) Insert a character every 3 digits for readability. ```bash -decimo "2^64" -d _ +decimo "2^64" -D _ # → 18_446_744_073_709_551_616 -decimo "pi" -p 30 -d _ +decimo "pi" -P 30 -D _ # → 3.141_592_653_589_793_238_462_643_383_28 ``` -### Rounding Mode (`--rounding-mode`, `-r`) +### Rounding Mode (`--rounding-mode`, `-R`) Choose how the final result is rounded. Default: **half-even** (banker's rounding). @@ -239,12 +243,87 @@ Choose how the final result is rounded. Default: **half-even** (banker's roundin | `floor` | Round toward −∞ | ```bash -decimo "1/6" -p 5 -r half-up # 0.16667 -decimo "1/6" -p 5 -r half-even # 0.16667 -decimo "1/6" -p 5 -r down # 0.16666 -decimo "1/6" -p 5 -r up # 0.16667 +decimo "1/6" -P 5 -R half-up # 0.16667 +decimo "1/6" -P 5 -R half-even # 0.16667 +decimo "1/6" -P 5 -R down # 0.16666 +decimo "1/6" -P 5 -R up # 0.16667 ``` +## Input Modes + +`decimo` accepts input in three ways: a single expression on the command line, piped stdin, or a file. + +| Mode | Invocation | When used | +| ---------- | ----------------------- | -------------------------------------------------- | +| Expression | `decimo "EXPR"` | A positional argument is provided as an expression | +| Pipe | `echo "EXPR" \| decimo` | No positional argument and stdin is not a TTY | +| File | `decimo -F FILE.dm` | The `-F`/`--file` option is used | + +If no expression is given and stdin is a TTY (interactive terminal), `decimo` prints an error and exits. + +### Expression Mode (Default) + +Pass a single expression as the positional argument: + +```bash +decimo "1/3" -P 100 +``` + +This is the most common usage. See [Expression Syntax](#expression-syntax) for what you can write. + +### Pipe Mode (stdin) + +When no positional argument is provided and stdin is piped, `decimo` reads all of stdin and evaluates each non-empty, non-comment line: + +```bash +# Single expression +echo "sqrt(2)" | decimo -P 30 +# → 1.41421356237309504880168872421 + +# Multiple expressions (one per line) +printf '1/3\nsqrt(2)\npi' | decimo -P 20 +# → 0.33333333333333333333 +# → 1.4142135623730950488 +# → 3.1415926535897932385 + +# Lines starting with '#' are comments; blank lines are skipped +printf '# constants\npi\n\ne' | decimo -P 10 +# → 3.141592654 +# → 2.718281828 +``` + +All CLI options (`-P`, `-S`, `-E`, `-D`, `-R`, `--pad`) apply to every line. + +If any expression fails, `decimo` prints the error for that line, continues evaluating the remaining lines, and exits with code 1. + +### File Mode (`-F`) + +Use the `-F` (or `--file`) flag to evaluate expressions from a file, one per line: + +```bash +decimo -F expressions.dm -P 50 +``` + +Example file (`expressions.dm`): + +```text +# Basic arithmetic +1 + 2 +100 * 12 - 23/17 + +# High-precision constants +pi +e + +# Functions +sqrt(2) +ln(10) +``` + +Comments start with `#`. Inline comments are also supported (e.g. `1+2 # add`). Leading whitespace before `#` is allowed. Blank lines and whitespace-only lines are skipped. + +If the specified file does not exist or cannot be read, `decimo` reports an error and exits. + ## Shell Integration ### Quoting Expressions @@ -277,19 +356,31 @@ decimo "-3*pi*(sin(1))" # → -7.930677192244368536658197969… # Options can appear before or after the expression -decimo -p 10 "-3*pi" -decimo "-3*pi" -p 10 +decimo -P 10 "-3*pi" +decimo "-3*pi" -P 10 ``` -**Caveat:** If the expression looks like an existing CLI flag (e.g., `-e` matches `--engineering`), ArgMojo will consume it as an option. Use `--` to force positional parsing in those cases: +Because all short option names are uppercase (`-P`, `-S`, `-E`, `-D`, `-R`), expressions like `-e`, `-sin(1)`, and `-pi` are never mistaken for flags: ```bash -# -e is the engineering flag, so use -- to treat it as an expression -decimo -- "-e" +# Euler's number, negated +decimo "-e" # → -2.71828… + +# Negative sine +decimo "-sin(1)" +# → -0.84147… + +# Negative pi +decimo "-pi" +# → -3.14159… ``` -> **Note:** A future release will rename short flags to uppercase (`-S`, `-E`) to eliminate these collisions entirely. +The `--` separator still works if you prefer explicit positional parsing: + +```bash +decimo -- "-e" +``` ### Using noglob @@ -328,34 +419,34 @@ decimo "2 ^ 256" ```bash # 200 digits of 1/7 -decimo "1/7" -p 200 +decimo "1/7" -P 200 # π to 1000 digits -decimo "pi" -p 1000 +decimo "pi" -P 1000 # e to 500 digits -decimo "e" -p 500 +decimo "e" -P 500 # sqrt(2) to 100 digits -decimo "sqrt(2)" -p 100 +decimo "sqrt(2)" -P 100 ``` ### Mathematical Functions ```bash # Trigonometry -decimo "sin(pi/6)" -p 50 # → 0.5 -decimo "cos(pi/3)" -p 50 # → 0.5 -decimo "tan(pi/4)" -p 50 # → 1 +decimo "sin(pi/6)" -P 50 # → 0.5 +decimo "cos(pi/3)" -P 50 # → 0.5 +decimo "tan(pi/4)" -P 50 # → 1 # Logarithms -decimo "ln(2)" -p 100 +decimo "ln(2)" -P 100 decimo "log10(1000)" # → 3 decimo "log(256, 2)" # → 8 # Nested functions -decimo "sqrt(abs(1.1 * -12 - 23/17))" -p 30 -decimo "exp(ln(100))" -p 30 # → 100 +decimo "sqrt(abs(1.1 * -12 - 23/17))" -P 30 +decimo "exp(ln(100))" -P 30 # → 100 # Cube root decimo "cbrt(27)" # → 3 @@ -366,19 +457,19 @@ decimo "root(1000000, 6)" # → 10 ```bash # Scientific notation -decimo "123456789.987654321" -s +decimo "123456789.987654321" -S # → 1.23456789987654321E+8 # Engineering notation -decimo "123456789.987654321" -e +decimo "123456789.987654321" -E # → 123.456789987654321E+6 # Digit separators -decimo "2^100" -d _ +decimo "2^100" -D _ # → 1_267_650_600_228_229_401_496_703_205_376 # Pad trailing zeros -decimo "1/4" -p 20 -P +decimo "1/4" -P 20 --pad # → 0.25000000000000000000 ``` @@ -386,11 +477,11 @@ decimo "1/4" -p 20 -P ```bash # Compare rounding of 2.5 to 0 decimal places: -decimo "2.5 + 0" -p 1 -r half-even # → 2 (banker's: round to even) -decimo "2.5 + 0" -p 1 -r half-up # → 3 (traditional) -decimo "2.5 + 0" -p 1 -r down # → 2 (truncate) -decimo "2.5 + 0" -p 1 -r ceiling # → 3 (toward +∞) -decimo "2.5 + 0" -p 1 -r floor # → 2 (toward −∞) +decimo "2.5 + 0" -P 1 -R half-even # → 2 (banker's: round to even) +decimo "2.5 + 0" -P 1 -R half-up # → 3 (traditional) +decimo "2.5 + 0" -P 1 -R down # → 2 (truncate) +decimo "2.5 + 0" -P 1 -R ceiling # → 3 (toward +∞) +decimo "2.5 + 0" -P 1 -R floor # → 2 (toward −∞) ``` ## Error Messages @@ -429,30 +520,33 @@ Error: unmatched '(' ```txt Arbitrary-precision CLI calculator powered by Decimo. -Note: if your expression contains *, ( or ), your shell may -intercept them before decimo runs. Use quotes or noglob: - decimo "2 * (3 + 4)" # with quotes - noglob decimo 2*(3+4) # with noglob - alias decimo='noglob decimo' # add to ~/.zshrc +Tip: If your expression contains *, ( or ), quote it: decimo "2 * (3 + 4)" +Tip: Or use noglob: alias decimo='noglob decimo' (add to ~/.zshrc) +Tip: Pipe expressions: echo '1/3' | decimo -P 100 +Tip: Evaluate a file: decimo -F expressions.dm -P 50 -Usage: decimo [OPTIONS] +Usage: decimo [OPTIONS] [EXPR] Arguments: - expr Math expression to evaluate (e.g. 'sqrt(abs(1.1*-12-23/17))') + expr Math expression to evaluate + (e.g. 'sqrt(2)') Options: - -p, --precision + -P, --precision Number of significant digits (default: 50) - -s, --scientific + -S, --scientific Output in scientific notation (e.g. 1.23E+10) - -e, --engineering + -E, --engineering Output in engineering notation (exponent multiple of 3) - -P, --pad + --pad Pad trailing zeros to the specified precision - -d, --delimiter + -D, --delimiter Digit-group separator inserted every 3 digits (e.g. '_' gives 1_234.567_89) - -r, --rounding-mode {half-even,half-up,half-down,up,down,ceiling,floor} + -R, --rounding-mode Rounding mode for the final result (default: half-even) + {half-even,half-up,half-down,up,down,ceiling,floor} + -F, --file + Evaluate expressions from a file (one per line) -h, --help Show this help message -V, --version diff --git a/src/cli/calculator/__init__.mojo b/src/cli/calculator/__init__.mojo index 3e8a023a..b4162186 100644 --- a/src/cli/calculator/__init__.mojo +++ b/src/cli/calculator/__init__.mojo @@ -45,3 +45,15 @@ from .tokenizer import ( from .parser import parse_to_rpn from .evaluator import evaluate_rpn, evaluate from .display import print_error, print_warning, print_hint +from .io import ( + stdin_is_tty, + read_stdin, + split_into_lines, + strip_comment, + is_blank, + is_comment_or_blank, + strip, + filter_expression_lines, + read_file_text, + file_exists, +) diff --git a/src/cli/calculator/io.mojo b/src/cli/calculator/io.mojo new file mode 100644 index 00000000..09d599dd --- /dev/null +++ b/src/cli/calculator/io.mojo @@ -0,0 +1,302 @@ +# ===----------------------------------------------------------------------=== # +# 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. +# ===----------------------------------------------------------------------=== # + +""" +I/O utilities for the Decimo CLI calculator. + +Provides functions for detecting whether stdin is a pipe or terminal, +reading lines from stdin, reading expression files, and line-level text +processing (comment stripping, whitespace handling). + +The text-processing primitives (``strip_comment``, ``is_blank``, ``strip``) +are designed to be composable and reusable across all input modes — +pipe, file, and future REPL. +""" + +from std.ffi import external_call + + +# ===----------------------------------------------------------------------=== # +# stdin detection +# ===----------------------------------------------------------------------=== # + + +def stdin_is_tty() -> Bool: + """Returns True if stdin is connected to a terminal (TTY), + False if it is a pipe or redirected file.""" + return external_call["isatty", Int32](Int32(0)) != 0 + + +# ===----------------------------------------------------------------------=== # +# stdin reading +# ===----------------------------------------------------------------------=== # + + +def read_stdin() -> String: + """Reads all data from stdin and return it as a String. + + Uses the C ``getchar()`` function to read one byte at a time until + EOF. This avoids FFI conflicts with the POSIX ``read()`` syscall. + Returns an empty string if stdin is empty. + """ + var chunks = List[UInt8]() + + while True: + var c = external_call["getchar", Int32]() + if c < 0: # EOF is -1 + break + chunks.append(UInt8(c)) + + if len(chunks) == 0: + return String("") + + # String(unsafe_from_utf8=...) adds its own null terminator. + return String(unsafe_from_utf8=chunks^) + + +# ===----------------------------------------------------------------------=== # +# Line splitting and text processing +# ===----------------------------------------------------------------------=== # + + +def split_into_lines(text: String) -> List[String]: + """Splits a string into individual lines. + + Handles both ``\\n`` and ``\\r\\n`` line endings. + Trailing empty lines from a final newline are not included. + """ + var lines = List[String]() + var start = 0 + var text_len = len(text) + + for i in range(text_len): + if text[byte=i] == "\n": + # Handle \r\n + var end = i + if end > start and text[byte=end - 1] == "\r": + end -= 1 + lines.append(String(text[byte=start:end])) + start = i + 1 + + # Handle last line without trailing newline + if start < text_len: + var last = String(text[byte=start:text_len]) + # Strip trailing \r if present + if len(last) > 0 and last[byte=len(last) - 1] == "\r": + last = String(last[byte = 0 : len(last) - 1]) + if len(last) > 0: + lines.append(last) + + return lines^ + + +def strip_comment(line: String) -> String: + """Removes a ``#``-style comment from a line. + + Returns everything before the first ``#`` character. If there is + no ``#``, the line is returned unchanged. + + This is a composable primitive — use it in combination with + ``strip()`` and ``is_blank()`` for full line processing. + + Examples:: + + strip_comment("1+2 # add") → "1+2 " + strip_comment("# comment") → "" + strip_comment("sqrt(2)") → "sqrt(2)" + strip_comment("") → "" + """ + var n = len(line) + if n == 0: + return String("") + + var bytes = StringSlice(line).as_bytes() + var ptr = bytes.unsafe_ptr() + + for i in range(n): + if ptr[i] == 35: # '#' + if i == 0: + return String("") + return String(line[byte=0:i]) + + return line + + +def is_blank(line: String) -> Bool: + """Returns True if the line is empty or contains only whitespace + (spaces and tabs). + + This is a composable primitive — combine with ``strip_comment()`` + to check for comment-or-blank lines. + """ + var n = len(line) + if n == 0: + return True + + var bytes = StringSlice(line).as_bytes() + var ptr = bytes.unsafe_ptr() + + for i in range(n): + var c = ptr[i] + if c != 32 and c != 9: # not space, not tab + return False + + return True + + +def is_comment_or_blank(line: String) -> Bool: + """Returns True if the line is blank, whitespace-only, or a comment + (first non-whitespace character is ``#``). + + Equivalent to ``is_blank(strip_comment(line))``. Provided as a + convenience for callers that do not need the intermediate results. + """ + return is_blank(strip_comment(line)) + + +def strip(s: String) -> String: + """Strips leading and trailing whitespace from a string. + + Removes spaces (32), tabs (9), carriage returns (13), and + newlines (10). + """ + var bytes = StringSlice(s).as_bytes() + var ptr = bytes.unsafe_ptr() + var start = 0 + var end = len(s) + + while start < end: + var c = ptr[start] + # space=32, tab=9, \r=13, \n=10 + if c != 32 and c != 9 and c != 13 and c != 10: + break + start += 1 + + while end > start: + var c = ptr[end - 1] + if c != 32 and c != 9 and c != 13 and c != 10: + break + end -= 1 + + if start >= end: + return String("") + return String(s[byte=start:end]) + + +def filter_expression_lines(lines: List[String]) -> List[String]: + """Filters a list of lines to only those that are valid expressions. + + Removes blank lines and comment lines (starting with ``#``). + Also strips inline comments and leading/trailing whitespace from + each expression line. + """ + var result = List[String]() + for i in range(len(lines)): + var line = strip(strip_comment(lines[i])) + if len(line) > 0: + result.append(line) + return result^ + + +# ===----------------------------------------------------------------------=== # +# File reading +# ===----------------------------------------------------------------------=== # + + +def read_file_text(path: String) raises -> String: + """Reads the entire contents of a file and returns it as a String. + + Uses POSIX ``open()`` + ``dup2()`` + ``getchar()`` to read the file + by temporarily redirecting stdin. This avoids FFI signature conflicts + with Mojo's stdlib and ArgMojo for ``read``/``fclose``. + + The original stdin is saved via ``dup()`` before redirection and + restored afterwards, so callers (e.g. a future REPL ``:load`` command) + can continue reading from the real stdin after this call returns. + + Args: + path: The file path to read. + + Raises: + If the file cannot be opened. + """ + var c_path = _to_cstr(path) + + # Save original stdin so we can restore it after reading. + var saved_stdin = external_call["dup", Int32](Int32(0)) + if saved_stdin < 0: + raise Error("cannot save stdin (dup failed)") + + # open(path, O_RDONLY=0) + var fd = external_call["open", Int32](c_path.unsafe_ptr(), Int32(0)) + if fd < 0: + # Restore stdin before raising. + _ = external_call["dup2", Int32](saved_stdin, Int32(0)) + _ = external_call["close", Int32](saved_stdin) + raise Error("cannot open file: " + path) + + # Redirect stdin (fd 0) to the file + var dup_result = external_call["dup2", Int32](fd, Int32(0)) + # Close the original fd — dup2 made a copy on fd 0. + _ = external_call["close", Int32](fd) + + if dup_result < 0: + # Restore stdin before raising. + _ = external_call["dup2", Int32](saved_stdin, Int32(0)) + _ = external_call["close", Int32](saved_stdin) + raise Error("cannot redirect stdin to file: " + path) + + # Read all bytes via getchar() + var chunks = List[UInt8]() + while True: + var c = external_call["getchar", Int32]() + if c < 0: # EOF + break + chunks.append(UInt8(c)) + + # Restore original stdin. + _ = external_call["dup2", Int32](saved_stdin, Int32(0)) + _ = external_call["close", Int32](saved_stdin) + + if len(chunks) == 0: + return String("") + + return String(unsafe_from_utf8=chunks^) + + +def file_exists(path: String) -> Bool: + """Returns True if the given path exists as a readable file. + + Uses the POSIX ``access()`` syscall with ``R_OK`` (4). + """ + var c_path = _to_cstr(path) + # access(path, R_OK=4) returns 0 on success + return external_call["access", Int32](c_path.unsafe_ptr(), Int32(4)) == 0 + + +# ===----------------------------------------------------------------------=== # +# Internal helpers +# ===----------------------------------------------------------------------=== # + + +def _to_cstr(s: String) -> List[UInt8]: + """Converts a Mojo String to a null-terminated C string (List[UInt8]).""" + var bytes = StringSlice(s).as_bytes() + var c = List[UInt8](capacity=len(bytes) + 1) + for i in range(len(bytes)): + c.append(bytes[i]) + c.append(0) + return c^ diff --git a/src/cli/main.mojo b/src/cli/main.mojo index f1555da8..006a79ab 100644 --- a/src/cli/main.mojo +++ b/src/cli/main.mojo @@ -5,8 +5,10 @@ # Decimo (BigDecimal) and ArgMojo (CLI parsing). # # Usage: -# mojo run -I src -I src/cli src/cli/main.mojo "100 * 12 - 23/17" -p 50 -# ./decimo "100 * 12 - 23/17" -p 50 +# mojo run -I src -I src/cli src/cli/main.mojo "100 * 12 - 23/17" -P 50 +# ./decimo "100 * 12 - 23/17" -P 50 +# echo "1+2" | mojo run -I src -I src/cli src/cli/main.mojo +# mojo run -I src -I src/cli src/cli/main.mojo -F expressions.dm -P 100 # ===----------------------------------------------------------------------=== # from std.sys import exit @@ -17,18 +19,34 @@ from calculator.tokenizer import tokenize from calculator.parser import parse_to_rpn from calculator.evaluator import evaluate_rpn, final_round from calculator.display import print_error +from calculator.io import ( + stdin_is_tty, + read_stdin, + split_into_lines, + filter_expression_lines, + read_file_text, +) struct DecimoArgs(Parsable): var expr: Positional[ String, - help="Math expression to evaluate (e.g. 'sqrt(abs(1.1*-12-23/17))')", - required=True, + help="Math expression to evaluate (e.g. 'sqrt(2)', '1/3 + pi')", + required=False, + ] + var file: Option[ + String, + long="file", + short="F", + help="Evaluate expressions from a file (one per line)", + default="", + value_name="PATH", + group="Input", ] var precision: Option[ Int, long="precision", - short="p", + short="P", help="Number of significant digits", default="50", value_name="N", @@ -39,26 +57,25 @@ struct DecimoArgs(Parsable): ] var scientific: Flag[ long="scientific", - short="s", + short="S", help="Output in scientific notation (e.g. 1.23E+10)", group="Formatting", ] var engineering: Flag[ long="engineering", - short="e", + short="E", help="Output in engineering notation (exponent multiple of 3)", group="Formatting", ] var pad: Flag[ long="pad", - short="P", help="Pad trailing zeros to the specified precision", group="Formatting", ] var delimiter: Option[ String, long="delimiter", - short="d", + short="D", help="Digit-group separator inserted every 3 digits (e.g. '_' gives 1_234.567_89)", default="", value_name="CHAR", @@ -67,7 +84,7 @@ struct DecimoArgs(Parsable): var rounding_mode: Option[ String, long="rounding-mode", - short="r", + short="R", help="Rounding mode for the final result", default="half-even", choices="half-even,half-up,half-down,up,down,ceiling,floor", @@ -101,7 +118,7 @@ def main(): def _run() raises: var cmd = DecimoArgs.to_command() - cmd.usage("decimo [OPTIONS] ") + cmd.usage("decimo [OPTIONS] [EXPR]") cmd.mutually_exclusive(["scientific", "engineering"]) # Allow expressions starting with '-' (e.g. "-3*pi*(sin(1))") to be # treated as positional values rather than option flags. @@ -113,9 +130,10 @@ def _run() raises: 'If your expression contains *, ( or ), quote it: decimo "2 * (3 + 4)"' ) cmd.add_tip("Or use noglob: alias decimo='noglob decimo' (add to ~/.zshrc)") + cmd.add_tip("Pipe expressions: echo '1/3' | decimo -P 100") + cmd.add_tip("Evaluate a file: decimo -F expressions.dm -P 50") var args = DecimoArgs.parse_from_command(cmd^) - var expr = args.expr.value var precision = args.precision.value var scientific = args.scientific.value var engineering = args.engineering.value @@ -123,14 +141,174 @@ def _run() raises: var delimiter = args.delimiter.value var rounding_mode = _parse_rounding_mode(args.rounding_mode.value) - # ── Phase 1: Tokenize & parse ────────────────────────────────────────── + # ── Mode detection ───────────────────────────────────────────────────── + # 1. --file flag provided → file mode + # 2. Positional expr provided → expression mode (one-shot) + # 3. No expr, stdin is piped → pipe mode + # 4. No expr, stdin is a TTY → error (no input) + + var has_file = len(args.file.value) > 0 + var has_expr = len(args.expr.value) > 0 + + if has_file and has_expr: + # Ambiguous: both --file and a positional expression were given. + print_error("cannot use both -F/--file and a positional expression") + exit(1) + elif has_file: + # ── File mode ──────────────────────────────────────────────────── + _run_file_mode( + args.file.value, + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, + ) + elif has_expr: + # ── Expression mode (one-shot) ─────────────────────────────────── + try: + _evaluate_and_print( + args.expr.value, + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, + show_expr_on_error=True, + ) + except: + exit(1) + elif not stdin_is_tty(): + # ── Pipe mode ──────────────────────────────────────────────────── + _run_pipe_mode( + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, + ) + else: + # No expression, no file, no pipe — show help. + print_error("no expression provided") + print( + "Usage: decimo [OPTIONS] [EXPR]\n" + " echo 'EXPR' | decimo [OPTIONS]\n" + " decimo -F FILE [OPTIONS]\n" + "\n" + "Run 'decimo --help' for more information." + ) + exit(1) + + +# ===----------------------------------------------------------------------=== # +# Mode implementations +# ===----------------------------------------------------------------------=== # + + +def _run_pipe_mode( + precision: Int, + scientific: Bool, + engineering: Bool, + pad: Bool, + delimiter: String, + rounding_mode: RoundingMode, +) raises: + """Read expressions from stdin (one per line) and evaluate each.""" + var text = read_stdin() + if len(text) == 0: + return + + var expressions = filter_expression_lines(split_into_lines(text)) + var had_error = False + + for i in range(len(expressions)): + try: + _evaluate_and_print( + expressions[i], + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, + show_expr_on_error=True, + ) + except: + had_error = True + # Continue processing remaining lines + + if had_error: + exit(1) + + +def _run_file_mode( + path: String, + precision: Int, + scientific: Bool, + engineering: Bool, + pad: Bool, + delimiter: String, + rounding_mode: RoundingMode, +) raises: + """Reads expressions from a file (one per line) and evaluates each.""" + var text: String + try: + text = read_file_text(path) + except e: + print_error("cannot read file '" + path + "': " + String(e)) + exit(1) + return # Unreachable, but keeps the compiler happy + + var expressions = filter_expression_lines(split_into_lines(text)) + var had_error = False + + for i in range(len(expressions)): + try: + _evaluate_and_print( + expressions[i], + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, + show_expr_on_error=True, + ) + except: + had_error = True + # Continue processing remaining lines + + if had_error: + exit(1) + + +# ===----------------------------------------------------------------------=== # +# Core evaluation and formatting +# ===----------------------------------------------------------------------=== # + + +def _evaluate_and_print( + expr: String, + precision: Int, + scientific: Bool, + engineering: Bool, + pad: Bool, + delimiter: String, + rounding_mode: RoundingMode, + show_expr_on_error: Bool = False, +) raises: + """Tokenize, parse, evaluate, and print one expression. + + On error, displays a coloured diagnostic and raises to signal failure + to the caller. + """ try: var tokens = tokenize(expr) var rpn = parse_to_rpn(tokens^) - # ── Phase 2: Evaluate ──────────────────────────────────────────── - # Syntax was fine — any error here is a math error (division by - # zero, negative sqrt, …). No glob hint needed. try: var value = final_round( evaluate_rpn(rpn^, precision), precision, rounding_mode @@ -149,16 +327,22 @@ def _run() raises: else: print(value.to_string(delimiter=delimiter)) except eval_err: - _display_calc_error(String(eval_err), expr) - exit(1) + if show_expr_on_error: + _display_calc_error(String(eval_err), expr) + else: + print_error(String(eval_err)) + raise eval_err^ except parse_err: - _display_calc_error(String(parse_err), expr) - exit(1) + if show_expr_on_error: + _display_calc_error(String(parse_err), expr) + else: + print_error(String(parse_err)) + raise parse_err^ def _display_calc_error(error_msg: String, expr: String): - """Parse a calculator error message and display it with colours + """Parses a calculator error message and displays it with colours and a caret indicator. The calculator engine produces errors in two forms: @@ -201,7 +385,7 @@ def _display_calc_error(error_msg: String, expr: String): def _pad_to_precision(plain: String, precision: Int) -> String: - """Pad (or add) trailing zeros so the fractional part has exactly + """Pads (or adds) trailing zeros so the fractional part has exactly `precision` digits. """ if precision <= 0: @@ -225,7 +409,8 @@ def _pad_to_precision(plain: String, precision: Int) -> String: def _parse_rounding_mode(name: String) -> RoundingMode: - """Convert a CLI rounding-mode name (hyphenated) to a RoundingMode value.""" + """Converts a CLI rounding-mode name (hyphenated) to a RoundingMode value. + """ if name == "half-even": return RoundingMode.half_even() elif name == "half-up": diff --git a/tests/cli/test_io.mojo b/tests/cli/test_io.mojo new file mode 100644 index 00000000..ab3e012b --- /dev/null +++ b/tests/cli/test_io.mojo @@ -0,0 +1,252 @@ +"""Test the I/O utilities: line splitting, comment handling, text processing.""" + +from std import testing + +from calculator.io import ( + split_into_lines, + strip_comment, + is_blank, + is_comment_or_blank, + strip, + filter_expression_lines, + file_exists, + _to_cstr, +) + + +# ===----------------------------------------------------------------------=== # +# split_into_lines +# ===----------------------------------------------------------------------=== # + + +def test_split_empty() raises: + var lines = split_into_lines(String("")) + testing.assert_equal(len(lines), 0) + + +def test_split_single_line() raises: + var lines = split_into_lines(String("hello")) + testing.assert_equal(len(lines), 1) + testing.assert_equal(lines[0], "hello") + + +def test_split_multiple_lines() raises: + var lines = split_into_lines(String("a\nb\nc")) + testing.assert_equal(len(lines), 3) + testing.assert_equal(lines[0], "a") + testing.assert_equal(lines[1], "b") + testing.assert_equal(lines[2], "c") + + +def test_split_trailing_newline() raises: + var lines = split_into_lines(String("a\nb\n")) + testing.assert_equal(len(lines), 2) + testing.assert_equal(lines[0], "a") + testing.assert_equal(lines[1], "b") + + +def test_split_blank_lines() raises: + var lines = split_into_lines(String("a\n\nb\n\n")) + testing.assert_equal(len(lines), 4) + testing.assert_equal(lines[0], "a") + testing.assert_equal(lines[1], "") + testing.assert_equal(lines[2], "b") + testing.assert_equal(lines[3], "") + + +def test_split_crlf() raises: + var lines = split_into_lines(String("a\r\nb\r\n")) + testing.assert_equal(len(lines), 2) + testing.assert_equal(lines[0], "a") + testing.assert_equal(lines[1], "b") + + +# ===----------------------------------------------------------------------=== # +# strip_comment +# ===----------------------------------------------------------------------=== # + + +def test_strip_comment_empty() raises: + testing.assert_equal(strip_comment(String("")), "") + + +def test_strip_comment_no_hash() raises: + testing.assert_equal(strip_comment(String("1+2")), "1+2") + testing.assert_equal(strip_comment(String("sqrt(2)")), "sqrt(2)") + + +def test_strip_comment_full_line_comment() raises: + testing.assert_equal(strip_comment(String("# comment")), "") + testing.assert_equal(strip_comment(String("#")), "") + + +def test_strip_comment_inline_comment() raises: + testing.assert_equal(strip_comment(String("1+2 # add")), "1+2 ") + testing.assert_equal(strip_comment(String("pi # constant")), "pi ") + testing.assert_equal(strip_comment(String("sqrt(2)#no space")), "sqrt(2)") + + +def test_strip_comment_indented_comment() raises: + testing.assert_equal(strip_comment(String(" # indented")), " ") + + +# ===----------------------------------------------------------------------=== # +# is_blank +# ===----------------------------------------------------------------------=== # + + +def test_is_blank_empty() raises: + testing.assert_true(is_blank(String(""))) + + +def test_is_blank_whitespace() raises: + testing.assert_true(is_blank(String(" "))) + testing.assert_true(is_blank(String("\t\t"))) + testing.assert_true(is_blank(String(" \t "))) + + +def test_is_blank_not_blank() raises: + testing.assert_false(is_blank(String("1+2"))) + testing.assert_false(is_blank(String(" x"))) + testing.assert_false(is_blank(String("#"))) + + +# ===----------------------------------------------------------------------=== # +# strip +# ===----------------------------------------------------------------------=== # + + +def test_strip_basic() raises: + testing.assert_equal(strip(String(" hello ")), "hello") + testing.assert_equal(strip(String("\thello\t")), "hello") + testing.assert_equal(strip(String(" hello")), "hello") + testing.assert_equal(strip(String("hello ")), "hello") + + +def test_strip_empty() raises: + testing.assert_equal(strip(String("")), "") + testing.assert_equal(strip(String(" ")), "") + + +def test_strip_no_change() raises: + testing.assert_equal(strip(String("hello")), "hello") + + +# ===----------------------------------------------------------------------=== # +# is_comment_or_blank (backward compat / composition) +# ===----------------------------------------------------------------------=== # + + +def test_blank_line() raises: + testing.assert_true(is_comment_or_blank(String(""))) + + +def test_whitespace_only() raises: + testing.assert_true(is_comment_or_blank(String(" "))) + testing.assert_true(is_comment_or_blank(String("\t\t"))) + testing.assert_true(is_comment_or_blank(String(" \t "))) + + +def test_comment_line() raises: + testing.assert_true(is_comment_or_blank(String("# comment"))) + testing.assert_true(is_comment_or_blank(String(" # indented comment"))) + testing.assert_true(is_comment_or_blank(String("\t# tab comment"))) + + +def test_expression_line() raises: + testing.assert_false(is_comment_or_blank(String("1+2"))) + testing.assert_false(is_comment_or_blank(String(" sqrt(2)"))) + testing.assert_false(is_comment_or_blank(String("pi"))) + + +# ===----------------------------------------------------------------------=== # +# filter_expression_lines +# ===----------------------------------------------------------------------=== # + + +def test_filter_basic() raises: + var lines = List[String]() + lines.append(String("# comment")) + lines.append(String("")) + lines.append(String("1+2")) + lines.append(String(" sqrt(2) ")) + lines.append(String("")) + lines.append(String("# another comment")) + lines.append(String("pi")) + var filtered = filter_expression_lines(lines) + testing.assert_equal(len(filtered), 3) + testing.assert_equal(filtered[0], "1+2") + testing.assert_equal(filtered[1], "sqrt(2)") + testing.assert_equal(filtered[2], "pi") + + +# ===----------------------------------------------------------------------=== # +# file_exists +# ===----------------------------------------------------------------------=== # + + +def test_file_exists_nonexistent() raises: + # Use a path with enough entropy to avoid false positives on any machine. + testing.assert_false( + file_exists(String("/tmp/_decimo_no_such_file_a1b2c3d4e5f6_.dm")) + ) + + +# ===----------------------------------------------------------------------=== # +# _to_cstr +# ===----------------------------------------------------------------------=== # + + +def test_to_cstr() raises: + var result = _to_cstr(String("hello")) + testing.assert_equal(len(result), 6) # 5 chars + null terminator + testing.assert_equal(result[5], UInt8(0)) + + +# ===----------------------------------------------------------------------=== # +# main +# ===----------------------------------------------------------------------=== # + + +def main() raises: + # split_into_lines + test_split_empty() + test_split_single_line() + test_split_multiple_lines() + test_split_trailing_newline() + test_split_blank_lines() + test_split_crlf() + + # strip_comment + test_strip_comment_empty() + test_strip_comment_no_hash() + test_strip_comment_full_line_comment() + test_strip_comment_inline_comment() + test_strip_comment_indented_comment() + + # is_blank + test_is_blank_empty() + test_is_blank_whitespace() + test_is_blank_not_blank() + + # strip + test_strip_basic() + test_strip_empty() + test_strip_no_change() + + # is_comment_or_blank (backward compat) + test_blank_line() + test_whitespace_only() + test_comment_line() + test_expression_line() + + # filter_expression_lines + test_filter_basic() + + # file_exists + test_file_exists_nonexistent() + + # _to_cstr + test_to_cstr() + + print("All io tests passed!") diff --git a/tests/test_cli.sh b/tests/test_cli.sh index 8ed0a87c..b6f044d3 100644 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -37,32 +37,32 @@ assert_output() { # Basic expression assert_output "basic addition" "5" "$BINARY" "2+3" -# Precision flag (-p) -assert_output "precision -p 10" "0.3333333333" "$BINARY" "1/3" -p 10 +# Precision flag (-P) +assert_output "precision -P 10" "0.3333333333" "$BINARY" "1/3" -P 10 -# Scientific notation (--scientific / -s) +# Scientific notation (--scientific / -S) assert_output "scientific notation" "1.2345678E+4" "$BINARY" "12345.678" --scientific -# Engineering notation (--engineering / -e) +# Engineering notation (--engineering / -E) assert_output "engineering notation" "12.345678E+3" "$BINARY" "12345.678" --engineering -# Delimiter flag (-d) -assert_output "delimiter underscore" "1_234_567.89" "$BINARY" "1234567.89" -d "_" +# Delimiter flag (-D) +assert_output "delimiter underscore" "1_234_567.89" "$BINARY" "1234567.89" -D "_" -# Rounding mode (--rounding-mode / -r) -assert_output "rounding mode ceiling" "0.33334" "$BINARY" "1/3" -p 5 -r ceiling +# Rounding mode (--rounding-mode / -R) +assert_output "rounding mode ceiling" "0.33334" "$BINARY" "1/3" -P 5 -R ceiling -# Pad flag (--pad / -P) -assert_output "pad trailing zeros" "0.33333" "$BINARY" "1/3" -p 5 --pad +# Pad flag (--pad) +assert_output "pad trailing zeros" "0.33333" "$BINARY" "1/3" -P 5 --pad # ── Negative expressions (allow_hyphen) ─────────────────────────────────── assert_output "negative number" "-3.14" "$BINARY" "-3.14" assert_output "negative integer" "-42" "$BINARY" "-42" assert_output "negative expression" "-6" "$BINARY" "-3*2" -assert_output "negative expression with pi" "-9.424777961" "$BINARY" "-3*pi" -p 10 -assert_output "negative expr with abs()" "-3" "$BINARY" "-abs(3)" -p 10 -assert_output "negative expression complex" "17.32428719" "$BINARY" "-3*pi/sin(10)" -p 10 -assert_output "negative expression with parens" "-7.9306771922443685366581979690091558499739419154171" "$BINARY" "-3*pi*(sin(1))" -p 50 +assert_output "negative expression with pi" "-9.424777961" "$BINARY" "-3*pi" -P 10 +assert_output "negative expr with abs()" "-3" "$BINARY" "-abs(3)" -P 10 +assert_output "negative expression complex" "17.32428719" "$BINARY" "-3*pi/sin(10)" -P 10 +assert_output "negative expression with parens" "-7.9306771922443685366581979690091558499739419154171" "$BINARY" "-3*pi*(sin(1))" -P 50 assert_output "negative zero" "-0" "$BINARY" "-0" assert_output "negative minus negative" "1" "$BINARY" "-1*-1" assert_output "negative power" "-8" "$BINARY" "-2^3" @@ -70,24 +70,29 @@ assert_output "negative cancel out" "0" "$BINARY" "-1+1" assert_output "negative subtraction" "-50" "$BINARY" "-100+50" # ── Option/positional ordering ──────────────────────────────────────────── -assert_output "options before expr" "0.3333333333" "$BINARY" -p 10 "1/3" -assert_output "options after expr" "0.3333333333" "$BINARY" "1/3" -p 10 -assert_output "multiple options before expr" "1.732428719E+1" "$BINARY" -s -p 10 "-3*pi/sin(10)" -assert_output "mixed order: flag expr option" "1.732428719E+1" "$BINARY" -s "-3*pi/sin(10)" -p 10 -assert_output "mixed order: option expr flag" "1.732428719E+1" "$BINARY" -p 10 "-3*pi/sin(10)" -s -assert_output "engineering before expr" "-12.345678E+3" "$BINARY" -e "-12345.678" -assert_output "engineering after expr" "-12.345678E+3" "$BINARY" "-12345.678" -e -assert_output "delimiter before expr" "3.141_592_654" "$BINARY" -d _ -p 10 "pi" -assert_output "delimiter after expr" "3.141_592_654" "$BINARY" "pi" -p 10 -d _ -assert_output "rounding before expr" "0.33334" "$BINARY" -p 5 -r ceiling "1/3" -assert_output "all options before expr" "0.33334" "$BINARY" -p 5 -r ceiling --pad "1/3" -assert_output "all options after expr" "0.33334" "$BINARY" "1/3" -p 5 -r ceiling --pad +assert_output "options before expr" "0.3333333333" "$BINARY" -P 10 "1/3" +assert_output "options after expr" "0.3333333333" "$BINARY" "1/3" -P 10 +assert_output "multiple options before expr" "1.732428719E+1" "$BINARY" -S -P 10 "-3*pi/sin(10)" +assert_output "mixed order: flag expr option" "1.732428719E+1" "$BINARY" -S "-3*pi/sin(10)" -P 10 +assert_output "mixed order: option expr flag" "1.732428719E+1" "$BINARY" -P 10 "-3*pi/sin(10)" -S +assert_output "engineering before expr" "-12.345678E+3" "$BINARY" -E "-12345.678" +assert_output "engineering after expr" "-12.345678E+3" "$BINARY" "-12345.678" -E +assert_output "delimiter before expr" "3.141_592_654" "$BINARY" -D _ -P 10 "pi" +assert_output "delimiter after expr" "3.141_592_654" "$BINARY" "pi" -P 10 -D _ +assert_output "rounding before expr" "0.33334" "$BINARY" -P 5 -R ceiling "1/3" +assert_output "all options before expr" "0.33334" "$BINARY" -P 5 -R ceiling --pad "1/3" +assert_output "all options after expr" "0.33334" "$BINARY" "1/3" -P 5 -R ceiling --pad # ── Double-dash separator ───────────────────────────────────────────────── assert_output "-- with negative expr" "-6" "$BINARY" -- "-3*2" assert_output "-- with negative number" "-3.14" "$BINARY" -- "-3.14" assert_output "-- with -e as expr" "-2.7182818284590452353602874713526624977572470937000" "$BINARY" -- "-e" +# ── Expressions that previously collided with short flags ───────────────── +assert_output "-e as expr (no --)" "-2.7182818284590452353602874713526624977572470937000" "$BINARY" "-e" +assert_output "-pi as expr" "-3.1415926535897932384626433832795028841971693993751" "$BINARY" "-pi" +assert_output "-sin(1) as expr" "-0.84147098480789650665250232163029899962256306079837" "$BINARY" "-sin(1)" + # ── Bare hyphen rejection ───────────────────────────────────────────────── if "$BINARY" -- - >/dev/null 2>&1; then echo "FAIL: bare hyphen should be rejected" @@ -96,6 +101,82 @@ else PASS=$((PASS + 1)) fi +# ── Pipe mode (stdin) ──────────────────────────────────────────────────── +assert_pipe_output() { + local description="$1" + local input="$2" + local expected="$3" + shift 3 + local actual + actual=$(printf '%s' "$input" | "$BINARY" "$@" 2>&1) + if [[ "$actual" == "$expected" ]]; then + PASS=$((PASS + 1)) + else + echo "FAIL: $description" + echo " expected: $expected" + echo " actual: $actual" + FAIL=$((FAIL + 1)) + fi +} + +assert_pipe_output "pipe single expression" "1+2" "3" +assert_pipe_output "pipe sqrt" "sqrt(2)" "1.4142135623730950488016887242096980785696718753769" +assert_pipe_output "pipe with precision" "1/3" "0.3333333333" -P 10 +assert_pipe_output "pipe multiple lines" \ + "$(printf '1+2\nsqrt(4)\npi')" \ + "$(printf '3\n2\n3.1415926535897932384626433832795028841971693993751')" +assert_pipe_output "pipe skip comments" \ + "$(printf '# comment\n1+2\n\n# another\nsqrt(4)')" \ + "$(printf '3\n2')" +assert_pipe_output "pipe skip blank lines" \ + "$(printf '\n\n1+2\n\n')" \ + "3" +assert_pipe_output "pipe with scientific" "12345.678" "1.2345678E+4" -S +assert_pipe_output "pipe with engineering" "12345.678" "12.345678E+3" -E +assert_pipe_output "pipe with delimiter" "1234567.89" "1_234_567.89" -D "_" + +# ── File mode (-F/--file flag) ──────────────────────────────────────────── +TMPFILE=$(mktemp /tmp/decimo_test_XXXXXX.dm) +cat > "$TMPFILE" << 'FILE_EOF' +# Test expression file +pi +e +sqrt(2) + +# Arithmetic +100 * 12 - 23/17 +FILE_EOF + +assert_output "file mode basic" \ + "$(printf '3.1415926535897932384626433832795028841971693993751\n2.7182818284590452353602874713526624977572470937000\n1.4142135623730950488016887242096980785696718753769\n1198.6470588235294117647058823529411764705882352941')" \ + "$BINARY" -F "$TMPFILE" + +assert_output "file mode with precision" \ + "$(printf '3.141592654\n2.718281828\n1.414213562\n1198.647059')" \ + "$BINARY" -F "$TMPFILE" -P 10 + +rm -f "$TMPFILE" + +# File mode: nonexistent file gives a clear error +NONEXIST_OUTPUT=$("$BINARY" -F "nonexistent_file.dm" 2>&1 || true) +if echo "$NONEXIST_OUTPUT" | grep -qi "cannot read file"; then + PASS=$((PASS + 1)) +else + echo "FAIL: file mode nonexistent should report 'cannot read file'" + echo " actual: $NONEXIST_OUTPUT" + FAIL=$((FAIL + 1)) +fi + +# File mode + positional expr should be rejected +BOTH_OUTPUT=$("$BINARY" -F "nonexistent_file.dm" "1+2" 2>&1 || true) +if echo "$BOTH_OUTPUT" | grep -qi "cannot use both"; then + PASS=$((PASS + 1)) +else + echo "FAIL: -F + positional expr should be rejected" + echo " actual: $BOTH_OUTPUT" + FAIL=$((FAIL + 1)) +fi + echo "" echo "CLI integration tests: $PASS passed, $FAIL failed" From d8865dfa1d7a12efbcab96a4f2c609db5c039382 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sun, 12 Apr 2026 01:43:58 +0200 Subject: [PATCH 14/17] [cli][doc] Add benchmarks against bc and python + Add shell completion guide in user manual (#204) --- benches/cli/bench_cli.sh | 372 ++++++++++++++++++++++++++++++ benches/run_bench.sh | 13 +- docs/changelog.md | 2 + docs/plans/cli_calculator.md | 4 +- docs/user_manual_cli.md | 56 +++++ pixi.toml | 4 +- tests/cli/test_data/basic.dm | 9 + tests/cli/test_data/comments.txt | 18 ++ tests/cli/test_data/edge_cases.dm | 24 ++ tests/cli/test_data/precision.dm | 17 ++ tests/cli/test_data/torture | 41 ++++ tests/test_cli.sh | 66 ++++-- 12 files changed, 601 insertions(+), 25 deletions(-) create mode 100644 benches/cli/bench_cli.sh create mode 100644 tests/cli/test_data/basic.dm create mode 100644 tests/cli/test_data/comments.txt create mode 100644 tests/cli/test_data/edge_cases.dm create mode 100644 tests/cli/test_data/precision.dm create mode 100644 tests/cli/test_data/torture diff --git a/benches/cli/bench_cli.sh b/benches/cli/bench_cli.sh new file mode 100644 index 00000000..87bcd640 --- /dev/null +++ b/benches/cli/bench_cli.sh @@ -0,0 +1,372 @@ +#!/bin/bash +# ===----------------------------------------------------------------------=== # +# CLI Calculator Benchmarks — Correctness & Performance +# +# Compares decimo against bc and python3 on every expression: +# 1. Correctness — first 15 significant digits must agree +# 2. Performance — average wall-clock latency over $ITERATIONS runs +# +# Usage: +# bash benches/cli/bench_cli.sh +# ITERATIONS=20 bash benches/cli/bench_cli.sh +# +# Requirements: +# - ./decimo binary (pixi run mojo build -I src -I src/cli src/cli/main.mojo -o decimo) +# - perl with Time::HiRes (standard on macOS) +# - bc (standard on macOS / Linux) +# - python3 with mpmath for function comparisons (pip install mpmath) +# ===----------------------------------------------------------------------=== # + +set -euo pipefail +export LC_ALL=C # consistent decimal formatting + +BINARY="${BINARY:-./decimo}" +export ITERATIONS="${ITERATIONS:-10}" +PREVIEW=35 # max chars of result preview + +# ── Prerequisites ────────────────────────────────────────────────────────── + +if ! [[ -x "$BINARY" ]]; then + echo "Error: $BINARY not found or not executable." + echo "Build first: pixi run mojo build -I src -I src/cli src/cli/main.mojo -o decimo" + exit 1 +fi + +HAS_BC=false; command -v bc &>/dev/null && HAS_BC=true +HAS_PY=false; command -v python3 &>/dev/null && HAS_PY=true + +# ── Counters ─────────────────────────────────────────────────────────────── + +COMPARISONS=0 +MATCHES=0 +MISMATCHES=0 +ERRORS=0 + +# ── Helpers ──────────────────────────────────────────────────────────────── + +# Time a command over $ITERATIONS runs, return average ms. +elapsed_ms() { + perl -MTime::HiRes=time -e ' + my $n = $ENV{ITERATIONS}; + my @cmd = @ARGV; + open(my $oldout, ">&", \*STDOUT); + open(my $olderr, ">&", \*STDERR); + # Warm-up (untimed) + open(STDOUT, ">/dev/null"); open(STDERR, ">/dev/null"); + system(@cmd); + open(STDOUT, ">&", $oldout); open(STDERR, ">&", $olderr); + # Timed + my $t0 = time(); + for (1 .. $n) { + open(STDOUT, ">/dev/null"); open(STDERR, ">/dev/null"); + system(@cmd); + open(STDOUT, ">&", $oldout); open(STDERR, ">&", $olderr); + } + printf "%.2f\n", (time() - $t0) * 1000.0 / $n; + ' -- "$@" +} + +# Extract a canonical comparison key from a numeric string: +# adjusted base-10 exponent + first 15 significant digits. +# This ensures values that differ only by exponent (e.g. 1E+10 vs 1E+11) +# are correctly detected as a MISMATCH. +sig_digits() { + local s="${1#-}" # strip sign; check_match handles sign separately + local explicit_exp=0 + local mantissa="$s" + + # Split off explicit exponent (e.g. 1.23E+45 → mantissa=1.23, exp=45) + if [[ "$mantissa" =~ ^([^eE]+)[eE]([+-]?[0-9]+)$ ]]; then + mantissa="${BASH_REMATCH[1]}" + explicit_exp="${BASH_REMATCH[2]}" + fi + + local int_part frac_part digits int_len adjusted_exp first_nonzero + + if [[ "$mantissa" == *.* ]]; then + int_part="${mantissa%%.*}" + frac_part="${mantissa#*.}" + # Strip trailing zeros from fractional part — they are not significant + frac_part=$(echo "$frac_part" | sed 's/0*$//') + else + int_part="$mantissa" + frac_part="" + fi + + digits="${int_part}${frac_part}" + digits=$(echo "$digits" | sed 's/^0*//; s/0*$//') + + if [[ -z "$digits" ]]; then + echo "ZERO" + return 0 + fi + + # Compute adjusted exponent so the key is position-independent + if [[ "$int_part" =~ [1-9] ]]; then + int_part=$(echo "$int_part" | sed 's/^0*//') + int_len=${#int_part} + adjusted_exp=$(( explicit_exp + int_len - 1 )) + else + first_nonzero=$(echo "$frac_part" | sed -n 's/^\(0*\)[1-9].*$/\1/p' | wc -c | tr -d ' ') + first_nonzero=$(( first_nonzero - 1 )) + adjusted_exp=$(( explicit_exp - first_nonzero - 1 )) + fi + + echo "${adjusted_exp}:$(echo "$digits" | cut -c1-15)" +} + +# Compare two results by leading significant digits. +check_match() { + local a="$1" b="$2" + local sign_a="" sign_b="" + if [[ "$a" == -* ]]; then sign_a="-"; fi + if [[ "$b" == -* ]]; then sign_b="-"; fi + if [[ "$sign_a" != "$sign_b" ]]; then echo "MISMATCH"; return 0; fi + local sa sb + sa=$(sig_digits "$a") + sb=$(sig_digits "$b") + if [[ "$sa" == "$sb" ]]; then echo "MATCH"; else echo "MISMATCH"; fi + return 0 +} + +# Truncate a result string for display. +preview() { + if (( ${#1} > PREVIEW )); then echo "${1:0:$PREVIEW}..."; else echo "$1"; fi +} + +# Record a comparison result. +record() { + local tag="$1" + if [[ "$tag" == "ERROR" ]]; then + ERRORS=$((ERRORS + 1)) + else + COMPARISONS=$((COMPARISONS + 1)) + if [[ "$tag" == "MATCH" ]]; then + MATCHES=$((MATCHES + 1)) + else + MISMATCHES=$((MISMATCHES + 1)) + fi + fi + return 0 +} + +# ── Main comparison driver ───────────────────────────────────────────────── +# +# bench_compare LABEL PREC DECIMO_EXPR [BC_EXPR] [PY_CODE] +# +# BC_EXPR: expression piped to "bc -l". "scale=PREC; " is prepended. +# Pass "" to skip bc. +# PY_CODE: full python3 -c code. "__P__" is replaced with PREC. +# Pass "" to skip python3. + +bench_compare() { + local label="$1" prec="$2" d_expr="$3" + local bc_expr="${4:-}" py_code="${5:-}" + + printf " %s (P=%s)\n" "$label" "$prec" + + # ── decimo ── + local d_result d_ms + d_result=$("$BINARY" "$d_expr" -P "$prec" 2>/dev/null || echo "ERROR") + d_ms=$(elapsed_ms "$BINARY" "$d_expr" -P "$prec") + printf " %-10s %-38s %8s ms\n" "decimo:" "$(preview "$d_result")" "$d_ms" + if [[ "$d_result" == "ERROR" ]]; then + record "ERROR" + echo "" + return + fi + + # ── bc ── + if [[ -n "$bc_expr" ]] && $HAS_BC; then + local full_bc="scale=$prec; $bc_expr" + local b_result b_ms tag + # tr -d '\\\n' removes bc's backslash line-continuations + b_result=$(echo "$full_bc" | bc -l 2>/dev/null | tr -d '\\\n' || echo "ERROR") + b_ms=$(elapsed_ms bash -c "echo '$full_bc' | bc -l") + if [[ "$b_result" == "ERROR" ]]; then + tag="ERROR" + else + tag=$(check_match "$d_result" "$b_result") + fi + printf " %-10s %-38s %8s ms %s\n" "bc:" "$(preview "$b_result")" "$b_ms" "$tag" + record "$tag" + fi + + # ── python3 ── + if [[ -n "$py_code" ]] && $HAS_PY; then + local full_py="${py_code//__P__/$prec}" + local p_result p_ms tag + p_result=$(python3 -c "$full_py" 2>/dev/null || echo "ERROR") + p_ms=$(elapsed_ms python3 -c "$full_py") + if [[ "$p_result" == "ERROR" ]]; then + tag="ERROR" + else + tag=$(check_match "$d_result" "$p_result") + fi + printf " %-10s %-38s %8s ms %s\n" "python3:" "$(preview "$p_result")" "$p_ms" "$tag" + record "$tag" + fi + + echo "" +} + +# ── Python expression templates (__P__ → precision) ──────────────────────── + +PY_DEC="from decimal import Decimal as D,getcontext as gc;gc().prec=__P__" +PY_MP="from mpmath import mp;mp.dps=__P__" + +# ── Header ───────────────────────────────────────────────────────────────── + +echo "============================================================" +echo " Decimo CLI Benchmark — Correctness & Performance" +echo "============================================================" +echo "Binary: $BINARY" +echo "Iterations: $ITERATIONS per expression" +echo "Tools: decimo$(${HAS_BC} && echo ', bc')$(${HAS_PY} && echo ', python3')" +echo "Date: $(date '+%Y-%m-%d %H:%M')" +echo "" + +# ── 1. Arithmetic ────────────────────────────────────────────────────────── + +echo "--- 1. Arithmetic -------------------------------------------" +echo "" + +bench_compare "1 + 1" 50 \ + "1+1" \ + "1+1" \ + "print(1+1)" + +bench_compare "100*12 - 23/17" 50 \ + "100*12-23/17" \ + "100*12-23/17" \ + "${PY_DEC};print(D('100')*12-D('23')/D('17'))" + +bench_compare "2^256" 50 \ + "2^256" \ + "2^256" \ + "print(2**256)" + +bench_compare "1/7" 50 \ + "1/7" \ + "1/7" \ + "${PY_DEC};print(D(1)/D(7))" + +bench_compare "(1+2) * (3+4)" 50 \ + "(1+2)*(3+4)" \ + "(1+2)*(3+4)" \ + "print((1+2)*(3+4))" + +# ── 2. Functions ─────────────────────────────────────────────────────────── + +echo "--- 2. Functions (P=50) -------------------------------------" +echo "" + +bench_compare "sqrt(2)" 50 \ + "sqrt(2)" \ + "sqrt(2)" \ + "${PY_DEC};print(D(2).sqrt())" + +bench_compare "ln(2)" 50 \ + "ln(2)" \ + "l(2)" \ + "${PY_MP};print(mp.log(2))" + +bench_compare "exp(1)" 50 \ + "exp(1)" \ + "e(1)" \ + "${PY_MP};print(mp.exp(1))" + +bench_compare "sin(1)" 50 \ + "sin(1)" \ + "s(1)" \ + "${PY_MP};print(mp.sin(1))" + +bench_compare "cos(0)" 50 \ + "cos(0)" \ + "c(0)" \ + "${PY_MP};print(mp.cos(0))" + +bench_compare "root(27, 3)" 50 \ + "root(27, 3)" \ + "" \ + "${PY_MP};print(mp.cbrt(27))" + +bench_compare "log(256, 2)" 50 \ + "log(256, 2)" \ + "" \ + "${PY_MP};print(mp.log(256,2))" + +# ── 3. Precision scaling — sqrt(2) ──────────────────────────────────────── + +echo "--- 3. Precision scaling — sqrt(2) --------------------------" +echo "" + +for p in 50 100 200 500 1000; do + bench_compare "sqrt(2)" "$p" \ + "sqrt(2)" \ + "sqrt(2)" \ + "${PY_DEC};print(D(2).sqrt())" +done + +# ── 4. Precision scaling — pi ───────────────────────────────────────────── + +echo "--- 4. Precision scaling — pi -------------------------------" +echo "" + +for p in 50 100 200 500 1000; do + bench_compare "pi" "$p" \ + "pi" \ + "4*a(1)" \ + "${PY_MP};print(mp.pi)" +done + +# ── 5. Complex expressions ──────────────────────────────────────────────── + +echo "--- 5. Complex expressions ----------------------------------" +echo "" + +bench_compare "ln(exp(1))" 50 \ + "ln(exp(1))" \ + "" \ + "${PY_MP};print(mp.log(mp.exp(1)))" + +bench_compare "sin(pi/4) + cos(pi/4)" 50 \ + "sin(pi/4)+cos(pi/4)" \ + "s(4*a(1)/4)+c(4*a(1)/4)" \ + "${PY_MP};print(mp.sin(mp.pi/4)+mp.cos(mp.pi/4))" + +bench_compare "2^256 + 3^100" 50 \ + "2^256+3^100" \ + "2^256+3^100" \ + "print(2**256+3**100)" + +# ── 6. Pipe mode (decimo only) ──────────────────────────────────────────── + +echo "--- 6. Pipe mode (decimo only) ------------------------------" +echo " No bc/python3 equivalent for multi-line pipe processing." +echo "" + +for entry in \ + "3 simple exprs|printf '1+2\n3*4\n5/6\n' | $BINARY" \ + "5 mixed exprs|printf '1+2\nsqrt(2)\npi\nln(10)\n2^64\n' | $BINARY"; do + desc="${entry%%|*}" + cmd="${entry#*|}" + ms=$(elapsed_ms bash -c "$cmd") + printf " %-42s %8s ms\n" "pipe: $desc" "$ms" +done +echo "" + +# ── Summary ──────────────────────────────────────────────────────────────── + +echo "============================================================" +printf " Summary: %d comparisons — %d MATCH, %d MISMATCH" \ + "$COMPARISONS" "$MATCHES" "$MISMATCHES" +if (( ERRORS > 0 )); then + printf ", %d ERROR (tool missing or failed)" "$ERRORS" +fi +echo "" +echo "============================================================" + +if (( MISMATCHES > 0 )); then + exit 1 +fi diff --git a/benches/run_bench.sh b/benches/run_bench.sh index 92420189..06bd2b01 100755 --- a/benches/run_bench.sh +++ b/benches/run_bench.sh @@ -22,10 +22,11 @@ if [[ -z "$TYPE" ]]; then echo "Usage: pixi run bench [operation]" echo "" echo "Types:" - echo " bigint (int) BigInt benchmarks (BigInt10 vs BigInt vs Python int)" - echo " biguint (uint) BigUInt benchmarks (BigUInt vs Python int)" + echo " bigint (int) BigInt benchmarks (BigInt10 vs BigInt vs Python int)" + echo " biguint (uint) BigUInt benchmarks (BigUInt vs Python int)" echo " decimal128 (dec128) Decimal128 benchmarks (Decimal128 vs Python decimal)" - echo " bigdecimal (dec) BigDecimal benchmarks (BigDecimal vs Python decimal)" + echo " bigdecimal (dec) BigDecimal benchmarks (BigDecimal vs Python decimal)" + echo " cli CLI calculator end-to-end latency benchmarks" echo "" echo "Omit operation to get interactive menu for that type." echo "" @@ -33,6 +34,7 @@ if [[ -z "$TYPE" ]]; then echo " pixi run bench bigint add" echo " pixi run bench dec sqrt" echo " pixi run bench biguint" + echo " pixi run bench cli" exit 0 fi @@ -44,6 +46,11 @@ case "$TYPE" in dec) TYPE="bigdecimal" ;; esac +# --- CLI benchmarks (special case — shell script, not Mojo) --- +if [[ "$TYPE" == "cli" ]]; then + exec bash benches/cli/bench_cli.sh +fi + DIR="benches/$TYPE" if [[ ! -d "$DIR" ]]; then diff --git a/docs/changelog.md b/docs/changelog.md index 0fba57c4..9f393a9b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,8 @@ This is a list of changes for the Decimo package (formerly DeciMojo). 1. Add **pipe/stdin mode**: read expressions from standard input, one per line, when no positional argument is given and stdin is piped (e.g. `echo "1+2" | decimo`, `printf "pi\nsqrt(2)" | decimo -P 100`). Blank lines and comment lines (starting with `#`) are automatically skipped. 1. Add **file mode**: use `--file` / `-F` flag to evaluate expressions from a file, one per line (e.g. `decimo -F expressions.dm -P 50`). Comments (`#`), inline comments, and blank lines are skipped. All CLI flags (precision, formatting, rounding) apply to every expression. +1. Add **shell completion** documentation for Bash, Zsh, and Fish (`decimo --completions bash|zsh|fish`). +1. Add **CLI performance benchmarks** (`benches/cli/bench_cli.sh`) comparing correctness and timing against `bc` and `python3` across 47 comparisons — all results match to 15 significant digits; `decimo` is 3–4× faster than `python3 -c`. ### 🦋 Changed in v0.10.0 diff --git a/docs/plans/cli_calculator.md b/docs/plans/cli_calculator.md index cb19c516..afa71e44 100644 --- a/docs/plans/cli_calculator.md +++ b/docs/plans/cli_calculator.md @@ -324,8 +324,8 @@ Format the final `BigDecimal` result based on CLI flags: | 3.9 | Argument groups in help output | ✓ | `Computation` and `Formatting` groups in `--help` | | 3.10 | Custom usage line | ✓ | `Usage: decimo [OPTIONS] ` | | 3.11 | `Parsable.run()` override | ✗ | Move eval logic into `DecimoArgs.run()` for cleaner separation | -| 3.12 | Performance validation | ✗ | No CLI-level benchmarks yet | -| 3.13 | Documentation (user manual for CLI) | ✗ | `docs/user_manual_cli.md`; include shell completion setup | +| 3.12 | Performance validation | ✓ | `benches/cli/bench_cli.sh`; 47 correctness checks + timing vs `bc` and `python3` | +| 3.13 | Documentation (user manual for CLI) | ✓ | `docs/user_manual_cli.md`; includes shell completions setup and performance data | | 3.14 | Build and distribute as single binary | ✗ | | | 3.15 | Allow negative expressions | ✓ | `allow_hyphen=True` on `Positional`; `decimo "-3*pi*(sin(1))"` works | | 3.16 | Make short names upper cases to avoid expression collisions | ✓ | `-P`, `-S`, `-E`, `-D`, `-R`; `--pad` has no short name; `-e`, `-pi`, `-sin(1)` all work | diff --git a/docs/user_manual_cli.md b/docs/user_manual_cli.md index ebbef539..3d02a966 100644 --- a/docs/user_manual_cli.md +++ b/docs/user_manual_cli.md @@ -26,6 +26,8 @@ - [Quoting Expressions](#quoting-expressions) - [Negative Expressions](#negative-expressions) - [Using noglob](#using-noglob) + - [Shell Completions](#shell-completions) +- [Performance](#performance) - [Examples](#examples) - [Basic Arithmetic](#basic-arithmetic) - [High-Precision Calculations](#high-precision-calculations) @@ -400,6 +402,60 @@ alias decimo='noglob decimo' decimo 2*(3+4) ``` +### Shell Completions + +`decimo` can generate completion scripts for **Bash**, **Zsh**, and **Fish**. Tab-completion will suggest option names, rounding mode values, and file paths. + +**Zsh** — add to `~/.zshrc`: + +```zsh +eval "$(decimo --completions zsh)" +``` + +**Bash** — add to `~/.bashrc`: + +```bash +eval "$(decimo --completions bash)" +``` + +**Fish** — run once: + +```sh +decimo --completions fish | source +# Or persist: +decimo --completions fish > ~/.config/fish/completions/decimo.fish +``` + +After reloading your shell, pressing Tab after `decimo -` will show all available options, and pressing Tab after `--rounding-mode` will list the seven available modes. + +## Performance + +`decimo` compiles to a single native binary. For most expressions, end-to-end latency is dominated by process startup rather than computation. The benchmark suite verifies both **correctness** (results agree with `bc` and `python3` to 15 significant digits) and **performance** (wall-clock timing). + +Typical latencies (measured on Apple M1 Max, macOS): + +| Expression | Precision | `decimo` | `bc -l` | `python3` | Match | +| ------------------- | :-------: | -------: | ------: | --------: | :---: | +| `1 + 1` | 50 | ~6 ms | ~4 ms | ~18 ms | ✓ | +| `100*12 - 23/17` | 50 | ~6 ms | ~4 ms | ~21 ms | ✓ | +| `sqrt(2)` | 50 | ~5 ms | ~4 ms | ~21 ms | ✓ | +| `ln(2)` | 50 | ~5 ms | ~4 ms | ~41 ms | ✓ | +| `sin(1)` | 50 | ~6 ms | ~4 ms | ~41 ms | ✓ | +| `pi` | 50 | ~6 ms | ~4 ms | ~41 ms | ✓ | +| `sqrt(2)` | 1000 | ~6 ms | ~5 ms | ~22 ms | ✓ | +| `pi` | 1000 | ~49 ms | ~13 ms | ~40 ms | ✓ | +| pipe: 5 mixed exprs | 50 | ~8 ms | N/A | N/A | | + +Tokenizer and parser overhead is negligible — trivial (`1+1`) and moderate (`sqrt(2)`) expressions complete in ~5 ms. Computation time only becomes visible for expensive operations at very high precision (e.g., computing π to 1000 digits). + +`decimo` is **3–4× faster than `python3`** and comparable to `bc` (a lightweight BSD utility). + +To run the full benchmark (correctness + performance, all 3 tools): + +```bash +bash benches/cli/bench_cli.sh +``` + ## Examples ### Basic Arithmetic diff --git a/pixi.toml b/pixi.toml index f11e72a5..0496ead4 100644 --- a/pixi.toml +++ b/pixi.toml @@ -49,7 +49,7 @@ bgmp = "clear && pixi run buildgmp" buildgmp = "bash src/decimo/gmp/build_gmp_wrapper.sh" # tests (use the mojo testing tool) -test = "pixi run package && bash ./tests/test_all.sh" +test = "pixi run package && pixi run buildcli && 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) @@ -91,7 +91,7 @@ bcli = "clear && pixi run buildcli" # && pixi run mojo build -I src -I src/cli -I temp -o decimo src/cli/main.mojo""" buildcli = """pixi run mojo build -I src -I src/cli -o decimo src/cli/main.mojo""" tcli = "clear && pixi run testcli" -testcli = "bash tests/test_cli.sh" +testcli = "pixi run buildcli && bash tests/test_cli.sh" # python bindings (mojo4py) bpy = "clear && pixi run buildpy" diff --git a/tests/cli/test_data/basic.dm b/tests/cli/test_data/basic.dm new file mode 100644 index 00000000..f15a0b41 --- /dev/null +++ b/tests/cli/test_data/basic.dm @@ -0,0 +1,9 @@ +# Basic expressions - the standard test file for decimo. +# Tests constants, functions, and arithmetic. + +pi +e +sqrt(2) + +# Arithmetic +100 * 12 - 23/17 diff --git a/tests/cli/test_data/comments.txt b/tests/cli/test_data/comments.txt new file mode 100644 index 00000000..9025eb4b --- /dev/null +++ b/tests/cli/test_data/comments.txt @@ -0,0 +1,18 @@ +# This file exercises comment handling and blank-line skipping. +# +# It has: leading comments, inline comments, multiple blank lines, +# lines with only whitespace, and indented expressions. +# + + +1 + 1 # inline comment + + + 2 + 2 # leading whitespace is fine + +# comment between expressions + +3 + 3 + + +# trailing comment diff --git a/tests/cli/test_data/edge_cases.dm b/tests/cli/test_data/edge_cases.dm new file mode 100644 index 00000000..db9e741d --- /dev/null +++ b/tests/cli/test_data/edge_cases.dm @@ -0,0 +1,24 @@ +# Edge cases - negatives, zeros, single-value expressions, etc. + +0 +-0 +42 +-42 +0.001 +-0.001 + +# Unary minus with operations +-3 * 2 +-1 + 1 +-100 + 50 + +# Parenthesised negation +(-5 + 2) * (-3 + 1) + +# Nested parens +((((1 + 2) * 3) - 4) / 5) + +# Power edge cases +1 ^ 100 +2 ^ 0 +10 ^ 10 diff --git a/tests/cli/test_data/precision.dm b/tests/cli/test_data/precision.dm new file mode 100644 index 00000000..49109dc7 --- /dev/null +++ b/tests/cli/test_data/precision.dm @@ -0,0 +1,17 @@ +# High-precision stress test - expressions that need many digits to +# distinguish correct answers from off-by-one rounding errors. + +# 1/7 has a 6-digit repeating cycle +1/7 + +# pi subtracted from a close rational approximation +pi - 355/113 + +# sqrt(2) * sqrt(2) should be exactly 2 +sqrt(2) * sqrt(2) + +# e^(pi*sqrt(163)) is close to an integer (Ramanujan's constant) +exp(pi * sqrt(163)) + +# Cancellation stress: nearly equal values +exp(1) - (1 + 1 + 1/2 + 1/6 + 1/24 + 1/120 + 1/720) diff --git a/tests/cli/test_data/torture b/tests/cli/test_data/torture new file mode 100644 index 00000000..2f45f6be --- /dev/null +++ b/tests/cli/test_data/torture @@ -0,0 +1,41 @@ +# Torture test - complex expressions to stress the tokenizer, parser, +# and the Decimo BigDecimal engine. + +# Deeply nested functions +sqrt(abs(sin(1) * cos(1) + ln(2))) + +# Chained arithmetic with many terms +1/7 + 2/11 + 3/13 + 4/17 + 5/19 + +# Large integer powers +2 ^ 256 +3 ^ 100 + +# Mixed operations with constants +pi * e + sqrt(2) * ln(10) + +# Multi-argument functions +root(27, 3) +log(1024, 2) +root(1000000, 6) + +# Function composition chains +ln(exp(sqrt(abs(-100)))) +exp(ln(pi)) + +# Trig function stress +sin(pi/6) + cos(pi/3) + tan(pi/4) +cot(pi/4) + csc(pi/2) + +# Negative expressions with functions +-3 * pi * (sin(1) + cos(2)) +-sqrt(2) * ln(10) + +# Very long arithmetic chain +1 + 2 - 3 * 4 / 5 ^ 2 + 6 * 7 - 8 / 9 + 10 + +# Big integer arithmetic +2^256 + 3^100 - 5^50 + +# Deeply parenthesised +(((((1 + 2) * (3 + 4)) - ((5 - 6) * (7 + 8))) / 2) + 1) diff --git a/tests/test_cli.sh b/tests/test_cli.sh index b6f044d3..72594a66 100644 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -1,6 +1,11 @@ #!/bin/bash set -e # Exit immediately if any command fails +# Derive repo root from the script's own location so the tests work +# regardless of the caller's working directory. +REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$REPO_ROOT" + # ── Unit tests ───────────────────────────────────────────────────────────── for f in tests/cli/*.mojo; do pixi run mojo run -I src -I src/cli -D ASSERT=all --debug-level=full "$f" @@ -136,27 +141,52 @@ assert_pipe_output "pipe with engineering" "12345.678" "12.345678E+3" -E assert_pipe_output "pipe with delimiter" "1234567.89" "1_234_567.89" -D "_" # ── File mode (-F/--file flag) ──────────────────────────────────────────── -TMPFILE=$(mktemp /tmp/decimo_test_XXXXXX.dm) -cat > "$TMPFILE" << 'FILE_EOF' -# Test expression file -pi -e -sqrt(2) - -# Arithmetic -100 * 12 - 23/17 -FILE_EOF - -assert_output "file mode basic" \ +# All test files live in tests/cli/test_data/ — no temp files needed. +DATA="tests/cli/test_data" + +# --- basic.dm: constants, functions, arithmetic --- +assert_output "file mode basic.dm" \ "$(printf '3.1415926535897932384626433832795028841971693993751\n2.7182818284590452353602874713526624977572470937000\n1.4142135623730950488016887242096980785696718753769\n1198.6470588235294117647058823529411764705882352941')" \ - "$BINARY" -F "$TMPFILE" + "$BINARY" -F "$DATA/basic.dm" -assert_output "file mode with precision" \ +assert_output "file mode basic.dm -P 10" \ "$(printf '3.141592654\n2.718281828\n1.414213562\n1198.647059')" \ - "$BINARY" -F "$TMPFILE" -P 10 - -rm -f "$TMPFILE" - + "$BINARY" -F "$DATA/basic.dm" -P 10 + +# --- comments.txt: comments, blank lines, inline comments, whitespace --- +assert_output "file mode comments.txt" \ + "$(printf '2\n4\n6')" \ + "$BINARY" -F "$DATA/comments.txt" + +# --- edge_cases.dm: zeros, negatives, nested parens, powers --- +assert_output "file mode edge_cases.dm" \ + "$(printf '0\n-0\n42\n-42\n0.001\n-0.001\n-6\n0\n-50\n6\n1\n1\n1\n10000000000')" \ + "$BINARY" -F "$DATA/edge_cases.dm" + +assert_output "file mode edge_cases.dm -P 10" \ + "$(printf '0\n-0\n42\n-42\n0.001\n-0.001\n-6\n0\n-50\n6\n1\n1\n1\n1.000000000E+10')" \ + "$BINARY" -F "$DATA/edge_cases.dm" -P 10 + +# --- torture: deeply nested functions, trig, multi-arg, long chains --- +assert_output "file mode torture (no ext)" \ + "$(printf '1.0713523668582555369923173752696402459121546287121\n1.0538965678284563733480142148872799027597789207696\n1.1579208923731619542357098500868790785326998466564E+77\n515377520732011331036461129765621272702107522001\n11.796081289703860754690015480540861635182913811879\n3\n1E+1\n10\n10.000000000000000000000000000000000000000000000000\n3.1415926535897932384626433832795028841971693993751\n2.0000000000000000000000000000000000000000000000000\n2.0000000000000000000000000000000000000000000000000\n-4.0085856587109635320394984475849874956541040162735\n-3.2563470670302936892264646109942871401480252761141\n53.631111111111111111111111111111111111111111111111\n1.1579208923731619542357098500920328537400190717884E+77\n19')" \ + "$BINARY" -F "$DATA/torture" + +# --- precision.dm: high-precision stress (repeating decimals, near-integers) --- +assert_output "file mode precision.dm" \ + "$(printf '0.14285714285714285714285714285714285714285714285714\n-2.6676418906242231236893288649633380405195232780734E-7\n2.0000000000000000000000000000000000000000000000000\n262537412640768743.99999999999925007259719818568888\n0.00022627290348967980473191579710694220169153814440402')" \ + "$BINARY" -F "$DATA/precision.dm" + +# --- File mode with formatting flags --- +assert_output "file mode basic.dm -S" \ + "$(printf '3.1415926535897932384626433832795028841971693993751E0\n2.7182818284590452353602874713526624977572470937E0\n1.4142135623730950488016887242096980785696718753769E0\n1.1986470588235294117647058823529411764705882352941E+3')" \ + "$BINARY" -F "$DATA/basic.dm" -S + +assert_output "file mode basic.dm -D _" \ + "$(printf '3.141_592_653_589_793_238_462_643_383_279_502_884_197_169_399_375_1\n2.718_281_828_459_045_235_360_287_471_352_662_497_757_247_093_700_0\n1.414_213_562_373_095_048_801_688_724_209_698_078_569_671_875_376_9\n1_198.647_058_823_529_411_764_705_882_352_941_176_470_588_235_294_1')" \ + "$BINARY" -F "$DATA/basic.dm" -D _ + +# --- Error cases --- # File mode: nonexistent file gives a clear error NONEXIST_OUTPUT=$("$BINARY" -F "nonexistent_file.dm" 2>&1 || true) if echo "$NONEXIST_OUTPUT" | grep -qi "cannot read file"; then From 76f1c0b4d0aed70c3b81849c31529a2be6d0990e Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sun, 12 Apr 2026 20:40:55 +0200 Subject: [PATCH 15/17] [cli] Add interactive REPL (#205) This PR adds an interactive REPL mode to the Decimo CLI so `decimo` with no arguments (and TTY stdin) launches an interactive calculator session, aligning CLI UX with tools like `bc`. The PR also updates related CLI docs/changelog and refines benchmark comparison logic. **Changes:** - Launch REPL when no expression/file is provided and stdin is a TTY; add REPL implementation, prompt writer, and line-reading helper. - Update CLI docs/changelog to describe REPL mode and revise the CLI benchmark script to compare full-precision digits (with a guard digit). - Normalize various docstrings to use single-backtick inline code formatting. --- benches/cli/bench_cli.sh | 137 +++++++++++++++++------ docs/changelog.md | 1 + docs/plans/cli_calculator.md | 100 +++++++++-------- src/cli/calculator/__init__.mojo | 5 +- src/cli/calculator/display.mojo | 32 ++++-- src/cli/calculator/engine.mojo | 151 +++++++++++++++++++++++++ src/cli/calculator/io.mojo | 65 ++++++++--- src/cli/calculator/parser.mojo | 2 +- src/cli/calculator/repl.mojo | 133 ++++++++++++++++++++++ src/cli/calculator/tokenizer.mojo | 2 +- src/cli/main.mojo | 154 +++----------------------- src/decimo/bigdecimal/bigdecimal.mojo | 40 +++---- src/decimo/bigfloat/mpfr_wrapper.mojo | 12 +- src/decimo/bigint/bigint.mojo | 2 +- src/decimo/errors.mojo | 12 +- tests/cli/test_engine.mojo | 73 ++++++++++++ 16 files changed, 638 insertions(+), 283 deletions(-) create mode 100644 src/cli/calculator/engine.mojo create mode 100644 src/cli/calculator/repl.mojo create mode 100644 tests/cli/test_engine.mojo diff --git a/benches/cli/bench_cli.sh b/benches/cli/bench_cli.sh index 87bcd640..4b6aeb10 100644 --- a/benches/cli/bench_cli.sh +++ b/benches/cli/bench_cli.sh @@ -3,9 +3,13 @@ # CLI Calculator Benchmarks — Correctness & Performance # # Compares decimo against bc and python3 on every expression: -# 1. Correctness — first 15 significant digits must agree +# 1. Correctness — all significant digits must agree at full precision +# (minus 1 guard digit for last-digit rounding differences) # 2. Performance — average wall-clock latency over $ITERATIONS runs # +# bc is the golden reference (mismatches fail the script). +# python3/mpmath is informational (mismatches are shown but do not fail). +# # Usage: # bash benches/cli/bench_cli.sh # ITERATIONS=20 bash benches/cli/bench_cli.sh @@ -32,15 +36,28 @@ if ! [[ -x "$BINARY" ]]; then exit 1 fi -HAS_BC=false; command -v bc &>/dev/null && HAS_BC=true +if ! command -v bc &>/dev/null; then + echo "Error: bc is required (golden reference) but not found." + exit 1 +fi HAS_PY=false; command -v python3 &>/dev/null && HAS_PY=true # ── Counters ─────────────────────────────────────────────────────────────── -COMPARISONS=0 -MATCHES=0 -MISMATCHES=0 -ERRORS=0 +# Decimo errors — any decimo failure is fatal. +DECIMO_ERRORS=0 + +# bc is the golden reference — mismatches here fail the script. +BC_COMPARISONS=0 +BC_MATCHES=0 +BC_MISMATCHES=0 +BC_ERRORS=0 + +# python3 is informational — mismatches are reported but do not fail. +PY_COMPARISONS=0 +PY_MATCHES=0 +PY_MISMATCHES=0 +PY_ERRORS=0 # ── Helpers ──────────────────────────────────────────────────────────────── @@ -67,9 +84,9 @@ elapsed_ms() { } # Extract a canonical comparison key from a numeric string: -# adjusted base-10 exponent + first 15 significant digits. +# adjusted base-10 exponent + ALL significant digits. # This ensures values that differ only by exponent (e.g. 1E+10 vs 1E+11) -# are correctly detected as a MISMATCH. +# are correctly detected as a MISMATCH, and full-precision agreement is verified. sig_digits() { local s="${1#-}" # strip sign; check_match handles sign separately local explicit_exp=0 @@ -112,10 +129,14 @@ sig_digits() { adjusted_exp=$(( explicit_exp - first_nonzero - 1 )) fi - echo "${adjusted_exp}:$(echo "$digits" | cut -c1-15)" + echo "${adjusted_exp}:${digits}" } -# Compare two results by leading significant digits. +# Compare two results by all significant digits. +# Both keys are "adjusted_exp:digits". The exponents must match exactly. +# The digit strings are compared up to the length of the shorter one, +# minus 1 guard digit (the very last digit often differs between tools +# due to rounding vs truncation — standard in MP arithmetic). check_match() { local a="$1" b="$2" local sign_a="" sign_b="" @@ -125,7 +146,26 @@ check_match() { local sa sb sa=$(sig_digits "$a") sb=$(sig_digits "$b") - if [[ "$sa" == "$sb" ]]; then echo "MATCH"; else echo "MISMATCH"; fi + + # Split into exponent and digit parts + local exp_a="${sa%%:*}" digits_a="${sa#*:}" + local exp_b="${sb%%:*}" digits_b="${sb#*:}" + + # Exponents must match exactly + if [[ "$exp_a" != "$exp_b" ]]; then echo "MISMATCH"; return 0; fi + + # Compare digits up to (shorter length - 1) to allow last-digit rounding + local len_a=${#digits_a} len_b=${#digits_b} + local min_len=$len_a + if (( len_b < min_len )); then min_len=$len_b; fi + local cmp_len=$(( min_len - 1 )) + if (( cmp_len < 1 )); then cmp_len=1; fi + + if [[ "${digits_a:0:$cmp_len}" == "${digits_b:0:$cmp_len}" ]]; then + echo "MATCH" + else + echo "MISMATCH" + fi return 0 } @@ -134,17 +174,30 @@ preview() { if (( ${#1} > PREVIEW )); then echo "${1:0:$PREVIEW}..."; else echo "$1"; fi } -# Record a comparison result. +# Record a comparison result for a specific tool. record() { - local tag="$1" - if [[ "$tag" == "ERROR" ]]; then - ERRORS=$((ERRORS + 1)) + local tool="$1" tag="$2" + if [[ "$tool" == "bc" ]]; then + if [[ "$tag" == "ERROR" ]]; then + BC_ERRORS=$((BC_ERRORS + 1)) + else + BC_COMPARISONS=$((BC_COMPARISONS + 1)) + if [[ "$tag" == "MATCH" ]]; then + BC_MATCHES=$((BC_MATCHES + 1)) + else + BC_MISMATCHES=$((BC_MISMATCHES + 1)) + fi + fi else - COMPARISONS=$((COMPARISONS + 1)) - if [[ "$tag" == "MATCH" ]]; then - MATCHES=$((MATCHES + 1)) + if [[ "$tag" == "ERROR" ]]; then + PY_ERRORS=$((PY_ERRORS + 1)) else - MISMATCHES=$((MISMATCHES + 1)) + PY_COMPARISONS=$((PY_COMPARISONS + 1)) + if [[ "$tag" == "MATCH" ]]; then + PY_MATCHES=$((PY_MATCHES + 1)) + else + PY_MISMATCHES=$((PY_MISMATCHES + 1)) + fi fi fi return 0 @@ -171,13 +224,13 @@ bench_compare() { d_ms=$(elapsed_ms "$BINARY" "$d_expr" -P "$prec") printf " %-10s %-38s %8s ms\n" "decimo:" "$(preview "$d_result")" "$d_ms" if [[ "$d_result" == "ERROR" ]]; then - record "ERROR" + DECIMO_ERRORS=$((DECIMO_ERRORS + 1)) echo "" return fi # ── bc ── - if [[ -n "$bc_expr" ]] && $HAS_BC; then + if [[ -n "$bc_expr" ]]; then local full_bc="scale=$prec; $bc_expr" local b_result b_ms tag # tr -d '\\\n' removes bc's backslash line-continuations @@ -189,7 +242,7 @@ bench_compare() { tag=$(check_match "$d_result" "$b_result") fi printf " %-10s %-38s %8s ms %s\n" "bc:" "$(preview "$b_result")" "$b_ms" "$tag" - record "$tag" + record bc "$tag" fi # ── python3 ── @@ -204,7 +257,7 @@ bench_compare() { tag=$(check_match "$d_result" "$p_result") fi printf " %-10s %-38s %8s ms %s\n" "python3:" "$(preview "$p_result")" "$p_ms" "$tag" - record "$tag" + record py "$tag" fi echo "" @@ -276,10 +329,12 @@ bench_compare "exp(1)" 50 \ "e(1)" \ "${PY_MP};print(mp.exp(1))" -bench_compare "sin(1)" 50 \ - "sin(1)" \ - "s(1)" \ - "${PY_MP};print(mp.sin(1))" +# NOTE: mpmath diverges from decimo & WolframAlpha at digit ~21 for sin(near-pi). +# See docs/internal_notes.md. Kept here as a reference comparison. +bench_compare "sin(3.1415926535897932384626433833)" 50 \ + "sin(3.1415926535897932384626433833)" \ + "s(3.1415926535897932384626433833)" \ + "${PY_MP};print(mp.sin(mp.mpf('3.1415926535897932384626433833')))" bench_compare "cos(0)" 50 \ "cos(0)" \ @@ -359,14 +414,32 @@ echo "" # ── Summary ──────────────────────────────────────────────────────────────── echo "============================================================" -printf " Summary: %d comparisons — %d MATCH, %d MISMATCH" \ - "$COMPARISONS" "$MATCHES" "$MISMATCHES" -if (( ERRORS > 0 )); then - printf ", %d ERROR (tool missing or failed)" "$ERRORS" +if (( DECIMO_ERRORS > 0 )); then + printf " decimo: %d ERROR(s)\n" "$DECIMO_ERRORS" +fi +printf " bc (golden): %d comparisons — %d MATCH, %d MISMATCH" \ + "$BC_COMPARISONS" "$BC_MATCHES" "$BC_MISMATCHES" +if (( BC_ERRORS > 0 )); then + printf ", %d ERROR" "$BC_ERRORS" +fi +echo "" +printf " python3 (ref): %d comparisons — %d MATCH, %d MISMATCH" \ + "$PY_COMPARISONS" "$PY_MATCHES" "$PY_MISMATCHES" +if (( PY_ERRORS > 0 )); then + printf ", %d ERROR" "$PY_ERRORS" fi echo "" echo "============================================================" -if (( MISMATCHES > 0 )); then +if (( DECIMO_ERRORS > 0 )); then + echo "FAIL: decimo evaluation errors detected." + exit 1 +fi +if (( BC_MISMATCHES > 0 )); then + echo "FAIL: bc (golden reference) mismatches detected." + exit 1 +fi +if (( BC_ERRORS > 0 )); then + echo "FAIL: bc (golden reference) errors detected." exit 1 fi diff --git a/docs/changelog.md b/docs/changelog.md index 9f393a9b..e15c24a6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,7 @@ This is a list of changes for the Decimo package (formerly DeciMojo). 1. Add **file mode**: use `--file` / `-F` flag to evaluate expressions from a file, one per line (e.g. `decimo -F expressions.dm -P 50`). Comments (`#`), inline comments, and blank lines are skipped. All CLI flags (precision, formatting, rounding) apply to every expression. 1. Add **shell completion** documentation for Bash, Zsh, and Fish (`decimo --completions bash|zsh|fish`). 1. Add **CLI performance benchmarks** (`benches/cli/bench_cli.sh`) comparing correctness and timing against `bc` and `python3` across 47 comparisons — all results match to 15 significant digits; `decimo` is 3–4× faster than `python3 -c`. +1. Add **interactive REPL**: launch with `decimo` (no arguments, TTY attached). Features coloured `decimo>` prompt on stderr, per-line error recovery with caret diagnostics, comment/blank-line skipping, and graceful exit via `exit`, `quit`, or Ctrl-D. All CLI flags (`-P`, `--scientific`, etc.) apply to the REPL session. ### 🦋 Changed in v0.10.0 diff --git a/docs/plans/cli_calculator.md b/docs/plans/cli_calculator.md index afa71e44..88e59ff3 100644 --- a/docs/plans/cli_calculator.md +++ b/docs/plans/cli_calculator.md @@ -33,8 +33,8 @@ Rows are sorted by implementation priority for `decimo` (top = implement first). 1. **Basic arithmetic + High-precision + Large integers + Pipeline** (Phase 1) — These are the raison d'être of `decimo`. Decimo already provides arbitrary-precision `BigDecimal`; wiring up tokenizer → parser → evaluator gives immediate value. Pipeline/batch is nearly free once one-shot works (just loop over stdin lines). 2. **Built-in math functions** (Phase 2) — `sqrt`, `ln`, `exp`, `sin`, `cos`, `tan`, `root` already exist in the Decimo API. Adding them mostly means extending the tokenizer/parser to recognize function names. 3. **Polish & ArgMojo integration** (Phase 3) — Error diagnostics, edge-case handling, and exploiting ArgMojo v0.5.0 features (shell completions, argument groups, numeric range validation, etc.). Mostly CLI UX refinement. -4. **Interactive REPL + Subcommands** (Phase 4) — Requires a read-eval-print loop, `ans` tracking, named variable storage, session-level precision management, and CLI restructuring with subcommands. More engineering effort, less urgency. -5. **Future enhancements** (Phase 5) — CJK full-width detection, response files, unit conversion, matrix, symbolic. Out of scope for now. +4. **Interactive REPL** (Phase 4) — Requires a read-eval-print loop, `ans` tracking, named variable storage, session-level precision management. No subcommands — mode is determined by invocation context (same as `bc`, `python3`, `calc`). +5. **Future enhancements** (Phase 5) — Binary distribution, CJK full-width detection, response files, unit conversion, matrix, symbolic. Out of scope for now. ## Usage Design @@ -326,25 +326,31 @@ Format the final `BigDecimal` result based on CLI flags: | 3.11 | `Parsable.run()` override | ✗ | Move eval logic into `DecimoArgs.run()` for cleaner separation | | 3.12 | Performance validation | ✓ | `benches/cli/bench_cli.sh`; 47 correctness checks + timing vs `bc` and `python3` | | 3.13 | Documentation (user manual for CLI) | ✓ | `docs/user_manual_cli.md`; includes shell completions setup and performance data | -| 3.14 | Build and distribute as single binary | ✗ | | -| 3.15 | Allow negative expressions | ✓ | `allow_hyphen=True` on `Positional`; `decimo "-3*pi*(sin(1))"` works | -| 3.16 | Make short names upper cases to avoid expression collisions | ✓ | `-P`, `-S`, `-E`, `-D`, `-R`; `--pad` has no short name; `-e`, `-pi`, `-sin(1)` all work | -| 3.17 | Define `allow_hyphen_values` in declarative API | ✗ | When argmojo supports it | - -### Phase 4: Interactive REPL & Subcommands - -1. Restructure CLI with subcommands: `decimo eval "expr"` (default), `decimo repl`, `decimo help functions`. -2. Persistent flags (`--precision`, `--scientific`, etc.) across subcommands. -3. Subcommand dispatch via `parse_full()`. -4. No-args + TTY detection → launch REPL directly. -5. Read-eval-print loop: read a line from stdin, evaluate, print result, repeat. -6. Custom prompt (`decimo>`). -7. `ans` variable to reference the previous result. -8. Variable assignment: `x = sqrt(2)`, usable in subsequent expressions. -9. Session-level precision: settable via `decimo -p 100` at launch or `:precision 100` command mid-session. -10. Graceful exit: `exit`, `quit`, `Ctrl-D`. -11. Clear error messages without crashing the session (e.g., "Error: division by zero", then continue). -12. History (if Mojo gets readline-like support). +| 3.14 | Allow negative expressions | ✓ | `allow_hyphen=True` on `Positional`; `decimo "-3*pi*(sin(1))"` works | +| 3.15 | Make short names upper cases to avoid expression collisions | ✓ | `-P`, `-S`, `-E`, `-D`, `-R`; `--pad` has no short name; `-e`, `-pi`, `-sin(1)` all work | +| 3.16 | Define `allow_hyphen_values` in declarative API | ✗ | When argmojo supports it | + +### Phase 4: Interactive REPL + +No subcommands — mode is determined by invocation context (same as `bc`, `python3`, `calc`): + +| Invocation | Mode | +| ---------------------------------- | -------- | +| `decimo "expr"` | One-shot | +| `echo "expr" \| decimo` | Pipe | +| `decimo -F file.dm` | File | +| `decimo` (no args, stdin is a TTY) | REPL | + +Design rationale: subcommands (`decimo eval`, `decimo repl`) create collision risk with expression identifiers (e.g. `log`, `exp`) and multi-arg function names. Every comparable tool (`bc`, `dc`, `calc`, `qalc`, `python3`) uses the same zero-subcommand pattern. + +**REPL features:** + +1. Read-eval-print loop: read a line, evaluate, print result, repeat. +2. `ans` — automatically holds the previous result. +3. Variable assignment — `x = ` stores a named value. +4. Meta-commands with `:` prefix — avoids collision with expressions. +5. Error recovery — display error and continue, don't crash the session. +6. Exit via `exit`, `quit`, or Ctrl-D. ```bash $ decimo @@ -356,37 +362,36 @@ decimo> x = sqrt(2) 1.41421356237309504880168872420969807856967187537694 decimo> x ^ 2 2 +decimo> :precision 100 +Precision set to 100 decimo> 1/0 Error: division by zero decimo> exit ``` -| # | Task | Status | Notes | -| ---- | ---------------------------------------- | :----: | --------------------------------------------------------------------------------------------------------------- | -| 4.1 | Subcommand restructure | ✗ | `decimo eval "expr"` (default), `decimo repl`, `decimo help functions`; use `subcommands()` hook | -| 4.2 | Persistent flags across subcommands | ✗ | `precision`, `--scientific`, etc. as `persistent=True`; both `decimo repl -p 100` and `decimo -p 100 repl` work | -| 4.3 | `parse_full()` for subcommand dispatch | ✗ | Typed struct + `ParseResult.subcommand` for dispatching to eval/repl/help handlers | -| 4.4 | No-args + TTY → launch REPL directly | ✗ | Replace `help_on_no_arguments()` with REPL auto-launch when terminal detected | -| 4.5 | Read-eval-print loop | ✗ | | -| 4.6 | Custom prompt (`decimo>`) | ✗ | | -| 4.7 | `ans` variable (previous result) | ✗ | | -| 4.8 | Variable assignment (`x = expr`) | ✗ | | -| 4.9 | Session-level precision (`:precision N`) | ✗ | | -| 4.10 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✗ | | -| 4.11 | Error recovery (don't crash session) | ✗ | | -| 4.12 | Interactive prompting for missing values | ✗ | Use `.prompt()` on subcommand args for interactive precision input, etc. | -| 4.13 | Subcommand aliases | ✗ | `command_aliases(["e"])` for `eval`, `command_aliases(["r"])` for `repl` | -| 4.14 | Hidden subcommands | ✗ | Hide `debug` / internal subcommands from help | +| # | Task | Status | Notes | +| --- | --------------------------------------- | :----: | -------------------------------------------------------------------------- | +| 4.1 | No-args + TTY → launch REPL | ✓ | Replace "no expression" error with REPL auto-launch when terminal detected | +| 4.2 | Read-eval-print loop | ✓ | `read_line()` via `getchar()`; one expression per line | +| 4.3 | Custom prompt (`decimo>`) | ✓ | Coloured prompt to stderr so results can be piped | +| 4.4 | `ans` variable (previous result) | ✗ | Injected as a constant into the evaluator; starts as `0` | +| 4.5 | Variable assignment (`x = expr`) | ✗ | Parse `name = expr` syntax; store in a name→BigDecimal map | +| 4.6 | Meta-commands (`:precision N`, `:vars`) | ✗ | `:` prefix avoids collision with expressions | +| 4.7 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✓ | | +| 4.8 | Error recovery (don't crash session) | ✓ | Catch exceptions per-line, display error, continue loop | +| 4.9 | History (if Mojo gets readline support) | ✗ | Future — depends on Mojo FFI evolution | ### Phase 5: Future Enhancements -1. Detect full-width digits/operators for CJK users while parsing. -2. Response files (`@expressions.txt`) — when Mojo compiler bug is fixed, use ArgMojo's `cmd.response_file_prefix("@")`. +1. Build and distribute as a single binary (Homebrew, GitHub Releases, etc.) — defer until REPL is stable so first-run experience is complete. +2. Detect full-width digits/operators for CJK users while parsing. +3. Response files (`@expressions.txt`) — when Mojo compiler bug is fixed, use ArgMojo's `cmd.response_file_prefix("@")`. | # | Task | Status | Notes | | --- | ------------------------------------------- | :----: | ------------------------------------------------------------------------------ | -| 5.1 | Full-width digit/operator detection for CJK | ✗ | Tokenizer-level handling for CJK users | -| 5.2 | Response files (`@expressions.txt`) | ✗ | Blocked on Mojo compiler bug; `cmd.response_file_prefix("@")` ready when fixed | +| 5.1 | Build and distribute as single binary | ✗ | Defer until REPL is stable; Homebrew, GitHub Releases, `curl \| sh` installer | +| 5.2 | Full-width digit/operator detection for CJK | ✗ | Tokenizer-level handling for CJK users | +| 5.3 | Response files (`@expressions.txt`) | ✗ | Blocked on Mojo compiler bug; `cmd.response_file_prefix("@")` ready when fixed | ## Design Decisions @@ -404,13 +409,12 @@ This is the natural choice for a calculator: users expect `7 / 2` to be `3.5`, n `decimo` automatically detects its mode based on how it is invoked: -| Invocation | Mode | -| ------------------------------------- | ------------------------------------- | -| `decimo "expr"` | One-shot: evaluate and exit | -| `echo "expr" \| decimo` | Pipe: read stdin line by line | -| `decimo -F file.dm` | File: read and evaluate each line | -| `decimo` (no args, terminal is a TTY) | REPL: interactive session | -| `decimo -i` | REPL: force interactive even if piped | +| Invocation | Mode | +| ------------------------------------- | --------------------------------- | +| `decimo "expr"` | One-shot: evaluate and exit | +| `echo "expr" \| decimo` | Pipe: read stdin line by line | +| `decimo -F file.dm` | File: read and evaluate each line | +| `decimo` (no args, terminal is a TTY) | REPL: interactive session | ## Notes diff --git a/src/cli/calculator/__init__.mojo b/src/cli/calculator/__init__.mojo index b4162186..83fda281 100644 --- a/src/cli/calculator/__init__.mojo +++ b/src/cli/calculator/__init__.mojo @@ -44,9 +44,11 @@ from .tokenizer import ( ) from .parser import parse_to_rpn from .evaluator import evaluate_rpn, evaluate -from .display import print_error, print_warning, print_hint +from .engine import evaluate_and_print, display_calc_error, pad_to_precision +from .display import print_error, print_warning, print_hint, write_prompt from .io import ( stdin_is_tty, + read_line, read_stdin, split_into_lines, strip_comment, @@ -57,3 +59,4 @@ from .io import ( read_file_text, file_exists, ) +from .repl import run_repl diff --git a/src/cli/calculator/display.mojo b/src/cli/calculator/display.mojo index ab96b81e..a162f388 100644 --- a/src/cli/calculator/display.mojo +++ b/src/cli/calculator/display.mojo @@ -59,9 +59,9 @@ comptime CARET_COLOR = GREEN def print_error(message: String): """Print a coloured error message to stderr. - Format: ``Error: `` + Format: `Error: ` - The label ``Error`` is displayed in bold red. The message text + The label `Error` is displayed in bold red. The message text follows in the default terminal colour. """ _write_stderr( @@ -93,11 +93,11 @@ def print_error(message: String, expr: String, position: Int): def print_warning(message: String): - """Print a coloured warning message to stderr. + """Prints a coloured warning message to stderr. - Format: ``Warning: `` + Format: `Warning: ` - The label ``Warning`` is displayed in bold orange/yellow. + The label `Warning` is displayed in bold orange/yellow. """ _write_stderr( BOLD + WARNING_COLOR + "Warning" + RESET + BOLD + ": " + RESET + message @@ -105,7 +105,7 @@ def print_warning(message: String): def print_warning(message: String, expr: String, position: Int): - """Print a coloured warning message with a caret indicator.""" + """Prints a coloured warning message with a caret indicator.""" _write_stderr( BOLD + WARNING_COLOR + "Warning" + RESET + BOLD + ": " + RESET + message ) @@ -113,27 +113,37 @@ def print_warning(message: String, expr: String, position: Int): def print_hint(message: String): - """Print a coloured hint message to stderr. + """Prints a coloured hint message to stderr. - Format: ``Hint: `` + Format: `Hint: ` - The label ``Hint`` is displayed in bold cyan. + The label `Hint` is displayed in bold cyan. """ _write_stderr( BOLD + HINT_COLOR + "Hint" + RESET + BOLD + ": " + RESET + message ) +def write_prompt(prompt: String): + """Writes a REPL prompt to stderr (no trailing newline). + + The prompt is written to stderr so that stdout remains clean for + piping results. + """ + var styled = BOLD + GREEN + prompt + RESET + print(styled, end="", file=stderr, flush=True) + + # ── Internal helpers ───────────────────────────────────────────────────────── def _write_stderr(msg: String): - """Write a line to stderr.""" + """Writes a line to stderr.""" print(msg, file=stderr) def _write_caret(expr: String, position: Int): - """Print the expression line and a green caret (^) under the + """Prints the expression line and a green caret (^) under the given column position to stderr. ```text diff --git a/src/cli/calculator/engine.mojo b/src/cli/calculator/engine.mojo new file mode 100644 index 00000000..2b58d463 --- /dev/null +++ b/src/cli/calculator/engine.mojo @@ -0,0 +1,151 @@ +# ===----------------------------------------------------------------------=== # +# 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. +# ===----------------------------------------------------------------------=== # + +""" +Shared evaluation pipeline for the Decimo CLI calculator. + +Provides `evaluate_and_print`, `display_calc_error`, and `pad_to_precision` +used by both one-shot/pipe/file modes (main.mojo) and the interactive REPL +(repl.mojo). +""" + +from decimo.rounding_mode import RoundingMode +from .tokenizer import tokenize +from .parser import parse_to_rpn +from .evaluator import evaluate_rpn, final_round +from .display import print_error + + +def evaluate_and_print( + expr: String, + precision: Int, + scientific: Bool, + engineering: Bool, + pad: Bool, + delimiter: String, + rounding_mode: RoundingMode, + show_expr_on_error: Bool = False, +) raises: + """Tokenize, parse, evaluate, and print one expression. + + On error, displays a coloured diagnostic and raises to signal failure + to the caller. + + Args: + expr: The expression string to evaluate. + precision: Number of significant digits. + scientific: Whether to format in scientific notation. + engineering: Whether to format in engineering notation. + pad: Whether to pad trailing zeros to the specified precision. + delimiter: Digit-group separator (empty string disables grouping). + rounding_mode: Rounding mode for the final result. + show_expr_on_error: If True, show the expression with a caret + indicator on error. If False, show only the error message. + """ + try: + var tokens = tokenize(expr) + var rpn = parse_to_rpn(tokens^) + var value = final_round( + evaluate_rpn(rpn^, precision), precision, rounding_mode + ) + + if scientific: + print(value.to_string(scientific=True, delimiter=delimiter)) + elif engineering: + print(value.to_string(engineering=True, delimiter=delimiter)) + elif pad: + print( + pad_to_precision(value.to_string(force_plain=True), precision) + ) + else: + print(value.to_string(delimiter=delimiter)) + except e: + if show_expr_on_error: + display_calc_error(String(e), expr) + else: + print_error(String(e)) + raise e^ + + +def display_calc_error(error_msg: String, expr: String): + """Parse a calculator error message and display it with colours + and a caret indicator. + + Handles two error formats: + + 1. `Error at position N: description` — with position info. + 2. `description` — without position info. + + For form (1), extracts the position and calls `print_error` with a + visual caret under the offending column. For form (2) falls back + to a plain coloured error. + """ + comptime PREFIX = "Error at position " + + if error_msg.startswith(PREFIX): + var after_prefix = len(PREFIX) + var colon_pos = -1 + for i in range(after_prefix, len(error_msg)): + if error_msg[byte=i] == ":": + colon_pos = i + break + + if colon_pos > after_prefix: + var pos_str = String(error_msg[byte=after_prefix:colon_pos]) + var description = String( + error_msg[byte = colon_pos + 2 :] + ) # skip ": " + + try: + var pos = Int(pos_str) + print_error(description, expr, pos) + return + except: + pass # fall through to plain display + + # Fallback: no position info — just show the message. + print_error(error_msg) + + +def pad_to_precision(plain: String, precision: Int) -> String: + """Pad trailing zeros so the fractional part has exactly + `precision` digits. + + Args: + plain: A plain (fixed-point) numeric string. + precision: Target number of fractional digits. + + Returns: + The string with trailing zeros appended as needed. + """ + if precision <= 0: + return plain + + var dot_pos = -1 + for i in range(len(plain)): + if plain[byte=i] == ".": + dot_pos = i + break + + if dot_pos < 0: + # No decimal point — add one with `precision` zeros + return plain + "." + "0" * precision + + var frac_len = len(plain) - dot_pos - 1 + if frac_len >= precision: + return plain + + return plain + "0" * (precision - frac_len) diff --git a/src/cli/calculator/io.mojo b/src/cli/calculator/io.mojo index 09d599dd..3b8b0a29 100644 --- a/src/cli/calculator/io.mojo +++ b/src/cli/calculator/io.mojo @@ -21,7 +21,7 @@ Provides functions for detecting whether stdin is a pipe or terminal, reading lines from stdin, reading expression files, and line-level text processing (comment stripping, whitespace handling). -The text-processing primitives (``strip_comment``, ``is_blank``, ``strip``) +The text-processing primitives (`strip_comment`, `is_blank`, `strip`) are designed to be composable and reusable across all input modes — pipe, file, and future REPL. """ @@ -45,11 +45,42 @@ def stdin_is_tty() -> Bool: # ===----------------------------------------------------------------------=== # +def read_line() -> Optional[String]: + """Reads a single line from stdin (up to and including the newline). + + Returns the line content (without the trailing newline), or None + on EOF (e.g. Ctrl-D on an empty line). + + This is designed for REPL use: it reads one character at a time + via `getchar()` and stops at `\\n` or EOF. + """ + var chars = List[UInt8]() + + while True: + var c = external_call["getchar", Int32]() + if c < 0: # EOF + if len(chars) == 0: + return None + break + if UInt8(c) == 10: # '\n' + break + chars.append(UInt8(c)) + + # Strip trailing \r if present (Windows line endings from copy-paste) + if len(chars) > 0 and chars[len(chars) - 1] == 13: + _ = chars.pop() + + if len(chars) == 0: + return String("") + + return String(unsafe_from_utf8=chars^) + + def read_stdin() -> String: """Reads all data from stdin and return it as a String. - Uses the C ``getchar()`` function to read one byte at a time until - EOF. This avoids FFI conflicts with the POSIX ``read()`` syscall. + Uses the C `getchar()` function to read one byte at a time until + EOF. This avoids FFI conflicts with the POSIX `read()` syscall. Returns an empty string if stdin is empty. """ var chunks = List[UInt8]() @@ -75,7 +106,7 @@ def read_stdin() -> String: def split_into_lines(text: String) -> List[String]: """Splits a string into individual lines. - Handles both ``\\n`` and ``\\r\\n`` line endings. + Handles both `\\n` and `\\r\\n` line endings. Trailing empty lines from a final newline are not included. """ var lines = List[String]() @@ -104,13 +135,13 @@ def split_into_lines(text: String) -> List[String]: def strip_comment(line: String) -> String: - """Removes a ``#``-style comment from a line. + """Removes a `#`-style comment from a line. - Returns everything before the first ``#`` character. If there is - no ``#``, the line is returned unchanged. + Returns everything before the first `#` character. If there is + no `#`, the line is returned unchanged. This is a composable primitive — use it in combination with - ``strip()`` and ``is_blank()`` for full line processing. + `strip()` and `is_blank()` for full line processing. Examples:: @@ -139,7 +170,7 @@ def is_blank(line: String) -> Bool: """Returns True if the line is empty or contains only whitespace (spaces and tabs). - This is a composable primitive — combine with ``strip_comment()`` + This is a composable primitive — combine with `strip_comment()` to check for comment-or-blank lines. """ var n = len(line) @@ -159,9 +190,9 @@ def is_blank(line: String) -> Bool: def is_comment_or_blank(line: String) -> Bool: """Returns True if the line is blank, whitespace-only, or a comment - (first non-whitespace character is ``#``). + (first non-whitespace character is `#`). - Equivalent to ``is_blank(strip_comment(line))``. Provided as a + Equivalent to `is_blank(strip_comment(line))`. Provided as a convenience for callers that do not need the intermediate results. """ return is_blank(strip_comment(line)) @@ -199,7 +230,7 @@ def strip(s: String) -> String: def filter_expression_lines(lines: List[String]) -> List[String]: """Filters a list of lines to only those that are valid expressions. - Removes blank lines and comment lines (starting with ``#``). + Removes blank lines and comment lines (starting with `#`). Also strips inline comments and leading/trailing whitespace from each expression line. """ @@ -219,12 +250,12 @@ def filter_expression_lines(lines: List[String]) -> List[String]: def read_file_text(path: String) raises -> String: """Reads the entire contents of a file and returns it as a String. - Uses POSIX ``open()`` + ``dup2()`` + ``getchar()`` to read the file + Uses POSIX `open()` + `dup2()` + `getchar()` to read the file by temporarily redirecting stdin. This avoids FFI signature conflicts - with Mojo's stdlib and ArgMojo for ``read``/``fclose``. + with Mojo's stdlib and ArgMojo for `read`/`fclose`. - The original stdin is saved via ``dup()`` before redirection and - restored afterwards, so callers (e.g. a future REPL ``:load`` command) + The original stdin is saved via `dup()` before redirection and + restored afterwards, so callers (e.g. a future REPL `:load` command) can continue reading from the real stdin after this call returns. Args: @@ -280,7 +311,7 @@ def read_file_text(path: String) raises -> String: def file_exists(path: String) -> Bool: """Returns True if the given path exists as a readable file. - Uses the POSIX ``access()`` syscall with ``R_OK`` (4). + Uses the POSIX `access()` syscall with `R_OK` (4). """ var c_path = _to_cstr(path) # access(path, R_OK=4) returns 0 on success diff --git a/src/cli/calculator/parser.mojo b/src/cli/calculator/parser.mojo index ffa4c910..66b5f581 100644 --- a/src/cli/calculator/parser.mojo +++ b/src/cli/calculator/parser.mojo @@ -38,7 +38,7 @@ from .tokenizer import ( def parse_to_rpn(tokens: List[Token]) raises -> List[Token]: - """Convert infix tokens to Reverse Polish Notation using + """Converts infix tokens to Reverse Polish Notation using Dijkstra's shunting-yard algorithm. Supports binary operators (+, -, *, /, ^), unary minus, diff --git a/src/cli/calculator/repl.mojo b/src/cli/calculator/repl.mojo new file mode 100644 index 00000000..87822c04 --- /dev/null +++ b/src/cli/calculator/repl.mojo @@ -0,0 +1,133 @@ +# ===----------------------------------------------------------------------=== # +# 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. +# ===----------------------------------------------------------------------=== # + +""" +Interactive REPL (Read-Eval-Print Loop) for the Decimo CLI calculator. + +Launched when `decimo` is invoked with no expression and stdin is a TTY. +Reads one expression per line, evaluates it, prints the result, and loops +until the user types `exit`, `quit`, or presses Ctrl-D. + +Architecture notes for future PRs: + +- `ans` variable (4.4): The evaluator will need a `variables` dict + passed into `evaluate_rpn`. The REPL will inject `ans` after each + successful evaluation. The tokenizer already treats unknown identifiers + as errors, so it will need a `known_names: Set[String]` parameter. + +- Variable assignment (4.5): The REPL will detect `name = expr` syntax + *before* calling the evaluator (simple string split on first `=` that + is not inside parentheses). The result is stored in the variables dict. + +- Meta-commands (4.6): Lines starting with `:` are intercepted before + evaluation. Examples: `:precision 100`, `:vars`, `:help`. +""" + +from std.sys import stderr + +from decimo.rounding_mode import RoundingMode +from .display import write_prompt +from .engine import evaluate_and_print +from .io import read_line, strip, is_comment_or_blank + + +def run_repl( + precision: Int, + scientific: Bool, + engineering: Bool, + pad: Bool, + delimiter: String, + rounding_mode: RoundingMode, +) raises: + """Runs the interactive REPL. + + Prints a welcome banner, then loops: prompt → read → eval → print. + Errors are caught per-line and displayed without crashing the session. + The loop exits on `exit`, `quit`, or EOF (Ctrl-D). + """ + _print_banner( + precision, scientific, engineering, pad, delimiter, rounding_mode + ) + + while True: + write_prompt("decimo> ") + + var maybe_line = read_line() + if not maybe_line: + # EOF (Ctrl-D) — exit gracefully + print(file=stderr) # newline after the prompt + break + + var line = strip(maybe_line.value()) + + # Skip blank lines and comments + if is_comment_or_blank(line): + continue + + # Exit commands + if line == "exit" or line == "quit": + break + + # Evaluate the expression. evaluate_and_print displays the + # error itself before raising, so we catch and continue. + # Mojo has no typed exceptions, so we cannot selectively catch + # only user-input errors here. + try: + evaluate_and_print( + line, + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, + show_expr_on_error=True, + ) + except: + continue # error already displayed; proceed to next prompt + + +def _print_banner( + precision: Int, + scientific: Bool, + engineering: Bool, + pad: Bool, + delimiter: String, + rounding_mode: RoundingMode, +): + """Prints the REPL welcome banner to stderr.""" + print( + "Decimo — arbitrary-precision calculator", + file=stderr, + ) + print( + "Type an expression, or 'exit' to quit.", + file=stderr, + ) + + # Build settings line: "Precision: N. Engineering notation." + var settings = "Precision: " + String(precision) + "." + if scientific: + settings += " Scientific notation." + elif engineering: + settings += " Engineering notation." + if pad: + settings += " Zero-padded." + if delimiter: + settings += " Delimiter: '" + delimiter + "'." + if not (rounding_mode == RoundingMode.half_even()): + settings += " Rounding: " + String(rounding_mode) + "." + print(settings, file=stderr) diff --git a/src/cli/calculator/tokenizer.mojo b/src/cli/calculator/tokenizer.mojo index 14037fec..c8170c45 100644 --- a/src/cli/calculator/tokenizer.mojo +++ b/src/cli/calculator/tokenizer.mojo @@ -51,7 +51,7 @@ struct Token(Copyable, ImplicitlyCopyable, Movable): var position: Int """0-based column index in the original expression where this token starts. Used to produce clear diagnostics such as - ``Error at position 5: unexpected '*'``.""" + `Error at position 5: unexpected '*'`.""" def __init__(out self, kind: Int, value: String = "", position: Int = 0): self.kind = kind diff --git a/src/cli/main.mojo b/src/cli/main.mojo index 006a79ab..3d666ba5 100644 --- a/src/cli/main.mojo +++ b/src/cli/main.mojo @@ -15,10 +15,8 @@ from std.sys import exit from argmojo import Parsable, Option, Flag, Positional, Command from decimo.rounding_mode import RoundingMode -from calculator.tokenizer import tokenize -from calculator.parser import parse_to_rpn -from calculator.evaluator import evaluate_rpn, final_round from calculator.display import print_error +from calculator.engine import evaluate_and_print from calculator.io import ( stdin_is_tty, read_stdin, @@ -26,6 +24,7 @@ from calculator.io import ( filter_expression_lines, read_file_text, ) +from calculator.repl import run_repl struct DecimoArgs(Parsable): @@ -145,7 +144,7 @@ def _run() raises: # 1. --file flag provided → file mode # 2. Positional expr provided → expression mode (one-shot) # 3. No expr, stdin is piped → pipe mode - # 4. No expr, stdin is a TTY → error (no input) + # 4. No expr, stdin is a TTY → interactive REPL var has_file = len(args.file.value) > 0 var has_expr = len(args.expr.value) > 0 @@ -168,7 +167,7 @@ def _run() raises: elif has_expr: # ── Expression mode (one-shot) ─────────────────────────────────── try: - _evaluate_and_print( + evaluate_and_print( args.expr.value, precision, scientific, @@ -191,16 +190,16 @@ def _run() raises: rounding_mode, ) else: - # No expression, no file, no pipe — show help. - print_error("no expression provided") - print( - "Usage: decimo [OPTIONS] [EXPR]\n" - " echo 'EXPR' | decimo [OPTIONS]\n" - " decimo -F FILE [OPTIONS]\n" - "\n" - "Run 'decimo --help' for more information." + # ── REPL mode ─────────────────────────────────────────────────── + # No expression, no file, no pipe — launch interactive session. + run_repl( + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, ) - exit(1) # ===----------------------------------------------------------------------=== # @@ -226,7 +225,7 @@ def _run_pipe_mode( for i in range(len(expressions)): try: - _evaluate_and_print( + evaluate_and_print( expressions[i], precision, scientific, @@ -267,7 +266,7 @@ def _run_file_mode( for i in range(len(expressions)): try: - _evaluate_and_print( + evaluate_and_print( expressions[i], precision, scientific, @@ -285,129 +284,6 @@ def _run_file_mode( exit(1) -# ===----------------------------------------------------------------------=== # -# Core evaluation and formatting -# ===----------------------------------------------------------------------=== # - - -def _evaluate_and_print( - expr: String, - precision: Int, - scientific: Bool, - engineering: Bool, - pad: Bool, - delimiter: String, - rounding_mode: RoundingMode, - show_expr_on_error: Bool = False, -) raises: - """Tokenize, parse, evaluate, and print one expression. - - On error, displays a coloured diagnostic and raises to signal failure - to the caller. - """ - try: - var tokens = tokenize(expr) - var rpn = parse_to_rpn(tokens^) - - try: - var value = final_round( - evaluate_rpn(rpn^, precision), precision, rounding_mode - ) - - if scientific: - print(value.to_string(scientific=True, delimiter=delimiter)) - elif engineering: - print(value.to_string(engineering=True, delimiter=delimiter)) - elif pad: - print( - _pad_to_precision( - value.to_string(force_plain=True), precision - ) - ) - else: - print(value.to_string(delimiter=delimiter)) - except eval_err: - if show_expr_on_error: - _display_calc_error(String(eval_err), expr) - else: - print_error(String(eval_err)) - raise eval_err^ - - except parse_err: - if show_expr_on_error: - _display_calc_error(String(parse_err), expr) - else: - print_error(String(parse_err)) - raise parse_err^ - - -def _display_calc_error(error_msg: String, expr: String): - """Parses a calculator error message and displays it with colours - and a caret indicator. - - The calculator engine produces errors in two forms: - - 1. ``Error at position N: `` — with position info. - 2. ```` — without position info. - - This function detects form (1), extracts the position, and calls - `print_error(description, expr, position)` so the user sees a - visual caret under the offending column. For form (2) it falls - back to a plain coloured error. - """ - comptime PREFIX = "Error at position " - - if error_msg.startswith(PREFIX): - # Find the colon after the position number. - var after_prefix = len(PREFIX) - var colon_pos = -1 - for i in range(after_prefix, len(error_msg)): - if error_msg[byte=i] == ":": - colon_pos = i - break - - if colon_pos > after_prefix: - # Extract position number and description. - var pos_str = String(error_msg[byte=after_prefix:colon_pos]) - var description = String( - error_msg[byte = colon_pos + 2 :] - ) # skip ": " - - try: - var pos = Int(pos_str) - print_error(description, expr, pos) - return - except: - pass # fall through to plain display - - # Fallback: no position info — just show the message. - print_error(error_msg) - - -def _pad_to_precision(plain: String, precision: Int) -> String: - """Pads (or adds) trailing zeros so the fractional part has exactly - `precision` digits. - """ - if precision <= 0: - return plain - - var dot_pos = -1 - for i in range(len(plain)): - if plain[byte=i] == ".": - dot_pos = i - break - - if dot_pos < 0: - # No decimal point — add one with `precision` zeros - return plain + "." + "0" * precision - - var frac_len = len(plain) - dot_pos - 1 - if frac_len >= precision: - return plain - - return plain + "0" * (precision - frac_len) - - def _parse_rounding_mode(name: String) -> RoundingMode: """Converts a CLI rounding-mode name (hyphenated) to a RoundingMode value. """ diff --git a/src/decimo/bigdecimal/bigdecimal.mojo b/src/decimo/bigdecimal/bigdecimal.mojo index e4dbcdc5..d7dc9f93 100644 --- a/src/decimo/bigdecimal/bigdecimal.mojo +++ b/src/decimo/bigdecimal/bigdecimal.mojo @@ -586,20 +586,20 @@ struct BigDecimal( ) -> String: """Returns string representation of the number. - This method follows CPython's ``Decimal.__str__`` logic exactly for + This method follows CPython's `Decimal.__str__` logic exactly for the default and scientific-notation paths. - - Engineering notation (``engineering=True``) is tried first. - The exponent is always a multiple of 3 (e.g. ``12.34E+6``, - ``500E-3``). Trailing zeros in the mantissa are stripped. + - Engineering notation (`engineering=True`) is tried first. + The exponent is always a multiple of 3 (e.g. `12.34E+6`, + `500E-3`). Trailing zeros in the mantissa are stripped. - Scientific notation is used when: - 1. ``scientific`` parameter is True, OR + 1. `scientific` parameter is True, OR 2. The internal exponent > 0 (i.e., scale < 0), OR 3. There are more than 6 leading zeros after the decimal - point (adjusted exponent < -6), unless ``force_plain=True``. + point (adjusted exponent < -6), unless `force_plain=True`. - Otherwise, plain (fixed-point) notation is used. - When both ``engineering`` and ``scientific`` are True, engineering + When both `engineering` and `scientific` are True, engineering notation takes precedence. Args: @@ -610,13 +610,13 @@ struct BigDecimal( notation (exponent is a multiple of 3, trailing zeros stripped). force_plain: If True, suppress the CPython-compatible - auto-detection of scientific notation (the ``scale < 0`` and - ``leftdigits <= -6`` rules are not applied). Useful when a + auto-detection of scientific notation (the `scale < 0` and + `leftdigits <= -6` rules are not applied). Useful when a guaranteed fixed-point string is needed regardless of - magnitude. Has no effect when ``scientific`` or - ``engineering`` is True. + magnitude. Has no effect when `scientific` or + `engineering` is True. delimiter: A string inserted every 3 digits in both the integer - and fractional parts (e.g. ``"_"`` gives ``1_234.567_89``). + and fractional parts (e.g. `"_"` gives `1_234.567_89`). An empty string (default) disables grouping. line_width: The maximum line width for the string representation. If 0, the string is returned as a single line. @@ -2424,18 +2424,18 @@ struct BigDecimal( def _insert_digit_separators(s: String, delimiter: String) -> String: - """Insert ``delimiter`` every 3 digits in both the integer and + """Insert `delimiter` every 3 digits in both the integer and fractional parts of a numeric string. - The function is aware of an optional leading ``-`` sign and a trailing - exponent suffix (``E+3``, ``E-12``, …). Only the mantissa digits are + The function is aware of an optional leading `-` sign and a trailing + exponent suffix (`E+3`, `E-12`, …). Only the mantissa digits are grouped; the sign and exponent are preserved verbatim. - Examples (with ``delimiter = "_"``): - ``"1234567"`` → ``"1_234_567"`` - ``"1234567.891011"`` → ``"1_234_567.891_011"`` - ``"12.345678E+6"`` → ``"12.345_678E+6"`` - ``"-0.00123"`` → ``"-0.001_23"`` + Examples (with `delimiter = "_"`): + `"1234567"` → `"1_234_567"` + `"1234567.891011"` → `"1_234_567.891_011"` + `"12.345678E+6"` → `"12.345_678E+6"` + `"-0.00123"` → `"-0.001_23"` """ if not delimiter: return s diff --git a/src/decimo/bigfloat/mpfr_wrapper.mojo b/src/decimo/bigfloat/mpfr_wrapper.mojo index 33425243..79caf8d0 100644 --- a/src/decimo/bigfloat/mpfr_wrapper.mojo +++ b/src/decimo/bigfloat/mpfr_wrapper.mojo @@ -131,13 +131,13 @@ fn mpfrw_get_raw_digits( """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``. + 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. + 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``. + `mpfrw_free_raw_str`. Args: handle: MPFR handle index. @@ -151,10 +151,10 @@ fn mpfrw_get_raw_digits( fn mpfrw_free_raw_str(addr: Int): - """Frees a digit string returned by ``mpfrw_get_raw_digits``. + """Frees a digit string returned by `mpfrw_get_raw_digits`. Args: - addr: Raw address returned by ``mpfrw_get_raw_digits``. + addr: Raw address returned by `mpfrw_get_raw_digits`. """ external_call["mpfrw_free_raw_str", NoneType](addr) diff --git a/src/decimo/bigint/bigint.mojo b/src/decimo/bigint/bigint.mojo index 55408738..05c128ce 100644 --- a/src/decimo/bigint/bigint.mojo +++ b/src/decimo/bigint/bigint.mojo @@ -753,7 +753,7 @@ struct BigInt( def to_decimal_string(self, line_width: Int = 0) -> String: """Returns the decimal string representation of the BigInt. - Deprecated: Use ``to_string(line_width=...)`` instead. + Deprecated: Use `to_string(line_width=...)` instead. Args: line_width: The maximum line width for the output. diff --git a/src/decimo/errors.mojo b/src/decimo/errors.mojo index d73ccc3a..01942519 100644 --- a/src/decimo/errors.mojo +++ b/src/decimo/errors.mojo @@ -27,7 +27,7 @@ ValueError: description of what went wrong File name and line number are automatically captured at the raise site using `call_location()`. The absolute path is automatically shortened to a relative -path (e.g. ``./src/...``, ``./tests/...``) for readability and privacy. +path (e.g. `./src/...`, `./tests/...`) for readability and privacy. Function name must be provided manually since Mojo does not have a built-in way to get the current function name at runtime. """ @@ -85,13 +85,13 @@ failures, missing native libraries).""" def _shorten_path(full_path: String) -> String: """Shorten an absolute file path to a relative path. - Looks for known directory markers (``src/``, ``tests/``, ``benches/``) and - returns a ``./``-prefixed relative path from the rightmost marker found. + Looks for known directory markers (`src/`, `tests/`, `benches/`) and + returns a `./`-prefixed relative path from the rightmost marker found. If no marker is found, returns just the filename. - Uses ``rfind`` (reverse search) to handle paths that contain a marker more - than once, e.g. ``/home/user/src/projects/decimo/src/decimo/bigint.mojo`` - correctly shortens to ``./src/decimo/bigint.mojo``. When more than one + Uses `rfind` (reverse search) to handle paths that contain a marker more + than once, e.g. `/home/user/src/projects/decimo/src/decimo/bigint.mojo` + correctly shortens to `./src/decimo/bigint.mojo`. When more than one marker type appears, the rightmost position wins to produce the shortest possible relative path. diff --git a/tests/cli/test_engine.mojo b/tests/cli/test_engine.mojo new file mode 100644 index 00000000..a8ae2c90 --- /dev/null +++ b/tests/cli/test_engine.mojo @@ -0,0 +1,73 @@ +"""Test the engine helpers: pad_to_precision, display_calc_error.""" + +from std import testing + +from calculator.engine import pad_to_precision + + +# ===----------------------------------------------------------------------=== # +# Tests: pad_to_precision +# ===----------------------------------------------------------------------=== # + + +def test_pad_integer() raises: + """Integer without decimal point gets one added.""" + testing.assert_equal(pad_to_precision("42", 5), "42.00000") + + +def test_pad_short_fraction() raises: + """Fractional part shorter than precision is zero-padded.""" + testing.assert_equal(pad_to_precision("3.14", 10), "3.1400000000") + + +def test_pad_exact_fraction() raises: + """Fractional part already at precision is unchanged.""" + testing.assert_equal(pad_to_precision("1.234", 3), "1.234") + + +def test_pad_long_fraction() raises: + """Fractional part longer than precision is unchanged (no truncation).""" + testing.assert_equal(pad_to_precision("1.23456789", 3), "1.23456789") + + +def test_pad_zero_precision() raises: + """Precision 0 returns the input unchanged.""" + testing.assert_equal(pad_to_precision("42", 0), "42") + + +def test_pad_negative_precision() raises: + """Negative precision returns the input unchanged.""" + testing.assert_equal(pad_to_precision("42", -1), "42") + + +def test_pad_zero_value() raises: + """Zero value gets padded normally.""" + testing.assert_equal(pad_to_precision("0", 3), "0.000") + + +def test_pad_already_has_dot_no_digits() raises: + """Value with dot but no fractional digits gets padded.""" + testing.assert_equal(pad_to_precision("5.", 4), "5.0000") + + +def test_pad_precision_one() raises: + """Precision 1 on integer adds '.0'.""" + testing.assert_equal(pad_to_precision("7", 1), "7.0") + + +# ===----------------------------------------------------------------------=== # +# Main +# ===----------------------------------------------------------------------=== # + + +def main() raises: + test_pad_integer() + test_pad_short_fraction() + test_pad_exact_fraction() + test_pad_long_fraction() + test_pad_zero_precision() + test_pad_negative_precision() + test_pad_zero_value() + test_pad_already_has_dot_no_digits() + test_pad_precision_one() + print("test_engine: all tests passed") From 970cc43c12e4d08d1eb78034f98b1f37fc71f3e8 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Tue, 14 Apr 2026 08:14:17 +0200 Subject: [PATCH 16/17] [cli] Add `ans` variable + variable assignment and evaluation (#206) --- docs/plans/cli_calculator.md | 38 +++--- docs/todo.md | 3 +- src/cli/calculator/__init__.mojo | 8 +- src/cli/calculator/engine.mojo | 50 +++++++- src/cli/calculator/evaluator.mojo | 46 +++++-- src/cli/calculator/parser.mojo | 19 +-- src/cli/calculator/repl.mojo | 200 ++++++++++++++++++++++++------ src/cli/calculator/tokenizer.mojo | 41 ++++-- src/decimo/__init__.mojo | 2 +- src/decimo/bigint/bigint.mojo | 2 + src/decimo/prelude.mojo | 2 +- tests/cli/test_evaluator.mojo | 74 +++++++++++ tests/cli/test_repl.mojo | 136 ++++++++++++++++++++ tests/cli/test_tokenizer.mojo | 70 +++++++++++ 14 files changed, 606 insertions(+), 85 deletions(-) create mode 100644 tests/cli/test_repl.mojo diff --git a/docs/plans/cli_calculator.md b/docs/plans/cli_calculator.md index 88e59ff3..9e092cf9 100644 --- a/docs/plans/cli_calculator.md +++ b/docs/plans/cli_calculator.md @@ -369,17 +369,22 @@ Error: division by zero decimo> exit ``` -| # | Task | Status | Notes | -| --- | --------------------------------------- | :----: | -------------------------------------------------------------------------- | -| 4.1 | No-args + TTY → launch REPL | ✓ | Replace "no expression" error with REPL auto-launch when terminal detected | -| 4.2 | Read-eval-print loop | ✓ | `read_line()` via `getchar()`; one expression per line | -| 4.3 | Custom prompt (`decimo>`) | ✓ | Coloured prompt to stderr so results can be piped | -| 4.4 | `ans` variable (previous result) | ✗ | Injected as a constant into the evaluator; starts as `0` | -| 4.5 | Variable assignment (`x = expr`) | ✗ | Parse `name = expr` syntax; store in a name→BigDecimal map | -| 4.6 | Meta-commands (`:precision N`, `:vars`) | ✗ | `:` prefix avoids collision with expressions | -| 4.7 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✓ | | -| 4.8 | Error recovery (don't crash session) | ✓ | Catch exceptions per-line, display error, continue loop | -| 4.9 | History (if Mojo gets readline support) | ✗ | Future — depends on Mojo FFI evolution | +| # | Task | Status | Notes | +| ---- | ------------------------------------------ | :----: | --------------------------------------------------------------------------- | +| 4.1 | No-args + TTY → launch REPL | ✓ | Replace "no expression" error with REPL auto-launch when terminal detected | +| 4.2 | Read-eval-print loop | ✓ | `read_line()` via `getchar()`; one expression per line | +| 4.3 | Custom prompt (`decimo>`) | ✓ | Coloured prompt to stderr so results can be piped | +| 4.4 | `ans` variable (previous result) | ✓ | Stored in `Dict[String, Decimal]`; updated after each successful evaluation | +| 4.5 | Variable assignment (`x = expr`) | ✓ | `name = expr` detection in REPL; protected names (pi, e, functions, ans) | +| 4.6 | Meta-commands (`:precision N`, `:vars`) | ✗ | `:` prefix avoids collision with expressions, allow short aliases | +| 4.7 | One-line quick setting | ✗ | `:p 100 s down` sets precision, scientific notation, and round_down mode | +| 4.8 | Same-line temp precision setting | ✗ | `2*sqrt(1.23):p 100 s down` for a temporary setting for the expression | +| 4.9 | Print settings (`:settings`) | ✗ | Display current precision, formatting options, etc. | +| 4.10 | Variable listing (`:vars` and `:variable`) | ✗ | List all user-defined variables and their values | +| 4.11 | Everything in the REPL is case-insensitive | ✗ | Map all input chars to lower case at pre-tokenizer stage | +| 4.12 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✓ | | +| 4.13 | Error recovery (don't crash session) | ✓ | Catch exceptions per-line, display error, continue loop | +| 4.14 | History (if Mojo gets readline support) | ✗ | Future — depends on Mojo FFI evolution | ### Phase 5: Future Enhancements @@ -387,11 +392,12 @@ decimo> exit 2. Detect full-width digits/operators for CJK users while parsing. 3. Response files (`@expressions.txt`) — when Mojo compiler bug is fixed, use ArgMojo's `cmd.response_file_prefix("@")`. -| # | Task | Status | Notes | -| --- | ------------------------------------------- | :----: | ------------------------------------------------------------------------------ | -| 5.1 | Build and distribute as single binary | ✗ | Defer until REPL is stable; Homebrew, GitHub Releases, `curl \| sh` installer | -| 5.2 | Full-width digit/operator detection for CJK | ✗ | Tokenizer-level handling for CJK users | -| 5.3 | Response files (`@expressions.txt`) | ✗ | Blocked on Mojo compiler bug; `cmd.response_file_prefix("@")` ready when fixed | +| # | Task | Status | Notes | +| --- | ------------------------------------------- | :----: | --------------------------------------------------------------------------------- | +| 5.1 | Full-width digit/operator detection for CJK | ✗ | Tokenizer-level handling for CJK users | +| 5.2 | Full-width to half-width normalization | ✗ | Pre-tokenizer normalization step to convert full-width chars to ASCII equivalents | +| 5.3 | Build and distribute as single binary | ✗ | Defer until REPL is stable; Homebrew, GitHub Releases, `curl \| sh` installer | +| 5.4 | Response files (`@expressions.txt`) | ✗ | Blocked on Mojo compiler bug; `cmd.response_file_prefix("@")` ready when fixed | ## Design Decisions diff --git a/docs/todo.md b/docs/todo.md index 737ec72a..b9da8ae2 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -8,9 +8,10 @@ This is a to-do list for Decimo. - [ ] Implement different methods for adding decimo types with `Int` types so that an implicit conversion is not required. - [ ] Use debug mode to check for unnecessary zero words before all arithmetic operations. This will help ensure that there are no zero words, which can simplify the speed of checking for zero because we only need to check the first word. - [ ] Check the `floor_divide()` function of `BigUInt`. Currently, the speed of division between imilar-sized numbers are okay, but the speed of 2n-by-n, 4n-by-n, and 8n-by-n divisions decreases unproportionally. This is likely due to the segmentation of the dividend in the Burnikel-Ziegler algorithm. +- [ ] Consider using `Decimal` as the struct name instead of `BigDecimal`, and use `comptime BigDecimal = Decimal` to create an alias for the `Decimal` struct. This just switches the alias and the struct name, but it may be more intuitive to use `Decimal` as the struct name since it is more consistent with Python's `decimal.Decimal`. Moreover, hovering over `Decimal` will show the docstring of the struct, which is more intuitive than hovering over `BigDecimal` to see the docstring of the struct. - [x] (PR #127, #128, #131) Make all default constructor "safe", which means that the words are checked and normalized to ensure that there are no zero words and that the number is in a valid state. This will help prevent bugs and ensure that all `BigUInt` instances are in a consistent state. Also allow users to create "unsafe" `BigUInt` instances if they want to, but there must be a key-word only argument, e.g., `raw_words`. -- [x] (#31) The `exp()` function performs slower than Python's counterpart in specific cases. Detailed investigation reveals the bottleneck stems from multiplication operations between decimals with significant fractional components. These operations currently rely on UInt256 arithmetic, which introduces performance overhead. Optimization of the `multiply()` function is required to address these performance bottlenecks, particularly for high-precision decimal multiplication with many digits after the decimal point. +- [x] (#31) The `exp()` function performs slower than Python's counterpart in specific cases. Detailed investigation reveals the bottleneck stems from multiplication operations between decimals with significant fractional components. These operations currently rely on UInt256 arithmetic, which introduces performance overhead. Optimization of the `multiply()` function is required to address these performance bottlenecks, particularly for high-precision decimal multiplication with many digits after the decimal point. Internally, also use `Decimal` instead of `BigDecimal` or `BDec` to be consistent. - [x] Implement different methods for augmented arithmetic assignments to improve memeory-efficiency and performance. - [x] Implement a method `remove_leading_zeros` for `BigUInt`, which removes the zero words from the most significant end of the number. - [x] Use debug mode to check for uninitialized `BigUInt` before all arithmetic operations. This will help ensure that there are no uninitialized `BigUInt`. diff --git a/src/cli/calculator/__init__.mojo b/src/cli/calculator/__init__.mojo index 83fda281..0b8b0a01 100644 --- a/src/cli/calculator/__init__.mojo +++ b/src/cli/calculator/__init__.mojo @@ -41,10 +41,16 @@ from .tokenizer import ( TOKEN_FUNC, TOKEN_CONST, TOKEN_COMMA, + TOKEN_VARIABLE, ) from .parser import parse_to_rpn from .evaluator import evaluate_rpn, evaluate -from .engine import evaluate_and_print, display_calc_error, pad_to_precision +from .engine import ( + evaluate_and_print, + evaluate_and_return, + display_calc_error, + pad_to_precision, +) from .display import print_error, print_warning, print_hint, write_prompt from .io import ( stdin_is_tty, diff --git a/src/cli/calculator/engine.mojo b/src/cli/calculator/engine.mojo index 2b58d463..5100af03 100644 --- a/src/cli/calculator/engine.mojo +++ b/src/cli/calculator/engine.mojo @@ -22,7 +22,9 @@ used by both one-shot/pipe/file modes (main.mojo) and the interactive REPL (repl.mojo). """ +from decimo import Decimal from decimo.rounding_mode import RoundingMode +from std.collections import Dict from .tokenizer import tokenize from .parser import parse_to_rpn from .evaluator import evaluate_rpn, final_round @@ -38,6 +40,7 @@ def evaluate_and_print( delimiter: String, rounding_mode: RoundingMode, show_expr_on_error: Bool = False, + variables: Dict[String, Decimal] = Dict[String, Decimal](), ) raises: """Tokenize, parse, evaluate, and print one expression. @@ -54,12 +57,13 @@ def evaluate_and_print( rounding_mode: Rounding mode for the final result. show_expr_on_error: If True, show the expression with a caret indicator on error. If False, show only the error message. + variables: A name→value mapping of user-defined variables. """ try: - var tokens = tokenize(expr) + var tokens = tokenize(expr, variables) var rpn = parse_to_rpn(tokens^) var value = final_round( - evaluate_rpn(rpn^, precision), precision, rounding_mode + evaluate_rpn(rpn^, precision, variables), precision, rounding_mode ) if scientific: @@ -149,3 +153,45 @@ def pad_to_precision(plain: String, precision: Int) -> String: return plain return plain + "0" * (precision - frac_len) + + +def evaluate_and_return( + expr: String, + precision: Int, + scientific: Bool, + engineering: Bool, + pad: Bool, + delimiter: String, + rounding_mode: RoundingMode, + variables: Dict[String, Decimal] = Dict[String, Decimal](), +) raises -> Decimal: + """Tokenize, parse, evaluate, print, and return the result. + + Like `evaluate_and_print` but also returns the `Decimal` value so the + REPL can store it in `ans` or a named variable. + + On error, displays a coloured diagnostic and raises to signal failure + to the caller. + """ + try: + var tokens = tokenize(expr, variables) + var rpn = parse_to_rpn(tokens^) + var value = final_round( + evaluate_rpn(rpn^, precision, variables), precision, rounding_mode + ) + + if scientific: + print(value.to_string(scientific=True, delimiter=delimiter)) + elif engineering: + print(value.to_string(engineering=True, delimiter=delimiter)) + elif pad: + print( + pad_to_precision(value.to_string(force_plain=True), precision) + ) + else: + print(value.to_string(delimiter=delimiter)) + + return value^ + except e: + display_calc_error(String(e), expr) + raise e^ diff --git a/src/cli/calculator/evaluator.mojo b/src/cli/calculator/evaluator.mojo index 66f8b52c..bbfc7eb6 100644 --- a/src/cli/calculator/evaluator.mojo +++ b/src/cli/calculator/evaluator.mojo @@ -20,8 +20,9 @@ RPN evaluator for the Decimo CLI calculator. Evaluates a Reverse Polish Notation token list using BigDecimal arithmetic. """ -from decimo import BDec +from decimo import Decimal from decimo.rounding_mode import RoundingMode +from std.collections import Dict from .tokenizer import ( Token, @@ -34,6 +35,7 @@ from .tokenizer import ( TOKEN_CARET, TOKEN_FUNC, TOKEN_CONST, + TOKEN_VARIABLE, ) from .parser import parse_to_rpn from .tokenizer import tokenize @@ -45,7 +47,7 @@ from .tokenizer import tokenize def _call_func( - name: String, mut stack: List[BDec], precision: Int, position: Int + name: String, mut stack: List[Decimal], precision: Int, position: Int ) raises: """Pop argument(s) from `stack`, call the named Decimo function, and push the result back. @@ -168,7 +170,11 @@ def _call_func( # ===----------------------------------------------------------------------=== # -def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec: +def evaluate_rpn( + rpn: List[Token], + precision: Int, + variables: Dict[String, Decimal] = Dict[String, Decimal](), +) raises -> Decimal: """Evaluate an RPN token list using BigDecimal arithmetic. Internally uses `working_precision = precision + GUARD_DIGITS` for all @@ -176,6 +182,13 @@ def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec: responsible for rounding the final result to `precision` significant digits (see `final_round`). + Args: + rpn: The Reverse Polish Notation token list. + precision: Number of significant digits. + variables: A name→value mapping of user-defined variables (e.g. + `ans`, `x`). If a TOKEN_VARIABLE token's name is not found + in this dict, an error is raised. + Raises: Error: On division by zero, missing operands, or other runtime errors — with source position when available. @@ -186,19 +199,19 @@ def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec: # accumulated rounding errors from intermediate operations. comptime GUARD_DIGITS = 9 var working_precision = precision + GUARD_DIGITS - var stack = List[BDec]() + var stack = List[Decimal]() for i in range(len(rpn)): var kind = rpn[i].kind if kind == TOKEN_NUMBER: - stack.append(BDec.from_string(rpn[i].value)) + stack.append(Decimal.from_string(rpn[i].value)) elif kind == TOKEN_CONST: if rpn[i].value == "pi": - stack.append(BDec.pi(working_precision)) + stack.append(Decimal.pi(working_precision)) elif rpn[i].value == "e": - stack.append(BDec.e(working_precision)) + stack.append(Decimal.e(working_precision)) else: raise Error( "Error at position " @@ -208,6 +221,19 @@ def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec: + "'" ) + elif kind == TOKEN_VARIABLE: + var var_name = rpn[i].value + if var_name in variables: + stack.append(variables[var_name].copy()) + else: + raise Error( + "Error at position " + + String(rpn[i].position) + + ": undefined variable '" + + var_name + + "'" + ) + elif kind == TOKEN_UNARY_MINUS: if len(stack) < 1: raise Error( @@ -306,10 +332,10 @@ def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec: def final_round( - value: BDec, + value: Decimal, precision: Int, rounding_mode: RoundingMode = RoundingMode.half_even(), -) raises -> BDec: +) raises -> Decimal: """Round a BigDecimal to `precision` significant digits. This should be called on the result of `evaluate_rpn` before @@ -327,7 +353,7 @@ def evaluate( expr: String, precision: Int = 50, rounding_mode: RoundingMode = RoundingMode.half_even(), -) raises -> BDec: +) raises -> Decimal: """Evaluate a math expression string and return a BigDecimal result. This is the main entry point for the calculator engine. diff --git a/src/cli/calculator/parser.mojo b/src/cli/calculator/parser.mojo index 66b5f581..0446979d 100644 --- a/src/cli/calculator/parser.mojo +++ b/src/cli/calculator/parser.mojo @@ -34,16 +34,17 @@ from .tokenizer import ( TOKEN_FUNC, TOKEN_CONST, TOKEN_COMMA, + TOKEN_VARIABLE, ) def parse_to_rpn(tokens: List[Token]) raises -> List[Token]: - """Converts infix tokens to Reverse Polish Notation using - Dijkstra's shunting-yard algorithm. + """Converts infix tokens to Reverse Polish Notation using Dijkstra's + shunting-yard algorithm. - Supports binary operators (+, -, *, /, ^), unary minus, - function calls (sqrt, ln, …), constants (pi, e), and commas - for multi-argument functions like root(x, n). + Supports binary operators (+, -, *, /, ^), unary minus, function calls + (sqrt, ln, …), constants (pi, e), and commas for multi-argument functions + like root(x, n). Raises: Error: On mismatched parentheses, misplaced commas, or trailing @@ -55,8 +56,12 @@ def parse_to_rpn(tokens: List[Token]) raises -> List[Token]: for i in range(len(tokens)): var kind = tokens[i].kind - # Numbers and constants go straight to output - if kind == TOKEN_NUMBER or kind == TOKEN_CONST: + # Numbers, constants, and variables go straight to output + if ( + kind == TOKEN_NUMBER + or kind == TOKEN_CONST + or kind == TOKEN_VARIABLE + ): output.append(tokens[i]) # Functions are pushed onto the operator stack diff --git a/src/cli/calculator/repl.mojo b/src/cli/calculator/repl.mojo index 87822c04..27a641d4 100644 --- a/src/cli/calculator/repl.mojo +++ b/src/cli/calculator/repl.mojo @@ -21,27 +21,32 @@ Launched when `decimo` is invoked with no expression and stdin is a TTY. Reads one expression per line, evaluates it, prints the result, and loops until the user types `exit`, `quit`, or presses Ctrl-D. -Architecture notes for future PRs: - -- `ans` variable (4.4): The evaluator will need a `variables` dict - passed into `evaluate_rpn`. The REPL will inject `ans` after each - successful evaluation. The tokenizer already treats unknown identifiers - as errors, so it will need a `known_names: Set[String]` parameter. +Features: +- `ans` — automatically holds the result of the last successful evaluation. +- Variable assignment — `x = ` stores a named value for later use. +- Error recovery — display error and continue, don't crash the session. -- Variable assignment (4.5): The REPL will detect `name = expr` syntax - *before* calling the evaluator (simple string split on first `=` that - is not inside parentheses). The result is stored in the variables dict. +Architecture notes for future PRs: - Meta-commands (4.6): Lines starting with `:` are intercepted before evaluation. Examples: `:precision 100`, `:vars`, `:help`. """ from std.sys import stderr +from std.collections import Dict +from decimo import Decimal from decimo.rounding_mode import RoundingMode -from .display import write_prompt -from .engine import evaluate_and_print +from .display import BOLD, RESET, YELLOW +from .display import write_prompt, print_error +from .engine import evaluate_and_return from .io import read_line, strip, is_comment_or_blank +from .tokenizer import ( + is_alpha_or_underscore, + is_alnum_or_underscore, + is_known_function, + is_known_constant, +) def run_repl( @@ -57,11 +62,18 @@ def run_repl( Prints a welcome banner, then loops: prompt → read → eval → print. Errors are caught per-line and displayed without crashing the session. The loop exits on `exit`, `quit`, or EOF (Ctrl-D). + + Maintains a variable store with: + - `ans`: automatically updated after each successful evaluation. + - User-defined variables via `name = expr` assignment syntax. """ _print_banner( precision, scientific, engineering, pad, delimiter, rounding_mode ) + var variables = Dict[String, Decimal]() + variables["ans"] = Decimal() # "0" by default, updated after each eval + while True: write_prompt("decimo> ") @@ -81,23 +93,138 @@ def run_repl( if line == "exit" or line == "quit": break - # Evaluate the expression. evaluate_and_print displays the - # error itself before raising, so we catch and continue. - # Mojo has no typed exceptions, so we cannot selectively catch - # only user-input errors here. - try: - evaluate_and_print( - line, - precision, - scientific, - engineering, - pad, - delimiter, - rounding_mode, - show_expr_on_error=True, - ) - except: - continue # error already displayed; proceed to next prompt + # Check for variable assignment: `name = expr` + var assignment = _parse_assignment(line) + + if assignment: + var var_name = assignment.value()[0] + var expr = assignment.value()[1] + + # Validate the variable name + var err = _validate_variable_name(var_name) + if err: + print_error(err.value()) + continue + + # Evaluate the expression and store the result + try: + var result = evaluate_and_return( + expr, + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, + variables, + ) + variables[var_name] = result.copy() + variables["ans"] = result^ + except: + continue # error already displayed + else: + # Regular expression — evaluate and update ans + try: + var result = evaluate_and_return( + line, + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, + variables, + ) + variables["ans"] = result^ + except: + continue # error already displayed + + +def _parse_assignment(line: String) -> Optional[Tuple[String, String]]: + """Detect `name = expr` assignment syntax. + + Returns (variable_name, expression) if the line is an assignment, + or None if it is a regular expression. + + The first `=` that is not `==` and is preceded by a valid identifier + (with optional whitespace) triggers assignment mode. If the identifier + is a function name followed by `(`, it is not an assignment (e.g. + `sqrt(2)` is not `sqrt = ...`). + """ + var line_bytes = StringSlice(line).as_bytes() + var n = len(line_bytes) + + # Skip leading whitespace to find the identifier start + var i = 0 + while i < n and (line_bytes[i] == 32 or line_bytes[i] == 9): + i += 1 + + if i >= n: + return None + + # Must start with alpha or underscore + if not is_alpha_or_underscore(line_bytes[i]): + return None + + var name_start = i + i += 1 + while i < n and is_alnum_or_underscore(line_bytes[i]): + i += 1 + var name_end = i + + # Skip whitespace after name + while i < n and (line_bytes[i] == 32 or line_bytes[i] == 9): + i += 1 + + # Check for '=' (but not '==') + if i >= n or line_bytes[i] != 61: # '=' + return None + if i + 1 < n and line_bytes[i + 1] == 61: # '==' + return None + + # Extract name and expression + var name_bytes = List[UInt8](capacity=name_end - name_start) + for j in range(name_start, name_end): + name_bytes.append(line_bytes[j]) + var var_name = String(unsafe_from_utf8=name_bytes^) + + var expr_start = i + 1 + # Skip whitespace after '=' + while expr_start < n and ( + line_bytes[expr_start] == 32 or line_bytes[expr_start] == 9 + ): + expr_start += 1 + + if expr_start >= n: + return None # `x =` with no expression — treat as regular expression + + var expr_bytes = List[UInt8](capacity=n - expr_start) + for j in range(expr_start, n): + expr_bytes.append(line_bytes[j]) + var expr = String(unsafe_from_utf8=expr_bytes^) + + return (var_name^, expr^) + + +def _validate_variable_name(name: String) -> Optional[String]: + """Validate a variable name for assignment. + + Returns an error message if the name is invalid, or None if valid. + Rejects: + - `ans` (read-only built-in) + - Built-in function names (sqrt, sin, etc.) + - Built-in constant names (pi, e) + """ + if name == "ans": + return ( + "cannot assign to 'ans' (read-only; it always holds the last" + " result)" + ) + if is_known_function(name): + return "cannot assign to '" + name + "' (built-in function)" + if is_known_constant(name): + return "cannot assign to '" + name + "' (built-in constant)" + return None def _print_banner( @@ -109,14 +236,17 @@ def _print_banner( rounding_mode: RoundingMode, ): """Prints the REPL welcome banner to stderr.""" - print( - "Decimo — arbitrary-precision calculator", - file=stderr, - ) - print( - "Type an expression, or 'exit' to quit.", - file=stderr, + comptime message = ( + BOLD + + YELLOW + + "Decimo — arbitrary-precision calculator, written in pure Mojo 🔥\n" + + RESET + + """Type an expression to evaluate, e.g., `pi + sin(-ln(1.23)) * sqrt(e^2)`. +You can assign variables with `name = expression`, e.g., `x = 1.023^365`. +You can use `ans` to refer to the last result. +Type 'exit' or 'quit', or press Ctrl-D, to quit.""" ) + print(message, file=stderr) # Build settings line: "Precision: N. Engineering notation." var settings = "Precision: " + String(precision) + "." diff --git a/src/cli/calculator/tokenizer.mojo b/src/cli/calculator/tokenizer.mojo index c8170c45..600494e1 100644 --- a/src/cli/calculator/tokenizer.mojo +++ b/src/cli/calculator/tokenizer.mojo @@ -20,6 +20,9 @@ Tokenizer for the Decimo CLI calculator. Converts an expression string into a list of tokens for the parser. """ +from std.collections import Dict +from decimo import Decimal + # ===----------------------------------------------------------------------=== # # Token kinds # ===----------------------------------------------------------------------=== # @@ -36,6 +39,7 @@ comptime TOKEN_CARET = 8 comptime TOKEN_FUNC = 9 comptime TOKEN_CONST = 10 comptime TOKEN_COMMA = 11 +comptime TOKEN_VARIABLE = 12 # ===----------------------------------------------------------------------=== # @@ -122,7 +126,7 @@ struct Token(Copyable, ImplicitlyCopyable, Movable): # Known function names and built-in constants. -def _is_known_function(name: String) -> Bool: +def is_known_function(name: String) -> Bool: """Returns True if `name` is a recognized function.""" return ( name == "sqrt" @@ -141,32 +145,42 @@ def _is_known_function(name: String) -> Bool: ) -def _is_known_constant(name: String) -> Bool: +def is_known_constant(name: String) -> Bool: """Returns True if `name` is a recognized constant.""" return name == "pi" or name == "e" -def _is_alpha_or_underscore(c: UInt8) -> Bool: +def is_alpha_or_underscore(c: UInt8) -> Bool: """Returns True if c is a-z, A-Z, or '_'.""" return (c >= 65 and c <= 90) or (c >= 97 and c <= 122) or c == 95 -def _is_alnum_or_underscore(c: UInt8) -> Bool: +def is_alnum_or_underscore(c: UInt8) -> Bool: """Returns True if c is a-z, A-Z, 0-9, or '_'.""" - return _is_alpha_or_underscore(c) or (c >= 48 and c <= 57) + return is_alpha_or_underscore(c) or (c >= 48 and c <= 57) -def tokenize(expr: String) raises -> List[Token]: +def tokenize( + expr: String, + known_variables: Dict[String, Decimal] = Dict[String, Decimal](), +) raises -> List[Token]: """Converts an expression string into a list of tokens. Handles: numbers (integer and decimal), operators (+, -, *, /, ^), parentheses, commas, function calls (sqrt, ln, …), built-in - constants (pi, e), and distinguishes unary minus from binary minus. + constants (pi, e), user-defined variables, and distinguishes unary + minus from binary minus. Each token records its 0-based column position in the source expression so that downstream stages can emit user-friendly diagnostics that pinpoint where the problem is. + Args: + expr: The expression string to tokenize. + known_variables: Optional name→value mapping of user-defined + variables. Identifiers matching a key are emitted as + TOKEN_VARIABLE tokens instead of raising an error. + Raises: Error: On empty/whitespace-only input (without position info), unknown identifiers, or unexpected characters (with the @@ -214,10 +228,10 @@ def tokenize(expr: String) raises -> List[Token]: continue # --- Alphabetical identifier: function name or constant --- - if _is_alpha_or_underscore(c): + if is_alpha_or_underscore(c): var start = i i += 1 - while i < n and _is_alnum_or_underscore(ptr[i]): + while i < n and is_alnum_or_underscore(ptr[i]): i += 1 var id_bytes = List[UInt8](capacity=i - start) for j in range(start, i): @@ -225,15 +239,20 @@ def tokenize(expr: String) raises -> List[Token]: var name = String(unsafe_from_utf8=id_bytes^) # Check if it is a known constant - if _is_known_constant(name): + if is_known_constant(name): tokens.append(Token(TOKEN_CONST, name^, position=start)) continue # Check if it is a known function - if _is_known_function(name): + if is_known_function(name): tokens.append(Token(TOKEN_FUNC, name^, position=start)) continue + # Check if it is a known variable + if name in known_variables: + tokens.append(Token(TOKEN_VARIABLE, name^, position=start)) + continue + raise Error( "Error at position " + String(start) diff --git a/src/decimo/__init__.mojo b/src/decimo/__init__.mojo index 1bf013c8..5ecf9d1e 100644 --- a/src/decimo/__init__.mojo +++ b/src/decimo/__init__.mojo @@ -26,7 +26,7 @@ from decimo import Decimal, BInt, RoundingMode # Core types from .decimal128.decimal128 import Decimal128, Dec128 -from .bigint.bigint import BigInt, BInt +from .bigint.bigint import BigInt, BInt, Integer from .biguint.biguint import BigUInt, BUInt from .bigdecimal.bigdecimal import BigDecimal, BDec, Decimal from .bigfloat.bigfloat import BigFloat, BFlt, Float diff --git a/src/decimo/bigint/bigint.mojo b/src/decimo/bigint/bigint.mojo index 05c128ce..da0eeb3a 100644 --- a/src/decimo/bigint/bigint.mojo +++ b/src/decimo/bigint/bigint.mojo @@ -48,6 +48,8 @@ from decimo.errors import ( # Type aliases comptime BInt = BigInt """An arbitrary-precision signed integer, similar to Python's `int`.""" +comptime Integer = BigInt +"""An arbitrary-precision signed integer, similar to Python's `int`.""" struct BigInt( diff --git a/src/decimo/prelude.mojo b/src/decimo/prelude.mojo index 8a3f9505..c34b432e 100644 --- a/src/decimo/prelude.mojo +++ b/src/decimo/prelude.mojo @@ -28,7 +28,7 @@ from decimo.prelude import * import decimo as dm from decimo.decimal128.decimal128 import Decimal128, Dec128 from decimo.bigdecimal.bigdecimal import BigDecimal, BDec, Decimal -from decimo.bigint.bigint import BigInt, BInt +from decimo.bigint.bigint import BigInt, BInt, Integer from decimo.rounding_mode import ( RoundingMode, ROUND_DOWN, diff --git a/tests/cli/test_evaluator.mojo b/tests/cli/test_evaluator.mojo index 7a4927d9..c6b52fbd 100644 --- a/tests/cli/test_evaluator.mojo +++ b/tests/cli/test_evaluator.mojo @@ -1,8 +1,13 @@ """Test the evaluator: end-to-end expression evaluation with BigDecimal.""" from std import testing +from std.collections import Dict from calculator import evaluate +from calculator.tokenizer import tokenize +from calculator.parser import parse_to_rpn +from calculator.evaluator import evaluate_rpn, final_round +from decimo import Decimal from decimo.rounding_mode import RoundingMode @@ -434,6 +439,75 @@ def test_rounding_half_up_division() raises: testing.assert_equal(result, "0.6667", "2/3 half_up p=4") +# ===----------------------------------------------------------------------=== # +# Tests: variable evaluation (Phase 4) +# ===----------------------------------------------------------------------=== # + + +def _eval_with_vars( + expr: String, variables: Dict[String, Decimal], precision: Int = 50 +) raises -> Decimal: + """Helper to evaluate an expression with variables.""" + var tokens = tokenize(expr, variables) + var rpn = parse_to_rpn(tokens^) + return final_round(evaluate_rpn(rpn^, precision, variables), precision) + + +def test_variable_simple() raises: + """Simple variable reference.""" + var vars = Dict[String, Decimal]() + vars["x"] = Decimal(42) + testing.assert_equal(String(_eval_with_vars("x", vars)), "42") + + +def test_variable_in_arithmetic() raises: + """Variable used in arithmetic.""" + var vars = Dict[String, Decimal]() + vars["x"] = Decimal(10) + testing.assert_equal(String(_eval_with_vars("x + 5", vars)), "15") + + +def test_variable_ans() raises: + """The 'ans' variable works like any other variable.""" + var vars = Dict[String, Decimal]() + vars["ans"] = Decimal(100) + testing.assert_equal(String(_eval_with_vars("ans * 2", vars)), "200") + + +def test_multiple_variables() raises: + """Multiple variables in one expression.""" + var vars = Dict[String, Decimal]() + vars["x"] = Decimal(3) + vars["y"] = Decimal(4) + testing.assert_equal(String(_eval_with_vars("x + y", vars)), "7") + + +def test_variable_with_function() raises: + """Variable as argument to a function.""" + var vars = Dict[String, Decimal]() + vars["x"] = Decimal(4) + testing.assert_equal(String(_eval_with_vars("sqrt(x)", vars)), "2") + + +def test_variable_in_power() raises: + """Variable as base in exponentiation.""" + var vars = Dict[String, Decimal]() + vars["x"] = Decimal(2) + testing.assert_equal(String(_eval_with_vars("x ^ 10", vars)), "1024") + + +def test_undefined_variable_raises() raises: + """Referencing an undefined variable raises an error.""" + var vars = Dict[String, Decimal]() + vars["x"] = Decimal(1) + var raised = False + try: + _ = _eval_with_vars("y + 1", vars) + except: + raised = True + testing.assert_true(raised, "should raise on undefined variable 'y'") + + # ===----------------------------------------------------------------------=== # # Main # ===----------------------------------------------------------------------=== # diff --git a/tests/cli/test_repl.mojo b/tests/cli/test_repl.mojo new file mode 100644 index 00000000..d8d970b0 --- /dev/null +++ b/tests/cli/test_repl.mojo @@ -0,0 +1,136 @@ +"""Test REPL helpers: _parse_assignment, _validate_variable_name.""" + +from std import testing + +from calculator.repl import _parse_assignment, _validate_variable_name + + +# ===----------------------------------------------------------------------=== # +# Tests: _parse_assignment +# ===----------------------------------------------------------------------=== # + + +def test_simple_assignment() raises: + """Simple `x = 1` is recognized as assignment.""" + var result = _parse_assignment("x = 1") + testing.assert_true(Bool(result), "should parse as assignment") + testing.assert_equal(result.value()[0], "x") + testing.assert_equal(result.value()[1], "1") + + +def test_assignment_with_expression() raises: + """Assignment with a compound expression.""" + var result = _parse_assignment("total = 2 + 3 * 4") + testing.assert_true(Bool(result), "should parse as assignment") + testing.assert_equal(result.value()[0], "total") + testing.assert_equal(result.value()[1], "2 + 3 * 4") + + +def test_assignment_whitespace_around_eq() raises: + """Whitespace around `=` is handled.""" + var result = _parse_assignment("y = 42") + testing.assert_true(Bool(result), "should parse with whitespace") + testing.assert_equal(result.value()[0], "y") + testing.assert_equal(result.value()[1], "42") + + +def test_assignment_underscore_name() raises: + """Variable names with underscores are valid.""" + var result = _parse_assignment("my_var = 10") + testing.assert_true(Bool(result), "underscore name ok") + testing.assert_equal(result.value()[0], "my_var") + testing.assert_equal(result.value()[1], "10") + + +def test_double_eq_not_assignment() raises: + """`==` is not treated as assignment.""" + var result = _parse_assignment("x == 5") + testing.assert_false(Bool(result), "`==` is not assignment") + + +def test_no_eq_not_assignment() raises: + """A plain expression without `=` is not assignment.""" + var result = _parse_assignment("2 + 3") + testing.assert_false(Bool(result), "no `=` means not assignment") + + +def test_number_start_not_assignment() raises: + """Lines starting with a number are not assignments.""" + var result = _parse_assignment("3x = 5") + testing.assert_false(Bool(result), "starts with digit") + + +def test_empty_expr_not_assignment() raises: + """`x =` (no expression after `=`) returns None.""" + var result = _parse_assignment("x = ") + testing.assert_false(Bool(result), "empty expression after =") + + +def test_blank_line_not_assignment() raises: + """Blank input is not assignment.""" + var result = _parse_assignment("") + testing.assert_false(Bool(result), "blank line") + + +def test_leading_whitespace() raises: + """Leading whitespace before the name is handled.""" + var result = _parse_assignment(" x = 7") + testing.assert_true(Bool(result), "leading spaces ok") + testing.assert_equal(result.value()[0], "x") + testing.assert_equal(result.value()[1], "7") + + +# ===----------------------------------------------------------------------=== # +# Tests: _validate_variable_name +# ===----------------------------------------------------------------------=== # + + +def test_valid_name() raises: + """A normal user name is accepted.""" + var err = _validate_variable_name("total") + testing.assert_false(Bool(err), "normal name should be valid") + + +def test_reject_ans() raises: + """`ans` is rejected as a variable name.""" + var err = _validate_variable_name("ans") + testing.assert_true(Bool(err), "ans should be rejected") + + +def test_reject_function_sqrt() raises: + """Built-in function name `sqrt` is rejected.""" + var err = _validate_variable_name("sqrt") + testing.assert_true(Bool(err), "sqrt should be rejected") + + +def test_reject_function_sin() raises: + """Built-in function name `sin` is rejected.""" + var err = _validate_variable_name("sin") + testing.assert_true(Bool(err), "sin should be rejected") + + +def test_reject_constant_pi() raises: + """Built-in constant `pi` is rejected.""" + var err = _validate_variable_name("pi") + testing.assert_true(Bool(err), "pi should be rejected") + + +def test_reject_constant_e() raises: + """Built-in constant `e` is rejected.""" + var err = _validate_variable_name("e") + testing.assert_true(Bool(err), "e should be rejected") + + +def test_valid_name_with_underscore() raises: + """Names with underscores are valid.""" + var err = _validate_variable_name("my_var") + testing.assert_false(Bool(err), "underscore name is valid") + + +# ===----------------------------------------------------------------------=== # +# Main +# ===----------------------------------------------------------------------=== # + + +def main() raises: + testing.TestSuite.discover_tests[__functions_in_module()]().run() diff --git a/tests/cli/test_tokenizer.mojo b/tests/cli/test_tokenizer.mojo index 870e3d57..a567a405 100644 --- a/tests/cli/test_tokenizer.mojo +++ b/tests/cli/test_tokenizer.mojo @@ -1,6 +1,8 @@ """Test the tokenizer: lexical analysis of expression strings.""" from std import testing +from std.collections import Dict +from decimo import Decimal from calculator.tokenizer import ( Token, @@ -17,6 +19,7 @@ from calculator.tokenizer import ( TOKEN_FUNC, TOKEN_CONST, TOKEN_COMMA, + TOKEN_VARIABLE, ) @@ -329,6 +332,73 @@ def test_unknown_identifier() raises: testing.assert_true(raised, "should raise on unknown identifier 'foo'") +# ===----------------------------------------------------------------------=== # +# Tests: variable tokens (Phase 4) +# ===----------------------------------------------------------------------=== # + + +def _make_vars(*names: String) -> Dict[String, Decimal]: + """Build a Dict[String, Decimal] from variadic name arguments.""" + var result = Dict[String, Decimal]() + for i in range(len(names)): + result[names[i]] = Decimal() + return result^ + + +def test_variable_token() raises: + """Known variable names produce TOKEN_VARIABLE tokens.""" + var vars = _make_vars("x", "y") + var toks = tokenize("x + y", vars) + testing.assert_equal(len(toks), 3, "x + y token count") + assert_token(toks, 0, TOKEN_VARIABLE, "x", "variable x") + assert_token(toks, 1, TOKEN_PLUS, "+", "plus") + assert_token(toks, 2, TOKEN_VARIABLE, "y", "variable y") + + +def test_variable_ans() raises: + """The special variable 'ans' is tokenized correctly.""" + var vars = _make_vars("ans") + var toks = tokenize("ans * 2", vars) + testing.assert_equal(len(toks), 3, "ans * 2 token count") + assert_token(toks, 0, TOKEN_VARIABLE, "ans", "variable ans") + + +def test_variable_vs_function() raises: + """Functions take priority over variable names.""" + var vars = _make_vars("sqrt") + var toks = tokenize("sqrt(4)", vars) + assert_token(toks, 0, TOKEN_FUNC, "sqrt", "function, not variable") + + +def test_variable_vs_constant() raises: + """Constants take priority over variable names.""" + var vars = _make_vars("pi") + var toks = tokenize("pi", vars) + assert_token(toks, 0, TOKEN_CONST, "pi", "constant, not variable") + + +def test_unknown_without_known_variables() raises: + """Without known variables, unknown identifiers still raise.""" + var raised = False + try: + _ = tokenize("x + 1") + except: + raised = True + testing.assert_true(raised, "should raise on unknown 'x' without vars") + + +def test_variable_in_expression() raises: + """Variables work in complex expressions.""" + var vars = _make_vars("x", "ans") + var toks = tokenize("x^2 + ans", vars) + testing.assert_equal(len(toks), 5, "x^2 + ans token count") + assert_token(toks, 0, TOKEN_VARIABLE, "x", "var x") + assert_token(toks, 1, TOKEN_CARET, "^", "caret") + assert_token(toks, 2, TOKEN_NUMBER, "2", "number 2") + assert_token(toks, 3, TOKEN_PLUS, "+", "plus") + assert_token(toks, 4, TOKEN_VARIABLE, "ans", "var ans") + + # ===----------------------------------------------------------------------=== # # Main # ===----------------------------------------------------------------------=== # From 910423ed14fb30d6df587c2d9c8aad7aff6b1a3a Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Tue, 14 Apr 2026 22:33:37 +0200 Subject: [PATCH 17/17] [doc][repo] Update documents + Re-organize "docs" folder (#207) This PR updates the documents and re-organize the "docs" folder. --- README.md | 10 +- benches/cli/bench_cli.sh | 2 +- docs/api.md | 81 ---- docs/{ => internal}/internal_notes.md | 0 .../{ => internal}/v0.8.0_benchmark_report.md | 0 docs/plans/todo.md | 23 ++ docs/readme_unreleased.md | 29 +- docs/readme_zht.md | 12 +- docs/todo.md | 12 +- docs/user_manual.md | 364 +++++++++--------- .../examples_on_bigdecimal.mojo | 6 +- {docs => examples}/examples_on_bigint.mojo | 0 .../examples_on_decimal128.mojo | 0 pixi.toml | 2 +- python/README.md | 16 +- src/decimo/bigdecimal/bigdecimal.mojo | 57 ++- tests/test_bigdecimal.sh | 1 + tests/test_bigint.sh | 1 + tests/test_bigint10.sh | 1 + tests/test_biguint.sh | 1 + tests/test_decimal128.sh | 1 + tests/test_toml.sh | 1 + 22 files changed, 301 insertions(+), 319 deletions(-) delete mode 100644 docs/api.md rename docs/{ => internal}/internal_notes.md (100%) rename docs/{ => internal}/v0.8.0_benchmark_report.md (100%) create mode 100644 docs/plans/todo.md rename {docs => examples}/examples_on_bigdecimal.mojo (95%) rename {docs => examples}/examples_on_bigint.mojo (100%) rename {docs => examples}/examples_on_decimal128.mojo (100%) diff --git a/README.md b/README.md index a9cd6a2a..8e20f358 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ The core types are[^auxiliary]: | Type | Other names | Information | Internal representation | | --------- | -------------------- | ---------------------------------------- | ----------------------- | | `BInt` | `BigInt` | Equivalent to Python's `int` | Base-2^32 | -| `Decimal` | `BDec`, `BigDecimal` | Equivalent to Python's `decimal.Decimal` | Base-10^9 | +| `Decimal` | `BigDecimal`, `BDec` | Equivalent to Python's `decimal.Decimal` | Base-10^9 | | `Dec128` | `Decimal128` | 128-bit fixed-precision decimal type | Triple 32-bit words | **Decimo** combines "**Deci**mal" and "**Mo**jo" - reflecting its purpose and implementation language. "Decimo" is also a Latin word meaning "tenth" and is the root of the word "decimal". @@ -94,7 +94,7 @@ from decimo import * This will import the following types or aliases into your namespace: - `BInt` (alias of `BigInt`): An arbitrary-precision signed integer type, equivalent to Python's `int`. -- `Decimal` or `BDec` (aliases of `BigDecimal`): An arbitrary-precision decimal type, equivalent to Python's `decimal.Decimal`. +- `Decimal` (also available as `BigDecimal` or `BDec`): An arbitrary-precision decimal type, equivalent to Python's `decimal.Decimal`. - `Dec128` (alias of `Decimal128`): A 128-bit fixed-precision decimal type. - `RoundingMode`: An enumeration for rounding modes. - `ROUND_DOWN`, `ROUND_HALF_UP`, `ROUND_HALF_EVEN`, `ROUND_UP`: Constants for common rounding modes. @@ -108,10 +108,10 @@ from decimo.prelude import * fn main() raises: - var a = BDec("123456789.123456789") # BDec is an alias for BigDecimal + var a = Decimal("123456789.123456789") var b = Decimal( "1234.56789" - ) # Decimal is a Python-like alias for BigDecimal + ) # === Basic Arithmetic === # print(a + b) # 123458023.691346789 @@ -342,4 +342,4 @@ This repository and its contributions are licensed under the Apache License v2.0 [^bigint]: The `BigInt` implementation uses a base-2^32 representation with a little-endian format, where the least significant word is stored at index 0. Each word is a `UInt32`, allowing for efficient storage and arithmetic operations on large integers. This design choice optimizes performance for binary computations while still supporting arbitrary precision. [^auxiliary]: The auxiliary types include a base-10 arbitrary-precision signed integer type (`BigInt10`) and a base-10 arbitrary-precision unsigned integer type (`BigUInt`) supporting unlimited digits[^bigint10]. `BigUInt` is used as the internal representation for `BigInt10` and `Decimal`. [^bigint10]: The BigInt10 implementation uses a base-10 representation for users (maintaining decimal semantics), while internally using an optimized base-10^9 storage system for efficient calculations. This approach balances human-readable decimal operations with high-performance computing. It provides both floor division (round toward negative infinity) and truncate division (round toward zero) semantics, enabling precise handling of division operations with correct mathematical behavior regardless of operand signs. -[^arbitrary]: Built on top of our completed BigInt10 implementation, BigDecimal will support arbitrary precision for both the integer and fractional parts, similar to `decimal` and `mpmath` in Python, `java.math.BigDecimal` in Java, etc. +[^arbitrary]: Built on top of our completed BigInt10 implementation, Decimal supports arbitrary precision for both the integer and fractional parts, similar to `decimal` and `mpmath` in Python, `java.math.BigDecimal` in Java, etc. diff --git a/benches/cli/bench_cli.sh b/benches/cli/bench_cli.sh index 4b6aeb10..506580ac 100644 --- a/benches/cli/bench_cli.sh +++ b/benches/cli/bench_cli.sh @@ -330,7 +330,7 @@ bench_compare "exp(1)" 50 \ "${PY_MP};print(mp.exp(1))" # NOTE: mpmath diverges from decimo & WolframAlpha at digit ~21 for sin(near-pi). -# See docs/internal_notes.md. Kept here as a reference comparison. +# See docs/internal/internal_notes.md. Kept here as a reference comparison. bench_compare "sin(3.1415926535897932384626433833)" 50 \ "sin(3.1415926535897932384626433833)" \ "s(3.1415926535897932384626433833)" \ diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 799e644d..00000000 --- a/docs/api.md +++ /dev/null @@ -1,81 +0,0 @@ -# API Reference - -## Initialization of BigUInt - -There are three issues associated with the initialization of `BigUInt`: - -1. The embedded list of words is **empty**. In this sense, the coefficient of the `BigUInt` is **uninitialized**. This situation, in most cases, is not desirable because it can lead to bugs and unexpected behavior if users try to perform arithmetic operations on an uninitialized `BigUInt`. However, in some cases, users may want to create an uninitialized `BigUInt` for performance reasons, e.g., when they want to fill the words with their own values later. Therefore, we can allow users to create an uninitialized `BigUInt` by providing a key-word only argument, e.g., `uninitialized=True`. -1. There are **leading zero words** in the embedded list of words, e.g., 000000000_123456789. This situation is not a safety issue, but it can lead to performance issues because it increases the number of words that need to be processed during arithmetic operations. In some cases, users may want to keep these leading zero words for specific applications, e.g., aligning the number of words for two `BigUInt` operations. -1. The value of a word is greater than **999_999_999**. This situation is a safety issue because it violates the invariant that each word should be smaller than a billion. It can lead to bugs and unexpected behavior if users try to perform arithmetic operations on a `BigUInt` with invalid words. - -To make sure that the users construct `BigUInt` safely by default, the default constructor of `BigUInt` will check for these issues so that the `BigUInt` is always non-empty, has no leading zero words, and all words are smaller than a billion. We also allow users, mainly developers, to create unsafe `BigUInt` instances if they want to, but they must explicitly choose to do so by providing a key-word only argument, e.g., `uninitialized=True`, or use specific methods, e.g., `from_list_unsafe()`. - -Note: Mojo now supports keyword-only arguments of the same data type. - -| Method | non-empty | no leading zero words | all words valid | notes | -| ---------------------------------------- | --------- | --------------------- | --------------- | -------------------------------------- | -| `BigUInt(var words: List[UInt32])` | ✓ | ✓ | ✓ | Validating constructor for word lists. | -| `BigUInt(*, uninitialized_capacity=Int)` | ✗ | ? | ? | Length of words list is 0 | -| `BigUInt(*, unsafe_uninit_length=Int)` | ✗ | ? | ? | Length of words list not 0 | -| `BigUInt(*, raw_words: List[UInt32])` | ✓ | ✗ | ✗ | | -| `BigUInt(value: Int)` | ✓ | ✓ | ✓ | | -| `BigUInt(value: Scalar)` | ✓ | ✓ | ✓ | Only unsigned scalars are supported. | - -## Initialization of BigDecimal - -### Python Interoperability: `from_python_decimal()` - -Method Signature is - -```mojo -@staticmethod -fn from_python_decimal(value: PythonObject) raises -> BigDecimal -``` - ---- - -Why use `as_tuple()` instead of direct memory copy (memcpy)? - -Python's `decimal` module (libmpdec) internally uses a base-10^9 representation on 64-bit systems (base 10^4 on 32-bit), which happens to match BigDecimal's internal representation. This raises the question: why not directly memcpy the internal limbs for better performance? - -Direct memcpy is theoretically possible because: - -- On 64-bit systems: libmpdec uses base 10^9, same as BigDecimal -- Both use `uint32_t` limbs for storage -- Direct memory mapping would avoid digit decomposition overhead - -However, this approach is **NOT** used due to significant practical issues: - -1. No mature API for direct access. -1. Using direct memory access would require unsafe pointer manipulation, breaking Decimo's current design principles of using safe Mojo as much as possible. -1. Platform dependency. 32-bit systems use base 10^4 (incompatible with BigDecimal's 10^9). This would require runtime platform detection. -1. Maintenance burden. CPython internal structure (`mpd_t`) may change between versions. -1. Marginal performance gain. `as_tuple()` overhead: O(n) where n = number of digits. Direct memcpy: O(m) where m = number of limbs. Theoretical speedup: ~10x. But how often are users really converting Python decimals to BigDecimal? - ---- - -The `as_tuple()` API returns a tuple of `(sign, digits, exponent)`: - -- `sign`: 0 for positive, 1 for negative -- `digits`: Tuple of individual decimal digits (0-9) -- `exponent`: Power of 10 to multiply by - -`as_tuple()` performs limb → digits decomposition internally. The digits returned are individual base-10 digits, not the base-10^9 limbs stored internally. - -Example: - -```python -# Python -from decimal import Decimal -d = Decimal("123456789012345678") -print(d.as_tuple()) -# DecimalTuple(sign=0, digits=(1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8), exponent=0) -``` - -The `as_tuple()` approach provides: - -- Safe: No unsafe pointer manipulation -- Stable: Public API guaranteed across Python versions -- Portable: Works on all platforms (32/64-bit, CPython/PyPy/etc.) -- Clean: Maintainable, readable code -- Adequate performance: O(n) is acceptable for typical use cases diff --git a/docs/internal_notes.md b/docs/internal/internal_notes.md similarity index 100% rename from docs/internal_notes.md rename to docs/internal/internal_notes.md diff --git a/docs/v0.8.0_benchmark_report.md b/docs/internal/v0.8.0_benchmark_report.md similarity index 100% rename from docs/v0.8.0_benchmark_report.md rename to docs/internal/v0.8.0_benchmark_report.md diff --git a/docs/plans/todo.md b/docs/plans/todo.md new file mode 100644 index 00000000..96ce7c3a --- /dev/null +++ b/docs/plans/todo.md @@ -0,0 +1,23 @@ +# TODO + +This is a to-do list for Decimo. + +- [ ] When Mojo supports **global variables**, implement a type `Context` and a global variable `context` for the `Decimal` class to store the precision of the decimal number and other configurations. This will allow users to set the precision globally, rather than having to set it for each function of the `Decimal` class. +- [ ] When Mojo supports **enum types**, implement an enum type for the rounding mode. +- [ ] Implement a complex number class `BigComplex` that uses `Decimal` for the real and imaginary parts. This will allow users to perform high-precision complex number arithmetic. +- [ ] Implement different methods for adding decimo types with `Int` types so that an implicit conversion is not required. +- [ ] Use debug mode to check for unnecessary zero words before all arithmetic operations. This will help ensure that there are no zero words, which can simplify the speed of checking for zero because we only need to check the first word. +- [ ] Check the `floor_divide()` function of `BigUInt`. Currently, the speed of division between imilar-sized numbers are okay, but the speed of 2n-by-n, 4n-by-n, and 8n-by-n divisions decreases disproportionally. This is likely due to the segmentation of the dividend in the Burnikel-Ziegler algorithm. +- [x] Consider using `Decimal` as the struct name instead of `BigDecimal`, and use `comptime BigDecimal = Decimal` to create an alias for the `Decimal` struct. This just switches the alias and the struct name, but it may be more intuitive to use `Decimal` as the struct name since it is more consistent with Python's `decimal.Decimal`. Moreover, hovering over `Decimal` will show the docstring of the struct, which is more intuitive than hovering over `BigDecimal` to see the docstring of the struct. +- [x] (PR #127, #128, #131) Make all default constructor "safe", which means that the words are checked and normalized to ensure that there are no zero words and that the number is in a valid state. This will help prevent bugs and ensure that all `BigUInt` instances are in a consistent state. Also allow users to create "unsafe" `BigUInt` instances if they want to, but there must be a key-word only argument, e.g., `raw_words`. + +- [x] (#31) The `exp()` function performs slower than Python's counterpart in specific cases. Detailed investigation reveals the bottleneck stems from multiplication operations between decimals with significant fractional components. These operations currently rely on UInt256 arithmetic, which introduces performance overhead. Optimization of the `multiply()` function is required to address these performance bottlenecks, particularly for high-precision decimal multiplication with many digits after the decimal point. Internally, also use `Decimal` instead of `BigDecimal` or `BDec` to be consistent. +- [x] Implement different methods for augmented arithmetic assignments to improve memeory-efficiency and performance. +- [x] Implement a method `remove_leading_zeros` for `BigUInt`, which removes the zero words from the most significant end of the number. +- [x] Use debug mode to check for uninitialized `BigUInt` before all arithmetic operations. This will help ensure that there are no uninitialized `BigUInt`. + +## Roadmap for Decimo + +- [x] Re-implement some methods of `BigUInt` to improve the performance, since it is the building block of `BigDecimal` and `BigInt10`. +- [x] Refine the methods of `BigDecimal` to improve the performance. +- [x] Implement the big **binary** integer type (`BigInt`) using base-2^32 internal representation. The new `BigInt` (alias `BInt`) replaces the previous base-10^9 implementation (now `BigInt10`) and delivers significantly improved performance. \ No newline at end of file diff --git a/docs/readme_unreleased.md b/docs/readme_unreleased.md index 7ab047dc..588d600c 100644 --- a/docs/readme_unreleased.md +++ b/docs/readme_unreleased.md @@ -37,10 +37,10 @@ The core types are[^auxiliary]: - A 128-bit fixed-point decimal implementation (`Dec128`) supporting up to 29 significant digits with a maximum of 28 decimal places[^fixed]. - An arbitrary-precision floating-point implementation (`Float`) backed by the GNU MPFR library, supporting computations with configurable precision and a wide exponent range. Unlike `Decimal`, which uses base-10 arithmetic, `Float` uses binary floating-point internally. This type is optional and requires MPFR/GMP to be installed on the user's system. -| Type | Other names | Information | Internal representation | +| Type | Alternative names | Information | Internal representation | | --------- | -------------------- | ---------------------------------------- | ----------------------- | -| `BInt` | `BigInt` | Equivalent to Python's `int` | Base-2^32 | -| `Decimal` | `BDec`, `BigDecimal` | Equivalent to Python's `decimal.Decimal` | Base-10^9 | +| `BInt` | `BigInt`, `Integer` | Equivalent to Python's `int` | Base-2^32 | +| `Decimal` | `BigDecimal`, `BDec` | Equivalent to Python's `decimal.Decimal` | Base-10^9 | | `Dec128` | `Decimal128` | 128-bit fixed-precision decimal type | Triple 32-bit words | | `Float` | `BigFloat` | Arbitrary-precision floating-point type | MPFR/GMP | @@ -65,7 +65,7 @@ Then, you can install Decimo using any of these methods: 1. In the `mojoproject.toml` file of your project, add the following dependency: ```toml - decimo = "==0.9.0" + decimo = "==0.10.0" ``` Then run `pixi install` to download and install the package. @@ -86,6 +86,7 @@ The following table summarizes the package versions and their corresponding Mojo | `decimojo` | v0.7.0 | ==0.26.1 | pixi | | `decimo` | v0.8.0 | ==0.26.1 | pixi | | `decimo` | v0.9.0 | ==0.26.2 | pixi | +| `decimo` | v0.10.0 | ==0.26.2 | pixi | ## Quick start @@ -97,9 +98,9 @@ from decimo import * This will import the following types or aliases into your namespace: -- `BInt` (alias of `BigInt`): An arbitrary-precision signed integer type, equivalent to Python's `int`. -- `Decimal` or `BDec` (aliases of `BigDecimal`): An arbitrary-precision decimal type, equivalent to Python's `decimal.Decimal`. -- `Dec128` (alias of `Decimal128`): A 128-bit fixed-precision decimal type. +- `BInt` (and its aliases `BigInt`, `Integer`): An arbitrary-precision signed integer type, equivalent to Python's `int`. +- `Decimal` (and its aliases `BigDecimal`, `BDec`): An arbitrary-precision decimal type, equivalent to Python's `decimal.Decimal`. +- `Dec128` (and its alias `Decimal128`): A 128-bit fixed-precision decimal type. - `RoundingMode`: An enumeration for rounding modes. - `ROUND_DOWN`, `ROUND_HALF_UP`, `ROUND_HALF_EVEN`, `ROUND_UP`: Constants for common rounding modes. @@ -112,10 +113,8 @@ from decimo.prelude import * fn main() raises: - var a = BDec("123456789.123456789") # BDec is an alias for BigDecimal - var b = Decimal( - "1234.56789" - ) # Decimal is a Python-like alias for BigDecimal + var a = Decimal("123456789.123456789") + var b = Decimal("1234.56789") # === Basic Arithmetic === # print(a + b) # 123458023.691346789 @@ -322,7 +321,7 @@ Bug reports and feature requests are welcome! If you encounter issues, please [f decimo/ ├── src/ # All source code │ ├── decimo/ # Core library (mojo package) -│ │ ├── bigdecimal/ # Arbitrary-precision decimal (Decimal/BDec) +│ │ ├── bigdecimal/ # Arbitrary-precision decimal (Decimal) │ │ ├── bigint/ # Arbitrary-precision signed integer (BInt) │ │ ├── bigint10/ # Base-10 signed integer (BigInt10) │ │ ├── biguint/ # Base-10 unsigned integer (BigUInt) @@ -333,7 +332,7 @@ decimo/ │ └── calculator/ # Calculator engine (mojo package) │ ├── tokenizer.mojo # Lexer: expression → tokens │ ├── parser.mojo # Shunting-yard: infix → RPN -│ └── evaluator.mojo # RPN evaluator using BigDecimal +│ └── evaluator.mojo # RPN evaluator using Decimal ├── tests/ # Unit tests (one subfolder per module) │ ├── bigdecimal/ │ ├── bigint/ @@ -367,7 +366,7 @@ If you find Decimo useful, consider listing it in your citations. year = {2026}, title = {Decimo: An arbitrary-precision integer and decimal library for Mojo}, url = {https://github.com/forfudan/decimo}, - version = {0.9.0}, + version = {0.10.0}, note = {Computer Software} } ``` @@ -382,4 +381,4 @@ The `BigFloat` type optionally uses the [GNU MPFR Library](https://www.mpfr.org/ [^bigint]: The `BigInt` implementation uses a base-2^32 representation with a little-endian format, where the least significant word is stored at index 0. Each word is a `UInt32`, allowing for efficient storage and arithmetic operations on large integers. This design choice optimizes performance for binary computations while still supporting arbitrary precision. [^auxiliary]: The auxiliary types include a base-10 arbitrary-precision signed integer type (`BigInt10`) and a base-10 arbitrary-precision unsigned integer type (`BigUInt`) supporting unlimited digits[^bigint10]. `BigUInt` is used as the internal representation for `BigInt10` and `Decimal`. [^bigint10]: The BigInt10 implementation uses a base-10 representation for users (maintaining decimal semantics), while internally using an optimized base-10^9 storage system for efficient calculations. This approach balances human-readable decimal operations with high-performance computing. It provides both floor division (round toward negative infinity) and truncate division (round toward zero) semantics, enabling precise handling of division operations with correct mathematical behavior regardless of operand signs. -[^arbitrary]: Built on top of our completed BigInt10 implementation, BigDecimal will support arbitrary precision for both the integer and fractional parts, similar to `decimal` and `mpmath` in Python, `java.math.BigDecimal` in Java, etc. +[^arbitrary]: Built on top of our completed BigInt10 implementation, Decimal supports arbitrary precision for both the integer and fractional parts, similar to `decimal` and `mpmath` in Python, `java.math.BigDecimal` in Java, etc. diff --git a/docs/readme_zht.md b/docs/readme_zht.md index 06639e9c..4b8a77ea 100644 --- a/docs/readme_zht.md +++ b/docs/readme_zht.md @@ -28,7 +28,7 @@ Decimo 爲 Mojo 提供任意精度整數和小數運算庫,爲金融建模、 | 類型 | 別名 | 信息 | 內部表示 | | --------- | -------------------- | ---------------------------------- | ------------ | | `BInt` | `BigInt` | 等價於 Python 的 `int` | Base-2^32 | -| `Decimal` | `BDec`, `BigDecimal` | 等價於 Python 的 `decimal.Decimal` | Base-10^9 | +| `Decimal` | `BigDecimal`, `BDec` | 等價於 Python 的 `decimal.Decimal` | Base-10^9 | | `Dec128` | `Decimal128` | 128 位定點精度小數類型 | 三個 32 位字 | 輔助類型包括基於 10 進制的任意精度有符號整數類型 (`BigInt10`) 和任意精度無符號整數類型 (`BigUInt`),支持無限位數[^bigint10]。`BigUInt` 是 `BigInt10` 和 `Decimal` 的內部表示。 @@ -92,24 +92,24 @@ from decimo import * 這將導入以下類型或別名到您的命名空間: - `BInt`(`BigInt` 的別名):任意精度有符號整數類型,等價於 Python 的 `int`。 -- `Decimal` 或 `BDec`(`BigDecimal` 的別名):任意精度小數類型,等價於 Python 的 `decimal.Decimal`。 +- `Decimal`(也可用 `BigDecimal` 或 `BDec`):任意精度小數類型,等價於 Python 的 `decimal.Decimal`。 - `Dec128`(`Decimal128` 的別名):128 位定點精度小數類型。 - `RoundingMode`:捨入模式的枚舉。 - `ROUND_DOWN`、`ROUND_HALF_UP`、`ROUND_HALF_EVEN`、`ROUND_UP`:常用捨入模式的常量。 --- -以下是一些展示 `BigDecimal` 類型(別名:`BDec` 和 `Decimal`)任意精度特性的例子。對於某些數學運算,默認精度(有效數字位數)設為 `28`。您可以通過向函數傳遞 `precision` 參數來更改精度。當 Mojo 支持全局變量時,此默認精度將可以全局配置。 +以下是一些展示 `Decimal` 類型任意精度特性的例子。對於某些數學運算,默認精度(有效數字位數)設為 `28`。您可以通過向函數傳遞 `precision` 參數來更改精度。當 Mojo 支持全局變量時,此默認精度將可以全局配置。 ```mojo from decimo.prelude import * fn main() raises: - var a = BDec("123456789.123456789") # BDec 是 BigDecimal 的別名 + var a = Decimal("123456789.123456789") var b = Decimal( "1234.56789" - ) # Decimal 是類似 Python 的 BigDecimal 別名 + ) # === 基本算術 === # print(a + b) # 123458023.691346789 @@ -339,4 +339,4 @@ fn main() raises: [^fixed]: `Decimal128` 類型可以表示最多 29 位有效數字,小數點後最多 28 位數字的值。當數值超過最大可表示值(`2^96 - 1`)時,Decimo 會拋出錯誤或將數值捨入以符合這些約束。例如,`8.8888888888888888888888888888`(總共 29 個 8,小數點後 28 位)的有效數字超過了最大可表示值(`2^96 - 1`),會自動捨入爲 `8.888888888888888888888888889`(總共 28 個 8,小數點後 27 位)。Decimo 的 `Decimal128` 類型類似於 `System.Decimal`(C#/.NET)、Rust 中的 `rust_decimal`、SQL Server 中的 `DECIMAL/NUMERIC` 等。 [^bigint]: `BigInt` 使用 base-2^32 表示,採用小端格式,最低有效字存儲在索引 0。每個字是一個 `UInt32`,允許對大整數進行高效存儲和算術運算。這種設計優化了二進制計算的性能,同時支持任意精度。 [^bigint10]: BigInt10 使用基於 10 的表示(保持十進制語義),而內部使用優化的基於 10^9 的存儲系統進行高效計算。這種方法在人類可讀的十進制操作與高性能計算之間取得平衡。它提供向下整除(向負無窮舍入)和截斷除法(向零舍入)語義,無論操作數符號如何,都能確保除法操作具有正確的數學行爲。 -[^arbitrary]: 建立在已完成的 BigInt10 實現之上,BigDecimal 支持整數和小數部分的任意精度,類似於 Python 中的 `decimal` 和 `mpmath`、Java 中的 `java.math.BigDecimal` 等。 +[^arbitrary]: 建立在已完成的 BigInt10 實現之上,Decimal 支持整數和小數部分的任意精度,類似於 Python 中的 `decimal` 和 `mpmath`、Java 中的 `java.math.BigDecimal` 等。 diff --git a/docs/todo.md b/docs/todo.md index b9da8ae2..f7501ded 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -2,17 +2,17 @@ This is a to-do list for Decimo. -- [ ] When Mojo supports **global variables**, implement a type `Context` and a global variable `context` for the `BigDecimal` class to store the precision of the decimal number and other configurations. This will allow users to set the precision globally, rather than having to set it for each function of the `BigDecimal` class. +- [ ] When Mojo supports **global variables**, implement a type `Context` and a global variable `context` for the `Decimal` class to store the precision of the decimal number and other configurations. This will allow users to set the precision globally, rather than having to set it for each function of the `Decimal` class. - [ ] When Mojo supports **enum types**, implement an enum type for the rounding mode. -- [ ] Implement a complex number class `BigComplex` that uses `BigDecimal` for the real and imaginary parts. This will allow users to perform high-precision complex number arithmetic. +- [ ] Implement a complex number class `BigComplex` that uses `Decimal` for the real and imaginary parts. This will allow users to perform high-precision complex number arithmetic. - [ ] Implement different methods for adding decimo types with `Int` types so that an implicit conversion is not required. - [ ] Use debug mode to check for unnecessary zero words before all arithmetic operations. This will help ensure that there are no zero words, which can simplify the speed of checking for zero because we only need to check the first word. -- [ ] Check the `floor_divide()` function of `BigUInt`. Currently, the speed of division between imilar-sized numbers are okay, but the speed of 2n-by-n, 4n-by-n, and 8n-by-n divisions decreases unproportionally. This is likely due to the segmentation of the dividend in the Burnikel-Ziegler algorithm. -- [ ] Consider using `Decimal` as the struct name instead of `BigDecimal`, and use `comptime BigDecimal = Decimal` to create an alias for the `Decimal` struct. This just switches the alias and the struct name, but it may be more intuitive to use `Decimal` as the struct name since it is more consistent with Python's `decimal.Decimal`. Moreover, hovering over `Decimal` will show the docstring of the struct, which is more intuitive than hovering over `BigDecimal` to see the docstring of the struct. +- [ ] Check the `floor_divide()` function of `BigUInt`. Currently, the speed of division between similar-sized numbers are okay, but the speed of 2n-by-n, 4n-by-n, and 8n-by-n divisions decreases disproportionally. This is likely due to the segmentation of the dividend in the Burnikel-Ziegler algorithm. +- [x] Consider using `Decimal` as the struct name instead of `BigDecimal`, and use `comptime BigDecimal = Decimal` to create an alias for the `Decimal` struct. This just switches the alias and the struct name, but it may be more intuitive to use `Decimal` as the struct name since it is more consistent with Python's `decimal.Decimal`. Moreover, hovering over `Decimal` will show the docstring of the struct, which is more intuitive than hovering over `BigDecimal` to see the docstring of the struct. - [x] (PR #127, #128, #131) Make all default constructor "safe", which means that the words are checked and normalized to ensure that there are no zero words and that the number is in a valid state. This will help prevent bugs and ensure that all `BigUInt` instances are in a consistent state. Also allow users to create "unsafe" `BigUInt` instances if they want to, but there must be a key-word only argument, e.g., `raw_words`. - [x] (#31) The `exp()` function performs slower than Python's counterpart in specific cases. Detailed investigation reveals the bottleneck stems from multiplication operations between decimals with significant fractional components. These operations currently rely on UInt256 arithmetic, which introduces performance overhead. Optimization of the `multiply()` function is required to address these performance bottlenecks, particularly for high-precision decimal multiplication with many digits after the decimal point. Internally, also use `Decimal` instead of `BigDecimal` or `BDec` to be consistent. -- [x] Implement different methods for augmented arithmetic assignments to improve memeory-efficiency and performance. +- [x] Implement different methods for augmented arithmetic assignments to improve memory-efficiency and performance. - [x] Implement a method `remove_leading_zeros` for `BigUInt`, which removes the zero words from the most significant end of the number. - [x] Use debug mode to check for uninitialized `BigUInt` before all arithmetic operations. This will help ensure that there are no uninitialized `BigUInt`. @@ -20,4 +20,4 @@ This is a to-do list for Decimo. - [x] Re-implement some methods of `BigUInt` to improve the performance, since it is the building block of `BigDecimal` and `BigInt10`. - [x] Refine the methods of `BigDecimal` to improve the performance. -- [x] Implement the big **binary** integer type (`BigInt`) using base-2^32 internal representation. The new `BigInt` (alias `BInt`) replaces the previous base-10^9 implementation (now `BigInt10`) and delivers significantly improved performance. \ No newline at end of file +- [x] Implement the big **binary** integer type (`BigInt`) using base-2^32 internal representation. The new `BigInt` (alias `BInt`) replaces the previous base-10^9 implementation (now `BigInt10`) and delivers significantly improved performance. diff --git a/docs/user_manual.md b/docs/user_manual.md index 9ddc92cf..98c73714 100644 --- a/docs/user_manual.md +++ b/docs/user_manual.md @@ -9,93 +9,40 @@ your Mojo file: from decimo.prelude import * ``` +- [Installation](#installation) +- [Quick Start](#quick-start) - [Part I — BigInt (`BInt`)](#part-i--bigint-bint) - [Overview](#overview) - [Construction](#construction) - - [From zero](#from-zero) - - [From `Int`](#from-int) - - [From `String`](#from-string) - - [From `Scalar` (any integral SIMD type)](#from-scalar-any-integral-simd-type) - - [Summary of constructors](#summary-of-constructors) - [Arithmetic Operations](#arithmetic-operations) - - [Binary operators](#binary-operators) - - [Unary operators](#unary-operators) - - [In-place operators](#in-place-operators) - [Division Semantics](#division-semantics) - [Comparison](#comparison) - [Bitwise Operations](#bitwise-operations) - [Shift Operations](#shift-operations) - [Mathematical Functions](#mathematical-functions) - - [Exponentiation](#exponentiation) - - [Integer square root](#integer-square-root) - [Number Theory](#number-theory) - - [GCD — Greatest Common Divisor](#gcd--greatest-common-divisor) - - [LCM — Least Common Multiple](#lcm--least-common-multiple) - - [Extended GCD](#extended-gcd) - - [Modular Exponentiation](#modular-exponentiation) - - [Modular Inverse](#modular-inverse) - [Conversion and Output](#conversion-and-output) - - [String conversions](#string-conversions) - - [Numeric conversions](#numeric-conversions) - [Query Methods](#query-methods) - [Constants and Factory Methods](#constants-and-factory-methods) -- [Part II — BigDecimal (`Decimal`)](#part-ii--bigdecimal-decimal) - - [Overview](#overview-1) +- [Part II — Decimal](#part-ii--decimal) + - [Overview — Decimal](#overview--decimal) - [How Precision Works](#how-precision-works) - - [Construction](#construction-1) - - [From zero](#from-zero-1) - - [From `Int`](#from-int-1) - - [From `String`](#from-string-1) - - [From integral scalars](#from-integral-scalars) - - [From floating-point — `from_float()`](#from-floating-point--from_float) - - [From Python — `from_python_decimal()`](#from-python--from_python_decimal) - - [Summary of constructors](#summary-of-constructors-1) - - [Arithmetic Operations](#arithmetic-operations-1) + - [Construction — Decimal](#construction--decimal) + - [Decimal Arithmetic](#decimal-arithmetic) - [Division Methods](#division-methods) - - [`true_divide()` — recommended for decimal division](#true_divide--recommended-for-decimal-division) - - [Operator `/` — true division with default precision](#operator---true-division-with-default-precision) - - [Operator `//` — truncated (integer) division](#operator---truncated-integer-division) - - [Comparison](#comparison-1) + - [Decimal Comparison](#decimal-comparison) - [Rounding and Formatting](#rounding-and-formatting) - - [`round()` — round to decimal places](#round--round-to-decimal-places) - - [`quantize()` — match scale of another decimal](#quantize--match-scale-of-another-decimal) - - [`normalize()` — remove trailing zeros](#normalize--remove-trailing-zeros) - - [`__ceil__`, `__floor__`, `__trunc__`](#__ceil__-__floor__-__trunc__) - [RoundingMode](#roundingmode) - [Mathematical Functions — Roots and Powers](#mathematical-functions--roots-and-powers) - - [Square root](#square-root) - - [Cube root](#cube-root) - - [Nth root](#nth-root) - - [Power / exponentiation](#power--exponentiation) - [Mathematical Functions — Exponential and Logarithmic](#mathematical-functions--exponential-and-logarithmic) - - [Exponential (e^x)](#exponential-ex) - - [Natural logarithm](#natural-logarithm) - - [Logarithm with arbitrary base](#logarithm-with-arbitrary-base) - - [Base-10 logarithm](#base-10-logarithm) - [Mathematical Functions — Trigonometric](#mathematical-functions--trigonometric) - - [Basic functions](#basic-functions) - - [Reciprocal functions](#reciprocal-functions) - - [Inverse functions](#inverse-functions) - [Mathematical Constants](#mathematical-constants) - - [π (pi)](#π-pi) - - [e (Euler's number)](#e-eulers-number) - - [Conversion and Output](#conversion-and-output-1) - - [String output](#string-output) - - [`repr()`](#repr) - - [Numeric conversions](#numeric-conversions-1) - - [Query Methods](#query-methods-1) - - [`as_tuple()` — Python-compatible decomposition](#as_tuple--python-compatible-decomposition) - - [Other methods](#other-methods) + - [Decimal Conversion and Output](#decimal-conversion-and-output) + - [Decimal Query Methods](#decimal-query-methods) - [Python Interoperability](#python-interoperability) - - [From Python](#from-python) - - [Matching Python's API](#matching-pythons-api) - [Appendix A — Import Paths](#appendix-a--import-paths) - [Appendix B — Traits Implemented](#appendix-b--traits-implemented) - - [BigInt](#bigint) - - [BigDecimal](#bigdecimal) - [Appendix C — Complete API Tables](#appendix-c--complete-api-tables) - - [BigInt — All Operators](#bigint--all-operators) - - [BigDecimal — Mathematical Functions](#bigdecimal--mathematical-functions) ## Installation @@ -114,7 +61,7 @@ pixi add decimo Or add it manually to `pixi.toml`: ```toml -decimo = "==0.9.0" +decimo = "==0.10.0" ``` Then run `pixi install`. @@ -141,29 +88,29 @@ fn main() raises: print(Decimal.pi(precision=1000)) # 1000 digits of π ``` -# Part I — BigInt (`BInt`) +## Part I — BigInt (`BInt`) -## Overview +### Overview -`BigInt` (alias `BInt`) is an arbitrary-precision signed integer type — the Mojo-native equivalent of Python's `int`. It supports unlimited-precision integer arithmetic, bitwise operations, and number-theoretic functions. +`BigInt` (aliases `BInt`, `Integer`) is an arbitrary-precision signed integer type — the Mojo-native equivalent of Python's `int`. It supports unlimited-precision integer arithmetic, bitwise operations, and number-theoretic functions. | Property | Value | | ----------------- | ---------------------------- | | Full name | `BigInt` | -| Short alias | `BInt` | +| Aliases | `BInt`, `Integer` | | Internal base | 2^32 (binary representation) | | Word type | `UInt32` (little-endian) | | Python equivalent | `int` | -## Construction +### Construction -### From zero +#### From zero ```mojo var x = BInt() # 0 ``` -### From `Int` +#### From `Int` ```mojo var x = BInt(42) @@ -173,7 +120,7 @@ var z: BInt = 42 # Implicit conversion from Int The constructor is marked `@implicit`, so Mojo can automatically convert `Int` to `BInt` where expected. -### From `String` +#### From `String` ```mojo var a = BInt("12345678901234567890") # Basic decimal string @@ -186,7 +133,7 @@ var f = BInt("1991_10,18") # Mixed separators (= 19911018) > **Note:** The string must represent an integer. `BInt("1.5")` raises an error. Scientific notation like `"1.23e5"` is accepted only if the result is an integer. -### From `Scalar` (any integral SIMD type) +#### From `Scalar` (any integral SIMD type) ```mojo var x = BInt(UInt32(42)) @@ -196,22 +143,31 @@ var z: BInt = UInt32(99) # Implicit conversion Accepts any integral scalar type (`Int8` through `Int256`, `UInt8` through `UInt256`, etc.). -### Summary of constructors +#### Summary of constructors -| Constructor | Description | -| --------------------------- | ------------------------------------- | -| `BInt()` | Zero | -| `BInt(value: Int)` | From `Int` (implicit) | -| `BInt(value: String)` | From decimal string (raises) | -| `BInt(value: Scalar)` | From integral scalar (implicit) | -| `BInt.from_uint64(value)` | From `UInt64` | -| `BInt.from_uint128(value)` | From `UInt128` | -| `BInt.from_string(value)` | Explicit factory from string (raises) | -| `BInt.from_bigint10(value)` | Convert from `BigInt10` | +| Constructor | Description | +| ---------------------------------- | ------------------------------------- | +| `BInt()` | Zero | +| `BInt(value: Int)` | From `Int` (implicit) | +| `BInt(value: String)` | From decimal string (raises) | +| `BInt(value: Scalar)` | From integral scalar (implicit) | +| `BInt.from_int(value)` | Explicit factory from `Int` | +| `BInt.from_integral_scalar(value)` | From any integral scalar type | +| `BInt.from_string(value)` | Explicit factory from string (raises) | +| `BInt.from_bigint10(value)` | Convert from `BigInt10` | -## Arithmetic Operations +#### Unsafe constructors -### Binary operators +These constructors skip validation for performance-sensitive code. The caller must ensure the data is valid. + +| Constructor | Description | +| ----------------------------------------- | ----------------------------------------- | +| `BInt(uninitialized_capacity=n)` | Empty words list with reserved capacity | +| `BInt(raw_words=List[UInt32], sign=Bool)` | From raw words, no leading-zero stripping | + +### Arithmetic Operations + +#### Binary operators | Expression | Description | Raises? | | ------------- | --------------------------------- | ------------------ | @@ -223,7 +179,7 @@ Accepts any integral scalar type (`Int8` through `Int256`, `UInt8` through `UInt | `divmod(a,b)` | Floor quotient and remainder | Yes (zero div) | | `a ** b` | Exponentiation | Yes (negative exp) | -### Unary operators +#### Unary operators | Expression | Description | | ---------- | ------------------------------ | @@ -233,7 +189,7 @@ Accepts any integral scalar type (`Int8` through `Int256`, `UInt8` through `UInt | `bool(a)` | `True` if nonzero | | `~a` | Bitwise NOT (two's complement) | -### In-place operators +#### In-place operators `+=`, `-=`, `*=`, `//=`, `%=`, `<<=`, `>>=` are all supported and perform true in-place mutation to reduce memory allocation. @@ -248,7 +204,7 @@ print(a % b) # 9615 print(BInt(2) ** 10) # 1024 ``` -## Division Semantics +### Division Semantics BigInt supports two division conventions: @@ -272,7 +228,7 @@ print(a.truncate_divide(b)) # -3 print(a.truncate_modulo(b)) # 1 ``` -## Comparison +### Comparison All six comparison operators (`==`, `!=`, `>`, `>=`, `<`, `<=`) are supported. Each accepts both `BInt` and `Int` as the right operand. @@ -290,7 +246,7 @@ a.compare(b) # Returns Int8: 1, 0, or -1 a.compare_magnitudes(b) # Compares |a| vs |b| ``` -## Bitwise Operations +### Bitwise Operations All bitwise operations follow **Python's two's complement semantics** for negative numbers. @@ -315,7 +271,7 @@ print(~a) # -13 print(BInt(-1) & BInt(255)) # 255 ``` -## Shift Operations +### Shift Operations | Operator | Description | | -------- | ----------------------------------- | @@ -328,9 +284,9 @@ print(x << 100) # 1267650600228229401496703205376 (= 2^100) print(BInt(1024) >> 5) # 32 ``` -## Mathematical Functions +### Mathematical Functions -### Exponentiation +#### Exponentiation ```mojo print(BInt(2).power(100)) # 2^100 @@ -339,7 +295,7 @@ print(BInt(2) ** 100) # Same via ** operator Both `power(exponent: Int)` and `power(exponent: BigInt)` are supported. The exponent must be non-negative. -### Integer square root +#### Integer square root ```mojo var x = BInt("100000000000000000000") @@ -349,7 +305,7 @@ print(x.isqrt()) # Same as sqrt() Raises if the value is negative. -## Number Theory +### Number Theory All number-theory operations are available as both **instance methods** and **free functions**: @@ -357,7 +313,7 @@ All number-theory operations are available as both **instance methods** and **fr from decimo import BInt, gcd, lcm, extended_gcd, mod_pow, mod_inverse ``` -### GCD — Greatest Common Divisor +#### GCD — Greatest Common Divisor ```mojo var a = BInt(48) @@ -366,14 +322,14 @@ print(a.gcd(b)) # 6 print(gcd(a, b)) # 6 (free function) ``` -### LCM — Least Common Multiple +#### LCM — Least Common Multiple ```mojo print(BInt(12).lcm(BInt(18))) # 36 print(lcm(BInt(12), BInt(18))) # 36 ``` -### Extended GCD +#### Extended GCD Returns `(g, x, y)` such that `a*x + b*y = g`: @@ -382,7 +338,7 @@ var result = BInt(35).extended_gcd(BInt(15)) # result = (5, 1, -2) — since 35×1 + 15×(−2) = 5 ``` -### Modular Exponentiation +#### Modular Exponentiation Computes $(base^{exp}) \mod m$ efficiently without computing the full power: @@ -391,7 +347,7 @@ print(BInt(2).mod_pow(BInt(100), BInt(1000000007))) print(mod_pow(BInt(2), BInt(100), BInt(1000000007))) # free function ``` -### Modular Inverse +#### Modular Inverse Finds $x$ such that $(a \cdot x) \equiv 1 \pmod{m}$: @@ -400,9 +356,9 @@ print(BInt(3).mod_inverse(BInt(7))) # 5 (since 3×5 = 15 ≡ 1 mod 7) print(mod_inverse(BInt(3), BInt(7))) # 5 ``` -## Conversion and Output +### Conversion and Output -### String conversions +#### String conversions | Method | Example output | | ---------------------------------- | ------------------- | @@ -414,7 +370,7 @@ print(mod_inverse(BInt(3), BInt(7))) # 5 | `x.to_binary_string()` | `"0b110101"` | | `x.to_string(line_width=20)` | Multi-line output | -### Numeric conversions +#### Numeric conversions | Method | Description | | ----------------- | ------------------------------------------------- | @@ -428,7 +384,7 @@ print(x.to_string_with_separators()) # 123_456_789_012_345_678_901_234_567_890 print(x.to_hex_string()) # 0x... ``` -## Query Methods +### Query Methods | Method | Return | Description | | ---------------------- | ------ | ------------------------------------- | @@ -449,7 +405,7 @@ print(x.number_of_digits()) # 2 print(x.is_positive()) # True ``` -## Constants and Factory Methods +### Constants and Factory Methods | Method / Constant | Value | | ---------------------- | ----- | @@ -460,27 +416,23 @@ print(x.is_positive()) # True | `BigInt.ONE` | 1 | | `BigInt.BITS_PER_WORD` | 32 | -# Part II — BigDecimal (`Decimal`) +## Part II — Decimal -## Overview +### Overview — Decimal -`BigDecimal` (aliases `Decimal`, `BDec`) is an arbitrary-precision decimal type — the Mojo-native equivalent of Python's `decimal.Decimal`. It can represent numbers with unlimited digits and decimal places, making it suitable for financial modeling, scientific computing, and applications where floating-point errors are unacceptable. +`Decimal` is an arbitrary-precision decimal type — the Mojo-native equivalent of Python's `decimal.Decimal`. It can represent numbers with unlimited digits and decimal places, making it suitable for financial modeling, scientific computing, and applications where floating-point errors are unacceptable. | Property | Value | | ----------------- | --------------------------------------- | -| Full name | `BigDecimal` | -| Aliases | `Decimal`, `BDec` | +| Name | `Decimal` | +| Aliases | `BigDecimal`, `BDec` | | Internal base | Base-10^9 (each word stores ≤ 9 digits) | | Default precision | 28 significant digits | | Python equivalent | `decimal.Decimal` | -`Decimal`, `BDec`, and `BigDecimal` are all the same type — use whichever you prefer: +`Decimal`, `BigDecimal`, and `BDec` are all the same type. We recommend `Decimal` for consistency with Python's `decimal.Decimal`. -- `Decimal` — familiar to Python users. -- `BDec` — short and concise. -- `BigDecimal` — full explicit name. - -## How Precision Works +### How Precision Works - **Addition, subtraction, multiplication** are always **exact** — no precision loss. - **Division** and **mathematical functions** (`sqrt`, `ln`, `exp`, etc.) accept an optional `precision` parameter specifying the number of **significant digits** in the result. @@ -495,31 +447,43 @@ print(x.sqrt(precision=1000)) # 1000 significant digits > **Note:** The default precision of 28 will be configurable globally in a future version when Mojo supports global variables. -## Construction +### Construction — Decimal + +Decimal can be constructed from various types of input using the `Decimal()` constructor or factory methods. Among these, the most common way is from a **string representation** of the decimal number, which is the most accurate way to create a Decimal without any precision loss. -### From zero +#### From `String` (Decimal) + +It is highly recommended to construct `Decimal` from a string. Please consider using this method whenever possible. ```mojo -var x = Decimal() # 0 +var a = Decimal("123456789.123456789") # Basic decimal string +var b = Decimal("1.23E+10") # Scientific notation +var c = Decimal("-0.000001") # Negative +var d = Decimal("1_000_000.50") # Separator support ``` -### From `Int` +#### From zero (Decimal) ```mojo -var x = Decimal(42) -var y: Decimal = 100 # Implicit conversion +var x = Decimal("0") # Explicitly from string +var y = Decimal() # Default constructor creates zero, same as Decimal("0") ``` -### From `String` +#### From `Int` (Decimal) + +Although you can construct a `Decimal` from an `Int` directly, it is still risky if the `Int` is so large that it exceeds the maximum value of `Int` (which is 2^63-1). ```mojo -var a = Decimal("123456789.123456789") # Plain notation -var b = BDec("1.23E+10") # Scientific notation -var c = Decimal("-0.000001") # Negative -var d = Decimal("1_000_000.50") # Separator support +# These work +var x = Decimal(42) # From Int +var y: Decimal = 100 # Implicit conversion (IntLiteral -> Int -> Decimal) + +# This is dangerous! +var z: Decimal = 9223372036854775808 # 2^63, exceeds Int range! +print(z) # Prints -9223372036854775808, overflowed! ``` -### From integral scalars +#### From integral scalars ```mojo var x = Decimal(Int64(123456789)) @@ -528,16 +492,24 @@ var y = Decimal(UInt128(99999999999999)) Works with all integral SIMD types. **Floating-point scalars are rejected at compile time** — use `from_float()` instead. -### From floating-point — `from_float()` +#### From floating-point — `from_float()` + +When constructing a `Decimal` from a floating-point number, the number is first converted to its string representation and then parsed as a `Decimal`. + +Note that not all decimal numbers can be represented exactly as binary floating-point. You may lose precision without awareness. + +To make the conversion from float to `Decimal` more explicit so that you are aware of the potential precision issues, the `Decimal()` constructor does not accept floating-point numbers directly. Instead, to create a `Decimal` from a float, you must use the `from_float()` factory method. + +Consider never using `Decimal.from_float()` in performance-sensitive code, but use string construction instead. ```mojo var x = Decimal.from_float(3.14159) -var y = BDec.from_float(Float64(2.71828)) +var y = Decimal.from_float(Float64(2.71828)) ``` -> **Why no implicit Float64 constructor?** Implicit conversion from float would silently introduce floating-point artifacts (e.g., `0.1` → `0.1000000000000000055...`). The `from_float()` method makes this explicit. +#### From Python — `from_python_decimal()` -### From Python — `from_python_decimal()` +You can always safely construct a `Decimal` from a Python `decimal.Decimal` using the `from_python_decimal()` method without worrying about precision loss. ```mojo from python import Python @@ -545,23 +517,38 @@ from python import Python var decimal = Python.import_module("decimal") var py_dec = decimal.Decimal("123.456") -var a = BigDecimal.from_python_decimal(py_dec) -var b = BigDecimal(py=py_dec) # Alternative keyword syntax +var a = Decimal.from_python_decimal(py_dec) +var b = Decimal(py=py_dec) # Alternative keyword-only syntax ``` -### Summary of constructors +#### Summary of Decimal constructors | Constructor | Description | | ------------------------------------- | ------------------------------- | | `Decimal()` | Zero | | `Decimal(value: Int)` | From `Int` (implicit) | +| `Decimal(value: UInt)` | From `UInt` (implicit) | | `Decimal(value: String)` | From string (raises) | | `Decimal(value: Scalar)` | From integral scalar (implicit) | +| `Decimal(py=py_obj)` | From Python `Decimal` (raises) | +| `Decimal.from_int(value)` | Explicit factory from `Int` | +| `Decimal.from_uint(value)` | Explicit factory from `UInt` | +| `Decimal.from_integral_scalar(value)` | From any integral scalar type | | `Decimal.from_float(value)` | From floating-point (raises) | +| `Decimal.from_string(value)` | Explicit factory from string | | `Decimal.from_python_decimal(py_obj)` | From Python `Decimal` (raises) | -| `Decimal(coefficient, scale, sign)` | From raw components | -## Arithmetic Operations +#### Unsafe Decimal constructors + +These constructors skip validation for performance-sensitive code. The caller must ensure the data is valid. + +| Constructor | Description | +| --------------------------------------------------------- | -------------------------------------- | +| `Decimal(coefficient: BigUInt, scale: Int, sign: Bool)` | From raw components | +| `Decimal.from_raw_components(words, scale=0, sign=False)` | From raw `List[UInt32]` words (unsafe) | +| `Decimal.from_raw_components(word, scale=0, sign=False)` | From a single `UInt32` word (unsafe) | + +### Decimal Arithmetic Addition, subtraction, and multiplication are always **exact** (no precision loss). @@ -593,11 +580,11 @@ print(a - b) # 123455554.555566789 print(a * b) # 152415787654.32099750190521 ``` -## Division Methods +### Division Methods Division is the primary operation where precision matters. Decimo provides several variants: -### `true_divide()` — recommended for decimal division +#### `true_divide()` — recommended for decimal division ```mojo var a = Decimal("1") @@ -607,20 +594,20 @@ print(a.true_divide(b, precision=50)) # 50 significant digits print(a.true_divide(b, precision=200)) # 200 significant digits ``` -### Operator `/` — true division with default precision +#### Operator `/` — true division with default precision ```mojo var result = a / b # Same as a.true_divide(b, precision=28) ``` -### Operator `//` — truncated (integer) division +#### Operator `//` — truncated (integer) division ```mojo print(Decimal("7") // Decimal("4")) # 1 print(Decimal("-7") // Decimal("4")) # -1 (toward zero) ``` -## Comparison +### Decimal Comparison All six comparison operators are supported: @@ -640,9 +627,9 @@ a.max(b) # Returns the larger value a.min(b) # Returns the smaller value ``` -## Rounding and Formatting +### Rounding and Formatting -### `round()` — round to decimal places +#### `round()` — round to decimal places ```mojo var x = Decimal("123.456") @@ -660,7 +647,7 @@ Also works with `round()` builtin: print(round(Decimal("123.456"), 2)) # 123.46 ``` -### `quantize()` — match scale of another decimal +#### `quantize()` — match scale of another decimal Adjusts the scale (number of decimal places) to match the scale of `exp`. The actual value of `exp` is ignored — only its scale matters. @@ -675,13 +662,13 @@ var price = Decimal("19.999") print(price.quantize(Decimal("0.01"))) # 20.00 ``` -### `normalize()` — remove trailing zeros +#### `normalize()` — remove trailing zeros ```mojo print(Decimal("1.2345000").normalize()) # 1.2345 ``` -### `__ceil__`, `__floor__`, `__trunc__` +#### `__ceil__`, `__floor__`, `__trunc__` ```mojo from math import ceil, floor, trunc @@ -690,7 +677,7 @@ print(floor(Decimal("1.9"))) # 1 print(trunc(Decimal("-1.9"))) # -1 ``` -## RoundingMode +### RoundingMode Seven rounding modes are available: @@ -714,32 +701,32 @@ print(x.round(0, ROUND_CEILING)) # 3 print(x.round(0, ROUND_FLOOR)) # 2 ``` -## Mathematical Functions — Roots and Powers +### Mathematical Functions — Roots and Powers All mathematical functions accept an optional `precision` parameter (default=28). -### Square root +#### Square root ```mojo print(Decimal("2").sqrt()) # 1.414213562373095048801688724 print(Decimal("2").sqrt(precision=100)) # 100 significant digits ``` -### Cube root +#### Cube root ```mojo print(Decimal("27").cbrt()) # 3 print(Decimal("2").cbrt(precision=50)) ``` -### Nth root +#### Nth root ```mojo print(Decimal("256").root(Decimal("8"))) # 2 print(Decimal("100").root(Decimal("3"))) # 4.641588833612778892... ``` -### Power / exponentiation +#### Power / exponentiation ```mojo print(Decimal("2").power(Decimal("10"))) # 1024 @@ -747,16 +734,16 @@ print(Decimal("2").power(Decimal("0.5"), precision=50)) # sqrt(2) to 50 digits print(Decimal("2") ** 10) # 1024 ``` -## Mathematical Functions — Exponential and Logarithmic +### Mathematical Functions — Exponential and Logarithmic -### Exponential (e^x) +#### Exponential (e^x) ```mojo print(Decimal("1").exp()) # e ≈ 2.718281828459045235360287471 print(Decimal("10").exp(precision=50)) # e^10 to 50 digits ``` -### Natural logarithm +#### Natural logarithm ```mojo print(Decimal("10").ln(precision=50)) # ln(10) to 50 digits @@ -772,25 +759,25 @@ var r1 = x1.ln(100, cache) var r2 = x2.ln(100, cache) # Reuses cached ln(2) and ln(1.25) ``` -### Logarithm with arbitrary base +#### Logarithm with arbitrary base ```mojo print(Decimal("100").log(Decimal("10"))) # 2 print(Decimal("8").log(Decimal("2"))) # 3 ``` -### Base-10 logarithm +#### Base-10 logarithm ```mojo print(Decimal("1000").log10()) # 3 (exact for powers of 10) print(Decimal("2").log10(precision=50)) ``` -## Mathematical Functions — Trigonometric +### Mathematical Functions — Trigonometric All trigonometric functions take inputs in **radians** and accept an optional `precision` parameter. -### Basic functions +#### Basic functions ```mojo print(Decimal("0.5").sin(precision=50)) @@ -798,7 +785,7 @@ print(Decimal("0.5").cos(precision=50)) print(Decimal("0.5").tan(precision=50)) ``` -### Reciprocal functions +#### Reciprocal functions ```mojo print(Decimal("1").cot(precision=50)) # cos/sin @@ -806,15 +793,15 @@ print(Decimal("1").csc(precision=50)) # 1/sin print(Decimal("1").sec(precision=50)) # 1/cos ``` -### Inverse functions +#### Inverse functions ```mojo print(Decimal("1").arctan(precision=50)) # π/4 to 50 digits ``` -## Mathematical Constants +### Mathematical Constants -### π (pi) +#### π (pi) Computed using the **Chudnovsky algorithm** with binary splitting: @@ -823,7 +810,7 @@ print(Decimal.pi(precision=100)) # 100 digits of π print(Decimal.pi(precision=1000)) # 1000 digits of π ``` -### e (Euler's number) +#### e (Euler's number) Computed as `exp(1)`: @@ -832,9 +819,9 @@ print(Decimal.e(precision=100)) # 100 digits of e print(Decimal.e(precision=1000)) # 1000 digits of e ``` -## Conversion and Output +### Decimal Conversion and Output -### String output +#### String output The `to_string()` method provides flexible formatting: @@ -858,20 +845,20 @@ x.to_eng_string() # to_string(engineering=True) x.to_string_with_separators("_") # to_string(delimiter="_") ``` -### `repr()` +#### `repr()` ```mojo print(repr(Decimal("123.45"))) # BigDecimal("123.45") ``` -### Numeric conversions +#### Decimal numeric conversions ```mojo var n = Int(Decimal("123.99")) # 123 (truncates) var f = Float64(Decimal("3.14")) # 3.14 (may lose precision) ``` -## Query Methods +### Decimal Query Methods | Method | Return | Description | | ---------------------- | ------ | ----------------------------------------- | @@ -885,14 +872,14 @@ var f = Float64(Decimal("3.14")) # 3.14 (may lose precision) | `x.adjusted()` | `Int` | Adjusted exponent (≈ floor(log10(\|x\|))) | | `x.same_quantum(y)` | `Bool` | `True` if both have same scale | -### `as_tuple()` — Python-compatible decomposition +#### `as_tuple()` — Python-compatible decomposition ```mojo var sign, digits, exp = Decimal("7.25").as_tuple() # sign=False, digits=[7, 2, 5], exp=-2 ``` -### Other methods +#### Other methods ```mojo x.copy_abs() # Copy with positive sign @@ -902,9 +889,9 @@ x.fma(a, b) # Fused multiply-add: x*a+b (exact) x.scaleb(n) # Multiply by 10^n (O(1), adjusts scale only) ``` -## Python Interoperability +### Python Interoperability -### From Python +#### From Python ```mojo from python import Python @@ -912,12 +899,12 @@ from python import Python var decimal = Python.import_module("decimal") var py_val = decimal.Decimal("3.14159265358979323846") -var d = BigDecimal.from_python_decimal(py_val) +var d = Decimal.from_python_decimal(py_val) # Or: -var d = BigDecimal(py=py_val) +var d = Decimal(py=py_val) ``` -### Matching Python's API +#### Matching Python's API Many methods mirror Python's `decimal.Decimal` API: @@ -933,26 +920,27 @@ Many methods mirror Python's `decimal.Decimal` API: | `d.adjusted()` | `x.adjusted()` | | `d.same_quantum(other)` | `x.same_quantum(other)` | -## Appendix A — Import Paths +### Appendix A — Import Paths ```mojo # Recommended: import everything commonly needed from decimo.prelude import * -# Brings in: BInt, Decimal, BDec, Dec128, RoundingMode, -# ROUND_DOWN, ROUND_HALF_UP, ROUND_HALF_EVEN, ROUND_UP, ROUND_CEILING, ROUND_FLOOR +# Brings in: BigInt, BInt, Integer, Decimal, BigDecimal, BDec, Dec128, +# RoundingMode, ROUND_DOWN, ROUND_HALF_UP, ROUND_HALF_EVEN, +# ROUND_UP, ROUND_CEILING, ROUND_FLOOR # Or import specific types -from decimo import BInt, BigInt -from decimo import Decimal, BDec, BigDecimal +from decimo import BInt, BigInt, Integer +from decimo import Decimal # also available as BigDecimal or BDec from decimo import RoundingMode # Number-theory free functions from decimo import gcd, lcm, extended_gcd, mod_pow, mod_inverse ``` -## Appendix B — Traits Implemented +### Appendix B — Traits Implemented -### BigInt +#### BigInt | Trait | What it enables | | ------------------ | -------------------------------- | @@ -966,7 +954,7 @@ from decimo import gcd, lcm, extended_gcd, mod_pow, mod_inverse | `Stringable` | `String(x)`, `str(x)` | | `Writable` | `print(x)`, writer protocol | -### BigDecimal +#### Decimal | Trait | What it enables | | ------------------ | -------------------------------- | @@ -981,9 +969,9 @@ from decimo import gcd, lcm, extended_gcd, mod_pow, mod_inverse | `Stringable` | `String(x)`, `str(x)` | | `Writable` | `print(x)`, writer protocol | -## Appendix C — Complete API Tables +### Appendix C — Complete API Tables -### BigInt — All Operators +#### BigInt — All Operators | Operator / Method | Accepts | Raises? | Description | | ------------------- | -------------------- | ------- | ---------------------- | @@ -1008,7 +996,7 @@ from decimo import gcd, lcm, extended_gcd, mod_pow, mod_inverse | `a.mod_pow(e, m)` | `BInt`/`Int`, `BInt` | Yes | Modular exponentiation | | `a.mod_inverse(m)` | `BInt` | Yes | Modular inverse | -### BigDecimal — Mathematical Functions +#### Decimal — Mathematical Functions | Function | Signature | Default | Description | | -------- | ---------------------------- | ------- | -------------------- | diff --git a/docs/examples_on_bigdecimal.mojo b/examples/examples_on_bigdecimal.mojo similarity index 95% rename from docs/examples_on_bigdecimal.mojo rename to examples/examples_on_bigdecimal.mojo index b9e61781..8ccff547 100644 --- a/docs/examples_on_bigdecimal.mojo +++ b/examples/examples_on_bigdecimal.mojo @@ -2,10 +2,8 @@ from decimo.prelude import * def main() raises: - var b = Decimal( - "1234.56789" - ) # Decimal is a Python-like alias for BigDecimal - var a = BDec("123456789.123456789") # BDec is another alias for BigDecimal + var a = Decimal("123456789.123456789") + var b = Decimal("1234.56789") # === Basic Arithmetic === # print(a + b) # 123458023.691346789 diff --git a/docs/examples_on_bigint.mojo b/examples/examples_on_bigint.mojo similarity index 100% rename from docs/examples_on_bigint.mojo rename to examples/examples_on_bigint.mojo diff --git a/docs/examples_on_decimal128.mojo b/examples/examples_on_decimal128.mojo similarity index 100% rename from docs/examples_on_decimal128.mojo rename to examples/examples_on_decimal128.mojo diff --git a/pixi.toml b/pixi.toml index 0496ead4..1929e948 100644 --- a/pixi.toml +++ b/pixi.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" name = "decimo" platforms = ["osx-arm64", "linux-64"] readme = "README.md" -version = "0.9.0" +version = "0.10.0" [dependencies] # argmojo = ">=0.4.0" # CLI argument parsing for the Decimo calculator (TODO: waiting for argmojo 0.4.0 compatible with mojo 0.26.2) diff --git a/python/README.md b/python/README.md index a2f008dc..43967d0a 100644 --- a/python/README.md +++ b/python/README.md @@ -14,7 +14,7 @@ ## What is decimo? `decimo` is an arbitrary-precision decimal and integer library, originally written in [Mojo](https://www.modular.com/mojo). -This package exposes `decimo`'s `BigDecimal` type to Python via a Mojo-built CPython extension module (`_decimo.so`), +This package exposes `decimo`'s `Decimal` type to Python via a Mojo-built CPython extension module (`_decimo.so`), with a thin Python wrapper providing full Pythonic operator support. ```python @@ -30,13 +30,13 @@ print(a / b) # 0.12499999... ## Status -| Feature | Status | -| ------------------------------------------------------ | ------------- | -| `Decimal` (BigDecimal) arithmetic (`+`, `-`, `*`, `/`) | ✓ Working | -| Comparison operators | ✓ Working | -| Unary `-`, `abs()`, `bool()` | ✓ Working | -| Pre-built wheels on PyPI | ? Coming soon | -| `BigInt` / `Decimal128` Python bindings | ? Planned | +| Feature | Status | +| ----------------------------------------- | ------------- | +| `Decimal` arithmetic (`+`, `-`, `*`, `/`) | ✓ Working | +| Comparison operators | ✓ Working | +| Unary `-`, `abs()`, `bool()` | ✓ Working | +| Pre-built wheels on PyPI | ? Coming soon | +| `BigInt` / `Decimal128` Python bindings | ? Planned | ## Building from source diff --git a/src/decimo/bigdecimal/bigdecimal.mojo b/src/decimo/bigdecimal/bigdecimal.mojo index d7dc9f93..83fd6222 100644 --- a/src/decimo/bigdecimal/bigdecimal.mojo +++ b/src/decimo/bigdecimal/bigdecimal.mojo @@ -33,10 +33,37 @@ from decimo.bigdecimal.rounding import round_to_precision from decimo.bigint10.bigint10 import BigInt10 import decimo.str -comptime BDec = BigDecimal -"""An arbitrary-precision decimal, similar to Python's `decimal.Decimal`.""" +# Type aliases for the arbitrary-precision decimal type. +# The names BigDecimal, Decimal, and BDec can be used interchangeably. +# The preferred name that is exposed to users is Decimal. comptime Decimal = BigDecimal -"""An arbitrary-precision decimal, similar to Python's `decimal.Decimal`.""" +"""An arbitrary-precision decimal, similar to Python's `decimal.Decimal`. + +Notes: + +Internal Representation: + +- A base-10 unsigned integer (BigUInt) for coefficient. +- A Int value for the scale +- A Bool value for the sign. + +Final value: +(-1)**sign * coefficient * 10^(-scale) +""" +comptime BDec = BigDecimal +"""An arbitrary-precision decimal, similar to Python's `decimal.Decimal`. + +Notes: + +Internal Representation: + +- A base-10 unsigned integer (BigUInt) for coefficient. +- A Int value for the scale +- A Bool value for the sign. + +Final value: +(-1)**sign * coefficient * 10^(-scale) +""" comptime PRECISION = 28 # Same as Python's decimal module default precision of 28 places. """Default precision for BigDecimal operations. @@ -44,6 +71,28 @@ This will be configurable in future when Mojo supports global variables. """ +# [Mojo Miji] +# The name BigDecimal, Decimal, and BDec can be used interchangeably. +# They are just aliases for the same struct. +# +# Initially, I chose BigDecimal as the canonical name because it is the most +# descriptive and unambiguous. +# +# However, now I prefer to use Decimal as the canonical name for two reasons: +# First, it is more familiar to Python users (`decimal.Decimal`). +# Second, BigDecimal is a bit long to type, and in most cases, users just use +# the Decimal alias. Nevertheless, Mojo does not provide a complete docstring +# when users hover over the alias, so it is important to name the struct itself +# with a more popular name to provide better discoverability and documentation. +# +# Nevertheless, there is a Mojo compiler issue (still in v0.26.2) that, when +# I change the struct name to Decimal and make BigDecimal an alias, the compile +# fails at setting `--debug-level=full`. I could not tell why this happens, +# but it seems that the compiler gets confused about the alias and the struct +# name when other modules import BigDecimal instead of Decimal. +# +# Thus, for now I will keep the struct name as BigDecimal and make Decimal an +# alias, and wait for the Mojo compiler to fix this issue in the future. struct BigDecimal( Absable, Comparable, @@ -188,7 +237,7 @@ struct BigDecimal( " avoid unintentional loss of precision. If you want to create" " a BigDecimal from a floating-point number, please consider" " wrapping it with quotation marks or using the" - " `BigDecimal.from_float()` (or `BDec.from_float()`) method" + " `Decimal.from_float()` method" " instead." "\n***********************************************************" ) diff --git a/tests/test_bigdecimal.sh b/tests/test_bigdecimal.sh index 43f07cb9..b66a2d95 100755 --- a/tests/test_bigdecimal.sh +++ b/tests/test_bigdecimal.sh @@ -2,5 +2,6 @@ set -e for f in tests/bigdecimal/*.mojo; do + echo "=== $f ===" pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done diff --git a/tests/test_bigint.sh b/tests/test_bigint.sh index 88790488..7bdf8066 100755 --- a/tests/test_bigint.sh +++ b/tests/test_bigint.sh @@ -2,5 +2,6 @@ set -e for f in tests/bigint/*.mojo; do + echo "=== $f ===" pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done diff --git a/tests/test_bigint10.sh b/tests/test_bigint10.sh index a5043d71..18dbba16 100755 --- a/tests/test_bigint10.sh +++ b/tests/test_bigint10.sh @@ -2,5 +2,6 @@ set -e for f in tests/bigint10/*.mojo; do + echo "=== $f ===" pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done diff --git a/tests/test_biguint.sh b/tests/test_biguint.sh index 309884ff..0d80345f 100755 --- a/tests/test_biguint.sh +++ b/tests/test_biguint.sh @@ -2,5 +2,6 @@ set -e for f in tests/biguint/*.mojo; do + echo "=== $f ===" pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done diff --git a/tests/test_decimal128.sh b/tests/test_decimal128.sh index e229f0c1..a7edaa25 100755 --- a/tests/test_decimal128.sh +++ b/tests/test_decimal128.sh @@ -2,5 +2,6 @@ set -e for f in tests/decimal128/*.mojo; do + echo "=== $f ===" pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done diff --git a/tests/test_toml.sh b/tests/test_toml.sh index 7fe978a3..38523abc 100755 --- a/tests/test_toml.sh +++ b/tests/test_toml.sh @@ -2,5 +2,6 @@ set -e for f in tests/toml/*.mojo; do + echo "=== $f ===" pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done