summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-25 22:33:42 +0200
committerPaul Buetow <paul@buetow.org>2026-03-25 22:33:42 +0200
commit18b0017e2603436dd418b8866279445ff45ad6fb (patch)
treeb50407397275129c5bc27d50070e74cf4cf69208
parent9ee6293b175710e7cf1ff5bc9a6dc555ddaf559f (diff)
rpn: Add := and =: assignment operators with = synonym
-rw-r--r--internal/rpn/number.go38
-rw-r--r--internal/rpn/operations.go49
-rw-r--r--internal/rpn/operations_test.go85
-rw-r--r--internal/rpn/rpn_parse.go94
-rw-r--r--internal/rpn/rpn_test.go65
-rwxr-xr-xmainbin3425628 -> 0 bytes
6 files changed, 310 insertions, 21 deletions
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
--- a/main
+++ /dev/null
Binary files differ