Turn a string like 2 + 3 * 4 into a syntax tree and evaluate it to 14, using the Pratt parsing technique where operator precedence and associativity fall out of one small loop of binding powers. Every lesson is one concrete spec with exact tokens, a fully parenthesized AST render, or an exact numeric result: 10 - 3 - 2 groups left to 5, 2 ^ 3 ^ 2 groups right to 512, parentheses override precedence, unary minus follows the -2^2 convention (which is -4), and malformed input reports a clear error with the position where it went wrong.
Over 30 lessons you build an arithmetic expression evaluator from first principles: a small library that turns a string like `2 + 3 * 4` into an abstract syntax tree and evaluates it to an exact number. The heart of it is a Pratt parser (top-down operator precedence), the technique that makes operator precedence and associativity fall out of a single small loop of binding powers rather than a tangle of grammar rules.
You start with a tokenizer that scans numbers, operators, and parentheses, then build the parser: prefix handlers for numbers, grouping, and unary minus, and infix handlers for the binary operators, with left-associative plus, minus, times, divide, and modulo and a right-associative power. On top of the tree you write an evaluator that computes with float64 values, an environment that maps variable names to numbers, and built-in functions like `sqrt`, `abs`, and `max` with comma-separated arguments. The final chapter adds clear error messages that point at the position where input went wrong, and a capstone that evaluates a batch of real expressions to their exact results.
This is a focused, fully offline numeric expression evaluator, not a general programming language: it computes with double-precision floats, offers a fixed set of built-in functions, and evaluates one expression at a time. The finalize pass wraps the unchanged library in a small calculator REPL you can run, so the same code you build lesson by lesson also becomes an interactive calculator.
Before you can parse anything you need to split a string into the small pieces a parser understands, called tokens. Today you define the token type and tokenize the simplest possible input, the empty string, which yields a single end-of-input marker.
Define a token that carries a kind, its text, and its position, and tokenize an empty string.
A parser does not work on raw characters; it works on tokens, the meaningful chunks of the input: a number, an operator, a parenthesis. The tokenizer (also called a lexer or scanner) is the first stage of the pipeline, and everything else builds on the stream it produces. A token needs three things: its kind (what it is), its text (the exact characters it came from), and its position (where in the input it started). That position rides along unused for a while, but it is what lets error messages later point at exactly where something went wrong.
Every token stream ends with a special EOF token so later stages have an unambiguous “nothing more to read” marker instead of checking lengths everywhere. Its position is the length of the input, which is the offset just past the last character. Defining the full set of kinds up front, even though most stay unused today, keeps the type stable as you teach the scanner one character class at a time.
type Kind intconst ( EOF Kind = iota; Number; Operator; LParen; RParen; Ident; Comma; Illegal )type Token struct { Kind Kind; Text string; Pos int }// scan left to right; here there is nothing to scan, so just// append the terminating EOF at the end of the inputfunc Tokenize(in string) []Token {var toks []Token// ... scanning goes here in later lessons ...return append(toks, Token{EOF, "", len(in)})}
The library is complete and lesson-faithful (tokenizer, Pratt parser, and float64 evaluator with variables, built-in functions, and positioned error messages), and the finalize pass wraps it unchanged in a genuinely usable calculator REPL with variable assignment and persistent state, but it deliberately stops short of comparisons and booleans, user-defined functions, and readline editing to avoid inventing subsystems the lessons never introduced.
The original 1973 paper that introduced the technique this project is built on. Pratt shows how attaching a binding power to each token and a small recursive loop makes precedence and associativity fall out without a formal grammar.
The clearest modern explanation of Pratt parsing: prefix and infix handlers, binding powers, and how left versus right binding power encodes associativity. The direct inspiration for this project arc.
The chapter Compiling Expressions builds a Pratt parser in a full language implementation. Read it to see the technique scale from a calculator to a real interpreter.
The classic stack-based alternative to Pratt parsing for the same problem. Comparing the two is the fastest way to understand what binding powers buy you over an explicit operator stack.
A careful walkthrough that connects Pratt parsing back to plain recursive descent and works several precedence and associativity examples by hand.
Builds a Pratt parser for a small language step by step, with the prefix and infix registration pattern laid out in full. A good companion once you want to grow this evaluator toward a language.