Interpreter
Defines a representation for a language's grammar and an interpreter that uses that representation to process sentences — each grammar rule becomes a class, and the syntax tree is built by composing those classes.
Intent
Given a language, define a representation for its grammar along with an interpreter that uses that representation to process sentences in the language. Each grammar rule maps to a class; complex expressions are formed by composing objects of those classes into syntax trees — which are, therefore, Composites.
Cataloged by the GoF (1994) as a behavioral pattern, Interpreter is more educational than widely applied in modern projects. It's useful for simple languages and small DSLs where the grammar has few rules and the manual expressiveness of classes pays off. For real grammars — with complex tokens, operator precedence, lexical and syntax error handling — use dedicated tools: parser generators (ANTLR, PEG.js, Tree-sitter), parser combinators (Parsimmon, Chevrotain), or PEG/BNF grammars. Interpreter's class-based approach quickly becomes unviable as the grammar grows.
Problem
Imagine a configuration system that accepts filters written as
mathematical expressions with variables: a + b * 3,
where a and b come from a context
(record, session, configuration). The naive approach evaluates the
expression as a string at runtime — unsafe and inflexible:
// Naive approach — DON'T do this:
function evaluate(expression: string, context: Record<string, number>): number {
// eval() executes arbitrary code — code injection risk.
// No validation, no typing, no useful error messages.
return eval(expression.replace(/([a-z]+)/g, (_, v) => String(context[v] ?? 0)));
}
console.log(evaluate('a + b * 3', { a: 5, b: 4 })); // 17 (but dangerous!)
// Worse: evaluate('process.exit(1)', {}) crashes the process.
Interpreter solves this by mapping each grammar rule to a class.
The expression is represented as a tree of objects — built
manually or by a simple parser. Each node knows how to interpret
itself given a context, and evaluation happens through
polymorphic calls over the tree — no eval(), no
arbitrary code.
Solution
Interpreter organizes the code into four participants:
-
AbstractExpression (interface): declares the
interpret(context): Tmethod that every expression must implement. The return type depends on the language: number, boolean, string. The context carries the external state needed for evaluation (variable values, data records, settings). -
TerminalExpression: implements the
interpretation of a terminal symbol of the grammar — a symbol
that doesn't expand into others. Examples:
NumberLiteral(returns a literal value) andVariable(looks up the context). These are the syntax tree's leaves. -
NonTerminalExpression: implements a grammar
rule that references other expressions. Examples:
Sum,Subtraction,Multiplication. Each NonTerminalExpression holds references to sub-expressions (left and right) and delegates interpretation to them before combining the results. Since each NonTerminalExpression contains otherIExpressionobjects, the structure is a Composite. -
Context: carries the global information needed
for interpretation — typically the variable values. In
TypeScript, it can be a simple
Record<string, number>; in more complex systems, an object with query methods and accumulated state.
Evaluation happens by calling
rootExpression.interpret(context): the method
recursively walks the tree, from internal nodes down to the
leaves, and returns the combined result. The grammar is implicit
in the classes' structure — there's no parser generator, just
composed objects.
Structure
«interface»
IExpression
┌────────────────────────────────────────┐
│ + interpret(ctx: Context): number │
└────────────────────────────────────────┘
▲
┌───────────┼────────────────────────────────┐
│ │ │ │ │
NumberLiteral Variable Sum Subtraction Multiplication
(Terminal) (Terminal) (NonTerminal)(NonTerminal)(NonTerminal)
│ │
│ returns │ looks up ctx[name]
│ this.value│
│ │
└─ doesn't reference other IExpression (leaf)
Sum / Subtraction / Multiplication:
┌───────────────────────────────────────────────┐
│ - left: IExpression │
│ - right: IExpression │
│ + interpret(ctx): number │
│ return left.interpret(ctx) │
│ OP right.interpret(ctx) │
└───────────────────────────────────────────────┘
(internal Composite — each node can contain other nodes)
Context:
{ a: 5, b: 4 } (Record<string, number>)
Flow for "a + (b * 3)", ctx = {a:5, b:4}:
Sum
├── Variable('a') → interpret(ctx) = 5
└── Multiplication
├── Variable('b') → interpret(ctx) = 4
└── NumberLiteral(3) → interpret(ctx) = 3
→ 4 * 3 = 12
→ 5 + 12 = 17
Code examples
Example 1 — Mathematical expressions with variables and context
A complete implementation with the IExpression
interface, two terminal expressions (NumberLiteral
and Variable), and three non-terminals
(Sum, Subtraction,
Multiplication). The context is a simple map of
variables. The tree is built manually — in real systems, a parser
would do this work.
// ── Context ───────────────────────────────────────────────────
// Map of variables available during interpretation.
type Context = Record<string, number>;
// ── AbstractExpression ────────────────────────────────────────
interface IExpression {
interpret(ctx: Context): number;
}
// ── TerminalExpressions ───────────────────────────────────────
// NumberLiteral: a leaf that returns a literal value.
class NumberLiteral implements IExpression {
constructor(private readonly value: number) {}
interpret(_ctx: Context): number {
return this.value;
}
}
// Variable: a leaf that looks up the context by name.
class Variable implements IExpression {
constructor(private readonly name: string) {}
interpret(ctx: Context): number {
if (!(this.name in ctx)) {
throw new Error(`Variable '${this.name}' not defined in context`);
}
return ctx[this.name];
}
}
// ── NonTerminalExpressions ────────────────────────────────────
class Sum implements IExpression {
constructor(
private readonly left: IExpression,
private readonly right: IExpression
) {}
interpret(ctx: Context): number {
return this.left.interpret(ctx) + this.right.interpret(ctx);
}
}
class Subtraction implements IExpression {
constructor(
private readonly left: IExpression,
private readonly right: IExpression
) {}
interpret(ctx: Context): number {
return this.left.interpret(ctx) - this.right.interpret(ctx);
}
}
class Multiplication implements IExpression {
constructor(
private readonly left: IExpression,
private readonly right: IExpression
) {}
interpret(ctx: Context): number {
return this.left.interpret(ctx) * this.right.interpret(ctx);
}
}
// ── Usage ────────────────────────────────────────────────────
// Expression: a + (b * 3) - 1
// AST built manually — in production, a parser would do this.
const expression: IExpression = new Subtraction(
new Sum(
new Variable('a'),
new Multiplication(new Variable('b'), new NumberLiteral(3))
),
new NumberLiteral(1)
);
// Context 1
const ctx1: Context = { a: 5, b: 4 };
console.log(expression.interpret(ctx1));
// 5 + (4 * 3) - 1 = 16
// Context 2 — same expression, different values
const ctx2: Context = { a: 10, b: 2 };
console.log(expression.interpret(ctx2));
// 10 + (2 * 3) - 1 = 15
// Composite expression: (a - b) * (a + b) → difference of squares
const diffOfSquares: IExpression = new Multiplication(
new Subtraction(new Variable('a'), new Variable('b')),
new Sum(new Variable('a'), new Variable('b'))
);
// a=5, b=3 → (5-3)*(5+3) = 2*8 = 16
console.log(diffOfSquares.interpret({ a: 5, b: 3 })); // 16
<?php
// ── AbstractExpression ────────────────────────────────────────
interface IExpression
{
/** @param array<string, float> $ctx */
public function interpret(array $ctx): float;
}
// ── TerminalExpressions ───────────────────────────────────────
class NumberLiteral implements IExpression
{
public function __construct(private readonly float $value) {}
public function interpret(array $ctx): float
{
return $this->value;
}
}
class Variable implements IExpression
{
public function __construct(private readonly string $name) {}
public function interpret(array $ctx): float
{
if (!array_key_exists($this->name, $ctx)) {
throw new \RuntimeException(
"Variable '{$this->name}' not defined in context"
);
}
return (float) $ctx[$this->name];
}
}
// ── NonTerminalExpressions ────────────────────────────────────
class Sum implements IExpression
{
public function __construct(
private readonly IExpression $left,
private readonly IExpression $right
) {}
public function interpret(array $ctx): float
{
return $this->left->interpret($ctx)
+ $this->right->interpret($ctx);
}
}
class Subtraction implements IExpression
{
public function __construct(
private readonly IExpression $left,
private readonly IExpression $right
) {}
public function interpret(array $ctx): float
{
return $this->left->interpret($ctx)
- $this->right->interpret($ctx);
}
}
class Multiplication implements IExpression
{
public function __construct(
private readonly IExpression $left,
private readonly IExpression $right
) {}
public function interpret(array $ctx): float
{
return $this->left->interpret($ctx)
* $this->right->interpret($ctx);
}
}
// ── Usage ────────────────────────────────────────────────────
// Expression: a + (b * 3) - 1
$expression = new Subtraction(
new Sum(
new Variable('a'),
new Multiplication(new Variable('b'), new NumberLiteral(3))
),
new NumberLiteral(1)
);
// Context 1
$ctx1 = ['a' => 5, 'b' => 4];
echo $expression->interpret($ctx1) . "\n";
// 5 + (4 * 3) - 1 = 16
// Context 2 — same expression, different values
$ctx2 = ['a' => 10, 'b' => 2];
echo $expression->interpret($ctx2) . "\n";
// 10 + (2 * 3) - 1 = 15
// Difference of squares: (a - b) * (a + b)
$diffOfSquares = new Multiplication(
new Subtraction(new Variable('a'), new Variable('b')),
new Sum(new Variable('a'), new Variable('b'))
);
echo $diffOfSquares->interpret(['a' => 5, 'b' => 3]) . "\n"; // 16
Example 2 — Mini boolean filter DSL
Interpreter is also well suited for small query or filter DSLs.
In this example, the language has three rules:
Equals (terminal — checks whether a record's field
has a given value), And, and Or as
non-terminals. The context is the record being evaluated. The
expression filters a list of records without eval()
and with type-safe composition.
// ── Context: a data record ────────────────────────────────────
type Record_ = Record<string, string | number>;
// ── AbstractExpression ────────────────────────────────────────
interface ICondition {
evaluate(record: Record_): boolean;
}
// ── TerminalExpression: field = value ─────────────────────────
class Equals implements ICondition {
constructor(
private readonly field: string,
private readonly value: string | number
) {}
evaluate(record: Record_): boolean {
return record[this.field] === this.value;
}
}
// ── NonTerminalExpression: And ────────────────────────────────
class And implements ICondition {
constructor(
private readonly left: ICondition,
private readonly right: ICondition
) {}
evaluate(record: Record_): boolean {
// Short-circuit: if left is false, right isn't evaluated.
return this.left.evaluate(record) && this.right.evaluate(record);
}
}
// ── NonTerminalExpression: Or ──────────────────────────────────
class Or implements ICondition {
constructor(
private readonly left: ICondition,
private readonly right: ICondition
) {}
evaluate(record: Record_): boolean {
return this.left.evaluate(record) || this.right.evaluate(record);
}
}
// ── Usage ────────────────────────────────────────────────────
// DSL: status = 'active' AND (plan = 'pro' OR plan = 'enterprise')
const filter: ICondition = new And(
new Equals('status', 'active'),
new Or(
new Equals('plan', 'pro'),
new Equals('plan', 'enterprise')
)
);
const records: Record_[] = [
{ status: 'active', plan: 'pro' },
{ status: 'active', plan: 'free' },
{ status: 'inactive', plan: 'pro' },
{ status: 'active', plan: 'enterprise' },
];
const result = records.filter(r => filter.evaluate(r));
console.log(result);
// [{ status: 'active', plan: 'pro' }, { status: 'active', plan: 'enterprise' }]
// The filter tree can be built dynamically from a JSON
// configuration — no eval(), with full control over what
// the language can express.
<?php
// ── AbstractExpression ────────────────────────────────────────
interface ICondition
{
/** @param array<string, string|float> $record */
public function evaluate(array $record): bool;
}
// ── TerminalExpression: field = value ─────────────────────────
class Equals implements ICondition
{
public function __construct(
private readonly string $field,
private readonly string|float $value
) {}
public function evaluate(array $record): bool
{
return isset($record[$this->field])
&& $record[$this->field] === $this->value;
}
}
// ── NonTerminalExpression: And ────────────────────────────────
// Named AndCondition (not And) because "and" is a reserved operator in PHP.
class AndCondition implements ICondition
{
public function __construct(
private readonly ICondition $left,
private readonly ICondition $right
) {}
public function evaluate(array $record): bool
{
// PHP's native short-circuit for &&.
return $this->left->evaluate($record)
&& $this->right->evaluate($record);
}
}
// ── NonTerminalExpression: Or ──────────────────────────────────
// Named OrCondition (not Or) because "or" is a reserved operator in PHP.
class OrCondition implements ICondition
{
public function __construct(
private readonly ICondition $left,
private readonly ICondition $right
) {}
public function evaluate(array $record): bool
{
return $this->left->evaluate($record)
|| $this->right->evaluate($record);
}
}
// ── Usage ────────────────────────────────────────────────────
// DSL: status = 'active' AND (plan = 'pro' OR plan = 'enterprise')
$filter = new AndCondition(
new Equals('status', 'active'),
new OrCondition(
new Equals('plan', 'pro'),
new Equals('plan', 'enterprise')
)
);
$records = [
['status' => 'active', 'plan' => 'pro'],
['status' => 'active', 'plan' => 'free'],
['status' => 'inactive', 'plan' => 'pro'],
['status' => 'active', 'plan' => 'enterprise'],
];
$result = array_filter($records, fn($r) => $filter->evaluate($r));
print_r(array_values($result));
// [
// ['status' => 'active', 'plan' => 'pro'],
// ['status' => 'active', 'plan' => 'enterprise']
// ]
When to use
- For small, stable DSLs: query filters, business rule expressions, small configuration mini-languages, or templates with few constructs. The number of expression classes should be small — up to ten rules is reasonable; beyond that, prefer a parser generator.
- When the grammar is well known and rarely changes: Interpreter is easy to extend with new operators (new classes), but any change to the grammar's core structure can cascade through every existing class.
- For educational purposes and prototyping: the pattern makes a grammar's structure explicit in class code — useful for learning how parsers work internally before using automated tools.
-
When security is critical: an
Interpreter-based DSL executes only the operations you defined
— with no risk of code injection via
eval(). Business rule systems, filtering configurations, and validation engines benefit from this explicit control.
When to avoid
- Complex grammars: each rule becomes a class; a grammar with 20 rules generates 20 classes — and the interaction between them quickly becomes hard to maintain. Use ANTLR, PEG.js, Chevrotain, or Tree-sitter for any language with non-trivial operator precedence, error recovery, or complex tokens.
- When performance is critical: recursing over the whole tree on every evaluation can be slow for complex or high-frequency expressions. Real compilers don't interpret the AST directly — they compile to bytecode or native code.
- When the pattern already exists in the language: many languages have idiomatic ways to compose predicates — closures, functional combinators, regular expressions. Class-based Interpreter is often more verbose than a functional solution.
Pros and cons
Pros
- The grammar is explicit in the code — each rule is a class with a descriptive name, easy to locate.
- Open for new rules: adding an operator means creating a new class implementing the interface — without modifying existing expressions.
- Safe, controlled evaluation: no eval(), no code injection, no access to unauthorized APIs.
- The expression tree can be built dynamically (from JSON, a database, or a UI) — enabling runtime-configurable DSLs.
- Clear separation between the expression's structure and the evaluation mechanism.
Cons
- Class explosion for non-trivial grammars — one class per rule becomes unviable above ~10-15 rules.
- No native support for lexical or syntax errors: building the tree manually doesn't detect invalid sentences — requires a separate parser.
- Lower performance than bytecode or compiled code — interpreted recursion over the AST is orders of magnitude slower than native code.
- Difficulty implementing operator precedence, associativity, and grouping without a dedicated parser.
Common pitfalls
1. Confusing Interpreter with Composite
Interpreter uses Composite internally — the syntax tree
is a Composite where the NonTerminalExpressions are the composite
nodes and the TerminalExpressions are the leaves. The difference
is one of intent: Composite organizes part-whole structures and
treats leaves and composites uniformly; Interpreter interprets
sentences of a specific grammar. When someone adds an
interpret() method to an existing Composite, they're
essentially applying Interpreter over the Composite's structure.
2. Uncontrolled class growth
The most critical scaling pitfall: every new grammar rule
requires a new class. A filter DSL that starts with
Equals, And, and Or seems
reasonable. But once you add GreaterThan,
LessThan, Contains,
StartsWith, Not, Between,
In — the number of classes grows quickly. Once that
point is reached, migrate to a parser combinator or a parser
generator.
Rule of thumb: if the grammar has more than 8 to 10 expression classes, seriously evaluate using a dedicated parsing library. Interpreter as a class-based pattern is meant for genuinely small DSLs — not for real languages.
3. No error handling when building the AST
Building the tree manually doesn't validate the sentence. An
expression like new Sum(null, new NumberLiteral(3))
compiles without errors but blows up at runtime with a null
reference exception. In TypeScript, use non-nullable types and
strict mode to mitigate this; in PHP, declare types in the
interface. For full validation of externally supplied sentences
(strings typed by the user), a parser with useful error messages
is indispensable.
4. Interpreter vs Composite — when to use which
Use Composite when the focus is the hierarchical part-whole structure and the main operation is uniform across all nodes (e.g.: calculating a folder's total size). Use Interpreter when the focus is evaluating sentences of a language with explicit grammar rules — where each node type has different evaluation semantics (Sum adds, Multiplication multiplies, Variable looks up the context). In practice, Interpreter is always a Composite; but not every Composite is an Interpreter.
Related patterns
Interpreter relates directly to structural and behavioral patterns that organize and traverse hierarchies:
Composite is Interpreter's structural foundation:
the abstract syntax tree (AST) is a Composite where the
NonTerminalExpressions are composite nodes and the
TerminalExpressions are leaves. The interpret()
method walks that tree recursively — exactly like the
operation() method walks a Composite.
Visitor is Interpreter's natural complement:
while Interpreter defines a single operation
(interpret()) directly on each expression class,
Visitor separates the operations from the classes — allowing new
behaviors (printing, optimizing, compiling) to be added to the
same AST without modifying it. In real compilers, the AST is
built with Composite, interpreted with Interpreter in early
phases, and later traversed by multiple analysis and
transformation passes implemented as Visitors.