diff options
Diffstat (limited to 'internal/rpn')
| -rw-r--r-- | internal/rpn/operations.go | 88 | ||||
| -rw-r--r-- | internal/rpn/rpn_ops.go | 8 | ||||
| -rw-r--r-- | internal/rpn/rpn_parse.go | 9 | ||||
| -rw-r--r-- | internal/rpn/rpn_state.go | 19 | ||||
| -rw-r--r-- | internal/rpn/rpn_test.go | 101 |
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() +} |
