Skip to content
FollowtheDamnCode

Interpreter

Represent each rule of a tiny language as a class, build sentences as trees of them, and evaluate by walking the tree.

Big idea

Interpreter models a small, well-defined language — rule expressions, search filters, unit formulas — as one class per grammar rule. A sentence becomes a tree of these objects (an abstract syntax tree), and evaluating it is just calling evaluate(context) on the root, which recursively evaluates its children.

The problem it solves

Product managers want feature flags like "on for users in India on the pro plan, or for anyone on the beta list" — and they want to change them without a deploy. Hard-coding each rule means a release per change. Storing rules as strings and parsing them with ad-hoc if "and" in rule hacks breaks on the first nested bracket.

A tiny language with a clear grammar, interpreted by a tree of objects, handles any combination — and adding a new operator is one new class.

Think of it like

Musical notation. A score is a sentence in a small language: notes, rests, repeats, a crescendo marking that applies to a phrase. A musician (the interpreter) reads it with a context — tempo, instrument — and performs it. "Repeat this bar twice" contains other notation inside it, just as or contains two sub-expressions.

Structure

Terminal and non-terminal expressions form a tree
  • Terminal expressions (Equals, InList) — leaves; evaluate directly against the context.
  • Non-terminal expressions (And, Or, Not) — hold sub-expressions and combine their results.
  • Context — the data a sentence is evaluated against (the current user).

In code

The interpreter itself is the classes; a tiny parser turns text into the tree. Here the parser handles field = value, field in a|b, not, and, or, and brackets.

Feature-flag rules as an interpreted language
import re

class Equals:
    def __init__(self, field, value): self.field, self.value = field, value
    def evaluate(self, ctx): return str(ctx.get(self.field)) == self.value

class InList:
    def __init__(self, field, values): self.field, self.values = field, set(values)
    def evaluate(self, ctx): return str(ctx.get(self.field)) in self.values

class And:
    def __init__(self, a, b): self.a, self.b = a, b
    def evaluate(self, ctx): return self.a.evaluate(ctx) and self.b.evaluate(ctx)

class Or:
    def __init__(self, a, b): self.a, self.b = a, b
    def evaluate(self, ctx): return self.a.evaluate(ctx) or self.b.evaluate(ctx)

class Not:
    def __init__(self, e): self.e = e
    def evaluate(self, ctx): return not self.e.evaluate(ctx)


def parse(text):                             # grammar: or > and > not > atom
    tokens = re.findall(r"\(|\)|[\w|]+|=", text)
    pos = 0
    def peek(): return tokens[pos] if pos < len(tokens) else None
    def take():
        nonlocal pos; pos += 1; return tokens[pos - 1]
    def parse_or():
        e = parse_and()
        while peek() == "or": take(); e = Or(e, parse_and())
        return e
    def parse_and():
        e = parse_not()
        while peek() == "and": take(); e = And(e, parse_not())
        return e
    def parse_not():
        if peek() == "not": take(); return Not(parse_not())
        return atom()
    def atom():
        if peek() == "(":
            take(); e = parse_or(); take(); return e
        field, op, value = take(), take(), take()
        return Equals(field, value) if op == "=" else InList(field, value.split("|"))
    return parse_or()


rule = parse("(country = IN and plan in pro|team) or beta = yes")
users = [
    {"name": "Asha", "country": "IN", "plan": "pro", "beta": "no"},
    {"name": "Ben", "country": "US", "plan": "pro", "beta": "no"},
    {"name": "Chen", "country": "SG", "plan": "free", "beta": "yes"},
    {"name": "Dev", "country": "IN", "plan": "free", "beta": "no"},
]
for u in users:
    print(f"{u['name']:5} {rule.evaluate(u)}")
print("not beta:", [u["name"] for u in users if parse("not beta = yes").evaluate(u)])
output
Asha  True
Ben   False
Chen  True
Dev   False
not beta: ['Asha', 'Ben', 'Dev']

Run and edit it in the app.

The rule is data — stored in a database, edited in an admin screen — and the tree evaluates it for any user.

Watch it

How to implement it

  1. Write the grammar down (even informally): what are the atoms, what combines them, what binds tighter.
  2. One class per rule; give every class the same evaluate(context) method.
  3. Build trees by hand, from a parser, or from JSON — the interpreter doesn't care how the tree was made.
  4. Keep the language small. When it grows loops, functions, and variables, use a real parser generator or an embedded scripting language.

In the rule "(a = 1 and b = 2) or c = 3", what is the root of the expression tree?

When to use it — and when not to

Use it for small, stable languages that change often in content but rarely in grammar: rule engines, search query filters, permission policies, simple calculators, configuration expressions. Avoid it for complex languages — performance and error messages suffer, and a hand-written class per rule becomes unmanageable.

Pros and cons

ProsCons
Rules become data, editable without deploysHard to maintain as the grammar grows
New operators are new classesSlow for heavy workloads compared with compiled approaches
Trees are easy to inspect, test, and printYou also need a parser, which is its own work

How it relates

  • The expression tree is a Composite.
  • New operations over the tree (pretty-printing, optimising, type-checking) are a natural fit for Visitor.
  • Iterator can walk the tree; Flyweight can share identical terminal nodes.
  • Strategy swaps code; Interpreter lets users write their own small programs.

Which is the best candidate for the Interpreter pattern?

Key terms

  • Interpreter — A pattern that represents grammar rules as classes and evaluates sentences as trees of them.
  • Abstract syntax tree (AST) — The tree of expression objects built from a sentence.
  • Terminal expression — A leaf that evaluates directly, such as country = IN.
  • Non-terminal expression — A node that combines sub-expressions, such as and or or.
  • Context — The data a sentence is evaluated against.