+, -, *, / with parentheses3.14, 42, 0.5)Decision: Approach 1. The AST adds minimal overhead but provides clean boundaries.
Input string
│
▼
┌───────┐
│ Lexer │ string → []Token
└───┬───┘
│
▼
┌────────┐
│ Parser │ []Token → AST (Node)
└───┬────┘
│
▼
┌───────────┐
│ Evaluator │ Node → float64
└───┬───────┘
│
▼
┌──────┐
│ REPL │ read line → eval → print result or error
└──────┘
package token type Type int const ( Number Type = iota Plus // + Minus // - Star // * Slash // / LParen // ( RParen // ) EOF ) type Token struct { Type Type Literal string // raw text, e.g. "3.14", "+" Pos int // position in input (for error messages) }
package lexer // Tokenize converts an input string into a slice of tokens. // Returns an error if the input contains invalid characters. func Tokenize(input string) ([]token.Token, error)
package ast // Node is the interface all AST nodes implement. type Node interface { node() // sealed marker method } // NumberLit represents a numeric literal. type NumberLit struct { Value float64 } // BinaryExpr represents a binary operation (e.g. 1 + 2). type BinaryExpr struct { Op token.Type // Plus, Minus, Star, Slash Left Node Right Node }
package parser // Parse converts a slice of tokens into an AST. // Returns an error for malformed expressions (mismatched parens, etc.). func Parse(tokens []token.Token) (ast.Node, error)
Grammar (recursive-descent):
expr → term (('+' | '-') term)*
term → factor (('*' | '/') factor)*
factor → NUMBER | '(' expr ')'
package evaluator // Eval evaluates an AST node and returns the result. // Returns an error on division by zero. func Eval(node ast.Node) (float64, error)
package repl // Run starts the read-eval-print loop, reading from r and writing to w. func Run(r io.Reader, w io.Writer)
matheval/ ├── cmd/ │ └── matheval/ │ └── main.go # entry point, calls repl.Run ├── token/ │ └── token.go # Token type and constants ├── lexer/ │ ├── lexer.go # Tokenize function │ └── lexer_test.go ├── ast/ │ └── ast.go # AST node types ├── parser/ │ ├── parser.go # Parse function │ └── parser_test.go ├── evaluator/ │ ├── evaluator.go # Eval function │ └── evaluator_test.go ├── repl/ │ ├── repl.go # REPL loop │ └── repl_test.go ├── docs/ │ ├── design.md │ └── plan.md ├── go.mod └── README.md
@, #)Tokenize(), Parse(), Eval() are stateless functions. No need for struct receivers since there's no configuration or state to carry.