diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-24 11:58:45 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-24 11:58:45 +0300 |
| commit | c705f26cd8f62009cafc1fb064f417926182cccd (patch) | |
| tree | 244d29e3c828a792cce49dea0dbebd12b59d8977 | |
| parent | 60f2d80da7b197be6f280abf254f946572e060ef (diff) | |
docs: add variables.md; tests: comprehensive assignment operator tests
- Document all assignment operators (:=, =:, =) with syntax table
- Document variable management commands (vars, clear, d)
- Document variable lifecycle and practical use cases
- Add table-driven tests for := and =: operators
- Add tests for = with expression continuation
- Add tests for variable reuse in expressions
- Add tests for chained assignments
- Add tests for vars/clear commands
- Add tests for d (delete) operator
- Add tests for variable persistence across expressions
| -rw-r--r-- | docs/variables.md | 176 | ||||
| -rw-r--r-- | internal/rpn/assignment_test.go | 248 |
2 files changed, 424 insertions, 0 deletions
diff --git a/docs/variables.md b/docs/variables.md new file mode 100644 index 0000000..60474cc --- /dev/null +++ b/docs/variables.md @@ -0,0 +1,176 @@ +# Variables and Assignment Operators + +The gt calculator supports named variables that store numeric values. Variables persist across expressions within a single RPN session (REPL or multi-expression input), enabling reusable values and multi-step calculations. + +## Assignment Operators + +Three assignment operators are available, differing in token order: + +| Operator | Syntax | Description | Example | +|----------|--------|-------------|---------| +| `:=` | `name value :=` | Right assignment | `x 5 :=` → `x = 5` | +| `=:` | `value name =:` | Left assignment | `10 y =:` → `y = 10` | +| `=` | `name value =` or `name = value` | Standard assignment | `x 10 =` or `x = 10` | + +### `:=` (Right Assignment) + +The value appears on the right (top of stack). This is the most idiomatic RPN form. + +``` +x 5 := → x = 5 +price 9.99 := → price = 9.99 +``` + +### `=:` (Left Assignment) + +The value appears on the left (bottom of stack). Useful when you think in "value first" order. + +``` +42 answer =: → answer = 42 +3.14159 pi =: → pi = 3.14159 +``` + +### `=` (Standard Assignment) + +Supports both infix-style (`name = value`) and postfix-style (`name value =`) syntax. Also supports expression continuation — you can append an expression after the assignment to evaluate immediately. + +``` +x = 5 → x = 5 +x 10 = → x = 10 +x 10 = x 5 + → 15 (assigns x=10, then evaluates x+5) +``` + +## Variable Management + +### `vars` — List Variables + +Shows all defined variables, sorted alphabetically with their values. + +``` +vars +→ No variables defined (when none exist) +→ a = 10 + b = 3 (when variables are defined) +``` + +### `clear` — Clear All Variables + +Removes all defined variables at once. + +``` +clear +→ All variables cleared +``` + +### `d` — Delete a Variable + +Removes a single variable. The variable name must be pushed as a symbol (using `:` prefix) onto the stack before `d`. + +``` +x 5 := → x = 5 +:x d → (deletes x) +x → :x (x is now undefined, shown as symbol) +``` + +Attempting to delete a non-existent variable returns an error: + +``` +:nonexistent d → error: variable not found: nonexistent +``` + +## Variable Lifecycle + +1. **Creation**: A variable is created when a value is assigned using `:=`, `=:`, or `=`. +2. **Usage**: Reference the variable by name in an expression; its value is pushed onto the stack. +3. **Reassignment**: Assigning to an existing variable overwrites its previous value. +4. **Deletion**: Remove with `:name d` or clear all with `clear`. +5. **Session scope**: Variables persist only within a single RPN session. Each CLI invocation starts fresh. + +## Using Variables in Expressions + +Once defined, variables are used by name in expressions. The variable's value is pushed onto the stack for computation. + +### Single Variable Reuse + +``` +x 5 := +x x + → 10 +x 2 * → 10 +``` + +### Multi-Variable Expressions + +``` +a 10 := +b 3 := +a b + → 13 +a b * → 30 +``` + +### Chained Assignments + +Multiple variables can be assigned in a single expression: + +``` +a 10 := b 3 := c 2 := +a b + c * → 26 +``` + +### Variable Reassignment + +Variables can be reassigned to new values: + +``` +x 5 := → x = 5 +x 10 := → x = 10 +x → 10 +``` + +### Assignment with Expression Continuation + +The `=` operator supports evaluating an expression immediately after assignment: + +``` +x 10 = x 5 + → 15 (assigns x=10, then computes x+5) +``` + +## Practical Use Cases + +### Storing Reusable Values + +Store constants or frequently-used values to avoid retyping: + +``` +pi 3.14159265 := +radius 7 := +2 pi radius * → 43.9822971 (circumference) +``` + +### Multi-Step Calculations + +Break complex calculations into named intermediate steps: + +``` +base 100 := +tax_rate 0.08 := +tax base tax_rate * → 8 +total base tax + → 108 +``` + +### Iterative Refinement + +Update a running value across multiple operations: + +``` +sum 0 := +sum 10 + → 10 +sum 20 + → 30 +sum 5 - → 25 +``` + +## Reference + +- **Implementation**: `internal/rpn/operations_variables.go` (assignment operators, variable CRUD) +- **Parsing**: `internal/rpn/rpn_parse.go` (`handleAssignmentOp()`, `handleStandardAssign()`) +- **Registry**: `internal/rpn/operator_registry.go` (operator registration for `:=`, `=:`, `=`, `d`) +- **Variable store**: `internal/rpn/variables.go` (thread-safe variable storage with save/load) diff --git a/internal/rpn/assignment_test.go b/internal/rpn/assignment_test.go index 1b3cfff..8c6a4b2 100644 --- a/internal/rpn/assignment_test.go +++ b/internal/rpn/assignment_test.go @@ -4,6 +4,7 @@ package rpn import ( + "strings" "testing" ) @@ -326,3 +327,250 @@ func TestAssignmentAfterEqualEqual(t *testing.T) { t.Errorf("x = %v (exists=%v), want 5", val, exists) } } + +// TestAssignRightOperator tests the := (right assignment) operator via ParseAndEvaluate. +func TestAssignRightOperator(t *testing.T) { + tests := []struct { + name string + expr string + want string + }{ + {"basic right assignment", "x 5 :=", "x = 5"}, + {"right assignment with decimal", "pi 3.14159 :=", "pi = 3.14159"}, + {"right assignment with negative value", "neg -10 :=", "neg = -10"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewRPN(NewVariables()) + result, err := r.ParseAndEvaluate(tt.expr) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) error = %v", tt.expr, err) + } + if result != tt.want { + t.Errorf("ParseAndEvaluate(%q) = %q, want %q", tt.expr, result, tt.want) + } + }) + } +} + +// TestAssignLeftOperator tests the =: (left assignment) operator via ParseAndEvaluate. +func TestAssignLeftOperator(t *testing.T) { + tests := []struct { + name string + expr string + want string + }{ + {"basic left assignment", "10 y =:", "y = 10"}, + {"left assignment with decimal", "2.71828 e =:", "e = 2.71828"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewRPN(NewVariables()) + result, err := r.ParseAndEvaluate(tt.expr) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) error = %v", tt.expr, err) + } + if result != tt.want { + t.Errorf("ParseAndEvaluate(%q) = %q, want %q", tt.expr, result, tt.want) + } + }) + } +} + +// TestStandardAssignOperator tests the = (standard assignment) operator via ParseAndEvaluate. +func TestStandardAssignOperator(t *testing.T) { + tests := []struct { + name string + expr string + want string + }{ + {"infix assignment", "x = 5", "x = 5"}, + {"postfix assignment", "x 10 =", "x = 10"}, + {"assignment with expression continuation", "x 10 = x 5 +", "15"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewRPN(NewVariables()) + result, err := r.ParseAndEvaluate(tt.expr) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) error = %v", tt.expr, err) + } + if result != tt.want { + t.Errorf("ParseAndEvaluate(%q) = %q, want %q", tt.expr, result, tt.want) + } + }) + } +} + +// TestVariableInExpression tests using variables in subsequent expressions. +func TestVariableInExpression(t *testing.T) { + tests := []struct { + name string + setup string + expr string + want string + }{ + {"single variable reuse", "x 5 :=", "x x +", "10"}, + {"multi-variable expression", "a 10 := b 3 :=", "a b +", "13"}, + {"variable in complex expression", "x 5 :=", "x 2 + 3 *", "21"}, + {"variable reassignment", "x 5 :=", "x 10 := x", "10"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewRPN(NewVariables()) + _, err := r.ParseAndEvaluate(tt.setup) + if err != nil { + t.Fatalf("Setup failed for %q: %v", tt.setup, err) + } + result, err := r.ParseAndEvaluate(tt.expr) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) error = %v", tt.expr, err) + } + if result != tt.want { + t.Errorf("ParseAndEvaluate(%q) = %q, want %q", tt.expr, result, tt.want) + } + }) + } +} + +// TestChainedAssignments tests chaining multiple assignments in one expression. +func TestChainedAssignments(t *testing.T) { + r := NewRPN(NewVariables()) + result, err := r.ParseAndEvaluate("a 10 := b 3 := c 2 :=") + if err != nil { + t.Fatalf("Chained assignment failed: %v", err) + } + // Chained assignments return empty result (side effects) + if result != "" { + t.Errorf("Chained assignment result = %q, want empty", result) + } + + // Verify variables were set + if val, exists := r.vars.GetVariable("a"); !exists || val != 10 { + t.Errorf("Variable a should be 10, got %v (exists=%v)", val, exists) + } + if val, exists := r.vars.GetVariable("b"); !exists || val != 3 { + t.Errorf("Variable b should be 3, got %v (exists=%v)", val, exists) + } + if val, exists := r.vars.GetVariable("c"); !exists || val != 2 { + t.Errorf("Variable c should be 2, got %v (exists=%v)", val, exists) + } + + // Use variables in expression + result, err = r.ParseAndEvaluate("a b + c *") + if err != nil { + t.Fatalf("Expression with variables failed: %v", err) + } + if result != "26" { + t.Errorf("a b + c * = %q, want %q", result, "26") + } +} + +// TestVarsCommand tests the vars command. +func TestVarsCommand(t *testing.T) { + r := NewRPN(NewVariables()) + + // Empty vars + result, err := r.ParseAndEvaluate("vars") + if err != nil { + t.Fatalf("vars failed: %v", err) + } + if !strings.Contains(result, "No variables defined") { + t.Errorf("Empty vars should say 'No variables defined', got: %q", result) + } + + // With variables + r.ParseAndEvaluate("z 3 :=") + r.ParseAndEvaluate("a 1 :=") + r.ParseAndEvaluate("m 2 :=") + + result, err = r.ParseAndEvaluate("vars") + if err != nil { + t.Fatalf("vars with data failed: %v", err) + } + + // Should contain all variable names + if !strings.Contains(result, "a") || !strings.Contains(result, "m") || !strings.Contains(result, "z") { + t.Errorf("vars should contain a, m, z; got: %q", result) + } +} + +// TestClearCommand tests the clear command. +func TestClearCommand(t *testing.T) { + r := NewRPN(NewVariables()) + r.ParseAndEvaluate("x 5 :=") + r.ParseAndEvaluate("y 10 :=") + + result, err := r.ParseAndEvaluate("clear") + if err != nil { + t.Fatalf("clear failed: %v", err) + } + if !strings.Contains(result, "All variables cleared") { + t.Errorf("clear result = %q, want to contain 'All variables cleared'", result) + } + + // Verify variables are gone + if r.vars.Count() != 0 { + t.Errorf("After clear, count = %d, want 0", r.vars.Count()) + } +} + +// TestDeleteVariableOperator tests the d (delete) operator. +func TestDeleteVariableOperator(t *testing.T) { + r := NewRPN(NewVariables()) + + // Create variable + r.ParseAndEvaluate("x 5 :=") + _, exists := r.vars.GetVariable("x") + if !exists { + t.Fatal("Variable x should exist before delete") + } + + // Delete with symbol syntax — d is a side-effect operator that leaves + // the stack empty, so ParseAndEvaluate returns "empty result". The delete + // still succeeds; we verify by checking the variable store directly. + _, _ = r.ParseAndEvaluate(":x d") + + // Verify it's gone + _, exists = r.vars.GetVariable("x") + if exists { + t.Error("Variable x should not exist after delete") + } +} + +// TestDeleteNonExistentVariableOperator tests deleting a variable that doesn't exist. +func TestDeleteNonExistentVariableOperator(t *testing.T) { + r := NewRPN(NewVariables()) + _, err := r.ParseAndEvaluate(":nonexistent d") + if err == nil { + t.Error("Deleting non-existent variable should return error") + } +} + +// TestVariablePersistenceAcrossExpressions tests that variables persist across +// multiple ParseAndEvaluate calls on the same RPN instance. +func TestVariablePersistenceAcrossExpressions(t *testing.T) { + r := NewRPN(NewVariables()) + + r.ParseAndEvaluate("a 10 :=") + r.ParseAndEvaluate("b 3 :=") + + // Variables should persist + result, err := r.ParseAndEvaluate("a b +") + if err != nil { + t.Fatalf("Expression failed: %v", err) + } + if result != "13" { + t.Errorf("a b + = %q, want %q", result, "13") + } + + // Verify vars shows both + result, _ = r.ParseAndEvaluate("vars") + if !strings.Contains(result, "a") || !strings.Contains(result, "b") { + t.Errorf("vars should contain a and b, got: %q", result) + } +} |
