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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions docs/plans/cli_calculator.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,29 +369,35 @@ Error: division by zero
decimo> exit
```

| # | Task | Status | Notes |
| --- | --------------------------------------- | :----: | -------------------------------------------------------------------------- |
| 4.1 | No-args + TTY → launch REPL | ✓ | Replace "no expression" error with REPL auto-launch when terminal detected |
| 4.2 | Read-eval-print loop | ✓ | `read_line()` via `getchar()`; one expression per line |
| 4.3 | Custom prompt (`decimo>`) | ✓ | Coloured prompt to stderr so results can be piped |
| 4.4 | `ans` variable (previous result) | ✗ | Injected as a constant into the evaluator; starts as `0` |
| 4.5 | Variable assignment (`x = expr`) | ✗ | Parse `name = expr` syntax; store in a name→BigDecimal map |
| 4.6 | Meta-commands (`:precision N`, `:vars`) | ✗ | `:` prefix avoids collision with expressions |
| 4.7 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✓ | |
| 4.8 | Error recovery (don't crash session) | ✓ | Catch exceptions per-line, display error, continue loop |
| 4.9 | History (if Mojo gets readline support) | ✗ | Future — depends on Mojo FFI evolution |
| # | Task | Status | Notes |
| ---- | ------------------------------------------ | :----: | --------------------------------------------------------------------------- |
| 4.1 | No-args + TTY → launch REPL | ✓ | Replace "no expression" error with REPL auto-launch when terminal detected |
| 4.2 | Read-eval-print loop | ✓ | `read_line()` via `getchar()`; one expression per line |
| 4.3 | Custom prompt (`decimo>`) | ✓ | Coloured prompt to stderr so results can be piped |
| 4.4 | `ans` variable (previous result) | ✓ | Stored in `Dict[String, Decimal]`; updated after each successful evaluation |
| 4.5 | Variable assignment (`x = expr`) | ✓ | `name = expr` detection in REPL; protected names (pi, e, functions, ans) |
| 4.6 | Meta-commands (`:precision N`, `:vars`) | ✗ | `:` prefix avoids collision with expressions, allow short aliases |
| 4.7 | One-line quick setting | ✗ | `:p 100 s down` sets precision, scientific notation, and round_down mode |
| 4.8 | Same-line temp precision setting | ✗ | `2*sqrt(1.23):p 100 s down` for a temporary setting for the expression |
| 4.9 | Print settings (`:settings`) | ✗ | Display current precision, formatting options, etc. |
| 4.10 | Variable listing (`:vars` and `:variable`) | ✗ | List all user-defined variables and their values |
| 4.11 | Everything in the REPL is case-insensitive | ✗ | Map all input chars to lower case at pre-tokenizer stage |
| 4.12 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✓ | |
| 4.13 | Error recovery (don't crash session) | ✓ | Catch exceptions per-line, display error, continue loop |
| 4.14 | History (if Mojo gets readline support) | ✗ | Future — depends on Mojo FFI evolution |

### Phase 5: Future Enhancements

1. Build and distribute as a single binary (Homebrew, GitHub Releases, etc.) — defer until REPL is stable so first-run experience is complete.
2. Detect full-width digits/operators for CJK users while parsing.
3. Response files (`@expressions.txt`) — when Mojo compiler bug is fixed, use ArgMojo's `cmd.response_file_prefix("@")`.

| # | Task | Status | Notes |
| --- | ------------------------------------------- | :----: | ------------------------------------------------------------------------------ |
| 5.1 | Build and distribute as single binary | ✗ | Defer until REPL is stable; Homebrew, GitHub Releases, `curl \| sh` installer |
| 5.2 | Full-width digit/operator detection for CJK | ✗ | Tokenizer-level handling for CJK users |
| 5.3 | Response files (`@expressions.txt`) | ✗ | Blocked on Mojo compiler bug; `cmd.response_file_prefix("@")` ready when fixed |
| # | Task | Status | Notes |
| --- | ------------------------------------------- | :----: | --------------------------------------------------------------------------------- |
| 5.1 | Full-width digit/operator detection for CJK | ✗ | Tokenizer-level handling for CJK users |
| 5.2 | Full-width to half-width normalization | ✗ | Pre-tokenizer normalization step to convert full-width chars to ASCII equivalents |
| 5.3 | Build and distribute as single binary | ✗ | Defer until REPL is stable; Homebrew, GitHub Releases, `curl \| sh` installer |
| 5.4 | Response files (`@expressions.txt`) | ✗ | Blocked on Mojo compiler bug; `cmd.response_file_prefix("@")` ready when fixed |

## Design Decisions

Expand Down
3 changes: 2 additions & 1 deletion docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ This is a to-do list for Decimo.
- [ ] Implement different methods for adding decimo types with `Int` types so that an implicit conversion is not required.
- [ ] Use debug mode to check for unnecessary zero words before all arithmetic operations. This will help ensure that there are no zero words, which can simplify the speed of checking for zero because we only need to check the first word.
- [ ] Check the `floor_divide()` function of `BigUInt`. Currently, the speed of division between imilar-sized numbers are okay, but the speed of 2n-by-n, 4n-by-n, and 8n-by-n divisions decreases unproportionally. This is likely due to the segmentation of the dividend in the Burnikel-Ziegler algorithm.
- [ ] Consider using `Decimal` as the struct name instead of `BigDecimal`, and use `comptime BigDecimal = Decimal` to create an alias for the `Decimal` struct. This just switches the alias and the struct name, but it may be more intuitive to use `Decimal` as the struct name since it is more consistent with Python's `decimal.Decimal`. Moreover, hovering over `Decimal` will show the docstring of the struct, which is more intuitive than hovering over `BigDecimal` to see the docstring of the struct.
- [x] (PR #127, #128, #131) Make all default constructor "safe", which means that the words are checked and normalized to ensure that there are no zero words and that the number is in a valid state. This will help prevent bugs and ensure that all `BigUInt` instances are in a consistent state. Also allow users to create "unsafe" `BigUInt` instances if they want to, but there must be a key-word only argument, e.g., `raw_words`.

- [x] (#31) The `exp()` function performs slower than Python's counterpart in specific cases. Detailed investigation reveals the bottleneck stems from multiplication operations between decimals with significant fractional components. These operations currently rely on UInt256 arithmetic, which introduces performance overhead. Optimization of the `multiply()` function is required to address these performance bottlenecks, particularly for high-precision decimal multiplication with many digits after the decimal point.
- [x] (#31) The `exp()` function performs slower than Python's counterpart in specific cases. Detailed investigation reveals the bottleneck stems from multiplication operations between decimals with significant fractional components. These operations currently rely on UInt256 arithmetic, which introduces performance overhead. Optimization of the `multiply()` function is required to address these performance bottlenecks, particularly for high-precision decimal multiplication with many digits after the decimal point. Internally, also use `Decimal` instead of `BigDecimal` or `BDec` to be consistent.
- [x] Implement different methods for augmented arithmetic assignments to improve memeory-efficiency and performance.
- [x] Implement a method `remove_leading_zeros` for `BigUInt`, which removes the zero words from the most significant end of the number.
- [x] Use debug mode to check for uninitialized `BigUInt` before all arithmetic operations. This will help ensure that there are no uninitialized `BigUInt`.
Expand Down
8 changes: 7 additions & 1 deletion src/cli/calculator/__init__.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@ from .tokenizer import (
TOKEN_FUNC,
TOKEN_CONST,
TOKEN_COMMA,
TOKEN_VARIABLE,
)
from .parser import parse_to_rpn
from .evaluator import evaluate_rpn, evaluate
from .engine import evaluate_and_print, display_calc_error, pad_to_precision
from .engine import (
evaluate_and_print,
evaluate_and_return,
display_calc_error,
pad_to_precision,
)
from .display import print_error, print_warning, print_hint, write_prompt
from .io import (
stdin_is_tty,
Expand Down
50 changes: 48 additions & 2 deletions src/cli/calculator/engine.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ used by both one-shot/pipe/file modes (main.mojo) and the interactive REPL
(repl.mojo).
"""

from decimo import Decimal
from decimo.rounding_mode import RoundingMode
from std.collections import Dict
from .tokenizer import tokenize
from .parser import parse_to_rpn
from .evaluator import evaluate_rpn, final_round
Expand All @@ -38,6 +40,7 @@ def evaluate_and_print(
delimiter: String,
rounding_mode: RoundingMode,
show_expr_on_error: Bool = False,
variables: Dict[String, Decimal] = Dict[String, Decimal](),
) raises:
"""Tokenize, parse, evaluate, and print one expression.

Expand All @@ -54,12 +57,13 @@ def evaluate_and_print(
rounding_mode: Rounding mode for the final result.
show_expr_on_error: If True, show the expression with a caret
indicator on error. If False, show only the error message.
variables: A name→value mapping of user-defined variables.
"""
try:
var tokens = tokenize(expr)
var tokens = tokenize(expr, variables)
var rpn = parse_to_rpn(tokens^)
var value = final_round(
evaluate_rpn(rpn^, precision), precision, rounding_mode
evaluate_rpn(rpn^, precision, variables), precision, rounding_mode
)

if scientific:
Expand Down Expand Up @@ -149,3 +153,45 @@ def pad_to_precision(plain: String, precision: Int) -> String:
return plain

return plain + "0" * (precision - frac_len)


def evaluate_and_return(
expr: String,
precision: Int,
scientific: Bool,
engineering: Bool,
pad: Bool,
delimiter: String,
rounding_mode: RoundingMode,
variables: Dict[String, Decimal] = Dict[String, Decimal](),
) raises -> Decimal:
"""Tokenize, parse, evaluate, print, and return the result.

Like `evaluate_and_print` but also returns the `Decimal` value so the
REPL can store it in `ans` or a named variable.

On error, displays a coloured diagnostic and raises to signal failure
to the caller.
"""
try:
var tokens = tokenize(expr, variables)
var rpn = parse_to_rpn(tokens^)
var value = final_round(
evaluate_rpn(rpn^, precision, variables), precision, rounding_mode
)

if scientific:
print(value.to_string(scientific=True, delimiter=delimiter))
elif engineering:
print(value.to_string(engineering=True, delimiter=delimiter))
elif pad:
print(
pad_to_precision(value.to_string(force_plain=True), precision)
)
else:
print(value.to_string(delimiter=delimiter))

return value^
except e:
display_calc_error(String(e), expr)
raise e^
46 changes: 36 additions & 10 deletions src/cli/calculator/evaluator.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -168,14 +170,25 @@ 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
computations to absorb intermediate rounding errors. The caller is
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.
Expand All @@ -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 "
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
19 changes: 12 additions & 7 deletions src/cli/calculator/parser.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ from .tokenizer import (
TOKEN_FUNC,
TOKEN_CONST,
TOKEN_COMMA,
TOKEN_VARIABLE,
)


def parse_to_rpn(tokens: List[Token]) raises -> List[Token]:
"""Converts infix tokens to Reverse Polish Notation using
Dijkstra's shunting-yard algorithm.
"""Converts infix tokens to Reverse Polish Notation using Dijkstra's
shunting-yard algorithm.

Supports binary operators (+, -, *, /, ^), unary minus,
function calls (sqrt, ln, …), constants (pi, e), and commas
for multi-argument functions like root(x, n).
Supports binary operators (+, -, *, /, ^), unary minus, function calls
(sqrt, ln, …), constants (pi, e), and commas for multi-argument functions
like root(x, n).

Raises:
Error: On mismatched parentheses, misplaced commas, or trailing
Expand All @@ -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
Expand Down
Loading
Loading