The calculator running in the hero of this site is a ~1,586 line C++23 expression language: tokenizer, recursive-descent parser, tree-walking evaluator, compiled to WebAssembly at 183 KB gzipped. This is the grammar it parses and the one factoring decision that kept the parser from turning into a wall of near-identical functions.
Three stages, each testable alone
The interpreter is the textbook shape, kept deliberately small so each stage reads as a worked example. A hand-written lexer emits a typed token stream; a recursive-descent parser turns tokens into a std::variant-based AST owned by unique_ptr; a tree-walking evaluator folds the AST to a double against a mutable state. Errors are typed exceptions (ParseError, EvalError, CommandError) caught at the REPL loop, so one bad line never kills the session.
The grammar as a precedence ladder
The language is an expression grammar with assignment, ternaries, comparisons, the usual arithmetic, right-associative power, and user-defined functions with recursion. Written as a precedence ladder from loosest to tightest binding, it is:
assignment := equality ( '=' assignment )? // right assoc
ternary := equality ( '?' expr ':' ternary )? // right assoc
equality := relational ( ('=='|'!=') relational )*
relational := additive ( ('<'|'>'|'<='|'>=') additive )*
additive := term ( ('+'|'-') term )*
term := power ( ('*'|'/') power )*
power := unary ( '^' power )? // right assoc
unary := ('-'|'+')? primary
primary := number | ident | call | '(' expr ')'Associativity is where a naive parser gets subtle bugs. Power binds right, so 2^3^2 is , not . The parser encodes this by recursing into power itself on the right, while the left-associative levels loop instead of recurse:
The one factoring I am proud of
Six precedence levels with the same shape is six near-identical parse functions, which is six places for a bug to hide and six places to edit when the grammar changes. Instead I wrote one higher-order helper, abstract_parse, parameterized by its sub-parser, the set of operators it accepts, and its associativity. Every binary level is one call to it. A left-associative level is the fixed point of:
which the helper implements as a loop that folds operators left while the next token is in the operator set; a right-associative level recurses on itself instead. The whole ladder (power, term, additive, relational, equality, assignment) derives from that single function. It trades a little indirection for deleting the repetition, and it is the cleanest part of the codebase.
// left-assoc additive: term ( ('+'|'-') term )*
Expr parse_additive() {
return abstract_parse(&Parser::parse_term, {Plus, Minus}, Assoc::Left);
}
// right-assoc power: unary ( '^' power )?
Expr parse_power() {
return abstract_parse(&Parser::parse_unary, {Caret}, Assoc::Right);
}Function definitions are just an assignment with shape
The evaluator decides that f(x) = x*x is a definition rather than an evaluation by looking at the shape of an assignment: a Binary node with operator = whose left side is a function-call node becomes a definition; anything else is ordinary evaluation. That one dispatch is what turns the calculator into a small language. Recursion then works for free, because user functions live in shared state and a fresh local scope is built per call:
- built-ins
- 25 functions plus 3 constants (, , ), resolved before user functions so a user cannot shadow them by accident.
- tests
- 21 Catch2 cases across the tokenizer, parser, evaluator, and integration suites, on a 3-OS CI matrix, building with zero warnings under
-Wall -Wextra -std=c++23.
The parse tree the hero draws
The AST node taxonomy is small: numbers, identifiers, unary and binary operators, function calls, and the ternary. The hero renders exactly these node shapes for whatever you type. The example below, x > 0 ? x : -x, is the same tree the live engine builds.
The full grammar and the AST node definitions are in DESIGN.md and include/repl/expression.hpp. The engine that runs the hero is this exact repl_core library compiled to WebAssembly. Source at github.com/ShreeChaturvedi/math-repl.