Skip to content

Commit 970cc43

Browse files
authored
[cli] Add ans variable + variable assignment and evaluation (#206)
1 parent 76f1c0b commit 970cc43

File tree

14 files changed

+606
-85
lines changed

14 files changed

+606
-85
lines changed

docs/plans/cli_calculator.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -369,29 +369,35 @@ Error: division by zero
369369
decimo> exit
370370
```
371371

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

384389
### Phase 5: Future Enhancements
385390

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

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

396402
## Design Decisions
397403

docs/todo.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ This is a to-do list for Decimo.
88
- [ ] Implement different methods for adding decimo types with `Int` types so that an implicit conversion is not required.
99
- [ ] 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.
1010
- [ ] 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.
11+
- [ ] 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.
1112
- [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`.
1213

13-
- [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.
14+
- [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.
1415
- [x] Implement different methods for augmented arithmetic assignments to improve memeory-efficiency and performance.
1516
- [x] Implement a method `remove_leading_zeros` for `BigUInt`, which removes the zero words from the most significant end of the number.
1617
- [x] Use debug mode to check for uninitialized `BigUInt` before all arithmetic operations. This will help ensure that there are no uninitialized `BigUInt`.

src/cli/calculator/__init__.mojo

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,16 @@ from .tokenizer import (
4141
TOKEN_FUNC,
4242
TOKEN_CONST,
4343
TOKEN_COMMA,
44+
TOKEN_VARIABLE,
4445
)
4546
from .parser import parse_to_rpn
4647
from .evaluator import evaluate_rpn, evaluate
47-
from .engine import evaluate_and_print, display_calc_error, pad_to_precision
48+
from .engine import (
49+
evaluate_and_print,
50+
evaluate_and_return,
51+
display_calc_error,
52+
pad_to_precision,
53+
)
4854
from .display import print_error, print_warning, print_hint, write_prompt
4955
from .io import (
5056
stdin_is_tty,

src/cli/calculator/engine.mojo

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ used by both one-shot/pipe/file modes (main.mojo) and the interactive REPL
2222
(repl.mojo).
2323
"""
2424

25+
from decimo import Decimal
2526
from decimo.rounding_mode import RoundingMode
27+
from std.collections import Dict
2628
from .tokenizer import tokenize
2729
from .parser import parse_to_rpn
2830
from .evaluator import evaluate_rpn, final_round
@@ -38,6 +40,7 @@ def evaluate_and_print(
3840
delimiter: String,
3941
rounding_mode: RoundingMode,
4042
show_expr_on_error: Bool = False,
43+
variables: Dict[String, Decimal] = Dict[String, Decimal](),
4144
) raises:
4245
"""Tokenize, parse, evaluate, and print one expression.
4346
@@ -54,12 +57,13 @@ def evaluate_and_print(
5457
rounding_mode: Rounding mode for the final result.
5558
show_expr_on_error: If True, show the expression with a caret
5659
indicator on error. If False, show only the error message.
60+
variables: A name→value mapping of user-defined variables.
5761
"""
5862
try:
59-
var tokens = tokenize(expr)
63+
var tokens = tokenize(expr, variables)
6064
var rpn = parse_to_rpn(tokens^)
6165
var value = final_round(
62-
evaluate_rpn(rpn^, precision), precision, rounding_mode
66+
evaluate_rpn(rpn^, precision, variables), precision, rounding_mode
6367
)
6468

6569
if scientific:
@@ -149,3 +153,45 @@ def pad_to_precision(plain: String, precision: Int) -> String:
149153
return plain
150154

151155
return plain + "0" * (precision - frac_len)
156+
157+
158+
def evaluate_and_return(
159+
expr: String,
160+
precision: Int,
161+
scientific: Bool,
162+
engineering: Bool,
163+
pad: Bool,
164+
delimiter: String,
165+
rounding_mode: RoundingMode,
166+
variables: Dict[String, Decimal] = Dict[String, Decimal](),
167+
) raises -> Decimal:
168+
"""Tokenize, parse, evaluate, print, and return the result.
169+
170+
Like `evaluate_and_print` but also returns the `Decimal` value so the
171+
REPL can store it in `ans` or a named variable.
172+
173+
On error, displays a coloured diagnostic and raises to signal failure
174+
to the caller.
175+
"""
176+
try:
177+
var tokens = tokenize(expr, variables)
178+
var rpn = parse_to_rpn(tokens^)
179+
var value = final_round(
180+
evaluate_rpn(rpn^, precision, variables), precision, rounding_mode
181+
)
182+
183+
if scientific:
184+
print(value.to_string(scientific=True, delimiter=delimiter))
185+
elif engineering:
186+
print(value.to_string(engineering=True, delimiter=delimiter))
187+
elif pad:
188+
print(
189+
pad_to_precision(value.to_string(force_plain=True), precision)
190+
)
191+
else:
192+
print(value.to_string(delimiter=delimiter))
193+
194+
return value^
195+
except e:
196+
display_calc_error(String(e), expr)
197+
raise e^

src/cli/calculator/evaluator.mojo

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ RPN evaluator for the Decimo CLI calculator.
2020
Evaluates a Reverse Polish Notation token list using BigDecimal arithmetic.
2121
"""
2222

23-
from decimo import BDec
23+
from decimo import Decimal
2424
from decimo.rounding_mode import RoundingMode
25+
from std.collections import Dict
2526

2627
from .tokenizer import (
2728
Token,
@@ -34,6 +35,7 @@ from .tokenizer import (
3435
TOKEN_CARET,
3536
TOKEN_FUNC,
3637
TOKEN_CONST,
38+
TOKEN_VARIABLE,
3739
)
3840
from .parser import parse_to_rpn
3941
from .tokenizer import tokenize
@@ -45,7 +47,7 @@ from .tokenizer import tokenize
4547

4648

4749
def _call_func(
48-
name: String, mut stack: List[BDec], precision: Int, position: Int
50+
name: String, mut stack: List[Decimal], precision: Int, position: Int
4951
) raises:
5052
"""Pop argument(s) from `stack`, call the named Decimo function,
5153
and push the result back.
@@ -168,14 +170,25 @@ def _call_func(
168170
# ===----------------------------------------------------------------------=== #
169171

170172

171-
def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec:
173+
def evaluate_rpn(
174+
rpn: List[Token],
175+
precision: Int,
176+
variables: Dict[String, Decimal] = Dict[String, Decimal](),
177+
) raises -> Decimal:
172178
"""Evaluate an RPN token list using BigDecimal arithmetic.
173179
174180
Internally uses `working_precision = precision + GUARD_DIGITS` for all
175181
computations to absorb intermediate rounding errors. The caller is
176182
responsible for rounding the final result to `precision` significant
177183
digits (see `final_round`).
178184
185+
Args:
186+
rpn: The Reverse Polish Notation token list.
187+
precision: Number of significant digits.
188+
variables: A name→value mapping of user-defined variables (e.g.
189+
`ans`, `x`). If a TOKEN_VARIABLE token's name is not found
190+
in this dict, an error is raised.
191+
179192
Raises:
180193
Error: On division by zero, missing operands, or other runtime
181194
errors — with source position when available.
@@ -186,19 +199,19 @@ def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec:
186199
# accumulated rounding errors from intermediate operations.
187200
comptime GUARD_DIGITS = 9
188201
var working_precision = precision + GUARD_DIGITS
189-
var stack = List[BDec]()
202+
var stack = List[Decimal]()
190203

191204
for i in range(len(rpn)):
192205
var kind = rpn[i].kind
193206

194207
if kind == TOKEN_NUMBER:
195-
stack.append(BDec.from_string(rpn[i].value))
208+
stack.append(Decimal.from_string(rpn[i].value))
196209

197210
elif kind == TOKEN_CONST:
198211
if rpn[i].value == "pi":
199-
stack.append(BDec.pi(working_precision))
212+
stack.append(Decimal.pi(working_precision))
200213
elif rpn[i].value == "e":
201-
stack.append(BDec.e(working_precision))
214+
stack.append(Decimal.e(working_precision))
202215
else:
203216
raise Error(
204217
"Error at position "
@@ -208,6 +221,19 @@ def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec:
208221
+ "'"
209222
)
210223

224+
elif kind == TOKEN_VARIABLE:
225+
var var_name = rpn[i].value
226+
if var_name in variables:
227+
stack.append(variables[var_name].copy())
228+
else:
229+
raise Error(
230+
"Error at position "
231+
+ String(rpn[i].position)
232+
+ ": undefined variable '"
233+
+ var_name
234+
+ "'"
235+
)
236+
211237
elif kind == TOKEN_UNARY_MINUS:
212238
if len(stack) < 1:
213239
raise Error(
@@ -306,10 +332,10 @@ def evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec:
306332

307333

308334
def final_round(
309-
value: BDec,
335+
value: Decimal,
310336
precision: Int,
311337
rounding_mode: RoundingMode = RoundingMode.half_even(),
312-
) raises -> BDec:
338+
) raises -> Decimal:
313339
"""Round a BigDecimal to `precision` significant digits.
314340
315341
This should be called on the result of `evaluate_rpn` before
@@ -327,7 +353,7 @@ def evaluate(
327353
expr: String,
328354
precision: Int = 50,
329355
rounding_mode: RoundingMode = RoundingMode.half_even(),
330-
) raises -> BDec:
356+
) raises -> Decimal:
331357
"""Evaluate a math expression string and return a BigDecimal result.
332358
333359
This is the main entry point for the calculator engine.

src/cli/calculator/parser.mojo

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,17 @@ from .tokenizer import (
3434
TOKEN_FUNC,
3535
TOKEN_CONST,
3636
TOKEN_COMMA,
37+
TOKEN_VARIABLE,
3738
)
3839

3940

4041
def parse_to_rpn(tokens: List[Token]) raises -> List[Token]:
41-
"""Converts infix tokens to Reverse Polish Notation using
42-
Dijkstra's shunting-yard algorithm.
42+
"""Converts infix tokens to Reverse Polish Notation using Dijkstra's
43+
shunting-yard algorithm.
4344
44-
Supports binary operators (+, -, *, /, ^), unary minus,
45-
function calls (sqrt, ln, …), constants (pi, e), and commas
46-
for multi-argument functions like root(x, n).
45+
Supports binary operators (+, -, *, /, ^), unary minus, function calls
46+
(sqrt, ln, …), constants (pi, e), and commas for multi-argument functions
47+
like root(x, n).
4748
4849
Raises:
4950
Error: On mismatched parentheses, misplaced commas, or trailing
@@ -55,8 +56,12 @@ def parse_to_rpn(tokens: List[Token]) raises -> List[Token]:
5556
for i in range(len(tokens)):
5657
var kind = tokens[i].kind
5758

58-
# Numbers and constants go straight to output
59-
if kind == TOKEN_NUMBER or kind == TOKEN_CONST:
59+
# Numbers, constants, and variables go straight to output
60+
if (
61+
kind == TOKEN_NUMBER
62+
or kind == TOKEN_CONST
63+
or kind == TOKEN_VARIABLE
64+
):
6065
output.append(tokens[i])
6166

6267
# Functions are pushed onto the operator stack

0 commit comments

Comments
 (0)