A closure's defining property: it captures the environment where it was DEFINED, not wherever it happens to be CALLED from. This exercise builds a tiny expression interpreter and asks you to implement exactly that rule for function calls -- get it wrong and a function's free variables end up depending on the caller instead of staying fixed at definition time.
Node shapes:
("num", value)-- an integer literal("var", name)-- look upnamein the current environment("add", left, right)--left + right("lambda", param, body)-- a one-argument function literal("call", func_expr, arg_expr)-- call a function with one argument
"num"/"var"/"add"/"lambda" are done for you (note that evaluating a
"lambda" captures the CURRENT env into a Closure right there). Implement
the "call" case in eval_expr(node, env):
- Evaluate
func_exprin the currentenvto get aClosure. - Evaluate
arg_exprin the current (calling)env. - Evaluate the closure's
bodyin a NEW environment built from the closure's CAPTUREDenv(closure.env, not the calling env) plus the argument bound toclosure.param.
Example
# (lambda x: x + 1)(5)
ast = ("call", ("lambda", "x", ("add", ("var", "x"), ("num", 1))), ("num", 5))
eval_expr(ast, {}) # -> 6
Constraints
- The property that matters: a closure must see the variables it captured at definition time, even when called from an environment where those same variable names mean something different.
Real-world grounding
Every language with first-class functions and lexical scoping (Python,
JavaScript, Rust closures, Go's function literals) implements this exact
rule -- a closure carries its defining environment along with it. CPython's
version of this uses "cell" objects to share captured variables between the
defining scope and the closure: Objects/funcobject.c / Objects/cellobject.c