From 18b0017e2603436dd418b8866279445ff45ad6fb Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 25 Mar 2026 22:33:42 +0200 Subject: rpn: Add := and =: assignment operators with = synonym --- internal/rpn/number.go | 38 ++++++++++++++++ internal/rpn/operations.go | 49 ++++++++++++++------- internal/rpn/operations_test.go | 85 ++++++++++++++++++++++++++++++++++++ internal/rpn/rpn_parse.go | 94 +++++++++++++++++++++++++++++++++++++--- internal/rpn/rpn_test.go | 65 +++++++++++++++++++++++++++ main | Bin 3425628 -> 0 bytes 6 files changed, 310 insertions(+), 21 deletions(-) delete mode 100755 main diff --git a/internal/rpn/number.go b/internal/rpn/number.go index c25b483..677f4c7 100644 --- a/internal/rpn/number.go +++ b/internal/rpn/number.go @@ -322,3 +322,41 @@ func ToRat(n Number) *big.Rat { func ToFloat(n Number) float64 { return n.Float64() } + +// StringNum represents a string value on the stack for variable names. +type StringNum struct { + value string +} + +// NewStringNum creates a new StringNum from a string. +func NewStringNum(s string) *StringNum { + return &StringNum{value: s} +} + +// String returns the string representation. +func (s *StringNum) String() string { + return s.value +} + +// Float64 returns 0 for string numbers (not numeric). +func (s *StringNum) Float64() float64 { + panic("string not supported for Float64()") +} + +// IsString returns true for StringNum. +func (s *StringNum) IsString() bool { + return true +} + +// Other methods panic as they're not supported for strings +func (s *StringNum) Add(other Number) Number { panic("string not supported for addition") } +func (s *StringNum) Sub(other Number) Number { panic("string not supported for subtraction") } +func (s *StringNum) Mul(other Number) Number { panic("string not supported for multiplication") } +func (s *StringNum) Div(other Number) (Number, error) { panic("string not supported for division") } +func (s *StringNum) Pow(other Number) Number { panic("string not supported for power") } +func (s *StringNum) Mod(other Number) (Number, error) { panic("string not supported for modulo") } +func (s *StringNum) IsZero() bool { return false } +func (s *StringNum) IsNegative() bool { return false } +func (s *StringNum) Compare(other Number) int { panic("string not supported for comparison") } +func (s *StringNum) Bool() bool { panic("string not supported for Bool()") } +func (s *StringNum) IsBool() bool { panic("string not supported for IsBool()") } diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go index 81ea78e..ef6bcd6 100644 --- a/internal/rpn/operations.go +++ b/internal/rpn/operations.go @@ -57,7 +57,8 @@ type StackOperator interface { type VariableOperator interface { ListVariables() (string, error) ClearVariables() - AssignVariableFromStack(stack *Stack) error + AssignLeft(stack *Stack) error + AssignRight(stack *Stack) error } // Operator is the combined interface for all operator implementations. @@ -70,6 +71,10 @@ type Operator interface { VariableOperator // SetMode sets the calculation mode for number formatting SetMode(CalculationMode) + // AssignLeft assigns a value to a variable (for := operator) + AssignLeft(stack *Stack) error + // AssignRight assigns a value to a variable (for =: operator) + AssignRight(stack *Stack) error } // Operations provides operator implementations and stack manipulation. @@ -148,7 +153,9 @@ func NewOperatorRegistry(op Operator) *OperatorRegistry { registry.registerStandardOperator("==", func(stack *Stack) error { return op.EQ(stack) }) registry.registerStandardOperator("neq", func(stack *Stack) error { return op.NEQ(stack) }) registry.registerStandardOperator("!=", func(stack *Stack) error { return op.NEQ(stack) }) - registry.registerStandardOperator("=", func(stack *Stack) error { return op.AssignVariableFromStack(stack) }) + registry.registerStandardOperator("=", func(stack *Stack) error { return op.AssignLeft(stack) }) + registry.registerStandardOperator(":=", func(stack *Stack) error { return op.AssignLeft(stack) }) + registry.registerStandardOperator("=:", func(stack *Stack) error { return op.AssignRight(stack) }) registry.registerStandardOperator("dup", func(stack *Stack) error { return op.Dup(stack) }) registry.registerStandardOperator("swap", func(stack *Stack) error { return op.Swap(stack) }) registry.registerStandardOperator("pop", func(stack *Stack) error { return op.Pop(stack) }) @@ -941,31 +948,41 @@ func (o *Operations) ClearVariables() { o.vars.ClearVariables() } -// AssignVariableFromStack assigns a value from the stack to a variable. -// It pops the variable name from the stack first, then pops the value. -// Usage: `name value =` or `x value =` (where x is on stack as a string) -func (o *Operations) AssignVariableFromStack(stack *Stack) error { - if stack.Len() < 1 { +// AssignLeft assigns a value to a variable (for := and = operators). +// Pops variable name from stack, pops value from stack, assigns value to name. +// For := operator, the stack order is: value name := (value on bottom, name on top). +// This function pops name first (top of stack), then value. +// Usage: `value name :=` +func (o *Operations) AssignLeft(stack *Stack) error { + name, err := stack.Pop() + if err != nil { return fmt.Errorf("insufficient operands for assignment: need variable name") } - nameVal, err := stack.Pop() + val, err := stack.Pop() if err != nil { - return err + return fmt.Errorf("insufficient operands for assignment: need value") } - // Get the variable name from the popped value - name := nameVal.String() + return o.vars.SetVariable(name.String(), val.Float64()) +} - if stack.Len() < 1 { + +// AssignRight assigns a value to a variable (for =: operator). +// Pops value from stack first, then pops variable name. +// For =: operator, the stack order is: name value =: (name on bottom, value on top). +// This function pops value first (top of stack), then name. +// Usage: `name value =:` +func (o *Operations) AssignRight(stack *Stack) error { + val, err := stack.Pop() + if err != nil { return fmt.Errorf("insufficient operands for assignment: need value") } - val, err := stack.Pop() + name, err := stack.Pop() if err != nil { - return err + return fmt.Errorf("insufficient operands for assignment: need variable name") } - // Convert to float64 for variable storage - return o.vars.SetVariable(name, val.Float64()) + return o.vars.SetVariable(name.String(), val.Float64()) } diff --git a/internal/rpn/operations_test.go b/internal/rpn/operations_test.go index 0e52bb0..25ba352 100644 --- a/internal/rpn/operations_test.go +++ b/internal/rpn/operations_test.go @@ -1011,3 +1011,88 @@ func TestOperatorRegistryHandleStandardOperator(t *testing.T) { }) } } + + +func TestAssignLeft(t *testing.T) { + v := NewVariables() + o := NewOperations(v) + s := NewStack() + + // For "5 x :=": + // Stack order is: value name := (value on bottom, name on top) + // Push value first (will be popped second), then name (will be popped first) + s.Push(NewNumber(5, FloatMode)) // value + s.Push(NewStringNum("x")) // name + + err := o.AssignLeft(s) + if err != nil { + t.Errorf("AssignLeft() error = %v", err) + } + + // Check that x = 5 + val, exists := v.GetVariable("x") + if !exists { + t.Errorf("Variable x should exist after assignment") + } + if val != 5 { + t.Errorf("Variable x = %v, want 5", val) + } + + // Stack should be empty + if s.Len() != 0 { + t.Errorf("Stack length = %d, want 0", s.Len()) + } +} + +func TestAssignRight(t *testing.T) { + v := NewVariables() + o := NewOperations(v) + s := NewStack() + + // For "x 5 =:": + // Stack order is: name value =: (name on bottom, value on top) + // Push name first (will be popped second), then value (will be popped first) + s.Push(NewStringNum("x")) // name + s.Push(NewNumber(5, FloatMode)) // value + + err := o.AssignRight(s) + if err != nil { + t.Errorf("AssignRight() error = %v", err) + } + + // Check that x = 5 + val, exists := v.GetVariable("x") + if !exists { + t.Errorf("Variable x should exist after assignment") + } + if val != 5 { + t.Errorf("Variable x = %v, want 5", val) + } + + // Stack should be empty + if s.Len() != 0 { + t.Errorf("Stack length = %d, want 0", s.Len()) + } +} + +func TestAssignLeftErrorCases(t *testing.T) { + v := NewVariables() + o := NewOperations(v) + s := NewStack() + + err := o.AssignLeft(s) + if err == nil { + t.Error("AssignLeft() should return error when stack is empty") + } +} + +func TestAssignRightErrorCases(t *testing.T) { + v := NewVariables() + o := NewOperations(v) + s := NewStack() + + err := o.AssignRight(s) + if err == nil { + t.Error("AssignRight() should return error when stack is empty") + } +} diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go index c755150..1a033f2 100644 --- a/internal/rpn/rpn_parse.go +++ b/internal/rpn/rpn_parse.go @@ -39,12 +39,12 @@ func (r *RPN) ParseAndEvaluate(input string) (string, error) { return "", fmt.Errorf("rpn: no valid tokens found in input: %q", input) } - return r.evaluate(tokens) + return r.evaluate(input, tokens) } // evaluate evaluates a list of tokens and returns the result. // This method is thread-safe for concurrent execution. -func (r *RPN) evaluate(tokens []string) (string, error) { +func (r *RPN) evaluate(input string, tokens []string) (string, error) { r.mu.Lock() defer r.mu.Unlock() @@ -80,6 +80,29 @@ func (r *RPN) evaluate(tokens []string) (string, error) { continue } + // Check if this is a variable name for assignment (:= or =:) + // For := (left assignment): value name := - peek ahead to see if next token is := or =: + // For =: (right assignment): name value =: - first token is always a variable name + shouldPushName := false + if i+1 < len(tokens) { + nextToken := tokens[i+1] + if nextToken == ":=" || nextToken == "=:" { + // This token is a variable name (for := case) + shouldPushName = true + } + } + + // Special case: first token in =: expression (e.g., "x 5 =:") + if i == 0 && len(tokens) >= 3 && tokens[len(tokens)-1] == "=:" { + shouldPushName = true + } + + if shouldPushName { + // This token is a variable name, push as StringNum + stack.Push(NewStringNum(token)) + continue + } + // Handle special operators and commands if result, err := r.handleOperator(stack, token, i); err != nil { return "", fmt.Errorf("rpn: failed to handle operator '%s' at position %d: %w", token, i, err) @@ -90,6 +113,12 @@ func (r *RPN) evaluate(tokens []string) (string, error) { // Check final stack state if stack.Len() == 0 { + // Empty stack might be valid for assignment operators (:= or =:) + // Check if the input was an assignment expression + if strings.Contains(input, ":=") || strings.Contains(input, "=:") { + // Assignment expression - empty stack is valid (side effect is variable assignment) + return "", nil + } return "", fmt.Errorf("empty result: expression evaluated to nothing") } @@ -141,8 +170,63 @@ func (r *RPN) handleOperator(stack *Stack, token string, tokenIndex int) (string // handleAssignment checks if the input is an assignment format and handles it. // Returns (result string, isAssignment bool, error error). func (r *RPN) handleAssignment(input string) (string, bool, error) { - // Check for assignment format (name = value or name value = expression) - // We look for either " = " (with trailing space) or " =" (just space before equals) + // Handle := operator + if strings.Contains(input, ":=") { + pos := strings.Index(input, ":=") + if pos >= 0 { + before := strings.TrimSpace(input[:pos]) + after := strings.TrimSpace(input[pos+2:]) + + beforeFields := strings.Fields(before) + if len(beforeFields) == 2 { + // value name := (for := operator) + name := beforeFields[1] + valueStr := beforeFields[0] + + val, err := strconv.ParseFloat(valueStr, 64) + if err == nil { + if err := r.vars.SetVariable(name, val); err != nil { + return "", false, err + } + if after == "" { + return fmt.Sprintf("%s = %.10g", name, val), true, nil + } + result, err := r.evaluate(input, strings.Fields(after)) + return result, true, err + } + } + } + } + + // Handle =: operator + if strings.Contains(input, "=:") { + pos := strings.Index(input, "=:") + if pos >= 0 { + before := strings.TrimSpace(input[:pos]) + after := strings.TrimSpace(input[pos+2:]) + + beforeFields := strings.Fields(before) + if len(beforeFields) == 2 { + // name value =: (for =: operator) + name := beforeFields[0] + valueStr := beforeFields[1] + + val, err := strconv.ParseFloat(valueStr, 64) + if err == nil { + if err := r.vars.SetVariable(name, val); err != nil { + return "", false, err + } + if after == "" { + return fmt.Sprintf("%s = %.10g", name, val), true, nil + } + result, err := r.evaluate(input, strings.Fields(after)) + return result, true, err + } + } + } + } + + // Check for standard assignment format (name = value or name value = expression) hasAssignment := strings.Contains(input, " = ") || strings.Contains(input, " =") if !hasAssignment { return "", false, nil @@ -197,7 +281,7 @@ func (r *RPN) handleAssignment(input string) (string, bool, error) { if after == "" { return fmt.Sprintf("%s = %.10g", name, val), true, nil } - result, err := r.evaluate(strings.Fields(after)) + result, err := r.evaluate(input, strings.Fields(after)) return result, true, err } } diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go index 76f6dbb..b5e1739 100644 --- a/internal/rpn/rpn_test.go +++ b/internal/rpn/rpn_test.go @@ -1324,3 +1324,68 @@ func TestRPNConcurrentModeAndEval(t *testing.T) { wg.Wait() } + +// TestParseAndEvaluateAssignmentLeftRight tests := and =: assignment operators in RPN +func TestParseAndEvaluateAssignmentLeftRight(t *testing.T) { + tests := []struct { + name string + input string + expectedVar string + expectedValue float64 + }{ + { + name: "5 x := (left assignment)", + input: "5 x :=", + expectedVar: "x", + expectedValue: 5, + }, + { + name: "x 5 =: (right assignment)", + input: "x 5 =:", + expectedVar: "x", + expectedValue: 5, + }, + { + name: "3 y := (left assignment)", + input: "3 y :=", + expectedVar: "y", + expectedValue: 3, + }, + { + name: "y 3 =: (right assignment)", + input: "y 3 =:", + expectedVar: "y", + expectedValue: 3, + }, + { + name: "pi 3.14159 =: (assignment with constant)", + input: "pi 3.14159 =:", + expectedVar: "pi", + expectedValue: 3.14159, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := NewVariables() + r := NewRPN(v) + + _, err := r.ParseAndEvaluate(tt.input) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) + } + + // For assignment, result may be empty (side effect is variable setting) + // The important thing is the variable was set correctly + + // Verify variable was set + val, exists := v.GetVariable(tt.expectedVar) + if !exists { + t.Errorf("Variable %q should exist after assignment", tt.expectedVar) + } + if val != tt.expectedValue { + t.Errorf("Variable %q = %v, want %v", tt.expectedVar, val, tt.expectedValue) + } + }) + } +} diff --git a/main b/main deleted file mode 100755 index be34d40..0000000 Binary files a/main and /dev/null differ -- cgit v1.2.3