blob: 9b9ffd5bfdd06f4d4685e92735849a0b8c440cca [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`)
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```
35Input 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
63package token
64
65type Type int
66
67const (
68 Number Type = iota
69 Plus // +
70 Minus // -
71 Star // *
72 Slash // /
73 LParen // (
74 RParen // )
75 EOF
76)
77
78type 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
88package lexer
89
90// Tokenize converts an input string into a slice of tokens.
91// Returns an error if the input contains invalid characters.
92func Tokenize(input string) ([]token.Token, error)
93```
94
95### AST (data types)
96
97```go
98package ast
99
100// Node is the interface all AST nodes implement.
101type Node interface {
102 node() // sealed marker method
103}
104
105// NumberLit represents a numeric literal.
106type NumberLit struct {
107 Value float64
108}
109
110// BinaryExpr represents a binary operation (e.g. 1 + 2).
111type BinaryExpr struct {
112 Op token.Type // Plus, Minus, Star, Slash
113 Left Node
114 Right Node
115}
116```
117
118### Parser
119
120```go
121package parser
122
123// Parse converts a slice of tokens into an AST.
124// Returns an error for malformed expressions (mismatched parens, etc.).
125func Parse(tokens []token.Token) (ast.Node, error)
126```
127
128Grammar (recursive-descent):
129```
130expr β†’ term (('+' | '-') term)*
131term β†’ factor (('*' | '/') factor)*
132factor β†’ NUMBER | '(' expr ')'
133```
134
135### Evaluator
136
137```go
138package evaluator
139
140// Eval evaluates an AST node and returns the result.
141// Returns an error on division by zero.
142func Eval(node ast.Node) (float64, error)
143```
144
145### REPL
146
147```go
148package repl
149
150// Run starts the read-eval-print loop, reading from r and writing to w.
151func Run(r io.Reader, w io.Writer)
152```
153
154## Package Layout
155
156```
157matheval/
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
1911. **Functional API over structs** β€” `Tokenize()`, `Parse()`, `Eval()` are stateless functions. No need for struct receivers since there's no configuration or state to carry.
1922. **Sealed AST interface** β€” unexported marker method prevents external implementations, keeping the node set closed.
1933. **Position tracking in tokens** β€” enables precise error messages ("error at position 5").
1944. **REPL takes io.Reader/io.Writer** β€” makes it testable without stdin/stdout.