summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-24 11:16:10 +0300
committerPaul Buetow <paul@buetow.org>2026-05-24 11:16:10 +0300
commitd35efdcdde0b6985473684f0b5668950a8477703 (patch)
tree107da3fcdee51e5bbedf8cc9871fcbf9ff35eeca /internal
parent065dd252af1d2f1c70f8929058ea364481ab0658 (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.go15
-rw-r--r--internal/repl/repl_test.go69
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())
+ }
+ }
+}