diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 0b700322..58eca682 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -1,5 +1,8 @@ -name: CI +name: Decimo Unit Tests + on: + push: + branches: [main, dev] pull_request: workflow_dispatch: @@ -18,10 +21,8 @@ jobs: # ── Test: BigDecimal ───────────────────────────────────────────────────────── test-bigdecimal: name: Test BigDecimal - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 30 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -32,19 +33,42 @@ jobs: echo "$HOME/.pixi/bin" >> $GITHUB_PATH - name: pixi install run: pixi install - - name: Build packages + - name: Build packages (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== build attempt $attempt ===" + if pixi run mojo package src/decimo && cp decimo.mojopkg tests/; then + echo "=== build succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== build failed after 3 attempts ===" + exit 1 + fi + echo "=== build crashed, retrying in 5s... ===" + sleep 5 + done + - name: Run tests (with retry for Mojo compiler intermittent crashes) run: | - pixi run mojo package src/decimo && cp decimo.mojopkg tests/ - - name: Run tests - run: bash ./tests/test_bigdecimal.sh + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if bash ./tests/test_bigdecimal.sh; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done # ── Test: BigInt ───────────────────────────────────────────────────────────── test-bigint: name: Test BigInt - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 30 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -55,19 +79,42 @@ jobs: echo "$HOME/.pixi/bin" >> $GITHUB_PATH - name: pixi install run: pixi install - - name: Build packages + - name: Build packages (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== build attempt $attempt ===" + if pixi run mojo package src/decimo && cp decimo.mojopkg tests/; then + echo "=== build succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== build failed after 3 attempts ===" + exit 1 + fi + echo "=== build crashed, retrying in 5s... ===" + sleep 5 + done + - name: Run tests (with retry for Mojo compiler intermittent crashes) run: | - pixi run mojo package src/decimo && cp decimo.mojopkg tests/ - - name: Run tests - run: bash ./tests/test_bigint.sh + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if bash ./tests/test_bigint.sh; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done # ── Test: BigUint ──────────────────────────────────────────────────────────── test-biguint: name: Test BigUint - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 30 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -78,19 +125,42 @@ jobs: echo "$HOME/.pixi/bin" >> $GITHUB_PATH - name: pixi install run: pixi install - - name: Build packages + - name: Build packages (with retry for Mojo compiler intermittent crashes) run: | - pixi run mojo package src/decimo && cp decimo.mojopkg tests/ - - name: Run tests - run: bash ./tests/test_biguint.sh + for attempt in 1 2 3; do + echo "=== build attempt $attempt ===" + if pixi run mojo package src/decimo && cp decimo.mojopkg tests/; then + echo "=== build succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== build failed after 3 attempts ===" + exit 1 + fi + echo "=== build crashed, retrying in 5s... ===" + sleep 5 + done + - name: Run tests (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if bash ./tests/test_biguint.sh; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done # ── Test: BigInt10 ─────────────────────────────────────────────────────────── test-bigint10: name: Test BigInt10 - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 30 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -101,19 +171,42 @@ jobs: echo "$HOME/.pixi/bin" >> $GITHUB_PATH - name: pixi install run: pixi install - - name: Build packages + - name: Build packages (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== build attempt $attempt ===" + if pixi run mojo package src/decimo && cp decimo.mojopkg tests/; then + echo "=== build succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== build failed after 3 attempts ===" + exit 1 + fi + echo "=== build crashed, retrying in 5s... ===" + sleep 5 + done + - name: Run tests (with retry for Mojo compiler intermittent crashes) run: | - pixi run mojo package src/decimo && cp decimo.mojopkg tests/ - - name: Run tests - run: bash ./tests/test_bigint10.sh + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if bash ./tests/test_bigint10.sh; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done # ── Test: Decimal128 ───────────────────────────────────────────────────────── test-decimal128: name: Test Decimal128 - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 30 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -124,19 +217,90 @@ jobs: echo "$HOME/.pixi/bin" >> $GITHUB_PATH - name: pixi install run: pixi install - - name: Build packages + - name: Build packages (with retry for Mojo compiler intermittent crashes) run: | - pixi run mojo package src/decimo && cp decimo.mojopkg tests/ - - name: Run tests - run: bash ./tests/test_decimal128.sh + for attempt in 1 2 3; do + echo "=== build attempt $attempt ===" + if pixi run mojo package src/decimo && cp decimo.mojopkg tests/; then + echo "=== build succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== build failed after 3 attempts ===" + exit 1 + fi + echo "=== build crashed, retrying in 5s... ===" + sleep 5 + done + - name: Run tests (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if bash ./tests/test_decimal128.sh; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done + + # ── Test: BigFloat ───────────────────────────────────────────────────────── + test-bigfloat: + name: Test BigFloat + runs-on: macos-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Install pixi + run: curl -fsSL https://pixi.sh/install.sh | sh + - name: Add pixi to PATH + run: | + echo "PIXI_HOME=$HOME/.pixi" >> $GITHUB_ENV + echo "$HOME/.pixi/bin" >> $GITHUB_PATH + - name: pixi install + run: pixi install + - name: Install MPFR + run: brew install mpfr + - name: Build packages (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== build attempt $attempt ===" + if pixi run mojo package src/decimo && cp decimo.mojopkg tests/; then + echo "=== build succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== build failed after 3 attempts ===" + exit 1 + fi + echo "=== build crashed, retrying in 5s... ===" + sleep 5 + done + - name: Run tests (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if bash ./tests/test_bigfloat.sh; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done # ── Test: TOML parser ───────────────────────────────────────────────────── test-toml: name: Test TOML parser - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 15 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -147,19 +311,42 @@ jobs: echo "$HOME/.pixi/bin" >> $GITHUB_PATH - name: pixi install run: pixi install - - name: Build packages + - name: Build packages (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== build attempt $attempt ===" + if pixi run mojo package src/decimo && cp decimo.mojopkg tests/; then + echo "=== build succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== build failed after 3 attempts ===" + exit 1 + fi + echo "=== build crashed, retrying in 5s... ===" + sleep 5 + done + - name: Run tests (with retry for Mojo compiler intermittent crashes) run: | - pixi run mojo package src/decimo && cp decimo.mojopkg tests/ - - name: Run tests - run: bash ./tests/test_toml.sh + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if bash ./tests/test_toml.sh; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done # ── Test: CLI ──────────────────────────────────────────────────────────────── test-cli: name: Test CLI - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 15 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -170,20 +357,42 @@ jobs: echo "$HOME/.pixi/bin" >> $GITHUB_PATH - name: pixi install run: pixi install - - name: Fetch argmojo - run: pixi run fetch - - name: Build CLI binary - run: pixi run buildcli - - name: Run tests - run: bash ./tests/test_cli.sh + - name: Build CLI binary (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== build attempt $attempt ===" + if pixi run buildcli; then + echo "=== build succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== build failed after 3 attempts ===" + exit 1 + fi + echo "=== build crashed, retrying in 5s... ===" + sleep 5 + done + - name: Run tests (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if bash ./tests/test_cli.sh; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done # ── Test: Python bindings ──────────────────────────────────────────────────── test-python: name: Test Python bindings - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 15 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -194,16 +403,58 @@ jobs: echo "$HOME/.pixi/bin" >> $GITHUB_PATH - name: pixi install run: pixi install - - name: Build & run Python tests - run: pixi run testpy + - name: Build & run Python tests (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== test attempt $attempt ===" + if pixi run testpy; then + echo "=== tests passed on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== tests failed after 3 attempts ===" + exit 1 + fi + echo "=== test run crashed, retrying in 5s... ===" + sleep 5 + done + + # ── Doc generation check ────────────────────────────────────────────────────── + doc-check: + name: Doc generation + runs-on: macos-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install pixi + run: curl -fsSL https://pixi.sh/install.sh | sh + - name: Add pixi to PATH + run: | + echo "PIXI_HOME=$HOME/.pixi" >> $GITHUB_ENV + echo "$HOME/.pixi/bin" >> $GITHUB_PATH + - name: pixi install + run: pixi install + - name: Generate docs (with retry for Mojo compiler intermittent crashes) + run: | + for attempt in 1 2 3; do + echo "=== doc attempt $attempt ===" + if pixi run doc; then + echo "=== doc succeeded on attempt $attempt ===" + break + fi + if [ "$attempt" -eq 3 ]; then + echo "=== doc failed after 3 attempts ===" + exit 1 + fi + echo "=== doc crashed, retrying in 5s... ===" + sleep 5 + done # ── Format check ───────────────────────────────────────────────────────────── format-check: name: Format check - runs-on: ubuntu-22.04 + runs-on: macos-latest timeout-minutes: 10 - env: - DEBIAN_FRONTEND: noninteractive steps: - uses: actions/checkout@v4 - name: Install pixi @@ -217,4 +468,4 @@ jobs: - name: Install pre-commit run: pip install pre-commit - name: Run format check - run: pre-commit run --all-files \ No newline at end of file + run: pre-commit run --all-files 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..8e20f358 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,12 @@ 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". ---- +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. @@ -96,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. @@ -110,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 @@ -344,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 new file mode 100644 index 00000000..506580ac --- /dev/null +++ b/benches/cli/bench_cli.sh @@ -0,0 +1,445 @@ +#!/bin/bash +# ===----------------------------------------------------------------------=== # +# CLI Calculator Benchmarks — Correctness & Performance +# +# Compares decimo against bc and python3 on every expression: +# 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 +# +# 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 + +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 ─────────────────────────────────────────────────────────────── + +# 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 ──────────────────────────────────────────────────────────────── + +# 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 + ALL significant digits. +# This ensures values that differ only by exponent (e.g. 1E+10 vs 1E+11) +# 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 + 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}:${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="" + 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") + + # 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 +} + +# Truncate a result string for display. +preview() { + if (( ${#1} > PREVIEW )); then echo "${1:0:$PREVIEW}..."; else echo "$1"; fi +} + +# Record a comparison result for a specific tool. +record() { + 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 + if [[ "$tag" == "ERROR" ]]; then + PY_ERRORS=$((PY_ERRORS + 1)) + else + 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 +} + +# ── 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 + DECIMO_ERRORS=$((DECIMO_ERRORS + 1)) + echo "" + return + fi + + # ── bc ── + 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 + 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 bc "$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 py "$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))" + +# NOTE: mpmath diverges from decimo & WolframAlpha at digit ~21 for sin(near-pi). +# See docs/internal/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)" \ + "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 "============================================================" +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 (( 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/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/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/changelog.md b/docs/changelog.md index 0e1b1fe4..e15c24a6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,20 @@ 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. +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 + ## 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/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/cli_calculator.md b/docs/plans/cli_calculator.md index 129d6a76..9e092cf9 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** (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 @@ -94,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`: @@ -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 @@ -257,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 @@ -290,34 +299,58 @@ 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 | ✗ | | +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 | ✓ | 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 | ✓ | `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 | 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 -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). +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 @@ -329,85 +362,42 @@ 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 | 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 | 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 -1. Detect full-width digits/operators for CJK users while parsing. - -### 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. +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("@")`. -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 | 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 @@ -425,13 +415,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 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/docs/plans/decimal128_enhancement.md b/docs/plans/decimal128_enhancement.md new file mode 100644 index 00000000..9002c309 --- /dev/null +++ b/docs/plans/decimal128_enhancement.md @@ -0,0 +1,663 @@ +# Decimal128 Enhancement Plan + +> **Date**: 2026-04-08 +> **Target**: decimo >=0.9.0 +> **Mojo Version**: >=0.26.2 +> +> 子曰工欲善其事必先利其器 +> The mechanic, who wishes to do his work well, must first sharpen his tools -- Confucius + +I did a thorough audit of `src/decimo/decimal128/` and compared it against other 128-bit fixed-precision decimal libraries — C# [`System.Decimal`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs), Rust [`rust_decimal`](https://github.com/paupino/rust-decimal), Apache Arrow [`Decimal128`](https://github.com/apache/arrow/blob/main/cpp/src/arrow/util/basic_decimal.h), and Go [`govalues/decimal`](https://github.com/govalues/decimal). This document records everything I found: correctness bugs, performance bottlenecks, and improvement opportunities. + +Scope: only 128-bit (or near-128-bit) fixed-precision, non-floating-point decimal types. Arbitrary-precision decimals (Python `decimal.Decimal`, Java `BigDecimal`) are out of scope — they are covered by `BigDecimal`. IEEE 754 decimal128 is also out of scope — it is a floating-point format with discontinuous representation, not comparable to our fixed-point design. + +## 1. Cross-Language Comparison + +### 1.1 Storage & Layout + +| Feature | Decimo Decimal128 | C# System.Decimal | Rust rust_decimal | Arrow Decimal128 | Go govalues/decimal | +| ---------------------- | -------------------- | -------------------- | -------------------- | -------------------------- | ------------------------------- | +| Total bits | 128 | 128 | 128 | 128 | ~128 (bool+uint64+int, may pad) | +| Coefficient storage | 96-bit (3×UInt32 LE) | 96-bit (3×UInt32 LE) | 96-bit (3×UInt32 LE) | 128-bit signed two's compl | 64-bit unsigned | +| Max coefficient | 2^96 − 1 | 2^96 − 1 | 2^96 − 1 | 10^38 − 1 | 10^19 − 1 | +| Bound type | Binary | Binary | Binary | Decimal | Decimal | +| Max significant digits | 29* | 29* | 29* | 38 | 19 | +| Scale range | 0–28 | 0–28 | 0–28 | User-defined | 0–19 | +| Sign storage | Bit 31 of flags | Bit 31 of flags | Bit 31 of flags | Two's complement | Bool field | +| Endianness | Little-endian | Little-endian | Little-endian | Platform-native | N/A | + +\* 29 digits, but the leading digit can only be 0–7 (since 10^29 − 1 > 2^96 − 1). This is pretty dirty and difficult to handle. I think the current implmention is not the most optimized. Need to check and refine. + +Decimo, C#, and Rust share the same layout — a proven design. Arrow and govalues use a fundamentally different approach with decimal-bounded coefficients (10^p − 1 instead of 2^N − 1), which gives them cleaner digit semantics at the cost of unused bit range. + +### 1.2 Special Values + +| Feature | Decimo | C# | Rust | Arrow | Go govalues | +| ------------- | --------------------- | ---------- | ---- | ----- | ----------- | +| +Infinity | ✓ (broken — see §3.1) | ✗ (throws) | ✗ | ✗ | ✗ | +| −Infinity | ✓ (broken) | ✗ | ✗ | ✗ | ✗ | +| NaN | ✓ (broken — see §3.1) | ✗ | ✗ | ✗ | ✗ | +| Negative zero | ✗ | ✗ | ✗ | ✗ | ✗ | +| Subnormals | ✗ | ✗ | ✗ | ✗ | ✗ | + +None of the comparable 128-bit fixed-precision libraries support NaN or Infinity. We are the only one, and our implementation is broken (§3.1). Maybe consider removing NaN/Infinity support to match the established paradigm — all four comparable libraries simply throw or return an error for undefined operations. If we keep them, the bugs must be fixed and full arithmetic propagation must be added, which is a lot of effort for a feature no peer library provides. + +### 1.3 Rounding Modes + +| Mode | Decimo | C# | Rust | Arrow | Go govalues | +| -------------------- | ------ | ------------------------ | ----------- | --------------- | -------------- | +| HALF_EVEN (banker's) | ✓ | ✓ (default) | ✓ (default) | ✓ (default) | ✓ (default) | +| HALF_UP | ✓ | ✓ (`AwayFromZero`) | ✓ | ✓ (`HALF_UP`) | ✓ (`HalfUp`) | +| HALF_DOWN | ✓ | ✗ | ✓ | ✓ (`HALF_DOWN`) | ✓ (`HalfDown`) | +| UP (away from zero) | ✓ | ✓ (`AwayFromZero`) | ✓ | ✓ (`UP`) | ✓ (`Up`) | +| DOWN (truncate) | ✓ | ✓ (`ToZero`) | ✓ | ✓ (`DOWN`) | ✓ (`Down`) | +| CEILING | ✓ | ✓ (`ToPositiveInfinity`) | ✓ | ✓ (`CEILING`) | ✓ (`Ceiling`) | +| FLOOR | ✓ | ✓ (`ToNegativeInfinity`) | ✓ | ✓ (`FLOOR`) | ✓ (`Floor`) | + +All five libraries (including us) support these 7 rounding modes. We are on par. + +### 1.4 Arithmetic Coverage + +| Operation | Decimo | C# | Rust | Arrow | Go govalues | +| -------------------- | --------------- | ---------------- | ---------- | --------- | ------------ | +| add | ✓ | ✓ | ✓ | ✓ | ✓ | +| subtract | ✓ (via add) | ✓ | ✓ | ✓ | ✓ | +| multiply | ✓ | ✓ | ✓ | ✓ | ✓ | +| divide | ✓ | ✓ | ✓ | ✓ | ✓ (`Quo`) | +| truncate_divide | ✓ | ✓ (`Truncate`) | ✓ | ✗ | ✓ (`QuoRem`) | +| modulo | ✓ | ✓ (`%`) | ✓ | ✗ | ✓ (`Rem`) | +| power (int exponent) | ✓ | ✗ (use Math.Pow) | ✓ (`powi`) | ✗ | ✓ (`PowInt`) | +| sqrt | ✓ | ✗ | ✓ | ✗ | ✓ | +| root (nth) | ✓ | ✗ | ✗ | ✗ | ✗ | +| exp | ✓ | ✗ | ✓ | ✗ | ✗ | +| ln | ✓ | ✗ | ✓ | ✗ | ✗ | +| log10 | ✓ | ✗ | ✗ | ✗ | ✗ | +| log (arbitrary base) | ✓ | ✗ | ✗ | ✗ | ✗ | +| abs | ✓ | ✓ | ✓ | ✓ | ✓ | +| negate | ✓ | ✓ | ✓ | ✓ | ✓ | +| round | ✓ | ✓ | ✓ | ✓ | ✓ | +| quantize | ✓ | ✗ | ✗ | via round | ✗ | +| factorial | ✓ (0–27 lookup) | ✗ | ✗ | ✗ | ✗ | +| min / max | ✗ | ✓ | ✓ | ✓ | ✓ | +| normalize | ✗ | ✗ | ✓ | ✗ | ✗ | + +Our arithmetic coverage is the most complete among all five libraries — we are the only one with `root`, `log10`, `log`, and `factorial`. Matching Rust on `exp` and `ln`. The gap is `min`/`max` which every other library provides and we do not. + +## 2. The Coefficient Bound Problem + +This is probably the biggest architectural concern I found. It affects performance, code complexity, and user-facing semantics. + +### 2.1 The Problem + +Our max coefficient is 2^96 − 1 = 79,228,162,514,264,337,593,543,950,335. This is a 29-digit number, but the leading digit can only be 0–7. The number 80,000,000,000,000,000,000,000,000,000 (which has only 2 significant digits) is out of range. Meanwhile, all 28-digit numbers fit. + +This creates a messy boundary: after every arithmetic operation that might produce a wide result (multiplication, addition with carry, etc.), I need to check whether the coefficient exceeds 2^96 − 1 and, if so, round it down. The rounding itself is non-trivial because the boundary is not at a clean decimal digit — I cannot just drop the last digit. The `truncate_to_max` function in `utility.mojo` handles this, and it is one of the most complex functions in the codebase. + +### 2.2 How Other Libraries Handle This + +#### C# System.Decimal — [`ScaleResult()`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs) (binary bound, same as us) + +.NET's approach is heavily optimized. The core function `ScaleResult()` in `Decimal.DecCalc.cs`: + +1. Estimates how many decimal digits to remove using `LeadingZeroCount` and the constant `log10(2) ≈ 77/256`. +2. Divides the wide result (stored in a `Buf24`, up to 192 bits) by powers of 10, using `DivByConst()` specialized per constant (10^1 through 10^9) for maximum speed — on 64-bit targets, these use compiler-generated multiply-by-reciprocal. +3. Applies banker's rounding with a sticky bit for lost precision. +4. If rounding causes a carry that pushes above 96 bits again, scales down by 10 one more time. + +Additional C# tricks: + +- `SearchScale()` — binary search using precomputed `OVFL_MAX_N_HI` constants to find the largest safe scale-up factor. +- `PowerOvflValues[]` — table of largest 96-bit values that won't overflow when multiplied by 10^1 through 10^8. +- `Unscale()` — efficiently removes trailing zeros using binary search: try 10^8, 10^4, 10^2, 10^1, with quick-reject bit checks (e.g., `(low & 0xF) == 0` before trying 10^4). +- `OverflowUnscale()` — when quotient overflows by exactly 1 bit, feeds the carry back in and divides by 10, avoiding a full rescale. + +The bottom line: .NET has hundreds of lines of intricate, heavily-optimized code just for this boundary handling. Every multiply that exceeds 96 bits pays for multi-word division. + +#### Rust rust_decimal — [Port of .NET](https://github.com/paupino/rust-decimal/blob/master/src/ops/common.rs) (binary bound, same as us) + +`rust_decimal` is essentially a Rust port of .NET's `DecCalc`. The `Buf24::rescale()` in `ops/common.rs` is the equivalent of `ScaleResult()`. Same `log10(2) × 256 = 77` trick, same `OVERFLOW_MAX_N_HI` constants, same `POWER_OVERFLOW_VALUES` table. + +One difference: `rust_decimal` returns `CalculationResult::Overflow` instead of throwing, letting the caller handle it. + +#### Apache Arrow Decimal128 — [256-bit promotion](https://github.com/apache/arrow/blob/main/cpp/src/gandiva/precompiled/decimal_ops.cc) (decimal bound) + +Arrow sidesteps the problem entirely by capping at 10^38 − 1 instead of 2^128 − 1. The overflow check is just `abs(value) < 10^precision` — a single comparison against a precomputed constant. + +For multiplication that might overflow 128 bits, Arrow promotes to `int256_t` (Boost or compiler `__int128`-based), multiplies, scales down by `10^delta_scale` in one clean division, then converts back. This replaces .NET's iterative divide-and-round loop with a single wide multiplication + one division. + +The `FitsInPrecision(precision)` check is trivially a comparison against a table entry. No multi-step rescaling needed for the check itself. + +#### Go govalues/decimal — [Two-tier fast path](https://github.com/govalues/decimal/blob/main/decimal.go) (decimal bound) + +`govalues/decimal` uses the most elegant approach. Max coefficient = 10^19 − 1 (fits in `uint64`). + +1. Fast path: try the operation using native 64-bit arithmetic. If overflow detected (e.g., `z/y != x || z > maxFint`), fall through. +2. Slow path: redo with `big.Int` (arbitrary precision), compute exact result, then round to 19 digits using `rshHalfEven` (right-shift in decimal = divide by 10^N, round half-to-even). + +Since the bound IS a power of 10, the rounding is clean: just count digits, divide by 10^excess, round. No awkward non-decimal boundary to deal with. + +### 2.3 Comparison + +| Strategy | Used by | Bound check cost | Overflow rounding cost | Code complexity | +| ------------------- | ----------------- | --------------------- | --------------------------------------- | --------------- | +| Binary (2^96 − 1) | C#, Rust, Decimo | Cheap (bit compare) | Expensive (multi-word ÷10^N, iterative) | High | +| Decimal (10^38 − 1) | Arrow, SQL Server | Cheap (one compare) | Cheap (one wide ÷10^N) | Low | +| Decimal (10^19 − 1) | govalues/decimal | Trivial (one compare) | Trivial (big.Int fallback + round) | Very low | + +### 2.4 Implications for Decimo + +Since we follow the C#/Rust paradigm (binary bound, 2^96 − 1), the `truncate_to_max` complexity is inherent. There are a few things I can do: + +1. Adopt .NET's optimization tricks. Specifically: the `log10(2) ≈ 77/256` estimation for scale-down amount, precomputed `POWER_OVERFLOW_VALUES` table for safe scale-up, and the `Unscale()` trailing-zero removal with quick-reject bit checks. These would make our existing `truncate_to_max` and `round_to_keep_first_n_digits` much faster. + +2. Consider decimal bound for a future Decimal256. If I ever widen to full 128-bit coefficient, using 10^38 − 1 as the max (matching Arrow and SQL Server) would eliminate this problem entirely and give us interoperability with Arrow wire format and SQL `decimal(38)`. + +3. Document the "29 digits but not 29 nines" behavior clearly. Users should know that "29 digits of precision" really means "28 full digits plus a leading digit 0–7". + +### 2.5 Using UInt128/UInt256 as Acceleration Bridge + +Mojo now has native `UInt128` and `UInt256` types (via `Scalar[DType.uint128]` and `Scalar[DType.uint256]`). The codebase already uses them — `coefficient()` bitcasts the three UInt32 words to UInt128, and `multiply()` uses UInt256 for intermediate products. But there are more opportunities: + +- In `truncate_to_max` and `round_to_keep_first_n_digits`, the divmod operations on UInt128/UInt256 could exploit the fact that Mojo compiles to LLVM IR, where UInt128 division on 64-bit targets translates to two hardware `div` instructions (high and low halves). This is much faster than manually managing three 32-bit limbs. We are already partially doing this, but some code paths still work with individual UInt32 words when they could just operate on the UInt128 directly. +- The `number_of_bits` loop (§4.1) could be replaced by casting UInt128 to two UInt64s and using `count_leading_zeros` on the high word — this gives O(1) bit width instead of a 96-iteration loop. +- Arrow's approach of promoting to 256-bit for multiply is directly applicable since we already have UInt256. Instead of the current 3×UInt32 partial-product approach in some code paths, we could do: `UInt256(x_coef) * UInt256(y_coef)`, then scale/truncate the result. This is simpler and likely just as fast since LLVM will optimize the wide multiply. +- For the `compare_absolute` overflow bug (§3.4), using UInt256 instead of UInt128 for the fractional scaling is a one-line fix thanks to the type already being available. + +In short: we already depend on UInt128/UInt256 for the core paths. The opportunity is to use them more consistently and eliminate the remaining manual multi-word arithmetic. + +## 3. Correctness Bugs + +### 3.1 NaN/Infinity Implementation Is Broken + +File: `decimal128.mojo` + +There are multiple compounding bugs in the NaN/Infinity support: + +Bug A — NaN mask mismatch: + +```txt +NAN_MASK = UInt32(0x00000002) # bit 1 +``` + +But the constructors set a different bit: + +```txt +NAN() → Self(0, 0, 0, 0x00000010) # bit 4 +NEGATIVE_NAN() → Self(0, 0, 0, 0x80000010) # bit 4 +``` + +So `Decimal128.NAN().is_nan()` returns False. + +Bug B — `is_zero()` returns True for NaN and Infinity: + +```mojo +fn is_zero(self) -> Bool: + return self.low == 0 and self.mid == 0 and self.high == 0 +``` + +NaN and Infinity have zero coefficients, so `INFINITY().is_zero()` → True. + +Bug C — No arithmetic propagation: none of the arithmetic operations check for NaN or Infinity inputs. Passing them into `add()`, `multiply()`, etc., produces garbage silently. + +Since no comparable 128-bit fixed-precision library supports NaN or Infinity (see §1.2), maybe the cleanest fix is to just remove NaN/Infinity support entirely and raise errors for undefined operations (matching C#, Rust, Arrow, and govalues). If I decide to keep them, all three bugs must be fixed: + +- Fix A: Change `NAN()` → `Self(0, 0, 0, 0x00000002)` and `NEGATIVE_NAN()` → `Self(0, 0, 0, 0x80000002)` to match the mask. +- Fix B: Guard `is_zero()` with `if self.is_nan() or self.is_infinity(): return False`. +- Fix C: Add early-return NaN/Inf checks to every arithmetic function (significant effort). + +### 3.2 `from_words` Uses `testing.assert_true` in Production Code + +File: `decimal128.mojo`, lines ~344, 351 + +```mojo +from testing import assert_true +assert_true(scale <= Self.MAX_SCALE, ...) +assert_true(coefficient <= Self.MAX_AS_UINT128, ...) +``` + +This imports the test framework into production code. `assert_true` panics with a test failure message rather than raising a recoverable error. Users expect `raise Error(...)`. + +Fix: + +```mojo +if scale > Self.MAX_SCALE: + raise Error("Error in Decimal128.from_words(): Scale must be <= 28, got " + str(scale)) +if coefficient > Self.MAX_AS_UINT128: + raise Error("Error in Decimal128.from_words(): Coefficient exceeds 96-bit max") +``` + +### 3.3 Division Hardcodes Rounding Behavior + +File: `arithmetics.mojo`, inside `divide()` + +The long division loop always uses banker's rounding (HALF_EVEN). The function does not accept a rounding mode parameter. + +This matches C# and Rust behavior — both hardcode banker's rounding for the `/` operator. Arrow and govalues also default to HALF_EVEN for division. So this is consistent with all comparable libraries and not really a bug. + +If I ever want configurable rounding in division, I would add a `divide(x, y, rounding_mode)` overload. Low priority. + +### 3.4 `compare_absolute` Potential Overflow + +File: `comparison.mojo` + +When comparing fractional parts, the code computes: + +```mojo +fractional_1 = UInt128(x1_frac_part) * UInt128(10) ** scale_diff +``` + +Since `scale_diff` can be up to 28 and `x1_frac_part` can be up to 2^96 − 1, the product can be as large as (2^96 − 1) × 10^28 ≈ 7.9 × 10^57. UInt128 max is ~3.4 × 10^38. This overflows. + +Fix: use UInt256 for this computation (see §2.5), or normalize both values to the same scale before extracting integer and fractional parts. + +### 3.5 `is_one()` Might Be Incomplete + +File: used in `exponential.mojo` (e.g., `log()` calls `x.is_one()`) + +I should verify `is_one()` handles all representations of 1: `1` (coef=1, scale=0), `1.0` (coef=10, scale=1), `1.00` (coef=100, scale=2), etc. If it only checks `coefficient == 1 and scale == 0`, it misses the other forms. + +## 4. Performance Bottlenecks + +### 4.1 `number_of_bits()` Uses a Loop + +File: `utility.mojo` + +```mojo +fn number_of_bits(n: UInt128) -> Int: + var count = 0 + var x = n + while x > 0: + count += 1 + x >>= 1 + return count +``` + +O(n) in bit count — up to 96 iterations. C#'s [`ScaleResult`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs) uses `LeadingZeroCount` which is a single instruction on modern CPUs. + +Fix: split UInt128 into two UInt64s and use `count_leading_zeros` on the high word. If the high word is zero, use CLZ on the low word. This gives O(1) bit width. See §2.5 for more on using UInt128/UInt256 as acceleration. + +### 4.2 `power_of_10` Is Not Using Precomputed Constants Efficiently + +File: `utility.mojo` + +The function already has hardcoded return values for n=0 through n=32, but for n=33 through n=56+ it falls back to `ValueType(10) ** n` which computes via loop. + +Since `power_of_10` is called in nearly every arithmetic operation (scale adjustment, `number_of_digits`, comparison, division), the fallback path matters. + +Fix: extend the hardcoded constants up to n=58 (the maximum needed for UInt256 products of two 29-digit numbers). The pattern is already there for n ≤ 32 — just continue it. For UInt256 values too large for integer literals, use the `@always_inline` constant function pattern from `constants.mojo`. + +### 4.3 `truncate_to_max` Could Use .NET Tricks + +File: `utility.mojo` + +Our `truncate_to_max` computes `number_of_digits`, then divides by `power_of_10`, then rounds. .NET's [`ScaleResult`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs) uses: + +- `LeadingZeroCount` × `77/256` to estimate digit count without a full `number_of_digits` call +- Division by compile-time constants (10^1 through 10^9) using multiply-by-reciprocal +- `Unscale()` trailing-zero removal with bit-check quick-rejects + +These tricks could significantly speed up our overflow handling. + +### 4.4 `ln()` Range Reduction Uses Loops + +File: `exponential.mojo` + +The `ln()` function reduces input to [0.5, 2.0) by repeatedly dividing by 10, then by 2: + +```mojo +while x_reduced > Decimal128(2, 0, 0, 0): + x_reduced = decimo.decimal128.arithmetics.divide(x_reduced, Decimal128(2, 0, 0, 0)) + halving_count += 1 +``` + +For `ln(1e28)`, this loop runs ~93 times (since 10^28 ≈ 2^93), each doing a full Decimal128 division. + +Fix: use the identity `ln(a × 10^n) = ln(a) + n × ln(10)` to strip the scale in one step. Then only the coefficient (which is in [1, 2^96)) needs halving, which could be reduced by computing the bit width and dividing by the appropriate power of 2 in one shot. + +### 4.5 `subtract()` Creates a Temporary + +File: `arithmetics.mojo` + +```mojo +def subtract(x1: Decimal128, x2: Decimal128) raises -> Decimal128: + return add(x1, negative(x2)) +``` + +Creates a temporary just to flip the sign bit. Probably minor — Decimal128 is 16 bytes and should be in registers. Low priority unless profiling shows otherwise. + +### 4.6 Series Computations Cap at 500 Iterations + +File: `exponential.mojo` + +Both `exp_series` and `ln_series` loop up to 500 with convergence check `term.is_zero()`. In practice they converge in 30–60 iterations. The issue: `is_zero()` triggers only when the term underflows to exactly zero, which may require a few extra iterations beyond when the term is already too small to affect the result. + +Fix: break early if the term is smaller than 10^(−29) — it cannot change the result at our precision. + +### 4.7 `from_string` Processes Digits One at a Time + +File: `decimal128.mojo` + +```mojo +coef = coef * 10 + digit +``` + +Each iteration does a UInt128 multiply-by-10 and add. + +Fix: batch up to 9 digits into a UInt64, then multiply `coef` by the appropriate power of 10 and add the batch. Reduces 128-bit multiplications from ~29 to ~4 for a max-length number. + +### 4.8 Division Loop: Separate `//` and `%` Operations + +File: `arithmetics.mojo` + +```mojo +digit = rem // x2_coef +rem = rem % x2_coef +``` + +Two separate 128-bit divisions on the same operands. Most hardware produces both quotient and remainder in a single `div` instruction. + +Fix: use `divmod()` if available in Mojo. + +## 5. Improvement Opportunities + +### 5.1 Add `__hash__` Support + +C# and Rust both support hashing their decimal types. I should implement `__hash__` so Decimal128 can be used as a dictionary key or in sets. + +Approach: hash the normalized form (strip trailing zeros, then hash coefficient + scale + sign). + +### 5.2 Add `Stringable` / `Representable` Protocol Conformance + +Check that we conform to Mojo's `Stringable` and `Representable` traits for `str()` and `repr()`. + +### 5.3 Better `from_float` Accuracy + +The current `from_float` (Float64) path likely goes through string conversion. Maybe consider exact float decomposition (extracting mantissa and exponent from IEEE 754 double bits). + +For reference: Rust [`rust_decimal`](https://github.com/paupino/rust-decimal) uses exact mantissa/exponent extraction from f64 bits. + +### 5.4 `min()` / `max()` / `clamp()` + +All four comparable libraries provide `min`/`max`. Easy to implement. + +### 5.5 Canonicalization / `normalize()` + +Strip trailing zeros: `1.200` (coef=1200, scale=3) → `1.2` (coef=12, scale=1). Useful for hashing (§5.1) and reducing coefficient size for faster subsequent arithmetic. Rust [`rust_decimal`](https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html#method.normalize) has `normalize()`. + +### 5.6 Wider Testing for Edge Cases + +Some test cases worth adding: + +| Test Case | Expected Behavior | +| -------------------------------------------- | ---------------------------- | +| `from_words` with scale > 28 | Error (not assertion panic) | +| `from_words` with coefficient > 2^96 − 1 | Error (not assertion panic) | +| `compare_absolute` with max scale difference | Correct result (no overflow) | +| `is_one()` with 1.0, 1.00, 1.000 | True | +| Max coefficient after multiply | Correct rounding | +| 29-digit numbers near 2^96 − 1 boundary | Correct truncate/round | + +## 6. Priority Summary + +| # | Issue | Severity | Effort | Priority | +| --- | --------------------------------------- | ----------- | ----------------------------- | -------- | +| 3.1 | NaN/Inf broken (fix or remove) | Critical | Small (remove) / Medium (fix) | P0 | +| 3.2 | `from_words` uses `testing.assert_true` | Medium | Small | P1 | +| 4.2 | `power_of_10` not fully precomputed | High | Small | P1 | +| 4.3 | `truncate_to_max` lacks .NET tricks | High | Medium | P1 | +| 3.4 | `compare_absolute` overflow | Medium | Small | P2 | +| 4.1 | `number_of_bits` loop | Medium | Small | P2 | +| 4.8 | Separate `//` and `%` in division | Medium | Small | P2 | +| 4.7 | `from_string` digit-by-digit | Medium | Medium | P2 | +| 4.4 | `ln()` range reduction loops | Medium | Medium | P2 | +| 5.4 | `min/max/clamp` | Enhancement | Trivial | P3 | +| 5.5 | `normalize()` | Enhancement | Small | P3 | +| 5.1 | `__hash__` | Enhancement | Small | P3 | +| 5.6 | Edge case tests | Enhancement | Medium | P3 | +| 4.5 | `subtract` temporary | Low | Trivial | P4 | +| 4.6 | Series convergence tolerance | Low | Small | P4 | +| 3.3 | Division rounding mode (configurable) | Low | Medium | P4 | +| 5.3 | Better `from_float` | Enhancement | Medium | P4 | +| 5.2 | `Stringable` conformance | Enhancement | Trivial | P4 | + +## 7. Execution Order + +Phase 1 — correctness: + +1. Decide: remove NaN/Infinity or fix them (§3.1). If removing, delete `NAN()`, `NEGATIVE_NAN()`, `INFINITY()`, `NEGATIVE_INFINITY()`, `NAN_MASK`, `INFINITY_MASK`, `is_nan()`, `is_infinity()` and change callers to raise errors. If fixing, apply fixes A/B/C from §3.1. +2. Fix `from_words` to use `raise Error` instead of `testing.assert_true` (§3.2). +3. Add edge case tests (§5.6). + +Phase 2 — performance (coefficient bound): + +1. Extend `power_of_10` hardcoded constants up to n=58 (§4.2). +2. Add .NET-style tricks to `truncate_to_max`: CLZ-based digit estimation, divide-by-constant optimization, trailing-zero quick-reject (§4.3). +3. Replace `number_of_bits` with hardware CLZ via UInt64 split (§4.1). +4. Fix `compare_absolute` overflow with UInt256 (§3.4). + +Phase 3 — performance (general): + +1. Optimize `from_string` digit batching (§4.7). +2. Use single divmod in division loop (§4.8). +3. Improve `ln()` range reduction (§4.4). + +Phase 4 — enhancements: + +1. Add `min/max/clamp` (§5.4). +2. Add `normalize()` (§5.5). +3. Add `__hash__` (§5.1). +4. Better `from_float` (§5.3). + +## Appendix A. Survey of 128-Bit Fixed-Precision Decimal Types + +> **Scope:** 128-bit (or near-128-bit) fixed-precision, non-floating-point decimal types across +> programming languages and libraries. This explicitly **excludes** arbitrary-precision decimals +> (Python `decimal.Decimal`, Java `BigDecimal`, Go `shopspring/decimal`, Go `cockroachdb/apd`) and +> IEEE 754 decimal128 (which is a floating-point format with 34-digit significand, exponent range +> −6176 to +6111, and NaN/Infinity/subnormals). + +### A.1 Detailed Comparison Table + +| Name | Language / Platform | Total Bits | Coefficient Storage | Max Coefficient | Max Sig. Digits | Scale Range | NaN / ±Inf | +| -------------------- | ----------------------------- | ------------------------------- | ------------------------------------------------ | ---------------------------------------------------- | --------------- | -------------------------- | ---------- | +| **System.Decimal** | C# / .NET CLR | 128 | 96-bit unsigned (3×Int32: lo, mid, hi) | 2^96 − 1 = 79,228,162,514,264,337,593,543,950,335 | 29 | 0–28 | No | +| **rust_decimal** | Rust (crate) | 128 | 96-bit unsigned (3×u32: lo, mid, hi) | 2^96 − 1 (same as C#) | 29 | 0–28 | No | +| **VB.NET Decimal** | VB.NET / .NET CLR | 128 | Identical to C# (same CLR type `System.Decimal`) | 2^96 − 1 | 29 | 0–28 | No | +| **Arrow Decimal128** | Apache Arrow (cross-language) | 128 | 128-bit two's complement signed integer | 10^p − 1 (bounded by declared precision p, max p=38) | 38 | User-defined (any integer) | No | +| **govalues/decimal** | Go (module) | 128 (1 bool + 1 uint64 + 1 int) | 64-bit unsigned integer (uint64 coefficient) | 10^19 − 1 = 9,999,999,999,999,999,999 | 19 | 0–19 | No | + +Other non-128-bit types for reference: + +| Name | Language / Platform | Total Bits | Coefficient Storage | Max Coefficient | Max Sig. Digits | Scale Range | NaN / ±Inf | +| ----------------------------- | ---------------------- | -------------- | -------------------------------------- | --------------------------------------------------------------------------------- | ----------------- | ---------------------- | ---------- | +| **Swift Decimal** (NSDecimal) | Swift / Foundation | 160 (20 bytes) | 128-bit mantissa (8×UInt16) | Up to 38 decimal digits; mantissa is 128 bits but capped at 10^38 − 1 in practice | 38 | Exponent: −128 to +127 | NaN only | +| **SQL Server decimal(38)** | T-SQL / SQL Server | 136 (17 bytes) | 128-bit unsigned integer (4×Int32) | 10^38 − 1 = 99,999,999,999,999,999,999,999,999,999,999,999,999 | 38 | 0 to p (max 38) | No | +| **Delphi Currency** | Delphi / Object Pascal | 64 | 64-bit signed integer (scaled by 10^4) | 2^63 − 1 = 922,337,203,685,477.5807 | 19 (4 fractional) | Fixed at 4 | No | + +### A.2 Notes on Each Type + +#### C# System.Decimal (and VB.NET, F#) + +The .NET CLR `System.Decimal` is the reference design that Decimo Decimal128, Rust `rust_decimal`, +and several others copy. Its 128-bit layout packs a 96-bit unsigned coefficient into three 32-bit +words (`lo`, `mid`, `hi`), with a 32-bit flags word encoding: sign in bit 31, scale in bits 16–20 +(value 0–28), and bits 0–15 & 21–30 reserved (must be zero). + +Constructor: `Decimal(Int32 lo, Int32 mid, Int32 hi, Boolean isNegative, Byte scale)`. + +Max value: ±79,228,162,514,264,337,593,543,950,335 (= 2^96 − 1). This is **not** a round decimal +number — the upper bound is a power-of-2 boundary, not 10^29 − 1. + +No NaN, no Infinity. `INumberBase.IsNaN()` always returns `false`. + +#### Rust rust_decimal + +Mirrors the C# layout exactly: `lo: u32, mid: u32, hi: u32` for the 96-bit coefficient, flags word +with sign + scale. `MAX = 79_228_162_514_264_337_593_543_950_335`. Serializes to 16 bytes +(4 bytes flags + 12 bytes coefficient). The `from_parts(lo, mid, hi, negative, scale)` constructor +matches C# directly. + +No NaN, no Infinity. The `MathematicalOps` trait adds `sqrt()`, `exp()`, `ln()`, `pow()`, etc. + +#### Apache Arrow Decimal128 + +Fundamentally different from the C#/Rust design. Arrow Decimal128 stores the value as a **128-bit +two's complement signed integer**, not a 96-bit unsigned coefficient. The value represents +`integer_value / 10^scale`, where `precision` (1–38) and `scale` are declared in the schema. + +The max representable coefficient for `decimal128(38, 0)` is 10^38 − 1 (= 38 nines), which is +much smaller than 2^127 − 1 ≈ 1.7 × 10^38. Arrow deliberately caps at 10^precision − 1 rather +than using the full bit range, to ensure consistent decimal digit semantics. + +Schema definition (Apache Arrow `Schema.fbs`): + +```txt +table Decimal { precision: int; scale: int; bitWidth: int = 128; } +``` + +No NaN, no Infinity. Each column has a single fixed precision and scale. + +#### Swift Foundation Decimal (NSDecimal) + +Not truly 128-bit — the struct is **160 bits (20 bytes)**. Layout: + +- `exponent: Int8` (−128 to +127) +- `lengthFlagsAndReserved: UInt8` (4-bit length, 1-bit isNegative, 1-bit isCompact, 2-bit reserved) +- `reserved: UInt16` +- `mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16)` — 8×UInt16 = 128 bits + +The 128-bit mantissa can theoretically hold values up to 2^128 − 1, but the `_length` field +(4 bits, max 15) indicates how many of the 8 UInt16 slots are used, and Apple documents the max as +38 significant decimal digits (i.e., effectively capped at 10^38 − 1). + +Unlike C# and Rust, Swift Decimal **supports NaN** (`isNaN` property). It does NOT support Infinity +in practice — the `isInfinite` property exists (inherited from `FloatingPoint` protocol) but Apple's +implementation does not produce or handle Infinity values meaningfully. + +#### SQL Server decimal / numeric + +SQL Server `decimal(p, s)` with max precision 38. Storage size varies by precision: + +| Precision | Storage Bytes | +| --------- | ------------- | +| 1–9 | 5 | +| 10–19 | 9 | +| 20–28 | 13 | +| 29–38 | 17 | + +At precision 29–38, the storage is 17 bytes: 1 byte for sign + 16 bytes (128 bits) for the +unsigned integer coefficient. The max value is 10^38 − 1 (38 nines). This is a decimal-bounded +maximum, not a binary one. Valid values range from `-(10^p - 1)` to `+(10^p - 1)`. + +No NaN, no Infinity. `decimal` and `numeric` are synonyms; both are fixed precision and scale. + +#### Go govalues/decimal + +A high-performance, zero-allocation decimal designed for financial systems. Internally uses a +`uint64` coefficient (max 10^19 − 1 = 9,999,999,999,999,999,999) with 19-digit precision and +scale 0–19. The struct fits in 128 bits total (bool sign + uint64 coefficient + int scale, though +Go struct layout may pad slightly). + +No NaN, no Infinity, no negative zero, no subnormals. Immutable, panic-free (returns errors). +Uses half-to-even rounding by default. Falls back to `big.Int` for intermediate calculations to +maintain correctness, but final results are always rounded to 19 digits. + +#### Delphi Currency + +Only 64 bits, included for completeness. A 64-bit signed integer scaled by 10^4 (i.e., always +exactly 4 decimal places). Max value: 922,337,203,685,477.5807. Not truly 128-bit, but it is a +notable example of a fixed-point decimal type in the wild. + +### A.3 Eliminated Candidates + +The following were investigated but **excluded** because they are arbitrary-precision (not +fixed-precision within 128 bits): + +| Name | Language | Reason for Exclusion | +| ---------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------ | +| **shopspring/decimal** | Go | Arbitrary precision; uses `math/big.Int` internally. No fixed bit width. | +| **cockroachdb/apd** | Go | Arbitrary precision; `Decimal.Coeff` is a `BigInt` (wrapper around `big.Int`). Implements the General Decimal Arithmetic spec. | +| **PostgreSQL numeric** | PostgreSQL | Variable-length arbitrary precision. Stored as variable-length array of base-10000 digits. No 128-bit limit. | +| **GCC __int128** | C/C++ | A 128-bit integer, not a decimal type. Could be used to build a decimal library but is not one itself. | + +### A.4 Critical Analysis: Coefficient Upper Bound Approaches + +There are **two fundamentally different approaches** to bounding the coefficient in fixed-precision +decimal types: + +#### Approach 1: Binary Bound (2^N − 1) + +**Used by:** C# System.Decimal, Rust rust_decimal, Decimo Decimal128 + +The coefficient is an N-bit unsigned integer, and the maximum value is the full binary range +2^N − 1. For 96-bit coefficients: + +- Max = 2^96 − 1 = **79,228,162,514,264,337,593,543,950,335** +- This is a 29-digit number, but the leading digit can only be 0–7 (since 10^29 − 1 > 2^96 − 1) +- Consequence: The first significant digit is constrained. You get 29 digits only when the leading + digit is ≤ 7. You get the full 0–9 range only for 28-digit numbers. + +**Pros:** + +- Natural fit for hardware — the coefficient is just a native (multi-word) integer +- Simple bounds check: just compare against 2^96 − 1 +- Slightly larger range than 10^28 − 1 (about 7.9× more values in the 29th digit range) + +**Cons:** + +- The "truncate-to-max" problem: when an operation produces a coefficient > 2^96 − 1, you must + either raise an error or round. The boundary is not at a clean decimal digit boundary, which + makes rounding semantics awkward. E.g., 80,000,000,000,000,000,000,000,000,000 (8×10^28) is + out of range, even though it only needs 2 significant digits. +- Non-uniform digit range: the 29th digit has range 0–7, not 0–9. This is confusing for users. + +#### Approach 2: Decimal Bound (10^p − 1) + +**Used by:** Apache Arrow Decimal128, SQL Server decimal(38), Swift Decimal, govalues/decimal + +The coefficient is bounded by 10^p − 1, where p is the declared precision. Even if the underlying +storage has more bits available, values above 10^p − 1 are not representable. + +For Arrow/SQL precision 38: + +- Max = 10^38 − 1 = **99,999,999,999,999,999,999,999,999,999,999,999,999** (38 nines) +- Fits in 128 bits (10^38 − 1 < 2^127 − 1), with ~90 bits of the 128 used +- Every digit position has the full 0–9 range + +For govalues/decimal precision 19: + +- Max = 10^19 − 1 = **9,999,999,999,999,999,999** (19 nines) +- Fits in a single uint64 (10^19 − 1 < 2^64 − 1), with ~63 bits of the 64 used + +**Pros:** + +- Clean decimal semantics: every digit position has the full 0–9 range +- No "truncate-to-max" confusion at non-decimal boundaries +- Precision is exactly p significant decimal digits, no asterisks +- Easier to reason about for financial applications + +**Cons:** + +- Wastes some of the available bit range (Arrow uses ~126.5 of 128 bits; govalues uses ~63.1 of 64) +- Bounds checking requires a comparison against a decimal constant, not a simple overflow check + +#### Implications for Decimo + +Decimo follows the C#/Rust approach (binary bound, 2^96 − 1). This means: + +1. **The `truncate_to_max` style bound-checking IS a concern:** When multiplying two 29-digit + numbers, the intermediate product can have up to 58 digits. If the result after scale adjustment + still exceeds 2^96 − 1, we must handle it. The current behavior should be documented: do we + raise an error, or do we round to fit? + +2. **The non-uniform 29th digit** should be documented. Users may expect that "29 digits of + precision" means they can represent any 29-digit number, but + `99,999,999,999,999,999,999,999,999,999` (29 nines) = ~10^29 is > 2^96 and therefore + out of range. The actual guarantee is "28 full digits plus a leading digit 0–7". + +3. **If we ever consider a Decimal256 or widen to full 128-bit coefficient:** We should evaluate + whether to switch to the decimal-bounded approach (10^38 − 1 with 128-bit storage, matching + Arrow/SQL) vs. staying with binary bound (2^128 − 1, giving ~38.5 digits with non-uniform + leading digit). The Arrow/SQL approach would give us exact compatibility with SQL Server + `decimal(38)` and Arrow `Decimal128` wire format, which is a significant interoperability + advantage. diff --git a/docs/plans/gmp_integration.md b/docs/plans/gmp_integration.md new file mode 100644 index 00000000..a0c32452 --- /dev/null +++ b/docs/plans/gmp_integration.md @@ -0,0 +1,1955 @@ +# 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 + +### 4.5 MPFR Memory Layout & Data Flow + +How data flows from Mojo `BigFloat` → C wrapper → MPFR library. + +#### 4.5.1 MPFR's `mpfr_t` Internal Structure (32 bytes on ARM64/x86_64) + +```txt +mpfr_t (typedef: __mpfr_struct[1]) +┌─────────────────────────────────────────────────────────────────┐ +│ offset field type meaning │ +├─────────────────────────────────────────────────────────────────┤ +│ 0 _mpfr_prec mpfr_prec_t precision in bits (≥2) │ +│ (8 bytes) (long) │ +├─────────────────────────────────────────────────────────────────┤ +│ 8 _mpfr_sign mpfr_sign_t +1 or −1 │ +│ (4 bytes) (int) │ +├─────────────────────────────────────────────────────────────────┤ +│ 12 (padding) 4 bytes align _mpfr_exp to 8-byte │ +│ boundary (long requires it) │ +├─────────────────────────────────────────────────────────────────┤ +│ 16 _mpfr_exp mpfr_exp_t binary exponent │ +│ (8 bytes) (long) (value = man × 2^exp) │ +├─────────────────────────────────────────────────────────────────┤ +│ 24 _mpfr_d mp_limb_t* → heap-allocated limb array │ +│ (8 bytes) (uint64_t*) (mantissa bits, MSB first) │ +└─────────────────────────────────────────────────────────────────┘ + Total: 32 bytes (struct) + heap limbs +``` + +**Limb Array Detail** + +The `_mpfr_d` pointer points to a heap-allocated array of 64-bit "limbs": + +```txt +_mpfr_d ────────┐ + ▼ + ┌────────────┬────────────┬────────┬────────────┐ + │ limb[0] │ limb[1] │ ... │ limb[n-1] │ + │ (64 bits) │ (64 bits) │ │ (64 bits) │ + └────────────┴────────────┴────────┴────────────┘ + MSB of mantissa ───────────────────────► LSB + n = ceil(precision / 64) + e.g. 200-bit precision → 4 limbs → 32 bytes of mantissa +``` + +**Value Representation** + +```txt +value = sign × mantissa × 2^(exponent − precision) + +Example: π at 200-bit precision + sign = +1 + prec = 200 + exp = 2 (so π ≈ mantissa × 2^(2−200)) + limbs = [0xC90FDAA2..., 0x2168C234..., 0xC4C6628B..., 0x80DC1CD1...] +``` + +#### 4.5.2 C Wrapper Handle Pool (`gmp_wrapper.c`) + +The C wrapper pre-allocates a flat array of 4096 "slots", each 64 bytes +(over-allocated from MPFR's 32-byte struct for ABI safety). + +```txt +handle_pool[4096][64] handle_in_use[4096] +┌──────────────────────────────────────────┐ ┌───────────────────┐ +│ slot[0] (64 bytes, 16-byte aligned) │ │ [0] = 1 (in use) │ +│ ┌──────┬──────┬──────┬──────┬──────────┐ │ │ │ +│ │ prec │ sign │ exp │ *d ──┼──→ heap │ │ │ │ +│ │ 8B │ 4B │ 8B │ 8B │ (unused) │ │ │ │ +│ └──────┴──────┴──────┴──────┴──────────┘ │ │ │ +├──────────────────────────────────────────┤ ├───────────────────┤ +│ slot[1] (64 bytes) │ │ [1] = 1 (in use) │ +│ ┌──────┬──────┬──────┬──────┬──────────┐ │ │ │ +│ │ prec │ sign │ exp │ *d ──┼──→ heap │ │ │ │ +│ └──────┴──────┴──────┴──────┴──────────┘ │ │ │ +├──────────────────────────────────────────┤ ├───────────────────┤ +│ slot[2] (64 bytes) │ │ [2] = 0 (free) │ +│ ┌──────────────────────────────────────┐ │ │ │ +│ │ (uninitialized / cleared) │ │ │ │ +│ └──────────────────────────────────────┘ │ │ │ +├──────────────────────────────────────────┤ ├───────────────────┤ +│ ... │ │ ... │ +├──────────────────────────────────────────┤ ├───────────────────┤ +│ slot[4095] │ │ [4095] = 0 (free) │ +└──────────────────────────────────────────┘ └───────────────────┘ + +Memory: _Alignas(16) char[4096][64] int[4096] + = 256 KB (static, pre-allocated) +``` + +#### 4.5.3 End-to-End Data Flows + +**Construction: `BigFloat("3.14", precision=100)`** + +```txt +Mojo (bigfloat.mojo) C wrapper (gmp_wrapper.c) MPFR library +───────────────────── ───────────────────────── ──────────── + +① _dps_to_bits(100) + = 100 × 4 + 64 + = 464 bits + +② mpfrw_init(464) ─────────→ ③ Find free slot (say i=0) + via external_call handle_in_use[0] = 1 + p_init2(&slot[0], 464) ──→ ④ mpfr_init2: + slot[0].prec = 464 + slot[0].sign = +1 + slot[0].exp = 0 + slot[0].*d = malloc(8 limbs) + ← return handle = 0 + +⑤ mpfrw_set_str(0, ⑥ Copy "3.14" to buf\0 + "3.14", 4) ─────────────→ p_set_str(&slot[0], ──→ ⑦ mpfr_set_str: + via external_call buf, 10, RNDN) parse "3.14" + set mantissa limbs + set exponent = 2 + ← return 0 (success) + +self.handle = 0 +self.precision = 100 +``` + +**Arithmetic: `a + b` (both are BigFloat)** + +```txt +Mojo C wrapper MPFR +──── ───────── ──── + +① mpfrw_init(464) ──────────→ alloc slot[2] + result handle = 2 handle_in_use[2] = 1 → mpfr_init2 + +② mpfrw_add(2, 0, 1) ──────→ ③ CHECK_HANDLES_3(2, 0, 1) + via external_call p_add(&slot[2], → ④ mpfr_add: + &slot[0], r.limbs = a.limbs + b.limbs + &slot[1], r.exp adjusted + RNDN) rounding applied + ← (void) + +⑤ return BigFloat( + _handle=2, + _precision=max(a,b)) +``` + +**String Export: `bf.to_string(30)`** + +```txt +Mojo C wrapper MPFR +──── ───────── ──── + +① mpfrw_get_str(0, 30) ────→ ② buf = malloc(94) + returns Int (raw address) p_snprintf(buf, 94, → ③ mpfr_snprintf: + "%.*Rg", 30, format mantissa limbs + &slot[0]) as decimal string + "3.14..." + ← return buf (raw address) + +④ _read_c_string(addr) + strlen(addr) → length + memcpy into List[Byte] + → String + +⑤ mpfrw_free_str(addr) ────→ ⑥ free(buf) +``` + +**Destruction: `__del__`** + +```txt +Mojo C wrapper MPFR +──── ───────── ──── + +① mpfrw_clear(0) ──────────→ ② p_clear(&slot[0]) → ③ mpfr_clear: + via external_call handle_in_use[0] = 0 free(slot[0].*d) + (limb array freed) + slot[0] bytes now stale + but slot is reusable +``` + +#### 4.5.4 BigFloat ↔ BigDecimal Conversion + +The two directions use different strategies: + +**BigFloat → BigDecimal** (`to_bigdecimal`): Uses `mpfr_get_str` to export raw +decimal digits and a base-10 exponent directly, then constructs the BigDecimal +via a single `memcpy` — no intermediate `String` or full `parse_numeric_string`. + +**BigDecimal → BigFloat**: Uses `BigDecimal.to_string()` → `mpfr_set_str` (the +standard string round-trip, since BigDecimal already stores base-10 digits). + +```txt +┌─────────────┐ mpfr_get_str (raw digits + exp) ┌──────────────┐ +│ BigFloat │ ─────────────────────────────────→ │ BigDecimal │ +│ (MPFR │ direct construction (memcpy) │ (coefficient │ +│ binary) │ │ + scale │ +│ │ ←───────────────────────────────── │ + sign) │ +│ │ .to_string() → mpfr_set_str │ │ +└─────────────┘ └──────────────┘ +``` + +Why asymmetric? The `to_bigdecimal` path avoids the overhead of formatting a +full decimal string and then re-parsing it. `mpfr_get_str` returns exactly the +significant digits needed, and the exponent is a separate integer, so BigDecimal +can be assembled directly. The reverse direction still goes through a string +because `mpfr_set_str` handles all the base-2 conversion internally. + +#### 4.5.5 Library Loading: dlopen Chain + +```txt + mpfrw_available() + │ + ▼ + ┌─── mpfrw_load() ────┐ + │ mpfr_load_attempted │ + │ == 0 ? │ + └────────┬────────────┘ + │ yes + ▼ + ┌── check DECIMO_NOGMP ──┐ + │ env var set? │ + └────────┬───────────────┘ + │ no + ▼ + ┌── dlopen("libmpfr.dylib") ───────────────────────┐ + │ try paths in order: │ + │ 1. libmpfr.dylib (rpath) │ + │ 2. /opt/homebrew/lib/libmpfr.dylib (macOS ARM) │ + │ 3. /usr/local/lib/libmpfr.dylib (macOS x86) │ + │ 4. libmpfr.so (Linux rpath) │ + │ 5. /usr/lib/.../libmpfr.so (Debian) │ + └────────────────┬─────────────────────────────────┘ + │ success + ▼ + ┌── dlsym for each function pointer ──┐ + │ p_init2 = dlsym("mpfr_init2") │ + │ p_clear = dlsym("mpfr_clear") │ + │ p_set_str = dlsym("mpfr_set_str") │ + │ p_add = dlsym("mpfr_add") │ + │ p_sqrt = dlsym("mpfr_sqrt") │ + │ p_snprintf = dlsym("mpfr_snprintf") │ + │ ... (21 function pointers total) │ + └─────────────────────────────────────┘ + │ + ▼ + ┌── verify critical pointers ──┐ + │ if any of init2, clear, │ + │ set_str, get_str, add, sub, │ + │ mul, div, neg, abs, cmp │ + │ is NULL → fail, dlclose │ + └──────────────┬───────────────┘ + │ all OK + ▼ + return 1 (available) +``` + +The result is cached: `mpfr_load_attempted = 1`. Subsequent calls skip all +of the above and return immediately. + +## 5. Performance Comparison: GMP vs Decimo + +All benchmarks run side-by-side in a single binary (`bench_gmp_vs_decimo.mojo`), compiled +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 | + +### 10.5 End-User Build & Run Guide (BigFloat) + +BigFloat requires linking the C wrapper at build time and having MPFR available at +runtime. The steps differ from pure-Mojo types (BigDecimal, BigInt, etc.) which work +with a bare `mojo run`. + +#### Prerequisites + +```bash +# macOS +brew install mpfr + +# Linux (Debian/Ubuntu) +sudo apt install libmpfr-dev +``` + +#### Step 1: Build the C wrapper + +```bash +# From the decimo source directory +bash src/decimo/gmp/build_gmp_wrapper.sh +# Produces: src/decimo/gmp/libdecimo_gmp_wrapper.dylib (macOS) +# or: src/decimo/gmp/libdecimo_gmp_wrapper.so (Linux) +``` + +The wrapper compiles with any C compiler — no MPFR headers needed at compile time +(it uses `dlopen`/`dlsym` internally). + +#### Step 2: Build your Mojo binary + +`mojo run` cannot link external libraries (`-Xlinker` flags are silently ignored in +JIT mode). You must use `mojo build`: + +```bash +# macOS +mojo build -I src \ + -Xlinker -L./src/decimo/gmp -Xlinker -ldecimo_gmp_wrapper \ + -o myprogram myprogram.mojo + +# Linux (same command) +mojo build -I src \ + -Xlinker -L./src/decimo/gmp -Xlinker -ldecimo_gmp_wrapper \ + -o myprogram myprogram.mojo +``` + +#### Step 3: Run with library path + +The OS needs to find the `.dylib`/`.so` at runtime: + +```bash +# macOS +DYLD_LIBRARY_PATH=./src/decimo/gmp ./myprogram + +# Linux +LD_LIBRARY_PATH=./src/decimo/gmp ./myprogram +``` + +#### All-in-one example + +```bash +bash src/decimo/gmp/build_gmp_wrapper.sh \ +&& 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 +``` + +#### Comparison with pure-Mojo types + +| Type | Build command | Extra dependencies | Library path needed? | +| ------------ | ----------------------------------------------- | ------------------ | -------------------- | +| BigDecimal | `mojo run -I src myprogram.mojo` | None | No | +| BigInt | `mojo run -I src myprogram.mojo` | None | No | +| Dec128 | `mojo run -I src myprogram.mojo` | None | No | +| **BigFloat** | `mojo build -I src -Xlinker ... myprogram.mojo` | MPFR + C wrapper | **Yes** | + +When Mojo gains native `DLHandle` support, the C wrapper can be eliminated and BigFloat +will work with `mojo run` like the other types. + +## 11. Cross-Platform Considerations + +### 11.1 Platform Matrix + +| 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 0: Prerequisites + +- [x] **0.1 Install MPFR** + `brew install mpfr` (macOS). Verify: `/opt/homebrew/lib/libmpfr.dylib` exists, + `#include ` compiles. MPFR depends on GMP (already installed). + +### Phase 1: Foundation — MPFR C Wrapper & BigFloat + +**Goal**: Get the MPFR-based C wrapper working, build the `BigFloat` struct as a +first-class user-facing type, and prove the pipeline with `sqrt`. + +- [x] **1.1 Extend C wrapper with `mpfr_t` handle pool** + Add to `gmp_wrapper.c`: + (a) `static mpfr_t f_handles[MAX_HANDLES]` pool, + (b) `mpfrw_init(prec_bits) → handle`, + (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. + *Done: MAX_HANDLES=4096, MAX_PREC_BITS=1048576.* +- [x] **1.2 Add MPFR arithmetic wrappers** (Small, deps: 1.1) + One-line C wrappers: `mpfrw_add(r,a,b)`, `mpfrw_sub(r,a,b)`, `mpfrw_mul(r,a,b)`, + `mpfrw_div(r,a,b)`, `mpfrw_sqrt(r,a)`, `mpfrw_neg(r,a)`, `mpfrw_abs(r,a)`, + `mpfrw_cmp(a,b)`. All use `MPFR_RNDN` (round-to-nearest). Each is 1 line. +- [x] **1.3 Add MPFR transcendental wrappers** (Small, deps: 1.1) + `mpfrw_exp(r,a)`, `mpfrw_log(r,a)`, `mpfrw_sin(r,a)`, `mpfrw_cos(r,a)`, + `mpfrw_tan(r,a)`, `mpfrw_const_pi(r)`, `mpfrw_pow(r,a,b)`, `mpfrw_rootn_ui(r,a,n)`. + Each is 1 line. +- [x] **1.4 Implement lazy `dlopen` loading** (Medium, deps: 1.2, 1.3) + Wrap all MPFR calls behind function pointers resolved via `dlopen`/`dlsym` at + first use (see Section 10.4). This makes the wrapper compilable without MPFR + headers. Add `mpfrw_available() → 0/1`. + **Add `DECIMO_NOGMP` check**: if set, `mpfrw_available()` returns 0 without + attempting `dlopen` (inspired by mpmath's `MPMATH_NOGMPY`). + *Done: 8 library search paths (macOS + Linux), DECIMO_NOGMP implemented.* +- [x] **1.5 Compile wrapper, run smoke test** (Small, deps: 1.4) + Build `libdecimo_gmp_wrapper.dylib` linking against `-lmpfr -lgmp`. Write minimal + Mojo test: `mpfrw_init(200) → set_str("3.14") → mpfrw_sqrt → get_str → print`. + Verify output. + *Done: wrapper compiles without -lmpfr (dlopen). 14 smoke tests pass.* +- [x] **1.6 Create `src/decimo/bigfloat/bigfloat.mojo`** (Medium, deps: 1.5) + Implement `BigFloat` struct (single `handle: Int32` field). Constructor from + string via `mpfrw_set_str`. Constructor from BigDecimal via `str(bd) → mpfrw_set_str`. + `to_bigdecimal(precision) → mpfrw_get_str → BigDecimal(s)`. Destructor calls + `mpfrw_clear`. Use guard bits: init MPFR with `dps_to_prec(precision) + 20` bits. + *Done: ~320 lines. Also constructors from Int. Aliases: BFlt, Float.* +- [x] **1.7 BigFloat arithmetic and transcendentals** (Medium, deps: 1.6) + Wire all MPFR wrappers into BigFloat methods: `sqrt()`, `exp()`, `ln()`, `sin()`, + `cos()`, `tan()`, `root(n)`, `power(y)`, `+`, `-`, `*`, `/`, `pi()`. + Each is a thin wrapper around the corresponding `mpfrw_*` call. + *Done: all listed + `__neg__`, `__abs__`, comparisons (`__eq__`, `__ne__`, `__lt__`, `__le__`, `__gt__`, `__ge__`).* +- [x] **1.8 BigFloat ↔ BigDecimal conversion** (Small, deps: 1.6) + `BigFloat(bd: BigDecimal, precision)` and `bf.to_bigdecimal(precision)`. + Both go through string representation (`mpfr_set_str` / `mpfr_get_str`). +- [ ] **1.9 Test BigFloat correctness** (Medium, deps: 1.7) + For each operation × x ∈ {2, 3, 0.5, 100, 10⁻⁸, 10¹⁰⁰⁰} × P ∈ {28, 100, 500, + 1000, 5000}: verify BigFloat results match BigDecimal results to P digits. + Edge cases: 0, negative, very large/small. +- [ ] **1.10 Benchmark BigFloat** (Small, deps: 1.9) + Measure wall time for BigFloat vs BigDecimal across operations and precisions. + Find break-even precision where BigFloat wins. Expected: above ~50–100 digits. +- [x] **1.11 Update build system** (Small, deps: 1.1) + `pixi.toml` tasks: `build-gmp-wrapper` (compiles C wrapper + links MPFR). + `build_gmp_wrapper.sh` for macOS + Linux. + *Done: pixi tasks (buildgmp, testfloat, tf), test_bigfloat.sh, CI job in run_tests.yaml.* + +**Deliverable**: `BigFloat("2", 5000).sqrt()` works. Users can construct BigFloat, +chain operations, and convert back to BigDecimal. + +### 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 + +- [x] **4.1 Cross-platform wrapper compilation (Linux)** (Small, deps: Phase 1) + *Done: build_gmp_wrapper.sh handles Linux (.so, -fPIC, -ldl). CI verified on ubuntu-22.04.* +- [ ] **4.2 Pre-built wrapper binaries (macOS ARM64, Linux x86)** (Medium, deps: 4.1) +- [ ] **4.3 CLI `--mode float` and `--gmp` flags** (Medium, deps: Phase 2) + `--mode float` evaluates in BigFloat; `--gmp` enables `gmp=True` on BigDecimal ops. +- [ ] **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** — 11 steps, Medium complexity (most work is C wrapper + BigFloat struct). **HIGH** priority. +- **Phase 2: BigDecimal gmp=True sugar** — 7 steps, **Low** complexity (each op delegates to BigFloat). **HIGH** priority. +- **Phase 3: BigInt backend** — 4 steps, Medium complexity. Deferred. +- **Phase 4: Polish + lazy eval** — 8 steps, **1/8 done** (4.1 Linux build). 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/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_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..588d600c 100644 --- a/docs/readme_unreleased.md +++ b/docs/readme_unreleased.md @@ -2,17 +2,19 @@ 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) [![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) ```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/user_manual_cli.md b/docs/user_manual_cli.md index 3d3467be..3d02a966 100644 --- a/docs/user_manual_cli.md +++ b/docs/user_manual_cli.md @@ -12,15 +12,22 @@ - [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) - [Using noglob](#using-noglob) + - [Shell Completions](#shell-completions) +- [Performance](#performance) - [Examples](#examples) - [Basic Arithmetic](#basic-arithmetic) - [High-Precision Calculations](#high-precision-calculations) @@ -65,15 +72,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" @@ -87,8 +94,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. See [Negative Expressions](#negative-expressions) for details. + ### Operators | Operator | Description | Example | Result | @@ -118,7 +128,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:** @@ -160,67 +170,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). @@ -235,12 +245,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 @@ -255,6 +340,50 @@ 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 +``` + +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 +# Euler's number, negated +decimo "-e" +# → -2.71828… + +# Negative sine +decimo "-sin(1)" +# → -0.84147… + +# Negative pi +decimo "-pi" +# → -3.14159… +``` + +The `--` separator still works if you prefer explicit positional parsing: + +```bash +decimo -- "-e" +``` + ### Using noglob On zsh, you can use `noglob` to prevent shell interpretation: @@ -273,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 @@ -292,34 +475,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 @@ -330,19 +513,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 ``` @@ -350,11 +533,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 @@ -393,30 +576,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/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.lock b/pixi.lock index b79dca34..5843ec35 100644 --- a/pixi.lock +++ b/pixi.lock @@ -12,6 +12,7 @@ environments: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://repo.prefix.dev/modular-community/linux-64/argmojo-0.5.0-hb0f4dca_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.3.0-py313h18e8e13_0.conda @@ -107,6 +108,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://repo.prefix.dev/modular-community/osx-arm64/argmojo-0.5.0-h60d57d3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.3.0-py313h48bb75e_0.conda @@ -212,6 +214,22 @@ packages: license_family: MIT size: 8191 timestamp: 1744137672556 +- conda: https://repo.prefix.dev/modular-community/linux-64/argmojo-0.5.0-hb0f4dca_0.conda + sha256: c1342a434a1150358ffbf150cb05eee5600f4df1755d81a7386b3c891229d7a8 + md5: bbeb104e87b1eeec44a39eda4d1f22ef + depends: + - mojo-compiler 0.26.2.* + license: Apache-2.0 + size: 2163429 + timestamp: 1775323629145 +- conda: https://repo.prefix.dev/modular-community/osx-arm64/argmojo-0.5.0-h60d57d3_0.conda + sha256: 8a6f43cc045c97ef335dca504cffe0b4b91d199d41f8f8e4deae79a801b4b53d + md5: f0a0c5c25cc6bb8b692176544b9b4000 + depends: + - mojo-compiler 0.26.2.* + license: Apache-2.0 + size: 2161961 + timestamp: 1775323591456 - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda sha256: e1c3dc8b5aa6e12145423fed262b4754d70fec601339896b9ccf483178f690a6 md5: 767d508c1a67e02ae8f50e44cacfadb2 diff --git a/pixi.toml b/pixi.toml index bdcf8d42..1929e948 100644 --- a/pixi.toml +++ b/pixi.toml @@ -6,10 +6,11 @@ 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) +argmojo = ">=0.5.0,<0.6.0" # CLI argument parsing for the Decimo calculator mojo = ">=0.26.2.0,<0.26.3" # Mojo language compiler and runtime python = ">=3.13" # For Python bindings and tests python-build = ">=0.2.0" # Build PyPI wheel (`pixi run wheel`) @@ -26,29 +27,37 @@ format = """pixi run mojo format ./src \ &&pixi run ruff format ./python""" # doc -doc = """pixi run mojo doc \ ---diagnose-missing-doc-strings --validate-doc-strings src/decimo""" +doc = "pixi run mojo doc --diagnose-missing-doc-strings src/decimo > /dev/null" # compile the package p = "clear && pixi run package" package = """pixi run format \ -&& pixi run package_decimo""" +&&pixi run doc \ +&&pixi run package_decimo""" package_decimo = """pixi run mojo package src/decimo \ -&& cp decimo.mojopkg tests/ \ -&& cp decimo.mojopkg benches/ \ -&& rm decimo.mojopkg""" +&&cp decimo.mojopkg tests/ \ +&&cp decimo.mojopkg benches/ \ +&&rm decimo.mojopkg""" # clean the package files in tests folder c = "clear && pixi run clean" clean = """rm tests/decimo.mojopkg && \ rm benches/decimo.mojopkg""" +# gmp/mpfr wrapper (compile C wrapper — no MPFR needed at build time) +bgmp = "clear && pixi run buildgmp" +buildgmp = "bash src/decimo/gmp/build_gmp_wrapper.sh" + # tests (use the mojo testing tool) -t = "clear && pixi run test" -tdecimo = "clear && pixi run testdecimo" -test = "pixi run package && bash ./tests/test_all.sh" +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) +testfloat = "pixi run buildgmp && bash ./tests/test_bigfloat.sh" +# shortcut for tests +t = "clear && pixi run test" +tdecimo = "clear && pixi run testdecimo" +tf = "clear && pixi run testfloat" ttoml = "clear && pixi run testtoml" # bench @@ -56,28 +65,33 @@ bench = "pixi run package && bash benches/run_bench.sh" # bench with debug assertions enabled bdec_debug = """clear && pixi run package && cd benches/bigdecimal \ -&& pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ -&& pixi run clean""" +&&pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ +&&pixi run clean""" bint_debug = """clear && pixi run package && cd benches/bigint \ -&& pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ -&& pixi run clean""" +&&pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ +&&pixi run clean""" buint_debug = """clear && pixi run package && cd benches/biguint \ -&& pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ -&& pixi run clean""" +&&pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ +&&pixi run clean""" dec_debug = """clear && pixi run package && cd benches/decimal128 \ -&& pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ -&& pixi run clean""" +&&pixi run mojo run -I ../ -D ASSERT=all bench.mojo && cd ../.. \ +&&pixi run clean""" # 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 24ca712fa291ec6a82dc9317f6ba5d0484d1fb47 2>/dev/null""" +&& cd temp/argmojo && git checkout 29b6f54545f850e19d9a9ccfd1185d87f54e92b2 2>/dev/null""" # cli calculator bcli = "clear && pixi run buildcli" -buildcli = """pixi run mojo package temp/argmojo/src/argmojo -o temp/argmojo.mojopkg \ -&& pixi run mojo build -I src -I src/cli -I temp -o decimo src/cli/main.mojo""" +# Uncomment the following lines if we build the CLI package with +# local clone of argmojo +# buildcli = """pixi run mojo package temp/argmojo/src/argmojo -o temp/argmojo.mojopkg \ +# && 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" @@ -93,7 +107,6 @@ all = """pixi run format \ &&pixi run package \ &&pixi run doc \ &&pixi run test \ -&&pixi run fetch \ &&pixi run buildcli \ &&pixi run testcli \ &&pixi run buildpy \ 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/cli/calculator/__init__.mojo b/src/cli/calculator/__init__.mojo index 3e8a023a..0b8b0a01 100644 --- a/src/cli/calculator/__init__.mojo +++ b/src/cli/calculator/__init__.mojo @@ -41,7 +41,28 @@ from .tokenizer import ( TOKEN_FUNC, TOKEN_CONST, TOKEN_COMMA, + TOKEN_VARIABLE, ) 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, + 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, + read_line, + read_stdin, + split_into_lines, + strip_comment, + is_blank, + is_comment_or_blank, + strip, + filter_expression_lines, + 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..5100af03 --- /dev/null +++ b/src/cli/calculator/engine.mojo @@ -0,0 +1,197 @@ +# ===----------------------------------------------------------------------=== # +# 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 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 +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, + variables: Dict[String, Decimal] = Dict[String, Decimal](), +) 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. + variables: A name→value mapping of user-defined variables. + """ + 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)) + 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) + + +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/io.mojo b/src/cli/calculator/io.mojo new file mode 100644 index 00000000..3b8b0a29 --- /dev/null +++ b/src/cli/calculator/io.mojo @@ -0,0 +1,333 @@ +# ===----------------------------------------------------------------------=== # +# 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_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. + 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/calculator/parser.mojo b/src/cli/calculator/parser.mojo index ffa4c910..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]: - """Convert 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 new file mode 100644 index 00000000..27a641d4 --- /dev/null +++ b/src/cli/calculator/repl.mojo @@ -0,0 +1,263 @@ +# ===----------------------------------------------------------------------=== # +# 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. + +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. + +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 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( + 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). + + 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> ") + + 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 + + # 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( + precision: Int, + scientific: Bool, + engineering: Bool, + pad: Bool, + delimiter: String, + rounding_mode: RoundingMode, +): + """Prints the REPL welcome banner to 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) + "." + 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..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 # ===----------------------------------------------------------------------=== # @@ -51,7 +55,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 @@ -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/cli/main.mojo b/src/cli/main.mojo index c49bf4ca..3d666ba5 100644 --- a/src/cli/main.mojo +++ b/src/cli/main.mojo @@ -5,18 +5,103 @@ # 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 -from argmojo import Arg, Command +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, + split_into_lines, + filter_expression_lines, + read_file_text, +) +from calculator.repl import run_repl + + +struct DecimoArgs(Parsable): + var expr: Positional[ + String, + 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", + 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", + help="Pad trailing zeros to the specified precision", + group="Formatting", + ] + var delimiter: Option[ + String, + long="delimiter", + 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, + long="rounding-mode", + short="R", + 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 + def description() -> String: + return "Arbitrary-precision CLI calculator powered by Decimo." + + @staticmethod + def version() -> String: + return "0.1.0" + + @staticmethod + def name() -> String: + return "decimo" def main(): @@ -31,210 +116,177 @@ def main(): def _run() raises: - var cmd = Command( - "decimo", - ( - "Arbitrary-precision CLI calculator powered by Decimo.\n" - "\n" - "Note: if your expression contains *, ( or ), your shell may\n" - "intercept them before decimo runs. Use quotes or noglob:\n" - ' decimo "2 * (3 + 4)" # with quotes\n' - " noglob decimo 2*(3+4) # with noglob\n" - " alias decimo='noglob decimo' # add to ~/.zshrc" - ), - version="0.1.0", - ) - - # Positional: the math expression - cmd.add_argument( - Arg( - "expr", - help=( - "Math expression to evaluate (e.g. 'sqrt(abs(1.1*-12-23/17))')" - ), - ) - .positional() - .required() - ) - - # Named option: number of significant digits - cmd.add_argument( - Arg("precision", help="Number of significant digits (default: 50)") - .long["precision"]() - .short["p"]() - .default["50"]() - ) - - # Output formatting flags - # Mutually exclusive: scientific, engineering - cmd.add_argument( - Arg("scientific", help="Output in scientific notation (e.g. 1.23E+10)") - .long["scientific"]() - .short["s"]() - .flag() + var cmd = DecimoArgs.to_command() + 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. + 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)"' ) - cmd.add_argument( - Arg( - "engineering", - help="Output in engineering notation (exponent multiple of 3)", + 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 precision = args.precision.value + var scientific = args.scientific.value + var engineering = args.engineering.value + var pad = args.pad.value + var delimiter = args.delimiter.value + var rounding_mode = _parse_rounding_mode(args.rounding_mode.value) + + # ── 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 → interactive REPL + + 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, ) - .long["engineering"]() - .short["e"]() - .flag() - ) - cmd.mutually_exclusive(["scientific", "engineering"]) - cmd.add_argument( - Arg( - "pad", - help="Pad trailing zeros to the specified precision", + 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, ) - .long["pad"]() - .short["P"]() - .flag() - ) - cmd.add_argument( - Arg( - "delimiter", - help=( - "Digit-group separator inserted every 3 digits" - " (e.g. '_' gives 1_234.567_89)" - ), + else: + # ── REPL mode ─────────────────────────────────────────────────── + # No expression, no file, no pipe — launch interactive session. + run_repl( + precision, + scientific, + engineering, + pad, + delimiter, + rounding_mode, ) - .long["delimiter"]() - .short["d"]() - .default[""]() - ) - # Rounding mode for the final result - cmd.add_argument( - Arg( - "rounding-mode", - help="Rounding mode for the final result (default: half-even)", - ) - .long["rounding-mode"]() - .short["r"]() - .choice["half-even"]() - .choice["half-up"]() - .choice["half-down"]() - .choice["up"]() - .choice["down"]() - .choice["ceiling"]() - .choice["floor"]() - .default["half-even"]() - ) - var result = cmd.parse() - var expr = result.get_string("expr") - var precision = result.get_int("precision") - var scientific = result.get_flag("scientific") - var engineering = result.get_flag("engineering") - var pad = result.get_flag("pad") - var delimiter = result.get_string("delimiter") - var rounding_mode = _parse_rounding_mode(result.get_string("rounding-mode")) +# ===----------------------------------------------------------------------=== # +# Mode implementations +# ===----------------------------------------------------------------------=== # - # ── Phase 1: Tokenize & parse ────────────────────────────────────────── - 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. +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: - var value = final_round( - evaluate_rpn(rpn^, precision), precision, rounding_mode + 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 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: - _display_calc_error(String(eval_err), expr) - exit(1) - - except parse_err: - _display_calc_error(String(parse_err), expr) + if had_error: exit(1) -def _display_calc_error(error_msg: String, expr: String): - """Parse a calculator error message and display 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: - """Pad (or add) 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 +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 - if dot_pos < 0: - # No decimal point — add one with `precision` zeros - return plain + "." + "0" * precision + var expressions = filter_expression_lines(split_into_lines(text)) + var had_error = False - var frac_len = len(plain) - dot_pos - 1 - if frac_len >= precision: - return plain + 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 - return plain + "0" * (precision - frac_len) + if had_error: + exit(1) 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/src/decimo/__init__.mojo b/src/decimo/__init__.mojo index c906185a..5ecf9d1e 100644 --- a/src/decimo/__init__.mojo +++ b/src/decimo/__init__.mojo @@ -26,9 +26,10 @@ 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 from .rounding_mode import ( RoundingMode, ROUND_DOWN, diff --git a/src/decimo/bigdecimal/arithmetics.mojo b/src/decimo/bigdecimal/arithmetics.mojo index 1cb61b88..41d9c553 100644 --- a/src/decimo/bigdecimal/arithmetics.mojo +++ b/src/decimo/bigdecimal/arithmetics.mojo @@ -20,6 +20,7 @@ Implements functions for mathematical operations on BigDecimal objects. from std import math +from decimo.errors import ZeroDivisionError from decimo.rounding_mode import RoundingMode # ===----------------------------------------------------------------------=== # @@ -307,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 @@ -319,7 +323,10 @@ def true_divide( """ # Check for division by zero if y.coefficient.is_zero(): - raise Error("bigdecimal.arithmetics.true_divide(): Division by zero") + raise ZeroDivisionError( + message="Division by zero.", + function="true_divide()", + ) # Handle dividend of zero if x.coefficient.is_zero(): @@ -688,11 +695,17 @@ def true_divide_inexact( Returns: The quotient of x1 and x2. + + Raises: + ZeroDivisionError: If the divisor is zero. """ # Check for division by zero if x2.coefficient.is_zero(): - raise Error("Division by zero") + raise ZeroDivisionError( + message="Division by zero.", + function="true_divide_inexact()", + ) # Handle dividend of zero if x1.coefficient.is_zero(): @@ -897,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. @@ -905,7 +918,10 @@ def truncate_divide(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: """ # Check for division by zero if x2.coefficient.is_zero(): - raise Error("Division by zero") + raise ZeroDivisionError( + message="Division by zero.", + function="truncate_divide()", + ) # Handle dividend of zero if x1.coefficient.is_zero(): @@ -941,11 +957,14 @@ 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(): - raise Error("Division by zero") + raise ZeroDivisionError( + message="Division by zero.", + function="truncate_modulo()", + ) return subtract( x1, diff --git a/src/decimo/bigdecimal/bigdecimal.mojo b/src/decimo/bigdecimal/bigdecimal.mojo index 9213be85..83fd6222 100644 --- a/src/decimo/bigdecimal/bigdecimal.mojo +++ b/src/decimo/bigdecimal/bigdecimal.mojo @@ -27,16 +27,43 @@ from std.memory import UnsafePointer from std.python import PythonObject from std import testing -from decimo.errors import DecimoError +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 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, @@ -107,33 +156,54 @@ struct BigDecimal( @implicit def __init__(out self, coefficient: BigUInt): - """Constructs a BigDecimal from a BigUInt object.""" + """Constructs a BigDecimal from a BigUInt object. + + Args: + coefficient: The unsigned integer coefficient. + """ self.coefficient = coefficient.copy() self.scale = 0 self.sign = False def __init__(out self, coefficient: BigUInt, scale: Int, sign: Bool): - """Constructs a BigDecimal from its components.""" + """Constructs a BigDecimal from its components. + + Args: + coefficient: The unsigned integer coefficient. + scale: The number of decimal places (power of 10 divisor). + sign: Whether the value is negative. + """ self.coefficient = coefficient.copy() self.scale = scale self.sign = sign @implicit def __init__(out self, value: BigInt10): - """Constructs a BigDecimal from a big integer.""" + """Constructs a BigDecimal from a big integer. + + Args: + value: The `BigInt10` to convert. + """ self.coefficient = value.magnitude.copy() self.scale = 0 self.sign = value.sign def __init__(out self, value: String) raises: - """Constructs a BigDecimal from a string representation.""" - # The string is normalized with `deciomojo.str.parse_numeric_string()`. + """Constructs a BigDecimal from a string representation. + + Args: + value: The string to parse (e.g. "123.456" or "1.23E+5"). + """ + # The string is normalized with `decimo.str.parse_numeric_string()`. self = Self.from_string(value) @implicit def __init__(out self, value: Int): """Constructs a BigDecimal from an `Int` object. See `from_int()` for more information. + + Args: + value: The integer to convert. """ self = Self.from_int(value) @@ -141,6 +211,9 @@ struct BigDecimal( def __init__(out self, value: UInt): """Constructs a BigDecimal from an `UInt` object. See `from_uint()` for more information. + + Args: + value: The unsigned integer to convert. """ self = Self.from_uint(value) @@ -151,6 +224,12 @@ struct BigDecimal( Constraints: The dtype of the scalar must be integral. + + Parameters: + dtype: The data type of the scalar. + + Args: + value: The integral scalar to convert. """ comptime assert dtype.is_integral(), ( "\n***********************************************************\n" @@ -158,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***********************************************************" ) @@ -166,7 +245,11 @@ struct BigDecimal( self = Self.from_integral_scalar(value) def __init__(out self, *, py: PythonObject) raises: - """Constructs a BigDecimal from a Python Decimal object.""" + """Constructs a BigDecimal from a Python Decimal object. + + Args: + py: A Python `decimal.Decimal` object. + """ self = Self.from_python_decimal(py) # ===------------------------------------------------------------------=== # @@ -206,12 +289,27 @@ struct BigDecimal( ) -> Self: """**UNSAFE** Creates a BigDecimal from its raw components. The raw components are a single word, scale, and sign. + + Args: + word: The single raw UInt32 word for the coefficient. + scale: The number of decimal places. + sign: Whether the value is negative. + + Returns: + A `BigDecimal` constructed from the raw components. """ return Self(BigUInt(raw_words=[word]), scale, sign) @staticmethod def from_int(value: Int) -> Self: - """Creates a BigDecimal from an integer.""" + """Creates a BigDecimal from an integer. + + Args: + value: The integer to convert. + + Returns: + A `BigDecimal` representing the given integer. + """ if value == 0: return Self(coefficient=BigUInt.zero(), scale=0, sign=False) @@ -246,7 +344,14 @@ struct BigDecimal( # TODO: This method is no longer needed as UInt is now an alias for SIMD. @staticmethod def from_uint(value: UInt) -> Self: - """Creates a BigDecimal from an unsigned integer.""" + """Creates a BigDecimal from an unsigned integer. + + Args: + value: The unsigned integer to convert. + + Returns: + A `BigDecimal` representing the given unsigned integer. + """ return Self( coefficient=BigUInt.from_unsigned_integral_scalar(value), scale=0, @@ -263,6 +368,9 @@ struct BigDecimal( Returns: The BigDecimal representation of the Scalar value. + + Parameters: + dtype: The data type of the scalar. """ comptime assert dtype.is_integral(), "dtype must be integral." @@ -286,10 +394,17 @@ 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 with full precision before converting to BigDecimal. + + Parameters: + dtype: The data type of the scalar. """ comptime assert ( @@ -300,20 +415,24 @@ struct BigDecimal( return Self(coefficient=BigUInt.zero(), scale=0, sign=False) if value != value: # Check for NaN - raise Error("`from_scalar()`: Cannot convert NaN to BigUInt") + 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( - "`from_scalar()`: Cannot get decimal from string\nTrace back: " - + String(e), + raise ConversionError( + message="Cannot convert scalar to BigDecimal.", + function="BigDecimal.from_scalar()", + previous_error=e^, ) @staticmethod def from_string(value: String) raises -> Self: """Initializes a BigDecimal from a string representation. - The string is normalized with `deciomojo.str.parse_numeric_string()`. + The string is normalized with `decimo.str.parse_numeric_string()`. Args: value: The string representation of the BigDecimal. @@ -364,7 +483,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: @@ -474,14 +593,10 @@ struct BigDecimal( return Self(coefficient=coefficient^, scale=scale, sign=sign) except e: - raise Error( - DecimoError( - file="src/decimo/bigdecimal/bigdecimal.mojo", - function="from_python_decimal()", - message="Failed to convert Python Decimal to BigDecimal: " - + "as_tuple() returned invalid data or conversion failed.", - previous_error=e^, - ), + raise ConversionError( + message="Failed to convert Python Decimal to BigDecimal.", + function="BigDecimal.from_python_decimal()", + previous_error=e^, ) # ===------------------------------------------------------------------=== # @@ -491,11 +606,19 @@ struct BigDecimal( # ===------------------------------------------------------------------=== # def __int__(self) raises -> Int: - """Converts the BigDecimal to an integer.""" + """Converts the BigDecimal to an integer. + + Returns: + The integer representation, truncating any fractional part. + """ return Int(self.to_string()) def __float__(self) raises -> Float64: - """Converts the BigDecimal to a floating-point number.""" + """Converts the BigDecimal to a floating-point number. + + Returns: + The `Float64` representation of this value. + """ return Float64(self.to_string()) # ===------------------------------------------------------------------=== # @@ -512,20 +635,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: @@ -536,13 +659,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. @@ -700,11 +823,24 @@ struct BigDecimal( def write_to[W: Writer](self, mut writer: W): """Writes the BigDecimal to a writer. This implement the `write` method of the `Writer` trait. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. """ writer.write(self.to_string()) def write_repr_to[W: Writer](self, mut writer: W): - """Writes the debug representation to a writer.""" + """Writes the debug representation to a writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ writer.write('BigDecimal("', self.to_string(), '")') def to_scientific_string(self) -> String: @@ -718,6 +854,9 @@ struct BigDecimal( BigDecimal("123456.789").to_scientific_string() # "1.23456789E+5" BigDecimal("0.00123").to_scientific_string() # "1.23E-3" ``` + + Returns: + The number formatted in scientific notation. """ return self.to_string(scientific=True) @@ -733,6 +872,9 @@ struct BigDecimal( BigDecimal("123456.789").to_eng_string() # "123.456789E+3" BigDecimal("0.00123").to_eng_string() # "1.23E-3" ``` + + Returns: + The number formatted in engineering notation. """ return self.to_string(engineering=True) @@ -753,6 +895,9 @@ struct BigDecimal( BigDecimal("-9876543210.123456").to_string_with_separators() # "-9_876_543_210.123_456" ``` + + Returns: + The number formatted with digit-group separators. """ return self.to_string(delimiter=separator) @@ -765,6 +910,9 @@ struct BigDecimal( def __abs__(self) -> Self: """Returns the absolute value of this number. See `absolute()` for more information. + + Returns: + The absolute value. """ return Self( coefficient=self.coefficient, @@ -776,6 +924,9 @@ struct BigDecimal( def __neg__(self) -> Self: """Returns the negation of this number. See `negative()` for more information. + + Returns: + The negated value. """ return Self( coefficient=self.coefficient, @@ -788,6 +939,9 @@ struct BigDecimal( """Returns True if the number is nonzero. This enables `if x:` syntax, consistent with Python's `decimal.Decimal`. + + Returns: + True if the number is nonzero, False otherwise. """ return not self.coefficient.is_zero() @@ -797,6 +951,9 @@ struct BigDecimal( This enables `+x` syntax, consistent with Python's `decimal.Decimal`. In Python, unary plus applies context rounding. Here it returns a copy. + + Returns: + A copy of this number. """ return self.copy() @@ -808,18 +965,50 @@ struct BigDecimal( @always_inline def __add__(self, other: Self) raises -> Self: + """Adds two values. + + Args: + other: The right-hand side operand. + + Returns: + The sum of the two values. + """ return decimo.bigdecimal.arithmetics.add(self, other) @always_inline def __sub__(self, other: Self) raises -> Self: + """Subtracts two values. + + Args: + other: The right-hand side operand. + + Returns: + The difference of the two values. + """ return decimo.bigdecimal.arithmetics.subtract(self, other) @always_inline def __mul__(self, other: Self) -> Self: + """Multiplies two values. + + Args: + other: The right-hand side operand. + + Returns: + The product of the two values. + """ return decimo.bigdecimal.arithmetics.multiply(self, other) @always_inline def __truediv__(self, other: Self) raises -> Self: + """Divides two values using true division. + + Args: + other: The right-hand side operand. + + Returns: + The quotient of the two values. + """ return decimo.bigdecimal.arithmetics.true_divide( self, other, precision=PRECISION ) @@ -828,6 +1017,12 @@ struct BigDecimal( def __floordiv__(self, other: Self) raises -> Self: """Returns the result of floor division. See `arithmetics.truncate_divide()` for more information. + + Args: + other: The right-hand side operand. + + Returns: + The integer quotient, truncated toward zero. """ return decimo.bigdecimal.arithmetics.truncate_divide(self, other) @@ -835,6 +1030,12 @@ struct BigDecimal( def __mod__(self, other: Self) raises -> Self: """Returns the result of modulo operation. See `arithmetics.truncate_modulo()` for more information. + + Args: + other: The right-hand side operand. + + Returns: + The remainder after truncated division. """ return decimo.bigdecimal.arithmetics.truncate_modulo( self, other, precision=PRECISION @@ -842,7 +1043,14 @@ struct BigDecimal( @always_inline def __pow__(self, exponent: Self) raises -> Self: - """Returns the result of exponentiation.""" + """Returns the result of exponentiation. + + Args: + exponent: The power to raise this value to. + + Returns: + This value raised to the given exponent. + """ return decimo.bigdecimal.exponential.power( self, exponent, precision=PRECISION ) @@ -857,6 +1065,12 @@ struct BigDecimal( Uses truncated division (toward zero), matching Python's `decimal.Decimal.__divmod__()` behavior. + + Args: + other: The right-hand side operand. + + Returns: + A tuple of `(quotient, remainder)` from truncated division. """ var quotient = decimo.bigdecimal.arithmetics.truncate_divide( self, other @@ -868,7 +1082,14 @@ struct BigDecimal( return (quotient^, remainder^) def __rdivmod__(self, other: Self) raises -> Tuple[Self, Self]: - """Returns `divmod(other, self)` for right-side divmod.""" + """Returns `divmod(other, self)` for right-side divmod. + + Args: + other: The left-hand side operand. + + Returns: + A tuple of `(quotient, remainder)` from truncated division. + """ var quotient = decimo.bigdecimal.arithmetics.truncate_divide( other, self ) @@ -886,34 +1107,90 @@ struct BigDecimal( @always_inline def __radd__(self, other: Self) raises -> Self: + """Adds two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The sum of the two values. + """ return decimo.bigdecimal.arithmetics.add(self, other) @always_inline def __rsub__(self, other: Self) raises -> Self: + """Subtracts two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The difference `other - self`. + """ return decimo.bigdecimal.arithmetics.subtract(other, self) @always_inline def __rmul__(self, other: Self) raises -> Self: + """Multiplies two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The product of the two values. + """ return decimo.bigdecimal.arithmetics.multiply(self, other) @always_inline def __rfloordiv__(self, other: Self) raises -> Self: + """Divides two values using floor division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The integer quotient `other // self`, truncated toward zero. + """ return decimo.bigdecimal.arithmetics.truncate_divide(other, self) @always_inline def __rmod__(self, other: Self) raises -> Self: + """Returns the remainder of division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The remainder `other % self`. + """ return decimo.bigdecimal.arithmetics.truncate_modulo( other, self, precision=PRECISION ) @always_inline def __rpow__(self, base: Self) raises -> Self: + """Raises to a power (reflected). + + Args: + base: The base to raise to the power of self. + + Returns: + The base raised to the power of self. + """ return decimo.bigdecimal.exponential.power( base, self, precision=PRECISION ) @always_inline def __rtruediv__(self, other: Self) raises -> Self: + """Divides two values using true division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The quotient `other / self`. + """ return decimo.bigdecimal.arithmetics.true_divide( other, self, precision=PRECISION ) @@ -927,28 +1204,58 @@ struct BigDecimal( @always_inline def __iadd__(mut self, other: Self) raises: + """Adds in place. + + Args: + other: The right-hand side operand. + """ decimo.bigdecimal.arithmetics.add_inplace(self, other) @always_inline def __isub__(mut self, other: Self) raises: + """Subtracts in place. + + Args: + other: The right-hand side operand. + """ decimo.bigdecimal.arithmetics.subtract_inplace(self, other) @always_inline def __imul__(mut self, other: Self) raises: + """Multiplies in place. + + Args: + other: The right-hand side operand. + """ decimo.bigdecimal.arithmetics.multiply_inplace(self, other) @always_inline def __itruediv__(mut self, other: Self) raises: + """Divides in place using true division. + + Args: + other: The right-hand side operand. + """ self = decimo.bigdecimal.arithmetics.true_divide( self, other, precision=PRECISION ) @always_inline def __ifloordiv__(mut self, other: Self) raises: + """Divides in place using floor division. + + Args: + other: The right-hand side operand. + """ self = decimo.bigdecimal.arithmetics.truncate_divide(self, other) @always_inline def __imod__(mut self, other: Self) raises: + """Computes the remainder in place. + + Args: + other: The right-hand side operand. + """ self = decimo.bigdecimal.arithmetics.truncate_modulo( self, other, precision=PRECISION ) @@ -960,32 +1267,74 @@ struct BigDecimal( @always_inline def __gt__(self, other: Self) -> Bool: - """Returns whether self is greater than other.""" + """Returns whether self is greater than other. + + Args: + other: The value to compare against. + + Returns: + True if self is greater than other, False otherwise. + """ return decimo.bigdecimal.comparison.compare(self, other) > 0 @always_inline def __ge__(self, other: Self) -> Bool: - """Returns whether self is greater than or equal to other.""" + """Returns whether self is greater than or equal to other. + + Args: + other: The value to compare against. + + Returns: + True if self is greater than or equal to other, False otherwise. + """ return decimo.bigdecimal.comparison.compare(self, other) >= 0 @always_inline def __lt__(self, other: Self) -> Bool: - """Returns whether self is less than other.""" + """Returns whether self is less than other. + + Args: + other: The value to compare against. + + Returns: + True if self is less than other, False otherwise. + """ return decimo.bigdecimal.comparison.compare(self, other) < 0 @always_inline def __le__(self, other: Self) -> Bool: - """Returns whether self is less than or equal to other.""" + """Returns whether self is less than or equal to other. + + Args: + other: The value to compare against. + + Returns: + True if self is less than or equal to other, False otherwise. + """ return decimo.bigdecimal.comparison.compare(self, other) <= 0 @always_inline def __eq__(self, other: Self) -> Bool: - """Returns whether self equals other.""" + """Returns whether self equals other. + + Args: + other: The value to compare against. + + Returns: + True if the two values are equal, False otherwise. + """ return decimo.bigdecimal.comparison.compare(self, other) == 0 @always_inline def __ne__(self, other: Self) -> Bool: - """Returns whether self does not equal other.""" + """Returns whether self does not equal other. + + Args: + other: The value to compare against. + + Returns: + True if the two values are not equal, False otherwise. + """ return decimo.bigdecimal.comparison.compare(self, other) != 0 # ===------------------------------------------------------------------=== # @@ -997,6 +1346,12 @@ struct BigDecimal( """Rounds the number to the specified number of decimal places. If `ndigits` is not given, rounds to 0 decimal places. If rounding causes errors, returns the value itself. + + Args: + ndigits: The number of decimal places to round to. + + Returns: + A new `BigDecimal` rounded to the specified decimal places. """ try: return decimo.bigdecimal.rounding.round( @@ -1011,6 +1366,9 @@ struct BigDecimal( """Rounds the number to the specified number of decimal places. If `ndigits` is not given, rounds to 0 decimal places. If rounding causes errors, returns the value itself. + + Returns: + A new `BigDecimal` rounded to 0 decimal places. """ try: return decimo.bigdecimal.rounding.round( @@ -1028,6 +1386,9 @@ struct BigDecimal( Equivalent to `math.ceil()` in Python. Returns a BigDecimal with scale 0. + + Returns: + The ceiling of this value. """ if self.scale <= 0: return self.copy() @@ -1045,6 +1406,9 @@ struct BigDecimal( Equivalent to `math.floor()` in Python. Returns a BigDecimal with scale 0. + + Returns: + The floor of this value. """ if self.scale <= 0: return self.copy() @@ -1062,6 +1426,9 @@ struct BigDecimal( Equivalent to `math.trunc()` in Python. Returns a BigDecimal with scale 0. + + Returns: + The truncated integer part of this value. """ if self.scale <= 0: return self.copy() @@ -1079,6 +1446,12 @@ struct BigDecimal( def compare(self, other: Self) raises -> Int8: """Compares two BigDecimal numbers. See `comparison.compare()` for more information. + + Args: + other: The value to compare against. + + Returns: + 1 if self > other, 0 if equal, -1 if self < other. """ return decimo.bigdecimal.comparison.compare(self, other) @@ -1086,6 +1459,12 @@ struct BigDecimal( def compare_absolute(self, other: Self) raises -> Int8: """Compares two BigDecimal numbers by absolute value. See `comparison.compare_absolute()` for more information. + + Args: + other: The value to compare against. + + Returns: + 1 if |self| > |other|, 0 if equal, -1 if |self| < |other|. """ return decimo.bigdecimal.comparison.compare_absolute(self, other) @@ -1093,12 +1472,26 @@ struct BigDecimal( @always_inline def max(self, other: Self) raises -> Self: - """Returns the maximum of two BigDecimal numbers.""" + """Returns the maximum of two BigDecimal numbers. + + Args: + other: The value to compare against. + + Returns: + The larger of the two values. + """ return decimo.bigdecimal.comparison.max(self, other) @always_inline def min(self, other: Self) raises -> Self: - """Returns the minimum of two BigDecimal numbers.""" + """Returns the minimum of two BigDecimal numbers. + + Args: + other: The value to compare against. + + Returns: + The smaller of the two values. + """ return decimo.bigdecimal.comparison.min(self, other) # === Constants === # @@ -1106,13 +1499,27 @@ struct BigDecimal( @always_inline @staticmethod def pi(precision: Int) raises -> Self: - """Returns the mathematical constant pi to the specified precision.""" + """Returns the mathematical constant pi to the specified precision. + + Args: + precision: The number of significant digits to compute. + + Returns: + The value of π to the specified precision. + """ return decimo.bigdecimal.constants.pi(precision=precision) @always_inline @staticmethod def e(precision: Int) raises -> Self: - """Returns the mathematical constant e to the specified precision.""" + """Returns the mathematical constant e to the specified precision. + + Args: + precision: The number of significant digits to compute. + + Returns: + The value of e to the specified precision. + """ return decimo.bigdecimal.exponential.exp( x=Self(BigUInt.one()), precision=precision ) @@ -1121,12 +1528,26 @@ struct BigDecimal( @always_inline def exp(self, precision: Int = PRECISION) raises -> Self: - """Returns the exponential of the BigDecimal number.""" + """Returns the exponential of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The value of e raised to the power of self. + """ return decimo.bigdecimal.exponential.exp(self, precision) @always_inline def ln(self, precision: Int = PRECISION) raises -> Self: - """Returns the natural logarithm of the BigDecimal number.""" + """Returns the natural logarithm of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The natural logarithm (base e) of this value. + """ return decimo.bigdecimal.exponential.ln(self, precision) @always_inline @@ -1135,76 +1556,176 @@ struct BigDecimal( precision: Int, mut cache: decimo.bigdecimal.exponential.MathCache, ) raises -> Self: - """Returns the natural logarithm using a cache for ln(2)/ln(1.25).""" + """Returns the natural logarithm using a cache for ln(2)/ln(1.25). + + Args: + precision: The number of significant digits for the result. + cache: The shared math constant cache for ln(2) and ln(1.25). + + Returns: + The natural logarithm (base e) of this value. + """ return decimo.bigdecimal.exponential.ln(self, precision, cache) @always_inline def log(self, base: Self, precision: Int = PRECISION) raises -> Self: """Returns the logarithm of the BigDecimal number with the given base. + + Args: + base: The logarithmic base. + precision: The number of significant digits for the result. + + Returns: + The logarithm of this value in the given base. """ return decimo.bigdecimal.exponential.log(self, base, precision) @always_inline def log10(self, precision: Int = PRECISION) raises -> Self: - """Returns the base-10 logarithm of the BigDecimal number.""" + """Returns the base-10 logarithm of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The base-10 logarithm of this value. + """ return decimo.bigdecimal.exponential.log10(self, precision) @always_inline def root(self, root: Self, precision: Int = PRECISION) raises -> Self: - """Returns the root of the BigDecimal number.""" + """Returns the root of the BigDecimal number. + + Args: + root: The degree of the root (e.g. 2 for square root, 3 for cube root). + precision: The number of significant digits for the result. + + Returns: + The nth root of this value. + """ return decimo.bigdecimal.exponential.root(self, root, precision) @always_inline def sqrt(self, precision: Int = PRECISION) raises -> Self: - """Returns the square root of the BigDecimal number.""" + """Returns the square root of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The square root of this value. + """ return decimo.bigdecimal.exponential.sqrt(self, precision) @always_inline def cbrt(self, precision: Int = PRECISION) raises -> Self: - """Returns the cube root of the BigDecimal number.""" + """Returns the cube root of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The cube root of this value. + """ return decimo.bigdecimal.exponential.cbrt(self, precision) @always_inline def power(self, exponent: Self, precision: Int = PRECISION) raises -> Self: """Returns the result of exponentiation with the given precision. See `exponential.power()` for more information. + + Args: + exponent: The power to raise this value to. + precision: The number of significant digits for the result. + + Returns: + This value raised to the given exponent. """ return decimo.bigdecimal.exponential.power(self, exponent, precision) # === Trigonometric operations === # @always_inline def sin(self, precision: Int = PRECISION) raises -> Self: - """Returns the sine of the BigDecimal number.""" + """Returns the sine of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The sine of this value. + """ return decimo.bigdecimal.trigonometric.sin(self, precision) @always_inline def cos(self, precision: Int = PRECISION) raises -> Self: - """Returns the cosine of the BigDecimal number.""" + """Returns the cosine of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The cosine of this value. + """ return decimo.bigdecimal.trigonometric.cos(self, precision) @always_inline def tan(self, precision: Int = PRECISION) raises -> Self: - """Returns the tangent of the BigDecimal number.""" + """Returns the tangent of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The tangent of this value. + """ return decimo.bigdecimal.trigonometric.tan(self, precision) @always_inline def cot(self, precision: Int = PRECISION) raises -> Self: - """Returns the cotangent of the BigDecimal number.""" + """Returns the cotangent of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The cotangent of this value. + """ return decimo.bigdecimal.trigonometric.cot(self, precision) @always_inline def csc(self, precision: Int = PRECISION) raises -> Self: - """Returns the cosecant of the BigDecimal number.""" + """Returns the cosecant of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The cosecant of this value. + """ return decimo.bigdecimal.trigonometric.csc(self, precision) @always_inline def sec(self, precision: Int = PRECISION) raises -> Self: - """Returns the secant of the BigDecimal number.""" + """Returns the secant of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The secant of this value. + """ return decimo.bigdecimal.trigonometric.sec(self, precision) @always_inline def arctan(self, precision: Int = PRECISION) raises -> Self: - """Returns the arctangent of the BigDecimal number.""" + """Returns the arctangent of the BigDecimal number. + + Args: + precision: The number of significant digits for the result. + + Returns: + The arctangent of this value in radians. + """ return decimo.bigdecimal.trigonometric.arctan(self, precision) # === Arithmetic operations === # @@ -1215,6 +1736,13 @@ struct BigDecimal( ) raises -> Self: """Returns the result of true division of two BigDecimal numbers. See `arithmetics.true_divide()` for more information. + + Args: + other: The divisor. + precision: The number of significant digits for the result. + + Returns: + The quotient of the division. """ return decimo.bigdecimal.arithmetics.true_divide(self, other, precision) @@ -1224,6 +1752,13 @@ struct BigDecimal( ) raises -> Self: """Returns the result of true division with inexact precision. See `arithmetics.true_divide_inexact()` for more information. + + Args: + other: The divisor. + number_of_significant_digits: The number of significant digits for the result. + + Returns: + The quotient of the division with the specified significant digits. """ return decimo.bigdecimal.arithmetics.true_divide_inexact( self, other, number_of_significant_digits @@ -1235,6 +1770,13 @@ struct BigDecimal( ) raises -> Self: """Returns the result of division by a small UInt32 integer. See `arithmetics.true_divide_inexact_by_uint32()` for more information. + + Args: + y: The small unsigned integer divisor. + number_of_significant_digits: The number of significant digits for the result. + + Returns: + The quotient of dividing this value by the given `UInt32`. """ return decimo.bigdecimal.arithmetics.true_divide_inexact_by_uint32( self, y, number_of_significant_digits @@ -1244,6 +1786,12 @@ struct BigDecimal( def truncate_divide(self, other: Self) raises -> Self: """Returns the result of truncating division of two BigDecimal numbers. See `arithmetics.truncate_divide()` for more information. + + Args: + other: The divisor. + + Returns: + The integer quotient, truncated toward zero. """ return decimo.bigdecimal.arithmetics.truncate_divide(self, other) @@ -1274,6 +1822,9 @@ struct BigDecimal( round(123.456, -2) -> 1E+2 round(123.456, -3) -> 0E+3 round(678.890, -3) -> 1E+3 + + Returns: + A new `BigDecimal` rounded to the specified decimal places. """ return decimo.bigdecimal.rounding.round(self, ndigits, rounding_mode) @@ -1294,6 +1845,12 @@ struct BigDecimal( specific number of decimal places, use `round()` instead. See `rounding.round_to_precision()` for more information. + + Args: + precision: The number of significant digits to round to. + rounding_mode: The rounding strategy to use. + remove_extra_digit_due_to_rounding: Whether to remove an extra leading digit caused by rounding up. + fill_zeros_to_precision: Whether to pad with trailing zeros to reach the target precision. """ decimo.bigdecimal.rounding.round_to_precision( self, @@ -1405,6 +1962,9 @@ struct BigDecimal( BigDecimal("1").adjusted() # 0 (1E0) BigDecimal("0.00").adjusted() # 0 (zero has no order of magnitude) ``` + + Returns: + The adjusted exponent of this number. """ if self.coefficient.is_zero(): return 0 @@ -1463,6 +2023,9 @@ struct BigDecimal( Equivalent to `abs(self)`. Matches Python's `decimal.Decimal.copy_abs()`. + + Returns: + A copy of this number with positive sign. """ return self.__abs__() @@ -1472,6 +2035,9 @@ struct BigDecimal( Equivalent to `-self`. Matches Python's `decimal.Decimal.copy_negate()`. + + Returns: + A copy of this number with the opposite sign. """ return self.__neg__() @@ -1483,6 +2049,9 @@ struct BigDecimal( Args: other: The BigDecimal whose sign to copy. + + Returns: + A copy of this number with the sign taken from `other`. """ return Self( coefficient=self.coefficient, @@ -1508,6 +2077,9 @@ struct BigDecimal( BigDecimal("1.2").same_quantum(BigDecimal("4.56")) # False (scale 1 vs 2) BigDecimal("100").same_quantum(BigDecimal("1")) # True (both scale=0) ``` + + Returns: + True if both values have the same scale, False otherwise. """ return self.scale == other.scale @@ -1622,7 +2194,11 @@ struct BigDecimal( self.scale += precision_diff def internal_representation(self) -> String: - """Returns the internal representation of the BigDecimal as a String.""" + """Returns the internal representation of the BigDecimal as a String. + + Returns: + A formatted string showing the coefficient, scale, sign, and words. + """ # Collect all labels to find max width var fixed_labels = List[String]() fixed_labels.append("number:") @@ -1717,7 +2293,11 @@ struct BigDecimal( ) def is_integer(self) -> Bool: - """Returns True if this number represents an integer value.""" + """Returns True if this number represents an integer value. + + Returns: + True if there is no fractional part, False otherwise. + """ var number_of_trailing_zeros = self.number_of_trailing_zeros() if number_of_trailing_zeros >= self.scale: return True @@ -1726,16 +2306,28 @@ struct BigDecimal( @always_inline def is_negative(self) -> Bool: - """Returns True if this number represents a negative value.""" + """Returns True if this number represents a negative value. + + Returns: + True if negative, False otherwise. + """ return self.sign @always_inline def is_positive(self) -> Bool: - """Returns True if this number represents a strictly positive value.""" + """Returns True if this number represents a strictly positive value. + + Returns: + True if strictly positive, False otherwise. + """ return not self.sign and not self.coefficient.is_zero() def is_odd(self) raises -> Bool: - """Returns True if this number represents an odd value.""" + """Returns True if this number represents an odd value. + + Returns: + True if the integer part is odd, False otherwise. + """ if self.scale < 0: return False @@ -1746,7 +2338,11 @@ struct BigDecimal( return True def is_one(self) raises -> Bool: - """Returns True if this number represents one.""" + """Returns True if this number represents one. + + Returns: + True if the value equals 1, False otherwise. + """ if self.sign: return False if self.scale < 0: @@ -1762,7 +2358,11 @@ struct BigDecimal( @always_inline def is_zero(self) -> Bool: - """Returns True if this number represents zero.""" + """Returns True if this number represents zero. + + Returns: + True if the value equals 0, False otherwise. + """ return self.coefficient.is_zero() def normalize(self) raises -> Self: @@ -1778,6 +2378,9 @@ struct BigDecimal( The information conveyed by trailing zeros in the coefficient may be useful for some applications, as it indicates the precision of the number. Normalization may cause loss of this information. + + Returns: + A new `BigDecimal` with trailing zeros removed. """ if self.coefficient.is_zero(): return Self(BigUInt(raw_words=[0]), 0, False) @@ -1820,7 +2423,11 @@ struct BigDecimal( ) def number_of_trailing_zeros(self) -> Int: - """Returns the number of trailing zeros in the coefficient.""" + """Returns the number of trailing zeros in the coefficient. + + Returns: + The count of trailing zero digits. + """ if self.coefficient.is_zero(): return 0 @@ -1853,6 +2460,9 @@ struct BigDecimal( BigDecimal("100.00").number_of_digits() # 5 (trailing zeros in fractional part) BigDecimal("100").number_of_digits() # 3 ``` + + Returns: + The total count of decimal digits in the coefficient. """ return self.coefficient.number_of_digits() @@ -1863,18 +2473,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/bigdecimal/comparison.mojo b/src/decimo/bigdecimal/comparison.mojo index 964edd87..adb3bfb9 100644 --- a/src/decimo/bigdecimal/comparison.mojo +++ b/src/decimo/bigdecimal/comparison.mojo @@ -111,44 +111,108 @@ def compare(x1: BigDecimal, x2: BigDecimal) -> Int8: def equal(x1: BigDecimal, x2: BigDecimal) -> Bool: - """Returns whether x1 equals x2.""" + """Returns whether x1 equals x2. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + True if x1 equals x2, False otherwise. + """ return compare(x1, x2) == 0 def not_equal(x1: BigDecimal, x2: BigDecimal) -> Bool: - """Returns whether x1 does not equal x2.""" + """Returns whether x1 does not equal x2. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + True if x1 does not equal x2, False otherwise. + """ return compare(x1, x2) != 0 def less(x1: BigDecimal, x2: BigDecimal) -> Bool: - """Returns whether x1 is less than x2.""" + """Returns whether x1 is less than x2. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + True if x1 is less than x2, False otherwise. + """ return compare(x1, x2) < 0 def less_equal(x1: BigDecimal, x2: BigDecimal) -> Bool: - """Returns whether x1 is less than or equal to x2.""" + """Returns whether x1 is less than or equal to x2. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + True if x1 is less than or equal to x2, False otherwise. + """ return compare(x1, x2) <= 0 def greater(x1: BigDecimal, x2: BigDecimal) -> Bool: - """Returns whether x1 is greater than x2.""" + """Returns whether x1 is greater than x2. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + True if x1 is greater than x2, False otherwise. + """ return compare(x1, x2) > 0 def greater_equal(x1: BigDecimal, x2: BigDecimal) -> Bool: - """Returns whether x1 is greater than or equal to x2.""" + """Returns whether x1 is greater than or equal to x2. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + True if x1 is greater than or equal to x2, False otherwise. + """ return compare(x1, x2) >= 0 def max(x1: BigDecimal, x2: BigDecimal) -> BigDecimal: - """Returns the maximum of x1 and x2.""" + """Returns the maximum of x1 and x2. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + The larger of the two values. + """ if compare(x1, x2) >= 0: return x1.copy() return x2.copy() def min(x1: BigDecimal, x2: BigDecimal) -> BigDecimal: - """Returns the minimum of x1 and x2.""" + """Returns the minimum of x1 and x2. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + The smaller of the two values. + """ if compare(x1, x2) <= 0: return x1.copy() return x2.copy() diff --git a/src/decimo/bigdecimal/constants.mojo b/src/decimo/bigdecimal/constants.mojo index 4b39614d..802759b2 100644 --- a/src/decimo/bigdecimal/constants.mojo +++ b/src/decimo/bigdecimal/constants.mojo @@ -18,6 +18,7 @@ """ from decimo.bigdecimal.bigdecimal import BigDecimal +from decimo.errors import ValueError from decimo.bigint10.bigint10 import BigInt10 from decimo.rounding_mode import RoundingMode @@ -154,10 +155,22 @@ comptime PI_1024 = BigDecimal( # we check whether the precision is higher than the current precision. # If yes, then we save it into the global scope as cached value. def pi(precision: Int) raises -> BigDecimal: - """Calculates π using the fastest available algorithm.""" + """Calculates π using the fastest available algorithm. + + Args: + precision: The number of significant digits to compute. + + Returns: + The value of π to the specified precision. + + Raises: + ValueError: If the precision is negative. + """ if precision < 0: - raise Error("Precision must be non-negative") + raise ValueError( + message="Precision must be non-negative", function="pi()" + ) # TODO: When global variables are supported, # we can check if we have a cached value for the requested precision. @@ -179,9 +192,17 @@ struct Rational: """Represents a rational number p/q for exact arithmetic.""" var p: BigInt10 # numerator + """The numerator of the rational number.""" var q: BigInt10 # denominator + """The denominator of the rational number.""" def __init__(out self, p: BigInt10, q: BigInt10): + """Initializes a rational number from a numerator and denominator. + + Args: + p: The numerator. + q: The denominator. + """ self.p = p.copy() self.q = q.copy() @@ -197,6 +218,12 @@ def pi_chudnovsky_binary_split(precision: Int) raises -> BigDecimal: (1) M(k) = (6k)! / ((3k)! * (k!)³) (2) L(k) = 545140134*k + 13591409 (3) X(k) = (-262537412640768000)^k + + Args: + precision: The number of significant digits to compute. + + Returns: + The value of π to the specified precision. """ var working_precision = precision + 9 # 1 words @@ -228,7 +255,16 @@ def pi_chudnovsky_binary_split(precision: Int) raises -> BigDecimal: def chudnovsky_split(a: Int, b: Int, precision: Int) raises -> Rational: - """Conducts binary splitting for Chudnovsky series from term a to b-1.""" + """Conducts binary splitting for Chudnovsky series from term a to b-1. + + Args: + a: The start index of the splitting range (inclusive). + b: The end index of the splitting range (exclusive). + precision: The working precision for intermediate calculations. + + Returns: + A `Rational` representing the partial sum of the Chudnovsky series. + """ var bint_1 = BigInt10(1) var bint_13591409 = BigInt10(13591409) @@ -273,7 +309,14 @@ def chudnovsky_split(a: Int, b: Int, precision: Int) raises -> Rational: def compute_m_k_rational(k: Int) raises -> Rational: - """Computes M(k) = (6k)! / ((3k)! * (k!)³) as exact rational.""" + """Computes M(k) = (6k)! / ((3k)! * (k!)³) as exact rational. + + Args: + k: The term index in the Chudnovsky series. + + Returns: + A `Rational` with numerator (6k)!/(3k)! and denominator (k!)³. + """ var bint_1 = BigInt10(1) @@ -296,7 +339,14 @@ def compute_m_k_rational(k: Int) raises -> Rational: def pi_machin(precision: Int) raises -> BigDecimal: - """Fallback π calculation using Machin's formula.""" + """Fallback π calculation using Machin's formula. + + Args: + precision: The number of significant digits to compute. + + Returns: + The value of π to the specified precision. + """ var working_precision = precision + 9 diff --git a/src/decimo/bigdecimal/exponential.mojo b/src/decimo/bigdecimal/exponential.mojo index 36a39511..9540ee7d 100644 --- a/src/decimo/bigdecimal/exponential.mojo +++ b/src/decimo/bigdecimal/exponential.mojo @@ -19,6 +19,7 @@ from std import math from decimo.bigdecimal.bigdecimal import BigDecimal +from decimo.errors import ValueError, OverflowError, ZeroDivisionError from decimo.rounding_mode import RoundingMode # ===----------------------------------------------------------------------=== # @@ -220,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: @@ -235,11 +237,14 @@ def power( # Special cases if base.coefficient.is_zero(): if exponent.coefficient.is_zero(): - raise Error("Error in power: 0^0 is undefined") + raise ValueError( + message="0^0 is undefined.", + function="power()", + ) elif exponent.sign: - raise Error( - "Error in power: Division by zero (negative exponent with zero" - " base)" + raise ZeroDivisionError( + message="Division by zero (negative exponent with zero base).", + function="power()", ) else: return BigDecimal(BigUInt.zero(), 0, False) @@ -263,9 +268,12 @@ def power( # Check for negative base with non-integer exponent if base.sign and not exponent.is_integer(): - raise Error( - "Error in power: Negative base with non-integer exponent would" - " produce a complex result" + raise ValueError( + message=( + "Negative base with non-integer exponent would produce" + " a complex result." + ), + function="power()", ) # Optimization for integer exponents @@ -418,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. @@ -434,7 +442,10 @@ def root(x: BigDecimal, n: BigDecimal, precision: Int) raises -> BigDecimal: # Check for n = 0 if n.coefficient.is_zero(): - raise Error("Error in `root`: Cannot compute zeroth root") + raise ValueError( + message="Cannot compute zeroth root.", + function="root()", + ) # Special case for integer roots - use more efficient implementation if not n.sign: @@ -492,9 +503,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( - "Error in `root`: Cannot compute non-odd-integer root of a" - " negative number" + 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) @@ -540,22 +553,31 @@ 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 # Handle special case: n must be a positive integer if n.sign: - raise Error("Error in `root`: Root value must be positive") + raise ValueError( + message="Root value must be positive.", + function="integer_root()", + ) if not n.is_integer(): - raise Error("Error in `root`: Root value must be an integer") + raise ValueError( + message="Root value must be an integer.", + function="integer_root()", + ) if n.coefficient.is_zero(): - raise Error("Error in `root`: Cannot compute zeroth root") + raise ValueError( + message="Cannot compute zeroth root.", + function="integer_root()", + ) # Special case: n = 1 (1st root is just the number itself) if n.is_one(): @@ -593,8 +615,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( - "Error in `root`: Cannot compute even root of a negative number" + raise ValueError( + message="Cannot compute even root of a negative number.", + function="integer_root()", ) # Extract n as Int for Newton's method @@ -994,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) @@ -1177,13 +1200,14 @@ 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 if x.sign: - raise Error( - "Error in `sqrt`: Cannot compute square root of negative number" + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt_exact()", ) if x.coefficient.is_zero(): @@ -1310,13 +1334,14 @@ 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 if x.sign: - raise Error( - "Error in `sqrt`: Cannot compute square root of negative number" + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt_reciprocal()", ) if x.coefficient.is_zero(): @@ -1472,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: @@ -1497,8 +1522,9 @@ def sqrt_newton(x: BigDecimal, precision: Int) raises -> BigDecimal: # Handle special cases if x.sign: - raise Error( - "Error in `sqrt`: Cannot compute square root of negative number" + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt_newton()", ) if x.coefficient.is_zero(): @@ -1568,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: @@ -1582,8 +1608,9 @@ def sqrt_decimal_approach(x: BigDecimal, precision: Int) raises -> BigDecimal: # Handle special cases if x.sign: - raise Error( - "Error in `sqrt`: Cannot compute square root of negative number" + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt_decimal_approach()", ) if x.coefficient.is_zero(): @@ -1720,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( @@ -1748,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 @@ -1765,7 +1792,9 @@ 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("Error in `exp`: Result too large to represent") + raise OverflowError( + message="Result too large to represent", function="exp()" + ) # For very large negative values, result will be effectively zero if x.sign and x.adjusted() >= 20: # x < -10^20 @@ -1942,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) @@ -1965,18 +1994,21 @@ 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 # Handle special cases if x.sign: - raise Error( - "Error in `ln`: Cannot compute logarithm of negative number" + raise ValueError( + message="Cannot compute logarithm of negative number", + function="ln()", ) if x.coefficient.is_zero(): - raise Error("Error in `ln`: Cannot compute logarithm of zero") + 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 @@ -2057,30 +2089,35 @@ 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 # Special cases if x.sign: - raise Error( - "Error in log(): Cannot compute logarithm of a negative number" + raise ValueError( + message="Cannot compute logarithm of a negative number", + function="log()", ) if x.coefficient.is_zero(): - raise Error("Error in log(): Cannot compute logarithm of zero") + raise ValueError( + message="Cannot compute logarithm of zero", function="log()" + ) # Base validation if base.sign: - raise Error("Error in log(): Cannot use a negative base") + raise ValueError(message="Cannot use a negative base", function="log()") if base.coefficient.is_zero(): - raise Error("Error in log(): Cannot use zero as a base") + 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("Error in log(): Cannot use base 1 for logarithm") + raise ValueError( + message="Cannot use base 1 for logarithm", function="log()" + ) # Special cases if ( @@ -2121,18 +2158,21 @@ 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 # Special cases if x.sign: - raise Error( - "Error in log10(): Cannot compute logarithm of a negative number" + raise ValueError( + message="Cannot compute logarithm of a negative number", + function="log10()", ) if x.coefficient.is_zero(): - raise Error("Error in log10(): Cannot compute logarithm of zero") + raise ValueError( + message="Cannot compute logarithm of zero", function="log10()" + ) # Fast path: Powers of 10 are handled directly if x.coefficient.is_power_of_10(): diff --git a/src/decimo/bigdecimal/rounding.mojo b/src/decimo/bigdecimal/rounding.mojo index fc1765be..a93ba4e8 100644 --- a/src/decimo/bigdecimal/rounding.mojo +++ b/src/decimo/bigdecimal/rounding.mojo @@ -54,6 +54,9 @@ def round( round(123.456, -2) -> 1E+2 round(123.456, -3) -> 0E+3 round(678.890, -3) -> 1E+3 + + Returns: + A new `BigDecimal` rounded to the specified number of decimal places. """ var ndigits_to_remove = number.scale - ndigits diff --git a/src/decimo/bigdecimal/trigonometric.mojo b/src/decimo/bigdecimal/trigonometric.mojo index c7962bb1..5ea652c3 100644 --- a/src/decimo/bigdecimal/trigonometric.mojo +++ b/src/decimo/bigdecimal/trigonometric.mojo @@ -21,6 +21,7 @@ from std import time from decimo.bigdecimal.bigdecimal import BigDecimal +from decimo.errors import ValueError from decimo.rounding_mode import RoundingMode import decimo.bigdecimal.constants import decimo.bigdecimal.exponential @@ -345,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 @@ -363,8 +367,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( - "bigdecimal.trigonometric.tan_cot: cot(nπ) is undefined." + raise ValueError( + message="cot(nπ) is undefined", function="tan_cot()" ) var pi = decimo.bigdecimal.constants.pi(precision=working_precision_pi) @@ -424,12 +428,15 @@ 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). """ if x.is_zero(): - raise Error("bigdecimal.trigonometric.csc: csc(nπ) is undefined.") + raise ValueError(message="csc(nπ) is undefined", function="csc()") comptime BUFFER_DIGITS = 9 var working_precision = precision + BUFFER_DIGITS @@ -477,6 +484,13 @@ def arctan(x: BigDecimal, precision: Int) raises -> BigDecimal: y = arctan(x), where x can be all real numbers, and y is in the range (-π/2, π/2). + + Args: + x: The input number to compute the arctangent of. + precision: The number of significant digits for the result. + + Returns: + The arctangent of x in radians, in the range (-π/2, π/2). """ comptime BUFFER_DIGITS = 9 # word-length, easy to append and trim diff --git a/src/decimo/bigfloat/__init__.mojo b/src/decimo/bigfloat/__init__.mojo new file mode 100644 index 00000000..7e59849b --- /dev/null +++ b/src/decimo/bigfloat/__init__.mojo @@ -0,0 +1,32 @@ +# ===----------------------------------------------------------------------=== # +# 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. +# ===----------------------------------------------------------------------=== # + +"""Sub-package for arbitrary-precision binary floating-point type. + +BigFloat is an MPFR-backed binary float type. It requires MPFR to be installed +on the system (`brew install mpfr` on macOS, `apt install libmpfr-dev` on Linux). + +BigFloat is the fast path for scientific computing at high precision. Every +operation (sqrt, exp, ln, sin, cos, tan, pi, divide) is a single MPFR call. + +For exact decimal arithmetic without external dependencies, use BigDecimal instead. + +Modules: +- bigfloat: Core BigFloat struct with constructors, arithmetic, transcendentals +- mpfr_wrapper: Low-level FFI bindings to the MPFR C wrapper +""" + +from .bigfloat import BigFloat, BFlt, Float, PRECISION as BIGFLOAT_PRECISION diff --git a/src/decimo/bigfloat/bigfloat.mojo b/src/decimo/bigfloat/bigfloat.mojo new file mode 100644 index 00000000..0c1fb43e --- /dev/null +++ b/src/decimo/bigfloat/bigfloat.mojo @@ -0,0 +1,786 @@ +# ===----------------------------------------------------------------------=== # +# 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. +# ===----------------------------------------------------------------------=== # + +"""Implements the BigFloat type: arbitrary-precision binary floating-point. + +BigFloat wraps a single MPFR handle via a C wrapper. Every arithmetic and +transcendental operation is a single MPFR call. Requires MPFR at runtime. + +Usage: + from decimo.bigfloat.bigfloat import BigFloat + + var x = BigFloat("3.14159", precision=1000) + var r = x.sqrt() + var bd = r.to_bigdecimal(1000) + +Design: + - Single field: `handle: Int32` (index into C wrapper's mpfr_t handle pool) + - Precision specified in decimal digits, converted to bits internally + - Guard bits (64 extra) ensure requested decimal digits are correct + - RAII: destructor frees MPFR handle via `mpfrw_clear` +""" + +from std.ffi import external_call, c_char +from std.memory import UnsafePointer + +from decimo.bigdecimal.bigdecimal import BigDecimal +from decimo.biguint.biguint import BigUInt +from decimo.errors import ConversionError, RuntimeError +from decimo.bigfloat.mpfr_wrapper import ( + mpfrw_available, + mpfrw_init, + mpfrw_clear, + mpfrw_set_str, + mpfrw_get_str, + mpfrw_free_str, + mpfrw_get_raw_digits, + mpfrw_free_raw_str, + mpfrw_add, + mpfrw_sub, + mpfrw_mul, + mpfrw_div, + mpfrw_neg, + mpfrw_abs, + mpfrw_cmp, + mpfrw_sqrt, + mpfrw_exp, + mpfrw_log, + mpfrw_sin, + mpfrw_cos, + mpfrw_tan, + mpfrw_pow, + mpfrw_rootn_ui, + mpfrw_const_pi, +) + +# Guard bits added to user-requested precision to absorb binary↔decimal rounding. +comptime _GUARD_BITS: Int = 64 + +# Approximate bits per decimal digit: ceil(log2(10)) ≈ 3.322. +# Use 4 for safety. +comptime _BITS_PER_DIGIT: Int = 4 + +# Default precision in decimal digits, same as BigDecimal. +comptime PRECISION: Int = 28 +"""Default precision in decimal digits for BigFloat.""" + +# Short alias, like BDec for BigDecimal. +comptime BFlt = BigFloat +"""Alias for `BigFloat`.""" +# Short alias, like Decimal for BigDecimal. +# Mojo's built-in floating-point types are all with number suffixes +# (e.g., `Float32`, `Float64`), so `Float` is available for BigFloat. +comptime Float = BigFloat +"""Alias for `BigFloat`.""" + + +fn _dps_to_bits(precision: Int) -> Int: + """Converts decimal digit precision to MPFR bit precision with guard bits. + """ + return precision * _BITS_PER_DIGIT + _GUARD_BITS + + +fn _read_c_string(address: Int) -> String: + """Reads a null-terminated C string at the given raw address into a Mojo + String. + + The caller is responsible for freeing the C string afterward. + """ + var length = external_call["strlen", Int]( + address + ) # Exclude null terminator + if length == 0: + return String("") + var buf = List[Byte](capacity=length) + for _ in range(length): + buf.append(0) + external_call["memcpy", NoneType](buf.unsafe_ptr(), address, length) + return String(unsafe_from_utf8=buf^) + + +# ===----------------------------------------------------------------------=== # +# BigFloat +# ===----------------------------------------------------------------------=== # + + +struct BigFloat(Comparable, Movable, Writable): + """Arbitrary-precision binary floating-point type backed by MPFR. + + Each BigFloat owns a single MPFR handle (index into the C wrapper's pool). + Precision is specified in decimal digits and converted to bits internally. + Arithmetic and transcendental operations are single MPFR calls. + + BigFloat is Movable but not Copyable. Transfer ownership with `^`: + + var a = BigFloat("2.0", 100) + var b = a^ # moves a into b; a is consumed + """ + + var handle: Int32 + """The MPFR context handle (index into the C wrapper's handle pool).""" + var precision: Int + """The number of significant decimal digits.""" + + # ===------------------------------------------------------------------=== # + # Constructors + # ===------------------------------------------------------------------=== # + + def __init__(out self, value: String, precision: Int = PRECISION) raises: + """Creates a BigFloat from a decimal string. + + Args: + value: A decimal number string (e.g. "3.14159", "-1.5e10"). + precision: Number of significant decimal digits. + + 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( + 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 RuntimeError( + message="MPFR handle pool exhausted", + function="BigFloat.__init__()", + ) + self.precision = precision + var s_bytes = value.as_bytes() + var result_code = mpfrw_set_str( + self.handle, + s_bytes.unsafe_ptr().bitcast[c_char](), + Int32(len(s_bytes)), + ) + if result_code != 0: + mpfrw_clear(self.handle) + raise ConversionError( + message="Invalid number string: " + value, + function="BigFloat.__init__()", + ) + + def __init__(out self, value: Int, precision: Int = PRECISION) raises: + """Creates a BigFloat from an integer. + + Args: + value: The integer to convert. + precision: The number of significant decimal digits. + """ + self = Self(String(value), precision) + + def __init__( + out self, decimal: BigDecimal, precision: Int = PRECISION + ) raises: + """Creates a BigFloat from a BigDecimal. + + Args: + decimal: The `BigDecimal` to convert. + precision: The number of significant decimal digits. + """ + self = Self(decimal.to_string(), precision) + + def __init__(out self, *, _handle: Int32, _precision: Int): + """Internal: wraps an existing MPFR handle. Caller transfers ownership. + + Args: + _handle: The MPFR context handle to take ownership of. + _precision: The number of significant decimal digits. + """ + self.handle = _handle + self.precision = _precision + + # ===------------------------------------------------------------------=== # + # Lifecycle + # ===------------------------------------------------------------------=== # + + def __init__(out self, *, deinit take: Self): + """Moves a BigFloat, transferring handle ownership. + + Args: + take: The instance to move from. + """ + self.handle = take.handle + self.precision = take.precision + + fn __del__(deinit self): + """Frees the MPFR handle.""" + if self.handle >= 0: + mpfrw_clear(self.handle) + + # ===------------------------------------------------------------------=== # + # String conversion + # ===------------------------------------------------------------------=== # + + def to_string(self, digits: Int = -1) raises -> String: + """Exports the value as a decimal string. + + Args: + digits: Number of significant digits. Defaults to the BigFloat's + precision. + + Returns: + A decimal string representation. + + Raises: + ConversionError: If string export fails. + """ + var d = digits if digits > 0 else self.precision + var address = mpfrw_get_str(self.handle, Int32(d)) + if address == 0: + raise ConversionError( + message="Failed to export string", + function="BigFloat.to_string()", + ) + var result = _read_c_string(address) + mpfrw_free_str(address) + return result + + def write_to[W: Writer](self, mut writer: W): + """Writes the decimal string representation to a Writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ + if self.handle < 0: + writer.write("BigFloat()") + return + var address = mpfrw_get_str(self.handle, Int32(self.precision)) + if address == 0: + writer.write("BigFloat()") + return + var s = _read_c_string(address) + mpfrw_free_str(address) + writer.write(s) + + def write_repr_to[W: Writer](self, mut writer: W): + """Writes a repr-style string to a Writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ + if self.handle < 0: + writer.write('BigFloat("")') + return + var address = mpfrw_get_str(self.handle, Int32(self.precision)) + if address == 0: + writer.write('BigFloat("")') + return + var s = _read_c_string(address) + mpfrw_free_str(address) + writer.write('BigFloat("', s, '")') + + # ===------------------------------------------------------------------=== # + # Conversion + # ===------------------------------------------------------------------=== # + + def to_bigdecimal(self, precision: Int = -1) raises -> BigDecimal: + """Converts this BigFloat to a BigDecimal. + + Uses MPFR's raw digit export to build a BigDecimal directly, + bypassing full string parsing for efficiency. + + Data flow (1 memcpy, 0 intermediate lists): + C: mpfr_get_str → MPFR-allocated digit buffer + exponent + memcpy → Mojo-owned byte buffer (single copy) + byte buffer → pack into base-10⁹ UInt32 words (in-place read) + + Args: + precision: Number of significant decimal digits for the conversion. + Defaults to the BigFloat's own precision. + + Returns: + A BigDecimal with the requested number of significant digits. + + Raises: + ConversionError: If raw digit export fails. + """ + var d = precision if precision > 0 else self.precision + + # 1. Get raw digits + exponent in one call + # mpfrw_get_raw_digits calls mpfr_get_str (resolved as p_get_str + # via dlsym). It returns a pure ASCII digit string like + # "31415926535897932385" (possibly "-" prefixed for negatives) and + # writes the base-10 exponent to out_exp. + # Meaning: value = 0. × 10^exp. + var exp = Int(0) + var address = mpfrw_get_raw_digits( + self.handle, Int32(d), UnsafePointer(to=exp) + ) + if address == 0: + raise ConversionError( + message="mpfr_get_str failed", + function="BigFloat.to_bigdecimal()", + ) + + # 2. Single memcpy into a Mojo-owned buffer + comptime ASCII_MINUS: UInt8 = 45 # ord("-") + comptime ASCII_ZERO: UInt8 = 48 # ord("0") + var n = external_call["strlen", Int](address) + var buf = List[UInt8](unsafe_uninit_length=n) + external_call["memcpy", NoneType](buf.unsafe_ptr(), address, n) + mpfrw_free_raw_str(address) # Free MPFR allocation immediately. + + # 3. Read bytes from the Mojo buffer (no further copies) + var ptr = buf.unsafe_ptr() + + # Detect sign (negative values have '-' prefix from MPFR). + var sign = False + var digit_start = 0 + if n > 0 and ptr[0] == ASCII_MINUS: + sign = True + digit_start = 1 + + var num_digits = n - digit_start + + # scale = number_of_significant_digits - exponent + # e.g. digits "31415" with exp=1 → 3.1415 → scale = 5 - 1 = 4 + var scale = num_digits - exp + + # 4. Pack ASCII bytes directly into base-10⁹ words + var number_of_words = num_digits // 9 + if num_digits % 9 != 0: + number_of_words += 1 + var words = List[UInt32](capacity=number_of_words) + var end = num_digits + while end >= 9: + var start = end - 9 + var word: UInt32 = 0 + for j in range(start, end): + word = word * 10 + UInt32(ptr[digit_start + j] - ASCII_ZERO) + words.append(word) + end = start + if end > 0: + var word: UInt32 = 0 + for j in range(0, end): + word = word * 10 + UInt32(ptr[digit_start + j] - ASCII_ZERO) + words.append(word) + + var coefficient = BigUInt(raw_words=words^) + return BigDecimal(coefficient=coefficient^, scale=scale, sign=sign) + + # ===------------------------------------------------------------------=== # + # Comparison + # ===------------------------------------------------------------------=== # + + def __eq__(self, other: Self) -> Bool: + """Checks whether two BigFloat values are equal. + + Args: + other: The value to compare against. + + Returns: + `True` if the values are equal, `False` otherwise. + """ + return mpfrw_cmp(self.handle, other.handle) == 0 + + def __ne__(self, other: Self) -> Bool: + """Checks whether two BigFloat values are not equal. + + Args: + other: The value to compare against. + + Returns: + `True` if the values are not equal, `False` otherwise. + """ + return mpfrw_cmp(self.handle, other.handle) != 0 + + def __lt__(self, other: Self) -> Bool: + """Checks whether this value is strictly less than another. + + Args: + other: The value to compare against. + + Returns: + `True` if `self < other`, `False` otherwise. + """ + var c = mpfrw_cmp(self.handle, other.handle) + return c != -2 and c < 0 + + def __le__(self, other: Self) -> Bool: + """Checks whether this value is less than or equal to another. + + Args: + other: The value to compare against. + + Returns: + `True` if `self <= other`, `False` otherwise. + """ + var c = mpfrw_cmp(self.handle, other.handle) + return c != -2 and c <= 0 + + def __gt__(self, other: Self) -> Bool: + """Checks whether this value is strictly greater than another. + + Args: + other: The value to compare against. + + Returns: + `True` if `self > other`, `False` otherwise. + """ + var c = mpfrw_cmp(self.handle, other.handle) + return c != -2 and c > 0 + + def __ge__(self, other: Self) -> Bool: + """Checks whether this value is greater than or equal to another. + + Args: + other: The value to compare against. + + Returns: + `True` if `self >= other`, `False` otherwise. + """ + var c = mpfrw_cmp(self.handle, other.handle) + return c != -2 and c >= 0 + + # ===------------------------------------------------------------------=== # + # Unary operators + # ===------------------------------------------------------------------=== # + + def __neg__(self) raises -> Self: + """Negates this value. + + Returns: + The negated value. + + Raises: + RuntimeError: If MPFR handle allocation fails. + """ + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__neg__()", + ) + mpfrw_neg(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def __abs__(self) raises -> Self: + """Computes the absolute value. + + Returns: + The absolute value. + + Raises: + RuntimeError: If MPFR handle allocation fails. + """ + var h = mpfrw_init(_dps_to_bits(self.precision)) + if h < 0: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__abs__()", + ) + mpfrw_abs(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + # ===------------------------------------------------------------------=== # + # Binary arithmetic operators + # ===------------------------------------------------------------------=== # + + def __add__(self, other: Self) raises -> Self: + """Adds two BigFloat values. + + Args: + other: The right-hand side operand. + + 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)) + if h < 0: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__add__()", + ) + mpfrw_add(h, self.handle, other.handle) + return Self(_handle=h, _precision=prec) + + def __sub__(self, other: Self) raises -> Self: + """Subtracts two BigFloat values. + + Args: + other: The right-hand side operand. + + 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)) + if h < 0: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__sub__()", + ) + mpfrw_sub(h, self.handle, other.handle) + return Self(_handle=h, _precision=prec) + + def __mul__(self, other: Self) raises -> Self: + """Multiplies two BigFloat values. + + Args: + other: The right-hand side operand. + + 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)) + if h < 0: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__mul__()", + ) + mpfrw_mul(h, self.handle, other.handle) + return Self(_handle=h, _precision=prec) + + def __truediv__(self, other: Self) raises -> Self: + """Divides two BigFloat values. + + Args: + other: The right-hand side operand. + + 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)) + if h < 0: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.__truediv__()", + ) + mpfrw_div(h, self.handle, other.handle) + return Self(_handle=h, _precision=prec) + + def __pow__(self, exponent: Self) raises -> Self: + """Raises this value to the given power. + + Args: + exponent: The exponent to raise to. + + Returns: + The result of `self` raised to `exponent`. + """ + return self.power(exponent) + + # ===------------------------------------------------------------------=== # + # Transcendental and math methods + # ===------------------------------------------------------------------=== # + + def sqrt(self) raises -> Self: + """Computes the square root. + + 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: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.sqrt()", + ) + mpfrw_sqrt(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def exp(self) raises -> Self: + """Computes the exponential function e^self. + + 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: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.exp()", + ) + mpfrw_exp(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def ln(self) raises -> Self: + """Computes the natural logarithm. + + 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: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.ln()", + ) + mpfrw_log(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def sin(self) raises -> Self: + """Computes the sine. + + 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: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.sin()", + ) + mpfrw_sin(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def cos(self) raises -> Self: + """Computes the cosine. + + 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: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.cos()", + ) + mpfrw_cos(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def tan(self) raises -> Self: + """Computes the tangent. + + 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: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.tan()", + ) + mpfrw_tan(h, self.handle) + return Self(_handle=h, _precision=self.precision) + + def power(self, exponent: Self) raises -> Self: + """Computes self raised to the given exponent. + + Args: + exponent: The exponent to raise to. + + 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)) + if h < 0: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.power()", + ) + mpfrw_pow(h, self.handle, exponent.handle) + return Self(_handle=h, _precision=prec) + + def root(self, n: UInt32) raises -> Self: + """Computes the n-th root. + + Args: + n: The root degree (e.g. 2 for square root, 3 for cube root). + + 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: + raise RuntimeError( + message="Handle allocation failed.", + function="BigFloat.root()", + ) + mpfrw_rootn_ui(h, self.handle, n) + return Self(_handle=h, _precision=self.precision) + + @staticmethod + def pi(precision: Int = PRECISION) raises -> BigFloat: + """Returns π to the specified number of decimal digits. + + Args: + precision: The number of significant decimal digits. + + 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( + 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 RuntimeError( + message="Handle allocation failed.", + function="BigFloat.pi()", + ) + mpfrw_const_pi(h) + return BigFloat(_handle=h, _precision=precision) diff --git a/src/decimo/bigfloat/mpfr_wrapper.mojo b/src/decimo/bigfloat/mpfr_wrapper.mojo new file mode 100644 index 00000000..79caf8d0 --- /dev/null +++ b/src/decimo/bigfloat/mpfr_wrapper.mojo @@ -0,0 +1,337 @@ +# ===----------------------------------------------------------------------=== # +# 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. +# ===----------------------------------------------------------------------=== # + +"""Low-level FFI bindings to the MPFR C wrapper (libdecimo_gmp_wrapper). + +This module provides thin Mojo wrappers around the C functions in +`gmp_wrapper.c`. The C wrapper uses `dlopen`/`dlsym` to load MPFR lazily +at runtime, so this code compiles and links without MPFR headers or libraries. + +The C wrapper manages a pool of `mpfr_t` handles. Each handle is identified +by an `Int32` index. The Mojo side never touches raw `mpfr_t` pointers. + +All functions here are internal — users interact with `BigFloat`, not these. +""" + +from std.ffi import external_call, c_int, c_char + +# ===----------------------------------------------------------------------=== # +# Availability check +# ===----------------------------------------------------------------------=== # + + +fn mpfrw_available() -> Bool: + """Checks if MPFR is available on this system. + + First call triggers the lazy load attempt. Result is cached in C. + + Returns: + True if the C wrapper successfully loaded libmpfr via dlopen. + """ + var result = external_call["mpfrw_available", c_int]() + return result != 0 + + +# ===----------------------------------------------------------------------=== # +# Handle lifecycle +# ===----------------------------------------------------------------------=== # + + +fn mpfrw_init(prec_bits: Int) -> Int32: + """Allocates an MPFR handle with the given precision in bits. + + Args: + prec_bits: Precision in bits for the MPFR value. + + Returns: + A handle index (>= 0) on success, or -1 if the pool is full + or MPFR is unavailable. + """ + return external_call["mpfrw_init", Int32](Int32(prec_bits)) + + +fn mpfrw_clear(handle: Int32): + """Frees an MPFR handle, returning it to the pool. + + Args: + handle: MPFR handle index to free. + """ + external_call["mpfrw_clear", NoneType](handle) + + +# ===----------------------------------------------------------------------=== # +# String conversion +# +# C returns malloc'd char* which Mojo can't represent as UnsafePointer[c_char] +# in return position (origin parameter can't be inferred). So we pass raw +# addresses as Int and reconstruct UnsafePointer at the call site. +# ===----------------------------------------------------------------------=== # + + +fn mpfrw_set_str( + handle: Int32, s: UnsafePointer[c_char, _], length: Int32 +) -> Int32: + """Sets the MPFR handle value from a decimal string. + + Args: + handle: MPFR handle index. + s: Pointer to a decimal string (e.g. "-3.14159"). + length: Length of the string (for safety, not relying on null terminator). + + Returns: + 0 on success, non-zero on parse error. + """ + return external_call["mpfrw_set_str", Int32](handle, s, length) + + +fn mpfrw_get_str(handle: Int32, digits: Int32) -> Int: + """Exports the MPFR handle value as a decimal string. + + Args: + handle: MPFR handle index. + digits: Number of significant decimal digits to export. + + Returns: + Raw address of a null-terminated C string (malloc'd). + Caller must free it with `mpfrw_free_str`. + """ + return external_call["mpfrw_get_str", Int](handle, digits) + + +fn mpfrw_free_str(addr: Int): + """Frees a string returned by `mpfrw_get_str`. + + Args: + addr: Raw address returned by `mpfrw_get_str`. + """ + external_call["mpfrw_free_str", NoneType](addr) + + +# ===----------------------------------------------------------------------=== # +# Raw digit export (fast BigFloat → BigDecimal) +# ===----------------------------------------------------------------------=== # + + +fn mpfrw_get_raw_digits( + handle: Int32, digits: Int32, out_exp: UnsafePointer[Int, _] +) -> Int: + """Exports MPFR value as raw digit string via mpfr_get_str. + + Returns a pointer to a null-terminated pure digit string (no dot, no + exponent notation). Negative values have a `-` prefix. The decimal + exponent is written to `out_exp`. + + Meaning: raw = `"31415..."` with exp = 1 → value = 0.31415… × 10^1. + + The digit string is allocated by MPFR and must be freed with + `mpfrw_free_raw_str`. + + Args: + handle: MPFR handle index. + digits: Number of significant decimal digits to export. + out_exp: Pointer to an Int; receives the decimal exponent. + + Returns: + Raw address of the digit string, or 0 on failure. + """ + return external_call["mpfrw_get_raw_digits", Int](handle, digits, out_exp) + + +fn mpfrw_free_raw_str(addr: Int): + """Frees a digit string returned by `mpfrw_get_raw_digits`. + + Args: + addr: Raw address returned by `mpfrw_get_raw_digits`. + """ + external_call["mpfrw_free_raw_str", NoneType](addr) + + +# ===----------------------------------------------------------------------=== # +# Arithmetic operations +# ===----------------------------------------------------------------------=== # + + +fn mpfrw_add(result: Int32, a: Int32, b: Int32): + """Computes result = a + b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Left operand handle. + b: Right operand handle. + """ + external_call["mpfrw_add", NoneType](result, a, b) + + +fn mpfrw_sub(result: Int32, a: Int32, b: Int32): + """Computes result = a - b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Left operand handle. + b: Right operand handle. + """ + external_call["mpfrw_sub", NoneType](result, a, b) + + +fn mpfrw_mul(result: Int32, a: Int32, b: Int32): + """Computes result = a * b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Left operand handle. + b: Right operand handle. + """ + external_call["mpfrw_mul", NoneType](result, a, b) + + +fn mpfrw_div(result: Int32, a: Int32, b: Int32): + """Computes result = a / b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Dividend handle. + b: Divisor handle. + """ + external_call["mpfrw_div", NoneType](result, a, b) + + +fn mpfrw_neg(result: Int32, a: Int32): + """Computes result = -a (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ + external_call["mpfrw_neg", NoneType](result, a) + + +fn mpfrw_abs(result: Int32, a: Int32): + """Computes result = |a| (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ + external_call["mpfrw_abs", NoneType](result, a) + + +fn mpfrw_cmp(a: Int32, b: Int32) -> Int32: + """Compares a and b. + + Args: + a: Left operand handle. + b: Right operand handle. + + Returns: + -1 if a < b, 0 if equal, 1 if a > b. -2 on invalid handle. + """ + return external_call["mpfrw_cmp", Int32](a, b) + + +# ===----------------------------------------------------------------------=== # +# Transcendental operations +# ===----------------------------------------------------------------------=== # + + +fn mpfrw_sqrt(result: Int32, a: Int32): + """Computes result = sqrt(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ + external_call["mpfrw_sqrt", NoneType](result, a) + + +fn mpfrw_exp(result: Int32, a: Int32): + """Computes result = exp(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ + external_call["mpfrw_exp", NoneType](result, a) + + +fn mpfrw_log(result: Int32, a: Int32): + """Computes result = ln(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ + external_call["mpfrw_log", NoneType](result, a) + + +fn mpfrw_sin(result: Int32, a: Int32): + """Computes result = sin(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ + external_call["mpfrw_sin", NoneType](result, a) + + +fn mpfrw_cos(result: Int32, a: Int32): + """Computes result = cos(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ + external_call["mpfrw_cos", NoneType](result, a) + + +fn mpfrw_tan(result: Int32, a: Int32): + """Computes result = tan(a) (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Operand handle. + """ + external_call["mpfrw_tan", NoneType](result, a) + + +fn mpfrw_pow(result: Int32, a: Int32, b: Int32): + """Computes result = a^b (MPFR_RNDN). + + Args: + result: Handle to store the result. + a: Base handle. + b: Exponent handle. + """ + external_call["mpfrw_pow", NoneType](result, a, b) + + +fn mpfrw_rootn_ui(result: Int32, a: Int32, n: UInt32): + """Computes result = a^(1/n) (MPFR_RNDN), the n-th root. + + Args: + result: Handle to store the result. + a: Operand handle. + n: Root degree. + """ + external_call["mpfrw_rootn_ui", NoneType](result, a, n) + + +fn mpfrw_const_pi(result: Int32): + """Computes result = π (MPFR_RNDN) to the handle's precision. + + Args: + result: Handle to store the result. + """ + external_call["mpfrw_const_pi", NoneType](result) diff --git a/src/decimo/bigint/arithmetics.mojo b/src/decimo/bigint/arithmetics.mojo index a9931a3b..013cbbc3 100644 --- a/src/decimo/bigint/arithmetics.mojo +++ b/src/decimo/bigint/arithmetics.mojo @@ -37,19 +37,22 @@ 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 +from decimo.errors import ValueError, ZeroDivisionError # Karatsuba cutoff: operands with this many words or fewer use schoolbook. # Tuned for Apple Silicon arm64. Adjust if benchmarking shows a better value. comptime CUTOFF_KARATSUBA: Int = 48 +"""The minimum number of words above which Karatsuba multiplication is used.""" # SIMD vector width: 4 x UInt32 = 128-bit, supported natively on arm64 NEON. comptime VECTOR_WIDTH: Int = 4 +"""The SIMD vector width used for vectorized operations.""" # Burnikel-Ziegler cutoff: divisors with this many words or fewer use # Knuth D (schoolbook). Must be even for the recursive halving to work. comptime CUTOFF_BURNIKEL_ZIEGLER: Int = 64 +"""The minimum number of words above which Burnikel-Ziegler division is used.""" # ===----------------------------------------------------------------------=== # @@ -712,13 +715,9 @@ def _divmod_magnitudes( divisor_is_zero = False break if divisor_is_zero: - raise Error( - DecimoError( - file="src/decimo/bigint/arithmetics", - function="_divmod_magnitudes()", - message="Division by zero", - previous_error=None, - ) + raise ZeroDivisionError( + function="_divmod_magnitudes()", + message="Division by zero.", ) # Compare magnitudes to handle trivial cases @@ -1150,13 +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( - file="src/decimo/bigint/arithmetics", - function="_divmod_knuth_d_from_slices()", - message="Division by zero in B-Z base case", - previous_error=None, - ) + raise ZeroDivisionError( + function="_divmod_knuth_d_from_slices()", + message="Division by zero in B-Z base case", ) # Single-word divisor fast path @@ -2037,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() @@ -2080,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] @@ -2129,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() @@ -2176,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() @@ -2208,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] @@ -2248,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] @@ -2281,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() @@ -2327,47 +2328,39 @@ def power(base: BigInt, exponent: Int) raises -> BigInt: multiplications. Args: - base: The base value. + base: The `BigInt` to raise to a power. exponent: The non-negative exponent. Returns: 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 Error( - DecimoError( - file="src/decimo/bigint/arithmetics.mojo", - function="power()", - message=( - "The exponent " - + String(exponent) - + " is negative.\n" - + "Consider using a non-negative exponent." - ), - previous_error=None, - ) + 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( - file="src/decimo/bigint/arithmetics.mojo", - function="power()", - message=( - "The exponent " - + String(exponent) - + " is too large.\n" - + "Consider using an exponent below 1_000_000_000." - ), - previous_error=None, - ) + 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 1b11d69d..da0eeb3a 100644 --- a/src/decimo/bigint/bigint.mojo +++ b/src/decimo/bigint/bigint.mojo @@ -38,11 +38,18 @@ import decimo.bigint.number_theory import decimo.str from decimo.bigint10.bigint10 import BigInt10 from decimo.biguint.biguint import BigUInt -from decimo.errors import DecimoError, ConversionError +from decimo.errors import ( + ConversionError, + OverflowError, + ValueError, + ZeroDivisionError, +) # 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( @@ -108,19 +115,31 @@ struct BigInt( @always_inline @staticmethod def zero() -> Self: - """Returns a BigInt with value 0.""" + """Returns a BigInt with value 0. + + Returns: + A `BigInt` with value 0. + """ return Self() @always_inline @staticmethod def one() -> Self: - """Returns a BigInt with value 1.""" + """Returns a BigInt with value 1. + + Returns: + A `BigInt` with value 1. + """ return Self(raw_words=[UInt32(1)], sign=False) @always_inline @staticmethod def negative_one() -> Self: - """Returns a BigInt with value -1.""" + """Returns a BigInt with value -1. + + Returns: + A `BigInt` with value -1. + """ return Self(raw_words=[UInt32(1)], sign=True) # ===------------------------------------------------------------------=== # @@ -176,6 +195,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) @@ -186,6 +208,9 @@ struct BigInt( Constraints: The dtype of the scalar must be integral. + + Args: + value: The integral scalar value to convert. """ self = Self.from_integral_scalar(value) @@ -248,6 +273,9 @@ struct BigInt( Returns: The BigInt representation of the Scalar value. + + Parameters: + dtype: The data type of the scalar value. """ if value == 0: @@ -428,11 +456,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] @@ -445,34 +483,26 @@ struct BigInt( # For BigInt (integer type), the fractional part must be zero. if scale > 0: if scale >= len(coef): - raise Error( - ConversionError( - file="src/decimo/bigint/bigint.mojo", + 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." ), - previous_error=None, - ) - ) - # Check that the fractional digits are all zero - for i in range(1, scale + 1): - if coef[-i] != 0: - raise Error( - ConversionError( - file="src/decimo/bigint/bigint.mojo", - function="BigInt.from_string(value: String)", - message=( - 'The input value "' - + value - + '" is not an integer.\n' - + "The fractional part is not zero." - ), - previous_error=None, - ) ) # Remove fractional zeros from coefficient coef.resize(len(coef) - scale, UInt8(0)) @@ -554,6 +584,12 @@ struct BigInt( def __int__(self) raises -> Int: """Returns the number as Int. See `to_int()` for more information. + + Returns: + The `Int` representation. + + Raises: + OverflowError: If the number exceeds the size of Int. """ return self.to_int() @@ -570,11 +606,25 @@ struct BigInt( return Float64(self.to_string()) def write_repr_to[W: Writer](self, mut writer: W): - """Writes the debug representation to a writer.""" + """Writes the debug representation to a writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ writer.write('BigInt("', self.to_string(), '")') def write_to[W: Writer](self, mut writer: W): - """Writes the decimal string representation to a writer.""" + """Writes the decimal string representation to a writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ writer.write(self.to_string()) # ===------------------------------------------------------------------=== # @@ -588,12 +638,15 @@ 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 if len(self.words) > 2: - raise Error("BigInt.to_int(): The number exceeds the size of Int") + raise OverflowError( + message="The number exceeds the size of Int", + function="BigInt.to_int()", + ) var magnitude: UInt64 = UInt64(self.words[0]) if len(self.words) == 2: @@ -602,8 +655,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( - "BigInt.to_int(): The number exceeds the size of 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 @@ -611,8 +665,9 @@ struct BigInt( else: # Positive: check against Int.MAX (2^63 - 1) if magnitude > UInt64(9_223_372_036_854_775_807): - raise Error( - "BigInt.to_int(): The number exceeds the size of Int" + raise OverflowError( + message="The number exceeds the size of Int", + function="BigInt.to_int()", ) return Int(magnitude) @@ -700,7 +755,13 @@ 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. + + Returns: + The decimal string representation. """ return self.to_string(line_width=line_width) @@ -804,13 +865,21 @@ struct BigInt( # ===------------------------------------------------------------------=== # def __neg__(self) -> Self: - """Returns the negation of the BigInt.""" + """Returns the negation of the BigInt. + + Returns: + The negated value. + """ if self.is_zero(): return Self() return Self(raw_words=self.words.copy(), sign=not self.sign) def __abs__(self) -> Self: - """Returns the absolute value of the BigInt.""" + """Returns the absolute value of the BigInt. + + Returns: + The absolute value. + """ return Self(raw_words=self.words.copy(), sign=False) @always_inline @@ -818,6 +887,9 @@ struct BigInt( """Returns True if the number is nonzero. This enables `if n:` syntax, consistent with Python's `int`. + + Returns: + `True` if non-zero, `False` otherwise. """ return not self.is_zero() @@ -826,6 +898,9 @@ struct BigInt( """Returns the number unchanged (unary plus). This enables `+n` syntax, consistent with Python's `int`. + + Returns: + A copy of this value. """ return Self(raw_words=self.words.copy(), sign=self.sign) @@ -834,6 +909,9 @@ struct BigInt( """Returns self unchanged. Integers are already integers. This enables `math.ceil()` compatibility with Python's `int`. + + Returns: + A copy of this value. """ return Self(raw_words=self.words.copy(), sign=self.sign) @@ -842,6 +920,9 @@ struct BigInt( """Returns self unchanged. Integers are already integers. This enables `math.floor()` compatibility with Python's `int`. + + Returns: + A copy of this value. """ return Self(raw_words=self.words.copy(), sign=self.sign) @@ -850,6 +931,9 @@ struct BigInt( """Returns self unchanged. Integers are already integers. This enables `math.trunc()` compatibility with Python's `int`. + + Returns: + A copy of this value. """ return Self(raw_words=self.words.copy(), sign=self.sign) @@ -861,74 +945,159 @@ struct BigInt( @always_inline def __add__(self, other: Self) -> Self: + """Adds two values. + + Args: + other: The right-hand side operand. + + Returns: + The sum of the two values. + """ return decimo.bigint.arithmetics.add(self, other) @always_inline def __sub__(self, other: Self) -> Self: + """Subtracts two values. + + Args: + other: The right-hand side operand. + + Returns: + The difference of the two values. + """ return decimo.bigint.arithmetics.subtract(self, other) @always_inline def __mul__(self, other: Self) -> Self: + """Multiplies two values. + + Args: + other: The right-hand side operand. + + Returns: + The product of the two values. + """ return decimo.bigint.arithmetics.multiply(self, other) @always_inline def __floordiv__(self, other: Self) raises -> Self: + """Divides two values using floor division. + + Args: + other: The right-hand side operand. + + Returns: + The quotient, rounded toward negative infinity. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ try: return decimo.bigint.arithmetics.floor_divide(self, other) except e: - raise Error( - DecimoError( - message=None, - function="BigInt.__floordiv__()", - file="src/decimo/bigint/bigint.mojo", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt.__floordiv__()", + previous_error=e^, ) @always_inline def __mod__(self, other: Self) raises -> Self: + """Returns the remainder of division. + + Args: + other: The right-hand side operand. + + 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) except e: - raise Error( - DecimoError( - message=None, - function="BigInt.__mod__()", - file="src/decimo/bigint/bigint.mojo", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt.__mod__()", + previous_error=e^, ) @always_inline def __divmod__(self, other: Self) raises -> Tuple[Self, Self]: + """Returns the quotient and remainder of division. + + Args: + other: The right-hand side operand. + + 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) except e: - raise Error( - DecimoError( - message=None, - function="BigInt.__divmod__()", - file="src/decimo/bigint/bigint.mojo", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt.__divmod__()", + previous_error=e^, ) @always_inline def __pow__(self, exponent: Self) raises -> Self: + """Raises to a power. + + Args: + exponent: The exponent. + + 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) @always_inline def __pow__(self, exponent: Int) raises -> Self: + """Raises to a power. + + Args: + exponent: The exponent. + + Returns: + The result of raising to the given power. + + Raises: + ValueError: If the exponent is negative or too large. + """ return self.power(exponent) @always_inline def __lshift__(self, shift: Int) -> Self: - """Returns self << shift (multiply by 2^shift).""" + """Returns self << shift (multiply by 2^shift). + + Args: + shift: The number of bits to shift left. + + Returns: + The left-shifted value. + """ return decimo.bigint.arithmetics.left_shift(self, shift) @always_inline def __rshift__(self, shift: Int) -> Self: - """Returns self >> shift (floor divide by 2^shift).""" + """Returns self >> shift (floor divide by 2^shift). + + Args: + shift: The number of bits to shift right. + + Returns: + The right-shifted value. + """ return decimo.bigint.arithmetics.right_shift(self, shift) # ===------------------------------------------------------------------=== # @@ -937,30 +1106,98 @@ struct BigInt( @always_inline def __radd__(self, other: Self) -> Self: + """Adds two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The sum of the two values. + """ return decimo.bigint.arithmetics.add(self, other) @always_inline def __rsub__(self, other: Self) -> Self: + """Subtracts two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The difference of the two values. + """ return decimo.bigint.arithmetics.subtract(other, self) @always_inline def __rmul__(self, other: Self) -> Self: + """Multiplies two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The product of the two values. + """ return decimo.bigint.arithmetics.multiply(self, other) @always_inline def __rfloordiv__(self, other: Self) raises -> Self: + """Divides two values using floor division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The quotient, rounded toward negative infinity. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ return decimo.bigint.arithmetics.floor_divide(other, self) @always_inline def __rmod__(self, other: Self) raises -> Self: + """Returns the remainder of division (reflected). + + Args: + other: The left-hand side operand. + + 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) @always_inline def __rdivmod__(self, other: Self) raises -> Tuple[Self, Self]: + """Returns the quotient and remainder of division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + A tuple of (quotient, remainder) using floor division. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ return decimo.bigint.arithmetics.floor_divmod(other, self) @always_inline def __rpow__(self, base: Self) raises -> Self: + """Raises to a power (reflected). + + Args: + base: The base to raise to this power. + + Returns: + The result of raising the base to this power. + + Raises: + ValueError: If the exponent is negative. + """ return base.power(self) # ===------------------------------------------------------------------=== # @@ -970,42 +1207,80 @@ struct BigInt( @always_inline def __iadd__(mut self, other: Self): - """True in-place addition: mutates self.words directly.""" + """True in-place addition: mutates self.words directly. + + Args: + other: The right-hand side operand. + """ decimo.bigint.arithmetics.add_inplace(self, other) @always_inline def __iadd__(mut self, other: Int): - """True in-place addition with Int: mutates self.words directly.""" + """True in-place addition with Int: mutates self.words directly. + + Args: + other: The right-hand side operand. + """ decimo.bigint.arithmetics.add_inplace_int(self, other) @always_inline def __isub__(mut self, other: Self): - """True in-place subtraction: mutates self.words directly.""" + """True in-place subtraction: mutates self.words directly. + + Args: + other: The right-hand side operand. + """ decimo.bigint.arithmetics.subtract_inplace(self, other) @always_inline def __imul__(mut self, other: Self): - """True in-place multiplication: computes product into self.words.""" + """True in-place multiplication: computes product into self.words. + + Args: + other: The right-hand side operand. + """ decimo.bigint.arithmetics.multiply_inplace(self, other) @always_inline def __ifloordiv__(mut self, other: Self) raises: - """True in-place floor division: moves quotient into self.words.""" + """True in-place floor division: moves quotient into self.words. + + Args: + other: The right-hand side operand. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ decimo.bigint.arithmetics.floor_divide_inplace(self, other) @always_inline def __imod__(mut self, other: Self) raises: - """True in-place modulo: moves remainder into self.words.""" + """True in-place modulo: moves remainder into self.words. + + Args: + other: The right-hand side operand. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ decimo.bigint.arithmetics.floor_modulo_inplace(self, other) @always_inline def __ilshift__(mut self, shift: Int): - """True in-place left shift: mutates self.words directly.""" + """True in-place left shift: mutates self.words directly. + + Args: + shift: The number of bits to shift left. + """ decimo.bigint.arithmetics.left_shift_inplace(self, shift) @always_inline def __irshift__(mut self, shift: Int): - """True in-place right shift: mutates self.words directly.""" + """True in-place right shift: mutates self.words directly. + + Args: + shift: The number of bits to shift right. + """ decimo.bigint.arithmetics.right_shift_inplace(self, shift) # ===------------------------------------------------------------------=== # @@ -1015,64 +1290,148 @@ struct BigInt( @always_inline def __gt__(self, other: Self) -> Bool: - """Returns True if self > other.""" + """Returns True if self > other. + + Args: + other: The value to compare against. + + Returns: + `True` if self is greater than other, `False` otherwise. + """ return decimo.bigint.comparison.greater(self, other) @always_inline def __gt__(self, other: Int) -> Bool: - """Returns True if self > other.""" + """Returns True if self > other. + + Args: + other: The value to compare against. + + Returns: + `True` if self is greater than other, `False` otherwise. + """ return decimo.bigint.comparison.greater(self, Self.from_int(other)) @always_inline def __ge__(self, other: Self) -> Bool: - """Returns True if self >= other.""" + """Returns True if self >= other. + + Args: + other: The value to compare against. + + Returns: + `True` if self is greater than or equal to other, `False` otherwise. + """ return decimo.bigint.comparison.greater_equal(self, other) @always_inline def __ge__(self, other: Int) -> Bool: - """Returns True if self >= other.""" + """Returns True if self >= other. + + Args: + other: The value to compare against. + + Returns: + `True` if self is greater than or equal to other, `False` otherwise. + """ return decimo.bigint.comparison.greater_equal( self, Self.from_int(other) ) @always_inline def __lt__(self, other: Self) -> Bool: - """Returns True if self < other.""" + """Returns True if self < other. + + Args: + other: The value to compare against. + + Returns: + `True` if self is less than other, `False` otherwise. + """ return decimo.bigint.comparison.less(self, other) @always_inline def __lt__(self, other: Int) -> Bool: - """Returns True if self < other.""" + """Returns True if self < other. + + Args: + other: The value to compare against. + + Returns: + `True` if self is less than other, `False` otherwise. + """ return decimo.bigint.comparison.less(self, Self.from_int(other)) @always_inline def __le__(self, other: Self) -> Bool: - """Returns True if self <= other.""" + """Returns True if self <= other. + + Args: + other: The value to compare against. + + Returns: + `True` if self is less than or equal to other, `False` otherwise. + """ return decimo.bigint.comparison.less_equal(self, other) @always_inline def __le__(self, other: Int) -> Bool: - """Returns True if self <= other.""" + """Returns True if self <= other. + + Args: + other: The value to compare against. + + Returns: + `True` if self is less than or equal to other, `False` otherwise. + """ return decimo.bigint.comparison.less_equal(self, Self.from_int(other)) @always_inline def __eq__(self, other: Self) -> Bool: - """Returns True if self == other.""" + """Returns True if self == other. + + Args: + other: The value to compare against. + + Returns: + `True` if the two values are equal, `False` otherwise. + """ return decimo.bigint.comparison.equal(self, other) @always_inline def __eq__(self, other: Int) -> Bool: - """Returns True if self == other.""" + """Returns True if self == other. + + Args: + other: The value to compare against. + + Returns: + `True` if the two values are equal, `False` otherwise. + """ return decimo.bigint.comparison.equal(self, Self.from_int(other)) @always_inline def __ne__(self, other: Self) -> Bool: - """Returns True if self != other.""" + """Returns True if self != other. + + Args: + other: The value to compare against. + + Returns: + `True` if the two values are not equal, `False` otherwise. + """ return decimo.bigint.comparison.not_equal(self, other) @always_inline def __ne__(self, other: Int) -> Bool: - """Returns True if self != other.""" + """Returns True if self != other. + + Args: + other: The value to compare against. + + Returns: + `True` if the two values are not equal, `False` otherwise. + """ return decimo.bigint.comparison.not_equal(self, Self.from_int(other)) # ===------------------------------------------------------------------=== # @@ -1083,6 +1442,12 @@ struct BigInt( def truncate_divide(self, other: Self) raises -> Self: """Performs a truncated division of two BigInt numbers. See `truncate_divide()` for more information. + + Args: + other: The divisor. + + Returns: + The quotient, truncated toward zero. """ return decimo.bigint.arithmetics.truncate_divide(self, other) @@ -1090,6 +1455,12 @@ struct BigInt( def floor_modulo(self, other: Self) raises -> Self: """Performs a floor modulo of two BigInt numbers. See `floor_modulo()` for more information. + + Args: + other: The divisor. + + Returns: + The floor remainder with the same sign as the divisor. """ return decimo.bigint.arithmetics.floor_modulo(self, other) @@ -1097,6 +1468,12 @@ struct BigInt( def truncate_modulo(self, other: Self) raises -> Self: """Performs a truncated modulo of two BigInt numbers. See `truncate_modulo()` for more information. + + Args: + other: The divisor. + + Returns: + The truncated remainder with the same sign as the dividend. """ return decimo.bigint.arithmetics.truncate_modulo(self, other) @@ -1110,7 +1487,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) @@ -1124,15 +1502,22 @@ 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 Error("BigInt.power(): Exponent must be non-negative") + 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("BigInt.power(): Exponent too large to fit in Int") + raise OverflowError( + message="Exponent too large to fit in Int", + function="BigInt.power()", + ) return self.power(exp_int) def sqrt(self) raises -> Self: @@ -1165,6 +1550,12 @@ struct BigInt( def compare_magnitudes(self, other: Self) -> Int8: """Compares the magnitudes (absolute values) of two BigInt numbers. See `compare_magnitudes()` for more information. + + Args: + other: The value to compare against. + + Returns: + 1 if |self| > |other|, 0 if equal, -1 if |self| < |other|. """ return decimo.bigint.comparison.compare_magnitudes(self, other) @@ -1172,6 +1563,12 @@ struct BigInt( def compare(self, other: Self) -> Int8: """Compares two BigInt numbers. See `compare()` for more information. + + Args: + other: The value to compare against. + + Returns: + 1 if self > other, 0 if equal, -1 if self < other. """ return decimo.bigint.comparison.compare(self, other) @@ -1182,54 +1579,109 @@ struct BigInt( @always_inline def __and__(self, other: Self) -> Self: """Returns self & other (bitwise AND, Python two's complement semantics). + + Args: + other: The right-hand side operand. + + Returns: + The bitwise AND of the two values. """ return decimo.bigint.bitwise.bitwise_and(self, other) @always_inline def __and__(self, other: Int) -> Self: - """Returns self & other where other is an Int.""" + """Returns self & other where other is an Int. + + Args: + other: The right-hand side operand. + + Returns: + The bitwise AND of the two values. + """ return decimo.bigint.bitwise.bitwise_and(self, Self(other)) @always_inline def __or__(self, other: Self) -> Self: """Returns self | other (bitwise OR, Python two's complement semantics). + + Args: + other: The right-hand side operand. + + Returns: + The bitwise OR of the two values. """ return decimo.bigint.bitwise.bitwise_or(self, other) @always_inline def __or__(self, other: Int) -> Self: - """Returns self | other where other is an Int.""" + """Returns self | other where other is an Int. + + Args: + other: The right-hand side operand. + + Returns: + The bitwise OR of the two values. + """ return decimo.bigint.bitwise.bitwise_or(self, Self(other)) @always_inline def __xor__(self, other: Self) -> Self: """Returns self ^ other (bitwise XOR, Python two's complement semantics). + + Args: + other: The right-hand side operand. + + Returns: + The bitwise XOR of the two values. """ return decimo.bigint.bitwise.bitwise_xor(self, other) @always_inline def __xor__(self, other: Int) -> Self: - """Returns self ^ other where other is an Int.""" + """Returns self ^ other where other is an Int. + + Args: + other: The right-hand side operand. + + Returns: + The bitwise XOR of the two values. + """ return decimo.bigint.bitwise.bitwise_xor(self, Self(other)) @always_inline def __invert__(self) -> Self: - """Returns ~self (bitwise NOT, Python two's complement semantics).""" + """Returns ~self (bitwise NOT, Python two's complement semantics). + + Returns: + The bitwise complement, equal to -(self + 1). + """ return decimo.bigint.bitwise.bitwise_not(self) @always_inline def __iand__(mut self, other: Self): - """True in-place bitwise AND: mutates self.words directly.""" + """True in-place bitwise AND: mutates self.words directly. + + Args: + other: The right-hand side operand. + """ decimo.bigint.bitwise.bitwise_and_inplace(self, other) @always_inline def __ior__(mut self, other: Self): - """True in-place bitwise OR: mutates self.words directly.""" + """True in-place bitwise OR: mutates self.words directly. + + Args: + other: The right-hand side operand. + """ decimo.bigint.bitwise.bitwise_or_inplace(self, other) @always_inline def __ixor__(mut self, other: Self): - """True in-place bitwise XOR: mutates self.words directly.""" + """True in-place bitwise XOR: mutates self.words directly. + + Args: + other: The right-hand side operand. + """ decimo.bigint.bitwise.bitwise_xor_inplace(self, other) # ===------------------------------------------------------------------=== # @@ -1238,7 +1690,14 @@ struct BigInt( @always_inline def gcd(self, other: Self) -> Self: - """Returns the greatest common divisor of self and other.""" + """Returns the greatest common divisor of self and other. + + Args: + other: The second value for the GCD computation. + + Returns: + The greatest common divisor of the two values. + """ return decimo.bigint.number_theory.gcd(self, other) @always_inline @@ -1252,18 +1711,35 @@ struct BigInt( Returns: A tuple (g, x, y) where g is the gcd of self and other, and x and y are the coefficients satisfying the equation. + + Args: + other: The second value for the extended GCD computation. """ return decimo.bigint.number_theory.extended_gcd(self, other) @always_inline def lcm(self, other: Self) raises -> Self: - """Returns the least common multiple of self and other.""" + """Returns the least common multiple of self and other. + + Args: + other: The second value for the LCM computation. + + Returns: + The least common multiple of the two values. + """ return decimo.bigint.number_theory.lcm(self, other) @always_inline def mod_pow(self, exponent: Self, modulus: Self) raises -> Self: """Returns (self ** exponent) % modulus efficiently using modular exponentiation. + + Args: + exponent: The exponent. + modulus: The modulus. + + Returns: + The result of (self ** exponent) % modulus. """ return decimo.bigint.number_theory.mod_pow(self, exponent, modulus) @@ -1271,6 +1747,13 @@ struct BigInt( def mod_pow(self, exponent: Int, modulus: Self) raises -> Self: """Returns (self ** exponent) % modulus efficiently using modular exponentiation. + + Args: + exponent: The exponent. + modulus: The modulus. + + Returns: + The result of (self ** exponent) % modulus. """ return decimo.bigint.number_theory.mod_pow( self, Self.from_int(exponent), modulus @@ -1280,6 +1763,12 @@ struct BigInt( def mod_inverse(self, modulus: Self) raises -> Self: """Returns the modular inverse of self modulo modulus, i.e. a number x such that (self * x) % modulus == 1. + + Args: + modulus: The modulus. + + Returns: + The modular multiplicative inverse. """ return decimo.bigint.number_theory.mod_inverse(self, modulus) @@ -1289,7 +1778,11 @@ struct BigInt( @always_inline def is_zero(self) -> Bool: - """Returns True if the value is zero.""" + """Returns True if the value is zero. + + Returns: + `True` if the value is zero, `False` otherwise. + """ if len(self.words) == 1 and self.words[0] == 0: return True for word in self.words: @@ -1299,20 +1792,36 @@ struct BigInt( @always_inline def is_negative(self) -> Bool: - """Returns True if the value is strictly negative.""" + """Returns True if the value is strictly negative. + + Returns: + `True` if the value is negative, `False` otherwise. + """ return self.sign and not self.is_zero() @always_inline def is_positive(self) -> Bool: - """Returns True if the value is strictly positive.""" + """Returns True if the value is strictly positive. + + Returns: + `True` if the value is positive, `False` otherwise. + """ return not self.sign and not self.is_zero() def is_one(self) -> Bool: - """Returns True if the value is exactly 1.""" + """Returns True if the value is exactly 1. + + Returns: + `True` if the value is 1, `False` otherwise. + """ return not self.sign and len(self.words) == 1 and self.words[0] == 1 def is_one_or_minus_one(self) -> Bool: - """Returns True if the value is 1 or -1.""" + """Returns True if the value is 1 or -1. + + Returns: + `True` if the value is 1 or -1, `False` otherwise. + """ return len(self.words) == 1 and self.words[0] == 1 def bit_length(self) -> Int: @@ -1364,7 +1873,11 @@ struct BigInt( return count def number_of_words(self) -> Int: - """Returns the number of words in the magnitude.""" + """Returns the number of words in the magnitude. + + Returns: + The number of `UInt32` words used to represent the magnitude. + """ return len(self.words) def number_of_digits(self) -> Int: @@ -1372,6 +1885,9 @@ struct BigInt( Notes: Zero has 1 digit. + + Returns: + The number of decimal digits. """ if self.is_zero(): return 1 @@ -1384,7 +1900,11 @@ struct BigInt( # ===------------------------------------------------------------------=== # def copy(self) -> Self: - """Returns a deep copy of this BigInt.""" + """Returns a deep copy of this BigInt. + + Returns: + A new `BigInt` with the same value. + """ var new_words = List[UInt32](capacity=len(self.words)) for word in self.words: new_words.append(word) @@ -1400,7 +1920,11 @@ struct BigInt( self.sign = False def internal_representation(self) -> String: - """Returns the internal representation details as a String.""" + """Returns the internal representation details as a String. + + Returns: + A string showing the sign and word-level magnitude representation. + """ # Collect all labels to find max width var fixed_labels = List[String]() fixed_labels.append("number:") diff --git a/src/decimo/bigint/bitwise.mojo b/src/decimo/bigint/bitwise.mojo index b4667cbd..ee8b0225 100644 --- a/src/decimo/bigint/bitwise.mojo +++ b/src/decimo/bigint/bitwise.mojo @@ -327,17 +327,41 @@ def _binary_bitwise_op_inplace[ def bitwise_and(a: BigInt, b: BigInt) -> BigInt: - """Returns a & b using Python-compatible two's complement semantics.""" + """Returns a & b using Python-compatible two's complement semantics. + + Args: + a: The first operand. + b: The second operand. + + Returns: + The bitwise AND of the two values. + """ return _binary_bitwise_op["and"](a, b) def bitwise_or(a: BigInt, b: BigInt) -> BigInt: - """Returns a | b using Python-compatible two's complement semantics.""" + """Returns a | b using Python-compatible two's complement semantics. + + Args: + a: The first operand. + b: The second operand. + + Returns: + The bitwise OR of the two values. + """ return _binary_bitwise_op["or"](a, b) def bitwise_xor(a: BigInt, b: BigInt) -> BigInt: - """Returns a ^ b using Python-compatible two's complement semantics.""" + """Returns a ^ b using Python-compatible two's complement semantics. + + Args: + a: The first operand. + b: The second operand. + + Returns: + The bitwise XOR of the two values. + """ return _binary_bitwise_op["xor"](a, b) @@ -348,6 +372,12 @@ def bitwise_not(x: BigInt) -> BigInt: For non-negative x: result is -(x+1), always negative (except ~(-1) = 0). For negative x (x = -|x|): result is |x| - 1, always non-negative. + + Args: + x: The value to invert. + + Returns: + The bitwise complement. """ if not x.sign: # ~non_negative = -(x + 1) @@ -384,18 +414,30 @@ def bitwise_not(x: BigInt) -> BigInt: def bitwise_and_inplace(mut a: BigInt, read b: BigInt): - """Performs a &= b in-place using Python-compatible two's complement - semantics.""" + """Performs `a &= b` in-place using Python-compatible two's complement semantics. + + Args: + a: The left-hand side operand, modified in place. + b: The right-hand side operand. + """ _binary_bitwise_op_inplace["and"](a, b) def bitwise_or_inplace(mut a: BigInt, read b: BigInt): - """Performs a |= b in-place using Python-compatible two's complement - semantics.""" + """Performs `a |= b` in-place using Python-compatible two's complement semantics. + + Args: + a: The left-hand side operand, modified in place. + b: The right-hand side operand. + """ _binary_bitwise_op_inplace["or"](a, b) def bitwise_xor_inplace(mut a: BigInt, read b: BigInt): - """Performs a ^= b in-place using Python-compatible two's complement - semantics.""" + """Performs `a ^= b` in-place using Python-compatible two's complement semantics. + + Args: + a: The left-hand side operand, modified in place. + b: The right-hand side operand. + """ _binary_bitwise_op_inplace["xor"](a, b) diff --git a/src/decimo/bigint/exponential.mojo b/src/decimo/bigint/exponential.mojo index 9671b867..b3fd618c 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 # ===----------------------------------------------------------------------=== # @@ -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: @@ -281,13 +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( - file="src/decimo/bigint/exponential.mojo", - function="sqrt()", - message="Cannot compute square root of a negative number", - previous_error=None, - ) + raise ValueError( + function="sqrt()", + message="Cannot compute square root of a negative number", ) if x.is_zero(): @@ -498,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 f6b6f489..b089e66d 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 # ===----------------------------------------------------------------------=== # @@ -242,26 +242,19 @@ 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 Error( - DecimoError( - file="src/decimo/bigint/number_theory.mojo", - function="mod_pow()", - message="Exponent must be non-negative", - previous_error=None, - ) + raise ValueError( + function="mod_pow()", + message="Exponent must be non-negative", ) if not modulus.is_positive(): - raise Error( - DecimoError( - file="src/decimo/bigint/number_theory.mojo", - function="mod_pow()", - message="Modulus must be positive", - previous_error=None, - ) + raise ValueError( + function="mod_pow()", + message="Modulus must be positive", ) # x mod 1 = 0 for all x @@ -303,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) @@ -327,16 +324,13 @@ 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 Error( - DecimoError( - file="src/decimo/bigint/number_theory.mojo", - function="mod_inverse()", - message="Modulus must be positive", - previous_error=None, - ) + raise ValueError( + function="mod_inverse()", + message="Modulus must be positive", ) var result = extended_gcd(a, modulus) @@ -344,13 +338,9 @@ def mod_inverse(a: BigInt, modulus: BigInt) raises -> BigInt: var x = result[1].copy() if not g.is_one(): - raise Error( - DecimoError( - file="src/decimo/bigint/number_theory.mojo", - function="mod_inverse()", - message="Modular inverse does not exist (gcd != 1)", - previous_error=None, - ) + 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 ccd00d1f..0da10b6a 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 @@ -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: @@ -225,13 +224,10 @@ def floor_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/arithmetics", - function="floor_divide()", - message=None, - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="floor_divide()", + previous_error=e^, ) return BigInt10(magnitude^, sign=False) @@ -242,13 +238,10 @@ def floor_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/arithmetics", - function="floor_divide()", - message=None, - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="floor_divide()", + previous_error=e^, ) return BigInt10(magnitude^, sign=True) @@ -266,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: @@ -274,13 +267,10 @@ def truncate_divide(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/arithmetics", - function="truncate_divide()", - message=None, - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="truncate_divide()", + previous_error=e^, ) return BigInt10(magnitude^, sign=x1.sign != x2.sign) @@ -298,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 @@ -311,13 +300,10 @@ def floor_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/arithmetics", - function="floor_modulo()", - message=None, - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="floor_modulo()", + previous_error=e^, ) return BigInt10(magnitude^, sign=x2.sign) @@ -328,13 +314,10 @@ def floor_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/arithmetics", - function="floor_modulo()", - message=None, - previous_error=e^, - ), + raise ZeroDivisionError( + message="See the above exception.", + function="floor_modulo()", + previous_error=e^, ) return BigInt10(magnitude^, sign=x2.sign) @@ -352,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: @@ -360,12 +343,9 @@ def truncate_modulo(x1: BigInt10, x2: BigInt10) raises -> BigInt10: x1.magnitude, x2.magnitude ) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/arithmetics", - function="truncate_modulo()", - message=None, - 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 457b13d3..11e1f3d3 100644 --- a/src/decimo/bigint10/bigint10.mojo +++ b/src/decimo/bigint10/bigint10.mojo @@ -30,7 +30,12 @@ import decimo.bigint10.arithmetics import decimo.bigint10.comparison from decimo.bigdecimal.bigdecimal import BigDecimal from decimo.biguint.biguint import BigUInt -from decimo.errors import DecimoError +from decimo.errors import ( + ValueError, + OverflowError, + ConversionError, + ZeroDivisionError, +) import decimo.str @@ -53,9 +58,9 @@ struct BigInt10( """ var magnitude: BigUInt - """The magnitude of the BigInt10.""" + """The absolute value stored as a `BigUInt`.""" var sign: Bool - """Sign information.""" + """The sign flag. `True` if negative, `False` if non-negative.""" # ===------------------------------------------------------------------=== # # Constructors and life time dunder methods @@ -75,7 +80,11 @@ struct BigInt10( @implicit def __init__(out self, magnitude: BigUInt): - """Constructs a BigInt10 from a BigUInt object.""" + """Constructs a BigInt10 from a BigUInt object. + + Args: + magnitude: The `BigUInt` to construct from. + """ self.magnitude = magnitude.copy() self.sign = False @@ -102,21 +111,21 @@ 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()`. """ try: self = Self.from_list(words^, sign=sign) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/bigint10.mojo", - function=( - "BigInt10.__init__(var words: List[UInt32], sign: Bool)" - ), - message=None, - 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): @@ -144,11 +153,21 @@ struct BigInt10( def __init__(out self, value: String) raises: """Initializes a BigInt10 from a string representation. See `from_string()` for more information. + + Args: + value: The string representation of the integer. + + Raises: + ConversionError: If the string cannot be parsed. """ try: self = Self.from_string(value) except e: - raise Error("Error in `BigInt10.__init__()` with String: ", 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], # we can remove this method. @@ -156,6 +175,9 @@ struct BigInt10( def __init__(out self, value: Int): """Initializes a BigInt10 from an `Int` object. See `from_int()` for more information. + + Args: + value: The integer value to convert. """ self = Self.from_int(value) @@ -166,11 +188,21 @@ struct BigInt10( Constraints: The dtype of the scalar must be integral. + + Args: + value: The scalar value to convert. """ self = Self.from_integral_scalar(value) def __init__(out self, *, py: PythonObject) raises: - """Constructs a BigInt10 from a Python int object.""" + """Constructs a BigInt10 from a Python int object. + + Args: + py: The Python integer object to convert from. + + Raises: + ConversionError: If the Python object cannot be converted. + """ self = Self.from_python_int(py) # ===------------------------------------------------------------------=== # @@ -195,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. @@ -203,16 +235,12 @@ struct BigInt10( try: return Self(BigUInt.from_list(words^), sign) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/bigint10.mojo", - function=( - "BigInt10.from_list(var words: List[UInt32], sign:" - " Bool)" - ), - message=None, - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function=( + "BigInt10.from_list(var words: List[UInt32], sign: Bool)" + ), + previous_error=e^, ) @staticmethod @@ -228,6 +256,12 @@ struct BigInt10( Notes: This method validates whether the words are smaller than `999_999_999`. + + 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)) @@ -235,9 +269,9 @@ struct BigInt10( # Check if the words are valid for word in words: if word > UInt32(999_999_999): - raise Error( - "Error in `BigInt10.__init__()`: Word value exceeds maximum" - " value of 999_999_999" + raise ValueError( + message="Word value exceeds maximum value of 999_999_999", + function="BigInt10.__init__()", ) else: list_of_words.append(word) @@ -246,7 +280,14 @@ struct BigInt10( @staticmethod def from_int(value: Int) -> Self: - """Creates a BigInt10 from an integer.""" + """Creates a BigInt10 from an integer. + + Args: + value: The integer value to convert. + + Returns: + A new `BigInt10` from the given integer. + """ if value == 0: return Self() @@ -291,6 +332,9 @@ struct BigInt10( Returns: The BigInt10 representation of the Scalar value. + + Parameters: + dtype: The data type of the input scalar. """ comptime assert dtype.is_integral(), "dtype must be integral." @@ -306,15 +350,29 @@ struct BigInt10( @staticmethod def from_string(value: String) raises -> Self: """Initializes a BigInt10 from a string representation. - The string is normalized with `deciomojo.str.parse_numeric_string()`. + The string is normalized with `decimo.str.parse_numeric_string()`. Args: value: The string representation of the 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] @@ -337,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 @@ -365,13 +422,10 @@ struct BigInt10( # Use the existing from_string() method to parse the string return Self.from_string(py_str) except e: - raise Error( - DecimoError( - file="src/decimo/bigint10/bigint10.mojo", - function="BigInt10.from_python_int(value: PythonObject)", - message="Failed to convert Python int to BigInt10.", - previous_error=e^, - ) + raise ConversionError( + message="Failed to convert Python int to BigInt10.", + function="BigInt10.from_python_int()", + previous_error=e^, ) # ===------------------------------------------------------------------=== # @@ -381,11 +435,24 @@ struct BigInt10( def __int__(self) raises -> Int: """Returns the number as Int. See `to_int()` for more information. + + Returns: + The `Int` representation of this value. + + Raises: + OverflowError: If the number exceeds the size of Int. """ return self.to_int() def write_repr_to[W: Writer](self, mut writer: W): - """Writes the debug representation to a writer.""" + """Writes the debug representation to a writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ writer.write('BigInt10("', self.to_string(), '")') # ===------------------------------------------------------------------=== # @@ -395,6 +462,12 @@ struct BigInt10( def write_to[W: Writer](self, mut writer: W): """Writes the BigInt10 to a writer. This implement the `write` method of the `Writer` trait. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. """ writer.write(self.to_string()) @@ -405,16 +478,16 @@ 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 # is larger than 10^18 -1 but smaller than 10^27 - 1 if len(self.magnitude.words) > 3: - raise Error( - "Error in `BigInt10.to_int()`: The number exceeds the size" - " of Int" + raise OverflowError( + message="The number exceeds the size of Int.", + function="BigInt10.to_int()", ) var value: Int128 = 0 @@ -431,9 +504,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( - "Error in `BigInt10.to_int()`: The number exceeds the size" - " of Int" + raise OverflowError( + message="The number exceeds the size of Int.", + function="BigInt10.to_int()", ) return Int(value) @@ -503,6 +576,9 @@ struct BigInt10( def __abs__(self) -> Self: """Returns the absolute value of this number. See `absolute()` for more information. + + Returns: + The absolute value. """ return decimo.bigint10.arithmetics.absolute(self) @@ -510,6 +586,9 @@ struct BigInt10( def __neg__(self) -> Self: """Returns the negation of this number. See `negative()` for more information. + + Returns: + The negated value. """ return decimo.bigint10.arithmetics.negative(self) @@ -521,46 +600,97 @@ struct BigInt10( @always_inline def __add__(self, other: Self) -> Self: + """Adds two values. + + Args: + other: The right-hand side operand. + + Returns: + The sum of the two values. + """ return decimo.bigint10.arithmetics.add(self, other) @always_inline def __sub__(self, other: Self) -> Self: + """Subtracts two values. + + Args: + other: The right-hand side operand. + + Returns: + The difference of the two values. + """ return decimo.bigint10.arithmetics.subtract(self, other) @always_inline def __mul__(self, other: Self) -> Self: + """Multiplies two values. + + Args: + other: The right-hand side operand. + + Returns: + The product of the two values. + """ return decimo.bigint10.arithmetics.multiply(self, other) @always_inline def __floordiv__(self, other: Self) raises -> Self: + """Divides two values using floor division. + + Args: + other: The right-hand side operand. + + Returns: + The floor division quotient. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ try: return decimo.bigint10.arithmetics.floor_divide(self, other) except e: - raise Error( - DecimoError( - message=None, - function="BigInt10.__floordiv__()", - file="src/decimo/bigint10/bigint10.mojo", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt10.__floordiv__()", + previous_error=e^, ) @always_inline def __mod__(self, other: Self) raises -> Self: + """Returns the remainder of division. + + Args: + other: The right-hand side operand. + + Returns: + The remainder of the floor division. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ try: return decimo.bigint10.arithmetics.floor_modulo(self, other) except e: - raise Error( - DecimoError( - message=None, - function="BigInt10.__mod__()", - file="src/decimo/bigint10/bigint10.mojo", - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigInt10.__mod__()", + previous_error=e^, ) @always_inline def __pow__(self, exponent: Self) raises -> Self: + """Raises to a power. + + Args: + exponent: The exponent. + + Returns: + The result of raising to the given power. + + Raises: + OverflowError: If the exponent is too large. + """ return self.power(exponent) # ===------------------------------------------------------------------=== # @@ -571,26 +701,74 @@ struct BigInt10( @always_inline def __radd__(self, other: Self) -> Self: + """Adds two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The sum of the two values. + """ return decimo.bigint10.arithmetics.add(self, other) @always_inline def __rsub__(self, other: Self) -> Self: + """Subtracts two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The difference of the two values. + """ return decimo.bigint10.arithmetics.subtract(other, self) @always_inline def __rmul__(self, other: Self) -> Self: + """Multiplies two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The product of the two values. + """ return decimo.bigint10.arithmetics.multiply(self, other) @always_inline def __rfloordiv__(self, other: Self) raises -> Self: + """Divides two values using floor division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The floor division quotient. + """ return decimo.bigint10.arithmetics.floor_divide(other, self) @always_inline def __rmod__(self, other: Self) raises -> Self: + """Returns the remainder of division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The remainder of the floor division. + """ return decimo.bigint10.arithmetics.floor_modulo(other, self) @always_inline def __rpow__(self, base: Self) raises -> Self: + """Raises to a power (reflected). + + Args: + base: The base to raise to this power. + + Returns: + The result of raising to the given power. + """ return base.power(self) # ===------------------------------------------------------------------=== # @@ -602,10 +780,20 @@ struct BigInt10( @always_inline def __iadd__(mut self, other: Self): + """Adds in place. + + Args: + other: The right-hand side operand. + """ decimo.bigint10.arithmetics.add_inplace(self, other) @always_inline def __iadd__(mut self, other: Int): + """Adds in place. + + Args: + other: The right-hand side operand. + """ # Optimize the case `i += 1` if (self >= 0) and (other >= 0) and (other <= 999_999_999): decimo.biguint.arithmetics.add_inplace_by_uint32( @@ -616,18 +804,38 @@ struct BigInt10( @always_inline def __isub__(mut self, other: Self): + """Subtracts in place. + + Args: + other: The right-hand side operand. + """ self = decimo.bigint10.arithmetics.subtract(self, other) @always_inline def __imul__(mut self, other: Self): + """Multiplies in place. + + Args: + other: The right-hand side operand. + """ self = decimo.bigint10.arithmetics.multiply(self, other) @always_inline def __ifloordiv__(mut self, other: Self) raises: + """Divides in place using floor division. + + Args: + other: The right-hand side operand. + """ self = decimo.bigint10.arithmetics.floor_divide(self, other) @always_inline def __imod__(mut self, other: Self) raises: + """Computes the remainder in place. + + Args: + other: The right-hand side operand. + """ self = decimo.bigint10.arithmetics.floor_modulo(self, other) # ===------------------------------------------------------------------=== # @@ -637,64 +845,148 @@ struct BigInt10( @always_inline def __gt__(self, other: Self) -> Bool: - """Returns True if self > other.""" + """Checks if this value is greater than `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self > other`, `False` otherwise. + """ return decimo.bigint10.comparison.greater(self, other) @always_inline def __gt__(self, other: Int) -> Bool: - """Returns True if self > other.""" + """Checks if this value is greater than `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self > other`, `False` otherwise. + """ return decimo.bigint10.comparison.greater(self, Self.from_int(other)) @always_inline def __ge__(self, other: Self) -> Bool: - """Returns True if self >= other.""" + """Checks if this value is greater than or equal to `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self >= other`, `False` otherwise. + """ return decimo.bigint10.comparison.greater_equal(self, other) @always_inline def __ge__(self, other: Int) -> Bool: - """Returns True if self >= other.""" + """Checks if this value is greater than or equal to `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self >= other`, `False` otherwise. + """ return decimo.bigint10.comparison.greater_equal( self, Self.from_int(other) ) @always_inline def __lt__(self, other: Self) -> Bool: - """Returns True if self < other.""" + """Checks if this value is less than `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self < other`, `False` otherwise. + """ return decimo.bigint10.comparison.less(self, other) @always_inline def __lt__(self, other: Int) -> Bool: - """Returns True if self < other.""" + """Checks if this value is less than `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self < other`, `False` otherwise. + """ return decimo.bigint10.comparison.less(self, Self.from_int(other)) @always_inline def __le__(self, other: Self) -> Bool: - """Returns True if self <= other.""" + """Checks if this value is less than or equal to `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self <= other`, `False` otherwise. + """ return decimo.bigint10.comparison.less_equal(self, other) @always_inline def __le__(self, other: Int) -> Bool: - """Returns True if self <= other.""" + """Checks if this value is less than or equal to `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self <= other`, `False` otherwise. + """ return decimo.bigint10.comparison.less_equal(self, Self.from_int(other)) @always_inline def __eq__(self, other: Self) -> Bool: - """Returns True if self == other.""" + """Checks if this value is equal to `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self == other`, `False` otherwise. + """ return decimo.bigint10.comparison.equal(self, other) @always_inline def __eq__(self, other: Int) -> Bool: - """Returns True if self == other.""" + """Checks if this value is equal to `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self == other`, `False` otherwise. + """ return decimo.bigint10.comparison.equal(self, Self.from_int(other)) @always_inline def __ne__(self, other: Self) -> Bool: - """Returns True if self != other.""" + """Checks if this value is not equal to `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self != other`, `False` otherwise. + """ return decimo.bigint10.comparison.not_equal(self, other) @always_inline def __ne__(self, other: Int) -> Bool: - """Returns True if self != other.""" + """Checks if this value is not equal to `other`. + + Args: + other: The value to compare against. + + Returns: + `True` if `self != other`, `False` otherwise. + """ return decimo.bigint10.comparison.not_equal(self, Self.from_int(other)) # ===------------------------------------------------------------------=== # @@ -702,7 +994,14 @@ struct BigInt10( # ===------------------------------------------------------------------=== # def __merge_with__[other_type: type_of(BigDecimal)](self) -> BigDecimal: - "Merges this BigInt10 with a BigDecimal into a BigDecimal." + """Merges this BigInt10 with a BigDecimal into a BigDecimal. + + Parameters: + other_type: The target type. + + Returns: + A BigDecimal value. + """ return BigDecimal(self) # ===------------------------------------------------------------------=== # @@ -713,6 +1012,12 @@ struct BigInt10( def floor_divide(self, other: Self) raises -> Self: """Performs a floor division of two BigInts. See `floor_divide()` for more information. + + Args: + other: The divisor. + + Returns: + The floor division quotient. """ return decimo.bigint10.arithmetics.floor_divide(self, other) @@ -720,6 +1025,12 @@ struct BigInt10( def truncate_divide(self, other: Self) raises -> Self: """Performs a truncated division of two BigInts. See `truncate_divide()` for more information. + + Args: + other: The divisor. + + Returns: + The truncated division quotient. """ return decimo.bigint10.arithmetics.truncate_divide(self, other) @@ -727,6 +1038,12 @@ struct BigInt10( def floor_modulo(self, other: Self) raises -> Self: """Performs a floor modulo of two BigInts. See `floor_modulo()` for more information. + + Args: + other: The divisor. + + Returns: + The floor division remainder. """ return decimo.bigint10.arithmetics.floor_modulo(self, other) @@ -734,12 +1051,27 @@ struct BigInt10( def truncate_modulo(self, other: Self) raises -> Self: """Performs a truncated modulo of two BigInts. See `truncate_modulo()` for more information. + + Args: + other: The divisor. + + Returns: + The truncated division remainder. """ return decimo.bigint10.arithmetics.truncate_modulo(self, other) def power(self, exponent: Int) raises -> Self: """Raises the BigInt10 to the power of an integer exponent. See `power()` for more information. + + Args: + exponent: The exponent. + + 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 @@ -750,9 +1082,22 @@ struct BigInt10( def power(self, exponent: Self) raises -> Self: """Raises the BigInt10 to the power of another BigInt10. See `power()` for more information. + + Args: + exponent: The exponent. + + 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 Error("Error in `BigUInt.power()`: The exponent is too large") + raise OverflowError( + message="The exponent is too large.", + function="BigInt10.power()", + ) var exponent_as_int = exponent.to_int() return self.power(exponent_as_int) @@ -760,6 +1105,12 @@ struct BigInt10( def compare_magnitudes(self, other: Self) -> Int8: """Compares the magnitudes of two BigInts. See `compare_magnitudes()` for more information. + + Args: + other: The value to compare against. + + Returns: + 1 if `self` magnitude is greater, -1 if less, 0 if equal. """ return decimo.bigint10.comparison.compare_magnitudes(self, other) @@ -767,6 +1118,12 @@ struct BigInt10( def compare(self, other: Self) -> Int8: """Compares two BigInts. See `compare()` for more information. + + Args: + other: The value to compare against. + + Returns: + 1 if `self` is greater, -1 if less, 0 if equal. """ return decimo.bigint10.comparison.compare(self, other) @@ -776,22 +1133,38 @@ struct BigInt10( @always_inline def is_zero(self) -> Bool: - """Returns True if this BigInt10 represents zero.""" + """Returns True if this BigInt10 represents zero. + + Returns: + `True` if zero, `False` otherwise. + """ return self.magnitude.is_zero() @always_inline def is_one_or_minus_one(self) -> Bool: - """Returns True if this BigInt10 represents one or negative one.""" + """Returns True if this BigInt10 represents one or negative one. + + Returns: + `True` if the value is 1 or -1, `False` otherwise. + """ return self.magnitude.is_one() @always_inline def is_negative(self) -> Bool: - """Returns True if this BigInt10 is negative.""" + """Returns True if this BigInt10 is negative. + + Returns: + `True` if negative, `False` otherwise. + """ return self.sign @always_inline def number_of_words(self) -> Int: - """Returns the number of words in the BigInt10.""" + """Returns the number of words in the BigInt10. + + Returns: + The number of internal words. + """ return len(self.magnitude.words) # ===------------------------------------------------------------------=== # @@ -799,7 +1172,11 @@ struct BigInt10( # ===------------------------------------------------------------------=== # def internal_representation(self) raises -> String: - """Returns the internal representation details as a String.""" + """Returns the internal representation details as a String. + + Returns: + A formatted string showing the internal representation. + """ # Collect all labels to find max width var max_label_len = len("number:") for i in range(len(self.magnitude.words)): diff --git a/src/decimo/biguint/arithmetics.mojo b/src/decimo/biguint/arithmetics.mojo index 46a7face..3055e5fd 100644 --- a/src/decimo/biguint/arithmetics.mojo +++ b/src/decimo/biguint/arithmetics.mojo @@ -24,7 +24,11 @@ from std.memory import memcpy, memset_zero from decimo.biguint.biguint import BigUInt import decimo.biguint.comparison -from decimo.errors import DecimoError, OverflowError, ZeroDivisionError +from decimo.errors import ( + OverflowError, + ValueError, + ZeroDivisionError, +) from decimo.rounding_mode import RoundingMode comptime CUTOFF_KARATSUBA = 64 @@ -94,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. @@ -103,13 +107,9 @@ def negative(x: BigUInt) raises -> BigUInt: debug_assert[assert_mode="none"]( len(x.words) == 1, "negative(): leading zero words" ) - raise Error( - OverflowError( - file="src/decimo/biguint/arithmetics.mojo", - function="negative()", - message="Negative of non-zero unsigned integer is undefined", - previous_error=None, - ) + raise OverflowError( + function="negative()", + message="Negative of non-zero unsigned integer is undefined", ) return BigUInt.zero() # Return zero @@ -453,7 +453,12 @@ def add_inplace_by_slice( def add_inplace_by_uint32(mut x: BigUInt, y: UInt32) -> None: - """Increments a BigUInt number by a UInt32 value.""" + """Increments a BigUInt number by a UInt32 value. + + Args: + x: The `BigUInt` number to increment. + y: The `UInt32` value to add. + """ var carry: UInt32 = y for i in range(len(x.words)): x.words[i] += carry @@ -481,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. @@ -530,16 +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( - file="src/decimo/biguint/arithmetics.mojo", - function="subtract_school()", - message=( - "biguint.arithmetics.subtract(): Result is negative due to" - " x < y" - ), - previous_error=None, - ) + 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 @@ -626,16 +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( - file="src/decimo/biguint/arithmetics.mojo", - function="subtract()", - message=( - "biguint.arithmetics.subtract(): Result is negative due to" - " x < y" - ), - previous_error=None, - ) + 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 @@ -679,7 +676,15 @@ def subtract_simd(x: BigUInt, y: BigUInt) raises -> BigUInt: def subtract_inplace(mut x: BigUInt, y: BigUInt) raises -> None: - """Subtracts y from x in place.""" + """Subtracts y from x in place. + + 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 if y.is_zero(): @@ -694,16 +699,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( - file="src/decimo/biguint/arithmetics.mojo", - function="subtract_inplace()", - message=( - "biguint.arithmetics.subtract(): Result is negative due to" - " x < y" - ), - previous_error=None, - ) + 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 @@ -740,6 +741,10 @@ def subtract_inplace_no_check(mut x: BigUInt, y: BigUInt) -> None: This function assumes that x >= y, and it does not check for underflow. It is the caller's responsibility to ensure that x is greater than or equal to y before calling this function. + + Args: + x: The `BigUInt` minuend, modified in place. + y: The `BigUInt` subtrahend. """ # If the subtrahend is zero, return the minuend @@ -954,6 +959,9 @@ def multiply_slices_school( y: The second BigUInt operand (multiplier). bounds_x: A tuple containing the start and end indices of the slice in x. bounds_y: A tuple containing the start and end indices of the slice in y. + + Returns: + The product of the two BigUInt slices. """ n_words_x_slice = bounds_x[1] - bounds_x[0] @@ -1797,6 +1805,9 @@ def multiply_by_power_of_billion(x: BigUInt, n: Int) -> BigUInt: In non-debug model, if n is less than or equal to 0, the function returns x unchanged. In debug mode, it asserts that n is non-negative. + + Returns: + A new `BigUInt` containing the result of the multiplication. """ debug_assert[assert_mode="none"]( n >= 0, @@ -1878,6 +1889,9 @@ def exact_divide_by_2_inplace(mut x: BigUInt): The caller must ensure that x is even (divisible by 2). Uses base-10^9 long division from MSB to LSB. + + Args: + x: The `BigUInt` value to divide, modified in place. """ var carry: UInt32 = 0 for i in range(len(x.words) - 1, -1, -1): @@ -1894,6 +1908,9 @@ def exact_divide_by_3_inplace(mut x: BigUInt): The caller must ensure that x is divisible by 3. Uses base-10^9 long division from MSB to LSB. + + Args: + x: The `BigUInt` value to divide, modified in place. """ var carry: UInt32 = 0 for i in range(len(x.words) - 1, -1, -1): @@ -1924,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. @@ -1954,13 +1971,9 @@ def floor_divide(x: BigUInt, y: BigUInt) raises -> BigUInt: # CASE: y is zero if y.is_zero(): - raise Error( - ZeroDivisionError( - file="src/decimo/biguint/arithmetics.mojo", - function="floor_divide()", - message="Division by zero", - previous_error=None, - ) + raise ZeroDivisionError( + function="floor_divide()", + message="Division by zero", ) # CASE: Dividend is zero @@ -2049,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 @@ -2057,7 +2070,9 @@ 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("biguint.arithmetics.floor_divide(): Division by zero") + raise ZeroDivisionError( + message="Division by zero", function="floor_divide()" + ) # CASE: Dividend is zero if x.is_zero(): @@ -2238,6 +2253,9 @@ def floor_divide_by_uint32(x: BigUInt, y: UInt32) -> BigUInt: This function is used internally for division by single word divisors. It is not intended for public use. You need to ensure that y is non-zero. + + Returns: + The quotient of x divided by y. """ debug_assert[assert_mode="none"]( y != 0, "biguint.arithmetics.floor_divide_by_uint32(): Division by zero" @@ -2304,8 +2322,11 @@ def floor_divide_by_uint64(x: BigUInt, y: UInt64) -> BigUInt: """Divides a BigUInt by UInt64. Args: - x: The BigUInt value to divide by the divisor. - y: The UInt64 divisor. Must be smaller than 10^18. + x: The `BigUInt` dividend. + y: The `UInt64` divisor. Must be smaller than 10^18. + + Returns: + The quotient of x divided by y. """ debug_assert[assert_mode="none"]( y != 0, @@ -2379,8 +2400,11 @@ def floor_divide_by_uint128(x: BigUInt, y: UInt128) -> BigUInt: """Divides a BigUInt by UInt128. Args: - x: The BigUInt value to divide by the divisor. - y: The UInt128 divisor. Must be smaller than 10^36. + x: The `BigUInt` dividend. + y: The `UInt128` divisor. Must be smaller than 10^36. + + Returns: + The quotient of x divided by y. """ debug_assert[assert_mode="none"]( y != 0, @@ -2630,6 +2654,9 @@ def floor_divide_burnikel_ziegler( cut_off: The cutoff value for the number of words in the divisor to use the schoolbook division algorithm. It also determines the size of the blocks used in the recursive division algorithm. + + Returns: + The quotient of `a` divided by `b`. """ var BLOCK_SIZE_OF_WORDS = cut_off @@ -3099,13 +3126,19 @@ 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. b = b1 * BASE + b0. """ if b1 < 500_000_000: - raise Error("b1 must be at least 500_000_000") + 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) @@ -3153,21 +3186,39 @@ 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: - raise Error("b1 must be at least 500_000_000") + raise ValueError( + message="b1 must be at least 500_000_000", + function="floor_divide_four_by_two_uint32()", + ) if a3 > b1: - raise Error("a must be less than b * 10^18") + 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("a must be less than b * 10^18") + 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("a must be less than b * 10^18") + 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("a must be less than b * 10^18") + 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) var q0, s1, s0 = floor_divide_three_by_two_uint32(r1, r0, a0, b1, b0) @@ -3179,6 +3230,16 @@ def truncate_divide(x1: BigUInt, x2: BigUInt) raises -> BigUInt: """Returns the quotient of two BigUInt numbers, truncating toward zero. It is equal to floored division for unsigned numbers. See `floor_divide` for more details. + + Args: + x1: The dividend. + x2: The divisor. + + Returns: + The quotient of `x1` divided by `x2`. + + Raises: + ZeroDivisionError: If the divisor is zero. """ return floor_divide(x1, x2) @@ -3194,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 @@ -3203,7 +3264,9 @@ def ceil_divide(x1: BigUInt, x2: BigUInt) raises -> BigUInt: len(x2.words) == 1, "ceil_divide(): leading zero words", ) - raise Error("biguint.arithmetics.ceil_divide(): Division by zero") + raise ZeroDivisionError( + message="Division by zero", function="ceil_divide()" + ) # Apply floor division and check if there is a remainder var quotient = floor_divide(x1, x2) @@ -3226,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. @@ -3238,13 +3299,9 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: len(x2.words) == 1, "truncate_modulo(): leading zero words", ) - raise Error( - ZeroDivisionError( - file="src/decimo/biguint/arithmetics.py", - function="floor_modulo()", - message="Division by zero", - previous_error=None, - ) + raise ZeroDivisionError( + function="floor_modulo()", + message="Division by zero", ) # CASE: Dividend is zero @@ -3267,13 +3324,10 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: try: quotient = floor_divide(x1, x2) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/arithmetics.py", - function="floor_modulo()", - message=None, - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="floor_modulo()", + previous_error=e^, ) # Calculate remainder: dividend - (divisor * quotient) @@ -3281,13 +3335,10 @@ def floor_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: try: remainder = subtract(x1, multiply(x2, quotient)) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/arithmetics.py", - function="floor_modulo()", - message=None, - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="floor_modulo()", + previous_error=e^, ) return remainder^ @@ -3307,19 +3358,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( - file="src/decimo/biguint/arithmetics.py", - function="truncate_modulo()", - message=None, - previous_error=e^, - ) - ) + raise e^ def ceil_modulo(x1: BigUInt, x2: BigUInt) raises -> BigUInt: @@ -3335,14 +3379,16 @@ 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("Error in `ceil_modulo`: Division by zero") + raise ZeroDivisionError( + message="Division by zero", function="ceil_modulo()" + ) # CASE: Dividend is zero if x1.is_zero(): @@ -3386,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. @@ -3398,13 +3443,10 @@ def floor_divide_modulo( var remainder = subtract(x1, multiply(x2, quotient)) return (quotient^, remainder^) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/arithmetics.py", - function="floor_divide_modulo()", - message=None, - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="floor_divide_modulo()", + previous_error=e^, ) @@ -3423,6 +3465,9 @@ def normalize_carries_lt_2_bases(mut x: BigUInt): a situation where some words are larger than BASE. This function normalizes the carries, ensuring that all words are within the valid range. It modifies the input BigUInt in-place. + + Args: + x: The `BigUInt` to normalize, modified in place. """ # Yuhao ZHU: @@ -3459,6 +3504,9 @@ def normalize_carries_lt_4_bases(mut x: BigUInt): a situation where some words are ge than BASE but le BASE * 4 - 4. This function normalizes the carries, ensuring that all words are within the valid range. It modifies the input BigUInt in-place. + + Args: + x: The `BigUInt` to normalize, modified in place. """ # Yuhao ZHU: @@ -3536,6 +3584,9 @@ def normalize_borrows(mut x: BigUInt): a situation where some words are **underflowed**. We can take advantage of the overflowed values of the words to normalize the borrows, ensuring that all words are within the valid range. + + Args: + x: The `BigUInt` to normalize, modified in place. """ comptime NEG_BASE_MAX = UInt32(3294967297) # UInt32(0) - BigUInt.BASE_MAX @@ -3573,16 +3624,12 @@ 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 Error( - DecimoError( - file="src/decimo/biguint/arithmetics.py", - function="power_of_10()", - message="Negative exponent not supported", - previous_error=None, - ) + raise ValueError( + function="power_of_10()", + message="Negative exponent not supported", ) if n == 0: @@ -3664,7 +3711,15 @@ def calculate_ndigits_for_normalization(msw: UInt32) -> Int: def to_uint64_with_2_words(a: BigUInt, bounds_x: Tuple[Int, Int]) -> UInt64: - """Convert two words at given index of the BigUInt to UInt64.""" + """Convert two words at given index of the BigUInt to UInt64. + + Args: + a: The `BigUInt` containing the words to convert. + bounds_x: A tuple of (start, end) indices specifying the word slice. + + Returns: + The `UInt64` representation of the specified words. + """ var n_words = bounds_x[1] - bounds_x[0] if n_words == 1: return ( @@ -3678,7 +3733,15 @@ def to_uint64_with_2_words(a: BigUInt, bounds_x: Tuple[Int, Int]) -> UInt64: def to_uint128_with_2_words(a: BigUInt, bounds_x: Tuple[Int, Int]) -> UInt128: - """Convert two words at given index of the BigUInt to UInt128.""" + """Convert two words at given index of the BigUInt to UInt128. + + Args: + a: The `BigUInt` containing the words to convert. + bounds_x: A tuple of (start, end) indices specifying the word slice. + + Returns: + The `UInt128` representation of the specified words. + """ var n_words = bounds_x[1] - bounds_x[0] if n_words == 1: return ( @@ -3696,7 +3759,15 @@ def to_uint128_with_2_words(a: BigUInt, bounds_x: Tuple[Int, Int]) -> UInt128: def to_uint128_with_4_words(a: BigUInt, bounds_x: Tuple[Int, Int]) -> UInt128: - """Convert four words at given index of the BigUInt to UInt128.""" + """Convert four words at given index of the BigUInt to UInt128. + + Args: + a: The `BigUInt` containing the words to convert. + bounds_x: A tuple of (start, end) indices specifying the word slice. + + Returns: + The `UInt128` representation of the specified words. + """ var n_words = bounds_x[1] - bounds_x[0] if n_words == 1: return ( diff --git a/src/decimo/biguint/biguint.mojo b/src/decimo/biguint/biguint.mojo index bac17abe..e6b6efaf 100644 --- a/src/decimo/biguint/biguint.mojo +++ b/src/decimo/biguint/biguint.mojo @@ -29,16 +29,17 @@ 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 # Type aliases comptime BUInt = BigUInt +"""A shorthand alias for `BigUInt`.""" struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @@ -84,28 +85,47 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): """The width of the SIMD vector used for arithmetic operations (128-bit).""" comptime ZERO = Self.zero() + """A `BigUInt` constant representing zero.""" comptime ONE = Self.one() + """A `BigUInt` constant representing one.""" comptime MAX_UINT64 = Self(raw_words=[709551615, 446744073, 18]) + """A `BigUInt` constant representing the maximum value of `UInt64`.""" comptime MAX_UINT128 = Self( raw_words=[768211455, 374607431, 938463463, 282366920, 340] ) + """A `BigUInt` constant representing the maximum value of `UInt128`.""" @always_inline @staticmethod def zero() -> Self: - """Returns a BigUInt with value 0.""" + """Returns a BigUInt with value 0. + + Returns: + A `BigUInt` with value 0. + """ return Self() @always_inline @staticmethod def one() -> Self: - """Returns a BigUInt with value 1.""" + """Returns a BigUInt with value 1. + + Returns: + A `BigUInt` with value 1. + """ return Self(raw_words=[UInt32(1)]) @staticmethod @always_inline def power_of_10(exponent: Int) raises -> Self: - """Calculates 10^exponent efficiently.""" + """Calculates 10^exponent efficiently. + + Args: + exponent: The power of 10 to compute. + + Returns: + A `BigUInt` representing 10 raised to the power of `exponent`. + """ return decimo.biguint.arithmetics.power_of_10(exponent) # ===------------------------------------------------------------------=== # @@ -176,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()`. @@ -183,13 +206,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: self = Self.from_list(words^) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__init__(var words: List[UInt32])", - message=None, - 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]): @@ -218,28 +238,31 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): # TODO: If Mojo makes Int type an alias of SIMD[DType.index, 1], # we can remove this method. def __init__(out self, value: Int) raises: - """Initializes a BigUInt from an Int. + """Initializes a BigUInt from an `Int`. See `from_int()` for more information. + Args: + 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) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__init__(value: Int)", - message=None, - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function="BigUInt.__init__(value: Int)", + previous_error=e^, ) @implicit def __init__(out self, value: Scalar): """Initializes a BigUInt from an unsigned integral scalar. See `from_unsigned_integral_scalar()` for more information. + + Args: + value: The unsigned integral scalar to initialize the `BigUInt` from. """ self = Self.from_unsigned_integral_scalar(value) @@ -253,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()`. @@ -261,13 +284,10 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): try: self = Self.from_string(value, ignore_sign=ignore_sign) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__init__(value: String)", - message=None, - previous_error=e^, - ) + raise ConversionError( + message="See the above exception.", + function="BigUInt.__init__(value: String)", + previous_error=e^, ) # ===------------------------------------------------------------------=== # @@ -293,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. @@ -305,17 +325,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()", - file="src/decimo/biguint/biguint.mojo", - previous_error=None, - ) + raise OverflowError( + message=( + "Word value " + + String(word) + + " exceeds maximum value of 999_999_999" + ), + function="BigUInt.from_list()", ) var res = Self(raw_words=words^) @@ -351,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: @@ -363,6 +379,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): BigUInt.from_words(123456789, 987654321) # 987654321_123456789 ``` End of examples. + + Returns: + The `BigUInt` representation of the given words. """ var list_of_words = List[UInt32](capacity=len(words)) @@ -370,17 +389,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()", - file="src/decimo/biguint/biguint.mojo", - previous_error=None, - ) + raise OverflowError( + message=( + "Word value " + + String(word) + + " exceeds maximum value of 999_999_999" + ), + function="BigUInt.from_words()", ) else: list_of_words.append(word) @@ -392,11 +407,14 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): """Initializes a BigUInt from a BigUInt slice. Args: - value: The BigUInt to copy from. + value: The `BigUInt` to copy from. bounds: A tuple of two integers representing the bounds for the words to copy. The first integer is the start index (inclusive), and the second integer is the end index (exclusive). + + Returns: + A new `BigUInt` containing the specified slice of words. """ # Safty checks on bounds var start_index: Int @@ -437,23 +455,19 @@ 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() if value < 0: - raise Error( - OverflowError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.from_int(value: Int)", - message=( - "The input value " - + String(value) - + " is negative and is not compatible with BigUInt." - ), - previous_error=None, - ) + 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]() @@ -470,7 +484,13 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @staticmethod def from_uint32_unsafe(unsafe_value: UInt32) -> Self: - """Creates a BigUInt from an `UInt32` object without checking the value. + """Creates a BigUInt from a `UInt32` object without checking the value. + + Args: + unsafe_value: The `UInt32` value to wrap directly as a single word. + + Returns: + A single-word `BigUInt` containing the given value. """ return Self(raw_words=[unsafe_value]) @@ -503,6 +523,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): - UInt32: Check whether one word or two words are needed. - UInt64, UInt128, etc: repeatedly divide by 1_000_000_000. - UIndex (UInt): repeatedly divide by 1_000_000_000. + + Parameters: + dtype: The scalar data type, must be integral and unsigned. """ # Only allow unsigned integral types @@ -560,6 +583,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Returns: The BigUInt representation of the Scalar value. + + Parameters: + dtype: The scalar data type, must be integral. """ comptime assert dtype.is_integral(), "dtype must be integral." @@ -608,7 +634,7 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @staticmethod def from_string(value: String, ignore_sign: Bool = False) raises -> BigUInt: """Initializes a BigUInt from a string representation. - The string is normalized with `deciomojo.str.parse_numeric_string()`. + The string is normalized with `decimo.str.parse_numeric_string()`. Args: value: The string representation of the BigUInt. @@ -633,19 +659,14 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): var sign: Bool = _tuple[2] if (not ignore_sign) and sign: - raise Error( - OverflowError( - file="src/decimo/biguint/biguint.mojo", - 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." - ), - previous_error=None, - ) + 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 @@ -657,33 +678,25 @@ 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( - file="src/decimo/biguint/biguint.mojo", + 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." ), - previous_error=None, - ) - ) - for i in range(1, scale + 1): - if coef[-i] != 0: - raise Error( - ConversionError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.from_string(value: String)", - message=( - 'The input value "' - + value - + '" is not an integer.\n' - + "The fractional part is not zero." - ), - previous_error=None, - ) ) coef.resize(len(coef) - scale, UInt8(0)) scale = 0 @@ -755,22 +768,26 @@ 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() except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__int__()", - message=None, - 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): - """Writes the debug representation to a writer.""" + """Writes the debug representation to a writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ writer.write('BigUInt("', self.to_string(), '")') # ===------------------------------------------------------------------=== # @@ -780,6 +797,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def write_to[W: Writer](self, mut writer: W): """Writes the BigUInt to a writer. This implement the `write` method of the `Writer` trait. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. """ writer.write(self.to_string()) @@ -796,25 +819,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( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.to_int()", - message="The number exceeds the size of Int (" - + String(Int.MAX) - + ")", - previous_error=None, - ) + 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) @@ -825,20 +847,16 @@ 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 Error( - OverflowError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.to_uint64()", - message=( - "The number exceeds the size of UInt64 (" - + String(UInt64.MAX) - + ")" - ), - previous_error=None, - ) + raise OverflowError( + function="BigUInt.to_uint64()", + message=( + "The number exceeds the size of UInt64 (" + + String(UInt64.MAX) + + ")" + ), ) if len(self.words) == 1: @@ -864,6 +882,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Notes: This method quickly convert BigUInt with 2 words into UInt64. + + Returns: + The `UInt64` representation of the first two words. """ if len(self.words) == 1: return self.words.unsafe_ptr().load[width=1]().cast[DType.uint64]() @@ -942,6 +963,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Notes: This method quickly convert BigUInt with 4 words into UInt128. + + Returns: + The `UInt128` representation of the first four words. """ if len(self.words) == 1: @@ -1047,6 +1071,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def __abs__(self) -> Self: """Returns the absolute value of this number. See `absolute()` for more information. + + Returns: + The absolute value. """ return decimo.biguint.arithmetics.absolute(self) @@ -1054,12 +1081,24 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def __neg__(self) raises -> Self: """Returns the negation of this number. See `negative()` for more information. + + Returns: + The negated value. + + Raises: + OverflowError: If the number is non-zero (negative of unsigned is undefined). """ return decimo.biguint.arithmetics.negative(self) @always_inline def __rshift__(self, shift_amount: Int) -> Self: """Returns the result of floored divison by 2 to the power of `shift_amount`. + + Args: + shift_amount: The number of bit positions to shift right. + + Returns: + The floor-divided value. """ var result = self.copy() for _ in range(shift_amount): @@ -1074,109 +1113,172 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def __add__(self, other: Self) -> Self: + """Adds two values. + + Args: + other: The right-hand side operand. + + Returns: + The sum. + """ return decimo.biguint.arithmetics.add(self, other) @always_inline def __sub__(self, other: Self) raises -> Self: + """Subtracts two values. + + Args: + other: The right-hand side operand. + + Returns: + The difference. + + Raises: + OverflowError: If the result would be negative. + """ try: return decimo.biguint.arithmetics.subtract(self, other) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__sub__(other: Self)", - message=None, - previous_error=e^, - ) - ) + raise e^ @always_inline def __mul__(self, other: Self) -> Self: + """Multiplies two values. + + Args: + other: The right-hand side operand. + + Returns: + The product. + """ return decimo.biguint.arithmetics.multiply(self, other) @always_inline def __floordiv__(self, other: Self) raises -> Self: + """Divides two values using floor division. + + Args: + other: The right-hand side operand. + + Returns: + The quotient. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ try: return decimo.biguint.arithmetics.floor_divide(self, other) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__floordiv__(other: Self)", - message=None, - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigUInt.__floordiv__(other: Self)", + previous_error=e^, ) @always_inline def __ceildiv__(self, other: Self) raises -> Self: - """Returns the result of ceiling division.""" + """Returns the result of ceiling division. + + Args: + other: The right-hand side operand. + + Returns: + The quotient rounded up. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ try: return decimo.biguint.arithmetics.ceil_divide(self, other) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__ceildiv__(other: Self)", - message=None, - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigUInt.__ceildiv__(other: Self)", + previous_error=e^, ) @always_inline def __mod__(self, other: Self) raises -> Self: + """Returns the remainder of division. + + Args: + other: The right-hand side operand. + + Returns: + The remainder. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ try: return decimo.biguint.arithmetics.floor_modulo(self, other) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__mod__(other: Self)", - message=None, - previous_error=e^, - ) + raise ZeroDivisionError( + message="See the above exception.", + function="BigUInt.__mod__(other: Self)", + previous_error=e^, ) @always_inline def __divmod__(self, other: Self) raises -> Tuple[Self, Self]: + """Returns the quotient and remainder of division. + + Args: + other: The right-hand side operand. + + Returns: + A tuple of (quotient, remainder). + + Raises: + ZeroDivisionError: If the divisor is zero. + """ try: return decimo.biguint.arithmetics.floor_divide_modulo(self, other) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__divmod__(other: Self)", - message=None, - previous_error=e^, - ) - ) + raise e^ @always_inline def __pow__(self, exponent: Self) raises -> Self: + """Raises to a power. + + Args: + exponent: The exponent to raise this number to. + + Returns: + The power `self` raised to `exponent`. + + Raises: + ValueError: If the exponent is too large. + """ try: return self.power(exponent) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__pow__(exponent: Self)", - message=None, - previous_error=e^, - ) + raise ValueError( + message="See the above exception.", + function="BigUInt.__pow__(exponent: Self)", + previous_error=e^, ) @always_inline def __pow__(self, exponent: Int) raises -> Self: + """Raises to a power. + + Args: + exponent: The exponent to raise this number to. + + Returns: + The power `self` raised to `exponent`. + + Raises: + ValueError: If the exponent is negative or too large. + """ try: return self.power(exponent) except e: - raise Error( - DecimoError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.__pow__(exponent: Int)", - message=None, - previous_error=e^, - ) + raise ValueError( + message="See the above exception.", + function="BigUInt.__pow__(exponent: Int)", + previous_error=e^, ) # ===------------------------------------------------------------------=== # @@ -1187,30 +1289,101 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def __radd__(self, other: Self) raises -> Self: + """Adds two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The sum. + """ return decimo.biguint.arithmetics.add(self, other) @always_inline def __rsub__(self, other: Self) raises -> Self: + """Subtracts two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The difference. + + Raises: + OverflowError: If the result would be negative. + """ return decimo.biguint.arithmetics.subtract(other, self) @always_inline def __rmul__(self, other: Self) raises -> Self: + """Multiplies two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The product. + """ return decimo.biguint.arithmetics.multiply(self, other) @always_inline def __rfloordiv__(self, other: Self) raises -> Self: + """Divides two values using floor division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The quotient. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ return decimo.biguint.arithmetics.floor_divide(other, self) @always_inline def __rmod__(self, other: Self) raises -> Self: + """Returns the remainder of division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The remainder. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ return decimo.biguint.arithmetics.floor_modulo(other, self) @always_inline def __rdivmod__(self, other: Self) raises -> Tuple[Self, Self]: + """Returns the quotient and remainder of division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + A tuple of (quotient, remainder). + + Raises: + ZeroDivisionError: If the divisor is zero. + """ return decimo.biguint.arithmetics.floor_divide_modulo(other, self) @always_inline def __rpow__(self, base: Self) raises -> Self: + """Raises to a power (reflected). + + Args: + base: The base to raise to the power of `self`. + + Returns: + The power `base` raised to `self`. + + Raises: + ValueError: If the exponent is too large. + """ return base.power(self) # ===------------------------------------------------------------------=== # @@ -1224,6 +1397,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def __iadd__(mut self, other: Self): """Adds `other` to `self` in place. See `biguint.arithmetics.add_inplace()` for more information. + + Args: + other: The operand to add. """ decimo.biguint.arithmetics.add_inplace(self, other) @@ -1231,19 +1407,46 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def __isub__(mut self, other: Self) raises: """Subtracts `other` from `self` in place. See `biguint.arithmetics.subtract_inplace()` for more information. + + Args: + other: The operand to subtract. + + Raises: + OverflowError: If the result would be negative. """ decimo.biguint.arithmetics.subtract_inplace(self, other) @always_inline def __imul__(mut self, other: Self) raises: + """Multiplies in place. + + Args: + other: The operand to multiply by. + """ self = decimo.biguint.arithmetics.multiply(self, other) @always_inline def __ifloordiv__(mut self, other: Self) raises: + """Divides in place using floor division. + + Args: + other: The divisor. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ self = decimo.biguint.arithmetics.floor_divide(self, other) @always_inline def __imod__(mut self, other: Self) raises: + """Computes the remainder in place. + + Args: + other: The divisor. + + Raises: + ZeroDivisionError: If the divisor is zero. + """ self = decimo.biguint.arithmetics.floor_modulo(self, other) # ===------------------------------------------------------------------=== # @@ -1253,32 +1456,74 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def __gt__(self, other: Self) -> Bool: - """Returns True if self > other.""" + """Returns True if self > other. + + Args: + other: The value to compare against. + + Returns: + `True` if `self` is greater than `other`, `False` otherwise. + """ return decimo.biguint.comparison.greater(self, other) @always_inline def __ge__(self, other: Self) -> Bool: - """Returns True if self >= other.""" + """Returns True if self >= other. + + Args: + other: The value to compare against. + + Returns: + `True` if `self` is greater than or equal to `other`, `False` otherwise. + """ return decimo.biguint.comparison.greater_equal(self, other) @always_inline def __lt__(self, other: Self) -> Bool: - """Returns True if self < other.""" + """Returns True if self < other. + + Args: + other: The value to compare against. + + Returns: + `True` if `self` is less than `other`, `False` otherwise. + """ return decimo.biguint.comparison.less(self, other) @always_inline def __le__(self, other: Self) -> Bool: - """Returns True if self <= other.""" + """Returns True if self <= other. + + Args: + other: The value to compare against. + + Returns: + `True` if `self` is less than or equal to `other`, `False` otherwise. + """ return decimo.biguint.comparison.less_equal(self, other) @always_inline def __eq__(self, other: Self) -> Bool: - """Returns True if self == other.""" + """Returns True if self == other. + + Args: + other: The value to compare against. + + Returns: + `True` if `self` equals `other`, `False` otherwise. + """ return decimo.biguint.comparison.equal(self, other) @always_inline def __ne__(self, other: Self) -> Bool: - """Returns True if self != other.""" + """Returns True if self != other. + + Args: + other: The value to compare against. + + Returns: + `True` if `self` does not equal `other`, `False` otherwise. + """ return decimo.biguint.comparison.not_equal(self, other) # ===------------------------------------------------------------------=== # @@ -1286,11 +1531,25 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): # ===------------------------------------------------------------------=== # def __merge_with__[other_type: type_of(BigInt10)](self) -> BigInt10: - "Merges this BigUInt with a BigInt10 into a BigInt10." + """Merges this BigUInt with a BigInt10 into a BigInt10. + + Parameters: + other_type: The target type. + + Returns: + A BigInt10 value. + """ return BigInt10(self) def __merge_with__[other_type: type_of(BigDecimal)](self) -> BigDecimal: - "Merges this BigUInt with a BigDecimal into a BigDecimal." + """Merges this BigUInt with a BigDecimal into a BigDecimal. + + Parameters: + other_type: The target type. + + Returns: + A BigDecimal value. + """ return BigDecimal(self) # ===------------------------------------------------------------------=== # @@ -1302,6 +1561,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): """Adds `other` to this number in place. It is equal to `self += other`. See `add_inplace()` for more information. + + Args: + other: The operand to add. """ decimo.biguint.arithmetics.add_inplace(self, other) @@ -1310,6 +1572,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): """Returns the result of floor dividing this number by `other`. It is equal to `self // other`. See `floor_divide()` for more information. + + Args: + other: The divisor. + + Returns: + The quotient. """ return decimo.biguint.arithmetics.floor_divide(self, other) @@ -1318,6 +1586,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): """Returns the result of truncate dividing this number by `other`. It is equal to `self // other`. See `truncate_divide()` for more information. + + Args: + other: The divisor. + + Returns: + The quotient. """ return decimo.biguint.arithmetics.truncate_divide(self, other) @@ -1325,6 +1599,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def ceil_divide(self, other: Self) raises -> Self: """Returns the result of ceil dividing this number by `other`. See `ceil_divide()` for more information. + + Args: + other: The divisor. + + Returns: + The quotient rounded up. """ return decimo.biguint.arithmetics.ceil_divide(self, other) @@ -1332,6 +1612,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def floor_modulo(self, other: Self) raises -> Self: """Returns the result of floor modulo this number by `other`. See `floor_modulo()` for more information. + + Args: + other: The divisor. + + Returns: + The remainder. """ return decimo.biguint.arithmetics.floor_modulo(self, other) @@ -1339,6 +1625,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def truncate_modulo(self, other: Self) raises -> Self: """Returns the result of truncate modulo this number by `other`. See `truncate_modulo()` for more information. + + Args: + other: The divisor. + + Returns: + The remainder. """ return decimo.biguint.arithmetics.truncate_modulo(self, other) @@ -1346,6 +1638,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def ceil_modulo(self, other: Self) raises -> Self: """Returns the result of ceil modulo this number by `other`. See `ceil_modulo()` for more information. + + Args: + other: The divisor. + + Returns: + The remainder. """ return decimo.biguint.arithmetics.ceil_modulo(self, other) @@ -1353,6 +1651,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def divmod(self, other: Self) raises -> Tuple[Self, Self]: """Returns the result of divmod this number by `other`. See `divmod()` for more information. + + Args: + other: The divisor. + + Returns: + A tuple of (quotient, remainder). """ return decimo.biguint.arithmetics.floor_divide_modulo(self, other) @@ -1367,6 +1671,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def multiply_by_power_of_ten(self, n: Int) -> Self: """Returns the result of multiplying this number by 10^n (n>=0). See `multiply_by_power_of_ten()` for more information. + + Args: + n: The power of 10 to multiply by. + + Returns: + The product of this number and 10^n. """ return decimo.biguint.arithmetics.multiply_by_power_of_ten(self, n) @@ -1374,6 +1684,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def multiply_inplace_by_power_of_ten(mut self, n: Int): """Multiplies this number in-place by 10^n (n>=0). See `multiply_inplace_by_power_of_ten()` for more information. + + Args: + n: The power of 10 to multiply by. """ decimo.biguint.arithmetics.multiply_inplace_by_power_of_ten(self, n) @@ -1382,6 +1695,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): """Returns the result of floored dividing this number by 10^n (n>=0). It is equal to removing the last n digits of the number. See `floor_divide_by_power_of_ten()` for more information. + + Args: + n: The power of 10 to divide by. + + Returns: + The quotient after removing the last `n` digits. """ return decimo.biguint.arithmetics.floor_divide_by_power_of_ten(self, n) @@ -1402,44 +1721,35 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): exponent: The exponent to raise the number to. Returns: - ValueError: If the exponent is negative. - ValueError: If the exponent is too large. + 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 Error( - ValueError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.power(exponent: Int)", - message=( - "The exponent " - + String(exponent) - + " is negative.\n" - + "Consider using a non-negative exponent." - ), - previous_error=None, - ) + 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( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.power(exponent: Int)", - message=( - "The exponent " - + String(exponent) - + " is too large.\n" - + "Consider using an exponent below 1_000_000_000." - ), - previous_error=None, - ) + 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]) @@ -1466,18 +1776,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( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.power(exponent: BigUInt)", - message=( - "The exponent " - + String(exponent) - + " is too large.\n" - + "Consider using an exponent below 1_000_000_000." - ), - previous_error=None, - ) + 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) @@ -1511,6 +1817,12 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): def compare(self, other: Self) -> Int8: """Compares the magnitudes of two BigUInts. See `compare()` for more information. + + Args: + other: The value to compare against. + + Returns: + A positive value if `self > other`, 0 if equal, or a negative value if `self < other`. """ return decimo.biguint.comparison.compare(self, other) @@ -1519,7 +1831,11 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): # ===------------------------------------------------------------------=== # def internal_representation(self) -> String: - """Returns the internal representation details as a String.""" + """Returns the internal representation details as a String. + + Returns: + A formatted string showing the number and its individual words. + """ # Collect all labels to find max width var max_label_len = len("number:") for i in range(len(self.words)): @@ -1561,7 +1877,11 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def is_zero(self) -> Bool: - """Returns True if this BigUInt represents zero.""" + """Returns True if this BigUInt represents zero. + + Returns: + `True` if the value is zero, `False` otherwise. + """ # Yuhao ZHU: # BigUInt are desgined to have no leading zero words, # so that we only need to check words[0] for zero. @@ -1603,7 +1923,11 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def is_one(self) -> Bool: - """Returns True if this BigUInt represents one.""" + """Returns True if this BigUInt represents one. + + Returns: + `True` if the value is one, `False` otherwise. + """ if self.words[0] != 1: # Least significant word is not 1 return False @@ -1621,7 +1945,11 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def is_two(self) -> Bool: - """Returns True if this BigUInt represents two.""" + """Returns True if this BigUInt represents two. + + Returns: + `True` if the value is two, `False` otherwise. + """ if len(self.words) != 2: return False for i in self.words[1:]: @@ -1631,7 +1959,11 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def is_power_of_10(x: BigUInt) -> Bool: - """Check if x is a power of 10.""" + """Check if x is a power of 10. + + Returns: + `True` if the value is a power of 10, `False` otherwise. + """ for i in range(len(x.words) - 1): if x.words[i] != 0: return False @@ -1652,12 +1984,20 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def is_unitialized(self) -> Bool: - """Returns True if the BigUInt is uninitialized.""" + """Returns True if the BigUInt is uninitialized. + + Returns: + `True` if the words list is empty, `False` otherwise. + """ return len(self.words) == 0 @always_inline def is_uint64_overflow(self) -> Bool: - """Returns True if the BigUInt larger than UInt64.MAX.""" + """Returns True if the BigUInt larger than UInt64.MAX. + + Returns: + `True` if the value exceeds `UInt64.MAX`, `False` otherwise. + """ # UInt64.MAX: 18_446_744_073_709_551_615 # word 0: 709551615 # word 1: 446744073 @@ -1677,7 +2017,11 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): @always_inline def is_uint128_overflow(self) -> Bool: - """Returns True if the BigUInt larger than UInt128.MAX.""" + """Returns True if the BigUInt larger than UInt128.MAX. + + Returns: + `True` if the value exceeds `UInt128.MAX`, `False` otherwise. + """ # UInt128.MAX: 340_282_366_920_938_463_463_374_607_431_768_211_455 # word 0: 768211455 # word 1: 374607431 @@ -1719,18 +2063,14 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): IndexError: If the index is negative. """ if i < 0: - raise Error( - IndexError( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.ith_digit(i: Int)", - message=( - "The index " - + String(i) - + " is negative.\n" - + "Consider using a non-negative index." - ), - previous_error=None, - ) + 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 @@ -1750,6 +2090,9 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): Notes: Zero has 1 digit. + + Returns: + The total number of decimal digits in the BigUInt. """ if self.is_zero(): debug_assert(len(self.words) == 1, "There are leading zero words.") @@ -1763,11 +2106,19 @@ struct BigUInt(Absable, Copyable, IntableRaising, Movable, Writable): return result def number_of_words(self) -> Int: - """Returns the number of words in the BigUInt.""" + """Returns the number of words in the BigUInt. + + Returns: + The number of `UInt32` words in the internal representation. + """ return len(self.words) def number_of_trailing_zeros(self) -> Int: - """Returns the number of trailing zeros in the BigUInt.""" + """Returns the number of trailing zeros in the BigUInt. + + Returns: + The count of trailing zero digits in the decimal representation. + """ var result: Int = 0 for i in range(len(self.words)): if self.words[i] == 0: @@ -1850,33 +2201,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( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.remove_trailing_digits_with_rounding()", - message=( - "The number of digits to remove is negative: " - + String(ndigits) - ), - previous_error=None, - ) + 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( - file="src/decimo/biguint/biguint.mojo", - 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()) - ), - previous_error=None, - ) + 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 @@ -1928,13 +2271,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( - file="src/decimo/biguint/biguint.mojo", - function="BigUInt.remove_trailing_digits_with_rounding()", - message=("Unknown rounding mode: " + String(rounding_mode)), - previous_error=None, - ) + raise ValueError( + function="BigUInt.remove_trailing_digits_with_rounding()", + message=("Unknown rounding mode: " + String(rounding_mode)), ) if round_up: diff --git a/src/decimo/biguint/exponential.mojo b/src/decimo/biguint/exponential.mojo index 9e69edb0..2abaf0f8 100644 --- a/src/decimo/biguint/exponential.mojo +++ b/src/decimo/biguint/exponential.mojo @@ -138,6 +138,12 @@ def sqrt_initial_guess(x: BigUInt) -> BigUInt: The words of the BigUInt should be more than 2. The initial guess is always smaller or equal to the actual square root. + + Args: + x: The `BigUInt` value to estimate the square root of. + + Returns: + An initial guess that is less than or equal to the actual square root. """ # Yuhao ZHU: diff --git a/src/decimo/decimal128/arithmetics.mojo b/src/decimo/decimal128/arithmetics.mojo index ff93d9f3..b8b5182d 100644 --- a/src/decimo/decimal128/arithmetics.mojo +++ b/src/decimo/decimal128/arithmetics.mojo @@ -36,6 +36,10 @@ from std import testing from decimo.decimal128.decimal128 import Decimal128 from decimo.rounding_mode import RoundingMode +from decimo.errors import ( + OverflowError, + ZeroDivisionError, +) import decimo.decimal128.utility @@ -53,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() @@ -124,7 +128,10 @@ 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("Error in `addition()`: Decimal128 overflow") + raise OverflowError( + message="Decimal128 overflow in addition.", + function="add()", + ) return Decimal128.from_uint128(summation, 0, x1.is_negative()) @@ -154,7 +161,10 @@ 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("Error in `addition()`: Decimal128 overflow") + raise OverflowError( + message="Decimal128 overflow in addition.", + function="add()", + ) # Determine the scale for the result var scale = UInt32( @@ -357,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. @@ -374,7 +387,11 @@ def subtract(x1: Decimal128, x2: Decimal128) raises -> Decimal128: try: return x1 + (-x2) except e: - raise Error("Error in `subtract()`; ", e) + raise OverflowError( + message="Subtraction failed.", + function="subtract()", + previous_error=e^, + ) def negative(x: Decimal128) -> Decimal128: @@ -425,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() @@ -544,11 +564,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( - String( - "Error in `multiply()`: The product is {}, which" - " exceeds the capacity of Decimal128 (2^96-1)" - ).format(prod) + raise OverflowError( + message=String( + "The product {} exceeds Decimal128 capacity." + ).format(prod), + function="multiply()", ) else: return Decimal128.from_uint128(prod, 0, is_negative) @@ -556,11 +576,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( - String( - "Error in `multiply()`: The product is {}, which exceeds" - " the capacity of Decimal128 (2^96-1)" - ).format(prod) + raise OverflowError( + message=String( + "The product {} exceeds Decimal128 capacity." + ).format(prod), + function="multiply()", ) # SPECIAL CASE: Both operands are integers but with scales @@ -576,7 +596,10 @@ def multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: x2_integral_part ) if prod > Decimal128.MAX_AS_UINT256: - raise Error("Error in `multiply()`: Decimal128 overflow") + raise OverflowError( + message="Decimal128 overflow in multiplication.", + function="multiply()", + ) else: var num_digits = decimo.decimal128.utility.number_of_digits(prod) var final_scale = min( @@ -668,7 +691,10 @@ 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("Error in `multiply()`: Decimal128 overflow") + raise OverflowError( + message="Decimal128 overflow in multiplication.", + function="multiply()", + ) # Otherwise, the value will not overflow even after rounding # Determine the final scale after rounding @@ -729,7 +755,10 @@ 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("Error in `multiply()`: Decimal128 overflow") + raise OverflowError( + message="Decimal128 overflow in multiplication.", + function="multiply()", + ) # Otherwise, the value will not overflow even after rounding # Determine the final scale after rounding @@ -781,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 @@ -791,7 +821,10 @@ def divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: # 特例: 除數爲零 # Check for division by zero if x2.is_zero(): - raise Error("Error in `__truediv__`: Division by zero") + raise ZeroDivisionError( + message="Division by zero.", + function="divide()", + ) # SPECIAL CASE: zero dividend # If dividend is zero, return zero with appropriate scale @@ -848,7 +881,10 @@ 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("Error in `true_divide()`: Decimal128 overflow") + raise OverflowError( + message="Decimal128 overflow in division.", + function="divide()", + ) else: var low = UInt32(quot & 0xFFFFFFFF) var mid = UInt32((quot >> 32) & 0xFFFFFFFF) @@ -913,7 +949,10 @@ 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("Error in `true_divide()`: Decimal128 overflow") + raise OverflowError( + message="Decimal128 overflow in division.", + function="divide()", + ) else: var low = UInt32(quot & 0xFFFFFFFF) var mid = UInt32((quot >> 32) & 0xFFFFFFFF) @@ -1190,7 +1229,10 @@ 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("Error in `true_divide()`: Decimal128 overflow") + raise OverflowError( + message="Decimal128 overflow in division.", + function="divide()", + ) var scale_of_truncated_quot = ( Decimal128.MAX_NUM_DIGITS - ndigits_quot_int_part @@ -1237,11 +1279,15 @@ 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()) except e: - raise Error("Error in `divide()`: ", e) + raise e^ def modulo(x1: Decimal128, x2: Decimal128) raises -> Decimal128: @@ -1254,8 +1300,12 @@ 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) except e: - raise Error("Error in `modulo()`: ", e) + raise e^ diff --git a/src/decimo/decimal128/constants.mojo b/src/decimo/decimal128/constants.mojo index b9b5bb67..97d5ad11 100644 --- a/src/decimo/decimal128/constants.mojo +++ b/src/decimo/decimal128/constants.mojo @@ -17,6 +17,7 @@ """Useful constants for Decimal128 type.""" from decimo.decimal128.decimal128 import Decimal128 +from decimo.errors import ValueError # ===----------------------------------------------------------------------=== # # @@ -31,67 +32,111 @@ from decimo.decimal128.decimal128 import Decimal128 @always_inline def M0() -> Decimal128: - """Returns 0 as a Decimal128.""" + """Returns 0 as a Decimal128. + + Returns: + The `Decimal128` value 0. + """ return Decimal128(0x0, 0x0, 0x0, 0x0) @always_inline def M1() -> Decimal128: - """Returns 1 as a Decimal128.""" + """Returns 1 as a Decimal128. + + Returns: + The `Decimal128` value 1. + """ return Decimal128(0x1, 0x0, 0x0, 0x0) @always_inline def M2() -> Decimal128: - """Returns 2 as a Decimal128.""" + """Returns 2 as a Decimal128. + + Returns: + The `Decimal128` value 2. + """ return Decimal128(0x2, 0x0, 0x0, 0x0) @always_inline def M3() -> Decimal128: - """Returns 3 as a Decimal128.""" + """Returns 3 as a Decimal128. + + Returns: + The `Decimal128` value 3. + """ return Decimal128(0x3, 0x0, 0x0, 0x0) @always_inline def M4() -> Decimal128: - """Returns 4 as a Decimal128.""" + """Returns 4 as a Decimal128. + + Returns: + The `Decimal128` value 4. + """ return Decimal128(0x4, 0x0, 0x0, 0x0) @always_inline def M5() -> Decimal128: - """Returns 5 as a Decimal128.""" + """Returns 5 as a Decimal128. + + Returns: + The `Decimal128` value 5. + """ return Decimal128(0x5, 0x0, 0x0, 0x0) @always_inline def M6() -> Decimal128: - """Returns 6 as a Decimal128.""" + """Returns 6 as a Decimal128. + + Returns: + The `Decimal128` value 6. + """ return Decimal128(0x6, 0x0, 0x0, 0x0) @always_inline def M7() -> Decimal128: - """Returns 7 as a Decimal128.""" + """Returns 7 as a Decimal128. + + Returns: + The `Decimal128` value 7. + """ return Decimal128(0x7, 0x0, 0x0, 0x0) @always_inline def M8() -> Decimal128: - """Returns 8 as a Decimal128.""" + """Returns 8 as a Decimal128. + + Returns: + The `Decimal128` value 8. + """ return Decimal128(0x8, 0x0, 0x0, 0x0) @always_inline def M9() -> Decimal128: - """Returns 9 as a Decimal128.""" + """Returns 9 as a Decimal128. + + Returns: + The `Decimal128` value 9. + """ return Decimal128(0x9, 0x0, 0x0, 0x0) @always_inline def M10() -> Decimal128: - """Returns 10 as a Decimal128.""" + """Returns 10 as a Decimal128. + + Returns: + The `Decimal128` value 10. + """ return Decimal128(0xA, 0x0, 0x0, 0x0) @@ -100,13 +145,21 @@ def M10() -> Decimal128: @always_inline def M0D5() -> Decimal128: - """Returns 0.5 as a Decimal128.""" + """Returns 0.5 as a Decimal128. + + Returns: + The `Decimal128` value 0.5. + """ return Decimal128(5, 0, 0, 0x10000) @always_inline def M0D25() -> Decimal128: - """Returns 0.25 as a Decimal128.""" + """Returns 0.25 as a Decimal128. + + Returns: + The `Decimal128` value 0.25. + """ return Decimal128(25, 0, 0, 0x20000) @@ -119,127 +172,211 @@ def M0D25() -> Decimal128: @always_inline def INV2() -> Decimal128: - """Returns 1/2 = 0.5.""" + """Returns 1/2 = 0.5. + + Returns: + The `Decimal128` representation of 1/2. + """ return Decimal128(0x5, 0x0, 0x0, 0x10000) @always_inline def INV10() -> Decimal128: - """Returns 1/10 = 0.1.""" + """Returns 1/10 = 0.1. + + Returns: + The `Decimal128` representation of 1/10. + """ return Decimal128(0x1, 0x0, 0x0, 0x10000) @always_inline def INV0D1() -> Decimal128: - """Returns 1/0.1 = 10.""" + """Returns 1/0.1 = 10. + + Returns: + The `Decimal128` representation of 1/0.1. + """ return Decimal128(0xA, 0x0, 0x0, 0x0) @always_inline def INV0D2() -> Decimal128: - """Returns 1/0.2 = 5.""" + """Returns 1/0.2 = 5. + + Returns: + The `Decimal128` representation of 1/0.2. + """ return Decimal128(0x5, 0x0, 0x0, 0x0) @always_inline def INV0D3() -> Decimal128: - """Returns 1/0.3 = 3.33333333333333333333333333333333...""" + """Returns 1/0.3 = 3.33333333333333333333333333333333... + + Returns: + The `Decimal128` representation of 1/0.3. + """ return Decimal128(0x35555555, 0xCF2607EE, 0x6BB4AFE4, 0x1C0000) @always_inline def INV0D4() -> Decimal128: - """Returns 1/0.4 = 2.5.""" + """Returns 1/0.4 = 2.5. + + Returns: + The `Decimal128` representation of 1/0.4. + """ return Decimal128(0x19, 0x0, 0x0, 0x10000) @always_inline def INV0D5() -> Decimal128: - """Returns 1/0.5 = 2.""" + """Returns 1/0.5 = 2. + + Returns: + The `Decimal128` representation of 1/0.5. + """ return Decimal128(0x2, 0x0, 0x0, 0x0) @always_inline def INV0D6() -> Decimal128: - """Returns 1/0.6 = 1.66666666666666666666666666666667...""" + """Returns 1/0.6 = 1.66666666666666666666666666666667... + + Returns: + The `Decimal128` representation of 1/0.6. + """ return Decimal128(0x1AAAAAAB, 0x679303F7, 0x35DA57F2, 0x1C0000) @always_inline def INV0D7() -> Decimal128: - """Returns 1/0.7 = 1.42857142857142857142857142857143...""" + """Returns 1/0.7 = 1.42857142857142857142857142857143... + + Returns: + The `Decimal128` representation of 1/0.7. + """ return Decimal128(0xCDB6DB6E, 0x3434DED3, 0x2E28DDAB, 0x1C0000) @always_inline def INV0D8() -> Decimal128: - """Returns 1/0.8 = 1.25.""" + """Returns 1/0.8 = 1.25. + + Returns: + The `Decimal128` representation of 1/0.8. + """ return Decimal128(0x7D, 0x0, 0x0, 0x20000) @always_inline def INV0D9() -> Decimal128: - """Returns 1/0.9 = 1.11111111111111111111111111111111...""" + """Returns 1/0.9 = 1.11111111111111111111111111111111... + + Returns: + The `Decimal128` representation of 1/0.9. + """ return Decimal128(0x671C71C7, 0x450CAD4F, 0x23E6E54C, 0x1C0000) @always_inline def INV1() -> Decimal128: - """Returns 1/1 = 1.""" + """Returns 1/1 = 1. + + Returns: + The `Decimal128` representation of 1/1. + """ return Decimal128(0x1, 0x0, 0x0, 0x0) @always_inline def INV1D1() -> Decimal128: - """Returns 1/1.1 = 0.90909090909090909090909090909091...""" + """Returns 1/1.1 = 0.90909090909090909090909090909091... + + Returns: + The `Decimal128` representation of 1/1.1. + """ return Decimal128(0x9A2E8BA3, 0x4FC48DCC, 0x1D5FD2E1, 0x1C0000) @always_inline def INV1D2() -> Decimal128: - """Returns 1/1.2 = 0.83333333333333333333333333333333...""" + """Returns 1/1.2 = 0.83333333333333333333333333333333... + + Returns: + The `Decimal128` representation of 1/1.2. + """ return Decimal128(0x8D555555, 0x33C981FB, 0x1AED2BF9, 0x1C0000) @always_inline def INV1D3() -> Decimal128: - """Returns 1/1.3 = 0.76923076923076923076923076923077...""" + """Returns 1/1.3 = 0.76923076923076923076923076923077... + + Returns: + The `Decimal128` representation of 1/1.3. + """ return Decimal128(0xC4EC4EC, 0x9243DA72, 0x18DAED83, 0x1C0000) @always_inline def INV1D4() -> Decimal128: - """Returns 1/1.4 = 0.71428571428571428571428571428571...""" + """Returns 1/1.4 = 0.71428571428571428571428571428571... + + Returns: + The `Decimal128` representation of 1/1.4. + """ return Decimal128(0xE6DB6DB7, 0x9A1A6F69, 0x17146ED5, 0x1C0000) @always_inline def INV1D5() -> Decimal128: - """Returns 1/1.5 = 0.66666666666666666666666666666667...""" + """Returns 1/1.5 = 0.66666666666666666666666666666667... + + Returns: + The `Decimal128` representation of 1/1.5. + """ return Decimal128(0xAAAAAAB, 0x296E0196, 0x158A8994, 0x1C0000) @always_inline def INV1D6() -> Decimal128: - """Returns 1/1.6 = 0.625.""" + """Returns 1/1.6 = 0.625. + + Returns: + The `Decimal128` representation of 1/1.6. + """ return Decimal128(0x271, 0x0, 0x0, 0x30000) @always_inline def INV1D7() -> Decimal128: - """Returns 1/1.7 = 0.58823529411764705882352941176471...""" + """Returns 1/1.7 = 0.58823529411764705882352941176471... + + Returns: + The `Decimal128` representation of 1/1.7. + """ return Decimal128(0x45A5A5A6, 0xE8520166, 0x1301C4AF, 0x1C0000) @always_inline def INV1D8() -> Decimal128: - """Returns 1/1.8 = 0.55555555555555555555555555555556...""" + """Returns 1/1.8 = 0.55555555555555555555555555555556... + + Returns: + The `Decimal128` representation of 1/1.8. + """ return Decimal128(0xB38E38E4, 0x228656A7, 0x11F372A6, 0x1C0000) @always_inline def INV1D9() -> Decimal128: - """Returns 1/1.9 = 0.52631578947368421052631578947368...""" + """Returns 1/1.9 = 0.52631578947368421052631578947368... + + Returns: + The `Decimal128` representation of 1/1.9. + """ return Decimal128(0xAA1AF287, 0x2E2E6D0A, 0x11019509, 0x1C0000) @@ -262,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 @@ -325,7 +462,10 @@ def N_DIVIDE_NEXT(n: Int) raises -> Decimal128: # 20/21 = 0.95238095238095238095238095238095... return Decimal128(0x33CF3CF4, 0xCD78948D, 0x1EC5E91C, 0x1C0000) else: - raise Error("N_DIVIDE_NEXT: n must be between 1 and 20, inclusive") + raise ValueError( + message="n must be between 1 and 20, inclusive.", + function="N_DIVIDE_NEXT()", + ) # ===----------------------------------------------------------------------=== # @@ -337,7 +477,11 @@ def N_DIVIDE_NEXT(n: Int) raises -> Decimal128: @always_inline def PI() -> Decimal128: - """Returns the value of pi (π) as a Decimal128.""" + """Returns the value of pi (π) as a Decimal128. + + Returns: + The value of pi (π). + """ return Decimal128(0x41B65F29, 0xB143885, 0x6582A536, 0x1C0000) @@ -361,109 +505,181 @@ def E() -> Decimal128: @always_inline def E2() -> Decimal128: - """Returns the value of e^2 as a Decimal128.""" + """Returns the value of e^2 as a Decimal128. + + Returns: + The value of e². + """ return Decimal128(0xE4DFDCAE, 0x89F7E295, 0xEEC0D6E9, 0x1C0000) @always_inline def E3() -> Decimal128: - """Returns the value of e^3 as a Decimal128.""" + """Returns the value of e^3 as a Decimal128. + + Returns: + The value of e³. + """ return Decimal128(0x236454F7, 0x62055A80, 0x40E65DE2, 0x1B0000) @always_inline def E4() -> Decimal128: - """Returns the value of e^4 as a Decimal128.""" + """Returns the value of e^4 as a Decimal128. + + Returns: + The value of e⁴. + """ return Decimal128(0x7121EFD3, 0xFB318FB5, 0xB06A87FB, 0x1B0000) @always_inline def E5() -> Decimal128: - """Returns the value of e^5 as a Decimal128.""" + """Returns the value of e^5 as a Decimal128. + + Returns: + The value of e⁵. + """ return Decimal128(0xD99BD974, 0x9F4BE5C7, 0x2FF472E3, 0x1A0000) @always_inline def E6() -> Decimal128: - """Returns the value of e^6 as a Decimal128.""" + """Returns the value of e^6 as a Decimal128. + + Returns: + The value of e⁶. + """ return Decimal128(0xADB57A66, 0xBD7A423F, 0x825AD8FF, 0x1A0000) @always_inline def E7() -> Decimal128: - """Returns the value of e^7 as a Decimal128.""" + """Returns the value of e^7 as a Decimal128. + + Returns: + The value of e⁷. + """ return Decimal128(0x22313FCF, 0x64D5D12F, 0x236F230A, 0x190000) @always_inline def E8() -> Decimal128: - """Returns the value of e^8 as a Decimal128.""" + """Returns the value of e^8 as a Decimal128. + + Returns: + The value of e⁸. + """ return Decimal128(0x1E892E63, 0xD1BF8B5C, 0x6051E812, 0x190000) @always_inline def E9() -> Decimal128: - """Returns the value of e^9 as a Decimal128.""" + """Returns the value of e^9 as a Decimal128. + + Returns: + The value of e⁹. + """ return Decimal128(0x34FAB691, 0xE7CD8DEA, 0x1A2EB6C3, 0x180000) @always_inline def E10() -> Decimal128: - """Returns the value of e^10 as a Decimal128.""" + """Returns the value of e^10 as a Decimal128. + + Returns: + The value of e¹⁰. + """ return Decimal128(0xBA7F4F65, 0x58692B62, 0x472BDD8F, 0x180000) @always_inline def E11() -> Decimal128: - """Returns the value of e^11 as a Decimal128.""" + """Returns the value of e^11 as a Decimal128. + + Returns: + The value of e¹¹. + """ return Decimal128(0x8C2C6D20, 0x2A86F9E7, 0xC176BAAE, 0x180000) @always_inline def E12() -> Decimal128: - """Returns the value of e^12 as a Decimal128.""" + """Returns the value of e^12 as a Decimal128. + + Returns: + The value of e¹². + """ return Decimal128(0xE924992A, 0x31CDC314, 0x3496C2C4, 0x170000) @always_inline def E13() -> Decimal128: - """Returns the value of e^13 as a Decimal128.""" + """Returns the value of e^13 as a Decimal128. + + Returns: + The value of e¹³. + """ return Decimal128(0x220130DB, 0xC386029A, 0x8EF393FB, 0x170000) @always_inline def E14() -> Decimal128: - """Returns the value of e^14 as a Decimal128.""" + """Returns the value of e^14 as a Decimal128. + + Returns: + The value of e¹⁴. + """ return Decimal128(0x3A24795C, 0xC412DF01, 0x26DBB5A0, 0x160000) @always_inline def E15() -> Decimal128: - """Returns the value of e^15 as a Decimal128.""" + """Returns the value of e^15 as a Decimal128. + + Returns: + The value of e¹⁵. + """ return Decimal128(0x6C1248BD, 0x90456557, 0x69A0AD8C, 0x160000) @always_inline def E16() -> Decimal128: - """Returns the value of e^16 as a Decimal128.""" + """Returns the value of e^16 as a Decimal128. + + Returns: + The value of e¹⁶. + """ return Decimal128(0xB46A97D, 0x90655BBD, 0x1CB66B18, 0x150000) @always_inline def E32() -> Decimal128: - """Returns the value of e^32 as a Decimal128.""" + """Returns the value of e^32 as a Decimal128. + + Returns: + The value of e³². + """ return Decimal128(0x18420EB, 0xCC2501E6, 0xFF24A138, 0xF0000) @always_inline def E0D5() -> Decimal128: - """Returns the value of e^0.5 = e^(1/2) as a Decimal128.""" + """Returns the value of e^0.5 = e^(1/2) as a Decimal128. + + Returns: + The value of e^(1/2). + """ return Decimal128(0x8E99DD66, 0xC210E35C, 0x3545E717, 0x1C0000) @always_inline def E0D25() -> Decimal128: - """Returns the value of e^0.25 = e^(1/4) as a Decimal128.""" + """Returns the value of e^0.25 = e^(1/4) as a Decimal128. + + Returns: + The value of e^(1/4). + """ return Decimal128(0xB43646F1, 0x2654858A, 0x297D3595, 0x1C0000) @@ -493,74 +709,122 @@ def E0D25() -> Decimal128: @always_inline def LN1() -> Decimal128: - """Returns ln(1) = 0.""" + """Returns ln(1) = 0. + + Returns: + The natural logarithm of 1. + """ return Decimal128(0x0, 0x0, 0x0, 0x0) @always_inline def LN2() -> Decimal128: - """Returns ln(2) = 0.69314718055994530941723212145818...""" + """Returns ln(2) = 0.69314718055994530941723212145818... + + Returns: + The natural logarithm of 2. + """ return Decimal128(0xAA7A65BF, 0x81F52F01, 0x1665943F, 0x1C0000) @always_inline def LN10() -> Decimal128: - """Returns ln(10) = 2.30258509299404568401799145468436...""" + """Returns ln(10) = 2.30258509299404568401799145468436... + + Returns: + The natural logarithm of 10. + """ return Decimal128(0x9FA69733, 0x1414B220, 0x4A668998, 0x1C0000) # Constants for values less than 1 @always_inline def LN0D1() -> Decimal128: - """Returns ln(0.1) = -2.30258509299404568401799145468436...""" + """Returns ln(0.1) = -2.30258509299404568401799145468436... + + Returns: + The natural logarithm of 0.1. + """ return Decimal128(0x9FA69733, 0x1414B220, 0x4A668998, 0x801C0000) @always_inline def LN0D2() -> Decimal128: - """Returns ln(0.2) = -1.60943791243410037460075933322619...""" + """Returns ln(0.2) = -1.60943791243410037460075933322619... + + Returns: + The natural logarithm of 0.2. + """ return Decimal128(0xF52C3174, 0x921F831E, 0x3400F558, 0x801C0000) @always_inline def LN0D3() -> Decimal128: - """Returns ln(0.3) = -1.20397280432593599262274621776184...""" + """Returns ln(0.3) = -1.20397280432593599262274621776184... + + Returns: + The natural logarithm of 0.3. + """ return Decimal128(0x2B8E6822, 0x8258467, 0x26E70795, 0x801C0000) @always_inline def LN0D4() -> Decimal128: - """Returns ln(0.4) = -0.91629073187415506518352721176801...""" + """Returns ln(0.4) = -0.91629073187415506518352721176801... + + Returns: + The natural logarithm of 0.4. + """ return Decimal128(0x4AB1CBB6, 0x102A541D, 0x1D9B6119, 0x801C0000) @always_inline def LN0D5() -> Decimal128: - """Returns ln(0.5) = -0.69314718055994530941723212145818...""" + """Returns ln(0.5) = -0.69314718055994530941723212145818... + + Returns: + The natural logarithm of 0.5. + """ return Decimal128(0xAA7A65BF, 0x81F52F01, 0x1665943F, 0x801C0000) @always_inline def LN0D6() -> Decimal128: - """Returns ln(0.6) = -0.51082562376599068320551409630366...""" + """Returns ln(0.6) = -0.51082562376599068320551409630366... + + Returns: + The natural logarithm of 0.6. + """ return Decimal128(0x81140263, 0x86305565, 0x10817355, 0x801C0000) @always_inline def LN0D7() -> Decimal128: - """Returns ln(0.7) = -0.35667494393873237891263871124118...""" + """Returns ln(0.7) = -0.35667494393873237891263871124118... + + Returns: + The natural logarithm of 0.7. + """ return Decimal128(0x348BC5A8, 0x8B755D08, 0xB865892, 0x801C0000) @always_inline def LN0D8() -> Decimal128: - """Returns ln(0.8) = -0.22314355131420975576629509030983...""" + """Returns ln(0.8) = -0.22314355131420975576629509030983... + + Returns: + The natural logarithm of 0.8. + """ return Decimal128(0xA03765F7, 0x8E35251B, 0x735CCD9, 0x801C0000) @always_inline def LN0D9() -> Decimal128: - """Returns ln(0.9) = -0.10536051565782630122750098083931...""" + """Returns ln(0.9) = -0.10536051565782630122750098083931... + + Returns: + The natural logarithm of 0.9. + """ return Decimal128(0xB7763910, 0xFC3656AD, 0x3678591, 0x801C0000) @@ -569,53 +833,89 @@ def LN0D9() -> Decimal128: @always_inline def LN1D1() -> Decimal128: - """Returns ln(1.1) = 0.09531017980432486004395212328077...""" + """Returns ln(1.1) = 0.09531017980432486004395212328077... + + Returns: + The natural logarithm of 1.1. + """ return Decimal128(0x7212FFD1, 0x7D9A10, 0x3146328, 0x1C0000) @always_inline def LN1D2() -> Decimal128: - """Returns ln(1.2) = 0.18232155679395462621171802515451...""" + """Returns ln(1.2) = 0.18232155679395462621171802515451... + + Returns: + The natural logarithm of 1.2. + """ return Decimal128(0x2966635C, 0xFBC4D99C, 0x5E420E9, 0x1C0000) @always_inline def LN1D3() -> Decimal128: - """Returns ln(1.3) = 0.26236426446749105203549598688095...""" + """Returns ln(1.3) = 0.26236426446749105203549598688095... + + Returns: + The natural logarithm of 1.3. + """ return Decimal128(0xE0BE71FD, 0xC254E078, 0x87A39F0, 0x1C0000) @always_inline def LN1D4() -> Decimal128: - """Returns ln(1.4) = 0.33647223662121293050459341021699...""" + """Returns ln(1.4) = 0.33647223662121293050459341021699... + + Returns: + The natural logarithm of 1.4. + """ return Decimal128(0x75EEA016, 0xF67FD1F9, 0xADF3BAC, 0x1C0000) @always_inline def LN1D5() -> Decimal128: - """Returns ln(1.5) = 0.40546510810816438197801311546435...""" + """Returns ln(1.5) = 0.40546510810816438197801311546435... + + Returns: + The natural logarithm of 1.5. + """ return Decimal128(0xC99DC953, 0x89F9FEB7, 0xD19EDC3, 0x1C0000) @always_inline def LN1D6() -> Decimal128: - """Returns ln(1.6) = 0.47000362924573555365093703114834...""" + """Returns ln(1.6) = 0.47000362924573555365093703114834... + + Returns: + The natural logarithm of 1.6. + """ return Decimal128(0xA42FFC8, 0xF3C009E6, 0xF2FC765, 0x1C0000) @always_inline def LN1D7() -> Decimal128: - """Returns ln(1.7) = 0.53062825106217039623154316318876...""" + """Returns ln(1.7) = 0.53062825106217039623154316318876... + + Returns: + The natural logarithm of 1.7. + """ return Decimal128(0x64BB9ED0, 0x4AB9978F, 0x11254107, 0x1C0000) @always_inline def LN1D8() -> Decimal128: - """Returns ln(1.8) = 0.58778666490211900818973114061886...""" + """Returns ln(1.8) = 0.58778666490211900818973114061886... + + Returns: + The natural logarithm of 1.8. + """ return Decimal128(0xF3042CAE, 0x85BED853, 0x12FE0EAD, 0x1C0000) @always_inline def LN1D9() -> Decimal128: - """Returns ln(1.9) = 0.64185388617239477599103597720349...""" + """Returns ln(1.9) = 0.64185388617239477599103597720349... + + Returns: + The natural logarithm of 1.9. + """ return Decimal128(0x12F992DC, 0xE7374425, 0x14BD4A78, 0x1C0000) diff --git a/src/decimo/decimal128/decimal128.mojo b/src/decimo/decimal128/decimal128.mojo index 142f10b2..69406d7a 100644 --- a/src/decimo/decimal128/decimal128.mojo +++ b/src/decimo/decimal128/decimal128.mojo @@ -32,6 +32,11 @@ import decimo.decimal128.constants import decimo.decimal128.exponential import decimo.decimal128.rounding from decimo.rounding_mode import RoundingMode +from decimo.errors import ( + ValueError, + OverflowError, + ConversionError, +) import decimo.decimal128.utility comptime Dec128 = Decimal128 @@ -102,10 +107,15 @@ struct Decimal128( # Constants comptime MAX_SCALE: Int = 28 + """The maximum scale (exponent) value for `Decimal128` (28).""" comptime MAX_AS_UINT128 = UInt128(79228162514264337593543950335) + """The maximum coefficient value as `UInt128`.""" comptime MAX_AS_INT128 = Int128(79228162514264337593543950335) + """The maximum coefficient value as `Int128`.""" comptime MAX_AS_UINT256 = UInt256(79228162514264337593543950335) + """The maximum coefficient value as `UInt256`.""" comptime MAX_AS_INT256 = Int256(79228162514264337593543950335) + """The maximum coefficient value as `Int256`.""" comptime MAX_AS_STRING = String("79228162514264337593543950335") """Maximum value as a string.""" comptime MAX_NUM_DIGITS = 29 @@ -132,6 +142,9 @@ struct Decimal128( def INFINITY() -> Self: """Returns a Decimal representing positive infinity. Internal representation: `0b0000_0000_0000_0000_0000_0000_0001`. + + Returns: + A `Decimal128` representing positive infinity. """ return Self(0, 0, 0, 0x00000001) @@ -140,6 +153,9 @@ struct Decimal128( def NEGATIVE_INFINITY() -> Self: """Returns a Decimal128 representing negative infinity. Internal representation: `0b1000_0000_0000_0000_0000_0000_0001`. + + Returns: + A `Decimal128` representing negative infinity. """ return Self(0, 0, 0, 0x80000001) @@ -148,6 +164,9 @@ struct Decimal128( def NAN() -> Self: """Returns a Decimal128 representing Not a Number (NaN). Internal representation: `0b0000_0000_0000_0000_0000_0000_0010`. + + Returns: + A `Decimal128` representing NaN. """ return Self(0, 0, 0, 0x00000010) @@ -156,19 +175,30 @@ struct Decimal128( def NEGATIVE_NAN() -> Self: """Returns a Decimal128 representing negative Not a Number. Internal representation: `0b1000_0000_0000_0000_0000_0000_0010`. + + Returns: + A `Decimal128` representing negative NaN. """ return Self(0, 0, 0, 0x80000010) @always_inline @staticmethod def ZERO() -> Decimal128: - """Returns a Decimal128 representing 0.""" + """Returns a Decimal128 representing 0. + + Returns: + A `Decimal128` representing zero. + """ return Self(0, 0, 0, 0) @always_inline @staticmethod def ONE() -> Decimal128: - """Returns a Decimal128 representing 1.""" + """Returns a Decimal128 representing 1. + + Returns: + A `Decimal128` representing one. + """ return Self(1, 0, 0, 0) @always_inline @@ -177,6 +207,9 @@ struct Decimal128( """ Returns the maximum possible Decimal128 value. This is equivalent to 79228162514264337593543950335. + + Returns: + The maximum `Decimal128` value. """ return Self(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0) @@ -185,19 +218,30 @@ struct Decimal128( def MIN() -> Decimal128: """Returns the minimum possible Decimal128 value (negative of MAX). This is equivalent to -79228162514264337593543950335. + + Returns: + The minimum `Decimal128` value. """ return Self(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, Decimal128.SIGN_MASK) @always_inline @staticmethod def PI() -> Decimal128: - """Returns the value of pi (π) as a Decimal128.""" + """Returns the value of pi (π) as a Decimal128. + + Returns: + The value of pi (π). + """ return decimo.decimal128.constants.PI() @always_inline @staticmethod def E() -> Decimal128: - """Returns the value of Euler's number (e) as a Decimal128.""" + """Returns the value of Euler's number (e) as a Decimal128. + + Returns: + The value of Euler's number (e). + """ return decimo.decimal128.constants.E() # ===------------------------------------------------------------------=== # @@ -217,6 +261,12 @@ struct Decimal128( """Initializes a Decimal128 with four raw words of internal representation. ***WARNING***: This method does not check the flags. If you are not sure about the flags, use `Decimal128.from_words()` instead. + + Args: + low: The least significant 32 bits of the coefficient. + mid: The middle 32 bits of the coefficient. + high: The most significant 32 bits of the coefficient. + flags: The raw flags word containing scale and sign. """ self.low = low @@ -234,48 +284,94 @@ struct Decimal128( ) raises: """Initializes a Decimal128 with five components. See `Decimal128.from_components()` for more information. + + Args: + low: The least significant 32 bits of the coefficient. + mid: The middle 32 bits of the coefficient. + 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: self = Decimal128.from_components(low, mid, high, scale, sign) except e: - raise Error( - "Error in `Decimal128.__init__()` with five components: ", e + raise ConversionError( + message="Cannot initialize with five components.", + function="Decimal128.__init__()", + previous_error=e^, ) def __init__(out self, value: Int): """Initializes a Decimal128 from an integer. See `from_int()` for more information. + + Args: + value: The integer value to convert. """ self = Decimal128.from_int(value) def __init__(out self, value: Int, scale: UInt32) raises: """Initializes a Decimal128 from an integer. See `from_int()` for more information. + + 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) except e: - raise Error("Error in `Decimal128.__init__()` with Int: ", e) + raise ConversionError( + message="Cannot initialize Decimal128 from Int.", + function="Decimal128.__init__()", + previous_error=e^, + ) def __init__(out self, value: String) raises: """Initializes a Decimal128 from a string representation. See `from_string()` for more information. + + Args: + value: The string representation of the decimal number. + + Raises: + ConversionError: If the string cannot be parsed. """ try: self = Decimal128.from_string(value) except e: - raise Error("Error in `Decimal__init__()` with String: ", e) + raise ConversionError( + message="Cannot initialize Decimal128 from String.", + function="Decimal128.__init__()", + previous_error=e^, + ) def __init__(out self, value: Float64) raises: """Initializes a Decimal128 from a floating-point value. See `from_float` for more information. + + Args: + value: The floating-point value to convert. + + Raises: + ConversionError: If the float cannot be converted. """ try: self = Decimal128.from_float(value) except e: - raise Error("Error in `Decimal__init__()` with Float64: ", e) + raise ConversionError( + message="Cannot initialize Decimal128 from Float64.", + function="Decimal128.__init__()", + previous_error=e^, + ) # ===------------------------------------------------------------------=== # # Constructing methods that are not dunders @@ -302,15 +398,15 @@ 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): - raise Error( - String( - "Error in Decimal128 constructor with five components:" - " Scale must be between 0 and 28, but got {}" - ).format(scale) + raise ValueError( + message=String( + "Scale must be between 0 and 28, but got {}." + ).format(scale), + function="Decimal128.from_components()", ) var flags: UInt32 = 0 @@ -410,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: @@ -430,11 +526,11 @@ struct Decimal128( var flags: UInt32 if scale > UInt32(Self.MAX_SCALE): - raise Error( - String( - "Error in Decimal128 constructor with Int: Scale must be" - " between 0 and 28, but got {}" - ).format(scale) + raise ValueError( + message=String( + "Scale must be between 0 and 28, but got {}" + ).format(scale), + function="Decimal128.from_int()", ) if value >= 0: @@ -467,24 +563,24 @@ 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: - raise Error( - String( - "Error in Decimal128 constructor with UInt128: Value must" - " fit in 96 bits, but got {}" - ).format(value) + 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( - String( - "Error in Decimal128 constructor with five components:" - " Scale must be between 0 and 28, but got {}" - ).format(scale) + 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]()[] @@ -504,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: @@ -531,10 +628,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( - String( - "There are invalid characters in decimal128 string: {}" - ).format(value) + raise ValueError( + message=String( + "Invalid characters in decimal128 string: {}" + ).format(value), + function="Decimal128.from_string()", ) # Yuhao's notes: @@ -566,13 +664,19 @@ struct Decimal128( elif code == 45: unexpected_end_char = True if exponent_sign_read: - raise Error("Minus sign cannot appear twice in exponent.") + 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( - "Minus sign can only appear once at the begining." + raise ValueError( + message=( + "Minus sign can only appear once at the beginning." + ), + function="Decimal128.from_string()", ) else: mantissa_sign = True @@ -581,12 +685,18 @@ struct Decimal128( elif code == 43: unexpected_end_char = True if exponent_sign_read: - raise Error("Plus sign cannot appear twice in exponent.") + 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( - "Plus sign can only appear once at the begining." + raise ValueError( + message=( + "Plus sign can only appear once at the beginning." + ), + function="Decimal128.from_string()", ) else: mantissa_sign_read = True @@ -594,7 +704,10 @@ struct Decimal128( elif code == 46: unexpected_end_char = False if decimal_point_read: - raise Error("Decimal point can only appear once.") + raise ValueError( + message="Decimal point can only appear once.", + function="Decimal128.from_string()", + ) else: decimal_point_read = True mantissa_sign_read = True @@ -602,9 +715,15 @@ struct Decimal128( elif code == 101 or code == 69: unexpected_end_char = True if exponent_notation_read: - raise Error("Exponential notation can only appear once.") + raise ValueError( + message="Exponential notation can only appear once.", + function="Decimal128.from_string()", + ) if not mantissa_start: - raise Error("Exponential notation must follow a number.") + raise ValueError( + message="Exponential notation must follow a number.", + function="Decimal128.from_string()", + ) else: exponent_notation_read = True # If the char is a digit 0 @@ -645,10 +764,11 @@ struct Decimal128( if (not exponent_sign) and ( raw_exponent > Decimal128.MAX_NUM_DIGITS * 2 ): - raise Error( - String("Exponent part is too large: {}").format( - raw_exponent - ) + 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 @@ -679,14 +799,18 @@ struct Decimal128( scale += 1 else: - raise Error( - String("Invalid character in decimal128 string: {}").format( - chr(Int(code)) - ) + raise ValueError( + message=String( + "Invalid character in decimal128 string: {}" + ).format(chr(Int(code))), + function="Decimal128.from_string()", ) if unexpected_end_char: - raise Error("Unexpected end character in decimal128 string.") + raise ValueError( + message="Unexpected end character in decimal128 string.", + function="Decimal128.from_string()", + ) # print("DEBUG: coef = ", coef) # print("DEBUG: scale = ", scale) @@ -779,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 @@ -807,11 +931,12 @@ struct Decimal128( # Early exit if the value is too large if UInt128(abs_value) > Decimal128.MAX_AS_UINT128: - raise Error( - String( - "Error in `from_float`: The float value {} is too" - " large (>=2^96) to be transformed into Decimal128" - ).format(value) + 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 @@ -826,7 +951,10 @@ struct Decimal128( # CASE: Infinity or NaN if biased_exponent == 0x7FF: - raise Error("Cannot convert infinity or NaN to Decimal128") + raise ValueError( + message="Cannot convert infinity or NaN to Decimal128.", + function="Decimal128.from_float()", + ) # Get unbias exponent var binary_exp: Int = biased_exponent - 1023 @@ -883,12 +1011,20 @@ struct Decimal128( @always_inline def copy(self) -> Self: - """Returns a copy of the Decimal128.""" + """Returns a copy of the Decimal128. + + Returns: + A copy of this `Decimal128`. + """ return Self(self.low, self.mid, self.high, self.flags) @always_inline def clone(self) -> Self: - """Returns a copy of the Decimal128.""" + """Returns a copy of the Decimal128. + + Returns: + A copy of this `Decimal128`. + """ return Self(self.low, self.mid, self.high, self.flags) # ===------------------------------------------------------------------=== # @@ -911,11 +1047,24 @@ struct Decimal128( def __int__(self) raises -> Int: """Returns the integral part of the Decimal128 as Int. See `to_int()` for more information. + + Returns: + The `Int` representation. + + Raises: + ConversionError: If the conversion fails. """ return self.to_int() def write_repr_to[W: Writer](self, mut writer: W): - """Writes the debug representation to a writer.""" + """Writes the debug representation to a writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ writer.write('Decimal128("', self.to_str(), '")') # ===------------------------------------------------------------------=== # @@ -925,12 +1074,21 @@ struct Decimal128( def write_to[W: Writer](self, mut writer: W): """Writes the Decimal128 to a writer. This implement the `write` method of the `Writer` trait. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. """ writer.write(self.to_str()) def repr_words(self) -> String: """Returns a string representation of the Decimal128's internal words. `Decimal128.from_words(low, mid, high, flags)`. + + Returns: + A string in the format `Decimal128(low, mid, high, flags)`. """ return ( "Decimal128(" @@ -947,6 +1105,9 @@ struct Decimal128( def repr_components(self) -> String: """Returns a string representation of the Decimal128's five components. `Decimal128.from_components(low, mid, high, scale, sign)`. + + Returns: + A string in the format `Decimal128(low=, mid=, high=, scale=, sign=)`. """ var scale = UInt8((self.flags & Self.SCALE_MASK) >> Self.SCALE_SHIFT) var sign = Bool((self.flags & Self.SIGN_MASK) == Self.SIGN_MASK) @@ -972,12 +1133,16 @@ 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()) except e: - raise Error("Error in `to_int()`: ", e) + raise ConversionError( + message="Cannot convert Decimal128 to Int.", + function="Decimal128.to_int()", + previous_error=e^, + ) def to_int64(self) raises -> Int64: """Returns the integral part of the Decimal128 as Int64. @@ -987,27 +1152,41 @@ 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() if result > Int128(Int64.MAX): - raise Error("Decimal128 is too large to fit in Int64") + raise OverflowError( + message="Decimal128 is too large to fit in Int64.", + function="Decimal128.to_int64()", + ) if result < Int128(Int64.MIN): - raise Error("Decimal128 is too small to fit in Int64") + raise OverflowError( + message="Decimal128 is too small to fit in Int64.", + function="Decimal128.to_int64()", + ) return Int64(result & 0xFFFF_FFFF_FFFF_FFFF) def to_int128(self) -> Int128: - """Returns the signed integral part of the Decimal128.""" + """Returns the signed integral part of the Decimal128. + + Returns: + The signed integral part as `Int128`. + """ var res = Int128(self.to_uint128()) return -res if self.is_negative() else res def to_uint128(self) -> UInt128: - """Returns the absolute integral part of the Decimal128 as UInt128.""" + """Returns the absolute integral part of the Decimal128 as UInt128. + + Returns: + The absolute integral part as `UInt128`. + """ var res: UInt128 if self.is_zero(): @@ -1031,6 +1210,9 @@ struct Decimal128( def to_str(self) -> String: """Returns string representation of the Decimal128. Preserves trailing zeros after decimal128 point to match the scale. + + Returns: + The string representation of this `Decimal128`. """ # Get the coefficient as a string (absolute value) var coef = String(self.coefficient()) @@ -1144,6 +1326,9 @@ struct Decimal128( def __abs__(self) -> Self: """Returns the absolute value of this Decimal128. See `absolute()` for more information. + + Returns: + The absolute value. """ return decimo.decimal128.arithmetics.absolute(self) @@ -1151,6 +1336,9 @@ struct Decimal128( def __neg__(self) -> Self: """Returns the negation of this Decimal128. See `negative()` for more information. + + Returns: + The negated value. """ return decimo.decimal128.arithmetics.negative(self) @@ -1162,62 +1350,170 @@ struct Decimal128( @always_inline def __add__(self, other: Self) raises -> Self: + """Adds two values. + + Args: + other: The right-hand side operand. + + Returns: + The sum. + """ return decimo.decimal128.arithmetics.add(self, other) @always_inline def __add__(self, other: Int) raises -> Self: + """Adds two values. + + Args: + other: The right-hand side operand. + + Returns: + The sum. + """ return decimo.decimal128.arithmetics.add(self, Self(other)) @always_inline def __sub__(self, other: Self) raises -> Self: + """Subtracts two values. + + Args: + other: The right-hand side operand. + + Returns: + The difference. + """ return decimo.decimal128.arithmetics.subtract(self, other) @always_inline def __sub__(self, other: Int) raises -> Self: + """Subtracts two values. + + Args: + other: The right-hand side operand. + + Returns: + The difference. + """ return decimo.decimal128.arithmetics.subtract(self, Self(other)) @always_inline def __mul__(self, other: Self) raises -> Self: + """Multiplies two values. + + Args: + other: The right-hand side operand. + + Returns: + The product. + """ return decimo.decimal128.arithmetics.multiply(self, other) @always_inline def __mul__(self, other: Int) raises -> Self: + """Multiplies two values. + + Args: + other: The right-hand side operand. + + Returns: + The product. + """ return decimo.decimal128.arithmetics.multiply(self, Self(other)) @always_inline def __truediv__(self, other: Self) raises -> Self: + """Divides two values using true division. + + Args: + other: The right-hand side operand. + + Returns: + The quotient. + """ return decimo.decimal128.arithmetics.divide(self, other) @always_inline def __truediv__(self, other: Int) raises -> Self: + """Divides two values using true division. + + Args: + other: The right-hand side operand. + + Returns: + The quotient. + """ return decimo.decimal128.arithmetics.divide(self, Self(other)) @always_inline def __floordiv__(self, other: Self) raises -> Self: - """Performs truncate division with // operator.""" + """Performs truncate division with // operator. + + Args: + other: The right-hand side operand. + + Returns: + The truncated quotient. + """ return decimo.decimal128.arithmetics.truncate_divide(self, other) @always_inline def __floordiv__(self, other: Int) raises -> Self: - """Performs truncate division with // operator.""" + """Performs truncate division with // operator. + + Args: + other: The right-hand side operand. + + Returns: + The truncated quotient. + """ return decimo.decimal128.arithmetics.truncate_divide(self, Self(other)) @always_inline def __mod__(self, other: Self) raises -> Self: - """Performs truncate modulo.""" + """Performs truncate modulo. + + Args: + other: The right-hand side operand. + + Returns: + The remainder. + """ return decimo.decimal128.arithmetics.modulo(self, other) @always_inline def __mod__(self, other: Int) raises -> Self: - """Performs truncate modulo.""" + """Performs truncate modulo. + + Args: + other: The right-hand side operand. + + Returns: + The remainder. + """ return decimo.decimal128.arithmetics.modulo(self, Self(other)) @always_inline def __pow__(self, exponent: Self) raises -> Self: + """Raises to a power. + + Args: + exponent: The exponent to raise to. + + Returns: + The value raised to the given power. + """ return decimo.decimal128.exponential.power(self, exponent) @always_inline def __pow__(self, exponent: Int) raises -> Self: + """Raises to a power. + + Args: + exponent: The exponent to raise to. + + Returns: + The value raised to the given power. + """ return decimo.decimal128.exponential.power(self, exponent) # ===------------------------------------------------------------------=== # @@ -1229,28 +1525,74 @@ struct Decimal128( @always_inline def __radd__(self, other: Int) raises -> Self: + """Adds two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The sum. + """ return decimo.decimal128.arithmetics.add(Self(other), self) @always_inline def __rsub__(self, other: Int) raises -> Self: + """Subtracts two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The difference. + """ return decimo.decimal128.arithmetics.subtract(Self(other), self) @always_inline def __rmul__(self, other: Int) raises -> Self: + """Multiplies two values (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The product. + """ return decimo.decimal128.arithmetics.multiply(Self(other), self) @always_inline def __rtruediv__(self, other: Int) raises -> Self: + """Divides two values using true division (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The quotient. + """ return decimo.decimal128.arithmetics.divide(Self(other), self) @always_inline def __rfloordiv__(self, other: Int) raises -> Self: - """Performs truncate division with // operator.""" + """Performs truncate division with // operator (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The truncated quotient. + """ return decimo.decimal128.arithmetics.truncate_divide(Self(other), self) @always_inline def __rmod__(self, other: Int) raises -> Self: - """Performs truncate modulo.""" + """Performs truncate modulo (reflected). + + Args: + other: The left-hand side operand. + + Returns: + The remainder. + """ return decimo.decimal128.arithmetics.modulo(Self(other), self) # ===------------------------------------------------------------------=== # @@ -1262,49 +1604,101 @@ struct Decimal128( @always_inline def __iadd__(mut self, other: Self) raises: + """Adds in place. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.add(self, other) @always_inline def __iadd__(mut self, other: Int) raises: + """Adds in place. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.add(self, Self(other)) @always_inline def __isub__(mut self, other: Self) raises: + """Subtracts in place. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.subtract(self, other) @always_inline def __isub__(mut self, other: Int) raises: + """Subtracts in place. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.subtract(self, Self(other)) @always_inline def __imul__(mut self, other: Self) raises: + """Multiplies in place. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.multiply(self, other) @always_inline def __imul__(mut self, other: Int) raises: + """Multiplies in place. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.multiply(self, Self(other)) @always_inline def __itruediv__(mut self, other: Self) raises: + """Divides in place using true division. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.divide(self, other) @always_inline def __itruediv__(mut self, other: Int) raises: + """Divides in place using true division. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.divide(self, Self(other)) @always_inline def __ifloordiv__(mut self, other: Self) raises: - """Performs truncate division with // operator.""" + """Performs truncate division with // operator. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.truncate_divide(self, other) @always_inline def __ifloordiv__(mut self, other: Int) raises: - """Performs truncate division with // operator.""" + """Performs truncate division with // operator. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.truncate_divide(self, Self(other)) @always_inline def __imod__(mut self, other: Self) raises: - """Performs truncate modulo.""" + """Performs truncate modulo. + + Args: + other: The right-hand side operand. + """ self = decimo.decimal128.arithmetics.modulo(self, other) # ===------------------------------------------------------------------=== # @@ -1316,6 +1710,12 @@ struct Decimal128( def __gt__(self, other: Decimal128) -> Bool: """Greater than comparison operator. See `greater()` for more information. + + Args: + other: The value to compare against. + + Returns: + `True` if this value is greater than `other`, `False` otherwise. """ return decimo.decimal128.comparison.greater(self, other) @@ -1323,6 +1723,12 @@ struct Decimal128( def __lt__(self, other: Decimal128) -> Bool: """Less than comparison operator. See `less()` for more information. + + Args: + other: The value to compare against. + + Returns: + `True` if this value is less than `other`, `False` otherwise. """ return decimo.decimal128.comparison.less(self, other) @@ -1330,6 +1736,12 @@ struct Decimal128( def __ge__(self, other: Decimal128) -> Bool: """Greater than or equal comparison operator. See `greater_equal()` for more information. + + Args: + other: The value to compare against. + + Returns: + `True` if this value is greater than or equal to `other`, `False` otherwise. """ return decimo.decimal128.comparison.greater_equal(self, other) @@ -1337,6 +1749,12 @@ struct Decimal128( def __le__(self, other: Decimal128) -> Bool: """Less than or equal comparison operator. See `less_equal()` for more information. + + Args: + other: The value to compare against. + + Returns: + `True` if this value is less than or equal to `other`, `False` otherwise. """ return decimo.decimal128.comparison.less_equal(self, other) @@ -1344,6 +1762,12 @@ struct Decimal128( def __eq__(self, other: Decimal128) -> Bool: """Equality comparison operator. See `equal()` for more information. + + Args: + other: The value to compare against. + + Returns: + `True` if the values are equal, `False` otherwise. """ return decimo.decimal128.comparison.equal(self, other) @@ -1351,6 +1775,12 @@ struct Decimal128( def __ne__(self, other: Decimal128) -> Bool: """Inequality comparison operator. See `not_equal()` for more information. + + Args: + other: The value to compare against. + + Returns: + `True` if the values are not equal, `False` otherwise. """ return decimo.decimal128.comparison.not_equal(self, other) @@ -1367,6 +1797,12 @@ struct Decimal128( raises: Error: Calling `round()` failed. + + Args: + ndigits: The number of decimal places to round to. + + Returns: + The rounded `Decimal128` value. """ try: return decimo.decimal128.rounding.round( @@ -1379,7 +1815,11 @@ struct Decimal128( @always_inline def __round__(self) -> Self: - """**OVERLOAD**.""" + """**OVERLOAD**. + + Returns: + The `Decimal128` rounded to 0 decimal places. + """ try: return decimo.decimal128.rounding.round( self, ndigits=0, rounding_mode=RoundingMode.half_even() @@ -1403,6 +1843,13 @@ struct Decimal128( (1) Allows specifying the rounding mode. (2) Raises an error if the operation would result in overflow. See `round()` for more information. + + Args: + ndigits: The number of decimal places to round to. + rounding_mode: The rounding mode to apply. + + Returns: + The rounded `Decimal128` value. """ return decimo.decimal128.rounding.round( self, ndigits=ndigits, rounding_mode=rounding_mode @@ -1416,6 +1863,13 @@ struct Decimal128( ) raises -> Self: """Quantizes this Decimal128 to the specified exponent. See `quantize()` for more information. + + Args: + exp: A `Decimal128` whose scale determines the target precision. + rounding_mode: The rounding mode to apply. + + Returns: + The quantized `Decimal128` value. """ return decimo.decimal128.rounding.quantize(self, exp, rounding_mode) @@ -1423,6 +1877,9 @@ struct Decimal128( def exp(self) raises -> Self: """Calculates the exponential of this Decimal128. See `exp()` for more information. + + Returns: + The value of e raised to this power. """ return decimo.decimal128.exponential.exp(self) @@ -1430,33 +1887,67 @@ struct Decimal128( def ln(self) raises -> Self: """Calculates the natural logarithm of this Decimal128. See `ln()` for more information. + + Returns: + The natural logarithm of this value. """ return decimo.decimal128.exponential.ln(self) @always_inline def log10(self) raises -> Decimal128: - """Computes the base-10 logarithm of this Decimal128.""" + """Computes the base-10 logarithm of this Decimal128. + + Returns: + The base-10 logarithm of this value. + """ return decimo.decimal128.exponential.log10(self) @always_inline def log(self, base: Decimal128) raises -> Decimal128: - """Computes the logarithm of this Decimal128 with an arbitrary base.""" + """Computes the logarithm of this Decimal128 with an arbitrary base. + + Args: + base: The logarithm base. + + Returns: + The logarithm of this value in the given base. + """ return decimo.decimal128.exponential.log(self, base) @always_inline def power(self, exponent: Int) raises -> Decimal128: - """Raises this Decimal128 to the power of an integer.""" + """Raises this Decimal128 to the power of an integer. + + Args: + exponent: The integer exponent to raise to. + + Returns: + The value raised to the given power. + """ return decimo.decimal128.exponential.power(self, Self(exponent)) @always_inline def power(self, exponent: Decimal128) raises -> Decimal128: - """Raises this Decimal128 to the power of another Decimal128.""" + """Raises this Decimal128 to the power of another Decimal128. + + Args: + exponent: The `Decimal128` exponent to raise to. + + Returns: + The value raised to the given power. + """ return decimo.decimal128.exponential.power(self, exponent) @always_inline def root(self, n: Int) raises -> Self: """Calculates the n-th root of this Decimal128. See `root()` for more information. + + Args: + n: The degree of the root to compute. + + Returns: + The n-th root of this value. """ return decimo.decimal128.exponential.root(self, n) @@ -1464,6 +1955,9 @@ struct Decimal128( def sqrt(self) raises -> Self: """Calculates the square root of this Decimal128. See `sqrt()` for more information. + + Returns: + The square root of this value. """ return decimo.decimal128.exponential.sqrt(self) @@ -1507,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 @@ -1527,8 +2021,9 @@ struct Decimal128( End of examples. """ if precision_diff < 0: - raise Error( - "Error in `scale_up()`: precision_diff must be greater than 0" + raise ValueError( + message="precision_diff must be greater than or equal to 0.", + function="Decimal128.extend_precision()", ) if precision_diff == 0: @@ -1577,7 +2072,11 @@ struct Decimal128( return result def internal_representation(self) -> String: - """Returns the internal representation details as a String.""" + """Returns the internal representation details as a String. + + Returns: + A formatted string showing all internal fields. + """ # All labels var labels = List[String]() labels.append("Decimal128:") @@ -1656,7 +2155,11 @@ struct Decimal128( @always_inline def is_negative(self) -> Bool: - """Returns True if this Decimal128 is negative.""" + """Returns True if this Decimal128 is negative. + + Returns: + `True` if negative, `False` otherwise. + """ return (self.flags & Self.SIGN_MASK) != 0 @always_inline @@ -1664,6 +2167,9 @@ struct Decimal128( """Returns True if this Decimal128 represents the value 1. If 10^scale == coefficient, then it's one. `1` and `1.00` are considered ones. + + Returns: + `True` if this value equals 1, `False` otherwise. """ if self.is_negative(): return False @@ -1684,22 +2190,36 @@ struct Decimal128( """Returns True if this Decimal128 represents zero. A decimal128 is zero when all coefficient parts (low, mid, high) are zero, regardless of its sign or scale. + + Returns: + `True` if this value is zero, `False` otherwise. """ return self.low == 0 and self.mid == 0 and self.high == 0 @always_inline def is_infinity(self) -> Bool: - """Returns True if this Decimal128 is positive or negative infinity.""" + """Returns True if this Decimal128 is positive or negative infinity. + + Returns: + `True` if infinity, `False` otherwise. + """ return (self.flags & Self.INFINITY_MASK) != 0 @always_inline def is_nan(self) -> Bool: - """Returns True if this Decimal128 is NaN (Not a Number).""" + """Returns True if this Decimal128 is NaN (Not a Number). + + Returns: + `True` if NaN, `False` otherwise. + """ return (self.flags & Self.NAN_MASK) != 0 @always_inline def scale(self) -> Int: """Returns the scale (number of decimal128 places) of this Decimal128. + + Returns: + The scale as an `Int`. """ return Int((self.flags & Self.SCALE_MASK) >> Self.SCALE_SHIFT) diff --git a/src/decimo/decimal128/exponential.mojo b/src/decimo/decimal128/exponential.mojo index 5a81cab2..8a0380f9 100644 --- a/src/decimo/decimal128/exponential.mojo +++ b/src/decimo/decimal128/exponential.mojo @@ -20,6 +20,7 @@ import std.math from std import testing from std import time +from decimo.errors import ValueError, OverflowError, ZeroDivisionError import decimo.decimal128.constants import decimo.decimal128.special import decimo.decimal128.utility @@ -43,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 @@ -55,13 +55,16 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: try: return power(base, Int(exponent)) except e: - raise Error("Error in `power()` with Decimal128 exponent: ", e) + raise e^ # CASE: For negative bases, only integer exponents are supported if base.is_negative(): - raise Error( - "Negative base with non-integer exponent results in a complex" - " number" + raise ValueError( + message=( + "Negative base with non-integer exponent results in a" + " complex number." + ), + function="power()", ) # CASE: If the exponent is simple fractions @@ -70,13 +73,21 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: try: return sqrt(base) except e: - raise Error("Error in `power()` with Decimal128 exponent: ", 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("Error in `power()` with Decimal128 exponent: ", e) + raise ZeroDivisionError( + message="See the above exception.", + function="power()", + previous_error=e^, + ) # GENERAL CASE # Use the identity x^y = e^(y * ln(x)) @@ -85,7 +96,7 @@ def power(base: Decimal128, exponent: Decimal128) raises -> Decimal128: var product = exponent * ln_base return exp(product) except e: - raise Error("Error in `power()` with Decimal128 exponent: ", e) + raise e^ def power(base: Decimal128, exponent: Int) raises -> Decimal128: @@ -97,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 @@ -114,7 +128,10 @@ def power(base: Decimal128, exponent: Int) raises -> Decimal128: return Decimal128.ZERO() else: # 0^n is undefined for n < 0 - raise Error("Zero cannot be raised to a negative power") + raise ValueError( + message="Zero cannot be raised to a negative power.", + function="power()", + ) if base.coefficient() == 1 and base.scale() == 0: # 1^n = 1 for any n @@ -158,14 +175,16 @@ 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() # Special cases for n if n <= 0: - raise Error("Error in `root()`: Cannot compute non-positive root") + raise ValueError( + message="Cannot compute non-positive root.", + function="root()", + ) if n == 1: return x if n == 2: @@ -178,9 +197,9 @@ def root(x: Decimal128, n: Int) raises -> Decimal128: return Decimal128.ONE() if x.is_negative(): if n % 2 == 0: - raise Error( - "Error in `root()`: Cannot compute even root of a negative" - " number" + 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) @@ -193,7 +212,11 @@ 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("Error in `root()`: ", e) + raise ValueError( + message="Root computation failed.", + function="root()", + previous_error=e^, + ) # Initial guess # use floating point approach to quickly find a good guess @@ -330,12 +353,13 @@ 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(): - raise Error( - "Error in sqrt: Cannot compute square root of a negative number" + raise ValueError( + message="Cannot compute square root of a negative number.", + function="sqrt()", ) if x.is_zero(): @@ -455,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, @@ -463,9 +487,12 @@ def exp(x: Decimal128) raises -> Decimal128: """ if x > Decimal128.from_int(value=6654, scale=UInt32(2)): - raise Error( - "decimal.exponential.exp(): x is too large. It must be no greater" - " than 66.54 to avoid overflow. Consider using `BigDecimal` type." + raise OverflowError( + message=( + "x is too large (must be <= 66.54). Consider using" + " BigDecimal type." + ), + function="exp()", ) # Handle special cases @@ -662,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. @@ -672,8 +699,9 @@ def ln(x: Decimal128) raises -> Decimal128: # Handle special cases if x.is_negative() or x.is_zero(): - raise Error( - "Error in ln(): Cannot compute logarithm of a non-positive number" + raise ValueError( + message="Cannot compute logarithm of a non-positive number.", + function="ln()", ) if x.is_one(): @@ -926,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: @@ -935,19 +962,24 @@ def log(x: Decimal128, base: Decimal128) raises -> Decimal128: """ # Special cases: x <= 0 if x.is_negative() or x.is_zero(): - raise Error( - "Error in log(): Cannot compute logarithm of a non-positive number" + 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( - "Error in log(): Cannot use non-positive base for logarithm" + raise ValueError( + message="Cannot use non-positive base for logarithm.", + function="log()", ) # Special case: base = 1 if base.is_one(): - raise Error("Error in log(): Cannot use base 1 for logarithm") + raise ValueError( + message="Cannot use base 1 for logarithm.", + function="log()", + ) # Special case: x = 1 # log_base(1) = 0 for any valid base @@ -980,16 +1012,16 @@ 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). """ # Special cases: x <= 0 if x.is_negative() or x.is_zero(): - raise Error( - "Error in log10(): Cannot compute logarithm of a non-positive" - " number" + 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 25a8e8b4..fe37968d 100644 --- a/src/decimo/decimal128/rounding.mojo +++ b/src/decimo/decimal128/rounding.mojo @@ -33,6 +33,7 @@ from std import testing from decimo.decimal128.decimal128 import Decimal128 from decimo.rounding_mode import RoundingMode +from decimo.errors import OverflowError import decimo.decimal128.utility # ===------------------------------------------------------------------------===# @@ -64,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 @@ -94,16 +98,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( - String( - "Error in `round()`: `ndigits = {}` causes the number of" - " digits in the significant figures of the result (={})" - " exceeds the maximum capacity (={})." + 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 @@ -114,12 +118,12 @@ def round( if (ndigits_of_x + scale_diff == Decimal128.MAX_NUM_DIGITS) and ( res_coef > Decimal128.MAX_AS_UINT128 ): - raise Error( - String( - "Error in `round()`: `ndigits = {}` causes the" - " significant digits of the result (={}) exceeds the" - " maximum capacity (={})." - ).format(ndigits, res_coef, Decimal128.MAX_AS_UINT128) + 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 7b14c58a..4ba89e10 100644 --- a/src/decimo/decimal128/special.mojo +++ b/src/decimo/decimal128/special.mojo @@ -20,6 +20,8 @@ """Implements functions for special operations on Decimal128 objects.""" +from decimo.errors import ValueError, OverflowError + def factorial(n: Int) raises -> Decimal128: """Calculates the factorial of a non-negative integer. @@ -30,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. @@ -37,11 +43,17 @@ def factorial(n: Int) raises -> Decimal128: """ if n < 0: - raise Error("Factorial is not defined for negative numbers") + raise ValueError( + message="Factorial is not defined for negative numbers.", + function="factorial()", + ) if n > 27: - raise Error( - String("{}! is too large to be represented by Decimal128").format(n) + 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 @@ -130,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. @@ -166,7 +181,10 @@ def factorial_reciprocal(n: Int) raises -> Decimal128: # 1/27! = 0.0000000000000000000000000001, Decimal128.from_words(0x1, 0x0, 0x0, 0x1c0000) if n < 0: - raise Error("Factorial reciprocal is not defined for negative numbers") + 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 # Return 0 with max scale diff --git a/src/decimo/decimal128/utility.mojo b/src/decimo/decimal128/utility.mojo index 9cc012bc..0ebf7900 100644 --- a/src/decimo/decimal128/utility.mojo +++ b/src/decimo/decimal128/utility.mojo @@ -561,6 +561,15 @@ def number_of_bits[dtype: DType, //](var value: Scalar[dtype]) -> Int: Constraints: `dtype` must be integral. + + Parameters: + dtype: The scalar type of the input value. + + Args: + value: The integer value to count bits in. + + Returns: + The number of significant bits in the value. """ comptime assert dtype.is_integral(), "must be intergral" diff --git a/src/decimo/errors.mojo b/src/decimo/errors.mojo index 183fa913..01942519 100644 --- a/src/decimo/errors.mojo +++ b/src/decimo/errors.mojo @@ -16,147 +16,242 @@ """ Implements error handling for Decimo. -""" - -from std.pathlib.path import cwd -import decimo.str -comptime OverflowError = DecimoError[error_type="OverflowError"] -"""Type for overflow errors in Decimo. +The error messages follow the Python traceback format as closely as possible: -Fields: +``` +Traceback (most recent call last): + File "./src/decimo/bigint/bigint.mojo", line 42, in my_function +ValueError: description of what went wrong +``` -file: The file where the error occurred.\\ -function: The function where the error occurred.\\ -message: An optional message describing the error.\\ -previous_error: An optional previous error that caused this error. +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. +Function name must be provided manually since Mojo does not have a built-in way +to get the current function name at runtime. """ -comptime IndexError = DecimoError[error_type="IndexError"] -"""Type for index errors in Decimo. +from std.reflection import call_location -Fields: -file: The file where the error occurred.\\ -function: The function where the error occurred.\\ -message: An optional message describing the error.\\ -previous_error: An optional previous error that caused this error. -""" +# ===--- ANSI Color Codes ---=== # +# Mimics Python/Rich traceback coloring style. -comptime KeyError = DecimoError[error_type="KeyError"] -"""Type for key errors in Decimo. +comptime _RESET = "\033[0m" +comptime _BOLD = "\033[1m" +comptime _DIM = "\033[2m" -Fields: +comptime _RED = "\033[31m" +comptime _GREEN = "\033[32m" +comptime _YELLOW = "\033[33m" +comptime _BLUE = "\033[34m" +comptime _MAGENTA = "\033[35m" +comptime _CYAN = "\033[36m" +comptime _WHITE = "\033[37m" -file: The file where the error occurred.\\ -function: The function where the error occurred.\\ -message: An optional message describing the error.\\ -previous_error: An optional previous error that caused this error. -""" +# Semantic aliases for error formatting. +comptime _CLR_ERROR_TYPE = _BOLD + _RED # Error type name (e.g., ValueError) +comptime _CLR_TRACEBACK = _BOLD # "Traceback (most recent call last):" +comptime _CLR_FILE_PATH = _MAGENTA # File path +comptime _CLR_LINE_NUM = _GREEN # Line number +comptime _CLR_FUNC_NAME = _YELLOW # Function name +comptime _CLR_MSG_TEXT = _BOLD # Error message text +comptime _CLR_CHAIN_MSG = _DIM # Chained error separator message -comptime ValueError = DecimoError[error_type="ValueError"] -"""Type for value errors in Decimo. +comptime OverflowError = DecimoError[error_type="OverflowError"] +"""Type for overflow errors in Decimo.""" -Fields: +comptime IndexError = DecimoError[error_type="IndexError"] +"""Type for index errors in Decimo.""" -file: The file where the error occurred.\\ -function: The function where the error occurred.\\ -message: An optional message describing the error.\\ -previous_error: An optional previous error that caused this error. -""" +comptime KeyError = DecimoError[error_type="KeyError"] +"""Type for key errors in Decimo.""" +comptime ValueError = DecimoError[error_type="ValueError"] +"""Type for value errors in Decimo.""" comptime ZeroDivisionError = DecimoError[error_type="ZeroDivisionError"] +"""Type for divided-by-zero errors in Decimo.""" -"""Type for divided-by-zero errors in Decimo. +comptime ConversionError = DecimoError[error_type="ConversionError"] +"""Type for conversion errors in Decimo.""" -Fields: +comptime RuntimeError = DecimoError[error_type="RuntimeError"] +"""Type for runtime infrastructure errors in Decimo (e.g., resource allocation +failures, missing native libraries).""" -file: The file where the error occurred.\\ -function: The function where the error occurred.\\ -message: An optional message describing the error.\\ -previous_error: An optional previous error that caused this error. -""" -comptime ConversionError = DecimoError[error_type="ConversionError"] +def _shorten_path(full_path: String) -> String: + """Shorten an absolute file path to a relative path. -"""Type for conversion errors in Decimo. + 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. -Fields: + 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. -file: The file where the error occurred.\\ -function: The function where the error occurred.\\ -message: An optional message describing the error.\\ -previous_error: An optional previous error that caused this error. -""" + Args: + full_path: The absolute file path to shorten. -comptime HEADER_OF_ERROR_MESSAGE = """ ---------------------------------------------------------------------------- -DecimoError Traceback (most recent call last) -""" + 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. + The error message format mimics Python's traceback: -struct DecimoError[error_type: String = "DecimoError"](Writable): - """Base type for all Decimo errors. + ``` + Traceback (most recent call last): + 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. Parameters: error_type: The type of the error, e.g., "OverflowError", "IndexError". - - Fields: - - file: The file where the error occurred.\\ - function: The function where the error occurred.\\ - message: An optional message describing the error.\\ - previous_error: An optional previous error that caused this error. """ var file: String + """The source file where the error occurred (auto-captured).""" + var line: Int + """The line number where the error occurred (auto-captured).""" var function: String - var message: Optional[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.""" + @always_inline def __init__( out self, - file: String, + *, + message: String, function: String, - message: Optional[String], - previous_error: Optional[Error], + previous_error: Optional[Error] = None, ): - self.file = file + """Creates a new `DecimoError` with auto-captured file and line. + + File name and line number are automatically captured from the call site. + + Args: + 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() # Comptime evaluated + self.file = _shorten_path(String(loc.file_name)) + self.line = loc.line self.function = function self.message = message if previous_error is None: self.previous_error = None else: - self.previous_error = "\n".join( - String(previous_error.value()).split("\n")[3:] - ) + self.previous_error = String(previous_error.value()) def write_to[W: Writer](self, mut writer: W): + """Writes a Python-style formatted error traceback to a writer. + + Output format (colored with ANSI codes): + + ``` + Traceback (most recent call last): + File "./src/decimo/bigint/bigint.mojo", line 42, in my_function + ValueError: description of what went wrong + ``` + + When a previous error is chained: + + ``` + Traceback (most recent call last): + 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 "./src/decimo/bigint/bigint.mojo", line 20, in outer_function + DecimoError: outer error message + ``` + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ + # Chained previous error (printed FIRST, like Python) + if self.previous_error is not None: + writer.write(self.previous_error.value()) + writer.write("\n") + writer.write(_CLR_CHAIN_MSG) + writer.write( + "The above exception was the direct cause of the following" + " exception:" + ) + writer.write(_RESET) + writer.write("\n") + + # "Traceback (most recent call last):" + writer.write(_CLR_TRACEBACK) + writer.write("Traceback (most recent call last):") + writer.write(_RESET) writer.write("\n") - writer.write(("-" * 80)) - writer.write("\n") - writer.write(decimo.str.ljust(String(Self.error_type), 47, " ")) - writer.write("Traceback (most recent call last)\n") - writer.write('File "') - try: - writer.write(String(cwd())) - except e: - pass - finally: - writer.write("/") + + # ' File "/path/to/file.mojo", line 42, in function_name' + writer.write(" File ") + writer.write('"') + writer.write(_CLR_FILE_PATH) writer.write(self.file) - writer.write('"\n') - writer.write("----> ") + writer.write(_RESET) + writer.write('"') + writer.write(", line ") + writer.write(_CLR_LINE_NUM) + writer.write(String(self.line)) + writer.write(_RESET) + writer.write(", in ") + writer.write(_CLR_FUNC_NAME) writer.write(self.function) - if self.message is None: - writer.write("\n") - else: - writer.write("\n\n") - writer.write(Self.error_type) - writer.write(": ") - 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") + + # "ValueError: description of what went wrong" + writer.write(_CLR_ERROR_TYPE) + writer.write(Self.error_type) + 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/gmp/README.md b/src/decimo/gmp/README.md new file mode 100644 index 00000000..51917229 --- /dev/null +++ b/src/decimo/gmp/README.md @@ -0,0 +1,49 @@ +# GMP/MPFR C Wrapper + +This directory contains the C wrapper that bridges Mojo to MPFR/GMP via FFI. + +## Architecture + +The wrapper uses `dlopen`/`dlsym` to load MPFR lazily at **runtime**. This means: + +- **Build time**: Only a C compiler is needed. No MPFR/GMP headers or libraries + are required. The wrapper manually declares `mpfr_t` storage and function pointer + signatures based on the public MPFR C API. +- **ABI note**: The `mpfr_t` storage is conservatively over-allocated (64 bytes, + 16-byte aligned) to accommodate all known MPFR builds. This has been verified + on ARM64 macOS and x86_64 Linux. If you encounter issues on an unusual platform, + rebuilding with `mpfr.h` included would provide a compile-time size check. +- **Runtime**: If MPFR is installed, `mpfrw_available()` returns 1 and all + operations work. If not installed, it returns 0 and BigFloat raises a clear error. + +## Build + +```bash +bash src/decimo/gmp/build_gmp_wrapper.sh +``` + +## Files + +- `gmp_wrapper.c` — The C wrapper source. Manages an `mpfr_t` handle pool and + provides thin wrappers around MPFR functions. +- `build_gmp_wrapper.sh` — Build script for macOS and Linux. +- `libdecimo_gmp_wrapper.dylib` / `.so` — Compiled output (not checked in). + +## Installing MPFR + +```bash +# macOS (Homebrew) +brew install mpfr + +# Linux (Debian/Ubuntu) +sudo apt install libmpfr-dev + +# Verify +ls /opt/homebrew/lib/libmpfr.dylib # macOS ARM64 +ls /usr/lib/x86_64-linux-gnu/libmpfr.so # Linux x86_64 +``` + +## Environment Variables + +- `DECIMO_NOGMP=1` — Force-disable MPFR loading even if installed. Useful for testing + pure-Mojo fallback paths. diff --git a/src/decimo/gmp/build_gmp_wrapper.sh b/src/decimo/gmp/build_gmp_wrapper.sh new file mode 100755 index 00000000..45563891 --- /dev/null +++ b/src/decimo/gmp/build_gmp_wrapper.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# ===----------------------------------------------------------------------=== # +# Build script for the Decimo GMP/MPFR C wrapper. +# +# This compiles gmp_wrapper.c into a shared library. The wrapper uses +# dlopen/dlsym to load MPFR at runtime, so it does NOT need -lmpfr or -lgmp +# at compile time. Any system with a C compiler can build this. +# +# Usage: +# bash src/decimo/gmp/build_gmp_wrapper.sh +# +# Output: +# src/decimo/gmp/libdecimo_gmp_wrapper.dylib (macOS) +# src/decimo/gmp/libdecimo_gmp_wrapper.so (Linux) +# +# Prerequisites: +# - A C compiler (cc, gcc, or clang) +# - For actual MPFR use at runtime: brew install mpfr (macOS) +# or apt install libmpfr-dev (Linux) +# ===----------------------------------------------------------------------=== # + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SRC="$SCRIPT_DIR/gmp_wrapper.c" + +OS="$(uname)" +if [[ "$OS" == "Darwin" ]]; then + OUT="$SCRIPT_DIR/libdecimo_gmp_wrapper.dylib" + cc -shared -O2 -o "$OUT" "$SRC" + # Set install name so the loader finds it via rpath + install_name_tool -id @rpath/libdecimo_gmp_wrapper.dylib "$OUT" + echo "Built: $OUT" +elif [[ "$OS" == "Linux" ]]; then + OUT="$SCRIPT_DIR/libdecimo_gmp_wrapper.so" + cc -shared -O2 -fPIC -o "$OUT" "$SRC" -ldl + echo "Built: $OUT" +else + echo "Unsupported OS: $OS" + exit 1 +fi diff --git a/src/decimo/gmp/gmp_wrapper.c b/src/decimo/gmp/gmp_wrapper.c new file mode 100644 index 00000000..861ba0d2 --- /dev/null +++ b/src/decimo/gmp/gmp_wrapper.c @@ -0,0 +1,418 @@ +/* + * ===----------------------------------------------------------------------=== * + * 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. + * ===----------------------------------------------------------------------=== * + * + * gmp_wrapper.c — C bridge between Mojo and MPFR/GMP. + * + * This file uses dlopen/dlsym to load MPFR lazily at runtime. It compiles + * on any system with a C compiler — no MPFR/GMP headers needed at build time. + * + * License note: This file does not contain any code copied from the GMP or + * MPFR projects. All function signatures were written from scratch based on + * the publicly documented MPFR C API. The only interaction with MPFR/GMP is + * through runtime symbol resolution via dlsym. MPFR (LGPLv3+) and GMP + * (LGPLv3+ or GPLv2+) are loaded dynamically only if installed by the user. + * + * Architecture: + * - A pool of mpfr_t handles (fixed-size array, index-based) + * - Mojo side only sees Int32 handle indices + * - All MPFR functions resolved via dlsym at first use + * - If MPFR is not installed, mpfrw_available() returns 0 + * + * Build: + * cc -shared -O2 -o libdecimo_gmp_wrapper.dylib gmp_wrapper.c -ldl + * (No -lmpfr or -lgmp needed — loaded at runtime via dlopen) + */ + +#include +#include +#include +#include + +/* ===----------------------------------------------------------------------=== * + * Constants + * ===----------------------------------------------------------------------=== */ + +#define MAX_HANDLES 4096 +#define MAX_PREC_BITS 1048576 /* 1M bits ≈ 315K decimal digits — safety cap */ + +/* MPFR rounding mode: round to nearest (ties to even) */ +#define MPFR_RNDN 0 + +/* ===----------------------------------------------------------------------=== * + * MPFR function pointer typedefs + * + * These match the MPFR C API signatures. We resolve them via dlsym so we + * don't need mpfr.h at compile time. + * ===----------------------------------------------------------------------=== */ + +/* mpfr_t is internally a struct. The size is not guaranteed by the MPFR ABI, + * but has been 32 bytes on all tested platforms (ARM64 macOS, x86_64 Linux) + * since MPFR 3.x. We over-allocate to 64 bytes and align to 16 bytes for + * safety. A runtime assertion in mpfrw_load() verifies the actual size. + * If MPFR headers are available at build time, the compile-time static_assert + * below catches any mismatch. */ +#define MPFR_T_SIZE 64 +#define MPFR_T_ALIGN 16 + +typedef void (*fn_mpfr_init2)(void *x, long prec); +typedef void (*fn_mpfr_clear)(void *x); +typedef int (*fn_mpfr_set_str)(void *rop, const char *s, int base, int rnd); +typedef char* (*fn_mpfr_get_str)(char *str, long *exp, int base, size_t n, const void *op, int rnd); +typedef void (*fn_mpfr_free_str)(char *str); +typedef int (*fn_mpfr_add)(void *rop, const void *op1, const void *op2, int rnd); +typedef int (*fn_mpfr_sub)(void *rop, const void *op1, const void *op2, int rnd); +typedef int (*fn_mpfr_mul)(void *rop, const void *op1, const void *op2, int rnd); +typedef int (*fn_mpfr_div)(void *rop, const void *op1, const void *op2, int rnd); +typedef int (*fn_mpfr_neg)(void *rop, const void *op, int rnd); +typedef int (*fn_mpfr_abs)(void *rop, const void *op, int rnd); +typedef int (*fn_mpfr_cmp)(const void *op1, const void *op2); +typedef int (*fn_mpfr_sqrt)(void *rop, const void *op, int rnd); +typedef int (*fn_mpfr_exp)(void *rop, const void *op, int rnd); +typedef int (*fn_mpfr_log)(void *rop, const void *op, int rnd); +typedef int (*fn_mpfr_sin)(void *rop, const void *op, int rnd); +typedef int (*fn_mpfr_cos)(void *rop, const void *op, int rnd); +typedef int (*fn_mpfr_tan)(void *rop, const void *op, int rnd); +typedef int (*fn_mpfr_pow)(void *rop, const void *op1, const void *op2, int rnd); +typedef int (*fn_mpfr_rootn_ui)(void *rop, const void *op, unsigned long n, int rnd); +typedef int (*fn_mpfr_const_pi)(void *rop, int rnd); +typedef long (*fn_mpfr_get_prec)(const void *x); +typedef int (*fn_mpfr_snprintf)(char *buf, size_t n, const char *fmt, ...); + +/* ===----------------------------------------------------------------------=== * + * Static state + * ===----------------------------------------------------------------------=== */ + +static void *mpfr_lib = NULL; +static int mpfr_load_attempted = 0; + +/* Function pointers — resolved by mpfrw_load() */ +static fn_mpfr_init2 p_init2 = NULL; +static fn_mpfr_clear p_clear = NULL; +static fn_mpfr_set_str p_set_str = NULL; +static fn_mpfr_get_str p_get_str = NULL; +static fn_mpfr_free_str p_free_str = NULL; +static fn_mpfr_add p_add = NULL; +static fn_mpfr_sub p_sub = NULL; +static fn_mpfr_mul p_mul = NULL; +static fn_mpfr_div p_div = NULL; +static fn_mpfr_neg p_neg = NULL; +static fn_mpfr_abs p_abs = NULL; +static fn_mpfr_cmp p_cmp = NULL; +static fn_mpfr_sqrt p_sqrt = NULL; +static fn_mpfr_exp p_exp = NULL; +static fn_mpfr_log p_log = NULL; +static fn_mpfr_sin p_sin = NULL; +static fn_mpfr_cos p_cos = NULL; +static fn_mpfr_tan p_tan = NULL; +static fn_mpfr_pow p_pow = NULL; +static fn_mpfr_rootn_ui p_rootn_ui = NULL; +static fn_mpfr_const_pi p_const_pi = NULL; +static fn_mpfr_snprintf p_snprintf = NULL; + +/* Handle pool: byte arrays sized to hold mpfr_t, with proper alignment */ +static _Alignas(MPFR_T_ALIGN) char handle_pool[MAX_HANDLES][MPFR_T_SIZE]; +static int handle_in_use[MAX_HANDLES]; /* 0 = free, 1 = in use */ + +/* ===----------------------------------------------------------------------=== * + * Library loading (dlopen/dlsym) + * ===----------------------------------------------------------------------=== */ + +static int mpfrw_load(void) { + if (mpfr_load_attempted) return (mpfr_lib != NULL); + mpfr_load_attempted = 1; + + /* Check DECIMO_NOGMP environment variable */ + const char *nogmp = getenv("DECIMO_NOGMP"); + if (nogmp && nogmp[0] != '0' && nogmp[0] != '\0') { + return 0; + } + + /* Try common library paths */ + const char *paths[] = { + "libmpfr.dylib", /* macOS via rpath */ + "/opt/homebrew/lib/libmpfr.dylib", /* macOS ARM64 (Homebrew) */ + "/usr/local/lib/libmpfr.dylib", /* macOS x86_64 */ + "libmpfr.so", /* Linux via rpath */ + "/usr/lib/x86_64-linux-gnu/libmpfr.so", /* Debian/Ubuntu */ + "/usr/lib/aarch64-linux-gnu/libmpfr.so", /* Debian ARM64 */ + "/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 function pointers */ + p_init2 = (fn_mpfr_init2) dlsym(mpfr_lib, "mpfr_init2"); + p_clear = (fn_mpfr_clear) dlsym(mpfr_lib, "mpfr_clear"); + p_set_str = (fn_mpfr_set_str) dlsym(mpfr_lib, "mpfr_set_str"); + p_get_str = (fn_mpfr_get_str) dlsym(mpfr_lib, "mpfr_get_str"); + p_free_str = (fn_mpfr_free_str) dlsym(mpfr_lib, "mpfr_free_str"); + p_add = (fn_mpfr_add) dlsym(mpfr_lib, "mpfr_add"); + p_sub = (fn_mpfr_sub) dlsym(mpfr_lib, "mpfr_sub"); + p_mul = (fn_mpfr_mul) dlsym(mpfr_lib, "mpfr_mul"); + p_div = (fn_mpfr_div) dlsym(mpfr_lib, "mpfr_div"); + p_neg = (fn_mpfr_neg) dlsym(mpfr_lib, "mpfr_neg"); + p_abs = (fn_mpfr_abs) dlsym(mpfr_lib, "mpfr_abs"); + p_cmp = (fn_mpfr_cmp) dlsym(mpfr_lib, "mpfr_cmp"); + p_sqrt = (fn_mpfr_sqrt) dlsym(mpfr_lib, "mpfr_sqrt"); + p_exp = (fn_mpfr_exp) dlsym(mpfr_lib, "mpfr_exp"); + p_log = (fn_mpfr_log) dlsym(mpfr_lib, "mpfr_log"); + p_sin = (fn_mpfr_sin) dlsym(mpfr_lib, "mpfr_sin"); + p_cos = (fn_mpfr_cos) dlsym(mpfr_lib, "mpfr_cos"); + p_tan = (fn_mpfr_tan) dlsym(mpfr_lib, "mpfr_tan"); + p_pow = (fn_mpfr_pow) dlsym(mpfr_lib, "mpfr_pow"); + p_rootn_ui = (fn_mpfr_rootn_ui) dlsym(mpfr_lib, "mpfr_rootn_ui"); + p_const_pi = (fn_mpfr_const_pi) dlsym(mpfr_lib, "mpfr_const_pi"); + p_snprintf = (fn_mpfr_snprintf) dlsym(mpfr_lib, "mpfr_snprintf"); + + /* Verify critical function pointers — if any core symbol is missing, + * the MPFR library is too old or incompatible. Fail the load. */ + if (!p_init2 || !p_clear || !p_set_str || !p_get_str || !p_free_str || + !p_add || !p_sub || !p_mul || !p_div || !p_neg || !p_abs || !p_cmp) { + dlclose(mpfr_lib); + mpfr_lib = NULL; + return 0; + } + + /* Initialize handle pool */ + memset(handle_in_use, 0, sizeof(handle_in_use)); + + return 1; +} + +/* ===----------------------------------------------------------------------=== * + * Public API — called from Mojo via external_call + * ===----------------------------------------------------------------------=== */ + +int mpfrw_available(void) { + return mpfrw_load(); +} + +int mpfrw_init(int prec_bits) { + if (!mpfrw_load()) return -1; + if (prec_bits < 2) prec_bits = 2; + if (prec_bits > MAX_PREC_BITS) return -1; /* precision cap */ + + /* Find a free handle slot */ + for (int i = 0; i < MAX_HANDLES; i++) { + if (!handle_in_use[i]) { + handle_in_use[i] = 1; + p_init2(&handle_pool[i], (long)prec_bits); + return i; + } + } + return -1; /* pool exhausted */ +} + +void mpfrw_clear(int handle) { + if (handle < 0 || handle >= MAX_HANDLES) return; + if (!handle_in_use[handle]) return; + if (!p_clear) return; /* MPFR not loaded — nothing to free */ + p_clear(&handle_pool[handle]); + handle_in_use[handle] = 0; +} + +int mpfrw_set_str(int handle, const char *s, int length) { + if (handle < 0 || handle >= MAX_HANDLES || !handle_in_use[handle]) return -1; + + /* Copy to null-terminated buffer (s may not be null-terminated) */ + char *buf = (char *)malloc(length + 1); + if (!buf) return -1; + memcpy(buf, s, length); + buf[length] = '\0'; + + int ret = p_set_str(&handle_pool[handle], buf, 10, MPFR_RNDN); + free(buf); + return ret; +} + +char* mpfrw_get_str(int handle, int digits) { + if (handle < 0 || handle >= MAX_HANDLES || !handle_in_use[handle]) return NULL; + if (digits < 1) digits = 1; + + /* Use mpfr_snprintf to format as decimal string */ + /* Format: %.RNf for fixed notation, %.RNg for general */ + int bufsize = digits + 64; /* extra space for sign, dot, exponent */ + char *buf = (char *)malloc(bufsize); + if (!buf) return NULL; + + if (p_snprintf) { + /* %.*Rg: general format with N significant digits, round to nearest */ + p_snprintf(buf, bufsize, "%.*Rg", digits, &handle_pool[handle]); + } else { + /* Fallback: use mpfr_get_str + manual formatting */ + long exp; + char *raw = p_get_str(NULL, &exp, 10, digits, &handle_pool[handle], MPFR_RNDN); + if (!raw) { free(buf); return NULL; } + /* TODO: Format raw mantissa + exponent into decimal string */ + snprintf(buf, bufsize, "%sE%ld", raw, exp); + if (p_free_str) p_free_str(raw); + } + return buf; +} + +void mpfrw_free_str(char *s) { + if (s) free(s); +} + +/* ===----------------------------------------------------------------------=== * + * Raw digit export via mpfr_get_str (for fast BigFloat → BigDecimal) + * + * p_get_str is mpfr_get_str resolved at runtime via dlsym. It returns a + * pure digit string plus a separate base-10 exponent: + * + * raw = "31415...", exp = 1 → value = 0.31415... × 10^1 = 3.1415... + * + * No dot, no 'e' notation. Negative values have a '-' prefix. + * The exponent is written to the caller through the out_exp pointer — + * no mutable global state, no two-call pattern. + * + * The returned string is allocated by MPFR and must be freed with + * mpfrw_free_raw_str(). + * ===----------------------------------------------------------------------=== */ + +char* mpfrw_get_raw_digits(int handle, int digits, long *out_exp) { + if (handle < 0 || handle >= MAX_HANDLES || !handle_in_use[handle]) return NULL; + if (!p_get_str) return NULL; + if (digits < 1) digits = 1; + long exp = 0; + char *raw = p_get_str(NULL, &exp, 10, (size_t)digits, + &handle_pool[handle], MPFR_RNDN); + if (out_exp) *out_exp = exp; + return raw; /* Caller frees with mpfrw_free_raw_str */ +} + +void mpfrw_free_raw_str(char *s) { + if (s && p_free_str) p_free_str(s); +} + +/* ===----------------------------------------------------------------------=== * + * Arithmetic operations + * ===----------------------------------------------------------------------=== */ + +#define CHECK_HANDLES_2(r, a) \ + if ((r) < 0 || (r) >= MAX_HANDLES || !handle_in_use[(r)]) return; \ + if ((a) < 0 || (a) >= MAX_HANDLES || !handle_in_use[(a)]) return; + +#define CHECK_HANDLES_3(r, a, b) \ + CHECK_HANDLES_2(r, a) \ + if ((b) < 0 || (b) >= MAX_HANDLES || !handle_in_use[(b)]) return; + +void mpfrw_add(int r, int a, int b) { + CHECK_HANDLES_3(r, a, b); + p_add(&handle_pool[r], &handle_pool[a], &handle_pool[b], MPFR_RNDN); +} + +void mpfrw_sub(int r, int a, int b) { + CHECK_HANDLES_3(r, a, b); + p_sub(&handle_pool[r], &handle_pool[a], &handle_pool[b], MPFR_RNDN); +} + +void mpfrw_mul(int r, int a, int b) { + CHECK_HANDLES_3(r, a, b); + p_mul(&handle_pool[r], &handle_pool[a], &handle_pool[b], MPFR_RNDN); +} + +void mpfrw_div(int r, int a, int b) { + CHECK_HANDLES_3(r, a, b); + p_div(&handle_pool[r], &handle_pool[a], &handle_pool[b], MPFR_RNDN); +} + +void mpfrw_neg(int r, int a) { + CHECK_HANDLES_2(r, a); + p_neg(&handle_pool[r], &handle_pool[a], MPFR_RNDN); +} + +void mpfrw_abs(int r, int a) { + CHECK_HANDLES_2(r, a); + p_abs(&handle_pool[r], &handle_pool[a], MPFR_RNDN); +} + +int mpfrw_cmp(int a, int b) { + if (a < 0 || a >= MAX_HANDLES || !handle_in_use[a]) return -2; /* error sentinel */ + if (b < 0 || b >= MAX_HANDLES || !handle_in_use[b]) return -2; /* error sentinel */ + int result = p_cmp(&handle_pool[a], &handle_pool[b]); + /* Normalize to -1/0/1 so that -2 stays reserved as error sentinel. */ + if (result < 0) return -1; + if (result > 0) return 1; + return 0; +} + +/* ===----------------------------------------------------------------------=== * + * Transcendental operations + * ===----------------------------------------------------------------------=== */ + +void mpfrw_sqrt(int r, int a) { + CHECK_HANDLES_2(r, a); + if (p_sqrt) p_sqrt(&handle_pool[r], &handle_pool[a], MPFR_RNDN); +} + +void mpfrw_exp(int r, int a) { + CHECK_HANDLES_2(r, a); + if (p_exp) p_exp(&handle_pool[r], &handle_pool[a], MPFR_RNDN); +} + +void mpfrw_log(int r, int a) { + CHECK_HANDLES_2(r, a); + if (p_log) p_log(&handle_pool[r], &handle_pool[a], MPFR_RNDN); +} + +void mpfrw_sin(int r, int a) { + CHECK_HANDLES_2(r, a); + if (p_sin) p_sin(&handle_pool[r], &handle_pool[a], MPFR_RNDN); +} + +void mpfrw_cos(int r, int a) { + CHECK_HANDLES_2(r, a); + if (p_cos) p_cos(&handle_pool[r], &handle_pool[a], MPFR_RNDN); +} + +void mpfrw_tan(int r, int a) { + CHECK_HANDLES_2(r, a); + if (p_tan) p_tan(&handle_pool[r], &handle_pool[a], MPFR_RNDN); +} + +void mpfrw_pow(int r, int a, int b) { + CHECK_HANDLES_3(r, a, b); + if (p_pow) p_pow(&handle_pool[r], &handle_pool[a], &handle_pool[b], MPFR_RNDN); +} + +void mpfrw_rootn_ui(int r, int a, unsigned int n) { + CHECK_HANDLES_2(r, a); + if (p_rootn_ui) p_rootn_ui(&handle_pool[r], &handle_pool[a], (unsigned long)n, MPFR_RNDN); +} + +void mpfrw_const_pi(int r) { + if (r < 0 || r >= MAX_HANDLES || !handle_in_use[r]) return; + if (p_const_pi) p_const_pi(&handle_pool[r], MPFR_RNDN); +} + +/* ===----------------------------------------------------------------------=== * + * Diagnostics + * ===----------------------------------------------------------------------=== */ + +int mpfrw_handles_in_use(void) { + int count = 0; + for (int i = 0; i < MAX_HANDLES; i++) { + if (handle_in_use[i]) count++; + } + return count; +} 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/src/decimo/rounding_mode.mojo b/src/decimo/rounding_mode.mojo index 68438588..3d83b8fe 100644 --- a/src/decimo/rounding_mode.mojo +++ b/src/decimo/rounding_mode.mojo @@ -54,12 +54,19 @@ struct RoundingMode(Copyable, ImplicitlyCopyable, Movable, Writable): # alias comptime ROUND_DOWN = Self.down() + """Truncate (toward zero).""" comptime ROUND_HALF_UP = Self.half_up() + """Round away from zero if >= 0.5.""" comptime ROUND_HALF_DOWN = Self.half_down() + """Round toward zero if <= 0.5.""" comptime ROUND_HALF_EVEN = Self.half_even() + """Round to nearest even digit if equidistant (banker's rounding).""" comptime ROUND_UP = Self.up() + """Round away from zero.""" comptime ROUND_CEILING = Self.ceiling() + """Round toward positive infinity.""" comptime ROUND_FLOOR = Self.floor() + """Round toward negative infinity.""" # Internal value var value: Int @@ -68,49 +75,106 @@ struct RoundingMode(Copyable, ImplicitlyCopyable, Movable, Writable): # Static constants for each rounding mode @staticmethod def down() -> Self: - """Truncate (toward zero).""" + """Creates a `RoundingMode` that truncates toward zero. + + Returns: + A `RoundingMode` representing truncation toward zero. + """ return Self(0) @staticmethod def half_up() -> Self: - """Round away from zero if >= 0.5.""" + """Creates a `RoundingMode` that rounds away from zero if >= 0.5. + + Returns: + A `RoundingMode` representing half-up rounding. + """ return Self(1) @staticmethod def half_down() -> Self: - """Round toward zero if <= 0.5.""" + """Creates a `RoundingMode` that rounds toward zero if <= 0.5. + + Returns: + A `RoundingMode` representing half-down rounding. + """ return Self(6) @staticmethod def half_even() -> Self: - """Round to nearest even digit if equidistant (banker's rounding).""" + """Creates a `RoundingMode` that rounds to nearest even digit if equidistant. + + Returns: + A `RoundingMode` representing banker's rounding. + """ return Self(2) @staticmethod def up() -> Self: - """Round away from zero.""" + """Creates a `RoundingMode` that rounds away from zero. + + Returns: + A `RoundingMode` representing round-up. + """ return Self(3) @staticmethod def ceiling() -> Self: - """Round toward positive infinity.""" + """Creates a `RoundingMode` that rounds toward positive infinity. + + Returns: + A `RoundingMode` representing ceiling rounding. + """ return Self(4) @staticmethod def floor() -> Self: - """Round toward negative infinity.""" + """Creates a `RoundingMode` that rounds toward negative infinity. + + Returns: + A `RoundingMode` representing floor rounding. + """ return Self(5) def __init__(out self, value: Int): + """Creates a `RoundingMode` from its internal integer representation. + + Args: + value: The integer code identifying the rounding mode. + """ self.value = value def __eq__(self, other: Self) -> Bool: + """Checks whether two `RoundingMode` values are equal. + + Args: + other: The `RoundingMode` to compare against. + + Returns: + `True` if both rounding modes have the same internal value. + """ return self.value == other.value def __eq__(self, other: String) -> Bool: + """Checks whether this rounding mode matches a string name. + + Args: + other: The rounding mode name to compare against (e.g. "ROUND_DOWN"). + + Returns: + `True` if the string representation of this mode equals `other`. + """ return String(self) == other def write_to[W: Writer](self, mut writer: W): + """Writes the rounding mode name to a writer. + + Parameters: + W: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ if self == Self.down(): writer.write("ROUND_DOWN") elif self == Self.half_up(): diff --git a/src/decimo/str.mojo b/src/decimo/str.mojo index 82e771ce..d43f38a6 100644 --- a/src/decimo/str.mojo +++ b/src/decimo/str.mojo @@ -18,9 +18,20 @@ 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 fillchar on the left.""" + """Right-justifies a string by padding with a fill character on the left. + + Args: + s: The string to right-justify. + width: The minimum total width of the resulting string. + fillchar: The character used for padding (defaults to space). + + Returns: + The right-justified string, or the original string if it is already at least `width` characters. + """ var n = len(s) if n >= width: return s @@ -28,7 +39,16 @@ def rjust(s: String, width: Int, fillchar: String = " ") -> String: def ljust(s: String, width: Int, fillchar: String = " ") -> String: - """Left-justifies a string by padding with fillchar on the right.""" + """Left-justifies a string by padding with a fill character on the right. + + Args: + s: The string to left-justify. + width: The minimum total width of the resulting string. + fillchar: The character used for padding (defaults to space). + + Returns: + The left-justified string, or the original string if it is already at least `width` characters. + """ var n = len(s) if n >= width: return s @@ -85,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] @@ -106,7 +130,10 @@ def parse_numeric_string( var n = len(value_bytes) if n == 0: - raise Error("Error in `parse_numeric_string`: Empty string.") + raise ValueError( + message="Empty string.", + function="parse_numeric_string()", + ) var ptr = value_bytes.unsafe_ptr() @@ -170,9 +197,15 @@ 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 ValueError( + message="Decimal point cannot appear in the exponent part.", + function="parse_numeric_string()", + ) if decimal_point_pos != -1: - raise Error("Decimal point can only appear once.") + raise ValueError( + message="Decimal point can only appear once.", + function="parse_numeric_string()", + ) decimal_point_pos = i sign_read = True @@ -180,9 +213,15 @@ 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 ValueError( + message="Exponential notation can only appear once.", + function="parse_numeric_string()", + ) if total_mantissa_digits == 0: - raise Error("Exponential notation must follow a number.") + raise ValueError( + message="Exponential notation must follow a number.", + function="parse_numeric_string()", + ) exponent_pos = i in_exponent = True @@ -191,15 +230,21 @@ def parse_numeric_string( last_was_separator = True if in_exponent: if exponent_sign_read: - raise Error( - "Exponent sign can only appear once," - " before exponent digits." + 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( - "Minus sign can only appear once at the beginning." + raise ValueError( + message=( + "Minus sign can only appear once at the beginning." + ), + function="parse_numeric_string()", ) sign = True sign_read = True @@ -209,30 +254,43 @@ def parse_numeric_string( last_was_separator = True if in_exponent: if exponent_sign_read: - raise Error( - "Exponent sign can only appear once," - " before exponent digits." + 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( - "Plus sign can only appear once at the beginning." + raise ValueError( + message=( + "Plus sign can only appear once at the beginning." + ), + function="parse_numeric_string()", ) sign_read = True else: - raise Error( - String( + raise ValueError( + message=String( "Invalid character in the string of the number: {}" - ).format(chr(Int(c))) + ).format(chr(Int(c))), + function="parse_numeric_string()", ) if last_was_separator: - raise Error("Unexpected end character in the string of the number.") + raise ValueError( + message="Unexpected end character in the string of the number.", + function="parse_numeric_string()", + ) if total_mantissa_digits == 0: - raise Error("No digits found in the string of the number.") + raise ValueError( + message="No digits found in the string of the number.", + function="parse_numeric_string()", + ) # ================================================================== # 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 8923533b..76f553c7 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 ValueError from std.python import Python, PythonObject from std.collections import List from std import os @@ -73,13 +74,25 @@ struct TestCase(Copyable, Movable, Writable): """ var a: String + """The first input operand as a numeric string.""" var b: String + """The second input operand as a numeric string (empty for unary tests).""" var expected: String + """The expected output value as a numeric string.""" var description: String + """A human-readable description of the test case.""" def __init__( out self, a: String, b: String, expected: String, description: String ): + """Creates a `TestCase` from the given operands and expected result. + + Args: + a: The first input operand as a numeric string. + b: The second input operand as a numeric string. + expected: The expected output value as a numeric string. + description: A short description of what the test case covers. + """ self.a = a self.b = b self.expected = expected @@ -88,18 +101,36 @@ struct TestCase(Copyable, Movable, Writable): ) def __init__(out self, *, copy: Self): + """Creates a copy of a `TestCase`. + + Args: + copy: The instance to copy from. + """ self.a = copy.a self.b = copy.b self.expected = copy.expected self.description = copy.description def __init__(out self, *, deinit take: Self): + """Moves a `TestCase` into a new instance. + + Args: + take: The instance to move from. + """ self.a = take.a^ self.b = take.b^ self.expected = take.expected^ self.description = take.description^ def write_to[T: Writer](self, mut writer: T): + """Writes a formatted representation of the test case to a writer. + + Parameters: + T: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ writer.write("TestCase:\n") writer.write(" a: " + self.a + "\n") writer.write(" b: " + self.b + "\n") @@ -116,25 +147,53 @@ struct BenchCase(Copyable, Movable, Writable): """A benchmark case with a name and one or two operands.""" var name: String + """The display name of the benchmark case.""" var a: String + """The first operand as a numeric string.""" var b: String + """The second operand as a numeric string (empty for unary benchmarks).""" def __init__(out self, name: String, a: String, b: String = ""): + """Creates a `BenchCase` with the given name and operands. + + Args: + name: The display name of the benchmark case. + a: The first operand as a numeric string. + b: The second operand as a numeric string (defaults to empty). + """ self.name = name self.a = a self.b = b def __init__(out self, *, copy: Self): + """Creates a copy of a `BenchCase`. + + Args: + copy: The instance to copy from. + """ self.name = copy.name self.a = copy.a self.b = copy.b def __init__(out self, *, deinit take: Self): + """Moves a `BenchCase` into a new instance. + + Args: + take: The instance to move from. + """ self.name = take.name^ self.a = take.a^ self.b = take.b^ def write_to[T: Writer](self, mut writer: T): + """Writes a formatted representation of the benchmark case to a writer. + + Parameters: + T: A type conforming to the `Writer` interface. + + Args: + writer: The writer instance. + """ writer.write( "BenchCase(name='", self.name, @@ -233,15 +292,24 @@ def expand_value(s: String) raises -> String: def parse_file(file_path: String) raises -> TOMLDocument: - """Parse a TOML file and return the TOMLDocument.""" + """Parses a TOML file and returns the parsed document. + + Args: + file_path: The path to the TOML file to parse. + + Returns: + A `TOMLDocument` containing the parsed TOML data. + + Raises: + ValueError: If the TOML file cannot be parsed. + """ try: return parse_toml_file(file_path) except e: - raise Error( - "tests.parse_file(): Failed to parse TOML file:", - file_path, - "\nTraceback:", - e, + raise ValueError( + message="Failed to parse TOML file: " + file_path, + function="parse_file()", + previous_error=e^, ) @@ -370,17 +438,35 @@ struct PrecisionLevel(Copyable, Movable): """ var precision: Int + """The number of significant digits for this precision level.""" var iterations: Int + """The number of timing iterations to run at this precision.""" def __init__(out self, precision: Int, iterations: Int): + """Creates a `PrecisionLevel` with the given precision and iteration count. + + Args: + precision: The number of significant digits. + iterations: The number of timing iterations to run. + """ self.precision = precision self.iterations = iterations def __init__(out self, *, copy: Self): + """Creates a copy of a `PrecisionLevel`. + + Args: + copy: The instance to copy from. + """ self.precision = copy.precision self.iterations = copy.iterations def __init__(out self, *, deinit take: Self): + """Moves a `PrecisionLevel` into a new instance. + + Args: + take: The instance to move from. + """ self.precision = take.precision self.iterations = take.iterations diff --git a/src/decimo/toml/parser.mojo b/src/decimo/toml/parser.mojo index 8cffdf79..5392190e 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 @@ -37,12 +38,19 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): """Represents a value in the TOML document.""" var type: TOMLValueType + """The value's TOML type.""" var string_value: String + """The string content when type is `STRING`.""" var int_value: Int + """The integer content when type is `INTEGER`.""" var float_value: Float64 + """The float content when type is `FLOAT`.""" var bool_value: Bool + """The boolean content when type is `BOOLEAN`.""" var array_values: List[TOMLValue] + """The array elements when type is `ARRAY`.""" var table_values: Dict[String, TOMLValue] + """The key-value pairs when type is `TABLE`.""" def __init__(out self): """Initialize an empty TOML value.""" @@ -55,7 +63,11 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): self.table_values = Dict[String, TOMLValue]() def __init__(out self, string_value: String): - """Initialize a string TOML value.""" + """Initialize a string TOML value. + + Args: + string_value: The string content. + """ self.type = TOMLValueType.STRING self.string_value = string_value self.int_value = 0 @@ -65,7 +77,11 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): self.table_values = Dict[String, TOMLValue]() def __init__(out self, int_value: Int): - """Initialize an integer TOML value.""" + """Initialize an integer TOML value. + + Args: + int_value: The integer content. + """ self.type = TOMLValueType.INTEGER self.string_value = "" self.int_value = int_value @@ -75,7 +91,11 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): self.table_values = Dict[String, TOMLValue]() def __init__(out self, float_value: Float64): - """Initialize a float TOML value.""" + """Initialize a float TOML value. + + Args: + float_value: The float content. + """ self.type = TOMLValueType.FLOAT self.string_value = "" self.int_value = 0 @@ -85,7 +105,11 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): self.table_values = Dict[String, TOMLValue]() def __init__(out self, bool_value: Bool): - """Initialize a boolean TOML value.""" + """Initialize a boolean TOML value. + + Args: + bool_value: The boolean content. + """ self.type = TOMLValueType.BOOLEAN self.string_value = "" self.int_value = 0 @@ -95,6 +119,11 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): self.table_values = Dict[String, TOMLValue]() def __init__(out self, *, copy: Self): + """Creates a copy of an existing `TOMLValue`. + + Args: + copy: The instance to copy from. + """ self.type = copy.type self.string_value = copy.string_value self.int_value = copy.int_value @@ -104,15 +133,27 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): self.table_values = copy.table_values.copy() def is_table(self) -> Bool: - """Check if this value is a table.""" + """Checks if this value is a table. + + Returns: + `True` if this value is a table, `False` otherwise. + """ return self.type == TOMLValueType.TABLE def is_array(self) -> Bool: - """Check if this value is an array.""" + """Checks if this value is an array. + + Returns: + `True` if this value is an array, `False` otherwise. + """ return self.type == TOMLValueType.ARRAY def as_string(self) -> String: - """Get the value as a string.""" + """Converts the value to a string representation. + + Returns: + The value as a string, or an empty string for unsupported types. + """ if self.type == TOMLValueType.STRING: return self.string_value elif self.type == TOMLValueType.INTEGER: @@ -125,14 +166,22 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): return "" def as_int(self) -> Int: - """Get the value as an integer.""" + """Returns the value as an integer. + + Returns: + The integer value, or `0` if the type is not `INTEGER`. + """ if self.type == TOMLValueType.INTEGER: return self.int_value else: return 0 def as_float(self) -> Float64: - """Get the value as a float.""" + """Returns the value as a float. + + Returns: + The float value, or `0.0` if the type is not numeric. + """ if self.type == TOMLValueType.FLOAT: return self.float_value elif self.type == TOMLValueType.INTEGER: @@ -141,20 +190,32 @@ struct TOMLValue(Copyable, ImplicitlyCopyable, Movable): return 0.0 def as_bool(self) -> Bool: - """Get the value as a boolean.""" + """Returns the value as a boolean. + + Returns: + The boolean value, or `False` if the type is not `BOOLEAN`. + """ if self.type == TOMLValueType.BOOLEAN: return self.bool_value else: return False def as_table(self) -> Dict[String, TOMLValue]: - """Get the value as a table dictionary.""" + """Returns the value as a table dictionary. + + Returns: + A copy of the table's key-value pairs, or an empty dictionary. + """ if self.type == TOMLValueType.TABLE: return self.table_values.copy() return Dict[String, TOMLValue]() def as_array(self) -> List[TOMLValue]: - """Get the value as an array.""" + """Returns the value as an array. + + Returns: + A copy of the array elements, or an empty list. + """ if self.type == TOMLValueType.ARRAY: return self.array_values.copy() return List[TOMLValue]() @@ -164,53 +225,122 @@ struct TOMLValueType(Copyable, ImplicitlyCopyable, Movable): """Types of values in TOML.""" comptime NULL = TOMLValueType.null() + """Value type for uninitialized or empty values.""" comptime STRING = TOMLValueType.string() + """Value type for string values.""" comptime INTEGER = TOMLValueType.integer() + """Value type for integer values.""" comptime FLOAT = TOMLValueType.float() + """Value type for floating-point values.""" comptime BOOLEAN = TOMLValueType.boolean() + """Value type for boolean values.""" comptime ARRAY = TOMLValueType.array() + """Value type for array values.""" comptime TABLE = TOMLValueType.table() + """Value type for table values.""" var value: Int + """The underlying integer identifier.""" @staticmethod def null() -> TOMLValueType: + """Creates a `TOMLValueType` representing null. + + Returns: + A `TOMLValueType` for null values. + """ return TOMLValueType(0) @staticmethod def string() -> TOMLValueType: + """Creates a `TOMLValueType` representing a string. + + Returns: + A `TOMLValueType` for string values. + """ return TOMLValueType(1) @staticmethod def integer() -> TOMLValueType: + """Creates a `TOMLValueType` representing an integer. + + Returns: + A `TOMLValueType` for integer values. + """ return TOMLValueType(2) @staticmethod def float() -> TOMLValueType: + """Creates a `TOMLValueType` representing a float. + + Returns: + A `TOMLValueType` for floating-point values. + """ return TOMLValueType(3) @staticmethod def boolean() -> TOMLValueType: + """Creates a `TOMLValueType` representing a boolean. + + Returns: + A `TOMLValueType` for boolean values. + """ return TOMLValueType(4) @staticmethod def array() -> TOMLValueType: + """Creates a `TOMLValueType` representing an array. + + Returns: + A `TOMLValueType` for array values. + """ return TOMLValueType(5) @staticmethod def table() -> TOMLValueType: + """Creates a `TOMLValueType` representing a table. + + Returns: + A `TOMLValueType` for table values. + """ return TOMLValueType(6) def __init__(out self, value: Int): + """Creates a new `TOMLValueType` from an integer identifier. + + Args: + value: The integer identifier for this value type. + """ self.value = value def __eq__(self, other: TOMLValueType) -> Bool: + """Checks equality between two value types. + + Args: + other: The value type to compare against. + + Returns: + `True` if the value types are equal, `False` otherwise. + """ return self.value == other.value def __ne__(self, other: TOMLValueType) -> Bool: + """Checks inequality between two value types. + + Args: + other: The value type to compare against. + + Returns: + `True` if the value types differ, `False` otherwise. + """ return self.value != other.value def to_string(self) -> String: + """Returns the human-readable name of this value type. + + Returns: + A string such as `"STRING"`, `"INTEGER"`, or `"UNKNOWN"`. + """ if self == Self.NULL: return "NULL" elif self == Self.STRING: @@ -233,18 +363,34 @@ struct TOMLDocument(Copyable, Movable): """Represents a parsed TOML document.""" var root: Dict[String, TOMLValue] + """The top-level key-value pairs.""" def __init__(out self): + """Initializes the instance.""" self.root = Dict[String, TOMLValue]() def get(self, key: String) raises -> TOMLValue: - """Get a value from the document.""" + """Retrieves a value from the document by key. + + Args: + key: The key to look up. + + Returns: + The value for `key`, or an empty `TOMLValue` if not found. + """ if key in self.root: return self.root[key] return TOMLValue() def get_table(self, table_name: String) raises -> Dict[String, TOMLValue]: - """Get a table from the document.""" + """Retrieves a table from the document by name. + + Args: + table_name: The name of the table to retrieve. + + Returns: + A copy of the table's key-value pairs, or an empty dictionary. + """ if ( table_name in self.root and self.root[table_name].type == TOMLValueType.TABLE @@ -253,7 +399,14 @@ struct TOMLDocument(Copyable, Movable): return Dict[String, TOMLValue]() def get_array(self, key: String) raises -> List[TOMLValue]: - """Get an array from the document.""" + """Retrieves an array from the document by key. + + Args: + key: The key of the array to retrieve. + + Returns: + A copy of the array elements, or an empty list. + """ if key in self.root and self.root[key].type == TOMLValueType.ARRAY: return self.root[key].array_values.copy() return List[TOMLValue]() @@ -261,7 +414,14 @@ struct TOMLDocument(Copyable, Movable): def get_array_of_tables( self, key: String ) raises -> List[Dict[String, TOMLValue]]: - """Get an array of tables from the document.""" + """Retrieves an array of tables from the document by key. + + Args: + key: The key of the array of tables to retrieve. + + Returns: + A list of table dictionaries, or an empty list. + """ var result = List[Dict[String, TOMLValue]]() if key in self.root: @@ -300,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 @@ -312,11 +475,16 @@ 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 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 ValueError( + message="Duplicate key: " + key, function="parse()" + ) root[key] = value^ return @@ -337,7 +505,10 @@ 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 ValueError( + message="Key exists but is not a table: " + first, + function="parse()", + ) var table = root[first].table_values.copy() var remaining = List[String]() @@ -354,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 @@ -374,7 +549,10 @@ 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 ValueError( + message="Key exists but is not a table: " + first, + function="parse()", + ) if len(path) > 1: var table = root[first].table_values.copy() @@ -392,9 +570,16 @@ 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 Error("Array of tables path cannot be empty") + raise ValueError( + message="Array of tables path cannot be empty", + function="parse()", + ) if len(path) == 1: var key = path[0] @@ -410,7 +595,10 @@ def _append_array_of_tables( _make_table(Dict[String, TOMLValue]()) ) else: - raise Error("Cannot redefine as array of tables: " + key) + raise ValueError( + message="Cannot redefine as array of tables: " + key, + function="parse()", + ) return # Multi-part path: navigate to the parent, then handle the last key @@ -437,21 +625,36 @@ 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 ValueError( + message="Key exists but is not a table or array: " + first, + function="parse()", + ) struct TOMLParser: """Parses TOML source text into a TOMLDocument.""" var tokens: List[Token] + """The list of tokens to parse.""" var pos: Int + """The current position in the token list.""" def __init__(out self, source: String): + """Creates a new `TOMLParser` from a TOML source string. + + Args: + source: The TOML source string to parse. + """ var tokenizer = Tokenizer(source) self.tokens = tokenizer.tokenize() self.pos = 0 def __init__(out self, tokens: List[Token]): + """Creates a new `TOMLParser` from a pre-tokenized token list. + + Args: + tokens: The pre-tokenized token list. + """ self.tokens = tokens.copy() self.pos = 0 @@ -494,11 +697,14 @@ 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]() if not self._is_key_token(): - raise Error("Expected key") + raise ValueError(message="Expected key", function="parse()") parts.append(self._tok().value) self._advance() @@ -507,7 +713,9 @@ 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 ValueError( + message="Expected key after dot", function="parse()" + ) parts.append(self._tok().value) self._advance() @@ -632,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: @@ -645,7 +857,10 @@ struct TOMLParser: # Expect equals if self._tok().type != TokenType.EQUAL: - raise Error("Expected '=' in inline table") + raise ValueError( + message="Expected '=' in inline table", + function="parse()", + ) self._advance() # Parse value @@ -654,8 +869,10 @@ struct TOMLParser: # Set value at potentially nested path if len(key_parts) == 1: if key_parts[0] in table: - raise Error( - "Duplicate key in inline table: " + key_parts[0] + raise ValueError( + message="Duplicate key in inline table: " + + key_parts[0], + function="TOMLParser._parse_inline_table()", ) table[key_parts[0]] = value^ else: @@ -673,44 +890,68 @@ struct TOMLParser: self._advance() break else: - raise Error("Expected ',' or '}' in inline table") + raise ValueError( + message="Expected ',' or '}' in inline table", + function="parse()", + ) return _make_table(table^) # ---- 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 ] if self._tok().type == TokenType.ARRAY_END: self._advance() else: - raise Error("Expected ']' after table header") + raise ValueError( + message="Expected ']' after table header", + function="parse()", + ) 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 ]] if self._tok().type == TokenType.ARRAY_END: self._advance() else: - raise Error("Expected ']]' after array of tables header") + raise 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 ValueError( + message="Expected ']]' after array of tables header", + function="parse()", + ) return path^ # ---- main parse loop ------------------------------------------------- def parse(mut self) raises -> TOMLDocument: - """Parse the tokens into a TOMLDocument.""" + """Parses the tokens into a `TOMLDocument`. + + Returns: + The parsed `TOMLDocument`. + """ var document = TOMLDocument() var current_path = List[String]() var is_array_of_tables = False @@ -777,13 +1018,27 @@ struct TOMLParser: def parse_string(input: String) raises -> TOMLDocument: - """Parse a TOML string into a document.""" + """Parses a TOML string into a document. + + Args: + input: The TOML-formatted string to parse. + + Returns: + The parsed `TOMLDocument`. + """ var parser = TOMLParser(input) return parser.parse() def parse_file(file_path: String) raises -> TOMLDocument: - """Parse a TOML file into a document.""" + """Parses a TOML file into a document. + + Args: + file_path: The path to the TOML file. + + Returns: + The parsed `TOMLDocument`. + """ with open(file_path, "r") as file: content = file.read() diff --git a/src/decimo/toml/tokenizer.mojo b/src/decimo/toml/tokenizer.mojo index fd288506..5520c1e9 100644 --- a/src/decimo/toml/tokenizer.mojo +++ b/src/decimo/toml/tokenizer.mojo @@ -19,22 +19,38 @@ A TOML tokenizer for Mojo, implementing the core TOML v1.0 specification. """ comptime WHITESPACE = " \t" +"""Characters treated as whitespace between tokens.""" comptime COMMENT_START = "#" +"""Character that begins a TOML comment.""" comptime QUOTE = '"' +"""Character for basic (double-quoted) strings.""" comptime LITERAL_QUOTE = "'" +"""Character for literal (single-quoted) strings.""" struct Token(Copyable, Movable): """Represents a token in the TOML document.""" var type: TokenType + """The token's category.""" var value: String + """The token's text content.""" var line: Int + """The line number where this token appears (1-based).""" var column: Int + """The column number where this token starts (1-based).""" def __init__( out self, type: TokenType, value: String, line: Int, column: Int ): + """Creates a new `Token`. + + Args: + type: The token's category. + value: The token's text content. + line: The line number where this token appears. + column: The column number where this token starts. + """ self.type = type self.value = value self.line = line @@ -45,16 +61,30 @@ struct SourcePosition: """Tracks position in the source text.""" var line: Int + """The current line number (1-based).""" var column: Int + """The current column number (1-based).""" var index: Int + """The byte offset in the source string.""" def __init__(out self, line: Int = 1, column: Int = 1, index: Int = 0): + """Creates a new `SourcePosition`. + + Args: + line: The initial line number. + column: The initial column number. + index: The initial byte offset. + """ self.line = line self.column = column self.index = index def advance(mut self, char: String): - """Update position after consuming a character.""" + """Advances the position past the given character. + + Args: + char: The character to advance past. + """ if char == "\n": self.line += 1 self.column = 1 @@ -70,114 +100,250 @@ struct TokenType(Copyable, ImplicitlyCopyable, Movable): # Aliases for TokenType static methods to mimic enum constants comptime KEY = TokenType.key() + """Token type for TOML keys.""" comptime STRING = TokenType.string() + """Token type for string values.""" comptime INTEGER = TokenType.integer() + """Token type for integer values.""" comptime FLOAT = TokenType.float() + """Token type for floating-point values.""" comptime BOOLEAN = TokenType.boolean() + """Token type for boolean values.""" comptime DATETIME = TokenType.datetime() + """Token type for datetime values.""" comptime ARRAY_START = TokenType.array_start() + """Token type for array opening bracket `[`.""" comptime ARRAY_END = TokenType.array_end() + """Token type for array closing bracket `]`.""" comptime TABLE_START = TokenType.table_start() + """Token type for table header opening bracket `[`.""" comptime TABLE_END = TokenType.table_end() + """Token type for table header closing bracket `]`.""" comptime ARRAY_OF_TABLES_START = TokenType.array_of_tables_start() + """Token type for array-of-tables opening `[[`.""" comptime EQUAL = TokenType.equal() + """Token type for the `=` separator.""" comptime COMMA = TokenType.comma() + """Token type for the `,` separator.""" comptime NEWLINE = TokenType.newline() + """Token type for newline characters.""" comptime DOT = TokenType.dot() + """Token type for the `.` separator in dotted keys.""" comptime EOF = TokenType.eof() + """Token type for end of file.""" comptime ERROR = TokenType.error() + """Token type for lexer errors.""" comptime INLINE_TABLE_START = TokenType.inline_table_start() + """Token type for inline table opening brace `{`.""" comptime INLINE_TABLE_END = TokenType.inline_table_end() + """Token type for inline table closing brace `}`.""" # Attributes var value: Int + """The underlying integer identifier.""" # Token type constants (lowercase method names) @staticmethod def key() -> TokenType: + """Creates a `TokenType` representing a TOML key. + + Returns: + A `TokenType` for keys. + """ return TokenType(0) @staticmethod def string() -> TokenType: + """Creates a `TokenType` representing a string value. + + Returns: + A `TokenType` for strings. + """ return TokenType(1) @staticmethod def integer() -> TokenType: + """Creates a `TokenType` representing an integer value. + + Returns: + A `TokenType` for integers. + """ return TokenType(2) @staticmethod def float() -> TokenType: + """Creates a `TokenType` representing a floating-point value. + + Returns: + A `TokenType` for floats. + """ return TokenType(3) @staticmethod def boolean() -> TokenType: + """Creates a `TokenType` representing a boolean value. + + Returns: + A `TokenType` for booleans. + """ return TokenType(4) @staticmethod def datetime() -> TokenType: + """Creates a `TokenType` representing a datetime value. + + Returns: + A `TokenType` for datetimes. + """ return TokenType(5) @staticmethod def array_start() -> TokenType: + """Creates a `TokenType` representing an array opening bracket `[`. + + Returns: + A `TokenType` for array starts. + """ return TokenType(6) @staticmethod def array_end() -> TokenType: + """Creates a `TokenType` representing an array closing bracket `]`. + + Returns: + A `TokenType` for array ends. + """ return TokenType(7) @staticmethod def table_start() -> TokenType: + """Creates a `TokenType` representing a table header opening `[`. + + Returns: + A `TokenType` for table starts. + """ return TokenType(8) @staticmethod def table_end() -> TokenType: + """Creates a `TokenType` representing a table header closing `]`. + + Returns: + A `TokenType` for table ends. + """ return TokenType(9) @staticmethod def array_of_tables_start() -> TokenType: + """Creates a `TokenType` representing an array-of-tables opening `[[`. + + Returns: + A `TokenType` for array-of-tables starts. + """ return TokenType(16) @staticmethod def equal() -> TokenType: + """Creates a `TokenType` representing the `=` separator. + + Returns: + A `TokenType` for equals signs. + """ return TokenType(10) @staticmethod def comma() -> TokenType: + """Creates a `TokenType` representing the `,` separator. + + Returns: + A `TokenType` for commas. + """ return TokenType(11) @staticmethod def newline() -> TokenType: + """Creates a `TokenType` representing a newline character. + + Returns: + A `TokenType` for newlines. + """ return TokenType(12) @staticmethod def dot() -> TokenType: + """Creates a `TokenType` representing the `.` separator in dotted keys. + + Returns: + A `TokenType` for dots. + """ return TokenType(13) @staticmethod def eof() -> TokenType: + """Creates a `TokenType` representing end of file. + + Returns: + A `TokenType` for end of file. + """ return TokenType(14) @staticmethod def error() -> TokenType: + """Creates a `TokenType` representing a lexer error. + + Returns: + A `TokenType` for errors. + """ return TokenType(15) @staticmethod def inline_table_start() -> TokenType: + """Creates a `TokenType` representing an inline table opening brace `{`. + + Returns: + A `TokenType` for inline table starts. + """ return TokenType(17) @staticmethod def inline_table_end() -> TokenType: + """Creates a `TokenType` representing an inline table closing brace `}`. + + Returns: + A `TokenType` for inline table ends. + """ return TokenType(18) # Constructor def __init__(out self, value: Int): + """Creates a new `TokenType` from an integer identifier. + + Args: + value: The integer identifier for this token type. + """ self.value = value # Comparison operators def __eq__(self, other: TokenType) -> Bool: + """Checks equality between two token types. + + Args: + other: The token type to compare against. + + Returns: + `True` if the token types are equal, `False` otherwise. + """ return self.value == other.value def __ne__(self, other: TokenType) -> Bool: + """Checks inequality between two token types. + + Args: + other: The token type to compare against. + + Returns: + `True` if the token types differ, `False` otherwise. + """ return self.value != other.value @@ -185,10 +351,18 @@ struct Tokenizer: """Tokenizes TOML source text.""" var source: String + """The TOML source string to tokenize.""" var position: SourcePosition + """The current position in the source.""" var current_char: String + """The character at the current position.""" def __init__(out self, source: String): + """Creates a new `Tokenizer` for the given source. + + Args: + source: The TOML source string to tokenize. + """ self.source = source self.position = SourcePosition() if len(source) > 0: @@ -523,7 +697,11 @@ struct Tokenizer: return Token(TokenType.KEY, result, start_line, start_column) def next_token(mut self) -> Token: - """Get the next token from the source.""" + """Returns the next token from the source. + + Returns: + The next `Token` from the source. + """ self._skip_whitespace() if not self.current_char: @@ -701,7 +879,11 @@ struct Tokenizer: return token^ def tokenize(mut self) -> List[Token]: - """Tokenize the entire source text.""" + """Tokenizes the entire source text. + + Returns: + A list of all tokens from the source. + """ var tokens = List[Token]() var token = self.next_token() diff --git a/tests/bigfloat/test_bigfloat.mojo b/tests/bigfloat/test_bigfloat.mojo new file mode 100644 index 00000000..e59270d0 --- /dev/null +++ b/tests/bigfloat/test_bigfloat.mojo @@ -0,0 +1,228 @@ +# ===----------------------------------------------------------------------=== # +# Copyright 2025 Yuhao Zhu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ===----------------------------------------------------------------------=== # + +"""Smoke tests for BigFloat: verify MPFR pipeline works end-to-end.""" + +from decimo.bigfloat.bigfloat import BigFloat, PRECISION +from decimo.bigfloat.mpfr_wrapper import mpfrw_available + + +def test_mpfr_available() raises: + print("test_mpfr_available ... ", end="") + if not mpfrw_available(): + print("SKIPPED (MPFR not installed)") + return + print("OK") + + +def test_construct_from_string() raises: + print("test_construct_from_string ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("3.14159") + print("OK value =", x) + + +def test_construct_from_int() raises: + print("test_construct_from_int ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat(42) + print("OK value =", x) + + +def test_sqrt() raises: + print("test_sqrt ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("2.0", precision=50) + var s = x.sqrt() + var result = s.to_string(50) + # sqrt(2) ≈ 1.4142135623730950488... + if not result.startswith("1.4142"): + raise Error("FAIL test_sqrt got: " + result) + print("OK sqrt(2) =", result) + + +def test_exp() raises: + print("test_exp ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("1.0", precision=50) + var e = x.exp() + var result = e.to_string(50) + # exp(1) ≈ 2.71828182845904523536... + if not result.startswith("2.7182"): + raise Error("FAIL test_exp got: " + result) + print("OK exp(1) =", result) + + +def test_ln() raises: + print("test_ln ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("2.0", precision=30) + var result = x.ln() + var s = result.to_string(15) + # ln(2) ≈ 0.693147180559945... + if not s.startswith("0.69314"): + raise Error("FAIL test_ln got: " + s) + print("OK ln(2) =", s) + + +def test_trig() raises: + print("test_trig ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var pi = BigFloat.pi(50) + var s = pi.sin() + var c = pi.cos() + var sin_s = s.to_string(20) + var cos_s = c.to_string(20) + # sin(π) ≈ 0, cos(π) ≈ -1 + print("OK sin(π) =", sin_s, " cos(π) =", cos_s) + + +def test_arithmetic() raises: + print("test_arithmetic ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var a = BigFloat("10.0") + var b = BigFloat("3.0") + var sum_ = a + b + var diff = a - b + var prod = a * b + var quot = a / b + print("OK 10+3=", sum_, " 10-3=", diff, " 10*3=", prod, " 10/3=", quot) + + +def test_comparison() raises: + print("test_comparison ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var a = BigFloat("1.0") + var b = BigFloat("2.0") + var c = BigFloat("1.0") + var ok = True + if not (a < b): + ok = False + if not (b > a): + ok = False + if not (a == c): + ok = False + if not (a != b): + ok = False + if not (a <= c): + ok = False + if not (b >= a): + ok = False + if ok: + print("OK") + else: + raise Error("FAIL test_comparison") + + +def test_pi() raises: + print("test_pi ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var pi = BigFloat.pi(100) + var s = pi.to_string(50) + # π ≈ 3.14159265358979323846... + if not s.startswith("3.14159265358979"): + raise Error("FAIL test_pi got: " + s) + print("OK π =", s) + + +def test_to_bigdecimal() raises: + print("test_to_bigdecimal ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("2.0", precision=50) + var s = x.sqrt() + var bd = s.to_bigdecimal(30) + var bd_s = String(bd) + # Should start with 1.4142... + if not bd_s.startswith("1.4142"): + raise Error("FAIL test_to_bigdecimal got: " + bd_s) + print("OK BigDecimal(sqrt(2)) =", bd_s) + + +def test_power_and_root() raises: + print("test_power_and_root ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("8.0", precision=30) + var third = BigFloat("0.333333333333333333", precision=30) + var cube_root = x.root(UInt32(3)) + var power_result = x.power(third) + print("OK cbrt(8) =", cube_root, " 8^(1/3) =", power_result) + + +def test_neg_and_abs() raises: + print("test_neg_and_abs ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("-5.0") + var neg_x = -x + var abs_x = x.__abs__() + print("OK -(-5) =", neg_x, " |(-5)| =", abs_x) + + +def test_high_precision_sqrt() raises: + print("test_high_precision_sqrt ... ", end="") + if not mpfrw_available(): + print("SKIPPED") + return + var x = BigFloat("2.0", precision=1000) + var s = x.sqrt() + var result = s.to_string(100) + # Verify many digits of sqrt(2) + if not result.startswith( + "1.41421356237309504880168872420969807856967187537694" + ): + raise Error("FAIL test_high_precision_sqrt got: " + result) + print("OK sqrt(2) to 100 digits verified") + + +def main() raises: + test_mpfr_available() + test_construct_from_string() + test_construct_from_int() + test_sqrt() + test_exp() + test_ln() + test_trig() + test_arithmetic() + test_comparison() + test_pi() + test_to_bigdecimal() + test_power_and_root() + test_neg_and_abs() + test_high_precision_sqrt() + print("\nAll BigFloat smoke tests completed.") diff --git a/tests/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/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") 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_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/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 # ===----------------------------------------------------------------------=== # diff --git a/tests/test_bigdecimal.sh b/tests/test_bigdecimal.sh index d62e5847..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 - pixi run mojo run -I src -D ASSERT=all "$f" + echo "=== $f ===" + pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done diff --git a/tests/test_bigfloat.sh b/tests/test_bigfloat.sh new file mode 100644 index 00000000..d623a0f2 --- /dev/null +++ b/tests/test_bigfloat.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# ===----------------------------------------------------------------------=== # +# BigFloat test runner. +# +# BigFloat tests require the C wrapper (libdecimo_gmp_wrapper) and MPFR. +# This script: +# 1. Builds the C wrapper (if not already built) +# 2. Compiles each test file as a binary (mojo run can't link .dylib/.so) +# 3. Runs each binary with the library path set +# +# Usage: +# bash tests/test_bigfloat.sh +# +# Prerequisites: +# MPFR installed (brew install mpfr / apt install libmpfr-dev) +# ===----------------------------------------------------------------------=== # + +set -e + +# Build C wrapper if needed +WRAPPER_DIR="src/decimo/gmp" +if [[ "$(uname)" == "Darwin" ]]; then + WRAPPER_LIB="$WRAPPER_DIR/libdecimo_gmp_wrapper.dylib" +else + WRAPPER_LIB="$WRAPPER_DIR/libdecimo_gmp_wrapper.so" +fi + +if [ ! -f "$WRAPPER_LIB" ]; then + echo "Building C wrapper..." + bash "$WRAPPER_DIR/build_gmp_wrapper.sh" +fi + +# Compile and run each test file +cleanup() { rm -f "$TMPBIN"; } +trap cleanup EXIT + +for f in tests/bigfloat/*.mojo; do + echo "=== $f ===" + TMPBIN=$(mktemp /tmp/decimo_test_bigfloat_XXXXXX) + pixi run mojo build -I src --debug-level=full \ + -Xlinker -L./"$WRAPPER_DIR" -Xlinker -ldecimo_gmp_wrapper \ + -o "$TMPBIN" "$f" + DYLD_LIBRARY_PATH="./$WRAPPER_DIR" LD_LIBRARY_PATH="./$WRAPPER_DIR" "$TMPBIN" + rm -f "$TMPBIN" +done diff --git a/tests/test_bigint.sh b/tests/test_bigint.sh index ac92ac07..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 - pixi run mojo run -I src -D ASSERT=all "$f" + 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 57856d28..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 - pixi run mojo run -I src -D ASSERT=all "$f" + 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 bee5f866..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 - pixi run mojo run -I src -D ASSERT=all "$f" + echo "=== $f ===" + pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done diff --git a/tests/test_cli.sh b/tests/test_cli.sh index 64a418e8..72594a66 100644 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -1,6 +1,215 @@ #!/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 "$f" + pixi run mojo run -I src -I src/cli -D ASSERT=all --debug-level=full "$f" done + +# ── Integration tests (exercise the compiled binary) ─────────────────────── +BINARY="./decimo" + +if [[ ! -x "$BINARY" ]]; then + echo "SKIP: CLI integration tests ($BINARY not found)" + exit 0 +fi + +PASS=0 +FAIL=0 + +assert_output() { + local description="$1" + shift + local expected="$1" + shift + local actual + actual=$("$@" 2>&1) + if [[ "$actual" == "$expected" ]]; then + PASS=$((PASS + 1)) + else + echo "FAIL: $description" + echo " expected: $expected" + echo " actual: $actual" + FAIL=$((FAIL + 1)) + fi +} + +# 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 + +# Scientific notation (--scientific / -S) +assert_output "scientific notation" "1.2345678E+4" "$BINARY" "12345.678" --scientific + +# 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 "_" + +# Rounding mode (--rounding-mode / -R) +assert_output "rounding mode ceiling" "0.33334" "$BINARY" "1/3" -P 5 -R ceiling + +# 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 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" + +# ── 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" + FAIL=$((FAIL + 1)) +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) ──────────────────────────────────────────── +# 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 "$DATA/basic.dm" + +assert_output "file mode basic.dm -P 10" \ + "$(printf '3.141592654\n2.718281828\n1.414213562\n1198.647059')" \ + "$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 + 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" + +if [[ "$FAIL" -gt 0 ]]; then + exit 1 +fi diff --git a/tests/test_decimal128.sh b/tests/test_decimal128.sh index d2a4d05f..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 - pixi run mojo run -I src -D ASSERT=all "$f" + 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 aef15bde..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 - pixi run mojo run -I src -D ASSERT=all "$f" + echo "=== $f ===" + pixi run mojo run -I src -D ASSERT=all --debug-level=full "$f" done