summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-20 22:16:39 +0200
committerPaul Buetow <paul@buetow.org>2026-03-20 22:16:39 +0200
commit59d8038a2188319640863ecad33ea855b2cf3c0e (patch)
tree1df458759107e533fe98914d1fb935ca9dabd6bd /internal
parent77c21d86ea4c9b8621af1dcbd64acec7b65706a3 (diff)
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 +'.
Diffstat (limited to 'internal')
-rw-r--r--internal/repl/repl.go5
-rw-r--r--internal/rpn/rpn.go6
-rw-r--r--internal/rpn/rpn_test.go35
-rw-r--r--internal/rpn/variables.go110
4 files changed, 75 insertions, 81 deletions
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]
-}