From 76b55ef1ae654ceb6266f162a1fcaf5fffccfb72 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 11 Apr 2026 23:47:30 +0200 Subject: [PATCH 1/4] Add benchmark cli + shellcompletion in manual --- benches/cli/bench_cli.sh | 328 +++++++++++++++++++++++++++++++++++ benches/run_bench.sh | 13 +- docs/changelog.md | 2 + docs/plans/cli_calculator.md | 4 +- docs/user_manual_cli.md | 56 ++++++ 5 files changed, 398 insertions(+), 5 deletions(-) create mode 100644 benches/cli/bench_cli.sh diff --git a/benches/cli/bench_cli.sh b/benches/cli/bench_cli.sh new file mode 100644 index 00000000..0a7e8c16 --- /dev/null +++ b/benches/cli/bench_cli.sh @@ -0,0 +1,328 @@ +#!/bin/bash +# ===----------------------------------------------------------------------=== # +# CLI Calculator Benchmarks — Correctness & Performance +# +# Compares decimo against bc and python3 on every expression: +# 1. Correctness — first 15 significant digits must agree +# 2. Performance — average wall-clock latency over $ITERATIONS runs +# +# Usage: +# bash benches/cli/bench_cli.sh +# ITERATIONS=20 bash benches/cli/bench_cli.sh +# +# Requirements: +# - ./decimo binary (pixi run mojo build -I src -I src/cli src/cli/main.mojo -o decimo) +# - perl with Time::HiRes (standard on macOS) +# - bc (standard on macOS / Linux) +# - python3 with mpmath for function comparisons (pip install mpmath) +# ===----------------------------------------------------------------------=== # + +set -euo pipefail +export LC_ALL=C # consistent decimal formatting + +BINARY="${BINARY:-./decimo}" +export ITERATIONS="${ITERATIONS:-10}" +PREVIEW=35 # max chars of result preview + +# ── Prerequisites ────────────────────────────────────────────────────────── + +if ! [[ -x "$BINARY" ]]; then + echo "Error: $BINARY not found or not executable." + echo "Build first: pixi run mojo build -I src -I src/cli src/cli/main.mojo -o decimo" + exit 1 +fi + +HAS_BC=false; command -v bc &>/dev/null && HAS_BC=true +HAS_PY=false; command -v python3 &>/dev/null && HAS_PY=true + +# ── Counters ─────────────────────────────────────────────────────────────── + +COMPARISONS=0 +MATCHES=0 +MISMATCHES=0 +ERRORS=0 + +# ── Helpers ──────────────────────────────────────────────────────────────── + +# Time a command over $ITERATIONS runs, return average ms. +elapsed_ms() { + perl -MTime::HiRes=time -e ' + my $n = $ENV{ITERATIONS}; + my @cmd = @ARGV; + open(my $oldout, ">&", \*STDOUT); + open(my $olderr, ">&", \*STDERR); + # Warm-up (untimed) + open(STDOUT, ">/dev/null"); open(STDERR, ">/dev/null"); + system(@cmd); + open(STDOUT, ">&", $oldout); open(STDERR, ">&", $olderr); + # Timed + my $t0 = time(); + for (1 .. $n) { + open(STDOUT, ">/dev/null"); open(STDERR, ">/dev/null"); + system(@cmd); + open(STDOUT, ">&", $oldout); open(STDERR, ">&", $olderr); + } + printf "%.2f\n", (time() - $t0) * 1000.0 / $n; + ' -- "$@" +} + +# Extract the first 15 significant digits from a numeric string. +sig_digits() { + local s="${1#-}" # strip sign + s=$(echo "$s" | sed 's/[eE][+-]*[0-9]*//') # strip exponent + if [[ "$s" == *.* ]]; then # strip trailing .0's + s=$(echo "$s" | sed 's/0*$//; s/\.$//') + fi + echo "$s" | tr -d '.' | sed 's/^0*//' | cut -c1-15 # first 15 sig digits +} + +# Compare two results by leading significant digits. +check_match() { + local a="$1" b="$2" + local sign_a="" sign_b="" + if [[ "$a" == -* ]]; then sign_a="-"; fi + if [[ "$b" == -* ]]; then sign_b="-"; fi + if [[ "$sign_a" != "$sign_b" ]]; then echo "MISMATCH"; return 0; fi + local sa sb + sa=$(sig_digits "$a") + sb=$(sig_digits "$b") + if [[ "$sa" == "$sb" ]]; then echo "MATCH"; else echo "MISMATCH"; fi + return 0 +} + +# Truncate a result string for display. +preview() { + if (( ${#1} > PREVIEW )); then echo "${1:0:$PREVIEW}..."; else echo "$1"; fi +} + +# Record a comparison result. +record() { + local tag="$1" + if [[ "$tag" == "ERROR" ]]; then + ERRORS=$((ERRORS + 1)) + else + COMPARISONS=$((COMPARISONS + 1)) + if [[ "$tag" == "MATCH" ]]; then + MATCHES=$((MATCHES + 1)) + else + MISMATCHES=$((MISMATCHES + 1)) + fi + fi + return 0 +} + +# ── Main comparison driver ───────────────────────────────────────────────── +# +# bench_compare LABEL PREC DECIMO_EXPR [BC_EXPR] [PY_CODE] +# +# BC_EXPR: expression piped to "bc -l". "scale=PREC; " is prepended. +# Pass "" to skip bc. +# PY_CODE: full python3 -c code. "__P__" is replaced with PREC. +# Pass "" to skip python3. + +bench_compare() { + local label="$1" prec="$2" d_expr="$3" + local bc_expr="${4:-}" py_code="${5:-}" + + printf " %s (P=%s)\n" "$label" "$prec" + + # ── decimo ── + local d_result d_ms + d_result=$("$BINARY" "$d_expr" -P "$prec" 2>/dev/null || echo "ERROR") + d_ms=$(elapsed_ms "$BINARY" "$d_expr" -P "$prec") + printf " %-10s %-38s %8s ms\n" "decimo:" "$(preview "$d_result")" "$d_ms" + + # ── bc ── + if [[ -n "$bc_expr" ]] && $HAS_BC; then + local full_bc="scale=$prec; $bc_expr" + local b_result b_ms tag + # tr -d '\\\n' removes bc's backslash line-continuations + b_result=$(echo "$full_bc" | bc -l 2>/dev/null | tr -d '\\\n' || echo "ERROR") + b_ms=$(elapsed_ms bash -c "echo '$full_bc' | bc -l") + if [[ "$b_result" == "ERROR" ]]; then + tag="ERROR" + else + tag=$(check_match "$d_result" "$b_result") + fi + printf " %-10s %-38s %8s ms %s\n" "bc:" "$(preview "$b_result")" "$b_ms" "$tag" + record "$tag" + fi + + # ── python3 ── + if [[ -n "$py_code" ]] && $HAS_PY; then + local full_py="${py_code//__P__/$prec}" + local p_result p_ms tag + p_result=$(python3 -c "$full_py" 2>/dev/null || echo "ERROR") + p_ms=$(elapsed_ms python3 -c "$full_py") + if [[ "$p_result" == "ERROR" ]]; then + tag="ERROR" + else + tag=$(check_match "$d_result" "$p_result") + fi + printf " %-10s %-38s %8s ms %s\n" "python3:" "$(preview "$p_result")" "$p_ms" "$tag" + record "$tag" + fi + + echo "" +} + +# ── Python expression templates (__P__ → precision) ──────────────────────── + +PY_DEC="from decimal import Decimal as D,getcontext as gc;gc().prec=__P__" +PY_MP="from mpmath import mp;mp.dps=__P__" + +# ── Header ───────────────────────────────────────────────────────────────── + +echo "============================================================" +echo " Decimo CLI Benchmark — Correctness & Performance" +echo "============================================================" +echo "Binary: $BINARY" +echo "Iterations: $ITERATIONS per expression" +echo "Tools: decimo$(${HAS_BC} && echo ', bc')$(${HAS_PY} && echo ', python3')" +echo "Date: $(date '+%Y-%m-%d %H:%M')" +echo "" + +# ── 1. Arithmetic ────────────────────────────────────────────────────────── + +echo "--- 1. Arithmetic -------------------------------------------" +echo "" + +bench_compare "1 + 1" 50 \ + "1+1" \ + "1+1" \ + "print(1+1)" + +bench_compare "100*12 - 23/17" 50 \ + "100*12-23/17" \ + "100*12-23/17" \ + "${PY_DEC};print(D('100')*12-D('23')/D('17'))" + +bench_compare "2^256" 50 \ + "2^256" \ + "2^256" \ + "print(2**256)" + +bench_compare "1/7" 50 \ + "1/7" \ + "1/7" \ + "${PY_DEC};print(D(1)/D(7))" + +bench_compare "(1+2) * (3+4)" 50 \ + "(1+2)*(3+4)" \ + "(1+2)*(3+4)" \ + "print((1+2)*(3+4))" + +# ── 2. Functions ─────────────────────────────────────────────────────────── + +echo "--- 2. Functions (P=50) -------------------------------------" +echo "" + +bench_compare "sqrt(2)" 50 \ + "sqrt(2)" \ + "sqrt(2)" \ + "${PY_DEC};print(D(2).sqrt())" + +bench_compare "ln(2)" 50 \ + "ln(2)" \ + "l(2)" \ + "${PY_MP};print(mp.log(2))" + +bench_compare "exp(1)" 50 \ + "exp(1)" \ + "e(1)" \ + "${PY_MP};print(mp.exp(1))" + +bench_compare "sin(1)" 50 \ + "sin(1)" \ + "s(1)" \ + "${PY_MP};print(mp.sin(1))" + +bench_compare "cos(0)" 50 \ + "cos(0)" \ + "c(0)" \ + "${PY_MP};print(mp.cos(0))" + +bench_compare "root(27, 3)" 50 \ + "root(27, 3)" \ + "" \ + "${PY_MP};print(mp.cbrt(27))" + +bench_compare "log(256, 2)" 50 \ + "log(256, 2)" \ + "" \ + "${PY_MP};print(mp.log(256,2))" + +# ── 3. Precision scaling — sqrt(2) ──────────────────────────────────────── + +echo "--- 3. Precision scaling — sqrt(2) --------------------------" +echo "" + +for p in 50 100 200 500 1000; do + bench_compare "sqrt(2)" "$p" \ + "sqrt(2)" \ + "sqrt(2)" \ + "${PY_DEC};print(D(2).sqrt())" +done + +# ── 4. Precision scaling — pi ───────────────────────────────────────────── + +echo "--- 4. Precision scaling — pi -------------------------------" +echo "" + +for p in 50 100 200 500 1000; do + bench_compare "pi" "$p" \ + "pi" \ + "4*a(1)" \ + "${PY_MP};print(mp.pi)" +done + +# ── 5. Complex expressions ──────────────────────────────────────────────── + +echo "--- 5. Complex expressions ----------------------------------" +echo "" + +bench_compare "ln(exp(1))" 50 \ + "ln(exp(1))" \ + "" \ + "${PY_MP};print(mp.log(mp.exp(1)))" + +bench_compare "sin(pi/4) + cos(pi/4)" 50 \ + "sin(pi/4)+cos(pi/4)" \ + "s(4*a(1)/4)+c(4*a(1)/4)" \ + "${PY_MP};print(mp.sin(mp.pi/4)+mp.cos(mp.pi/4))" + +bench_compare "2^256 + 3^100" 50 \ + "2^256+3^100" \ + "2^256+3^100" \ + "print(2**256+3**100)" + +# ── 6. Pipe mode (decimo only) ──────────────────────────────────────────── + +echo "--- 6. Pipe mode (decimo only) ------------------------------" +echo " No bc/python3 equivalent for multi-line pipe processing." +echo "" + +for entry in \ + "3 simple exprs|printf '1+2\n3*4\n5/6\n' | $BINARY" \ + "5 mixed exprs|printf '1+2\nsqrt(2)\npi\nln(10)\n2^64\n' | $BINARY"; do + desc="${entry%%|*}" + cmd="${entry#*|}" + ms=$(elapsed_ms bash -c "$cmd") + printf " %-42s %8s ms\n" "pipe: $desc" "$ms" +done +echo "" + +# ── Summary ──────────────────────────────────────────────────────────────── + +echo "============================================================" +printf " Summary: %d comparisons — %d MATCH, %d MISMATCH" \ + "$COMPARISONS" "$MATCHES" "$MISMATCHES" +if (( ERRORS > 0 )); then + printf ", %d ERROR (tool missing or failed)" "$ERRORS" +fi +echo "" +echo "============================================================" + +if (( MISMATCHES > 0 )); then + exit 1 +fi diff --git a/benches/run_bench.sh b/benches/run_bench.sh index 92420189..06bd2b01 100755 --- a/benches/run_bench.sh +++ b/benches/run_bench.sh @@ -22,10 +22,11 @@ if [[ -z "$TYPE" ]]; then echo "Usage: pixi run bench [operation]" echo "" echo "Types:" - echo " bigint (int) BigInt benchmarks (BigInt10 vs BigInt vs Python int)" - echo " biguint (uint) BigUInt benchmarks (BigUInt vs Python int)" + echo " bigint (int) BigInt benchmarks (BigInt10 vs BigInt vs Python int)" + echo " biguint (uint) BigUInt benchmarks (BigUInt vs Python int)" echo " decimal128 (dec128) Decimal128 benchmarks (Decimal128 vs Python decimal)" - echo " bigdecimal (dec) BigDecimal benchmarks (BigDecimal vs Python decimal)" + echo " bigdecimal (dec) BigDecimal benchmarks (BigDecimal vs Python decimal)" + echo " cli CLI calculator end-to-end latency benchmarks" echo "" echo "Omit operation to get interactive menu for that type." echo "" @@ -33,6 +34,7 @@ if [[ -z "$TYPE" ]]; then echo " pixi run bench bigint add" echo " pixi run bench dec sqrt" echo " pixi run bench biguint" + echo " pixi run bench cli" exit 0 fi @@ -44,6 +46,11 @@ case "$TYPE" in dec) TYPE="bigdecimal" ;; esac +# --- CLI benchmarks (special case — shell script, not Mojo) --- +if [[ "$TYPE" == "cli" ]]; then + exec bash benches/cli/bench_cli.sh +fi + DIR="benches/$TYPE" if [[ ! -d "$DIR" ]]; then diff --git a/docs/changelog.md b/docs/changelog.md index 0fba57c4..9f393a9b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,8 @@ This is a list of changes for the Decimo package (formerly DeciMojo). 1. Add **pipe/stdin mode**: read expressions from standard input, one per line, when no positional argument is given and stdin is piped (e.g. `echo "1+2" | decimo`, `printf "pi\nsqrt(2)" | decimo -P 100`). Blank lines and comment lines (starting with `#`) are automatically skipped. 1. Add **file mode**: use `--file` / `-F` flag to evaluate expressions from a file, one per line (e.g. `decimo -F expressions.dm -P 50`). Comments (`#`), inline comments, and blank lines are skipped. All CLI flags (precision, formatting, rounding) apply to every expression. +1. Add **shell completion** documentation for Bash, Zsh, and Fish (`decimo --completions bash|zsh|fish`). +1. Add **CLI performance benchmarks** (`benches/cli/bench_cli.sh`) comparing correctness and timing against `bc` and `python3` across 47 comparisons — all results match to 15 significant digits; `decimo` is 3–4× faster than `python3 -c`. ### 🦋 Changed in v0.10.0 diff --git a/docs/plans/cli_calculator.md b/docs/plans/cli_calculator.md index cb19c516..afa71e44 100644 --- a/docs/plans/cli_calculator.md +++ b/docs/plans/cli_calculator.md @@ -324,8 +324,8 @@ Format the final `BigDecimal` result based on CLI flags: | 3.9 | Argument groups in help output | ✓ | `Computation` and `Formatting` groups in `--help` | | 3.10 | Custom usage line | ✓ | `Usage: decimo [OPTIONS] ` | | 3.11 | `Parsable.run()` override | ✗ | Move eval logic into `DecimoArgs.run()` for cleaner separation | -| 3.12 | Performance validation | ✗ | No CLI-level benchmarks yet | -| 3.13 | Documentation (user manual for CLI) | ✗ | `docs/user_manual_cli.md`; include shell completion setup | +| 3.12 | Performance validation | ✓ | `benches/cli/bench_cli.sh`; 47 correctness checks + timing vs `bc` and `python3` | +| 3.13 | Documentation (user manual for CLI) | ✓ | `docs/user_manual_cli.md`; includes shell completions setup and performance data | | 3.14 | Build and distribute as single binary | ✗ | | | 3.15 | Allow negative expressions | ✓ | `allow_hyphen=True` on `Positional`; `decimo "-3*pi*(sin(1))"` works | | 3.16 | Make short names upper cases to avoid expression collisions | ✓ | `-P`, `-S`, `-E`, `-D`, `-R`; `--pad` has no short name; `-e`, `-pi`, `-sin(1)` all work | diff --git a/docs/user_manual_cli.md b/docs/user_manual_cli.md index ebbef539..3d02a966 100644 --- a/docs/user_manual_cli.md +++ b/docs/user_manual_cli.md @@ -26,6 +26,8 @@ - [Quoting Expressions](#quoting-expressions) - [Negative Expressions](#negative-expressions) - [Using noglob](#using-noglob) + - [Shell Completions](#shell-completions) +- [Performance](#performance) - [Examples](#examples) - [Basic Arithmetic](#basic-arithmetic) - [High-Precision Calculations](#high-precision-calculations) @@ -400,6 +402,60 @@ alias decimo='noglob decimo' decimo 2*(3+4) ``` +### Shell Completions + +`decimo` can generate completion scripts for **Bash**, **Zsh**, and **Fish**. Tab-completion will suggest option names, rounding mode values, and file paths. + +**Zsh** — add to `~/.zshrc`: + +```zsh +eval "$(decimo --completions zsh)" +``` + +**Bash** — add to `~/.bashrc`: + +```bash +eval "$(decimo --completions bash)" +``` + +**Fish** — run once: + +```sh +decimo --completions fish | source +# Or persist: +decimo --completions fish > ~/.config/fish/completions/decimo.fish +``` + +After reloading your shell, pressing Tab after `decimo -` will show all available options, and pressing Tab after `--rounding-mode` will list the seven available modes. + +## Performance + +`decimo` compiles to a single native binary. For most expressions, end-to-end latency is dominated by process startup rather than computation. The benchmark suite verifies both **correctness** (results agree with `bc` and `python3` to 15 significant digits) and **performance** (wall-clock timing). + +Typical latencies (measured on Apple M1 Max, macOS): + +| Expression | Precision | `decimo` | `bc -l` | `python3` | Match | +| ------------------- | :-------: | -------: | ------: | --------: | :---: | +| `1 + 1` | 50 | ~6 ms | ~4 ms | ~18 ms | ✓ | +| `100*12 - 23/17` | 50 | ~6 ms | ~4 ms | ~21 ms | ✓ | +| `sqrt(2)` | 50 | ~5 ms | ~4 ms | ~21 ms | ✓ | +| `ln(2)` | 50 | ~5 ms | ~4 ms | ~41 ms | ✓ | +| `sin(1)` | 50 | ~6 ms | ~4 ms | ~41 ms | ✓ | +| `pi` | 50 | ~6 ms | ~4 ms | ~41 ms | ✓ | +| `sqrt(2)` | 1000 | ~6 ms | ~5 ms | ~22 ms | ✓ | +| `pi` | 1000 | ~49 ms | ~13 ms | ~40 ms | ✓ | +| pipe: 5 mixed exprs | 50 | ~8 ms | N/A | N/A | | + +Tokenizer and parser overhead is negligible — trivial (`1+1`) and moderate (`sqrt(2)`) expressions complete in ~5 ms. Computation time only becomes visible for expensive operations at very high precision (e.g., computing π to 1000 digits). + +`decimo` is **3–4× faster than `python3`** and comparable to `bc` (a lightweight BSD utility). + +To run the full benchmark (correctness + performance, all 3 tools): + +```bash +bash benches/cli/bench_cli.sh +``` + ## Examples ### Basic Arithmetic From 52626d4f034a716a86ad783231fce45c9d8b9ff4 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sun, 12 Apr 2026 00:03:20 +0200 Subject: [PATCH 2/4] Add test files for cli --- tests/cli/test_data/basic.dm | 9 +++++ tests/cli/test_data/comments.txt | 18 +++++++++ tests/cli/test_data/edge_cases.dm | 24 ++++++++++++ tests/cli/test_data/precision.dm | 17 +++++++++ tests/cli/test_data/torture | 41 +++++++++++++++++++++ tests/test_cli.sh | 61 ++++++++++++++++++++++--------- 6 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 tests/cli/test_data/basic.dm create mode 100644 tests/cli/test_data/comments.txt create mode 100644 tests/cli/test_data/edge_cases.dm create mode 100644 tests/cli/test_data/precision.dm create mode 100644 tests/cli/test_data/torture diff --git a/tests/cli/test_data/basic.dm b/tests/cli/test_data/basic.dm new file mode 100644 index 00000000..f15a0b41 --- /dev/null +++ b/tests/cli/test_data/basic.dm @@ -0,0 +1,9 @@ +# Basic expressions - the standard test file for decimo. +# Tests constants, functions, and arithmetic. + +pi +e +sqrt(2) + +# Arithmetic +100 * 12 - 23/17 diff --git a/tests/cli/test_data/comments.txt b/tests/cli/test_data/comments.txt new file mode 100644 index 00000000..9025eb4b --- /dev/null +++ b/tests/cli/test_data/comments.txt @@ -0,0 +1,18 @@ +# This file exercises comment handling and blank-line skipping. +# +# It has: leading comments, inline comments, multiple blank lines, +# lines with only whitespace, and indented expressions. +# + + +1 + 1 # inline comment + + + 2 + 2 # leading whitespace is fine + +# comment between expressions + +3 + 3 + + +# trailing comment diff --git a/tests/cli/test_data/edge_cases.dm b/tests/cli/test_data/edge_cases.dm new file mode 100644 index 00000000..db9e741d --- /dev/null +++ b/tests/cli/test_data/edge_cases.dm @@ -0,0 +1,24 @@ +# Edge cases - negatives, zeros, single-value expressions, etc. + +0 +-0 +42 +-42 +0.001 +-0.001 + +# Unary minus with operations +-3 * 2 +-1 + 1 +-100 + 50 + +# Parenthesised negation +(-5 + 2) * (-3 + 1) + +# Nested parens +((((1 + 2) * 3) - 4) / 5) + +# Power edge cases +1 ^ 100 +2 ^ 0 +10 ^ 10 diff --git a/tests/cli/test_data/precision.dm b/tests/cli/test_data/precision.dm new file mode 100644 index 00000000..49109dc7 --- /dev/null +++ b/tests/cli/test_data/precision.dm @@ -0,0 +1,17 @@ +# High-precision stress test - expressions that need many digits to +# distinguish correct answers from off-by-one rounding errors. + +# 1/7 has a 6-digit repeating cycle +1/7 + +# pi subtracted from a close rational approximation +pi - 355/113 + +# sqrt(2) * sqrt(2) should be exactly 2 +sqrt(2) * sqrt(2) + +# e^(pi*sqrt(163)) is close to an integer (Ramanujan's constant) +exp(pi * sqrt(163)) + +# Cancellation stress: nearly equal values +exp(1) - (1 + 1 + 1/2 + 1/6 + 1/24 + 1/120 + 1/720) diff --git a/tests/cli/test_data/torture b/tests/cli/test_data/torture new file mode 100644 index 00000000..2f45f6be --- /dev/null +++ b/tests/cli/test_data/torture @@ -0,0 +1,41 @@ +# Torture test - complex expressions to stress the tokenizer, parser, +# and the Decimo BigDecimal engine. + +# Deeply nested functions +sqrt(abs(sin(1) * cos(1) + ln(2))) + +# Chained arithmetic with many terms +1/7 + 2/11 + 3/13 + 4/17 + 5/19 + +# Large integer powers +2 ^ 256 +3 ^ 100 + +# Mixed operations with constants +pi * e + sqrt(2) * ln(10) + +# Multi-argument functions +root(27, 3) +log(1024, 2) +root(1000000, 6) + +# Function composition chains +ln(exp(sqrt(abs(-100)))) +exp(ln(pi)) + +# Trig function stress +sin(pi/6) + cos(pi/3) + tan(pi/4) +cot(pi/4) + csc(pi/2) + +# Negative expressions with functions +-3 * pi * (sin(1) + cos(2)) +-sqrt(2) * ln(10) + +# Very long arithmetic chain +1 + 2 - 3 * 4 / 5 ^ 2 + 6 * 7 - 8 / 9 + 10 + +# Big integer arithmetic +2^256 + 3^100 - 5^50 + +# Deeply parenthesised +(((((1 + 2) * (3 + 4)) - ((5 - 6) * (7 + 8))) / 2) + 1) diff --git a/tests/test_cli.sh b/tests/test_cli.sh index b6f044d3..55a356c4 100644 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -136,27 +136,52 @@ assert_pipe_output "pipe with engineering" "12345.678" "12.345678E+3" -E assert_pipe_output "pipe with delimiter" "1234567.89" "1_234_567.89" -D "_" # ── File mode (-F/--file flag) ──────────────────────────────────────────── -TMPFILE=$(mktemp /tmp/decimo_test_XXXXXX.dm) -cat > "$TMPFILE" << 'FILE_EOF' -# Test expression file -pi -e -sqrt(2) - -# Arithmetic -100 * 12 - 23/17 -FILE_EOF - -assert_output "file mode basic" \ +# All test files live in tests/cli/test_data/ — no temp files needed. +DATA="tests/cli/test_data" + +# --- basic.dm: constants, functions, arithmetic --- +assert_output "file mode basic.dm" \ "$(printf '3.1415926535897932384626433832795028841971693993751\n2.7182818284590452353602874713526624977572470937000\n1.4142135623730950488016887242096980785696718753769\n1198.6470588235294117647058823529411764705882352941')" \ - "$BINARY" -F "$TMPFILE" + "$BINARY" -F "$DATA/basic.dm" -assert_output "file mode with precision" \ +assert_output "file mode basic.dm -P 10" \ "$(printf '3.141592654\n2.718281828\n1.414213562\n1198.647059')" \ - "$BINARY" -F "$TMPFILE" -P 10 - -rm -f "$TMPFILE" - + "$BINARY" -F "$DATA/basic.dm" -P 10 + +# --- comments.txt: comments, blank lines, inline comments, whitespace --- +assert_output "file mode comments.txt" \ + "$(printf '2\n4\n6')" \ + "$BINARY" -F "$DATA/comments.txt" + +# --- edge_cases.dm: zeros, negatives, nested parens, powers --- +assert_output "file mode edge_cases.dm" \ + "$(printf '0\n-0\n42\n-42\n0.001\n-0.001\n-6\n0\n-50\n6\n1\n1\n1\n10000000000')" \ + "$BINARY" -F "$DATA/edge_cases.dm" + +assert_output "file mode edge_cases.dm -P 10" \ + "$(printf '0\n-0\n42\n-42\n0.001\n-0.001\n-6\n0\n-50\n6\n1\n1\n1\n1.000000000E+10')" \ + "$BINARY" -F "$DATA/edge_cases.dm" -P 10 + +# --- torture: deeply nested functions, trig, multi-arg, long chains --- +assert_output "file mode torture (no ext)" \ + "$(printf '1.0713523668582555369923173752696402459121546287121\n1.0538965678284563733480142148872799027597789207696\n1.1579208923731619542357098500868790785326998466564E+77\n515377520732011331036461129765621272702107522001\n11.796081289703860754690015480540861635182913811879\n3\n1E+1\n10\n10.000000000000000000000000000000000000000000000000\n3.1415926535897932384626433832795028841971693993751\n2.0000000000000000000000000000000000000000000000000\n2.0000000000000000000000000000000000000000000000000\n-4.0085856587109635320394984475849874956541040162735\n-3.2563470670302936892264646109942871401480252761141\n53.631111111111111111111111111111111111111111111111\n1.1579208923731619542357098500920328537400190717884E+77\n19')" \ + "$BINARY" -F "$DATA/torture" + +# --- precision.dm: high-precision stress (repeating decimals, near-integers) --- +assert_output "file mode precision.dm" \ + "$(printf '0.14285714285714285714285714285714285714285714285714\n-2.6676418906242231236893288649633380405195232780734E-7\n2.0000000000000000000000000000000000000000000000000\n262537412640768743.99999999999925007259719818568888\n0.00022627290348967980473191579710694220169153814440402')" \ + "$BINARY" -F "$DATA/precision.dm" + +# --- File mode with formatting flags --- +assert_output "file mode basic.dm -S" \ + "$(printf '3.1415926535897932384626433832795028841971693993751E0\n2.7182818284590452353602874713526624977572470937E0\n1.4142135623730950488016887242096980785696718753769E0\n1.1986470588235294117647058823529411764705882352941E+3')" \ + "$BINARY" -F "$DATA/basic.dm" -S + +assert_output "file mode basic.dm -D _" \ + "$(printf '3.141_592_653_589_793_238_462_643_383_279_502_884_197_169_399_375_1\n2.718_281_828_459_045_235_360_287_471_352_662_497_757_247_093_700_0\n1.414_213_562_373_095_048_801_688_724_209_698_078_569_671_875_376_9\n1_198.647_058_823_529_411_764_705_882_352_941_176_470_588_235_294_1')" \ + "$BINARY" -F "$DATA/basic.dm" -D _ + +# --- Error cases --- # File mode: nonexistent file gives a clear error NONEXIST_OUTPUT=$("$BINARY" -F "nonexistent_file.dm" 2>&1 || true) if echo "$NONEXIST_OUTPUT" | grep -qi "cannot read file"; then From 2e369d5f065d6af54364bd2fd9ef2114b358f232 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sun, 12 Apr 2026 00:06:33 +0200 Subject: [PATCH 3/4] Fix pixi --- pixi.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pixi.toml b/pixi.toml index f11e72a5..0496ead4 100644 --- a/pixi.toml +++ b/pixi.toml @@ -49,7 +49,7 @@ bgmp = "clear && pixi run buildgmp" buildgmp = "bash src/decimo/gmp/build_gmp_wrapper.sh" # tests (use the mojo testing tool) -test = "pixi run package && bash ./tests/test_all.sh" +test = "pixi run package && pixi run buildcli && bash ./tests/test_all.sh" testdecimo = "pixi run package && bash ./tests/test_decimo.sh" testtoml = "pixi run package && bash ./tests/test_toml.sh" # bigfloat (build test binary linking the C wrapper, then run it) @@ -91,7 +91,7 @@ bcli = "clear && pixi run buildcli" # && pixi run mojo build -I src -I src/cli -I temp -o decimo src/cli/main.mojo""" buildcli = """pixi run mojo build -I src -I src/cli -o decimo src/cli/main.mojo""" tcli = "clear && pixi run testcli" -testcli = "bash tests/test_cli.sh" +testcli = "pixi run buildcli && bash tests/test_cli.sh" # python bindings (mojo4py) bpy = "clear && pixi run buildpy" From 9d691f1eb3ec01597c9dc2bac7bd26266be412c5 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sun, 12 Apr 2026 01:26:51 +0200 Subject: [PATCH 4/4] Address comments --- benches/cli/bench_cli.sh | 56 +++++++++++++++++++++++++++++++++++----- tests/test_cli.sh | 5 ++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/benches/cli/bench_cli.sh b/benches/cli/bench_cli.sh index 0a7e8c16..87bcd640 100644 --- a/benches/cli/bench_cli.sh +++ b/benches/cli/bench_cli.sh @@ -66,14 +66,53 @@ elapsed_ms() { ' -- "$@" } -# Extract the first 15 significant digits from a numeric string. +# Extract a canonical comparison key from a numeric string: +# adjusted base-10 exponent + first 15 significant digits. +# This ensures values that differ only by exponent (e.g. 1E+10 vs 1E+11) +# are correctly detected as a MISMATCH. sig_digits() { - local s="${1#-}" # strip sign - s=$(echo "$s" | sed 's/[eE][+-]*[0-9]*//') # strip exponent - if [[ "$s" == *.* ]]; then # strip trailing .0's - s=$(echo "$s" | sed 's/0*$//; s/\.$//') + 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 - echo "$s" | tr -d '.' | sed 's/^0*//' | cut -c1-15 # first 15 sig digits + + local int_part frac_part digits int_len adjusted_exp first_nonzero + + if [[ "$mantissa" == *.* ]]; then + int_part="${mantissa%%.*}" + frac_part="${mantissa#*.}" + # Strip trailing zeros from fractional part — they are not significant + frac_part=$(echo "$frac_part" | sed 's/0*$//') + else + int_part="$mantissa" + frac_part="" + fi + + digits="${int_part}${frac_part}" + digits=$(echo "$digits" | sed 's/^0*//; s/0*$//') + + if [[ -z "$digits" ]]; then + echo "ZERO" + return 0 + fi + + # Compute adjusted exponent so the key is position-independent + if [[ "$int_part" =~ [1-9] ]]; then + int_part=$(echo "$int_part" | sed 's/^0*//') + int_len=${#int_part} + adjusted_exp=$(( explicit_exp + int_len - 1 )) + else + first_nonzero=$(echo "$frac_part" | sed -n 's/^\(0*\)[1-9].*$/\1/p' | wc -c | tr -d ' ') + first_nonzero=$(( first_nonzero - 1 )) + adjusted_exp=$(( explicit_exp - first_nonzero - 1 )) + fi + + echo "${adjusted_exp}:$(echo "$digits" | cut -c1-15)" } # Compare two results by leading significant digits. @@ -131,6 +170,11 @@ bench_compare() { d_result=$("$BINARY" "$d_expr" -P "$prec" 2>/dev/null || echo "ERROR") d_ms=$(elapsed_ms "$BINARY" "$d_expr" -P "$prec") printf " %-10s %-38s %8s ms\n" "decimo:" "$(preview "$d_result")" "$d_ms" + if [[ "$d_result" == "ERROR" ]]; then + record "ERROR" + echo "" + return + fi # ── bc ── if [[ -n "$bc_expr" ]] && $HAS_BC; then diff --git a/tests/test_cli.sh b/tests/test_cli.sh index 55a356c4..72594a66 100644 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -1,6 +1,11 @@ #!/bin/bash set -e # Exit immediately if any command fails +# Derive repo root from the script's own location so the tests work +# regardless of the caller's working directory. +REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$REPO_ROOT" + # ── Unit tests ───────────────────────────────────────────────────────────── for f in tests/cli/*.mojo; do pixi run mojo run -I src -I src/cli -D ASSERT=all --debug-level=full "$f"