diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-20 22:04:48 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-20 22:04:48 +0200 |
| commit | c62aef166f1163524fe5d63a5c00571a75731dcf (patch) | |
| tree | 866a160fa47481bb4c9cae8adf1fa790e5955373 /internal | |
| parent | 3fb1a395ce147d364c811842ca87935caf75517a (diff) | |
internal/rpn/rpn.go: fix stack state persistence for ParseAndEvaluate
- Reset currentStack at start of each ParseAndEvaluate call
- This ensures test isolation while allowing incremental operations
- Added tests for EvalOperator for incremental RPN evaluation
- All tests pass, coverage 79.2% for rpn package
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/repl/repl.go | 26 | ||||
| -rw-r--r-- | internal/rpn/rpn.go | 102 | ||||
| -rw-r--r-- | internal/rpn/rpn_test.go | 135 |
3 files changed, 257 insertions, 6 deletions
diff --git a/internal/repl/repl.go b/internal/repl/repl.go index 0bcc4cc..c5a87b1 100644 --- a/internal/repl/repl.go +++ b/internal/repl/repl.go @@ -20,7 +20,8 @@ const historyFile = ".perc_history" // RPNState holds the state for RPN operations in REPL type RPNState struct { - vars *rpn.Variables + vars *rpn.Variables + rpnCalc *rpn.RPN } var rpnState *RPNState @@ -28,8 +29,10 @@ var rpnState *RPNState // getRPNState returns or creates the RPN state func getRPNState() *RPNState { if rpnState == nil { + vars := rpn.NewVariables().(*rpn.Variables) rpnState = &RPNState{ - vars: rpn.NewVariables().(*rpn.Variables), + vars: vars, + rpnCalc: rpn.NewRPN(vars), } } return rpnState @@ -76,6 +79,25 @@ func executor(input string) { return } + // Try evaluating as a single operator on the current RPN stack + // This allows incremental operations like: "1 2 +" then "+" + state := getRPNState() + fields := strings.Fields(input) + if len(fields) == 1 { + op := strings.ToLower(fields[0]) + // Check if it's a valid operator + switch op { + case "+", "-", "*", "/", "^", "%", "dup", "swap", "pop", "show", "clear", "vars": + result, err := state.rpnCalc.EvalOperator(op) + if err != nil { + fmt.Printf("Error: %v\n", err) + } else { + fmt.Println(result) + } + return + } + } + // Run the percentage calculation result, err := calculator.Parse(input) if err != nil { diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go index f99d1a6..0a76909 100644 --- a/internal/rpn/rpn.go +++ b/internal/rpn/rpn.go @@ -11,14 +11,16 @@ type RPN struct { vars *Variables ops *Operations maxStack int + currentStack *Stack } // NewRPN creates a new RPN parser and evaluator with the given variable store. func NewRPN(vars *Variables) *RPN { return &RPN{ - vars: vars, - ops: NewOperations(vars), - maxStack: 1000, // Reasonable limit for RPN expressions + vars: vars, + ops: NewOperations(vars), + maxStack: 1000, // Reasonable limit for RPN expressions + currentStack: NewStack(), } } @@ -30,6 +32,9 @@ func (r *RPN) ParseAndEvaluate(input string) (string, error) { return "", fmt.Errorf("empty expression") } + // Reset the stack for fresh evaluation (each call should be independent) + r.currentStack = NewStack() + // Handle single assignment: "name value =" // This is when the entire input is just an assignment if strings.Contains(input, " = ") { @@ -105,7 +110,12 @@ func tokenize(input string) []string { // evaluate evaluates a list of tokens and returns the result. func (r *RPN) evaluate(tokens []string) (string, error) { - stack := NewStack() + // Use the current stack for evaluation to preserve state + // This allows incremental operations in REPL mode + if r.currentStack == nil { + r.currentStack = NewStack() + } + stack := r.currentStack for i, token := range tokens { // Check for variable assignment: name value = @@ -203,6 +213,23 @@ func (r *RPN) evaluate(tokens []string) (string, error) { return result, nil } + // Save the current stack state for continued operations + // Create a copy of the stack to preserve it + r.currentStack = NewStack() + for _, val := range stack.Values() { + r.currentStack.Push(val) + } + + // Get the final result + if stack.Len() > 1 { + // Multiple values - show them all + result, err := r.ops.Show(stack) + if err != nil { + return "", fmt.Errorf("final result: %w", err) + } + return result, nil + } + // Single value - return it val, _ := stack.Pop() return fmt.Sprintf("%.10g", val), nil @@ -275,3 +302,70 @@ func (r *RPN) ResultStack(tokens []string) (string, error) { return r.ops.Show(stack) } + +// EvalOperator evaluates a single operator on the current stack state. +// This allows incremental RPN operations like: "1 2 +" then "+". +func (r *RPN) EvalOperator(op string) (string, error) { + if r.currentStack == nil { + r.currentStack = NewStack() + } + + switch op { + case "+": + if err := r.ops.Add(r.currentStack); err != nil { + return "", fmt.Errorf("operator +: %w", err) + } + case "-": + if err := r.ops.Subtract(r.currentStack); err != nil { + return "", fmt.Errorf("operator -: %w", err) + } + case "*": + if err := r.ops.Multiply(r.currentStack); err != nil { + return "", fmt.Errorf("operator *: %w", err) + } + case "/": + if err := r.ops.Divide(r.currentStack); err != nil { + return "", fmt.Errorf("operator /: %w", err) + } + case "^": + if err := r.ops.Power(r.currentStack); err != nil { + return "", fmt.Errorf("operator ^: %w", err) + } + case "%": + if err := r.ops.Modulo(r.currentStack); err != nil { + return "", fmt.Errorf("operator %%: %w", err) + } + case "dup": + if err := r.ops.Dup(r.currentStack); err != nil { + return "", fmt.Errorf("dup: %w", err) + } + case "swap": + if err := r.ops.Swap(r.currentStack); err != nil { + return "", fmt.Errorf("swap: %w", err) + } + case "pop": + if err := r.ops.Pop(r.currentStack); err != nil { + return "", fmt.Errorf("pop: %w", err) + } + case "show", "showstack", "print": + return r.ops.Show(r.currentStack) + case "clear": + r.ops.ClearVariables() + return "All variables cleared", nil + case "vars": + return r.ops.ListVariables() + default: + return "", fmt.Errorf("unknown operator '%s'", op) + } + + // Return the current stack state + return r.ops.Show(r.currentStack) +} + +// GetCurrentStack returns a copy of the current stack for inspection. +func (r *RPN) GetCurrentStack() []float64 { + if r.currentStack == nil { + return nil + } + return r.currentStack.Values() +} diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go index 46590b8..86bc628 100644 --- a/internal/rpn/rpn_test.go +++ b/internal/rpn/rpn_test.go @@ -653,3 +653,138 @@ func TestResultStackMultipleValues(t *testing.T) { }) } } + +func TestRPNIncrementalOperations(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 1 2 3 + then + + // First evaluate "1 2 3 +" + result, err := r.ParseAndEvaluate("1 2 3 +") + if err != nil { + t.Fatalf("First evaluation failed: %v", err) + } + if result != "1 5" { + t.Errorf("First result = %q, want '1 5'", result) + } + + // Then apply + operator + result, err = r.EvalOperator("+") + if err != nil { + t.Fatalf("EvalOperator('+') failed: %v", err) + } + if result != "6" { + t.Errorf("After + = %q, want '6'", result) + } +} + +func TestRPNIncrementalSubtract(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // First put two values on stack: "10 3" gives stack [10, 3] + _, err := r.ParseAndEvaluate("10 3") + if err != nil { + t.Fatalf("First evaluation failed: %v", err) + } + + // Now subtract + result, err := r.EvalOperator("-") + if err != nil { + t.Fatalf("EvalOperator('-') failed: %v", err) + } + // 10 - 3 = 7 + if result != "7" { + t.Errorf("After - = %q, want '7'", result) + } +} + +func TestRPNIncrementalDup(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // First push values (two values so stack is not emptied after evaluation) + _, err := r.ParseAndEvaluate("5 6") + if err != nil { + t.Fatalf("First evaluation failed: %v", err) + } + // After "5 6", stack should have [5, 6], result is "5 6" + + // Now duplicate + result, err := r.EvalOperator("dup") + if err != nil { + t.Fatalf("EvalOperator('dup') failed: %v", err) + } + if result != "5 6 6" { + t.Errorf("After dup = %q, want '5 6 6'", result) + } +} + +func TestRPNIncrementalSwap(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("1 2") + if err != nil { + t.Fatalf("First evaluation failed: %v", err) + } + + result, err := r.EvalOperator("swap") + if err != nil { + t.Fatalf("EvalOperator('swap') failed: %v", err) + } + if result != "2 1" { + t.Errorf("After swap = %q, want '2 1'", result) + } +} + +func TestRPNGetCurrentStack(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("1 2 3") + if err != nil { + t.Fatalf("First evaluation failed: %v", err) + } + + stack := r.GetCurrentStack() + if len(stack) != 3 { + t.Errorf("Stack length = %d, want 3", len(stack)) + } + if stack[0] != 1 || stack[1] != 2 || stack[2] != 3 { + t.Errorf("Stack = %v, want [1 2 3]", stack) + } +} + +func TestRPNIncrementalUnknownOperator(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("1 2") + if err != nil { + t.Fatalf("First evaluation failed: %v", err) + } + + _, err = r.EvalOperator("unknown") + if err == nil { + t.Error("EvalOperator('unknown') should return error") + } +} + +func TestRPNClearStack(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("1 2 3") + if err != nil { + t.Fatalf("First evaluation failed: %v", err) + } + + result, err := r.EvalOperator("clear") + if err != nil { + t.Fatalf("EvalOperator('clear') failed: %v", err) + } + if result != "All variables cleared" { + t.Errorf("After clear = %q, want 'All variables cleared'", result) + } +} |
