To get correct lexical scoping and closures in an interpreter, all you need to do is follow these rules:
- In your interpreter, variables are always looked up in an environment table passed in by the caller/kept as a variable, not some global env-stack. That is,
eval(expression, env) => value
.
- When interpreted code calls a function, the environment is NOT passed to that function.
apply(function, arguments) => value
.
- When an interpreted function is called, the environment its body is evaluated in is the environment in which the function definition was made, and has nothing whatsoever to do with the caller. So if you have a local function, then it is a closure, that is, a data structure containing fields
{function definition, env-at-definition-time}
.
To expand on that last bit in Python-ish syntax:
x = 1
return lambda y: x + y
gets executed as if it were
x = 1
return makeClosure(<AST for "lambda y: x + y">, {"x": x})
where the second dict argument may be just the current-env rather than a data structure constructed at that time. (On the other hand, retaining the entire env rather than just the closed-over variables can cause memory leaks.)