Interpreter
Learning Objectives
Represent a small grammar as an abstract syntax tree (AST) and evaluate it.
Write a recursive-descent parser for a tiny language.
Know the limits of the pattern and when to use parser libraries.
Prerequisites: Modules 35, 49.
Intuition and Definition
Intent: given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences.
Participants: Expression interface with evaluate(context), terminal expressions (numbers, variables), non-terminal expressions (operators combining sub-expressions) — a Composite — plus a parser that builds the tree and a Context holding variable values.
Why It Matters
Rule engines, alarm conditions ("coolant > 110 and rpm > 3000"), feature flags and configuration formulas are small languages. A tiny interpreter keeps them out of hard-coded ifs and lets non-programmers change them.
Syntax and Language Notes
Grammar for this module's rule language (EBNF):
expr := term (("and" | "or") term)*
term := value ((">" | "<") value)?
value := NUMBER | IDENTIFIER | "(" expr ")"Complete Example
#include <cctype>
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
using Context = std::map<std::string, double>;
struct Expr { // AST node (Composite)
virtual ~Expr() = default;
virtual double eval(const Context& c) const = 0;
};
struct Num : Expr { double v; explicit Num(double x) : v(x) {} double eval(const Context&) const override { return v; } };
struct Var : Expr {
std::string name; explicit Var(std::string n) : name(std::move(n)) {}
double eval(const Context& c) const override { auto it = c.find(name); if (it == c.end()) throw std::runtime_error("unknown signal " + name); return it->second; }
};
struct Bin : Expr {
std::string op; std::unique_ptr<Expr> l, r;
Bin(std::string o, std::unique_ptr<Expr> a, std::unique_ptr<Expr> b) : op(std::move(o)), l(std::move(a)), r(std::move(b)) {}
double eval(const Context& c) const override {
if (op == "and") return (l->eval(c) != 0 && r->eval(c) != 0) ? 1 : 0; // short-circuit
if (op == "or") return (l->eval(c) != 0 || r->eval(c) != 0) ? 1 : 0;
double a = l->eval(c), b = r->eval(c);
…coolant=112 rpm=2500 load=85 -> WARN
coolant=112 rpm=2500 load=40 -> ok
coolant=95 rpm=4000 load=90 -> ok
parse error: unexpected character at position 10Step-by-Step Walkthrough
The parser turns the text into a tree: and(>(coolant,110), or(>(rpm,3000), >(load,80))).
Evaluation walks the tree with a context of current signal values; and/or short-circuit.
The third case is ok because coolant > 110 is false — the right side isn't even evaluated.
Malformed input is rejected with a clear message instead of misbehaving.
Common Mistakes
Growing a "tiny" language until it needs precedence rules, functions and error recovery — use a real parser generator or an embedded scripting language.
No validation of identifiers before evaluation (unknown signals at run time).
Evaluating untrusted expressions with eval()-style host-language execution — a security hole; always use a restricted interpreter.
Deep recursion on pathological input — limit nesting depth.
Trade-offs and Alternatives
Interpreter is right for small, stable grammars. For larger languages use parser generators (ANTLR), parser combinators, PEG libraries, or embed an existing language (Lua). For configuration, consider data formats (JSON/YAML) with schema validation instead of a language.
Real-World Use
Rule engines, alarm/telltale conditions, SQL WHERE clause evaluation, regular-expression engines, spreadsheet formulas, template languages, A2L/ODX-like calibration formulas.
Practice
Q1. (Easy) Which structural pattern does an AST use?
Answer: Composite.
Q2. (Medium) Why must and short-circuit?
Answer: For correctness (the right side might be invalid when the left is false, e.g. division by zero or a missing signal) and efficiency.
Q3. (Advanced) How would you add a "not" operator?
Answer: Extend the grammar (value := "not" value | ...), add a Not node whose eval returns !inner->eval(c), and parse not before other values in value().
Interview Question
Q1. "Why is building an AST better than evaluating the string directly each time?"
Answer: Parsing once separates syntax checking from evaluation: errors are reported up front, the tree can be validated (known identifiers, type checks), optimized (constant folding), visited for other purposes (pretty-printing, explaining why a rule fired), and evaluated many times quickly against changing contexts.
Quick Quiz
Q1. What are terminal expressions?
Answer: Leaves such as numbers and variables.
Q2. What is a context in the Interpreter pattern?
Answer: The current values (variables/signals) used during evaluation.
Q3. When should you stop hand-writing an interpreter?
Answer: When the grammar grows beyond a small, stable language — then use a parser library or an embedded language.
Summary
Interpreter = grammar → AST (Composite) → evaluate with a context.
Great for tiny rule languages; switch to tools as languages grow.
Related: Modules 35, 49.