+, -, *, / with parentheses3.14, 42, 0.5)f(x) = x + 1 syntaxf(x) = x + 1 — name, parenthesized params, =, body expressionf(x, y) = x + yDecision: Approach 1. The AST adds minimal overhead but provides clean boundaries.
Input string
│
▼
┌───────┐
│ Lexer │ string → []Token
└───┬───┘
│
▼
┌────────┐
│ Parser │ []Token → Statement (ExprStmt | FuncDef)
└───┬────┘
│
▼
┌───────────┐
│ Evaluator │ stateful: function registry + expression evaluation
└───┬───────┘
│
▼
┌──────┐
│ REPL │ read line → parse → route (define or eval) → print
└──────┘
package token type Type int const ( Number Type = iota // numeric literal Plus // + Minus // - Star // * Slash // / LParen // ( RParen // ) Ident // identifier (e.g. f, x, myFunc) Comma // , Equals // = EOF // end of input ) type Token struct { Type Type Literal string // raw text, e.g. "3.14", "+", "f" 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. // Recognizes: numbers, operators, parens, identifiers, comma, equals. func Tokenize(input string) ([]token.Token, error)
package ast // Node is the interface all expression 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 } // Ident represents a variable reference (function parameter). type Ident struct { Name string } // FuncCall represents a function call (e.g. f(1+2, 3)). type FuncCall struct { Name string Args []Node } // Statement is the interface for top-level parsed constructs. type Statement interface { stmt() // sealed marker method } // ExprStmt wraps an expression used as a statement. type ExprStmt struct { Expr Node } // FuncDef represents a function definition: name(params) = body type FuncDef struct { Name string Params []string Body Node }
package parser // Parse converts a slice of tokens into an expression AST. // Kept for backward compatibility. func Parse(tokens []token.Token) (ast.Node, error) // ParseLine converts a slice of tokens into a Statement. // Distinguishes function definitions from expressions. func ParseLine(tokens []token.Token) (ast.Statement, error)
Grammar (extended):
line → funcdef | expr
funcdef → IDENT '(' params ')' '=' expr
params → IDENT (',' IDENT)*
expr → term (('+' | '-') term)*
term → factor (('*' | '/') factor)*
factor → NUMBER | IDENT '(' args ')' | IDENT | '(' expr ')'
args → expr (',' expr)*
Definition detection: Scan token stream for Equals token. If present → parse as function definition. If absent → parse as expression. This works because = is not valid in expressions.
package evaluator // Evaluator holds function definitions and evaluates expressions. type Evaluator struct { funcs map[string]*ast.FuncDef } // New creates a new Evaluator with an empty function registry. func New() *Evaluator // Define registers a function definition. // Returns an error if a function with the same name is already defined. func (e *Evaluator) Define(def *ast.FuncDef) error // Eval evaluates an expression AST node. // env provides variable bindings (function parameters). // Pass nil for top-level evaluation. func (e *Evaluator) Eval(node ast.Node, env map[string]float64) (float64, error)
Function call evaluation:
param[i] → argValue[i]Late binding: Function body references are resolved at call time, not definition time. This naturally supports cross-function calls as long as the called function is defined before the call is evaluated.
package repl // Run starts the read-eval-print loop, reading from r and writing to w. // Maintains function registry across lines. func Run(r io.Reader, w io.Writer)
Line processing flow:
ParseLine() → Statement*ast.FuncDef → evaluator.Define(def), print "defined "*ast.ExprStmt → evaluator.Eval(expr, nil), print resultmatheval/ ├── 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 + Statement types ├── parser/ │ ├── parser.go # Parse + ParseLine functions │ └── parser_test.go ├── evaluator/ │ ├── evaluator.go # Evaluator struct with Define + Eval │ └── evaluator_test.go ├── repl/ │ ├── repl.go # REPL loop with state │ └── repl_test.go ├── docs/ │ ├── design.md │ └── plan.md ├── go.mod └── README.md
@, #)Statement interface separates top-level constructs (definitions vs expressions) from expression nodes. This keeps the expression evaluator clean.Eval() function. Required to hold the function registry. The Eval method still takes an explicit environment for testability.Parse() function kept. New ParseLine() added for the REPL.= cannot appear in expressions.