// SPDX-License-Identifier: MIT // Copyright (c) 2026 Paul Buetow package repl import ( "fmt" "strconv" "strings" "codeberg.org/snonux/gt/internal/perc" "codeberg.org/snonux/gt/internal/rpn" ) // rpnPrefixes lists the command prefixes that trigger RPN evaluation. // Adding a new prefix (e.g., "expr") requires only appending to this slice. var rpnPrefixes = []string{"rpn", "calc"} // RPNCalculator defines the methods needed by REPL handlers to interact with // the RPN engine. By depending on this interface instead of the concrete *rpn.RPN, // handlers obey DIP and the Law of Demeter. type RPNCalculator interface { ParseAndEvaluate(string) (string, error) EvalOperator(string) (string, error) GetCurrentStack() []rpn.StackValue SetCurrentStack([]rpn.StackValue) SetMode(rpn.CalculationMode) GetMode() rpn.CalculationMode IsStandardOperator(string) bool IsHyperOperator(string) bool } // 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) } // Ensure all handler types implement CommandHandler at compile time. var ( _ CommandHandler = (*BuiltInCommandHandler)(nil) _ CommandHandler = (*RPNHandler)(nil) _ CommandHandler = (*PercentageHandler)(nil) _ CommandHandler = (*ErrorHandler)(nil) ) // 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. // 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. // 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 } return h.next.Handle(repl, input) } // 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 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) if len(args) > 0 { subCmd := strings.ToLower(args[0]) // Handle rat command specially - needs RPN state access if subCmd == "rat" { return handleRatCommand(repl, input) } } output, err := ExecuteCommand(cmd) if err != nil { return "", true, err } return output, true, nil } return h.Next(repl, input) } // 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 { return "rat command requires an argument: on, off, or toggle", true, nil } modeArg := strings.ToLower(args[1]) calc := repl.RpnCalculator() switch modeArg { case "on": calc.SetMode(rpn.RationalMode) return "Rational mode enabled", true, nil case "off": calc.SetMode(rpn.FloatMode) return "Rational mode disabled (using float64)", true, nil case "toggle": if calc.GetMode() == rpn.FloatMode { calc.SetMode(rpn.RationalMode) return "Rational mode enabled", true, nil } else { calc.SetMode(rpn.FloatMode) return "Rational mode disabled (using float64)", true, nil } default: return "Unknown rat mode: " + modeArg + ". Valid modes: on, off, toggle", true, nil } } // 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 } // evalWithStackRestore evaluates an RPN expression, restoring the stack on error // so that a failed parse never corrupts the user's stack. func (h *RPNHandler) evalWithStackRestore(repl *REPL, input string) (string, error) { calc := repl.RpnCalculator() savedStack := calc.GetCurrentStack() result, err := calc.ParseAndEvaluate(input) if err != nil { calc.SetCurrentStack(savedStack) return "", err } return result, nil } // looksLikeRPN checks if an input string appears to be an RPN expression. // It returns true if at least one token is a number, known operator, or symbol. // This prevents non-RPN phrases like "hello world" from being evaluated as RPN. func looksLikeRPN(input string, calc RPNCalculator) bool { for _, token := range strings.Fields(input) { if _, err := strconv.ParseFloat(token, 64); err == nil { return true } if calc.IsStandardOperator(token) || calc.IsHyperOperator(token) { return true } if len(token) > 0 && token[0] == ':' { return true } } return false } // 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) // - Symbol syntax (e.g., ":x") that pushes a symbol onto the 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 (data-driven; see rpnPrefixes) lowerInput := strings.ToLower(input) for _, prefix := range rpnPrefixes { if strings.HasPrefix(lowerInput, prefix+" ") { rest := strings.TrimSpace(strings.TrimPrefix(input, strings.SplitN(input, " ", 2)[0])) result, err := h.evalWithStackRestore(repl, rest) if err != nil { return "", true, err } return result, true, nil } } // Try RPN parsing first (for bare RPN expressions like "3 4 +") calc := repl.RpnCalculator() if calc != nil { // Check if input looks like RPN (contains spaces or is a single known operator) if strings.Contains(input, " ") { if looksLikeRPN(input, calc) { result, err := h.evalWithStackRestore(repl, input) if err != nil { return "", true, err } return result, true, nil } } // Single-token input: try as operator, number, or symbol fields := strings.Fields(input) if len(fields) == 1 { token := fields[0] if calc.IsStandardOperator(token) || calc.IsHyperOperator(token) { result, err := calc.EvalOperator(token) if err != nil { return "", true, err } return result, true, nil } // Numbers and symbols (:x) are handled by ParseAndEvaluate if _, err := strconv.ParseFloat(token, 64); err == nil { result, err := h.evalWithStackRestore(repl, token) if err != nil { return "", true, err } return result, true, nil } if len(token) > 0 && token[0] == ':' { result, err := h.evalWithStackRestore(repl, token) if err != nil { return "", true, err } return result, true, nil } } } return h.Next(repl, input) } // PercentageHandler handles percentage calculation expressions. // It uses the perc.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. // 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 := perc.Parse(input) if err != nil { // Not a percentage expression, pass to next handler return h.Next(repl, input) } return result, true, nil } // 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. // 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. // 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{} rpnHandler := &RPNHandler{} percentageHandler := &PercentageHandler{} errorHandler := &ErrorHandler{} // Build the chain: BuiltIn -> RPN -> Percentage -> Error builtInHandler.SetNext(rpnHandler) rpnHandler.SetNext(percentageHandler) percentageHandler.SetNext(errorHandler) return builtInHandler }