| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 1 | # 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🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 7 | - **Functions: user-defined with `f(x) = x + 1` syntax** |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 8 | - Interface: CLI REPL |
| 9 | - Error handling: print error message, continue REPL |
| 10 | |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 11 | ### 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🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 21 | ## 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 | ``` |
| 45 | Input string |
| 46 | │ |
| 47 | ▼ |
| 48 | ┌───────┐ |
| 49 | │ Lexer │ string → []Token |
| 50 | └───┬───┘ |
| 51 | │ |
| 52 | ▼ |
| 53 | ┌────────┐ |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 54 | │ Parser │ []Token → Statement (ExprStmt | FuncDef) |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 55 | └───┬────┘ |
| 56 | │ |
| 57 | ▼ |
| 58 | ┌───────────┐ |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 59 | │ Evaluator │ stateful: function registry + expression evaluation |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 60 | └───┬───────┘ |
| 61 | │ |
| 62 | ▼ |
| 63 | ┌──────┐ |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 64 | │ REPL │ read line → parse → route (define or eval) → print |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 65 | └──────┘ |
| 66 | ``` |
| 67 | |
| 68 | ## Component Interfaces |
| 69 | |
| 70 | ### Token (data type) |
| 71 | |
| 72 | ```go |
| 73 | package token |
| 74 | |
| 75 | type Type int |
| 76 | |
| 77 | const ( |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 78 | 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🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 89 | ) |
| 90 | |
| 91 | type Token struct { |
| 92 | Type Type |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 93 | Literal string // raw text, e.g. "3.14", "+", "f" |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 94 | Pos int // position in input (for error messages) |
| 95 | } |
| 96 | ``` |
| 97 | |
| 98 | ### Lexer |
| 99 | |
| 100 | ```go |
| 101 | package lexer |
| 102 | |
| 103 | // Tokenize converts an input string into a slice of tokens. |
| 104 | // Returns an error if the input contains invalid characters. |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 105 | // Recognizes: numbers, operators, parens, identifiers, comma, equals. |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 106 | func Tokenize(input string) ([]token.Token, error) |
| 107 | ``` |
| 108 | |
| 109 | ### AST (data types) |
| 110 | |
| 111 | ```go |
| 112 | package ast |
| 113 | |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 114 | // Node is the interface all expression AST nodes implement. |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 115 | type Node interface { |
| 116 | node() // sealed marker method |
| 117 | } |
| 118 | |
| 119 | // NumberLit represents a numeric literal. |
| 120 | type NumberLit struct { |
| 121 | Value float64 |
| 122 | } |
| 123 | |
| 124 | // BinaryExpr represents a binary operation (e.g. 1 + 2). |
| 125 | type BinaryExpr struct { |
| 126 | Op token.Type // Plus, Minus, Star, Slash |
| 127 | Left Node |
| 128 | Right Node |
| 129 | } |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 130 | |
| 131 | // Ident represents a variable reference (function parameter). |
| 132 | type Ident struct { |
| 133 | Name string |
| 134 | } |
| 135 | |
| 136 | // FuncCall represents a function call (e.g. f(1+2, 3)). |
| 137 | type FuncCall struct { |
| 138 | Name string |
| 139 | Args []Node |
| 140 | } |
| 141 | |
| 142 | // Statement is the interface for top-level parsed constructs. |
| 143 | type Statement interface { |
| 144 | stmt() // sealed marker method |
| 145 | } |
| 146 | |
| 147 | // ExprStmt wraps an expression used as a statement. |
| 148 | type ExprStmt struct { |
| 149 | Expr Node |
| 150 | } |
| 151 | |
| 152 | // FuncDef represents a function definition: name(params) = body |
| 153 | type FuncDef struct { |
| 154 | Name string |
| 155 | Params []string |
| 156 | Body Node |
| 157 | } |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 158 | ``` |
| 159 | |
| 160 | ### Parser |
| 161 | |
| 162 | ```go |
| 163 | package parser |
| 164 | |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 165 | // Parse converts a slice of tokens into an expression AST. |
| 166 | // Kept for backward compatibility. |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 167 | func Parse(tokens []token.Token) (ast.Node, error) |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 168 | |
| 169 | // ParseLine converts a slice of tokens into a Statement. |
| 170 | // Distinguishes function definitions from expressions. |
| 171 | func ParseLine(tokens []token.Token) (ast.Statement, error) |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 172 | ``` |
| 173 | |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 174 | Grammar (extended): |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 175 | ``` |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 176 | line → funcdef | expr |
| 177 | funcdef → IDENT '(' params ')' '=' expr |
| 178 | params → IDENT (',' IDENT)* |
| 179 | expr → term (('+' | '-') term)* |
| 180 | term → factor (('*' | '/') factor)* |
| 181 | factor → NUMBER | IDENT '(' args ')' | IDENT | '(' expr ')' |
| 182 | args → expr (',' expr)* |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 183 | ``` |
| 184 | |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 185 | **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🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 187 | ### Evaluator |
| 188 | |
| 189 | ```go |
| 190 | package evaluator |
| 191 | |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 192 | // Evaluator holds function definitions and evaluates expressions. |
| 193 | type Evaluator struct { |
| 194 | funcs map[string]*ast.FuncDef |
| 195 | } |
| 196 | |
| 197 | // New creates a new Evaluator with an empty function registry. |
| 198 | func New() *Evaluator |
| 199 | |
| 200 | // Define registers a function definition. |
| 201 | // Returns an error if a function with the same name is already defined. |
| 202 | func (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. |
| 207 | func (e *Evaluator) Eval(node ast.Node, env map[string]float64) (float64, error) |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 208 | ``` |
| 209 | |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 210 | **Function call evaluation:** |
| 211 | 1. Look up function name in registry |
| 212 | 2. Evaluate each argument expression in caller's environment |
| 213 | 3. Check argument count matches parameter count |
| 214 | 4. Create new environment: `param[i] → argValue[i]` |
| 215 | 5. 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🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 219 | ### REPL |
| 220 | |
| 221 | ```go |
| 222 | package repl |
| 223 | |
| 224 | // Run starts the read-eval-print loop, reading from r and writing to w. |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 225 | // Maintains function registry across lines. |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 226 | func Run(r io.Reader, w io.Writer) |
| 227 | ``` |
| 228 | |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 229 | **Line processing flow:** |
| 230 | 1. Tokenize line |
| 231 | 2. `ParseLine()` → `Statement` |
| 232 | 3. Switch on statement type: |
| 233 | - `*ast.FuncDef` → `evaluator.Define(def)`, print "defined <name>" |
| 234 | - `*ast.ExprStmt` → `evaluator.Eval(expr, nil)`, print result |
| 235 | |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 236 | ## Package Layout |
| 237 | |
| 238 | ``` |
| 239 | matheval/ |
| 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🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 249 | │ └── ast.go # AST node types + Statement types |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 250 | ├── parser/ |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 251 | │ ├── parser.go # Parse + ParseLine functions |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 252 | │ └── parser_test.go |
| 253 | ├── evaluator/ |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 254 | │ ├── evaluator.go # Evaluator struct with Define + Eval |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 255 | │ └── evaluator_test.go |
| 256 | ├── repl/ |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 257 | │ ├── repl.go # REPL loop with state |
| Sketch🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 258 | │ └── 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🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 268 | - 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🕴️ | 719e823 | 2026-02-28 19:07:50 +0400 | [diff] [blame] | 270 | - REPL: catches any error, prints it, prompts for next input |
| 271 | |
| 272 | ## Key Design Decisions |
| Sketch🕴️ | c3ec07d | 2026-02-28 20:35:06 +0400 | [diff] [blame] | 273 | 1. **Statement vs Node separation** — `Statement` interface separates top-level constructs (definitions vs expressions) from expression nodes. This keeps the expression evaluator clean. |
| 274 | 2. **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. |
| 275 | 3. **Late binding** — function bodies reference other functions by name; resolved at call time. Simple and correct. |
| 276 | 4. **Backward-compatible Parse()** — existing `Parse()` function kept. New `ParseLine()` added for the REPL. |
| 277 | 5. **Sealed AST interface** — unexported marker method prevents external implementations, keeping the node set closed. |
| 278 | 6. **Position tracking in tokens** — enables precise error messages. |
| 279 | 7. **REPL takes io.Reader/io.Writer** — makes it testable without stdin/stdout. |
| 280 | 8. **Definition detection via Equals scan** — simple and unambiguous since `=` cannot appear in expressions. |