Chapter 20

Compilers & Interpreters

College
At a glance
Core ideaSource becomes runnable through lex → parse → evaluate stages.
Key termAST — the tree that captures grouping and operator precedence.
You can…Build a recursive-descent parser and evaluate expressions.
Watch outPrecedence comes from grammar structure, not special-case code.
Theory

Programs are written as text for humans but must ultimately run as instructions a machine executes. The translation happens in stages. A lexer (tokenizer) scans raw source text and groups characters into tokens — numbers, operators, identifiers — discarding irrelevant whitespace. A parser takes that token stream and builds an abstract syntax tree (AST), a structured representation that captures grouping and precedence according to the language's grammar (a formal, typically context-free, description of legal sentence structure).

Stage 1LexGroup characters into tokens; drop whitespace.
Stage 2ParseBuild an AST that captures grouping & precedence.
Stage 3EvaluateWalk the tree — or compile it to machine code.
Stage 4RunThe result executes on the machine.

A compiler translates the AST all the way down to machine code (or another lower-level language) ahead of time, producing a standalone executable. An interpreter walks the AST directly (or a simplified bytecode form of it) and executes it on the fly, redoing the translation work every run. Many modern languages blend both: a just-in-time (JIT) compiler interprets at first, then compiles the "hot" parts that run repeatedly into fast machine code mid-execution.

Compiled

  • Translated fully ahead of time
  • Produces a standalone executable
  • Fast to run, slower to build
  • e.g. C, Rust, Go

Interpreted

  • Walks the AST / bytecode each run
  • No separate build step
  • Flexible, slower per line
  • e.g. Python, Ruby, JS (often JIT)
Explanation

Why not skip straight from text to execution? Because each stage solves one clean problem. The lexer's only job is "what are the words?" The parser's only job is "how do the words nest, given the grammar's precedence rules?" — this is exactly why 2 + 3 * 4 parses as 2 + (3 * 4): multiplication binds tighter, so it sits deeper in the tree. Only once structure is unambiguous does evaluation or code generation even make sense. Separating these concerns is the same abstraction principle from every earlier chapter, applied to language itself.

Big idea. A parser doesn't just check that input is "grammatically legal" — it builds the very structure that gives the input meaning. Ambiguous grammar produces ambiguous trees, which is why precedence rules exist: they resolve which reading is intended before any evaluation happens.

2 + 3 * 4 + 2 * 3 4 * binds tighter → deeper
The AST for 2 + 3 * 4: multiplication sits deeper than addition, so it is evaluated first — precedence made structural.
Practical

We build a real, working interpreter for arithmetic expressions with + - * / and parentheses, respecting standard precedence — the same three-stage pipeline (lex, parse, evaluate) that real language implementations use, just for a tiny language.

import re

def tokenize(text):
    tokens = re.findall(r"\d+\.?\d*|[()+\-*/]", text)     # numbers, or one symbol
    return [t for t in tokens]

class Parser:
    """Recursive-descent parser: each grammar rule becomes one method.
       Precedence is expressed structurally: expr calls term calls factor."""
    def __init__(self, tokens):
        self.tokens = tokens
        self.pos = 0

    def peek(self):
        return self.tokens[self.pos] if self.pos < len(self.tokens) else None

    def eat(self):
        tok = self.tokens[self.pos]
        self.pos += 1
        return tok

    def parse_expr(self):                        # expr := term (('+'|'-') term)*
        node = self.parse_term()
        while self.peek() in ("+", "-"):
            op = self.eat()
            node = (op, node, self.parse_term())  # AST node: (operator, left, right)
        return node

    def parse_term(self):                         # term := factor (('*'|'/') factor)*
        node = self.parse_factor()
        while self.peek() in ("*", "/"):
            op = self.eat()
            node = (op, node, self.parse_factor())
        return node

    def parse_factor(self):                       # factor := NUMBER | '(' expr ')'
        tok = self.eat()
        if tok == "(":
            node = self.parse_expr()
            self.eat()                             # consume ')'
            return node
        return float(tok)                          # a leaf of the AST

