Building jlox - A Tree-Walk Interpreter in Java

Worked through Part I of Robert Nystrom's Crafting Interpreters, building jlox by hand in Java with IntelliSense disabled — every line typed, every mechanism traced until it stopped being mysterious.
The Pipeline
A Scanner converts raw source text into tokens using a two-pointer approach and maximal munch. A Parser uses recursive descent to build an abstract syntax tree, with operator precedence encoded directly into the grammar's rule hierarchy — lower-precedence rules delegating to higher-precedence ones, so the tree's shape enforces evaluation order structurally rather than through any explicit precedence-checking logic.
The Visitor Pattern
The Interpreter walks that tree using the Visitor pattern — a dispatch mechanism where each node type routes to the correct evaluation method through two stages: runtime polymorphism picks which accept() runs, and that method makes a fixed compile-time call to the right visit method on the Interpreter. The same pattern drives three completely separate tree walks: the Interpreter evaluates, the AstPrinter prints structure, and the Resolver performs static analysis.
Environments and Scope
Variables live in a parent-chained Environment structure — a linked list of hash maps where each scope points outward to its enclosing one. Lookup walks outward until the variable is found or the chain ends. Block scope is managed by executeBlock, which saves the current environment, switches to a new inner one, and restores the original in a finally block — guaranteeing cleanup even on errors.
Functions and Closures
Functions are first-class values. Each function declaration creates a LoxFunction object capturing the environment active at definition time as its closure. Every call creates a fresh environment whose parent is that captured closure — not the current environment. This single distinction is the entire closure mechanism:
new Environment(closure) // function — parent is defining scope
new Environment(environment) // block — parent is current scope
The Return statement unwinds the call stack via a Java exception, caught only in LoxFunction.call() — the same exception-as-control-flow trick used for panic-mode error recovery in the Parser.
The Resolver
The Resolver exists because closures alone are not enough. Capturing an environment object is insufficient when that object is mutable — a variable declared after a function definition can contaminate the function's closure at runtime, causing it to see a binding that did not exist when the function was written.
The Resolver fixes this by walking the AST once before interpretation, computing for every variable reference exactly how many scope hops up it lives, and storing those distances in a hash map. The Interpreter then jumps directly to the right scope instead of walking dynamically — making later mutations to intermediate scopes structurally invisible.
What's Next
jlox is complete and correct. The same language gets reimplemented next as clox — a bytecode virtual machine written in C, where everything Java provided for free (garbage collection, object identity, dynamic arrays, hash tables) gets built from scratch, and the AST disappears entirely in favor of flat bytecode executed by a stack-based VM.