Scoped Environments
The bug is clear: our evaluator has a single flat environment. Every function call writes into the same set of bindings, so make-adder(5) overwrites the x that make-adder(3) set.
The fix: give each function call its own scope — a private set of bindings that doesn’t interfere with any other call.
The key insight is lexical scoping: a function should remember the environment where it was defined, not where it’s called. When make-adder is called with 3, the inner lambda should remember that x = 3, no matter what happens later.
The match library now provides three new functions:
getEnv()— returns the current environmentsetEnv(env)— switches to a different environmentcreateEnv(parent)— creates a new scope that extends a parent environment
The set and get functions still work on the current environment, but get now walks up the parent chain to find names defined in outer scopes.
What to Change
Lambda: capture the current environment so the function remembers where it was defined.
({ param: expr[1], body: expr[2], env: ... })
Call: create a new scope extending the lambda’s captured environment, not the caller’s environment.
- Evaluate
fnExprto get the function - Evaluate
argExprin the caller’s scope (before switching!) - Save the current environment
- Create a new scope from
fn.env(the lambda’s captured environment) - Bind the parameter to the already-evaluated argument
- Evaluate the body
- Restore the saved environment, return the result
WARNING
The argument must be evaluated before switching to the new scope. Otherwise, when you call ["add3", 10], the 10 would be evaluated inside add3’s scope instead of the caller’s.
NOTE
Native functions don’t need scoping — they capture values in JavaScript closures. Only handle scoping for lambdas.