Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

About This Book

This is an introduction to the concepts used to design programming languages — paradigms, scope, binding, types, evaluation — taught not by touring a handful of languages but by building one. Over the course of the book you implement an interpreter, in Rust, for a single language that grows from a calculator into a small multi-paradigm language named Bridger, with imperative, functional, and declarative features.

It grew out of an undergraduate programming-languages course, but it is written to stand on its own. If you read the chapters and build the interpreter as you go, you will learn the material and end up with a working language.

Why an interpreter?

There are two ways to give a programming language a precise meaning, and they lead to two different kinds of book.

  • A compiler foregrounds translation: it turns your program into another language (machine code, bytecode, a lower-level IR) and cares deeply about code generation, optimization, and the constraints of the target machine.
  • An interpreter foregrounds meaning: it says directly what a program is and does. You write down a rule like (“expression evaluates to value ”), and the evaluator you build is that rule, made executable.

This book takes the semantics lens. That single choice explains most of what follows — including why we hand you a parser instead of making you write one: parsing is a translation problem that belongs to the compiler’s world, and a course on it already exists in most curricula. Here we start from the abstract syntax tree and spend our time on what programs mean. Code generation, optimization, and register allocation are real and important; they are simply a different book, and we point you to good ones where the boundary comes up.

Who this book is for

Anyone who wants to learn how programming languages work, with a practical flair. This text is designed to be self-contained: the concepts, the specifications, and the build are all here, and the scaffolding you build on top of is publicly available. It assumes you can program in some language and are curious about what is going on underneath — not that you already know Rust, type theory, or logic programming.

How the book is organized

The chapters follow the arc of the language as it grows, and each part from Part II onward closes with a project milestone (M1–M8). Treat the milestones as a learning structure: a checkpoint that says “build this much next, and here is how to know it works.” By the end you will have built:

  • a tree-walking evaluator with clear runtime errors,
  • lexical scope, and the closures that make it observable,
  • first-class functions and closures,
  • a static type checker that rules out whole classes of runtime errors before a program runs,
  • a relational (Datalog-style) sublanguage you can query from ordinary code,
  • algebraic data types with pattern matching, and
  • objects built as structs with methods and traits.

Every chapter follows a similar narrative. Each chapter will broadly introduce the relevant programming-language concepts: what they are meant to capture, and the history of how they emerged and changed over time and across communities. It will then discuss how those concepts shape the design decisions underlying Bridger. Then the lens widens to the design space: how ML, Rust, Prolog, or Python answer the same questions, and the pros and cons of each design. Some of those questions Bridger answers by declining — negation in logic rules, full type inference, classical inheritance — and those forks get the same treatment, with pointers for reading further.

The starter repository

Because the book is meant to be self-contained, the scaffolding you build on top of ships publicly: the Value type, the environment structure, error and source-span types, the AST, and the parser. You implement semantics — the evaluator, the type checker, unification — not the memory plumbing that would otherwise have you fighting the borrow checker instead of learning what a closure is. The starter is the bridger-interpreter repository; its README covers setup, and each milestone says what to build next.

How to read this book

  • Code you implement is shown in Rust. Where a snippet is self-contained you can press the play button to run it.

    fn main() {
        println!("Hello from the metalanguage.");
    }

    You do not need to know Rust first. Each construct is introduced where the interpreter first needs it (enum/match at the AST, Rc<RefCell> at closures, Result/Option at error handling), as a short aside you can skip if you’re already familiar with Rust. For those looking for a slightly more in-depth introduction, Appendix A is a self-contained tutorial with an M0 warm-up milestone.

  • Programs in Bridger are shown in monospace:

    fn fib(n: Int) -> Int =
        if n < 2 { n } else { fib(n - 1) + fib(n - 2) };
    
    fn main() { println(fib(10)); }
    
  • Specifications are written as inference rules. For example, addition evaluates its operands and adds the results:

    The notation reference collects the symbols and rule shapes we use.

A note on tools and AI assistants

Syntax is the part that varies. Every language spells the same idea differently, and the spelling changes with every release — which is exactly the kind of detail a coding assistant handles well. It handles semantics well too: ask one to explain closures or type inference and you will usually get a fluent and largely correct answer. The limit is not that these tools only know syntax.

The limit is accountability. An assistant can make a design decision; it cannot be answerable for one. Whether a problem wants a relation or a fold, what a representation choice will cost three modules later, which trade-off a language made and whether it was the right one for your system — those are judgments you own, and owning them means understanding the concepts well enough to evaluate an answer rather than accept it.

The test arrives when someone asks why a system is built the way it is. “The assistant did that” reports only that nobody decided; the engineer who can explain the reasoning understood the choice, whether or not an assistant helped reach it.

That is what this book is for: the concepts, where they came from, and the pros and cons that make one design fit where another fights. It is what you need in order to design and assess systems — including the ones you build with an assistant’s help.