def evaluate(node):
    if isinstance(node, float):
        return node                                 # leaf: just a number
    op, left, right = node
    l, r = evaluate(left), evaluate(right)          # recursively evaluate children first
    return {"+": l + r, "-": l - r, "*": l * r, "/": l / r}[op]

def run(text):
    ast = Parser(tokenize(text)).parse_expr()
    return evaluate(ast)

print(run("2 + 3 * 4"))         # 14.0  -- * binds tighter, per the grammar's structure
print(run("(2 + 3) * 4"))       # 20.0  -- parentheses override precedence
print(run("10 - 2 - 3"))        # 5.0   -- left-associative: (10 - 2) - 3

Because parse_expr calls parse_term which calls parse_factor, multiplication always ends up nested deeper in the tree than addition — precedence emerges purely from which method calls which, with no special-casing needed. evaluate then walks that tree bottom-up, exactly like the recursion in Chapter 12.

Q&A
Q1 What's the real difference between a compiler and an interpreter?

A compiler translates the whole program ahead of time into a lower-level target and produces a standalone artifact. An interpreter executes the AST or bytecode directly each run, redoing the translation work every time. A JIT compiler blends both: interpret first, then compile the frequently-run "hot" paths to native code mid-execution.

Q2 Why does parse_expr/parse_term/parse_factor enforce precedence with no "if operator == '*' do this first" logic?

Because deeper function calls correspond to tighter-binding operators — the recursive call structure itself is the precedence, once you design the grammar rules to nest in the right order. No special-case branching is needed; the structure encodes the rule automatically.

Q3 What happens if the tokenizer or parser encounters invalid input?

A production parser raises a syntax error citing the exact position and expected token; our minimal version would simply raise a Python exception like an IndexError. Producing clear, helpful error messages from invalid input is an entire subfield of language design in its own right.

Q4 Why do modern language runtimes (JavaScript engines, the JVM, PyPy) favour JIT compilation?

Pure interpreting is flexible but slow, since it redoes work every run. Pure compiling is fast but loses runtime information and flexibility. JIT starts by interpreting — fast startup, and it gathers real usage data — then compiles only the frequently-run "hot" code paths to native machine code, approaching compiled speed without sacrificing interpreter flexibility for the rest.

Concept mind map

How the ideas connect

Every key idea in this chapter, branching from the core concept — use it to see the whole picture at a glance.

source codelexer tokensparserASTevaluateprecedenceCompilers, interpreters
Infographic

The process, step by step

Step 1LexScan characters into tokens like numbers, operators, parentheses.
Step 2ParseBuild an AST that captures grouping and operator precedence.
Step 3EvaluateWalk the tree, computing each node from its children.
Step 4OutputReturn the result (interpreter) or emit target code (compiler).
Solved examples

Worked problems, step by step

Follow each solution line by line, then try to reproduce it on paper before moving on.

Example 1Evaluate 2 + 3 * 4 respecting precedence.

  1. * binds tighter than +.
  2. Compute 3 * 4 = 12 first.
  3. Then 2 + 12.

Example 2How does the AST group 2 + 3 * 4?

  1. The multiply is a subtree.
  2. The plus is the root with 2 and the multiply as children.
Practice problem set

Now you try

Work each one out first, then tap to reveal the worked answer.

1What does a lexer do?
It scans the raw source text and groups characters into tokens.
2What is an AST?
An abstract syntax tree: a tree form of the code that captures grouping and operator precedence.
3How does a compiler differ from an interpreter?
A compiler translates source into target code ahead of time; an interpreter evaluates it directly at run time.
4Why does the parser encode precedence?
So expressions like 2 + 3 * 4 evaluate with multiplication before addition, matching math rules.
5What is recursive-descent parsing?
A top-down technique with one function per grammar rule that calls itself for nested structures.
6Evaluate (2 + 3) * 4.
Parentheses group first: 2 + 3 = 5, then 5 * 4 = 20.