summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-20 22:04:48 +0200
committerPaul Buetow <paul@buetow.org>2026-03-20 22:04:48 +0200
commitc62aef166f1163524fe5d63a5c00571a75731dcf (patch)
tree866a160fa47481bb4c9cae8adf1fa790e5955373 /internal
parent3fb1a395ce147d364c811842ca87935caf75517a (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.go26
-rw-r--r--internal/rpn/rpn.go102
-rw-r--r--internal/rpn/rpn_test.go135
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)
+ }
+}