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.

class Task { init(description, priority) { this.description = description; this.priority = priority; } describe() { return this.description; } } class UrgentTask < Task { // Chapter 8: single inheritance, method override describe() { return "URGENT: " + this.description; } } class TaskNode { // a linked-list node, built from an ordinary class init(task, next) { this.task = task; this.next = next; } } class TaskQueue { init() { this.head = nil; this.count = 0; } add(task) { this.head = TaskNode(task, this.head); // prepend this.count = this.count + 1; } forEach(callback) { // Chapter 7: functions are ordinary values var node = this.head; while (node != nil) { // Chapter 6: control flow callback(node.task); node = node.next; } } }

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

fun makeUrgencyChecker(threshold) { // the exact shape of Ch.7's own makeAdder fun isUrgent(task) { return task.priority >= threshold; // 'threshold' is CAPTURED, not a parameter } return isUrgent; } fun countMatching(queue, checker) { var count = 0; fun tally(task) { if (checker(task)) { // calling a function PASSED IN as a value count = count + 1; // mutating a captured local -- Ch.5 + Ch.7's own mechanism } } queue.forEach(tally); return count; }

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

var queue = TaskQueue(); queue.add(Task("Water the plants", 2)); queue.add(UrgentTask("Submit tax filing", 9)); queue.add(Task("Read a book", 1)); queue.add(UrgentTask("Fix the leak", 8)); print "All tasks:"; print queue.count; fun printTask(task) { print " - " + task.describe(); } queue.forEach(printTask); var isCritical = makeUrgencyChecker(8); print "Critical tasks:"; print countMatching(queue, isCritical); var isLowPriority = makeUrgencyChecker(2); print "Priority >= 2:"; print countMatching(queue, isLowPriority);
Verified directly — the complete program, run end to end through the real interpreter
All tasks:
4
  - URGENT: Fix the leak
  - Read a book
  - URGENT: Submit tax filing
  - Water the plants
Critical tasks:
2
Priority >= 2:
3
Every 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.

queue.forEach(nil);
Verified directly — a correctly-attributed, two-frame stack trace
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 componentChapter
Tokenizing the whole program, with real line numbersChapter 1 (lexer) + Chapter 9 (line tracking)
Parsing classes, functions, control flow, and expressions into one ASTChapter 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 interpreterChapter 3
Statements, truthiness, string/number/boolean literalsChapter 4
var, TaskQueue's own fields, the live Environment chainChapter 5
The while loop walking the linked listChapter 6
Functions as values, makeUrgencyChecker's own closuresChapter 7
Task/UrgentTask/TaskNode/TaskQueue, inheritance, thisChapter 8
The line-numbered error and correctly-attributed stack traceChapter 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 Environment chain (a naive copy-based version verified broken, then fixed)
  • Ch.6: if/while/for (desugared), short-circuit Logical (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, this bound fresh per access (a real over-mutation bug verified and fixed), single inheritance via find_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