Interpreter Pattern
- Description: Define a grammar as a class hierarchy where each rule maps to a node, then walk the resulting AST to evaluate sentences in the language — useful for tiny DSLs; for anything non-trivial, use a parser generator instead.
- My Notion Note ID: K2C-2-23
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Intent
- 2. Structure
- 3. C++ Example
- 4. When to Use / When Not To
- 5. Variants and Pitfalls
- 6. Related Patterns
- 7. References
1. Intent
- Given a language with a defined grammar, build a class per grammar rule, instantiate an AST from a sentence, then
interpret(context)recursively. - Rules of the grammar → classes; sentences → object trees.
- Best fit: small, stable DSLs — regex (the textbook GoF example), boolean filters, configuration expressions, calculator-like arithmetic, simple query languages.
- For real languages with non-trivial parsing or evaluation requirements: use a parser generator (ANTLR, Bison, PEG) and treat AST as data, not behavior.
2. Structure
- AbstractExpression — interface with
interpret(Context&). - TerminalExpression — leaf nodes (literals, variables, identifiers).
- NonterminalExpression — composite; holds child expressions; implements rules built from sub-expressions.
- Context — global info during interpretation (variable bindings, input stream, output buffer).
- Client — builds the AST (by hand or via a parser) and calls
interpret.
The AST is a Composite. Interpretation is recursive descent through that Composite.
3. C++ Example
Boolean expression grammar: Expr ::= Var | And(Expr, Expr) | Or(Expr, Expr) | Not(Expr).
#include <memory>
#include <string>
#include <unordered_map>
#include <iostream>
using Context = std::unordered_map<std::string, bool>;
struct BoolExpr {
virtual ~BoolExpr() = default;
virtual bool interpret(const Context&) const = 0;
};
class Var : public BoolExpr {
std::string name_;
public:
explicit Var(std::string n) : name_(std::move(n)) {}
bool interpret(const Context& c) const override { return c.at(name_); }
};
class And : public BoolExpr {
std::unique_ptr<BoolExpr> lhs_, rhs_;
public:
And(std::unique_ptr<BoolExpr> l, std::unique_ptr<BoolExpr> r) : lhs_(std::move(l)), rhs_(std::move(r)) {}
bool interpret(const Context& c) const override { return lhs_->interpret(c) && rhs_->interpret(c); }
};
class Or : public BoolExpr {
std::unique_ptr<BoolExpr> lhs_, rhs_;
public:
Or(std::unique_ptr<BoolExpr> l, std::unique_ptr<BoolExpr> r) : lhs_(std::move(l)), rhs_(std::move(r)) {}
bool interpret(const Context& c) const override { return lhs_->interpret(c) || rhs_->interpret(c); }
};
class Not : public BoolExpr {
std::unique_ptr<BoolExpr> e_;
public:
explicit Not(std::unique_ptr<BoolExpr> e) : e_(std::move(e)) {}
bool interpret(const Context& c) const override { return !e_->interpret(c); }
};
int main() {
// (a AND b) OR (NOT c)
auto expr = std::make_unique<Or>(
std::make_unique<And>(std::make_unique<Var>("a"), std::make_unique<Var>("b")),
std::make_unique<Not>(std::make_unique<Var>("c"))
);
Context ctx{{"a", true}, {"b", false}, {"c", false}};
std::cout << expr->interpret(ctx) << "\n"; // true (NOT c is true)
}
Notice: parsing (string → AST) is not part of the Interpreter pattern. GoF treats AST construction as separate; in practice a tiny recursive-descent parser feeds it.
4. When to Use / When Not To
Use when:
- Grammar is small, stable, and recursive in shape.
- Sentences are evaluated frequently; precompiled AST beats reparsing.
- You want operations on the tree expressed as methods (or Visitor, see § 6) rather than table-driven interpretation.
Avoid when:
- Grammar is large or evolving — class explosion; one class per production gets unmanageable past ~20 rules.
- Performance matters — tree-walking interpreters are 10–100x slower than bytecode VMs or JIT.
- You need anything beyond evaluation (good error messages, static analysis, optimization passes) — use a real parser generator + a proper IR.
Default position in modern code: reach for a parser generator (ANTLR, Boost.Spirit, PEGTL, Lark, tree-sitter) or an existing expression library (CEL, JsonLogic, mu-expressions); the Interpreter pattern itself is rarely written from scratch.
5. Variants and Pitfalls
- Tree-walking vs bytecode — Interpreter as GoF describes is tree-walking. Real interpreters compile AST → bytecode for one to two orders of magnitude speedup. Tree-walking is for teaching and tiny DSLs only.
- Class explosion — each grammar rule → class. Grammar with 50 productions = 50 classes. Mitigation: collapse similar rules (
BinaryOpwith an enum), or generate the classes from the grammar. - Recursion depth — tree walk uses host-language stack. Deep expressions overflow. Convert recursion to a worklist for production code.
- Mixing parsing in nodes — GoF deliberately splits parsing from interpretation; some implementations conflate them, then the parser is impossible to replace.
- Context object — global state for the walk. Make it the place for variable bindings, output, errors; don't let interpretation reach into globals.
- Adding new operations — methods on each node scale linearly with operation count. Use Visitor (§ 6) to isolate each new operation.
6. Related Patterns
- Composite — the Interpreter's AST is a Composite. Interpreter adds the
interpretoperation to a Composite tree of expression nodes. - Visitor — the standard way to add operations to an Interpreter's AST without modifying each node. Eval, pretty-print, type-check, optimize → each as a separate Visitor. See Visitor Pattern.
- Iterator — used to step through tokens or AST children.
- Flyweight — when many leaf nodes are identical (e.g., the same
Var("x")), share a single instance. - Modern replacement — parser generators — ANTLR, Bison/Yacc, PEGTL, Boost.Spirit. They generate the AST + parser; you write only the semantic actions. For anything beyond a calculator, default to one of these.
- Modern replacement —
std::variantAST — represent the AST as a sum type and evaluate withstd::visit(see Visitor Pattern § 4). Same effect, less boilerplate, no virtual hierarchy.
7. References
- Gamma, Helm, Johnson, Vlissides. Design Patterns. Interpreter chapter.
- Aho, Lam, Sethi, Ullman. Compilers: Principles, Techniques, and Tools ("Dragon Book") — for the real story on parsing + IR.
- ANTLR — modern parser generator.
- PEGTL — header-only C++ PEG parser.
- Boost.Spirit — C++ expression-template parser.
- tree-sitter — incremental parser generator used by editors.