builds / interpreter / stage-5SHEET 5 / 6 · REV ASIGN IN
ASSEMBLY DIAGRAM — YOUR INTERPRETERSCALE: LEARNING
source filethe outside worldSCANNER✓ builtPARSER✓ builtEVALUATOR✓ builtENV ROOMS✓ builtCLOSURE CELLS⚙ buildingDIAG PANELstage 6
BUILTUNDER CONSTRUCTIONNOT YET IMAGINED INTO EXISTENCE
STAGE 5 · THE CLOSURE CELLS

Functions That Remember Where They Were Born

A function keeps a pointer to the room it was defined in, not the room it was called from.

What you're wiring up

In blu, functions are values. let add = fn(a, b) { a + b; }; stores a function in a variable exactly like an integer. That alone makes the machine a real programming language, and it costs almost nothing now that expressions and environments exist.

The deep move is the closure. A function object captures the environment where it was defined, so makeAdder(2) can return a function that remembers 2 forever. Calling a function means: make a new room whose outer pointer is the function's birth room, bind the arguments, evaluate the body. That one sentence is the whole implementation.

Recursion, higher-order functions, and composition all fall out of it for free — and the acceptance spec makes you prove each one. If you extend the caller's environment instead of the birth environment you get dynamic scope, and every closure test fails at once.

Assembly steps

[ 01 ]
Extend the parser and AST with fn literals and call expressions, slotting calls into the Pratt table as an infix operator on the opening parenthesis at the highest precedence.
hint

Registering ( as an infix operator is the trick that makes makeAdder(2)(3) parse with zero extra code.

[ 02 ]
Define the Function object: parameters, body, and the environment captured when the literal was evaluated.
[ 03 ]
Implement calls: evaluate the callee, evaluate arguments left to right, then evaluate the body in a new room enclosed by the function's captured environment with parameters bound.
hint

Extend the function's birth environment, never the caller's.

hint

Unwrap the return wrapper at this boundary, not deeper inside.

[ 04 ]
Reject wrong argument counts with an arity error instead of binding whatever showed up.
[ 05 ]
Verify the classics in your own REPL before running the harness: recursive fib, a counter closure that increments captured state, and a function passed as an argument.
hint

If recursion cannot find its own name, check that lookup happens at call time rather than definition time — the let binds before the body ever runs.

Go deeper (after it passes)

This is where the track pays off: read your call implementation and notice that closures were free once environments were explicit. If you want the formal treatment, the Monkey and Lox lineages both cover it, and the next stage is where the language stops being a toy.