Capstone: A Complete Tree-Walking Interpreter for Wisp
Writing a Compiler/Interpreter: Fundamentals
Chapter 10 · Capstone: A Complete Tree-Walking Interpreter for Wisp
Nine chapters, each adding one working piece: a lexer, a parser, a tree of typed nodes, a tree-walking evaluator, an environment chain, control flow, functions and closures, classes and inheritance, and a real error-reporting layer with line numbers and stack traces. Nothing from any of those chapters gets replaced here — this capstone is one continuous, moderately complex Wisp program, run through the exact interpreter those nine chapters built, exercising closures, classes, and control flow together in a single working system rather than in nine separate, disconnected demonstrations.
The Program: A Task Queue, Built Without Arrays
Wisp never gained a native array or list type across this course — so the capstone's own data structure is a linked list, built entirely out of classes, the same way a real language without a builtin collection type would have to. This is deliberate: it's a genuine, useful demonstration of what classes alone can build, not a workaround apologized for.
UrgentTask overrides describe() exactly the way Chapter 8's own Dog < Animal example did. TaskQueue.forEach takes a function as its own argument — callback is called like any other value, because Chapter 7 established that functions in Wisp are ordinary, first-class values, not a special kind of name.
A Closure Factory, Reused Directly From Chapter 7
tally is a closure over two different things at once: checker, a parameter of countMatching, and count, a local variable it mutates on every matching task. It's also passed into queue.forEach as a plain value — three chapters' worth of machinery (Chapter 5's live environment chain, Chapter 7's closures, and Chapter 7's first-class functions) cooperating in four lines, with nothing here that any single earlier chapter didn't already establish on its own.
Running It
All tasks: 4 - URGENT: Fix the leak - Read a book - URGENT: Submit tax filing - Water the plants Critical tasks: 2 Priority >= 2: 3Every task prints in reverse of its own insertion order, because
add() prepends — the linked list's own head always points at whichever task was added most recently, so walking it in forEach naturally visits "Fix the leak" (added last) first. Critical tasks correctly counts the two tasks with priority 8 or higher; Priority >= 2 correctly counts three, excluding only "Read a book" at priority 1. Two calls to makeUrgencyChecker with two different thresholds produced two genuinely independent closures — isCritical and isLowPriority — the same guarantee Chapter 7 verified with addFive/addTen, reused here for a real purpose instead of a synthetic example.
Breaking It on Purpose
A capstone that only ever shows a program succeeding doesn't exercise Chapter 9's own contribution at all. Appending one deliberately broken line — passing nil where forEach expects a callable — triggers the real error-reporting pipeline this course spent an entire chapter building.
RUNTIME ERROR: [line 37] WispRuntimeError: can only call functions and classes at forEach() (called from line 82) at <script>Line 37 is
callback(node.task); — inside TaskQueue.forEach's own body, exactly where nil was actually called. Line 82 is queue.forEach(nil); — the real call site, one frame further out. Chapter 9's own call_stack mechanism gets this exactly right on a program it has never seen before, built entirely out of pieces (a class method, a first-class function argument, a runtime type check) that were each verified independently, in isolation, back in the chapters that introduced them.
Where Each Piece Came From
| Capstone component | Chapter |
|---|---|
| Tokenizing the whole program, with real line numbers | Chapter 1 (lexer) + Chapter 9 (line tracking) |
| Parsing classes, functions, control flow, and expressions into one AST | Chapter 2 (recursive descent), Chapter 6 (control flow grammar), Chapter 7 (call/function grammar), Chapter 8 (class grammar) |
Typed nodes with accept(), walked by a Visitor-based interpreter | Chapter 3 |
| Statements, truthiness, string/number/boolean literals | Chapter 4 |
var, TaskQueue's own fields, the live Environment chain | Chapter 5 |
The while loop walking the linked list | Chapter 6 |
Functions as values, makeUrgencyChecker's own closures | Chapter 7 |
Task/UrgentTask/TaskNode/TaskQueue, inheritance, this | Chapter 8 |
| The line-numbered error and correctly-attributed stack trace | Chapter 9 |
What This Course Doesn't Cover
Wisp, as built across these ten chapters, has no array or list type (the capstone's own linked list is the workaround), no string formatting or number-to-string conversion (every print in this chapter avoided concatenating a number into a string, because Chapter 4's own + operator deliberately refuses to), no super.method() syntax for calling an overridden method's own parent implementation, no break/continue, no static methods or class-level fields, and no standard library beyond print. None of these are oversights discovered too late to fix — they're honest scope boundaries, each traceable to a specific chapter that could have covered them but chose a narrower, more teachable slice instead.
Where This Connects
Every one of these nine chapters' own components — the lexer, the AST, the Visitor pattern, environments, closures, classes, error handling — carries forward unchanged into Course 2: Writing a Compiler/Interpreter: Advanced. That course doesn't discard this interpreter; it rebuilds Wisp's own runtime as a bytecode compiler and a stack-based virtual machine, closing with a direct, measured performance comparison against the exact tree-walking interpreter finished here. The language stays the same. How it runs changes completely.
Course 1 Complete — Fundamentals Quick Reference
- Ch.1-2: lexer (maximal munch, line tracking) → recursive descent parser (precedence, associativity)
- Ch.3: typed AST nodes + the Visitor pattern (13 duplicated isinstance checks vs. 4
accept()methods, verified) - Ch.4-5: statements, truthiness, a live
Environmentchain (a naive copy-based version verified broken, then fixed) - Ch.6:
if/while/for(desugared), short-circuitLogical(verified avoiding a real crash) - Ch.7:
WispFunction, closures (a real over-capture bug verified and fixed), recursion bounded by Python's own stack (measured: 163 levels at the default limit) - Ch.8:
WispClass/WispInstance,thisbound fresh per access (a real over-mutation bug verified and fixed), single inheritance viafind_method() - Ch.9: a unified error hierarchy, real line numbers, and a genuine multi-frame Wisp stack trace
- Ch.10: everything above, assembled into one working program — verified correct, then verified failing correctly on purpose
- Next: Writing a Compiler/Interpreter: Advanced — the same language, a bytecode VM instead of a tree walk