summaryrefslogtreecommitdiff
path: root/internal/rpn
diff options
context:
space:
mode:
Diffstat (limited to 'internal/rpn')
-rw-r--r--internal/rpn/operations.go88
-rw-r--r--internal/rpn/rpn_ops.go8
-rw-r--r--internal/rpn/rpn_parse.go9
-rw-r--r--internal/rpn/rpn_state.go19
-rw-r--r--internal/rpn/rpn_test.go101
5 files changed, 208 insertions, 17 deletions
diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go
index 1300d99..81ea78e 100644
--- a/internal/rpn/operations.go
+++ b/internal/rpn/operations.go
@@ -6,6 +6,7 @@ package rpn
import (
"fmt"
"math"
+ "sync"
)
// ArithmeticOperator defines the interface for basic arithmetic operators.
@@ -56,6 +57,7 @@ type StackOperator interface {
type VariableOperator interface {
ListVariables() (string, error)
ClearVariables()
+ AssignVariableFromStack(stack *Stack) error
}
// Operator is the combined interface for all operator implementations.
@@ -74,8 +76,14 @@ type Operator interface {
type Operations struct {
vars VariableStore
mode CalculationMode
+ mu sync.RWMutex
}
+// Ensure Operations implements Operator at compile time.
+// This is an explicit interface satisfaction check that will fail to compile
+// if Operations doesn't implement all methods required by the Operator interface.
+var _ Operator = (*Operations)(nil)
+
// NewOperations creates a new Operations instance with the given variable store.
func NewOperations(vars VariableStore) *Operations {
return &Operations{
@@ -85,10 +93,21 @@ func NewOperations(vars VariableStore) *Operations {
}
// SetMode sets the calculation mode for the Operations instance.
+// This method is thread-safe for writes.
func (o *Operations) SetMode(mode CalculationMode) {
+ o.mu.Lock()
+ defer o.mu.Unlock()
o.mode = mode
}
+// GetMode returns the current calculation mode.
+// This method is thread-safe for reads.
+func (o *Operations) GetMode() CalculationMode {
+ o.mu.RLock()
+ defer o.mu.RUnlock()
+ return o.mode
+}
+
// OperatorHandler represents a function that handles an operator.
// Returns (result string, handled bool, error error).
// result is non-empty only for commands that return immediately (like show, vars).
@@ -129,6 +148,7 @@ 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("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) })
@@ -348,7 +368,8 @@ func (o *Operations) Log2(stack *Stack) error {
}
// Compute log2 using the number interface
- stack.Push(NewNumber(math.Log2(val), o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(math.Log2(val), mode))
return nil
}
@@ -367,7 +388,8 @@ func (o *Operations) Log10(stack *Stack) error {
}
// Compute log10 using the number interface
- stack.Push(NewNumber(math.Log10(val), o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(math.Log10(val), mode))
return nil
}
@@ -386,7 +408,8 @@ func (o *Operations) Ln(stack *Stack) error {
}
// Compute ln using the number interface
- stack.Push(NewNumber(math.Log(val), o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(math.Log(val), mode))
return nil
}
@@ -418,7 +441,8 @@ func (o *Operations) HyperAdd(stack *Stack) error {
for i := 0; i < len(values); i++ {
sum += values[i].Float64()
}
- stack.Push(NewNumber(sum, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(sum, mode))
return nil
}
@@ -436,7 +460,8 @@ func (o *Operations) HyperMultiply(stack *Stack) error {
}
product *= val.Float64()
}
- stack.Push(NewNumber(product, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(product, mode))
return nil
}
@@ -466,7 +491,8 @@ func (o *Operations) HyperSubtract(stack *Stack) error {
for i := 1; i < len(values); i++ {
result -= values[i].Float64()
}
- stack.Push(NewNumber(result, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(result, mode))
return nil
}
@@ -500,7 +526,8 @@ func (o *Operations) HyperDivide(stack *Stack) error {
}
result /= val
}
- stack.Push(NewNumber(result, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(result, mode))
return nil
}
@@ -530,7 +557,8 @@ func (o *Operations) HyperPower(stack *Stack) error {
for i := 1; i < len(values); i++ {
result = math.Pow(result, values[i].Float64())
}
- stack.Push(NewNumber(result, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(result, mode))
return nil
}
@@ -564,7 +592,8 @@ func (o *Operations) HyperModulo(stack *Stack) error {
}
result = math.Mod(result, val)
}
- stack.Push(NewNumber(result, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(result, mode))
return nil
}
@@ -602,7 +631,8 @@ func (o *Operations) HyperLog2(stack *Stack) error {
}
// Push the result as a Number
- stack.Push(NewNumber(result, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(result, mode))
return nil
}
@@ -640,7 +670,8 @@ func (o *Operations) HyperLog10(stack *Stack) error {
}
// Push the result as a Number
- stack.Push(NewNumber(result, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(result, mode))
return nil
}
@@ -676,7 +707,8 @@ func (o *Operations) HyperLn(stack *Stack) error {
}
result += math.Log(val)
}
- stack.Push(NewNumber(result, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(result, mode))
return nil
}
@@ -878,7 +910,8 @@ func (o *Operations) UseVariable(stack *Stack, name string) error {
return fmt.Errorf("%w: %s", ErrVariableNotFound, name)
}
- stack.Push(NewNumber(val, o.mode))
+ mode := o.GetMode()
+ stack.Push(NewNumber(val, mode))
return nil
}
@@ -907,3 +940,32 @@ func (o *Operations) ListVariables() (string, error) {
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 {
+ return fmt.Errorf("insufficient operands for assignment: need variable name")
+ }
+
+ nameVal, err := stack.Pop()
+ if err != nil {
+ return err
+ }
+
+ // Get the variable name from the popped value
+ name := nameVal.String()
+
+ if stack.Len() < 1 {
+ return fmt.Errorf("insufficient operands for assignment: need value")
+ }
+
+ val, err := stack.Pop()
+ if err != nil {
+ return err
+ }
+
+ // Convert to float64 for variable storage
+ return o.vars.SetVariable(name, val.Float64())
+}
diff --git a/internal/rpn/rpn_ops.go b/internal/rpn/rpn_ops.go
index 52a03fc..0c46576 100644
--- a/internal/rpn/rpn_ops.go
+++ b/internal/rpn/rpn_ops.go
@@ -19,6 +19,10 @@ func Tokenize(input string) []string {
// 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) {
+ r.mu.RLock()
+ mode := r.mode
+ r.mu.RUnlock()
+
stack := NewStack()
for _, token := range tokens {
@@ -37,7 +41,7 @@ func (r *RPN) ResultStack(tokens []string) (string, error) {
if stack.Len() >= r.maxStack {
return "", fmt.Errorf("stack overflow")
}
- stack.Push(NewNumber(num, r.mode))
+ stack.Push(NewNumber(num, mode))
continue
}
@@ -58,7 +62,7 @@ func (r *RPN) ResultStack(tokens []string) (string, error) {
// Check if it's a variable reference (push its value)
val, exists := r.vars.GetVariable(token)
if exists {
- stack.Push(NewNumber(val, r.mode))
+ stack.Push(NewNumber(val, mode))
} else {
return "", fmt.Errorf("unknown token '%s'", token)
}
diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go
index 4686122..c755150 100644
--- a/internal/rpn/rpn_parse.go
+++ b/internal/rpn/rpn_parse.go
@@ -11,15 +11,20 @@ import (
// ParseAndEvaluate parses and evaluates an RPN expression.
// Returns the result as a formatted string or an error.
+// This method is thread-safe for concurrent execution.
func (r *RPN) ParseAndEvaluate(input string) (string, error) {
// Validate input and initialize
input = strings.TrimSpace(input)
if input == "" {
return "", fmt.Errorf("rpn: empty expression")
}
+
+ // Lock for write operations on currentStack
+ r.mu.Lock()
if r.currentStack == nil {
r.currentStack = NewStack()
}
+ r.mu.Unlock()
// Handle assignment formats
if assignmentResult, isAssignment, err := r.handleAssignment(input); err != nil {
@@ -38,7 +43,11 @@ func (r *RPN) ParseAndEvaluate(input string) (string, error) {
}
// 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) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
// Use the current stack for evaluation to preserve state
// This allows incremental operations in REPL mode
if r.currentStack == nil {
diff --git a/internal/rpn/rpn_state.go b/internal/rpn/rpn_state.go
index 684f758..93f0ff4 100644
--- a/internal/rpn/rpn_state.go
+++ b/internal/rpn/rpn_state.go
@@ -3,8 +3,15 @@
package rpn
+import (
+ "sync"
+)
+
// RPN represents the RPN parser and evaluator with state management.
+// It is thread-safe for concurrent read operations, but write operations
+// on the stack or mode should be synchronized externally or use the provided methods.
type RPN struct {
+ mu sync.RWMutex
vars VariableStore
ops Operator
opRegistry *OperatorRegistry
@@ -28,19 +35,28 @@ func NewRPN(vars VariableStore) *RPN {
}
// GetMode returns the current calculation mode.
+// This method is thread-safe for concurrent reads.
func (r *RPN) GetMode() CalculationMode {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
return r.mode
}
// SetMode sets the calculation mode.
+// This method is thread-safe for writes.
func (r *RPN) SetMode(mode CalculationMode) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
r.mode = mode
r.ops.SetMode(mode)
}
// GetCurrentStack returns a copy of the current stack for inspection.
// Returns []Number to preserve value types (numbers and booleans).
+// This method is thread-safe for concurrent reads.
func (r *RPN) GetCurrentStack() []Number {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
if r.currentStack == nil {
return nil
}
@@ -49,7 +65,10 @@ func (r *RPN) GetCurrentStack() []Number {
// SetCurrentStack sets the current stack from a slice of numbers.
// This is useful for restoring stack state.
+// This method is thread-safe for writes.
func (r *RPN) SetCurrentStack(values []Number) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
r.currentStack = NewStack()
for _, v := range values {
r.currentStack.Push(v)
diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go
index 7bd1d9a..76f6dbb 100644
--- a/internal/rpn/rpn_test.go
+++ b/internal/rpn/rpn_test.go
@@ -6,6 +6,7 @@ package rpn
import (
"fmt"
"strings"
+ "sync"
"testing"
)
@@ -591,9 +592,9 @@ func TestResultStackErrors(t *testing.T) {
expectedError: "insufficient operands",
},
{
- name: "invalid assignment syntax in ResultStack",
+ name: "insufficient operands for =",
input: []string{"="},
- expectedError: "unknown token '='",
+ expectedError: "insufficient operands for assignment",
},
}
@@ -1227,3 +1228,99 @@ func TestRPNStackPreservation(t *testing.T) {
t.Errorf("Stack should have 2 values, got %d", len(stack))
}
}
+
+// TestRPNModeThreadSafety verifies that mode changes are thread-safe
+func TestRPNModeThreadSafety(t *testing.T) {
+ r := NewRPN(NewVariables())
+
+ // Run multiple goroutines that change mode and perform operations
+ done := make(chan bool, 100)
+ for i := 0; i < 100; i++ {
+ go func() {
+ // Toggle mode
+ r.SetMode(FloatMode)
+ r.SetMode(RationalMode)
+
+ // Perform an evaluation while mode might be changing
+ _, _ = r.ParseAndEvaluate("1 2 +")
+ done <- true
+ }()
+ }
+
+ // Wait for all goroutines to complete
+ for i := 0; i < 100; i++ {
+ <-done
+ }
+}
+
+// TestRPNModeDirectAccess verifies direct mode access doesn't have race conditions
+func TestRPNModeDirectAccess(t *testing.T) {
+ r := NewRPN(NewVariables())
+
+ var wg sync.WaitGroup
+ iterations := 100
+
+ // Goroutine 1: Direct mode reads (simulating evaluate, ResultStack, EvalOperator)
+ // This simulates what happens in evaluate() - reading mode while holding lock
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < iterations; i++ {
+ // Simulate evaluate() - acquire lock, read mode, then release lock
+ r.mu.RLock()
+ _ = r.mode
+ r.mu.RUnlock()
+ _, _ = r.ParseAndEvaluate("1 2 +")
+ }
+ }()
+
+ // Goroutine 2: SetMode (simulating handleRatCommand)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < iterations; i++ {
+ r.SetMode(FloatMode)
+ r.SetMode(RationalMode)
+ }
+ }()
+
+ // Goroutine 3: GetMode (mutex-protected)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < iterations; i++ {
+ _ = r.GetMode()
+ }
+ }()
+
+ wg.Wait()
+}
+
+// TestRPNConcurrentModeAndEval tests concurrent mode changes and evaluations
+func TestRPNConcurrentModeAndEval(t *testing.T) {
+ r := NewRPN(NewVariables())
+
+ var wg sync.WaitGroup
+ iterations := 50
+
+ // Goroutine 1: Change mode
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < iterations; i++ {
+ r.SetMode(FloatMode)
+ r.SetMode(RationalMode)
+ }
+ }()
+
+ // Goroutine 2: Evaluate expressions
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < iterations; i++ {
+ _, _ = r.ParseAndEvaluate("1 2 +")
+ }
+ }()
+
+ wg.Wait()
+}