diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-20 22:22:34 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-20 22:22:34 +0200 |
| commit | 8fb02e7bfc0669fea2adb5d38af2a8a12a1eb6b5 (patch) | |
| tree | 5ac5faf8f9e660d8e76e25806ab235ecb30bac01 | |
| parent | 59d8038a2188319640863ecad33ea855b2cf3c0e (diff) | |
feat: Add hyper operator methods for stack operations
- Added HyperAdd, HyperMultiply, HyperSubtract, HyperDivide, HyperPower, HyperModulo
- Each hyper operator pops all values from stack and applies left-associative operation
- Updated token evaluation to recognize [op] pattern (e.g., [+], [-], [*], [/], [^], [%])
- Added comprehensive test cases for all hyper operators
- Updated README.md with hyper operator documentation and fixed persistent RPN state notes
Hyper operators allow operating on all stack values at once, while regular operators
work on the top two values.
| -rw-r--r-- | README.md | 40 | ||||
| -rw-r--r-- | internal/rpn/operations.go | 176 | ||||
| -rw-r--r-- | internal/rpn/rpn.go | 24 | ||||
| -rw-r--r-- | internal/rpn/rpn_test.go | 152 |
4 files changed, 386 insertions, 6 deletions
@@ -136,19 +136,47 @@ perc calc 1 2 3 show # Show stack without modifying ### REPL Mode Notes -In REPL mode, each line is evaluated independently. To chain operations: -- Use the `rpn` subcommand for multi-step expressions: `rpn 2 3 + 4 -` -- Or use the `rpn` command to set variables: `rpn x 5 =` then `rpn x x +` -- The `show` command can display the final stack state +In REPL mode, RPN operations maintain persistent state between commands. This allows you to build up values on the stack across multiple commands. Example REPL session: ``` perc> rpn 2 3 4 + # Push 2, 3, 4; add last two 2 7 -perc> rpn show # Show full stack (but we start fresh per command) +perc> + # Add top two: 2 + 7 = 9 +9 +perc> 5 * # Multiply by 5: 9 * 5 = 45 +45 ``` -Note: Each REPL command is evaluated independently, so you cannot chain operations like `2 3 4 +` then `-` on separate lines. Use `rpn 2 3 4 + -` for a single expression instead. +To show the current stack without modifying it: +``` +perc> show # Show current stack state +45 +``` + +## Hyper Operators + +Hyper operators work on all values on the stack simultaneously: + +```bash +perc calc 1 2 3 4 5 [+] # Sum all: 1+2+3+4+5 = 15 +# → 15 + +perc calc 2 3 4 [*] # Multiply all: 2*3*4 = 24 +# → 24 + +perc calc 10 3 2 [-] # 10 - 3 - 2 = 5 +# → 5 + +perc calc 100 5 2 [/] # 100 / 5 / 2 = 10 +# → 10 + +perc calc 2 3 2 [^] # (2^3)^2 = 64 +# → 64 + +perc calc 100 7 3 [%] # 100 % 7 % 3 = 2 +# → 2 +``` ## Building diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go index cbcc8fe..2f4b84c 100644 --- a/internal/rpn/operations.go +++ b/internal/rpn/operations.go @@ -123,6 +123,182 @@ func (o *Operations) Modulo(stack *Stack) error { return nil } +// Hyper operators - operate on all values on the stack + +// HyperAdd pops all values from stack, adds them left-associative, and pushes result. +func (o *Operations) HyperAdd(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperadd: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperadd: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + sum := 0.0 + for i := 0; i < len(values); i++ { + sum += values[i] + } + stack.Push(sum) + return nil +} + +// HyperMultiply pops all values from stack, multiplies them left-associative, and pushes result. +func (o *Operations) HyperMultiply(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hypermultiply: need at least 2 values") + } + + product := 1.0 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hypermultiply: %w", err) + } + product *= val + } + stack.Push(product) + return nil +} + +// HyperSubtract pops all values from stack, subtracts them left-associative, and pushes result. +func (o *Operations) HyperSubtract(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hypersubtract: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hypersubtract: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + result := values[0] + for i := 1; i < len(values); i++ { + result -= values[i] + } + stack.Push(result) + return nil +} + +// HyperDivide pops all values from stack, divides them left-associative, and pushes result. +func (o *Operations) HyperDivide(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperdivide: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperdivide: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + result := values[0] + for i := 1; i < len(values); i++ { + if values[i] == 0 { + return fmt.Errorf("division by zero") + } + result /= values[i] + } + stack.Push(result) + return nil +} + +// HyperPower pops all values from stack, raises to power left-associative, and pushes result. +func (o *Operations) HyperPower(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperpower: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperpower: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + result := values[0] + for i := 1; i < len(values); i++ { + result = math.Pow(result, values[i]) + } + stack.Push(result) + return nil +} + +// HyperModulo pops all values from stack, computes modulo left-associative, and pushes result. +func (o *Operations) HyperModulo(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hypermodulo: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hypermodulo: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + result := values[0] + for i := 1; i < len(values); i++ { + if values[i] == 0 { + return fmt.Errorf("modulo by zero") + } + result = math.Mod(result, values[i]) + } + stack.Push(result) + return nil +} + // stack manipulation operators // Dup duplicates the top stack value. diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go index 07a5fb4..d21dd67 100644 --- a/internal/rpn/rpn.go +++ b/internal/rpn/rpn.go @@ -160,6 +160,30 @@ func (r *RPN) evaluate(tokens []string) (string, error) { if err := r.ops.Modulo(stack); err != nil { return "", fmt.Errorf("operator %%: %w", err) } + case "[+]": + if err := r.ops.HyperAdd(stack); err != nil { + return "", fmt.Errorf("hyperoperator [+]: %w", err) + } + case "[-]": + if err := r.ops.HyperSubtract(stack); err != nil { + return "", fmt.Errorf("hyperoperator [-]: %w", err) + } + case "[*]": + if err := r.ops.HyperMultiply(stack); err != nil { + return "", fmt.Errorf("hyperoperator [*]: %w", err) + } + case "[/]": + if err := r.ops.HyperDivide(stack); err != nil { + return "", fmt.Errorf("hyperoperator [/]: %w", err) + } + case "[^]": + if err := r.ops.HyperPower(stack); err != nil { + return "", fmt.Errorf("hyperoperator [^]: %w", err) + } + case "[%]": + if err := r.ops.HyperModulo(stack); err != nil { + return "", fmt.Errorf("hyperoperator [%%]: %w", err) + } case "dup": if err := r.ops.Dup(stack); err != nil { return "", fmt.Errorf("dup: %w", err) diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go index aa285ac..412d3db 100644 --- a/internal/rpn/rpn_test.go +++ b/internal/rpn/rpn_test.go @@ -781,3 +781,155 @@ func TestRPNClearStack(t *testing.T) { t.Errorf("After clear = %q, want 'All variables cleared'", result) } } + +// Hyper operator tests + +func TestHyperAdd(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 1 2 3 4 5 [+] + result, err := r.ParseAndEvaluate("1 2 3 4 5 [+]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "15" { + t.Errorf("1 2 3 4 5 [+] = %q, want '15'", result) + } +} + +func TestHyperAddEdgeCases(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test with two values: 10 20 [+] + result, err := r.ParseAndEvaluate("10 20 [+]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "30" { + t.Errorf("10 20 [+] = %q, want '30'", result) + } + + // Test with single value should error - use fresh instance to avoid stack state + v2 := NewVariables().(*Variables) + r2 := NewRPN(v2) + _, err = r2.ParseAndEvaluate("5 [+]") + if err == nil { + t.Error("5 [+] should return error") + } +} + +func TestHyperSubtract(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 10 3 2 [-] => 10 - 3 - 2 = 5 + result, err := r.ParseAndEvaluate("10 3 2 [-]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "5" { + t.Errorf("10 3 2 [-] = %q, want '5'", result) + } +} + +func TestHyperMultiply(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 2 3 4 [*] => 2 * 3 * 4 = 24 + result, err := r.ParseAndEvaluate("2 3 4 [*]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "24" { + t.Errorf("2 3 4 [*] = %q, want '24'", result) + } +} + +func TestHyperDivide(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 100 5 2 [/] => 100 / 5 / 2 = 10 + result, err := r.ParseAndEvaluate("100 5 2 [/]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "10" { + t.Errorf("100 5 2 [/] = %q, want '10'", result) + } +} + +func TestHyperDivideByZero(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("100 0 [/]") + if err == nil { + t.Error("100 0 [/] should return error") + } +} + +func TestHyperPower(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 2 3 2 [^] => 2 ^ 3 ^ 2 = (2 ^ 3) ^ 2 = 8 ^ 2 = 64 + result, err := r.ParseAndEvaluate("2 3 2 [^]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "64" { + t.Errorf("2 3 2 [^] = %q, want '64'", result) + } +} + +func TestHyperModulo(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 100 7 3 [%%] => 100 %% 7 %% 3 = 2 %% 3 = 2 + result, err := r.ParseAndEvaluate("100 7 3 [%]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "2" { + t.Errorf("100 7 3 [%%] = %q, want '2'", result) + } +} + +func TestHyperModuloByZero(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("100 0 [%]") + if err == nil { + t.Error("100 0 [%] should return error") + } +} + +func TestHyperOperatorEdgeCases(t *testing.T) { + // Test with single value should error for all hyper operators + testCases := []struct { + input string + operands int + }{ + {"100 [%]", 1}, + {"5 [+]", 1}, + {"10 [-]", 1}, + {"2 [*]", 1}, + {"100 [/]", 1}, + {"2 [^]", 1}, + } + + for _, tc := range testCases { + v := NewVariables().(*Variables) + r := NewRPN(v) + _, err := r.ParseAndEvaluate(tc.input) + if err == nil { + t.Errorf("%s should return error for insufficient operands", tc.input) + } + } +} |
