diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-25 16:37:15 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-25 16:37:15 +0200 |
| commit | baea2931a8520858b4708a306ba5092e312b3f63 (patch) | |
| tree | 6d0ed6fc932d3b83b951e48760c35881f0951f6e /internal | |
| parent | 62b487ed9da06cd564237ef4df81cf2cffa11af9 (diff) | |
docs: Add comprehensive Go documentation for REPL functions
- Enhanced NewREPL documentation with detailed parameter descriptions
- Enhanced RunREPL documentation clarifying it's a convenience wrapper
- Improved executor documentation explaining backward compatibility and testing usage
- Enhanced defaultExecutor documentation with input processing details and panic recovery
- Enhanced defaultCompleter documentation with tab-completion behavior details
- Enhanced defaultGetCommandDescription documentation with command description details
- Improved TTYChecker methods (IsTTY, EnsureTTY) documentation
- Improved SignalHandler.Start method documentation
All exported and non-exported functions in the REPL package now have comprehensive documentation comments that describe their purpose, parameters, and return values.
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/repl/commands.go | 27 | ||||
| -rw-r--r-- | internal/repl/completer.go | 13 | ||||
| -rw-r--r-- | internal/repl/handlers.go | 99 | ||||
| -rw-r--r-- | internal/repl/history.go | 24 | ||||
| -rw-r--r-- | internal/repl/prompt.go | 44 | ||||
| -rw-r--r-- | internal/repl/repl.go | 113 | ||||
| -rw-r--r-- | internal/repl/repl_test.go | 10 | ||||
| -rw-r--r-- | internal/repl/signal.go | 10 | ||||
| -rw-r--r-- | internal/repl/tty.go | 8 | ||||
| -rw-r--r-- | internal/rpn/number.go | 17 | ||||
| -rw-r--r-- | internal/rpn/operations.go | 45 | ||||
| -rw-r--r-- | internal/rpn/variables.go | 74 |
12 files changed, 401 insertions, 83 deletions
diff --git a/internal/repl/commands.go b/internal/repl/commands.go index 6fb0145..be49df1 100644 --- a/internal/repl/commands.go +++ b/internal/repl/commands.go @@ -7,19 +7,30 @@ import ( // builtinCommandsList is the list of built-in REPL commands. // It's exposed as a variable to allow for dependency injection in tests. +// 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() } -// ExecuteCommand runs a built-in command and returns its output or error +// ExecuteCommand runs a built-in command and returns its output or error. +// It dispatches to the appropriate command handler based on the command name. +// +// cmd: the full command string (e.g., "help", "clear", "rpn 3 4 +") +// Returns the command output string and an error if the command failed func ExecuteCommand(cmd string) (string, error) { args := strings.Fields(cmd) if len(args) == 0 { @@ -44,6 +55,12 @@ func ExecuteCommand(cmd string) (string, error) { } } +// cmdHelp returns help text for built-in commands. +// When called with no subcommands, it returns comprehensive help for all commands. +// When called with a subcommand, it returns specific help for that command. +// +// subCmds: optional slice of subcommand arguments (e.g., ["help"] for "help help") +// Returns the help text as a string func cmdHelp(subCmds []string) string { helpText := `PERC - Percentage Calculator REPL @@ -106,12 +123,20 @@ Press Ctrl+D or type 'quit'/'exit' to exit. } } +// cmdClear clears the terminal screen using ANSI escape sequences. +// It prints \033[2J\033[H to clear all content and move the cursor to (0,0). +// +// Returns nil on success func cmdClear() error { // Clear screen using ANSI escape sequence fmt.Print("\033[2J\033[H") return nil } +// cmdQuit displays a farewell message and signals REPL exit. +// It's called when the user enters "quit" or "exit" commands. +// +// Returns nil (exit is handled by the REPL itself) func cmdQuit() error { fmt.Println("Goodbye!") return nil diff --git a/internal/repl/completer.go b/internal/repl/completer.go index c6df823..e9387fd 100644 --- a/internal/repl/completer.go +++ b/internal/repl/completer.go @@ -7,6 +7,13 @@ import ( ) // completer provides auto-completion for built-in commands. +// It returns suggestions for commands that match the current word being typed. +// The matching is case-insensitive and includes descriptions for each command. +// +// This function is typically used as the completer function for the prompt.Prompt. +// +// d: the current prompt.Document containing cursor position and text +// Returns a slice of prompt.Suggest for matching built-in commands func completer(d prompt.Document) []prompt.Suggest { text := d.GetWordBeforeCursor() @@ -44,7 +51,11 @@ func completer(d prompt.Document) []prompt.Suggest { return suggestions } -// getCommandDescription returns the description for a command. +// getCommandDescription returns the description for a built-in command. +// It's used by the completer function to provide helpful descriptions during tab-completion. +// +// cmd: the built-in command name (e.g., "help", "clear", "quit") +// Returns the description string for the command, or empty string if not found func getCommandDescription(cmd string) string { descriptions := map[string]string{ "help": "Show help information", diff --git a/internal/repl/handlers.go b/internal/repl/handlers.go index 626da51..622396f 100644 --- a/internal/repl/handlers.go +++ b/internal/repl/handlers.go @@ -9,24 +9,38 @@ import ( "codeberg.org/snonux/perc/internal/rpn" ) -// CommandHandler represents a handler in the chain of responsibility -// Each handler can process a command or pass it to the next handler +// CommandHandler represents a handler in the chain of responsibility pattern. +// Each handler can process a command or pass it to the next handler in the chain. +// +// Handlers implement the Handle method to process REPL commands and expressions. +// If a handler cannot process the input, it calls Next() to forward to the next handler. type CommandHandler interface { Handle(repl *REPL, input string) (output string, handled bool, err error) SetNext(next CommandHandler) } -// BaseHandler provides common functionality for all handlers +// BaseHandler provides common functionality for all handlers in the chain. +// It stores a reference to the next handler and provides the Next() method +// for forwarding requests. type BaseHandler struct { next CommandHandler } -// SetNext sets the next handler in the chain +// SetNext sets the next handler in the chain. +// This enables building a chain of responsibility by linking handlers together. +// +// next: the next CommandHandler in the chain func (h *BaseHandler) SetNext(next CommandHandler) { h.next = next } -// Next forwards the request to the next handler in the chain +// Next forwards the request to the next handler in the chain. +// If there is no next handler, it returns (false, nil) indicating the request +// was not handled. +// +// repl: the REPL instance +// input: the command string to process +// Returns: (output string, handled bool, err error) func (h *BaseHandler) Next(repl *REPL, input string) (output string, handled bool, err error) { if h.next == nil { return "", false, nil @@ -34,12 +48,22 @@ func (h *BaseHandler) Next(repl *REPL, input string) (output string, handled boo return h.next.Handle(repl, input) } -// BuiltInCommandHandler handles built-in commands like help, clear, quit, exit +// BuiltInCommandHandler handles built-in commands like help, clear, quit, exit. +// It also handles special commands that require RPN state access (e.g., "rat"). +// If the input doesn't match a built-in command, it forwards to the next handler. type BuiltInCommandHandler struct { BaseHandler } -// Handle processes built-in commands +// Handle processes built-in commands from the input string. +// It first checks if the input starts with a built-in command using isBuiltinCommand. +// Special handling is provided for the "rat" command which requires RPN state access. +// If the command is handled, it returns the output and sets handled=true. +// Otherwise, it forwards to the next handler in the chain. +// +// repl: the REPL instance +// input: the command string to process +// Returns: (output string, handled bool, err error) func (h *BuiltInCommandHandler) Handle(repl *REPL, input string) (output string, handled bool, err error) { if cmd, ok := isBuiltinCommand(input); ok { args := strings.Fields(cmd) @@ -60,6 +84,16 @@ func (h *BuiltInCommandHandler) Handle(repl *REPL, input string) (output string, } // handleRatCommand handles the rat mode command with access to RPN state. +// It allows switching between float64 and rational number modes for RPN calculations. +// +// Valid modes: +// - "on": Enable rational number mode +// - "off": Disable rational mode (use float64) +// - "toggle": Switch between the two modes +// +// repl: the REPL instance (provides access to RPN state) +// input: the full command string (e.g., "rat on") +// Returns: (output string, handled bool, err error) func handleRatCommand(repl *REPL, input string) (string, bool, error) { args := strings.Fields(input) if len(args) < 2 { @@ -89,12 +123,25 @@ func handleRatCommand(repl *REPL, input string) (string, bool, error) { } } -// RPNHandler handles RPN expressions and RPN-related commands +// RPNHandler handles RPN (Reverse Polish Notation) expressions and RPN-related commands. +// It processes commands with "rpn" or "calc" prefixes, bare RPN expressions, +// and single RPN operators (e.g., "+", "dup", "swap", "show"). type RPNHandler struct { BaseHandler } -// Handle processes RPN commands and expressions +// Handle processes RPN commands and expressions. +// It handles: +// - Commands with "rpn" or "calc" prefix +// - Bare RPN expressions (e.g., "3 4 +") +// - Single RPN operators on the current stack +// - Single numbers (push onto stack) +// +// If the input doesn't match any RPN pattern, it forwards to the next handler. +// +// repl: the REPL instance (provides access to RPN state) +// input: the command string to process +// Returns: (output string, handled bool, err error) func (h *RPNHandler) Handle(repl *REPL, input string) (output string, handled bool, err error) { // Check for rpn/calc prefix lowerInput := strings.ToLower(input) @@ -155,12 +202,23 @@ func (h *RPNHandler) Handle(repl *REPL, input string) (output string, handled bo return h.Next(repl, input) } -// PercentageHandler handles percentage calculations +// PercentageHandler handles percentage calculation expressions. +// It uses the calculator.Parse function to evaluate expressions like: +// - "20% of 150" +// - "what is 20% of 150" +// - "30 is what % of 150" +// - "30 is 20% of what" type PercentageHandler struct { BaseHandler } -// Handle processes percentage calculation expressions +// Handle processes percentage calculation expressions. +// If the input matches a percentage expression pattern, it evaluates and returns +// the result. Otherwise, it forwards to the next handler. +// +// repl: the REPL instance +// input: the command string to process +// Returns: (output string, handled bool, err error) func (h *PercentageHandler) Handle(repl *REPL, input string) (output string, handled bool, err error) { // Run the percentage calculation result, err := calculator.Parse(input) @@ -171,18 +229,31 @@ func (h *PercentageHandler) Handle(repl *REPL, input string) (output string, han return result, true, nil } -// ErrorHandler handles unknown commands +// ErrorHandler handles unknown commands and invalid expressions. +// It returns an error indicating that the input was not recognized. type ErrorHandler struct { BaseHandler } -// Handle processes unknown commands by returning an error +// Handle processes unknown commands by returning an error. +// This is typically the last handler in the chain. +// +// repl: the REPL instance +// input: the command string that was not handled by previous handlers +// Returns: ("", false, error) with an error describing the unknown command func (h *ErrorHandler) Handle(repl *REPL, input string) (output string, handled bool, err error) { // Unknown command - return error return "", false, fmt.Errorf("unknown command or invalid expression: %s", input) } -// NewCommandChain creates and returns the complete command handling chain +// NewCommandChain creates and returns the complete command handling chain. +// The chain is built in the following order: +// 1. BuiltInCommandHandler: handles built-in commands (help, clear, quit, exit, rat) +// 2. RPNHandler: handles RPN expressions and operators +// 3. PercentageHandler: handles percentage calculations +// 4. ErrorHandler: handles unknown commands (returns error) +// +// Returns a CommandHandler representing the first handler in the chain func NewCommandChain() CommandHandler { // Create handlers builtInHandler := &BuiltInCommandHandler{} diff --git a/internal/repl/history.go b/internal/repl/history.go index a43b29b..f670dc7 100644 --- a/internal/repl/history.go +++ b/internal/repl/history.go @@ -7,13 +7,18 @@ import ( "path/filepath" ) -// HistoryManager handles history file operations. +// HistoryManager handles history file operations for the REPL. +// It provides methods to load, save, and manage command history with a maximum entry limit. type HistoryManager struct { historyFile string maxEntries int } // NewHistoryManager creates a new history manager with the given file name. +// The history manager will store up to maxEntries (default: 1000) in the history file. +// +// historyFile: the filename to use for history (without path) +// Returns a new HistoryManager instance func NewHistoryManager(historyFile string) *HistoryManager { return &HistoryManager{ historyFile: historyFile, @@ -21,7 +26,10 @@ func NewHistoryManager(historyFile string) *HistoryManager { } } -// Path returns the path to the history file. +// Path returns the absolute path to the history file. +// The history file is stored in the user's home directory. +// +// Returns the full path to the history file, or empty string if the home directory cannot be determined func (h *HistoryManager) Path() string { home, err := os.UserHomeDir() if err != nil { @@ -30,7 +38,10 @@ func (h *HistoryManager) Path() string { return filepath.Join(home, h.historyFile) } -// Load reads history from file. +// Load reads history from the history file. +// It returns all entries from the file, or nil if the file doesn't exist. +// +// Returns a slice of history entries (each line is one entry), or nil on error func (h *HistoryManager) Load() []string { path := h.Path() if path == "" { @@ -56,7 +67,12 @@ func (h *HistoryManager) Load() []string { return history } -// Save writes history to file, keeping only the most recent entries. +// Save writes history to the history file, keeping only the most recent entries. +// It ensures the file doesn't grow unlimited by keeping only the last maxEntries. +// The function creates the file if it doesn't exist and truncates it if needed. +// +// history: the slice of history entries to save +// Returns an error if the file cannot be written func (h *HistoryManager) Save(history []string) error { path := h.Path() if path == "" { diff --git a/internal/repl/prompt.go b/internal/repl/prompt.go index 3b99bb1..656ad6a 100644 --- a/internal/repl/prompt.go +++ b/internal/repl/prompt.go @@ -5,6 +5,7 @@ import ( ) // PromptBuilder constructs a prompt instance with the given configuration. +// It uses the builder pattern to configure all aspects of the prompt before calling Build. type PromptBuilder struct { prefix string title string @@ -14,7 +15,15 @@ type PromptBuilder struct { livePrefix func() (string, bool) } -// NewPromptBuilder creates a new prompt builder. +// NewPromptBuilder creates a new prompt builder with default values. +// Default values: +// - prefix: "> " +// - title: "gt - Percentage Calculator" +// - executor: empty function +// - completer: function that returns nil +// - livePrefix: function that returns ("> ", true) +// +// Returns a new PromptBuilder instance func NewPromptBuilder() *PromptBuilder { return &PromptBuilder{ prefix: "> ", @@ -25,43 +34,72 @@ func NewPromptBuilder() *PromptBuilder { } } -// SetPrefix sets the prompt prefix. +// SetPrefix sets the prompt prefix string. +// This is the string displayed before each input line (default: "> "). +// +// prefix: the prefix string to display +// Returns the builder for method chaining func (b *PromptBuilder) SetPrefix(prefix string) *PromptBuilder { b.prefix = prefix return b } // SetTitle sets the prompt title. +// This title is displayed in the terminal window/tab title. +// +// title: the title string to set +// Returns the builder for method chaining func (b *PromptBuilder) SetTitle(title string) *PromptBuilder { b.title = title return b } // SetHistory sets the history for the prompt. +// The history is a slice of strings representing previously entered commands. +// This allows users to navigate through their command history using arrow keys. +// +// history: the slice of history entries +// Returns the builder for method chaining func (b *PromptBuilder) SetHistory(history []string) *PromptBuilder { b.history = history return b } // SetExecutor sets the executor function for processing input. +// The executor is called for each non-empty input line after the user presses Enter. +// +// executor: the function to call with each input line +// Returns the builder for method chaining func (b *PromptBuilder) SetExecutor(executor func(string)) *PromptBuilder { b.executor = executor return b } // SetCompleter sets the completer function for auto-completion. +// The completer is called when the user presses Tab to get suggestions. +// +// completer: the function to call for tab-completion suggestions +// Returns the builder for method chaining func (b *PromptBuilder) SetCompleter(completer func(prompt.Document) []prompt.Suggest) *PromptBuilder { b.completer = completer return b } // SetLivePrefix sets the live prefix function. +// The live prefix is displayed on the left side of the current input line +// and can be used to show context-dependent information (e.g., multi-line input). +// +// livePrefix: the function that returns the current prefix string +// Returns the builder for method chaining func (b *PromptBuilder) SetLivePrefix(livePrefix func() (string, bool)) *PromptBuilder { b.livePrefix = livePrefix return b } -// Build creates and returns a new prompt instance. +// Build creates and returns a new prompt instance with the configured options. +// After calling Build, the PromptBuilder should not be modified. +// +// Returns a new prompt.Prompt instance ready to use with prompt.Run() func (b *PromptBuilder) Build() *prompt.Prompt { return prompt.New( b.executor, diff --git a/internal/repl/repl.go b/internal/repl/repl.go index d8b65c3..905b390 100644 --- a/internal/repl/repl.go +++ b/internal/repl/repl.go @@ -10,7 +10,9 @@ import ( "github.com/c-bata/go-prompt" ) -// RPNState holds the state for RPN operations in REPL. +// RPNState holds the state for RPN (Reverse Polish Notation) operations in the REPL. +// It maintains a variable store and RPN calculator instance. +// // Note: This struct should never be copied - use pointer receivers only. type RPNState struct { vars rpn.VariableStore @@ -18,12 +20,22 @@ type RPNState struct { } // rpnState holds the singleton RPN state for REPL operations. +// It is initialized lazily using sync.Once to ensure thread-safe initialization. var rpnState *RPNState // rpnStateOnce ensures rpnState is initialized exactly once. +// It's used by getRPNState to guarantee lazy singleton initialization. var rpnStateOnce sync.Once -// REPL manages the interactive command-line interface. +// 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. +// +// The REPL integrates various components: +// - TTYChecker: validates stdin is a terminal +// - HistoryManager: manages command history persistence +// - SignalHandler: handles SIGINT (Ctrl+C) +// - commandChain: processes commands via chain of responsibility type REPL struct { ttyChecker *TTYChecker historyMgr *HistoryManager @@ -33,8 +45,11 @@ type REPL struct { } // NewREPL creates a new REPL instance with default components. -// If executor is nil, it uses a default executor. -// If completer is nil, it uses a default completer. +// If executor is nil, it uses defaultExecutor which processes input through commandChain. +// If completer is nil, it uses defaultCompleter which provides built-in command suggestions. +// +// The executor function is called for each non-empty input line. +// The completer function provides tab-completion suggestions for the prompt. func NewREPL(executor func(string), completer func(prompt.Document) []prompt.Suggest) *REPL { repl := &REPL{ ttyChecker: &TTYChecker{}, @@ -93,7 +108,16 @@ func (r *REPL) Run() error { return nil } -// defaultExecutor is the default executor function. +// defaultExecutor is the default executor function used when no custom executor is provided. +// It processes input through the command chain of responsibility pattern. +// It includes panic recovery to gracefully handle unexpected errors during command execution. +// +// Input processing: +// - Trims whitespace from input +// - Skips empty input +// - Routes to commandChain for processing +// - Displays output and errors appropriately +// - Adds handled commands to history func defaultExecutor(r *REPL, input string) { // Add panic recovery for better resilience defer func() { @@ -128,7 +152,12 @@ func defaultExecutor(r *REPL, input string) { } } -// defaultCompleter is the default completer function. +// defaultCompleter is the default completer function used when no custom completer is provided. +// It provides tab-completion suggestions for built-in REPL commands. +// Suggestions are case-insensitive and include descriptions. +// +// d: the current prompt.Document containing cursor position and text +// Returns a slice of prompt.Suggest for matching built-in commands func defaultCompleter(r *REPL, d prompt.Document) []prompt.Suggest { text := d.GetWordBeforeCursor() if text == "" { @@ -147,7 +176,11 @@ func defaultCompleter(r *REPL, d prompt.Document) []prompt.Suggest { return suggestions } -// defaultGetCommandDescription returns the description for a command. +// defaultGetCommandDescription returns the description for a built-in command. +// It's used by the default completer to provide helpful descriptions during tab-completion. +// +// cmd: the built-in command name (e.g., "help", "clear", "quit") +// Returns the description string for the command, or empty string if not found func (r *REPL) defaultGetCommandDescription(cmd string) string { descriptions := map[string]string{ "help": "Show help information", @@ -160,16 +193,25 @@ func (r *REPL) defaultGetCommandDescription(cmd string) string { return descriptions[cmd] } -// RunREPL starts the interactive REPL. -// This is a convenience wrapper around NewREPL().Run(). +// RunREPL starts the interactive REPL with default components. +// This is a convenience wrapper around NewREPL(nil, nil).Run(). +// It's typically used when the standard REPL behavior is sufficient. +// +// Returns an error if the REPL cannot start (e.g., stdin is not a TTY) 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. -// It creates a minimal REPL instance without a prompt for testing purposes. +// 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) { // Create a minimal REPL instance without building a prompt r := &REPL{ @@ -181,8 +223,11 @@ func executor(input string) { defaultExecutor(r, input) } -// getRPNState returns or creates the RPN state. -// Thread-safe implementation using sync.Once for simpler singleton initialization. +// getRPNState returns or creates the RPN state using lazy initialization. +// It's thread-safe using sync.Once to ensure the RPN state is initialized exactly once. +// The RPN state is shared across all REPL instances. +// +// Returns the RPNState instance for performing RPN calculations func getRPNState() *RPNState { rpnStateOnce.Do(func() { vars := rpn.NewVariables() @@ -195,45 +240,67 @@ func getRPNState() *RPNState { } // getRPNState returns the RPN state. -// This is a REPL instance method for backward compatibility. +// This is a REPL instance method for backward compatibility that delegates to the package-level getRPNState. +// +// Returns the RPNState instance for performing RPN calculations func (r *REPL) getRPNState() *RPNState { return getRPNState() } -// runRPN parses and evaluates an RPN expression. +// runRPN parses and evaluates an RPN (Reverse Polish Notation) expression. +// It uses the shared RPN state to maintain stack state across multiple calls. +// +// input: the RPN expression to evaluate (e.g., "3 4 +" or "x 5 = x x +") +// Returns the result string and an error if the expression is invalid func runRPN(input string) (string, error) { state := getRPNState() return state.rpnCalc.ParseAndEvaluate(input) } -// runRPN parses and evaluates an RPN expression. -// This is a REPL instance method for backward compatibility. +// runRPN parses and evaluates an RPN (Reverse Polish Notation) expression. +// This is a REPL instance method for backward compatibility that delegates to the package-level runRPN. +// +// input: the RPN expression to evaluate +// Returns the result string and an error if the expression is invalid func (r *REPL) runRPN(input string) (string, error) { return runRPN(input) } -// getHistoryPath returns the path to the history file. +// 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 file. -// This is a package-level wrapper for backward compatibility. +// 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 file. -// This is a package-level wrapper for backward compatibility. +// 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 +// 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 { diff --git a/internal/repl/repl_test.go b/internal/repl/repl_test.go index f3314e6..6a8f20d 100644 --- a/internal/repl/repl_test.go +++ b/internal/repl/repl_test.go @@ -596,19 +596,19 @@ func TestDefaultExecutorCodePaths(t *testing.T) { // 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(" ") } diff --git a/internal/repl/signal.go b/internal/repl/signal.go index 9c7ec4d..e0a5ca2 100644 --- a/internal/repl/signal.go +++ b/internal/repl/signal.go @@ -7,11 +7,15 @@ import ( ) // SignalHandler manages signal handling for the REPL. +// It specifically listens for SIGINT (Ctrl+C) and executes a callback. type SignalHandler struct { sigChan chan os.Signal } // NewSignalHandler creates a new signal handler that listens for SIGINT. +// It creates a buffered channel to receive signals. +// +// Returns a new SignalHandler instance func NewSignalHandler() *SignalHandler { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT) @@ -21,6 +25,11 @@ func NewSignalHandler() *SignalHandler { } // Start starts the signal handler goroutine with the given callback. +// When SIGINT is received, the callback function is executed in the goroutine. +// The function blocks until Stop is called. +// +// callback: the function to execute when SIGINT is received +// Returns: no return value; executes callback in a separate goroutine func (s *SignalHandler) Start(callback func()) { go func() { <-s.sigChan @@ -29,6 +38,7 @@ func (s *SignalHandler) Start(callback func()) { } // Stop stops the signal handler by unregistering signals. +// After calling Stop, the signal handler will no longer trigger the callback. func (s *SignalHandler) Stop() { signal.Stop(s.sigChan) } diff --git a/internal/repl/tty.go b/internal/repl/tty.go index 955a890..c5e77a1 100644 --- a/internal/repl/tty.go +++ b/internal/repl/tty.go @@ -8,14 +8,22 @@ import ( ) // TTYChecker provides TTY detection functionality. +// It uses the go-isatty package to determine if stdin is a terminal. type TTYChecker struct{} // IsTTY returns true if stdin is a terminal. +// This is useful for determining whether to run in interactive REPL mode. +// +// Returns true if stdin is a TTY, false otherwise func (c *TTYChecker) IsTTY() bool { return isatty.IsTerminal(os.Stdin.Fd()) } // EnsureTTY checks if stdin is a TTY and returns an error if not. +// This is used to prevent running the REPL in non-interactive contexts +// (e.g., when stdin is piped from a file or another command). +// +// Returns nil if stdin is a TTY, or an error describing the issue otherwise func (c *TTYChecker) EnsureTTY() error { if !c.IsTTY() { fmt.Fprintln(os.Stderr, "REPL mode requires a TTY. Use 'gt <calculation>' for non-interactive mode.") diff --git a/internal/rpn/number.go b/internal/rpn/number.go index 45869cb..98c84a5 100644 --- a/internal/rpn/number.go +++ b/internal/rpn/number.go @@ -6,6 +6,23 @@ import ( "math/big" ) +// toNumber converts a Value to float64. +// If the value is a boolean, true returns 1 and false returns 0. +// If the value is a number, it returns the numeric value directly. +// This enables automatic coercion of booleans to numbers in arithmetic operations. +// +// v: the Value to convert +// Returns the float64 representation +func toNumber(v Value) float64 { + if v.isBool { + if v.boolVal { + return 1 + } + return 0 + } + return v.numVal +} + // Number represents a number that can be used in RPN calculations. // It can be either a float64 or a *big.Rat for precise rational calculations. type Number interface { diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go index 5169fc5..01f4f87 100644 --- a/internal/rpn/operations.go +++ b/internal/rpn/operations.go @@ -195,19 +195,20 @@ func (r *OperatorRegistry) IsHyperOperator(token string) bool { // arithmetic operators -// Add pops two values from stack, adds them, and pushes result. +// Add pops two values from stack, adds them (with boolean-to-number coercion), and pushes result. func (o *Operations) Add(stack *Stack) error { - b, err := stack.Pop() + bVal, err := stack.Pop() if err != nil { return fmt.Errorf("insufficient operands for +: %w", err) } - a, err := stack.Pop() + aVal, err := stack.Pop() if err != nil { return fmt.Errorf("insufficient operands for +: %w", err) } - stack.Push(a + b) + // Use toNumber for automatic boolean-to-number coercion + stack.Push(NewNumberValue(toNumber(aVal) + toNumber(bVal))) return nil } @@ -223,7 +224,7 @@ func (o *Operations) Subtract(stack *Stack) error { return fmt.Errorf("insufficient operands for -: %w", err) } - stack.Push(a - b) + stack.Push(NewNumberValue(toNumber(a) - toNumber(b))) return nil } @@ -239,7 +240,7 @@ func (o *Operations) Multiply(stack *Stack) error { return fmt.Errorf("insufficient operands for *: %w", err) } - stack.Push(a * b) + stack.Push(NewNumberValue(toNumber(a) * toNumber(b))) return nil } @@ -255,11 +256,11 @@ func (o *Operations) Divide(stack *Stack) error { return fmt.Errorf("insufficient operands for /: %w", err) } - if b == 0 { + if toNumber(b) == 0 { return fmt.Errorf("division by zero") } - stack.Push(a / b) + stack.Push(NewNumberValue(toNumber(a) / toNumber(b))) return nil } @@ -275,7 +276,7 @@ func (o *Operations) Power(stack *Stack) error { return fmt.Errorf("insufficient operands for ^: %w", err) } - stack.Push(math.Pow(a, b)) + stack.Push(NewNumberValue(math.Pow(toNumber(a), toNumber(b)))) return nil } @@ -291,11 +292,11 @@ func (o *Operations) Modulo(stack *Stack) error { return fmt.Errorf("insufficient operands for %%: %w", err) } - if b == 0 { + if toNumber(b) == 0 { return fmt.Errorf("modulo by zero") } - stack.Push(math.Mod(a, b)) + stack.Push(NewNumberValue(math.Mod(toNumber(a), toNumber(b)))) return nil } @@ -306,11 +307,11 @@ func (o *Operations) Log2(stack *Stack) error { return fmt.Errorf("insufficient operands for lg: %w", err) } - if a <= 0 { + if toNumber(a) <= 0 { return fmt.Errorf("log2 undefined for non-positive numbers") } - stack.Push(math.Log2(a)) + stack.Push(NewNumberValue(math.Log2(toNumber(a)))) return nil } @@ -321,11 +322,11 @@ func (o *Operations) Log10(stack *Stack) error { return fmt.Errorf("insufficient operands for log: %w", err) } - if a <= 0 { + if toNumber(a) <= 0 { return fmt.Errorf("log10 undefined for non-positive numbers") } - stack.Push(math.Log10(a)) + stack.Push(NewNumberValue(math.Log10(toNumber(a)))) return nil } @@ -336,24 +337,24 @@ func (o *Operations) Ln(stack *Stack) error { return fmt.Errorf("insufficient operands for ln: %w", err) } - if a <= 0 { + if toNumber(a) <= 0 { return fmt.Errorf("ln undefined for non-positive numbers") } - stack.Push(math.Log(a)) + stack.Push(NewNumberValue(math.Log(toNumber(a)))) return nil } // Hyper operators - operate on all values on the stack -// HyperAdd pops all values from stack, adds them left-associative, and pushes result. +// HyperAdd pops all values from stack, adds them left-associative (with boolean-to-number coercion), 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 + var values []Value for stack.Len() > 0 { val, err := stack.Pop() if err != nil { @@ -367,12 +368,12 @@ func (o *Operations) HyperAdd(stack *Stack) error { values[i], values[j] = values[j], values[i] } - // Process left-associative + // Process left-associative with toNumber coercion sum := 0.0 for i := 0; i < len(values); i++ { - sum += values[i] + sum += toNumber(values[i]) } - stack.Push(sum) + stack.Push(NewNumberValue(sum)) return nil } diff --git a/internal/rpn/variables.go b/internal/rpn/variables.go index b7818a9..de98e2f 100644 --- a/internal/rpn/variables.go +++ b/internal/rpn/variables.go @@ -13,28 +13,79 @@ var ( ErrInvalidVariableName = fmt.Errorf("invalid variable name") ) -// Stack represents a simple float64 stack for RPN calculations. +// Value represents a variant type that can hold either a number (float64) or a boolean. +type Value struct { + isBool bool + boolVal bool + numVal float64 +} + +// NewNumberValue creates a new Value containing a float64 number. +func NewNumberValue(n float64) Value { + return Value{isBool: false, numVal: n} +} + +// NewBoolValue creates a new Value containing a boolean. +func NewBoolValue(b bool) Value { + return Value{isBool: true, boolVal: b} +} + +// IsBool returns true if the value is a boolean. +func (v Value) IsBool() bool { + return v.isBool +} + +// IsNumber returns true if the value is a number. +func (v Value) IsNumber() bool { + return !v.isBool +} + +// Bool returns the boolean value, or false if the value is not a boolean. +func (v Value) Bool() bool { + return v.boolVal +} + +// Number returns the float64 value, or 0 if the value is not a number. +func (v Value) Number() float64 { + return v.numVal +} + +// String returns the string representation of the value. +// For booleans, it returns "true" or "false". +// For numbers, it returns the formatted float64 value. +func (v Value) String() string { + if v.isBool { + if v.boolVal { + return "true" + } + return "false" + } + return fmt.Sprintf("%.10g", v.numVal) +} + +// Stack represents a variant stack for RPN calculations. +// It can hold both number and boolean values. type Stack struct { - values []float64 + values []Value } // NewStack creates a new empty stack. func NewStack() *Stack { return &Stack{ - values: make([]float64, 0), + values: make([]Value, 0), } } // Push adds a value to the top of the stack. -func (s *Stack) Push(val float64) { +func (s *Stack) Push(val Value) { s.values = append(s.values, val) } // Pop removes and returns the top value from the stack. // Returns an error if the stack is empty. -func (s *Stack) Pop() (float64, error) { +func (s *Stack) Pop() (Value, error) { if len(s.values) == 0 { - return 0, fmt.Errorf("stack is empty") + return Value{}, fmt.Errorf("stack is empty") } val := s.values[len(s.values)-1] @@ -44,9 +95,9 @@ func (s *Stack) Pop() (float64, error) { // Peek returns the top value without removing it. // Returns an error if the stack is empty. -func (s *Stack) Peek() (float64, error) { +func (s *Stack) Peek() (Value, error) { if len(s.values) == 0 { - return 0, fmt.Errorf("stack is empty") + return Value{}, fmt.Errorf("stack is empty") } return s.values[len(s.values)-1], nil } @@ -57,8 +108,8 @@ func (s *Stack) Len() int { } // Values returns a copy of all stack values (top-to-bottom order). -func (s *Stack) Values() []float64 { - vals := make([]float64, len(s.values)) +func (s *Stack) Values() []Value { + vals := make([]Value, len(s.values)) copy(vals, s.values) return vals } @@ -113,6 +164,9 @@ func NewVariables() *Variables { // isValidVariableName checks if a variable name is valid. // Variable names must be non-empty and contain only alphanumeric characters and underscores. +// +// name: the variable name to validate +// Returns true if the name is valid, false otherwise func isValidVariableName(name string) bool { if name == "" { return false |
