diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-24 11:16:10 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-24 11:16:10 +0300 |
| commit | d35efdcdde0b6985473684f0b5668950a8477703 (patch) | |
| tree | 107da3fcdee51e5bbedf8cc9871fcbf9ff35eeca /internal | |
| parent | 065dd252af1d2f1c70f8929058ea364481ab0658 (diff) | |
fix: restore RPN stack on ParseAndEvaluate error in RPNHandler
RPNHandler.Handle silently swallowed ParseAndEvaluate errors on
multi-word input, leaving the persistent REPL stack in a corrupted
state. evaluate() modifies r.currentStack directly during token
processing, so partial evaluation on failure left orphan values.
Fix: save the stack before ParseAndEvaluate, restore it on error.
Applied to both the bare RPN path and the rpn/calc prefix path.
Added tests:
- TestRPNHandlerStackNotCorruptedOnError (bare multi-word input)
- TestRPNHandlerErrorReturnedNotSwallowed (error propagation)
- TestRPNHandlerStackNotCorruptedOnPrefixedError (rpn/calc prefix)
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/repl/handlers.go | 15 | ||||
| -rw-r--r-- | internal/repl/repl_test.go | 69 |
2 files changed, 82 insertions, 2 deletions
diff --git a/internal/repl/handlers.go b/internal/repl/handlers.go index c5b28fc..fd4c1a6 100644 --- a/internal/repl/handlers.go +++ b/internal/repl/handlers.go @@ -152,8 +152,13 @@ func (h *RPNHandler) Handle(repl *REPL, input string) (output string, handled bo if strings.HasPrefix(lowerInput, "rpn ") || strings.HasPrefix(lowerInput, "calc ") { // Extract the expression after rpn/calc rest := strings.TrimSpace(strings.TrimPrefix(input, strings.SplitN(input, " ", 2)[0])) + + // Save stack state so a failed expression doesn't corrupt it + savedStack := repl.rpnState.rpnCalc.GetCurrentStack() + result, err := repl.rpnState.rpnCalc.ParseAndEvaluate(rest) if err != nil { + repl.rpnState.rpnCalc.SetCurrentStack(savedStack) return "", true, err } return result, true, nil @@ -163,10 +168,16 @@ func (h *RPNHandler) Handle(repl *REPL, input string) (output string, handled bo if repl.rpnState != nil { // Check if input looks like RPN (contains spaces or is a single known operator) if strings.Contains(input, " ") { + // Save stack state so a failed expression doesn't corrupt it + savedStack := repl.rpnState.rpnCalc.GetCurrentStack() + result, err := repl.rpnState.rpnCalc.ParseAndEvaluate(input) - if err == nil { - return result, true, nil + if err != nil { + // Restore the stack to its state before the failed expression + repl.rpnState.rpnCalc.SetCurrentStack(savedStack) + return "", true, err } + return result, true, nil } // Try evaluating as a single operator on the current RPN stack diff --git a/internal/repl/repl_test.go b/internal/repl/repl_test.go index 89fb89a..f8e22a2 100644 --- a/internal/repl/repl_test.go +++ b/internal/repl/repl_test.go @@ -774,3 +774,72 @@ func TestREPLDefaultGetCommandDescription(t *testing.T) { }) } } + +func TestRPNHandlerStackNotCorruptedOnError(t *testing.T) { + repl := createTestREPL() + + // First, push some values onto the stack + defaultExecutor(repl, "1 2 3 ") + + // Save the stack state before the failing expression + beforeStack := repl.rpnState.rpnCalc.GetCurrentStack() + if len(beforeStack) != 3 { + t.Fatalf("expected 3 values on stack before error, got %d", len(beforeStack)) + } + + // Now try a failing multi-word RPN expression + defaultExecutor(repl, "4 5 + invalidtoken") + + // Verify the stack was restored to its state before the failed expression + afterStack := repl.rpnState.rpnCalc.GetCurrentStack() + if len(afterStack) != len(beforeStack) { + t.Errorf("stack corrupted by failed expression: had %d values, now has %d", len(beforeStack), len(afterStack)) + } + for i := range beforeStack { + if afterStack[i].String() != beforeStack[i].String() { + t.Errorf("stack[%d] corrupted: was %q, now %q", i, beforeStack[i].String(), afterStack[i].String()) + } + } +} + +func TestRPNHandlerErrorReturnedNotSwallowed(t *testing.T) { + repl := createTestREPL() + + // Test that ParseAndEvaluate errors on multi-word input are propagated + h := &RPNHandler{} + _, handled, err := h.Handle(repl, "3 4 + invalidtoken") + + if !handled { + t.Error("multi-word RPN with error should be handled (not fall through)") + } + if err == nil { + t.Error("multi-word RPN with invalid token should return an error") + } +} + +func TestRPNHandlerStackNotCorruptedOnPrefixedError(t *testing.T) { + repl := createTestREPL() + + // Push values onto the stack + defaultExecutor(repl, "10 20 ") + + // Save the stack state before the failing prefixed expression + beforeStack := repl.rpnState.rpnCalc.GetCurrentStack() + if len(beforeStack) != 2 { + t.Fatalf("expected 2 values on stack before error, got %d", len(beforeStack)) + } + + // Try a failing prefixed RPN expression + defaultExecutor(repl, "rpn 30 40 + invalidtoken") + + // Verify the stack was restored + afterStack := repl.rpnState.rpnCalc.GetCurrentStack() + if len(afterStack) != len(beforeStack) { + t.Errorf("stack corrupted by failed rpn-prefixed expression: had %d values, now has %d", len(beforeStack), len(afterStack)) + } + for i := range beforeStack { + if afterStack[i].String() != beforeStack[i].String() { + t.Errorf("stack[%d] corrupted: was %q, now %q", i, beforeStack[i].String(), afterStack[i].String()) + } + } +} |
