| 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`) |
| 7 | - Variables: none |
| 8 | - Interface: CLI REPL |
| 9 | - Error handling: print error message, continue REPL |
| 10 | |
| 11 | ## Approaches Considered |
| 12 | |
| 13 | ### 1. Recursive-Descent with AST (chosen) |
| 14 | - **Lexer β Parser β AST β Evaluator β REPL** |
| 15 | - Clean separation: each stage is independently testable |
| 16 | - AST is a reusable intermediate representation |
| 17 | - Easy to extend (new operators, pretty-printing, optimization) |
| 18 | - Well-suited for 2 precedence levels + parentheses |
| 19 | |
| 20 | ### 2. Recursive-Descent with Direct Evaluation |
| 21 | - Parser evaluates inline β no AST |
| 22 | - Fewer types, less code |
| 23 | - Couples parsing and evaluation β harder to test, extend |
| 24 | |
| 25 | ### 3. Shunting-Yard Algorithm |
| 26 | - Converts to RPN then evaluates |
| 27 | - Good for many precedence levels; overkill here |
| 28 | - Harder to produce clear error messages |
| 29 | |
| 30 | **Decision:** Approach 1. The AST adds minimal overhead but provides clean boundaries. |
| 31 | |
| 32 | ## Architecture |
| 33 | |
| 34 | ``` |
| 35 | Input string |
| 36 | β |
| 37 | βΌ |
| 38 | βββββββββ |
| 39 | β Lexer β string β []Token |
| 40 | βββββ¬ββββ |
| 41 | β |
| 42 | βΌ |
| 43 | ββββββββββ |
| 44 | β Parser β []Token β AST (Node) |
| 45 | βββββ¬βββββ |
| 46 | β |
| 47 | βΌ |
| 48 | βββββββββββββ |
| 49 | β Evaluator β Node β float64 |
| 50 | βββββ¬ββββββββ |
| 51 | β |
| 52 | βΌ |
| 53 | ββββββββ |
| 54 | β REPL β read line β eval β print result or error |
| 55 | ββββββββ |
| 56 | ``` |
| 57 | |
| 58 | ## Component Interfaces |
| 59 | |
| 60 | ### Token (data type) |
| 61 | |
| 62 | ```go |
| 63 | package token |
| 64 | |
| 65 | type Type int |
| 66 | |
| 67 | const ( |
| 68 | Number Type = iota |
| 69 | Plus // + |
| 70 | Minus // - |
| 71 | Star // * |
| 72 | Slash // / |
| 73 | LParen // ( |
| 74 | RParen // ) |
| 75 | EOF |
| 76 | ) |
| 77 | |
| 78 | type Token struct { |
| 79 | Type Type |
| 80 | Literal string // raw text, e.g. "3.14", "+" |
| 81 | Pos int // position in input (for error messages) |
| 82 | } |
| 83 | ``` |
| 84 | |
| 85 | ### Lexer |
| 86 | |
| 87 | ```go |
| 88 | package lexer |
| 89 | |
| 90 | // Tokenize converts an input string into a slice of tokens. |
| 91 | // Returns an error if the input contains invalid characters. |
| 92 | func Tokenize(input string) ([]token.Token, error) |
| 93 | ``` |
| 94 | |
| 95 | ### AST (data types) |
| 96 | |
| 97 | ```go |
| 98 | package ast |
| 99 | |
| 100 | // Node is the interface all AST nodes implement. |
| 101 | type Node interface { |
| 102 | node() // sealed marker method |
| 103 | } |
| 104 | |
| 105 | // NumberLit represents a numeric literal. |
| 106 | type NumberLit struct { |
| 107 | Value float64 |
| 108 | } |
| 109 | |
| 110 | // BinaryExpr represents a binary operation (e.g. 1 + 2). |
| 111 | type BinaryExpr struct { |
| 112 | Op token.Type // Plus, Minus, Star, Slash |
| 113 | Left Node |
| 114 | Right Node |
| 115 | } |
| 116 | ``` |
| 117 | |
| 118 | ### Parser |
| 119 | |
| 120 | ```go |
| 121 | package parser |
| 122 | |
| 123 | // Parse converts a slice of tokens into an AST. |
| 124 | // Returns an error for malformed expressions (mismatched parens, etc.). |
| 125 | func Parse(tokens []token.Token) (ast.Node, error) |
| 126 | ``` |
| 127 | |
| 128 | Grammar (recursive-descent): |
| 129 | ``` |
| 130 | expr β term (('+' | '-') term)* |
| 131 | term β factor (('*' | '/') factor)* |
| 132 | factor β NUMBER | '(' expr ')' |
| 133 | ``` |
| 134 | |
| 135 | ### Evaluator |
| 136 | |
| 137 | ```go |
| 138 | package evaluator |
| 139 | |
| 140 | // Eval evaluates an AST node and returns the result. |
| 141 | // Returns an error on division by zero. |
| 142 | func Eval(node ast.Node) (float64, error) |
| 143 | ``` |
| 144 | |
| 145 | ### REPL |
| 146 | |
| 147 | ```go |
| 148 | package repl |
| 149 | |
| 150 | // Run starts the read-eval-print loop, reading from r and writing to w. |
| 151 | func Run(r io.Reader, w io.Writer) |
| 152 | ``` |
| 153 | |
| 154 | ## Package Layout |
| 155 | |
| 156 | ``` |
| 157 | matheval/ |
| 158 | βββ cmd/ |
| 159 | β βββ matheval/ |
| 160 | β βββ main.go # entry point, calls repl.Run |
| 161 | βββ token/ |
| 162 | β βββ token.go # Token type and constants |
| 163 | βββ lexer/ |
| 164 | β βββ lexer.go # Tokenize function |
| 165 | β βββ lexer_test.go |
| 166 | βββ ast/ |
| 167 | β βββ ast.go # AST node types |
| 168 | βββ parser/ |
| 169 | β βββ parser.go # Parse function |
| 170 | β βββ parser_test.go |
| 171 | βββ evaluator/ |
| 172 | β βββ evaluator.go # Eval function |
| 173 | β βββ evaluator_test.go |
| 174 | βββ repl/ |
| 175 | β βββ repl.go # REPL loop |
| 176 | β βββ repl_test.go |
| 177 | βββ docs/ |
| 178 | β βββ design.md |
| 179 | β βββ plan.md |
| 180 | βββ go.mod |
| 181 | βββ README.md |
| 182 | ``` |
| 183 | |
| 184 | ## Error Handling |
| 185 | - Lexer: returns error for invalid characters (e.g. `@`, `#`) |
| 186 | - Parser: returns error for syntax errors (unexpected token, mismatched parens) |
| 187 | - Evaluator: returns error for division by zero |
| 188 | - REPL: catches any error, prints it, prompts for next input |
| 189 | |
| 190 | ## Key Design Decisions |
| 191 | 1. **Functional API over structs** β `Tokenize()`, `Parse()`, `Eval()` are stateless functions. No need for struct receivers since there's no configuration or state to carry. |
| 192 | 2. **Sealed AST interface** β unexported marker method prevents external implementations, keeping the node set closed. |
| 193 | 3. **Position tracking in tokens** β enables precise error messages ("error at position 5"). |
| 194 | 4. **REPL takes io.Reader/io.Writer** β makes it testable without stdin/stdout. |