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
hint
Registering ( as an infix operator is the trick that makes makeAdder(2)(3) parse with zero extra code.
hint
Extend the function's birth environment, never the caller's.
hint
Unwrap the return wrapper at this boundary, not deeper inside.
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.