Interpreters

An interpreter runs the program by evaluating its structure directly — no compilation step. For DSLs, interpretation is usually the first runtime that works, the one the REPL uses, and often the only runtime a DSL needs.

Interpretation Strategies

Tree-Walking

Evaluate the AST directly: expressions compute values, statements perform effects, control flow recurses into child nodes. Simplest to build (five steps from Crafting Compilers get a runner in a day) and trivial to debug, since the tree is your call stack.

Source-to-Source

Translate the DSL into a host language (Python, SQL, C) and let the host run it. Zero runtime code of your own; the cost is mapping host errors back onto DSL text — which is why a thin error column matters.

Bytecode (Preview)

Compile to an instruction list and loop over it (Bytecode & Codegen + Virtual Machines). Faster and more predictable, but you now maintain a VM — the chapter after next.

Evaluation Core

Environments Again

The semantics from Semantics & Scoping become one class: Env holds a dict plus a link to its parent, and lookup(name) walks parents. Everything — variables, functions, DSL tables — is data in scopes.

Statements and Effects

Expressions return values; statements change state. A tiny dispatcher on the node type (print, let, if, loop) keeps the interpreter readable, and one RuntimeError type carries user-level failures to the error reporter from Errors & Diagnostics.

The REPL Loop

read (lex+parse) → eval (with one persistent environment) → print result → loop. A REPL is the cheapest teaching tool a DSL can have, and it costs one small function on top of the evaluator.

Interpreter Performance

When to Optimize

Tree-walking pays an indirection per node; that is fine for interactive and config-scale DSLs. When profiling says otherwise, the fix order is: cache resolved lookups, then compile to bytecode (next two topics) — never hand-optimize the walker before measuring.

Example: A Tree-Walker

The interpretive heart of the mini-language from Crafting Compilers, in one commented class.

evaluator.py (commented)

# evaluator.py — evaluate the AST from the parsing chapter
class Evaluator:
    def __init__(self): self.env = {}           # one flat scope is fine for mini

    def eval(self, node):
        # dispatch on the node kind, exactly like an interpreter switch
        kind = node[0]
        if kind == "num":   return node[1]
        if kind == "binop": # recurse into children, apply the operation
            left  = self.eval(node[1])
            right = self.eval(node[2])
            op = node[0][1]
            return {"+": add, "-": sub, "*": mul}[op](  left, right)
        if kind == "print": print(self.eval(node[1])); return None
        if kind == "let":   self.env[node[1]] = self.eval(node[2]); return None
        if kind == "name":  return self.env[node[1]]   # resolved earlier
        raise SyntaxError(f"unknown node {node!r}")

How to Read It

One dispatch per node kind, recursion where the grammar recursed: this is the entire semantics of the language in fifty lines, readable top to bottom — the property that makes interpreters the right first runtime.

Next Steps

Continue Phase 2

Interpretation is immediate; compilation is scalable. Move the same language to a compiled form: Bytecode & Codegen, then Virtual Machines & Runtimes.

Resources