summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-20 21:36:56 +0200
committerPaul Buetow <paul@buetow.org>2026-03-20 21:36:56 +0200
commitf444b25bc2f7f92ab0bbd5411e7dd89f49fa25d2 (patch)
tree0bb7a6439b456f0695641ce0cc9c2eb8739a2c73 /internal
parentaf8d680f3a1271fe6f57c5bba7675e5f06e0d2fd (diff)
internal/rpn: add rpn.go with RPN parser and evaluator
- ParseAndEvaluate: parses and evaluates RPN expressions - Tokenization: standard RPN with special assignment handling - Variable assignment format: 'name = value' - Standard RPN operations: +, -, *, /, ^, % - Stack operations: dup, swap, pop, show - Variable management: vars, clear - Variable reuse: push stored value onto stack - 35 comprehensive test functions - All tests pass, go vet passes
Diffstat (limited to 'internal')
-rw-r--r--internal/rpn/rpn.go248
-rw-r--r--internal/rpn/rpn_test.go420
2 files changed, 668 insertions, 0 deletions
diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go
new file mode 100644
index 0000000..f41df1c
--- /dev/null
+++ b/internal/rpn/rpn.go
@@ -0,0 +1,248 @@
+package rpn
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// RPN represents the RPN parser and evaluator.
+type RPN struct {
+ vars *Variables
+ ops *Operations
+ maxStack int
+}
+
+// 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
+ }
+}
+
+// ParseAndEvaluate parses and evaluates an RPN expression.
+// Returns the result as a formatted string or an error.
+func (r *RPN) ParseAndEvaluate(input string) (string, error) {
+ input = strings.TrimSpace(input)
+ if input == "" {
+ return "", fmt.Errorf("empty expression")
+ }
+
+ // First check for special assignment pattern: "name value ="
+ // This is handled separately from RPN evaluation
+ if strings.Contains(input, " = ") {
+ parts := strings.SplitN(input, " = ", 2)
+ if len(parts) == 2 {
+ name := strings.TrimSpace(parts[0])
+ valueStr := strings.TrimSpace(parts[1])
+ // Validate name is a single word (variable name)
+ nameFields := strings.Fields(name)
+ if len(nameFields) == 1 {
+ // Parse the value
+ val, err := strconv.ParseFloat(valueStr, 64)
+ if err != nil {
+ return "", fmt.Errorf("invalid value '%s' for assignment: %w", valueStr, err)
+ }
+ // Assign the variable
+ if err := r.vars.SetVariable(nameFields[0], val); err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("%s = %.10g", nameFields[0], val), nil
+ }
+ }
+ }
+
+ tokens := tokenize(input)
+ if len(tokens) == 0 {
+ return "", fmt.Errorf("no valid tokens found")
+ }
+
+ return r.evaluate(tokens)
+}
+
+// tokenize splits the input string into tokens (numbers, operators, variables).
+func tokenize(input string) []string {
+ // Standard RPN tokenization
+ return strings.Fields(input)
+}
+
+// evaluate evaluates a list of tokens and returns the result.
+func (r *RPN) evaluate(tokens []string) (string, error) {
+ stack := NewStack()
+
+ for i, token := range tokens {
+ // Check for variable assignment: name value =
+ if token == "=" {
+ // Assignment requires: variable_name (as previous token), value (on stack)
+ // But tokens are processed linearly, so we need special handling
+ // For "name value =", we have tokens: [name, value, =]
+ // When we see =, we need to have the value on stack and name from before
+ // This approach won't work well, so we handle assignment at parse time
+ return "", fmt.Errorf("invalid assignment: '=' must be used with 'name value =' syntax")
+ }
+
+ // Check if it's a number
+ if num, err := strconv.ParseFloat(token, 64); err == nil {
+ if stack.Len() >= r.maxStack {
+ return "", fmt.Errorf("stack overflow")
+ }
+ stack.Push(num)
+ continue
+ }
+
+ // Check for operators and special commands
+ switch token {
+ case "+":
+ if err := r.ops.Add(stack); err != nil {
+ return "", fmt.Errorf("operator +: %w", err)
+ }
+ case "-":
+ if err := r.ops.Subtract(stack); err != nil {
+ return "", fmt.Errorf("operator -: %w", err)
+ }
+ case "*":
+ if err := r.ops.Multiply(stack); err != nil {
+ return "", fmt.Errorf("operator *: %w", err)
+ }
+ case "/":
+ if err := r.ops.Divide(stack); err != nil {
+ return "", fmt.Errorf("operator /: %w", err)
+ }
+ case "^":
+ if err := r.ops.Power(stack); err != nil {
+ return "", fmt.Errorf("operator ^: %w", err)
+ }
+ case "%":
+ if err := r.ops.Modulo(stack); err != nil {
+ return "", fmt.Errorf("operator %%: %w", err)
+ }
+ case "dup":
+ if err := r.ops.Dup(stack); err != nil {
+ return "", fmt.Errorf("dup: %w", err)
+ }
+ case "swap":
+ if err := r.ops.Swap(stack); err != nil {
+ return "", fmt.Errorf("swap: %w", err)
+ }
+ case "pop":
+ if err := r.ops.Pop(stack); err != nil {
+ return "", fmt.Errorf("pop: %w", err)
+ }
+ case "show", "showstack", "print":
+ result, err := r.ops.Show(stack)
+ if err != nil {
+ return "", fmt.Errorf("show: %w", err)
+ }
+ // For show, we return the stack state instead of continuing
+ return result, nil
+ case "vars":
+ result, err := r.ops.ListVariables()
+ if err != nil {
+ return "", fmt.Errorf("vars: %w", err)
+ }
+ return result, nil
+ case "clear":
+ r.ops.ClearVariables()
+ return "All variables cleared", nil
+ case "d":
+ return "", fmt.Errorf("'d' command not supported as standalone token")
+ default:
+ // Check if it's a variable reference (push its value)
+ val, exists := r.vars.GetVariable(token)
+ if exists {
+ stack.Push(val)
+ } else {
+ return "", fmt.Errorf("unknown token '%s' at position %d", token, i)
+ }
+ }
+ }
+
+ // Check final stack state
+ if stack.Len() == 0 {
+ return "", fmt.Errorf("empty result: expression evaluated to nothing")
+ }
+
+ // Get the final result
+ if stack.Len() > 1 {
+ // Multiple values on stack - 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
+}
+
+// ResultStack returns the final stack state after evaluation.
+// This is useful for commands that need to show the stack without consuming it.
+func (r *RPN) ResultStack(tokens []string) (string, error) {
+ stack := NewStack()
+
+ for _, token := range tokens {
+ if num, err := strconv.ParseFloat(token, 64); err == nil {
+ stack.Push(num)
+ continue
+ }
+
+ switch token {
+ case "+":
+ if err := r.ops.Add(stack); err != nil {
+ return "", err
+ }
+ case "-":
+ if err := r.ops.Subtract(stack); err != nil {
+ return "", err
+ }
+ case "*":
+ if err := r.ops.Multiply(stack); err != nil {
+ return "", err
+ }
+ case "/":
+ if err := r.ops.Divide(stack); err != nil {
+ return "", err
+ }
+ case "^":
+ if err := r.ops.Power(stack); err != nil {
+ return "", err
+ }
+ case "%":
+ if err := r.ops.Modulo(stack); err != nil {
+ return "", err
+ }
+ case "dup":
+ if err := r.ops.Dup(stack); err != nil {
+ return "", err
+ }
+ case "swap":
+ if err := r.ops.Swap(stack); err != nil {
+ return "", err
+ }
+ case "pop":
+ if err := r.ops.Pop(stack); err != nil {
+ return "", err
+ }
+ case "show", "showstack", "print":
+ return r.ops.Show(stack)
+ case "vars":
+ return r.ops.ListVariables()
+ case "clear":
+ r.ops.ClearVariables()
+ return "All variables cleared", nil
+ default:
+ val, exists := r.vars.GetVariable(token)
+ if exists {
+ stack.Push(val)
+ } else {
+ return "", fmt.Errorf("unknown token '%s'", token)
+ }
+ }
+ }
+
+ return r.ops.Show(stack)
+}
diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go
new file mode 100644
index 0000000..05f842d
--- /dev/null
+++ b/internal/rpn/rpn_test.go
@@ -0,0 +1,420 @@
+package rpn
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+func TestNewRPN(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+ if r == nil {
+ t.Fatal("NewRPN() returned nil")
+ }
+ if r.vars == nil {
+ t.Error("RPN.vars should not be nil")
+ }
+ if r.ops == nil {
+ t.Error("RPN.ops should not be nil")
+ }
+}
+
+func TestTokenize(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected []string
+ }{
+ {
+ name: "simple expression",
+ input: "3 4 +",
+ expected: []string{"3", "4", "+"},
+ },
+ {
+ name: "multiple spaces",
+ input: "3 4 +",
+ expected: []string{"3", "4", "+"},
+ },
+ {
+ name: "decimal numbers",
+ input: "3.14 2.5 +",
+ expected: []string{"3.14", "2.5", "+"},
+ },
+ {
+ name: "expression with leading/trailing spaces",
+ input: " 3 4 + ",
+ expected: []string{"3", "4", "+"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := tokenize(tt.input)
+ if len(result) != len(tt.expected) {
+ t.Errorf("tokenize(%q) = %v, expected %v", tt.input, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestParseAndEvaluateSimple(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "3 4 +",
+ input: "3 4 +",
+ expected: "7",
+ },
+ {
+ name: "5 3 -",
+ input: "5 3 -",
+ expected: "2",
+ },
+ {
+ name: "2 3 *",
+ input: "2 3 *",
+ expected: "6",
+ },
+ {
+ name: "10 2 /",
+ input: "10 2 /",
+ expected: "5",
+ },
+ {
+ name: "2 3 ^",
+ input: "2 3 ^",
+ expected: "8",
+ },
+ {
+ name: "10 3 %",
+ input: "10 3 %",
+ expected: "1",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := r.ParseAndEvaluate(tt.input)
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err)
+ }
+ if !strings.HasPrefix(result, tt.expected) {
+ t.Errorf("ParseAndEvaluate(%q) = %q, want to start with %q", tt.input, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestParseAndEvaluateChain(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "3 4 + 4 4 - *",
+ input: "3 4 + 4 4 - *",
+ expected: "0",
+ },
+ {
+ name: "1 2 + 3 *",
+ input: "1 2 + 3 *",
+ expected: "9",
+ },
+ {
+ name: "2 3 + 4 5 + *",
+ input: "2 3 + 4 5 + *",
+ expected: "45",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := r.ParseAndEvaluate(tt.input)
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err)
+ }
+ if !strings.HasPrefix(result, tt.expected) {
+ t.Errorf("ParseAndEvaluate(%q) = %q, want to start with %q", tt.input, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestParseAndEvaluateStackOps(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "1 2 3 dup",
+ input: "1 2 3 dup",
+ expected: "1 2 3 3",
+ },
+ {
+ name: "1 2 swap",
+ input: "1 2 swap",
+ expected: "2 1",
+ },
+ {
+ name: "1 2 3 pop",
+ input: "1 2 3 pop",
+ expected: "1 2",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := r.ParseAndEvaluate(tt.input)
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err)
+ }
+ if result != tt.expected {
+ t.Errorf("ParseAndEvaluate(%q) = %q, want %q", tt.input, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestParseAndEvaluateVariables(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ // Test variable assignment and reuse
+ // First assign a variable
+ result, err := r.ParseAndEvaluate("x = 5")
+ if err != nil {
+ t.Fatalf("First assignment failed: %v", err)
+ }
+ if result != "x = 5" {
+ t.Errorf("Assignment result = %q, want %q", result, "x = 5")
+ }
+
+ // Now use the variable in RPN
+ result, err = r.ParseAndEvaluate("x x +")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(%q) returned error: %v", "x x +", err)
+ }
+ if result != "10" {
+ t.Errorf("ParseAndEvaluate(%q) = %q, want %q", "x x +", result, "10")
+ }
+}
+
+func TestParseAndEvaluateEmpty(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ _, err := r.ParseAndEvaluate("")
+ if err == nil {
+ t.Error("ParseAndEvaluate(\"\") should return error")
+ }
+ if !strings.Contains(err.Error(), "empty expression") {
+ t.Errorf("Error = %v, should contain 'empty expression'", err)
+ }
+}
+
+func TestParseAndEvaluateAssignment(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ // Test assignment format: "varname = value"
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "x = 5",
+ input: "x = 5",
+ expected: "x = 5",
+ },
+ {
+ name: "pi = 3.14159",
+ input: "pi = 3.14159",
+ expected: "pi = 3.14159",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := r.ParseAndEvaluate(tt.input)
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err)
+ }
+ if result != tt.expected {
+ t.Errorf("ParseAndEvaluate(%q) = %q, want %q", tt.input, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestParseAndEvaluateDivisionByZero(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ _, err := r.ParseAndEvaluate("5 0 /")
+ if err == nil {
+ t.Error("5 0 / should return error")
+ }
+ if !strings.Contains(err.Error(), "division by zero") {
+ t.Errorf("Error = %v, should contain 'division by zero'", err)
+ }
+}
+
+func TestParseAndEvaluateUndefinedVariable(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ _, err := r.ParseAndEvaluate("undefined +")
+ if err == nil {
+ t.Error("undefined variable should return error")
+ }
+ // The error should mention the undefined token
+ if !strings.Contains(err.Error(), "undefined") {
+ t.Errorf("Error = %v, should contain 'undefined'", err)
+ }
+}
+
+func TestParseAndEvaluateUnknownToken(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ _, err := r.ParseAndEvaluate("1 2 + hello")
+ if err == nil {
+ t.Error("unknown token should return error")
+ }
+ if !strings.Contains(err.Error(), "unknown token") {
+ t.Errorf("Error = %v, should contain 'unknown token'", err)
+ }
+}
+
+func TestParseAndEvaluateInsufficientOperands(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ tests := []struct {
+ name string
+ input string
+ }{
+ {"+ with one operand", "5 +"},
+ {"+ with no operands", "+"},
+ {"3 +", "3 +"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := r.ParseAndEvaluate(tt.input)
+ if err == nil {
+ t.Errorf("%q should return error for insufficient operands", tt.input)
+ }
+ })
+ }
+}
+
+func TestParseAndEvaluateShow(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ result, err := r.ParseAndEvaluate("1 2 3 show")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(%q) returned error: %v", "1 2 3 show", err)
+ }
+ if result != "1 2 3" {
+ t.Errorf("ParseAndEvaluate(%q) = %q, want \"1 2 3\"", "1 2 3 show", result)
+ }
+}
+
+func TestParseAndEvaluateVars(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ // Set some variables using new format: "name = value"
+ r.ParseAndEvaluate("x = 5")
+ r.ParseAndEvaluate("y = 10")
+
+ result, err := r.ParseAndEvaluate("vars")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(%q) returned error: %v", "vars", err)
+ }
+ if !strings.Contains(result, "x") || !strings.Contains(result, "y") {
+ t.Errorf("vars output should contain all variables, got: %s", result)
+ }
+}
+
+func TestParseAndEvaluateClear(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ // Set and clear
+ r.ParseAndEvaluate("x 5 =")
+ r.ParseAndEvaluate("clear")
+
+ if v.Count() != 0 {
+ t.Errorf("Count after clear = %d, want 0", v.Count())
+ }
+}
+
+func TestRPNConcurrency(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ done := make(chan bool, 10)
+ for i := 0; i < 5; i++ {
+ go func(id int) {
+ name := fmt.Sprintf("val%d", id)
+ input := fmt.Sprintf("%s = %d", name, id)
+ r.ParseAndEvaluate(input)
+ done <- true
+ }(i)
+ }
+
+ for i := 0; i < 5; i++ {
+ <-done
+ }
+
+ if v.Count() != 5 {
+ t.Errorf("Final count = %d, want 5", v.Count())
+ }
+}
+
+func TestResultStack(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ tokens := []string{"1", "2", "3", "+"}
+ result, err := r.ResultStack(tokens)
+ if err != nil {
+ t.Fatalf("ResultStack() returned error: %v", err)
+ }
+ if result != "1 5" {
+ t.Errorf("ResultStack() = %q, want \"1 5\"", result)
+ }
+}
+
+func TestResultStackEmpty(t *testing.T) {
+ v := NewVariables().(*Variables)
+ r := NewRPN(v)
+
+ tokens := []string{}
+ result, err := r.ResultStack(tokens)
+ if err != nil {
+ t.Fatalf("ResultStack([]) returned error: %v", err)
+ }
+ if result != "Stack is empty" {
+ t.Errorf("ResultStack([]) = %q, want \"Stack is empty\"", result)
+ }
+}