Names, Bindings, and Rooms
let x = 5 is a promise the machine has to keep — but only in here.
What you're wiring up
An environment is a map from names to values plus one pointer to the enclosing environment. Lookup walks outward, room by room, until it finds the name or runs out of rooms. That single linked-map idea is the entire theory of variable scope, and building it makes shadowing and undefined-variable errors obvious instead of mystical.
Statements arrive with it. A program stops being one expression and becomes a sequence: let statements, return statements, expression statements, and blocks that are statement lists of their own. Each block gets its own room, so an inner let shadows the outer binding and vanishes at the closing brace.
return needs to punch up through nested blocks. The trick is to wrap the value in a special return object that statement-list evaluation stops on and passes upward untouched, unwrapping only at the program or function boundary.
Assembly steps
hint
Resist a global environment variable. Passing env explicitly is exactly what makes closures nearly free next stage.
hint
Type let x = 1; if (true) { let x = 2; } x into the REPL. If it prints 1, your rooms work.
hint
The classic bug is unwrapping too early. if (true) { if (true) { return 10; } return 1; } must be 10 — it is a shipped golden.
Go deeper (after it passes)
Environment chains and the parser's token cursor are both state made explicit rather than hidden in flags — pair with HFDP M9 — The Gumball State Machine. Then read your own Get method again: that fall-through is what every scoping rule in every language you use reduces to.