From 59d8038a2188319640863ecad33ea855b2cf3c0e Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 20 Mar 2026 22:16:39 +0200 Subject: feat: Implement persistent RPN state for all RPN-related input - Modified ParseAndEvaluate() to not reset stack at beginning, only initialize if nil - Changed runRPN() to use getRPNState() for persistent state across calls - Fixed Stack type definition ordering in variables.go to follow Go best practices - Updated tests to create fresh RPN instances where independence is needed This enables REPL-style incremental RPN calculations like '3 4 +' followed by '5 +'. --- internal/repl/repl.go | 5 +-- internal/rpn/rpn.go | 6 ++- internal/rpn/rpn_test.go | 35 ++++++--------- internal/rpn/variables.go | 110 +++++++++++++++++++++++----------------------- 4 files changed, 75 insertions(+), 81 deletions(-) (limited to 'internal') diff --git a/internal/repl/repl.go b/internal/repl/repl.go index f79f92f..3e2d4c9 100644 --- a/internal/repl/repl.go +++ b/internal/repl/repl.go @@ -109,9 +109,8 @@ func executor(input string) { // runRPN parses and evaluates an RPN expression func runRPN(input string) (string, error) { - vars := rpn.NewVariables() - rpnCalc := rpn.NewRPN(vars) - return rpnCalc.ParseAndEvaluate(input) + state := getRPNState() + return state.rpnCalc.ParseAndEvaluate(input) } // isBuiltinCommand checks if input starts with a built-in command diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go index dce3199..07a5fb4 100644 --- a/internal/rpn/rpn.go +++ b/internal/rpn/rpn.go @@ -32,8 +32,10 @@ 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() + // Initialize stack if nil (first use) + if r.currentStack == nil { + r.currentStack = NewStack() + } // Handle single assignment: "name value =" // This is when the entire input is just an assignment diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go index 86bc628..aa285ac 100644 --- a/internal/rpn/rpn_test.go +++ b/internal/rpn/rpn_test.go @@ -59,9 +59,6 @@ func TestTokenize(t *testing.T) { } func TestParseAndEvaluateSimple(t *testing.T) { - v := NewVariables().(*Variables) - r := NewRPN(v) - tests := []struct { name string input string @@ -101,6 +98,8 @@ func TestParseAndEvaluateSimple(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) result, err := r.ParseAndEvaluate(tt.input) if err != nil { t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) @@ -113,9 +112,6 @@ func TestParseAndEvaluateSimple(t *testing.T) { } func TestParseAndEvaluateChain(t *testing.T) { - v := NewVariables().(*Variables) - r := NewRPN(v) - tests := []struct { name string input string @@ -140,6 +136,8 @@ func TestParseAndEvaluateChain(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) result, err := r.ParseAndEvaluate(tt.input) if err != nil { t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) @@ -152,9 +150,6 @@ func TestParseAndEvaluateChain(t *testing.T) { } func TestParseAndEvaluateStackOps(t *testing.T) { - v := NewVariables().(*Variables) - r := NewRPN(v) - tests := []struct { name string input string @@ -179,6 +174,8 @@ func TestParseAndEvaluateStackOps(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) result, err := r.ParseAndEvaluate(tt.input) if err != nil { t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) @@ -228,9 +225,6 @@ func TestParseAndEvaluateEmpty(t *testing.T) { } func TestParseAndEvaluateAssignment(t *testing.T) { - v := NewVariables().(*Variables) - r := NewRPN(v) - // Test assignment format: "varname = value" tests := []struct { name string @@ -251,6 +245,8 @@ func TestParseAndEvaluateAssignment(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) result, err := r.ParseAndEvaluate(tt.input) if err != nil { t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) @@ -420,9 +416,6 @@ func TestResultStackEmpty(t *testing.T) { } func TestParseAndEvaluateAssignmentExpression(t *testing.T) { - v := NewVariables().(*Variables) - r := NewRPN(v) - // Test assignment with expression: "name value = expression..." tests := []struct { name string @@ -448,6 +441,8 @@ func TestParseAndEvaluateAssignmentExpression(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) result, err := r.ParseAndEvaluate(tt.input) if err != nil { t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) @@ -460,9 +455,6 @@ func TestParseAndEvaluateAssignmentExpression(t *testing.T) { } func TestParseAndEvaluateAssignmentErrors(t *testing.T) { - v := NewVariables().(*Variables) - r := NewRPN(v) - // Test error cases in assignment handling tests := []struct { name string @@ -493,6 +485,8 @@ func TestParseAndEvaluateAssignmentErrors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) _, err := r.ParseAndEvaluate(tt.input) if err == nil { t.Errorf("ParseAndEvaluate(%q) expected error, got nil", tt.input) @@ -506,9 +500,6 @@ func TestParseAndEvaluateAssignmentErrors(t *testing.T) { } func TestParseAndEvaluateEvaluateErrors(t *testing.T) { - v := NewVariables().(*Variables) - r := NewRPN(v) - // Test error cases in evaluate function tests := []struct { name string @@ -544,6 +535,8 @@ func TestParseAndEvaluateEvaluateErrors(t *testing.T) { continue } t.Run(tt.name, func(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) _, err := r.ParseAndEvaluate(tt.input) if err == nil { t.Errorf("ParseAndEvaluate(%q) expected error, got nil", tt.input) diff --git a/internal/rpn/variables.go b/internal/rpn/variables.go index 91b7ef7..46efa2c 100644 --- a/internal/rpn/variables.go +++ b/internal/rpn/variables.go @@ -13,6 +13,61 @@ var ( ErrInvalidVariableName = fmt.Errorf("invalid variable name") ) +// Stack represents a simple float64 stack for RPN calculations. +type Stack struct { + values []float64 +} + +// NewStack creates a new empty stack. +func NewStack() *Stack { + return &Stack{ + values: make([]float64, 0), + } +} + +// Push adds a value to the top of the stack. +func (s *Stack) Push(val float64) { + s.values = append(s.values, val) +} + +// Pop removes and returns the top value from the stack. +// Returns an error if the stack is empty. +func (s *Stack) Pop() (float64, error) { + if len(s.values) == 0 { + return 0, fmt.Errorf("stack is empty") + } + + val := s.values[len(s.values)-1] + s.values = s.values[:len(s.values)-1] + return val, nil +} + +// Peek returns the top value without removing it. +// Returns an error if the stack is empty. +func (s *Stack) Peek() (float64, error) { + if len(s.values) == 0 { + return 0, fmt.Errorf("stack is empty") + } + return s.values[len(s.values)-1], nil +} + +// Len returns the number of values on the stack. +func (s *Stack) Len() int { + return len(s.values) +} + +// Values returns a copy of all stack values (top-to-bottom order). +func (s *Stack) Values() []float64 { + vals := make([]float64, len(s.values)) + copy(vals, s.values) + return vals +} + +// Clear removes all values from the stack. +func (s *Stack) Clear() { + s.values = s.values[:0] +} + // Variables stores variable name-value pairs for RPN calculations. // It provides thread-safe access to variable storage. type Variables struct { @@ -142,58 +197,3 @@ func (v *Variables) HasVariable(name string) bool { _, exists := v.variables[name] return exists } - -// Stack represents a simple float64 stack for RPN calculations. -type Stack struct { - values []float64 -} - -// NewStack creates a new empty stack. -func NewStack() *Stack { - return &Stack{ - values: make([]float64, 0), - } -} - -// Push adds a value to the top of the stack. -func (s *Stack) Push(val float64) { - s.values = append(s.values, val) -} - -// Pop removes and returns the top value from the stack. -// Returns an error if the stack is empty. -func (s *Stack) Pop() (float64, error) { - if len(s.values) == 0 { - return 0, fmt.Errorf("stack is empty") - } - - val := s.values[len(s.values)-1] - s.values = s.values[:len(s.values)-1] - return val, nil -} - -// Peek returns the top value without removing it. -// Returns an error if the stack is empty. -func (s *Stack) Peek() (float64, error) { - if len(s.values) == 0 { - return 0, fmt.Errorf("stack is empty") - } - return s.values[len(s.values)-1], nil -} - -// Len returns the number of values on the stack. -func (s *Stack) Len() int { - return len(s.values) -} - -// Values returns a copy of all stack values (top-to-bottom order). -func (s *Stack) Values() []float64 { - vals := make([]float64, len(s.values)) - copy(vals, s.values) - return vals -} - -// Clear removes all values from the stack. -func (s *Stack) Clear() { - s.values = s.values[:0] -} -- cgit v1.2.3