blob: d53870839e758867293520d8cdc966845db20490 [file] [log] [blame] [view]
Sketch🕴️719e8232026-02-28 19:07:50 +04001# Math Expression Evaluator — Design Document
2
3## Requirements Summary
4- Language: Go
5- Operators: `+`, `-`, `*`, `/` with parentheses
6- Numbers: floating point (e.g. `3.14`, `42`, `0.5`)
Sketch🕴️c3ec07d2026-02-28 20:35:06 +04007- **Functions: user-defined with `f(x) = x + 1` syntax**
Sketch🕴️719e8232026-02-28 19:07:50 +04008- Interface: CLI REPL
9- Error handling: print error message, continue REPL
10
Sketch🕴️c3ec07d2026-02-28 20:35:06 +040011### Function Definition Requirements
12- **Syntax:** `f(x) = x + 1` — name, parenthesized params, `=`, body expression
13- **Multiple params:** `f(x, y) = x + y`
14- **Function calls:** Allowed anywhere a number can appear; arguments are arbitrary expressions
15- **Persistence:** Definitions persist across REPL lines
16- **Cross-calling:** Functions can call other user-defined functions (late binding)
17- **Built-ins:** None
18- **Redefinition:** Not allowed (error)
19- **Output on define:** Print "defined <name>"
20
Sketch🕴️719e8232026-02-28 19:07:50 +040021## Approaches Considered
22
23### 1. Recursive-Descent with AST (chosen)
24- **Lexer → Parser → AST → Evaluator → REPL**
25- Clean separation: each stage is independently testable
26- AST is a reusable intermediate representation
27- Easy to extend (new operators, pretty-printing, optimization)
28- Well-suited for 2 precedence levels + parentheses
29
30### 2. Recursive-Descent with Direct Evaluation
31- Parser evaluates inline — no AST
32- Fewer types, less code
33- Couples parsing and evaluation — harder to test, extend
34
35### 3. Shunting-Yard Algorithm
36- Converts to RPN then evaluates
37- Good for many precedence levels; overkill here
38- Harder to produce clear error messages
39
40**Decision:** Approach 1. The AST adds minimal overhead but provides clean boundaries.
41
42## Architecture
43
44```
45Input string
46
47
48 ┌───────┐
49 │ Lexer │ string → []Token
50 └───┬───┘
51
52
53 ┌────────┐
Sketch🕴️c3ec07d2026-02-28 20:35:06 +040054 │ Parser │ []Token → Statement (ExprStmt | FuncDef)
Sketch🕴️719e8232026-02-28 19:07:50 +040055 └───┬────┘
56
57
58 ┌───────────┐
Sketch🕴️c3ec07d2026-02-28 20:35:06 +040059 │ Evaluator │ stateful: function registry + expression evaluation
Sketch🕴️719e8232026-02-28 19:07:50 +040060 └───┬───────┘
61
62
63 ┌──────┐
Sketch🕴️c3ec07d2026-02-28 20:35:06 +040064 │ REPL │ read line → parse → route (define or eval) → print
Sketch🕴️719e8232026-02-28 19:07:50 +040065 └──────┘
66```
67
68## Component Interfaces
69
70### Token (data type)
71
72```go
73package token
74
75type Type int
76
77const (
Sketch🕴️c3ec07d2026-02-28 20:35:06 +040078 Number Type = iota // numeric literal
79 Plus // +
80 Minus // -
81 Star // *
82 Slash // /
83 LParen // (
84 RParen // )
85 Ident // identifier (e.g. f, x, myFunc)
86 Comma // ,
87 Equals // =
88 EOF // end of input
Sketch🕴️719e8232026-02-28 19:07:50 +040089)
90
91type Token struct {
92 Type Type
Sketch🕴️c3ec07d2026-02-28 20:35:06 +040093 Literal string // raw text, e.g. "3.14", "+", "f"
Sketch🕴️719e8232026-02-28 19:07:50 +040094 Pos int // position in input (for error messages)
95}
96```
97
98### Lexer
99
100```go
101package lexer
102
103// Tokenize converts an input string into a slice of tokens.
104// Returns an error if the input contains invalid characters.
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400105// Recognizes: numbers, operators, parens, identifiers, comma, equals.
Sketch🕴️719e8232026-02-28 19:07:50 +0400106func Tokenize(input string) ([]token.Token, error)
107```
108
109### AST (data types)
110
111```go
112package ast
113
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400114// Node is the interface all expression AST nodes implement.
Sketch🕴️719e8232026-02-28 19:07:50 +0400115type Node interface {
116 node() // sealed marker method
117}
118
119// NumberLit represents a numeric literal.
120type NumberLit struct {
121 Value float64
122}
123
124// BinaryExpr represents a binary operation (e.g. 1 + 2).
125type BinaryExpr struct {
126 Op token.Type // Plus, Minus, Star, Slash
127 Left Node
128 Right Node
129}
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400130
131// Ident represents a variable reference (function parameter).
132type Ident struct {
133 Name string
134}
135
136// FuncCall represents a function call (e.g. f(1+2, 3)).
137type FuncCall struct {
138 Name string
139 Args []Node
140}
141
142// Statement is the interface for top-level parsed constructs.
143type Statement interface {
144 stmt() // sealed marker method
145}
146
147// ExprStmt wraps an expression used as a statement.
148type ExprStmt struct {
149 Expr Node
150}
151
152// FuncDef represents a function definition: name(params) = body
153type FuncDef struct {
154 Name string
155 Params []string
156 Body Node
157}
Sketch🕴️719e8232026-02-28 19:07:50 +0400158```
159
160### Parser
161
162```go
163package parser
164
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400165// Parse converts a slice of tokens into an expression AST.
166// Kept for backward compatibility.
Sketch🕴️719e8232026-02-28 19:07:50 +0400167func Parse(tokens []token.Token) (ast.Node, error)
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400168
169// ParseLine converts a slice of tokens into a Statement.
170// Distinguishes function definitions from expressions.
171func ParseLine(tokens []token.Token) (ast.Statement, error)
Sketch🕴️719e8232026-02-28 19:07:50 +0400172```
173
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400174Grammar (extended):
Sketch🕴️719e8232026-02-28 19:07:50 +0400175```
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400176line → funcdef | expr
177funcdef → IDENT '(' params ')' '=' expr
178params → IDENT (',' IDENT)*
179expr → term (('+' | '-') term)*
180term → factor (('*' | '/') factor)*
181factor → NUMBER | IDENT '(' args ')' | IDENT | '(' expr ')'
182args → expr (',' expr)*
Sketch🕴️719e8232026-02-28 19:07:50 +0400183```
184
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400185**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.
186
Sketch🕴️719e8232026-02-28 19:07:50 +0400187### Evaluator
188
189```go
190package evaluator
191
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400192// Evaluator holds function definitions and evaluates expressions.
193type Evaluator struct {
194 funcs map[string]*ast.FuncDef
195}
196
197// New creates a new Evaluator with an empty function registry.
198func New() *Evaluator
199
200// Define registers a function definition.
201// Returns an error if a function with the same name is already defined.
202func (e *Evaluator) Define(def *ast.FuncDef) error
203
204// Eval evaluates an expression AST node.
205// env provides variable bindings (function parameters).
206// Pass nil for top-level evaluation.
207func (e *Evaluator) Eval(node ast.Node, env map[string]float64) (float64, error)
Sketch🕴️719e8232026-02-28 19:07:50 +0400208```
209
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400210**Function call evaluation:**
2111. Look up function name in registry
2122. Evaluate each argument expression in caller's environment
2133. Check argument count matches parameter count
2144. Create new environment: `param[i] → argValue[i]`
2155. Evaluate function body in new environment
216
217**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.
218
Sketch🕴️719e8232026-02-28 19:07:50 +0400219### REPL
220
221```go
222package repl
223
224// Run starts the read-eval-print loop, reading from r and writing to w.
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400225// Maintains function registry across lines.
Sketch🕴️719e8232026-02-28 19:07:50 +0400226func Run(r io.Reader, w io.Writer)
227```
228
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400229**Line processing flow:**
2301. Tokenize line
2312. `ParseLine()` → `Statement`
2323. Switch on statement type:
233 - `*ast.FuncDef` → `evaluator.Define(def)`, print "defined <name>"
234 - `*ast.ExprStmt` → `evaluator.Eval(expr, nil)`, print result
235
Sketch🕴️719e8232026-02-28 19:07:50 +0400236## Package Layout
237
238```
239matheval/
240├── cmd/
241│ └── matheval/
242│ └── main.go # entry point, calls repl.Run
243├── token/
244│ └── token.go # Token type and constants
245├── lexer/
246│ ├── lexer.go # Tokenize function
247│ └── lexer_test.go
248├── ast/
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400249│ └── ast.go # AST node types + Statement types
Sketch🕴️719e8232026-02-28 19:07:50 +0400250├── parser/
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400251│ ├── parser.go # Parse + ParseLine functions
Sketch🕴️719e8232026-02-28 19:07:50 +0400252│ └── parser_test.go
253├── evaluator/
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400254│ ├── evaluator.go # Evaluator struct with Define + Eval
Sketch🕴️719e8232026-02-28 19:07:50 +0400255│ └── evaluator_test.go
256├── repl/
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400257│ ├── repl.go # REPL loop with state
Sketch🕴️719e8232026-02-28 19:07:50 +0400258│ └── repl_test.go
259├── docs/
260│ ├── design.md
261│ └── plan.md
262├── go.mod
263└── README.md
264```
265
266## Error Handling
267- Lexer: returns error for invalid characters (e.g. `@`, `#`)
Sketch🕴️c3ec07d2026-02-28 20:35:06 +0400268- Parser: returns error for syntax errors (unexpected token, mismatched parens, malformed definitions)
269- Evaluator: returns error for division by zero, undefined function, undefined variable, argument count mismatch, function redefinition
Sketch🕴️719e8232026-02-28 19:07:50 +0400270- REPL: catches any error, prints it, prompts for next input
271
272## Key Design Decisions
Sketch🕴️c3ec07d2026-02-28 20:35:06 +04002731. **Statement vs Node separation** — `Statement` interface separates top-level constructs (definitions vs expressions) from expression nodes. This keeps the expression evaluator clean.
2742. **Stateful Evaluator struct** — replaces the previous stateless `Eval()` function. Required to hold the function registry. The `Eval` method still takes an explicit environment for testability.
2753. **Late binding** — function bodies reference other functions by name; resolved at call time. Simple and correct.
2764. **Backward-compatible Parse()** — existing `Parse()` function kept. New `ParseLine()` added for the REPL.
2775. **Sealed AST interface** — unexported marker method prevents external implementations, keeping the node set closed.
2786. **Position tracking in tokens** — enables precise error messages.
2797. **REPL takes io.Reader/io.Writer** — makes it testable without stdin/stdout.
2808. **Definition detection via Equals scan** — simple and unambiguous since `=` cannot appear in expressions.