From b315ebbcd92e58249c6ed8f04217ef7adcdde5d5 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 11 Apr 2026 21:50:15 +0300 Subject: more on this --- internal/repl/commands.go | 31 +- internal/repl/completer.go | 2 +- internal/repl/completer_test.go | 2 +- internal/repl/concurrent_test.go | 53 ++- internal/repl/repl.go | 145 +------- internal/repl/repl_completer_test.go | 2 +- internal/repl/repl_test.go | 644 ++++++++++++++++------------------- internal/rpn/number.go | 41 ++- internal/rpn/operations_test.go | 170 ++++++--- internal/rpn/rpn_parse.go | 6 +- 10 files changed, 522 insertions(+), 574 deletions(-) diff --git a/internal/repl/commands.go b/internal/repl/commands.go index 841265b..3bdcde5 100644 --- a/internal/repl/commands.go +++ b/internal/repl/commands.go @@ -13,20 +13,12 @@ import ( // Commands: help, clear, quit, exit, rpn, calc, rat var builtinCommandsList = []string{"help", "clear", "quit", "exit", "rpn", "calc", "rat"} -// builtinCommands returns the list of built-in commands. -// This is a package-level wrapper for backward compatibility. -// -// Returns a slice of built-in command names -func builtinCommands() []string { - return builtinCommandsList -} - // Commands returns the list of built-in command names supported by the REPL. // This is a public function that exposes the built-in command list. // // Returns a slice of built-in command names (e.g., "help", "clear", "quit") func Commands() []string { - return builtinCommands() + return builtinCommandsList } // ExecuteCommand runs a built-in command and returns its output or error. @@ -144,3 +136,24 @@ func cmdQuit() error { fmt.Println("Goodbye!") return nil } + +// isBuiltinCommand checks if input starts with a built-in command. +// It performs case-insensitive matching against known built-in commands. +// +// input: the command string to check +// Returns the input string and true if it starts with a built-in command, +// or empty string and false otherwise +func isBuiltinCommand(input string) (string, bool) { + args := strings.Fields(input) + if len(args) == 0 { + return "", false + } + + cmd := strings.ToLower(args[0]) + for _, builtin := range builtinCommandsList { + if cmd == builtin { + return input, true + } + } + return "", false +} diff --git a/internal/repl/completer.go b/internal/repl/completer.go index d66a5ef..0eb31e0 100644 --- a/internal/repl/completer.go +++ b/internal/repl/completer.go @@ -43,7 +43,7 @@ func completer(d prompt.Document) []prompt.Suggest { } var suggestions []prompt.Suggest - for _, cmd := range builtinCommands() { + for _, cmd := range Commands() { if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(text)) { suggestions = append(suggestions, prompt.Suggest{ Text: cmd, diff --git a/internal/repl/completer_test.go b/internal/repl/completer_test.go index e64a000..4166843 100644 --- a/internal/repl/completer_test.go +++ b/internal/repl/completer_test.go @@ -140,7 +140,7 @@ func TestCompleter(t *testing.T) { // Only generate suggestions if text is not empty // (empty string is a prefix of all strings, so we need to handle it specially) if tt.text != "" { - for _, cmd := range builtinCommands() { + for _, cmd := range Commands() { if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(tt.text)) { suggestions = append(suggestions, prompt.Suggest{ Text: cmd, diff --git a/internal/repl/concurrent_test.go b/internal/repl/concurrent_test.go index d07b9ce..501b363 100644 --- a/internal/repl/concurrent_test.go +++ b/internal/repl/concurrent_test.go @@ -3,61 +3,92 @@ package repl import ( "sync" "testing" + + "codeberg.org/snonux/gt/internal/rpn" ) +// TestConcurrentExecutor tests concurrent calls to defaultExecutor with fresh state func TestConcurrentExecutor(t *testing.T) { - // Test concurrent calls to executor() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() - executor("20% of 150") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "20% of 150") }(i) } wg.Wait() } +// TestConcurrentRPN tests concurrent inline RPN evaluation func TestConcurrentRPN(t *testing.T) { - // Test concurrent calls to runRPN() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() - // Ignore error return as the expression "3 4 +" should always succeed - _, _ = runRPN("3 4 +") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + _, _ = rpnCalc.ParseAndEvaluate("3 4 +") }(i) } wg.Wait() } +// TestConcurrentRatModeToggle tests concurrent rat mode toggles with fresh state func TestConcurrentRatModeToggle(t *testing.T) { - // Test concurrent calls to executor() that change mode var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() - executor("rat toggle") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "rat toggle") }(i) } wg.Wait() } +// TestConcurrentExecutorAndRPN tests concurrent executor and RPN calls with fresh state func TestConcurrentExecutorAndRPN(t *testing.T) { - // Test concurrent calls to executor() and runRPN() var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(2) go func(id int) { defer wg.Done() - executor("20% of 150") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "20% of 150") }(i) go func(id int) { defer wg.Done() - // Ignore error return as the expression "3 4 +" should always succeed - _, _ = runRPN("3 4 +") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + _, _ = rpnCalc.ParseAndEvaluate("3 4 +") }(i) } wg.Wait() diff --git a/internal/repl/repl.go b/internal/repl/repl.go index f452351..e70be01 100644 --- a/internal/repl/repl.go +++ b/internal/repl/repl.go @@ -6,7 +6,6 @@ package repl import ( "fmt" "strings" - "sync" "codeberg.org/snonux/gt/internal/rpn" @@ -22,26 +21,6 @@ type RPNState struct { rpnCalc *rpn.RPN } -// executorREPL holds the REPL instance created by the executor function. -// This is used for backward compatibility with tests that need to access RPN state -// after calling executor(). It's not part of the main REPL architecture. -// Thread safety: Use executorREPLOnce for lazy initialization and executorREPLMu for access. -var executorREPL *REPL -var executorREPLOnce sync.Once -var executorREPLMu sync.Mutex - -// ResetExecutorREPL resets the executorREPL for clean test isolation. -// This should be called between tests that use executor() and getRPNState() -// to ensure each test starts with a fresh RPN state. -// -// Note: This function is intended for test use only and should not be used -// in production code. For production use, create new REPL instances with NewREPL(). -func ResetExecutorREPL() { - executorREPLMu.Lock() - defer executorREPLMu.Unlock() - executorREPL = nil -} - // REPL manages the interactive command-line interface for the percentage calculator. // It provides an interactive prompt with history, tab-completion, signal handling, // and command processing through a chain of responsibility pattern. @@ -190,7 +169,7 @@ func defaultCompleter(r *REPL, d prompt.Document) []prompt.Suggest { } var suggestions []prompt.Suggest - for _, cmd := range builtinCommands() { + for _, cmd := range Commands() { if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(text)) { suggestions = append(suggestions, prompt.Suggest{ Text: cmd, @@ -237,125 +216,3 @@ func RunREPL() error { repl := NewREPL(nil, nil) return repl.Run() } - -// executor runs a calculation command and returns the result. -// This is a package-level wrapper for backward compatibility and testing. -// It creates a minimal REPL instance without building a prompt, allowing -// calculation execution in non-interactive contexts. -// -// input: the calculation or command string to execute -// The function processes the input through defaultExecutor, which handles -// commands via the chain of responsibility pattern, including percentage -// calculations, RPN expressions, and built-in commands. -func executor(input string) { - // Initialize executorREPL only once using sync.Once for thread-safe lazy initialization - executorREPLOnce.Do(func() { - vars := rpn.NewVariables() - rpnState := &RPNState{ - vars: vars, - rpnCalc: rpn.NewRPN(vars), - } - - // Create a minimal REPL instance without building a prompt - executorREPL = &REPL{ - ttyChecker: &TTYChecker{}, - historyMgr: NewHistoryManager(".gt_history"), - signalHandler: NewSignalHandler(), - commandChain: NewCommandChain(), - rpnState: rpnState, - } - }) - - // Use mutex to protect access to executorREPL during execution - executorREPLMu.Lock() - repl := executorREPL - executorREPLMu.Unlock() - - defaultExecutor(repl, input) -} - -// runRPN parses and evaluates an RPN (Reverse Polish Notation) expression. -// This is a package-level wrapper for backward compatibility that delegates to -// the executor's REPL runRPN method. -// -// input: the RPN expression to evaluate -// Returns the result string and an error if the expression is invalid -func runRPN(input string) (string, error) { - executorREPLMu.Lock() - defer executorREPLMu.Unlock() - - if executorREPL != nil { - return executorREPL.rpnState.rpnCalc.ParseAndEvaluate(input) - } - return "", fmt.Errorf("no executor REPL available - call executor() first") -} - -// getRPNState returns the RPN state from the executor's REPL. -// This is a package-level helper for backward compatibility with tests that need -// to access RPN state after calling executor(). It's not part of the main REPL -// architecture. -// -// Returns the RPNState instance from the last executor() call, or nil if executor() hasn't been called -func getRPNState() *RPNState { - executorREPLMu.Lock() - defer executorREPLMu.Unlock() - - if executorREPL != nil { - return executorREPL.rpnState - } - return nil -} - - - - - -// getHistoryPath returns the absolute path to the history file. -// This is a package-level wrapper for backward compatibility. -// The history file is stored in the user's home directory. -// -// Returns the full path to the history file, or empty string on error -func getHistoryPath() string { - historyMgr := NewHistoryManager(".gt_history") - return historyMgr.Path() -} - -// loadHistory loads history from the history file. -// This is a package-level wrapper for backward compatibility that uses NewHistoryManager. -// -// Returns a slice of history entries, or nil if the file doesn't exist -func loadHistory() []string { - historyMgr := NewHistoryManager(".gt_history") - return historyMgr.Load() -} - -// saveHistory saves history to the history file. -// This is a package-level wrapper for backward compatibility that uses NewHistoryManager. -// -// history: the slice of history entries to save -// Returns an error if the file cannot be written -func saveHistory(history []string) error { - historyMgr := NewHistoryManager(".gt_history") - return historyMgr.Save(history) -} - -// isBuiltinCommand checks if input starts with a built-in command. -// It performs case-insensitive matching against known built-in commands. -// -// input: the command string to check -// Returns the input string and true if it starts with a built-in command, -// or empty string and false otherwise -func isBuiltinCommand(input string) (string, bool) { - args := strings.Fields(input) - if len(args) == 0 { - return "", false - } - - cmd := strings.ToLower(args[0]) - for _, builtin := range builtinCommands() { - if cmd == builtin { - return input, true - } - } - return "", false -} diff --git a/internal/repl/repl_completer_test.go b/internal/repl/repl_completer_test.go index 05ef194..9eb0a37 100644 --- a/internal/repl/repl_completer_test.go +++ b/internal/repl/repl_completer_test.go @@ -48,7 +48,7 @@ func TestCompleterLogic(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Simulate the completer logic var found bool - for _, cmd := range builtinCommands() { + for _, cmd := range Commands() { if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(tc.text)) { found = true break diff --git a/internal/repl/repl_test.go b/internal/repl/repl_test.go index 3abb664..a242956 100644 --- a/internal/repl/repl_test.go +++ b/internal/repl/repl_test.go @@ -8,50 +8,63 @@ import ( "testing" "codeberg.org/snonux/gt/internal/rpn" - - "github.com/c-bata/go-prompt" ) -func TestExecutor(t *testing.T) { - // Test that executor doesn't panic on empty input - executor("") +// Helper to create a minimal REPL for testing without prompt (no TTY required) +func createTestREPL() *REPL { + vars := rpn.NewVariables() + return &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpn.NewRPN(vars)}, + } } func TestExecutorWithHelp(t *testing.T) { - // Test executor with help command - executor("help") + repl := createTestREPL() + defaultExecutor(repl, "help") } func TestExecutorWithClear(t *testing.T) { - executor("clear") + repl := createTestREPL() + defaultExecutor(repl, "clear") } func TestExecutorWithQuit(t *testing.T) { - executor("quit") + repl := createTestREPL() + defaultExecutor(repl, "quit") } func TestExecutorWithExit(t *testing.T) { - executor("exit") + repl := createTestREPL() + defaultExecutor(repl, "exit") } func TestExecutorWithPercentage(t *testing.T) { - executor("20% of 150") + repl := createTestREPL() + defaultExecutor(repl, "20% of 150") } func TestExecutorWithRPN(t *testing.T) { - executor("rpn 3 4 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 3 4 +") } func TestExecutorWithInvalid(t *testing.T) { - executor("invalid input") + repl := createTestREPL() + defaultExecutor(repl, "invalid input") } func TestExecutorWithVars(t *testing.T) { - executor("rpn x 5 = vars") + repl := createTestREPL() + defaultExecutor(repl, "rpn x 5 = vars") } func TestExecutorWithClearVariables(t *testing.T) { - executor("rpn clear") + repl := createTestREPL() + defaultExecutor(repl, "rpn clear") } func TestIsBuiltinCommand(t *testing.T) { @@ -152,7 +165,7 @@ func TestIsBuiltinCommandWithMixedCase(t *testing.T) { } } -// TestRunRPN tests the runRPN helper function +// TestRunRPN tests inline RPN evaluation (like cmd/gt/main.go does) func TestRunRPN(t *testing.T) { tests := []struct { name string @@ -172,9 +185,19 @@ func TestRunRPN(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := runRPN(tt.input) + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + + input := strings.TrimSpace(tt.input) + if strings.HasPrefix(input, "rpn ") { + input = strings.TrimPrefix(input, "rpn ") + } else if strings.HasPrefix(input, "calc ") { + input = strings.TrimPrefix(input, "calc ") + } + + _, err := rpnCalc.ParseAndEvaluate(input) if (err != nil) != tt.wantErr { - t.Errorf("runRPN(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + t.Errorf("RPN evaluation error = %v, wantErr %v", err, tt.wantErr) } }) } @@ -212,198 +235,230 @@ func TestGetCommandDescriptionForUnknownCommand(t *testing.T) { } func TestExecutorWithSingleOperator(t *testing.T) { - executor("+") - executor("-") - executor("*") - executor("/") - executor("^") - executor("%") - executor("dup") - executor("swap") - executor("pop") - executor("show") - executor("vars") - executor("clear") + repl := createTestREPL() + for _, op := range []string{"+", "-", "*", "/", "^", "%", "dup", "swap", "pop", "show", "vars", "clear"} { + t.Run(op, func(t *testing.T) { + defaultExecutor(repl, op) + }) + } } func TestExecutorWithPercentageExpression(t *testing.T) { - executor("20% of 150") - executor("30 is what %% of 150") - executor("30 is 20%% of what") + repl := createTestREPL() + defaultExecutor(repl, "20% of 150") + defaultExecutor(repl, "30 is what %% of 150") + defaultExecutor(repl, "30 is 20%% of what") } func TestExecutorWithInvalidPercentage(t *testing.T) { - executor("invalid percentage input") + repl := createTestREPL() + defaultExecutor(repl, "invalid percentage input") } func TestExecutorWithOperatorOnly(t *testing.T) { - executor("1 2 +") - executor("+") + repl := createTestREPL() + defaultExecutor(repl, "1 2 +") + defaultExecutor(repl, "+") } func TestExecutorWithRPNPrefix(t *testing.T) { - executor("rpn 3 4 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 3 4 +") } func TestExecutorWithCalcPrefix(t *testing.T) { - executor("calc 5 6 +") + repl := createTestREPL() + defaultExecutor(repl, "calc 5 6 +") } func TestExecutorWithEmptyInput(t *testing.T) { - executor("") + repl := createTestREPL() + defaultExecutor(repl, "") } func TestExecutorWithWhitespaceOnly(t *testing.T) { - executor(" ") + repl := createTestREPL() + defaultExecutor(repl, " ") } func TestExecutorWithInvalidInput(t *testing.T) { tests := []string{"invalid input", "not a valid command", "xyz"} for _, input := range tests { t.Run(input, func(t *testing.T) { - executor(input) + repl := createTestREPL() + defaultExecutor(repl, input) }) } } func TestExecutorWithInvalidRPN(t *testing.T) { - executor("rpn 1 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 +") } func TestExecutorWithEmptyRPNPrefix(t *testing.T) { - executor("rpn") - executor("calc") + repl := createTestREPL() + defaultExecutor(repl, "rpn") + defaultExecutor(repl, "calc") } func TestExecutorWithAssignment(t *testing.T) { - executor("rpn x 42 =") - executor("rpn x") + repl := createTestREPL() + defaultExecutor(repl, "rpn x 42 =") + defaultExecutor(repl, "rpn x") } func TestExecutorWithPercentageAndRPNFallback(t *testing.T) { - executor("20% of 150") - executor("3 4 +") -} - -func TestGetHistoryPath(t *testing.T) { - path := getHistoryPath() - if path == "" { - t.Error("getHistoryPath() returned empty string") - } -} - -func TestLoadHistory(t *testing.T) { - history := loadHistory() - _ = history -} - -func TestSaveHistory(t *testing.T) { - err := saveHistory([]string{"test1", "test2"}) - _ = err + repl := createTestREPL() + defaultExecutor(repl, "20% of 150") + defaultExecutor(repl, "3 4 +") } func TestExecutorWithRPNExpressionOnly(t *testing.T) { - executor("5 3 +") + repl := createTestREPL() + defaultExecutor(repl, "5 3 +") } func TestExecutorWithRPNThenOperator(t *testing.T) { - executor("1 2 +") - executor("+") + repl := createTestREPL() + defaultExecutor(repl, "1 2 +") + defaultExecutor(repl, "+") } func TestExecutorWithRPNThenRPN(t *testing.T) { - executor("rpn 1 2 +") - executor("rpn 3 4 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 2 +") + defaultExecutor(repl, "rpn 3 4 +") } func TestExecutorWithRPNShow(t *testing.T) { - executor("rpn show") + repl := createTestREPL() + defaultExecutor(repl, "rpn show") } func TestExecutorWithRPNDup(t *testing.T) { - executor("rpn dup") + repl := createTestREPL() + defaultExecutor(repl, "rpn dup") } func TestExecutorWithRPNSwap(t *testing.T) { - executor("rpn swap") + repl := createTestREPL() + defaultExecutor(repl, "rpn swap") } func TestExecutorWithRPNSingle(t *testing.T) { - executor("rpn 42") + repl := createTestREPL() + defaultExecutor(repl, "rpn 42") } func TestExecutorWithRPNMulti(t *testing.T) { - executor("rpn 1 2 3 4 5 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 2 3 4 5 +") } func TestExecutorWithStackOps(t *testing.T) { - executor("dup") - executor("swap") - executor("pop") - executor("show") + repl := createTestREPL() + defaultExecutor(repl, "dup") + defaultExecutor(repl, "swap") + defaultExecutor(repl, "pop") + defaultExecutor(repl, "show") } func TestExecutorWithRPNClear(t *testing.T) { - executor("rpn clear") + repl := createTestREPL() + defaultExecutor(repl, "rpn clear") } func TestExecutorWithHistoryCommands(t *testing.T) { - executor("vars") - executor("clear") + repl := createTestREPL() + defaultExecutor(repl, "vars") + defaultExecutor(repl, "clear") } func TestExecutorWithMixedInput(t *testing.T) { - executor("25% of 200") - executor("10 20 +") + repl := createTestREPL() + defaultExecutor(repl, "25% of 200") + defaultExecutor(repl, "10 20 +") } func TestExecutorWithRPNCalcMixed(t *testing.T) { - executor("rpn 1 2 +") - executor("3 4 +") - executor("calc 5 6 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 2 +") + defaultExecutor(repl, "3 4 +") + defaultExecutor(repl, "calc 5 6 +") } func TestExecutorCommandsEdgeCases(t *testing.T) { - executor(" clear ") - executor("HELP") - executor("CLEAR") + repl := createTestREPL() + defaultExecutor(repl, " clear ") + defaultExecutor(repl, "HELP") + defaultExecutor(repl, "CLEAR") } func TestExecutorWithRPMPrefix(t *testing.T) { - executor("rpn 1 2 +") + repl := createTestREPL() + defaultExecutor(repl, "rpn 1 2 +") } func TestExecutorWithCalcPrefixMixed(t *testing.T) { - executor("calc 1 2 +") + repl := createTestREPL() + defaultExecutor(repl, "calc 1 2 +") } +// TestExecutorWithRatModeOn tests that rat on works with fresh REPL func TestExecutorWithRatModeOn(t *testing.T) { - executor("rat on") - state := getRPNState() - if state.rpnCalc.GetMode() != rpn.RationalMode { - t.Errorf("Expected RationalMode after rat on, got %v", state.rpnCalc.GetMode()) + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "rat on") + if rpnCalc.GetMode() != rpn.RationalMode { + t.Errorf("Expected RationalMode after rat on, got %v", rpnCalc.GetMode()) } } +// TestExecutorWithRatModeOff tests that rat off works with fresh REPL func TestExecutorWithRatModeOff(t *testing.T) { - executor("rat off") - state := getRPNState() - if state.rpnCalc.GetMode() != rpn.FloatMode { - t.Errorf("Expected FloatMode after rat off, got %v", state.rpnCalc.GetMode()) + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + defaultExecutor(rpl, "rat off") + if rpnCalc.GetMode() != rpn.FloatMode { + t.Errorf("Expected FloatMode after rat off, got %v", rpnCalc.GetMode()) } } +// TestExecutorWithRatModeToggle tests that rat toggle works with fresh REPL func TestExecutorWithRatModeToggle(t *testing.T) { - // First toggle - should enable rational mode if currently float - executor("rat toggle") - state := getRPNState() - mode1 := state.rpnCalc.GetMode() - - // Second toggle - should toggle back - executor("rat toggle") - state = getRPNState() - mode2 := state.rpnCalc.GetMode() - + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + + // First toggle + defaultExecutor(rpl, "rat toggle") + mode1 := rpnCalc.GetMode() + + // Second toggle + defaultExecutor(rpl, "rat toggle") + mode2 := rpnCalc.GetMode() + // Modes should be different after toggle if mode1 == mode2 { t.Errorf("Modes should be different after toggle: %v -> %v", mode1, mode2) @@ -411,13 +466,15 @@ func TestExecutorWithRatModeToggle(t *testing.T) { } func TestExecutorWithRatModeInvalid(t *testing.T) { + repl := createTestREPL() // Just verify it doesn't panic - executor("rat invalid") + defaultExecutor(repl, "rat invalid") } func TestExecutorWithRatModeNoArg(t *testing.T) { + repl := createTestREPL() // Just verify it doesn't panic - executor("rat") + defaultExecutor(repl, "rat") } func TestIsBuiltinCommandWithSubcommandHelp(t *testing.T) { @@ -427,222 +484,31 @@ func TestIsBuiltinCommandWithSubcommandHelp(t *testing.T) { } } -func TestRPNHandlerWithUnknownInput(t *testing.T) { - // Test that unknown input falls through to next handler - chain := NewCommandChain() - - // Create a minimal REPL - r := &REPL{ - ttyChecker: &TTYChecker{}, - historyMgr: NewHistoryManager(".gt_history"), - signalHandler: NewSignalHandler(), - commandChain: chain, - } - - // Test unknown input - should not be handled by RPNHandler directly - // but will be handled by Error handler after RPNHandler passes it through - output, handled, err := chain.Handle(r, "unknowncommand") - if handled { - t.Errorf("Expected unknowncommand to be handled by error handler, got handled=%v, err=%v, output=%q", handled, err, output) - } -} - -func TestRPNHandlerWithPercentageExpression(t *testing.T) { - // Test that percentage expressions are handled by PercentageHandler, not RPNHandler - chain := NewCommandChain() - r := &REPL{ - ttyChecker: &TTYChecker{}, - historyMgr: NewHistoryManager(".gt_history"), - signalHandler: NewSignalHandler(), - commandChain: chain, - } - - // Test percentage expression - output, handled, err := chain.Handle(r, "20% of 150") - if !handled { - t.Errorf("Expected percentage expression to be handled, got handled=%v, err=%v, output=%q", handled, err, output) - } - if err != nil { - t.Errorf("Expected no error for percentage expression, got %v", err) - } -} - -func TestRPNHandlerWithRPNExpression(t *testing.T) { - // Test RPN expressions - chain := NewCommandChain() - vars := rpn.NewVariables() - rpnState := &RPNState{ - vars: vars, - rpnCalc: rpn.NewRPN(vars), - } - r := &REPL{ - ttyChecker: &TTYChecker{}, - historyMgr: NewHistoryManager(".gt_history"), - signalHandler: NewSignalHandler(), - commandChain: chain, - rpnState: rpnState, - } - - // Test RPN expression - output, handled, err := chain.Handle(r, "3 4 +") - if !handled { - t.Errorf("Expected RPN expression to be handled, got handled=%v, err=%v, output=%q", handled, err, output) - } - if err != nil { - t.Errorf("Expected no error for RPN expression, got %v", err) - } -} - -func TestRPNHandlerWithSingleNumber(t *testing.T) { - // Test single number input (RPN - pushes number onto stack) - chain := NewCommandChain() +// TestExecutorWithAssignmentRight tests := and =: operators +func TestExecutorWithAssignmentRight(t *testing.T) { vars := rpn.NewVariables() - rpnState := &RPNState{ - vars: vars, - rpnCalc: rpn.NewRPN(vars), - } - r := &REPL{ + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ ttyChecker: &TTYChecker{}, historyMgr: NewHistoryManager(".gt_history"), signalHandler: NewSignalHandler(), - commandChain: chain, - rpnState: rpnState, - } - - // Test single number - output, handled, err := chain.Handle(r, "42") - if !handled { - t.Errorf("Expected single number to be handled, got handled=%v, err=%v, output=%q", handled, err, output) - } - if err != nil { - t.Errorf("Expected no error for single number, got %v", err) - } -} - -// TestNewREPL tests that NewREPL creates a valid REPL instance. -// Note: This test is skipped when not running in a TTY because the prompt -// library requires TTY access. -func TestNewREPL(t *testing.T) { - // Skip this test if not running in a TTY - ttyChecker := &TTYChecker{} - if !ttyChecker.IsTTY() { - t.Skip("Skipping test - not running in a TTY") - } - - // Test that NewREPL creates a valid REPL instance without panicking - repl := NewREPL(nil, nil) - if repl == nil { - t.Fatal("Expected REPL to be created, got nil") - } - if repl.prompt == nil { - t.Error("Expected prompt to be set") - } - if repl.commandChain == nil { - t.Error("Expected commandChain to be set") - } - if repl.ttyChecker == nil { - t.Error("Expected ttyChecker to be set") - } - if repl.historyMgr == nil { - t.Error("Expected historyMgr to be set") - } - if repl.signalHandler == nil { - t.Error("Expected signalHandler to be set") - } -} - -func TestDefaultCompleter(t *testing.T) { - // Test the default completer function directly - // Note: This test has limited coverage because defaultCompleter uses - // GetWordBeforeCursor() which requires proper cursor position. - // The actual completer logic is tested in completer_test.go - - // Test with text that would match if cursor position was set correctly - repl := &REPL{} - doc := prompt.Document{Text: "h"} - suggestions := defaultCompleter(repl, doc) - - // When cursor is at position 0 (default), GetWordBeforeCursor returns empty - // But the test in completer_test.go verifies the actual behavior - _ = suggestions - - // Test with clear prefix - doc2 := prompt.Document{Text: "cl"} - suggestions2 := defaultCompleter(repl, doc2) - _ = suggestions2 -} - -func TestDefaultGetCommandDescription(t *testing.T) { - // Create a REPL and test the defaultGetCommandDescription method - repl := &REPL{} - - tests := []struct { - cmd string - wantPrefix string - }{ - {"help", "Show"}, - {"clear", "Clear"}, - {"quit", "Exit"}, - {"exit", "Exit"}, - {"rpn", "Evaluate"}, - {"calc", "Same"}, - } - - for _, tt := range tests { - t.Run(tt.cmd, func(t *testing.T) { - desc := repl.defaultGetCommandDescription(tt.cmd) - if !strings.Contains(desc, tt.wantPrefix) { - t.Errorf("defaultGetCommandDescription(%q) = %q, should contain %q", tt.cmd, desc, tt.wantPrefix) - } - }) + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, } -} - -func TestExecutorWithUnknownCommand(t *testing.T) { - // Test that unknown commands are handled by the error handler - // This should exercise the "Not handled by any handler" path - executor("completelyunknowncommand123") -} - -func TestDefaultExecutorCodePaths(t *testing.T) { - // Test all code paths in defaultExecutor - // 1. Empty input (returns early at line 110) - // 2. Handled=true with error (prints error, returns at line 124) - // 3. Handled=true with output (prints output, returns at line 124) - // 4. Handled=false with error (prints error at line 130) - // 5. Handled=false without error (does nothing) - - // Path 1: Empty input - executor("") - - // Path 2: Built-in command with error (clear should not error but let's verify) - executor("clear") - - // Path 3: Built-in command with output (help returns help text) - executor("help") - - // Path 4: Unknown command (error handler returns handled=false, err!=nil) - executor("completelyunknowncommand123") - - // Path 5: Whitespace only (trimmed to empty, returns early) - executor(" ") -} - -func TestExecutorWithAssignmentRight(t *testing.T) { - // Test := and =: operators - executor("5 x :=") - state := getRPNState() - val, exists := state.vars.GetVariable("x") + + // Test := operator + defaultExecutor(rpl, "5 x :=") + val, exists := vars.GetVariable("x") if !exists { t.Errorf("Variable x should exist after x :=") } if val != 5 { t.Errorf("Variable x = %v, want 5", val) } - - executor("y 3 =:") - state = getRPNState() - val, exists = state.vars.GetVariable("y") + + // Test =: operator + defaultExecutor(rpl, "y 3 =:") + val, exists = vars.GetVariable("y") if !exists { t.Errorf("Variable y should exist after y =:") } @@ -651,14 +517,21 @@ func TestExecutorWithAssignmentRight(t *testing.T) { } } - +// TestExecutorWithAssignmentAfterCalculation tests assignment after a calculation func TestExecutorWithAssignmentAfterCalculation(t *testing.T) { + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + // Test that assignment works after a calculation - // Note: This test uses a fresh variable name to avoid conflicts with previous tests - // that may have set x=5 from TestExecutorWithAssignmentRight - executor("1 2 + z =:") - state := getRPNState() - val, exists := state.vars.GetVariable("z") + defaultExecutor(rpl, "1 2 + z =:") + val, exists := vars.GetVariable("z") if !exists { t.Errorf("Variable z should exist") } @@ -667,15 +540,25 @@ func TestExecutorWithAssignmentAfterCalculation(t *testing.T) { } } +// TestExecutorWithIncrementalAssignment tests that assignment works after a calculation with separate commands func TestExecutorWithIncrementalAssignment(t *testing.T) { - // Test that assignment works after a calculation with separate commands - // This should use the value from the stack for assignment - executor("1 2 +") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + + // Test that assignment works after a calculation + defaultExecutor(rpl, "1 2 +") // Now use z =: to assign the top of stack (3) to variable z - executor("z =:") - - val, exists := getRPNState().vars.GetVariable("z") + defaultExecutor(rpl, "z =:") + + val, exists := vars.GetVariable("z") if !exists { t.Errorf("Variable z should exist after z =:") } @@ -686,12 +569,22 @@ func TestExecutorWithIncrementalAssignment(t *testing.T) { // TestExecutorWithSimpleIncrementalAssignment tests x =: after 2 in REPL func TestExecutorWithSimpleIncrementalAssignment(t *testing.T) { + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + // First execute 2 to put it on the stack - executor("2") + defaultExecutor(rpl, "2") // Then use x =: to assign the top of stack to variable x - executor("x =:") - val, exists := getRPNState().vars.GetVariable("x") + defaultExecutor(rpl, "x =:") + val, exists := vars.GetVariable("x") if !exists { t.Errorf("Variable x should exist after x =:") } @@ -702,18 +595,28 @@ func TestExecutorWithSimpleIncrementalAssignment(t *testing.T) { // TestExecutorWithExactUserScenario tests the exact user scenario: 2 then x =: func TestExecutorWithExactUserScenario(t *testing.T) { + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + // This test replicates the exact user interaction: // > 2 // > x =: // The variable should be assigned the value 2 - - executor("2") + + defaultExecutor(rpl, "2") // Verify stack has 2 // (can't directly check stack without exposing it, but next command will fail if stack is empty) - executor("x =:") - val, exists := getRPNState().vars.GetVariable("x") + defaultExecutor(rpl, "x =:") + val, exists := vars.GetVariable("x") if !exists { t.Errorf("Variable x should exist after x =:") } @@ -724,20 +627,28 @@ func TestExecutorWithExactUserScenario(t *testing.T) { // TestExecutorWithExactUserScenarioWithOutput tests that x =: assigns and shows result func TestExecutorWithExactUserScenarioWithOutput(t *testing.T) { - // First, clear state - executor("rpn clear") + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + + // Clear any previous state + defaultExecutor(rpl, "rpn clear") // Put 2 on stack - executor("2") - state := getRPNState() - _, _ = state.rpnCalc.ResultStack([]string{}) + defaultExecutor(rpl, "2") + _, _ = rpnCalc.ResultStack([]string{}) // Assign to x =: - result, err := state.rpnCalc.ParseAndEvaluate("x =:") + result, err := rpnCalc.ParseAndEvaluate("x =:") t.Logf("ParseAndEvaluate('x =:') returned result=%q, err=%v", result, err) - state = getRPNState() - val, exists := state.vars.GetVariable("x") + val, exists := vars.GetVariable("x") if !exists { t.Errorf("Variable x should exist after x =:") } @@ -748,18 +659,27 @@ func TestExecutorWithExactUserScenarioWithOutput(t *testing.T) { // TestExecutorWithExactUserScenarioDirect simulates REPL input flow func TestExecutorWithExactUserScenarioDirect(t *testing.T) { + vars := rpn.NewVariables() + rpnCalc := rpn.NewRPN(vars) + rpl := &REPL{ + ttyChecker: &TTYChecker{}, + historyMgr: NewHistoryManager(".gt_history"), + signalHandler: NewSignalHandler(), + commandChain: NewCommandChain(), + rpnState: &RPNState{vars: vars, rpnCalc: rpnCalc}, + } + // Clear any previous state - executor("rpn clear") + defaultExecutor(rpl, "rpn clear") // Simulate typing "2" in REPL - executor("2") + defaultExecutor(rpl, "2") // Simulate typing "x =:" in REPL - executor("x =:") + defaultExecutor(rpl, "x =:") // Verify variable was set - state := getRPNState() - val, exists := state.vars.GetVariable("x") + val, exists := vars.GetVariable("x") if !exists { t.Errorf("Variable x should exist after x =:") } @@ -767,3 +687,29 @@ func TestExecutorWithExactUserScenarioDirect(t *testing.T) { t.Errorf("Variable x = %v, want 2", val) } } + +func TestExecutorWithUnknownCommand(t *testing.T) { + repl := createTestREPL() + // Test that unknown commands are handled by the error handler + defaultExecutor(repl, "completelyunknowncommand123") +} + +func TestDefaultExecutorCodePaths(t *testing.T) { + // Test all code paths in defaultExecutor + repl := createTestREPL() + + // Path 1: Empty input + defaultExecutor(repl, "") + + // Path 2: Built-in command with error (clear should not error but let's verify) + defaultExecutor(repl, "clear") + + // Path 3: Built-in command with output (help returns help text) + defaultExecutor(repl, "help") + + // Path 4: Unknown command (error handler returns handled=false, err!=nil) + defaultExecutor(repl, "completelyunknowncommand123") + + // Path 5: Whitespace only (trimmed to empty, returns early) + defaultExecutor(repl, " ") +} diff --git a/internal/rpn/number.go b/internal/rpn/number.go index 2915a2a..1dd71e0 100644 --- a/internal/rpn/number.go +++ b/internal/rpn/number.go @@ -123,8 +123,12 @@ func (f *Float) Add(other Number) (Number, error) { if err != nil { return nil, fmt.Errorf("cannot add: %w", err) } + fF, err := f.Float64() + if err != nil { + return nil, fmt.Errorf("cannot add: %w", err) + } // Use Float64() to handle both regular numbers and boolean values - return NewFloat(f.n + otherF), nil + return NewFloat(fF + otherF), nil } // Sub returns the difference of two float numbers. @@ -133,8 +137,12 @@ func (f *Float) Sub(other Number) (Number, error) { if err != nil { return nil, fmt.Errorf("cannot subtract: %w", err) } + fF, err := f.Float64() + if err != nil { + return nil, fmt.Errorf("cannot subtract: %w", err) + } // Use Float64() to handle both regular numbers and boolean values - return NewFloat(f.n - otherF), nil + return NewFloat(fF - otherF), nil } // Mul returns the product of two float numbers. @@ -143,8 +151,12 @@ func (f *Float) Mul(other Number) (Number, error) { if err != nil { return nil, fmt.Errorf("cannot multiply: %w", err) } + fF, err := f.Float64() + if err != nil { + return nil, fmt.Errorf("cannot multiply: %w", err) + } // Use Float64() to handle both regular numbers and boolean values - return NewFloat(f.n * otherF), nil + return NewFloat(fF * otherF), nil } // Div returns the quotient of two float numbers. @@ -153,11 +165,15 @@ func (f *Float) Div(other Number) (Number, error) { if err != nil { return nil, fmt.Errorf("cannot divide: %w", err) } + fF, err := f.Float64() + if err != nil { + return nil, fmt.Errorf("cannot divide: %w", err) + } if other.IsZero() { return nil, fmt.Errorf("division by zero") } // Use Float64() to handle both regular numbers and boolean values - return NewFloat(f.n / otherF), nil + return NewFloat(fF / otherF), nil } // Pow returns this float raised to the power of another. @@ -166,8 +182,12 @@ func (f *Float) Pow(other Number) (Number, error) { if err != nil { return nil, fmt.Errorf("cannot power: %w", err) } + fF, err := f.Float64() + if err != nil { + return nil, fmt.Errorf("cannot power: %w", err) + } // Use Float64() to handle both regular numbers and boolean values - return NewFloat(math.Pow(f.n, otherF)), nil + return NewFloat(math.Pow(fF, otherF)), nil } // Mod returns the remainder of this float divided by another. @@ -176,11 +196,15 @@ func (f *Float) Mod(other Number) (Number, error) { if err != nil { return nil, fmt.Errorf("cannot modulo: %w", err) } + fF, err := f.Float64() + if err != nil { + return nil, fmt.Errorf("cannot modulo: %w", err) + } if other.IsZero() { return nil, fmt.Errorf("modulo by zero") } // Use Float64() to handle both regular numbers and boolean values - return NewFloat(math.Mod(f.n, otherF)), nil + return NewFloat(math.Mod(fF, otherF)), nil } // IsZero returns true if the float is zero. @@ -277,7 +301,10 @@ func (r *Rat) Float64() (float64, error) { } return 0, nil } - f, _ := r.n.Float64() + f, ok := r.n.Float64() + if !ok { + return 0, fmt.Errorf("cannot convert rational number to float64") + } return f, nil } diff --git a/internal/rpn/operations_test.go b/internal/rpn/operations_test.go index b9dcc98..013e212 100644 --- a/internal/rpn/operations_test.go +++ b/internal/rpn/operations_test.go @@ -35,7 +35,9 @@ func TestStackPushPop(t *testing.T) { if err != nil { t.Fatalf("Pop() returned error: %v", err) } - if val.Float64() != 3.0 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 3.0 { t.Errorf("Pop() = %v, want 3.0", val) } @@ -52,7 +54,9 @@ func TestStackPeek(t *testing.T) { if err != nil { t.Fatalf("Peek() returned error: %v", err) } - if val.Float64() != 5.0 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 5.0 { t.Errorf("Peek() = %v, want 5.0", val) } @@ -94,8 +98,14 @@ func TestStackValues(t *testing.T) { // Values() returns values in storage order (bottom-to-top) // Push order: 1, 2, 3 so storage is [1, 2, 3] with 3 on top - if vals[0].Float64() != 1.0 || vals[1].Float64() != 2.0 || vals[2].Float64() != 3.0 { - t.Errorf("Values() = %v, want [1 2 3] (bottom-to-top)", vals) + for i, v := range vals { + val, err := v.Float64() + if err != nil { + t.Fatalf("Float64() returned error: %v", err) + } + if val != float64(i+1) { + t.Errorf("Values()[%d] = %v, want %d", i, v, i+1) + } } } @@ -128,7 +138,9 @@ func TestOperationsAdd(t *testing.T) { if err != nil { t.Fatalf("Pop() after Add() returned error: %v", err) } - if val.Float64() != 7.0 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 7.0 { t.Errorf("Add result = %v, want 7.0", val) } } @@ -149,7 +161,9 @@ func TestOperationsSubtract(t *testing.T) { if err != nil { t.Fatalf("Pop() after Subtract() returned error: %v", err) } - if val.Float64() != 6.0 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 6.0 { t.Errorf("Subtract result = %v, want 6.0 (10 - 4)", val) } } @@ -170,7 +184,9 @@ func TestOperationsMultiply(t *testing.T) { if err != nil { t.Fatalf("Pop() after Multiply() returned error: %v", err) } - if val.Float64() != 15.0 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 15.0 { t.Errorf("Multiply result = %v, want 15.0", val) } } @@ -191,7 +207,9 @@ func TestOperationsDivide(t *testing.T) { if err != nil { t.Fatalf("Pop() after Divide() returned error: %v", err) } - if val.Float64() != 5.0 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 5.0 { t.Errorf("Divide result = %v, want 5.0", val) } } @@ -228,7 +246,9 @@ func TestOperationsPower(t *testing.T) { if err != nil { t.Fatalf("Pop() after Power() returned error: %v", err) } - if val.Float64() != 8.0 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 8.0 { t.Errorf("Power result = %v, want 8.0 (2^3)", val) } } @@ -249,7 +269,9 @@ func TestOperationsModulo(t *testing.T) { if err != nil { t.Fatalf("Pop() after Modulo() returned error: %v", err) } - if val.Float64() != 1.0 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 1.0 { t.Errorf("Modulo result = %v, want 1.0 (10 %% 3)", val) } } @@ -297,8 +319,15 @@ func TestOperationsDup(t *testing.T) { val1, _ := s.Pop() val2, _ := s.Pop() - if val1.Float64() != 7.0 || val2.Float64() != 7.0 { - t.Errorf("Dup values = %v, %v, want both 7.0", val1, val2) + if v1, err := val1.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v1 != 7.0 { + t.Errorf("val1.Float64() = %v, want 7.0", v1) + } + if v2, err := val2.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v2 != 7.0 { + t.Errorf("val2.Float64() = %v, want 7.0", v2) } } @@ -316,8 +345,15 @@ func TestOperationsSwap(t *testing.T) { val1, _ := s.Pop() val2, _ := s.Pop() - if val1.Float64() != 1.0 || val2.Float64() != 2.0 { - t.Errorf("After Swap, values = %v, %v, want 1.0, 2.0 (swapped)", val1, val2) + if v1, err := val1.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v1 != 1.0 { + t.Errorf("val1.Float64() = %v, want 1.0", v1) + } + if v2, err := val2.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v2 != 2.0 { + t.Errorf("val2.Float64() = %v, want 2.0", v2) } } @@ -449,7 +485,9 @@ func TestOperationsUseVariable(t *testing.T) { if err != nil { t.Fatalf("Pop() after UseVariable() returned error: %v", err) } - if val.Float64() != 3.14159 { + if v, err := val.Float64(); err != nil { + t.Fatalf("Float64() returned error: %v", err) + } else if v != 3.14159 { t.Errorf("Variable value pushed to stack = %v, want 3.14159", val) } } @@ -576,8 +614,10 @@ func TestLog2(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 3.0 { - t.Errorf("Log2(8) = %f, want 3.0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 3.0 { + t.Errorf("Log2(8) = %f, want 3.0)", v) } // Test log₂(1) = 0 @@ -590,8 +630,10 @@ func TestLog2(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 0.0 { - t.Errorf("Log2(1) = %f, want 0.0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 0.0 { + t.Errorf("Log2(1) = %f, want 0.0)", v) } // Test log₂(0) should error @@ -616,8 +658,10 @@ func TestLog10(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 2.0 { - t.Errorf("Log10(100) = %f, want 2.0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 2.0 { + t.Errorf("Log10(100) = %f, want 2.0)", v) } // Test log₁₀(1) = 0 @@ -630,8 +674,10 @@ func TestLog10(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 0.0 { - t.Errorf("Log10(1) = %f, want 0.0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 0.0 { + t.Errorf("Log10(1) = %f, want 0.0)", v) } } @@ -649,8 +695,10 @@ func TestLn(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if math.Abs(val.Float64()-1.0) > 0.0001 { - t.Errorf("ln(e) = %f, want ~1.0", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if math.Abs(v-1.0) > 0.0001 { + t.Errorf("ln(e) = %f, want ~1.0", v) } // Test ln(1) = 0 @@ -663,8 +711,10 @@ func TestLn(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 0.0 { - t.Errorf("Ln(1) = %f, want 0.0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 0.0 { + t.Errorf("Ln(1) = %f, want 0.0)", v) } } @@ -682,8 +732,10 @@ func TestLog2WithBoolean(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 0.0 { - t.Errorf("Log2(true) = %f, want 0.0 (log₂(1) = 0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 0.0 { + t.Errorf("Log2(true) = %f, want 0.0 (log₂(1) = 0)", v) } // Test with boolean false (should be converted to 0, log₂(0) should error) @@ -708,8 +760,10 @@ func TestLog10WithBoolean(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 0.0 { - t.Errorf("Log10(true) = %f, want 0.0 (log₁₀(1) = 0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 0.0 { + t.Errorf("Log10(true) = %f, want 0.0 (log₁₀(1) = 0)", v) } // Test with boolean false (should be converted to 0, log₁₀(0) should error) @@ -734,8 +788,10 @@ func TestLnWithBoolean(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 0.0 { - t.Errorf("Ln(true) = %f, want 0.0 (ln(1) = 0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 0.0 { + t.Errorf("Ln(true) = %f, want 0.0 (ln(1) = 0)", v) } // Test with boolean false (should be converted to 0, ln(0) should error) @@ -774,8 +830,10 @@ func TestLnEdgeCases(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() > -6.0 || val.Float64() < -7.0 { - t.Errorf("Ln(0.001) = %f, want ~-6.9 (ln(0.001))", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v > -6.0 || v < -7.0 { + t.Errorf("Ln(0.001) = %f, want ~-6.9 (ln(0.001))", v) } } @@ -795,8 +853,10 @@ func TestHyperLog2WithBoolean(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 2.0 { - t.Errorf("HyperLog2(4, true) = %f, want 2.0 (log₂(4) + log₂(1) = 2 + 0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 2.0 { + t.Errorf("HyperLog2(4, true) = %f, want 2.0 (log₂(4) + log₂(1) = 2 + 0)", v) } // Test hyperlog₂(4, false) = log₂(4) + log₂(0) should error @@ -825,8 +885,10 @@ func TestHyperLog10WithBoolean(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 1.0 { - t.Errorf("HyperLog10(10, true) = %f, want 1.0 (log₁₀(10) + log₁₀(1) = 1 + 0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 1.0 { + t.Errorf("HyperLog10(10, true) = %f, want 1.0 (log₁₀(10) + log₁₀(1) = 1 + 0)", v) } // Test hyperlog₁₀(10, false) = log₁₀(10) + log₁₀(0) should error @@ -854,8 +916,10 @@ func TestHyperLnWithBoolean(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if math.Abs(val.Float64()-1.0) > 0.0001 { - t.Errorf("HyperLn(e, true) = %f, want ~1.0 (ln(e) + ln(1) = 1 + 0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if math.Abs(v-1.0) > 0.0001 { + t.Errorf("HyperLn(e, true) = %f, want ~1.0 (ln(e) + ln(1) = 1 + 0)", v) } // Test hyperln(e, false) = ln(e) + ln(0) should error @@ -882,8 +946,10 @@ func TestHyperLog2(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 6.0 { - t.Errorf("HyperLog2(4, 16) = %f, want 6.0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 6.0 { + t.Errorf("HyperLog2(4, 16) = %f, want 6.0)", v) } // Test with single value (should error, like other hyper operators) @@ -909,8 +975,10 @@ func TestHyperLog10(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != 3.0 { - t.Errorf("HyperLog10(10, 100) = %f, want 3.0)", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != 3.0 { + t.Errorf("HyperLog10(10, 100) = %f, want 3.0)", v) } } @@ -929,8 +997,10 @@ func TestHyperLn(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if math.Abs(val.Float64()-3.0) > 0.0001 { - t.Errorf("HyperLn(e, e²) = %f, want ~3.0", val.Float64()) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if math.Abs(v-3.0) > 0.0001 { + t.Errorf("HyperLn(e, e²) = %f, want ~3.0", v) } } @@ -1005,8 +1075,10 @@ func TestOperatorRegistryHandleStandardOperator(t *testing.T) { if err != nil { t.Errorf("Pop() returned error: %v", err) } - if val.Float64() != tc.expected { - t.Errorf("Result = %f, want %f", val.Float64(), tc.expected) + if v, err := val.Float64(); err != nil { + t.Errorf("Float64() returned error: %v", err) + } else if v != tc.expected { + t.Errorf("Result = %f, want %f", v, tc.expected) } }) } diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go index 45531bb..9d6d8e2 100644 --- a/internal/rpn/rpn_parse.go +++ b/internal/rpn/rpn_parse.go @@ -506,8 +506,10 @@ func (r *RPN) handleOperator(stack *Stack, token string, tokenIndex int) (string // isValidIdentifier checks if a token looks like a valid variable identifier. // Valid identifiers contain only alphanumeric characters and underscores, // and start with a letter or underscore (not a digit or special character). -// For RPN symbol support, we also limit to single-character identifiers -// (like x, y, z) to avoid converting percentage expression words into symbols. +// +// IMPORTANT: To prevent natural language words (like "what", "is", "of") from +// being incorrectly treated as RPN symbols in mixed-mode expressions, +// this function currently restricts valid identifiers to a length of 1. func isValidIdentifier(token string) bool { if len(token) == 0 { return false -- cgit v1.2.3