Intent
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in that language. Each grammar rule becomes a class; expressions become trees; evaluation becomes recursion.
The Problem
Your product team wants to define feature-flag rollout rules in a small expression language:
plan = "premium" AND (region IN ["EU", "UK"] OR is_internal)
You could:
- Hard-code the rules in PHP/JS. Now product needs an engineer for every change. Iteration grinds to a stop.
- Use a full parser library. ANTLR or PEG.js for a five-keyword DSL is a lot of dependency for a small problem.
eval()the string. Don't.
The shape of the smell: you have a small, stable grammar and you need to evaluate sentences in it — without the weight of a real compiler frontend.
The Solution
Interpreter says: turn each grammar rule into a class. Compose them into expression trees. Each class implements one method — interpret(context) — that evaluates itself, recursively interpreting any children.
interface Expression
{
public function interpret(array $context): bool;
}
final class Equals implements Expression
{
public function __construct(private string $key, private mixed $value) {}
public function interpret(array $context): bool
{
return ($context[$this->key] ?? null) === $this->value;
}
}
final class AndExpr implements Expression
{
public function __construct(private Expression $left, private Expression $right) {}
public function interpret(array $context): bool
{
return $this->left->interpret($context) && $this->right->interpret($context);
}
}
final class InList implements Expression
{
public function __construct(private string $key, private array $values) {}
public function interpret(array $context): bool
{
return in_array($context[$this->key] ?? null, $this->values, true);
}
}
The expression plan == "premium" AND region IN ["EU","UK"] becomes a tree:
AndExpr
├── Equals("plan", "premium")
└── InList("region", ["EU", "UK"])
tree->interpret(['plan' => 'premium', 'region' => 'EU']) returns true. Adding a new operator is a new class implementing Expression. Product can edit the rule in a config file; you (or a tiny parser) build the tree from it.
Real-World Analogy
Reading a sentence aloud. "The hungry cat chased the small mouse." Your brain doesn't run a single uniform process — each word interprets itself. The article ("the") signals the next noun is specific. The adjective ("hungry") attaches to the noun that follows. The verb ("chased") looks for a subject before it and an object after. Each word knows what kind of thing it is and what part it plays. The meaning emerges from how the parts compose.
That's exactly Interpreter. Each grammar node is a small class that knows what to do with itself; composition does the rest.
Structure
Four roles you'll see in every Interpreter implementation:
- Abstract Expression — the interface every grammar node implements. Usually has one method:
interpret(context). Here:Expression. - Terminal Expression — a leaf in the grammar (literal values, identifiers, constants). Has no children. Here:
Equals,InList. - Non-Terminal Expression — a composite grammar rule (operators, function calls, conditionals). Holds child expressions and combines their results. Here:
AndExpr,OrExpr,NotExpr. - Context — the data being interpreted. Here: a
["plan" => "premium", "region" => "EU"]map.
The defining property: the tree of nodes is the program; calling interpret on the root recursively interprets the whole thing. The pattern is fundamentally a Composite with one operation defined on it.
Code Examples
Here's a feature-flag rules interpreter in five languages. Watch how each grammar node is small and focused, and how the recursive interpret call does the evaluation work.
type Context = Record<string, unknown>;
interface Expression {
interpret(ctx: Context): boolean;
}
class Equals implements Expression {
constructor(private key: string, private value: unknown) {}
interpret(ctx: Context): boolean { return ctx[this.key] === this.value; }
}
class InList implements Expression {
constructor(private key: string, private values: unknown[]) {}
interpret(ctx: Context): boolean { return this.values.includes(ctx[this.key]); }
}
class HasFlag implements Expression {
constructor(private key: string) {}
interpret(ctx: Context): boolean { return Boolean(ctx[this.key]); }
}
class And implements Expression {
constructor(private left: Expression, private right: Expression) {}
interpret(ctx: Context): boolean { return this.left.interpret(ctx) && this.right.interpret(ctx); }
}
class Or implements Expression {
constructor(private left: Expression, private right: Expression) {}
interpret(ctx: Context): boolean { return this.left.interpret(ctx) || this.right.interpret(ctx); }
}
// Build the tree for: plan == "premium" AND (region IN ["EU","UK"] OR is_internal)
const rule = new And(
new Equals("plan", "premium"),
new Or(
new InList("region", ["EU", "UK"]),
new HasFlag("is_internal"),
),
);
rule.interpret({ plan: "premium", region: "EU", is_internal: false }); // true
from abc import ABC, abstractmethod
class Expression(ABC):
@abstractmethod
def interpret(self, ctx): ...
class Equals(Expression):
def __init__(self, key, value):
self.key, self.value = key, value
def interpret(self, ctx):
return ctx.get(self.key) == self.value
class InList(Expression):
def __init__(self, key, values):
self.key, self.values = key, values
def interpret(self, ctx):
return ctx.get(self.key) in self.values
class HasFlag(Expression):
def __init__(self, key):
self.key = key
def interpret(self, ctx):
return bool(ctx.get(self.key))
class And(Expression):
def __init__(self, left, right):
self.left, self.right = left, right
def interpret(self, ctx):
return self.left.interpret(ctx) and self.right.interpret(ctx)
class Or(Expression):
def __init__(self, left, right):
self.left, self.right = left, right
def interpret(self, ctx):
return self.left.interpret(ctx) or self.right.interpret(ctx)
rule = And(
Equals("plan", "premium"),
Or(InList("region", ["EU", "UK"]), HasFlag("is_internal")),
)
rule.interpret({"plan": "premium", "region": "EU", "is_internal": False}) # True
public interface Expression {
boolean interpret(Map<String, Object> ctx);
}
public final class Equals implements Expression {
private final String key;
private final Object value;
public Equals(String key, Object value) { this.key = key; this.value = value; }
@Override public boolean interpret(Map<String, Object> ctx) {
return Objects.equals(ctx.get(key), value);
}
}
public final class InList implements Expression {
private final String key;
private final List<Object> values;
public InList(String key, List<Object> values) { this.key = key; this.values = values; }
@Override public boolean interpret(Map<String, Object> ctx) {
return values.contains(ctx.get(key));
}
}
public final class And implements Expression {
private final Expression left, right;
public And(Expression left, Expression right) { this.left = left; this.right = right; }
@Override public boolean interpret(Map<String, Object> ctx) {
return left.interpret(ctx) && right.interpret(ctx);
}
}
public final class Or implements Expression {
private final Expression left, right;
public Or(Expression left, Expression right) { this.left = left; this.right = right; }
@Override public boolean interpret(Map<String, Object> ctx) {
return left.interpret(ctx) || right.interpret(ctx);
}
}
<?php
namespace App\Rules;
interface Expression
{
public function interpret(array $ctx): bool;
}
final class Equals implements Expression
{
public function __construct(private string $key, private mixed $value) {}
public function interpret(array $ctx): bool
{
return ($ctx[$this->key] ?? null) === $this->value;
}
}
final class InList implements Expression
{
public function __construct(private string $key, private array $values) {}
public function interpret(array $ctx): bool
{
return in_array($ctx[$this->key] ?? null, $this->values, true);
}
}
final class HasFlag implements Expression
{
public function __construct(private string $key) {}
public function interpret(array $ctx): bool
{
return (bool) ($ctx[$this->key] ?? false);
}
}
final class AndExpr implements Expression
{
public function __construct(private Expression $left, private Expression $right) {}
public function interpret(array $ctx): bool
{
return $this->left->interpret($ctx) && $this->right->interpret($ctx);
}
}
final class OrExpr implements Expression
{
public function __construct(private Expression $left, private Expression $right) {}
public function interpret(array $ctx): bool
{
return $this->left->interpret($ctx) || $this->right->interpret($ctx);
}
}
package rules
type Context map[string]any
type Expression interface {
Interpret(ctx Context) bool
}
type Equals struct {
Key string
Value any
}
func (e Equals) Interpret(ctx Context) bool {
return ctx[e.Key] == e.Value
}
type InList struct {
Key string
Values []any
}
func (i InList) Interpret(ctx Context) bool {
v := ctx[i.Key]
for _, allowed := range i.Values {
if v == allowed {
return true
}
}
return false
}
type HasFlag struct{ Key string }
func (h HasFlag) Interpret(ctx Context) bool {
v, ok := ctx[h.Key].(bool)
return ok && v
}
type And struct{ Left, Right Expression }
func (a And) Interpret(ctx Context) bool {
return a.Left.Interpret(ctx) && a.Right.Interpret(ctx)
}
type Or struct{ Left, Right Expression }
func (o Or) Interpret(ctx Context) bool {
return o.Left.Interpret(ctx) || o.Right.Interpret(ctx)
}
The whole interpreter is a few small classes. Each one knows how to evaluate exactly its own kind of node. Adding a new operator (a NotExpr, an In Range, a Matches "regex") is a single new class. The tree builder — parsing the user's string into the tree — is a separate concern and usually a separate module.
When to Use It
Reach for Interpreter when you can answer "yes" to all of these:
- You have a small, stable grammar — fewer than ~10 rules.
- The grammar is your application's domain — pricing rules, feature flags, query filters, validation expressions, configuration logic.
- External users (often non-engineers) need to write expressions the system evaluates.
- You don't need full programming-language features — just enough to express what the domain requires.
- Performance isn't a hot path. Walking a tree of small objects is slower than compiled code; for cold paths it doesn't matter.
If the grammar will grow into a real programming language — variables, functions, scopes, types — use a parser generator (ANTLR, PEG.js, Tree-sitter, Lark) instead. Interpreter doesn't scale to large grammars.
Pros and Cons
Pros
- Adding new grammar rules is just a new class — no edits to existing rules.
- The class structure mirrors the grammar one-for-one, so the code is easy to read alongside the grammar.
- Composes with Composite (the grammar tree is a composite) and Visitor (multiple operations across the tree) cleanly.
- Trees are easy to construct programmatically (factory methods, builder), to serialise (for storage), and to inspect (for debugging).
- Useful even without a parser — you can let users build trees through a UI or YAML config rather than a string syntax.
Cons
- Doesn't scale to complex grammars. Eight rules is fine. Eighty rules and you've written a parser badly.
- Performance is poor for tight loops. Each node is an object dispatch; tight evaluation loops want compiled bytecode, not a tree of method calls.
- You still need a parser to convert text into the tree. The pattern covers evaluation, not parsing — those are separate problems.
- Operator precedence and parsing edge cases are easy to get wrong if you write your own parser; usually worth a small parser library even when the interpreter itself is hand-rolled.
Pro Tips
- Separate parsing from interpretation. A
Parserproduces anExpressiontree; the tree'sinterpretevaluates it. Mixing them produces a mess that's neither a clean interpreter nor a clean parser. - Build the tree programmatically first, then add a parser. Confirm the tree-walking part works with hand-built trees in tests. Then put a parser on top once the evaluation is solid.
- Cache parsed trees. Parsing the same rule string repeatedly is wasteful. Parse once, store the tree, evaluate many times.
- For anything beyond ~10 rules, reach for a parser library. Hand-written interpreters get ugly fast as the grammar grows.
- Use Visitor to add operations on the tree. Pretty-print, optimise, type-check, compile — all separate visitors. Keep
interpretas the one default operation; everything else lives in visitors. - Validate at parse time, not at interpret time. A bad rule should fail when it's added, not the millionth time it's evaluated.
Relations with Other Patterns
- Composite is the natural shape: every Interpreter tree is a Composite where the leaves are terminal expressions and the branches are non-terminal ones. Reading the patterns together, Interpreter is "Composite + one operation called interpret."
- Visitor is the right partner when you want to add multiple operations across the grammar tree. The
interpretmethod on each node is the primary operation; visitors are where additional ones live. - Iterator can walk the tree in a particular order — depth-first for evaluation, breadth-first for some optimisations.
- Flyweight can share the small grammar nodes across many tree instances — useful for things like glyph trees in a text renderer.
- Compiler / parser libraries (ANTLR, PEG.js, Tree-sitter, Lark) are the industrial-strength alternative. They generate parsers and tree types for you; you can then add Interpreter or Visitor on top.
Final Tips
The Interpreter I've shipped most often isn't a literal language interpreter — it's a rules engine. Pricing rules, validation rules, feature-flag rules, permission rules. Each was a small grammar with a tree of small classes. Each let non-engineers (product, ops, support) edit the rules themselves through a UI or a YAML file. The engineering team kept the language stable; the rules grew without code changes.
Reach for Interpreter when you have a small DSL and you want to keep it small. The pattern doesn't scale to a real language, and that's fine — most of the time you don't need a real language. You need a few well-named operators, composed into trees, evaluated against context. Interpreter is exactly that, and almost nothing more.


