41. Closures in Drip
One field on a function value, and one line in its call, give Drip closures. The surprise is what that does to environments: they stop dying when their call ends.
- Explain which environment a Drip function stores, and why it is the one current when the function is declared.
- Trace a call whose new environment hangs off the function’s closure, and predict what it can see.
- Explain why an environment can outlive the call that made it, and what keeps it alive.
- Predict and fix closures that share one loop variable in Drip.
A counter, in Drip
In chapter 6 you wrote makeCounter in JavaScript. Here it is in Drip:
fn makeCounter() {
let n = 0;
fn count() {
n = n + 1;
return n;
}
return count;
}
let a = makeCounter();
print a(); // 1
print a(); // 2
Drip functions are values, so makeCounter can hand count back.
Then a() runs count’s body, which uses n. But n was a local of
a call that has already returned. Where does the interpreter find it?
Everything hangs on one decision. When a call starts, it gets a fresh environment for its parameters. Every environment has a parent. Which environment should be the parent of a call’s environment?
Three possible parents
A function is like a letter written in a particular house. When you read it later, the words “the kettle” mean the kettle in that house, the one it was written in. Not the house of whoever happens to open it, and not some town hall everyone shares.
There are three candidates:
- The globals. Simple, but then
countcan’t seenat all. Looking upnsearches the call’s environment, then the globals, and fails withUndefined variable 'n'. - The caller’s environment. Then what a name means depends on who
calls the function. Chapter 6’s
show/runexample would printinner. That rule has a name, dynamic scope, and Drip doesn’t use it. - The environment the function was declared in. That is lexical scope, the rule JavaScript follows[2], and the rule Drip follows too.
Option 3 has a cost: the function must remember where it was declared.
That’s the closure field chapter 39 gave DripFunction and promised to
explain.
The closure field
You’ve seen both halves of it in chapter 39. Look at them again with
closures in mind. When the interpreter runs a fn declaration, it
stores the current environment in the new function value:
case 'function':
this.env.define(s.name.lexeme,
new DripFunction(s, this.env));
return;
And when the function is called, the new environment hangs off that stored one, not off whatever environment the caller is in:
class DripFunction {
constructor(
readonly decl: FunctionDecl,
readonly closure: Environment,
) {}
call(interp, args: Value[]): Value {
const env = new Environment(
this.closure,
);
this.decl.params.forEach((p, i) =>
env.define(p.lexeme, args[i]),
);
return interp.executeFunctionBody(
this.decl, env,
);
}
}
That’s the whole feature: one field, filled in at declaration, and used
as the parent at every call. Inside makeCounter, the current
environment is the call’s own, so that is what count keeps. Step
through it:
fn makeCounter() {let n = 0;fn count() {n = n + 1;return n;}return count;}let a = makeCounter();print a();print a();
A captured environment is the environment a function value stores
when it is created: the one that was current when its declaration ran.
In the companion code it is the closure field of DripFunction. Every
call of the function makes a new environment whose parent is the captured
one, so the body sees the names around where it was written. The
function plus its captured environment is a closure.
Environments that outlive their call
Look again at step 5. makeCounter has returned. So have the
interpreter’s own TypeScript calls that ran its body, like
executeFunctionBody, and their frames on the host stack (chapter 40)
are gone. Yet the environment with n in it is still there.
That’s because an Environment is not in that stack frame. It is an
ordinary object, made with new, so it lives on the heap (chapter 17).
The frame only held a reference to it. A heap object stays alive as long
as something still refers to it, and count.closure does. When nothing
refers to it any more (say a is reassigned), the host language’s
garbage collector is free to reclaim it.
Environment lifetime is how long an environment exists. In a tree-walking interpreter, environments are heap objects, so an environment lives as long as anything references it: the running call, a child environment, or a function’s captured environment. It can therefore outlive the call that created it.
Two functions made in the same call share one captured environment, and so they share its variables, live:
let get;
let set;
fn makeBox(v) {
fn g() { return v; }
fn s(x) { v = x; }
get = g;
set = s;
}
makeBox("old");
print get();
set("new");
print get();Closures in a for loop
In chapter 6, each pass of a JavaScript for (let i ...) loop got its
own i. Drip is different, and you can see why from chapter 38: a Drip
for is desugared into a block that declares i once, wrapped around a
while loop.
let f1;
let f2;
for (let i = 1; i <= 2; i = i + 1) {
fn show() { print i; }
if (i == 1) f1 = show;
else f2 = show;
}
f1(); // 3
f2(); // 3
The loop body is a block, so each pass gets a new body environment. But
i isn’t in it. It lives one level out, in the environment of the
desugared block, and there is only one of those. To give each function
its own value, make a call per pass, because every call makes a new
environment:
fn capture(v) {
fn show() { print v; }
return show;
}
Your interpreter gets closures almost for free because it keeps every
environment on the heap and lets the host’s garbage collector decide
when one is dead. The price is that every call allocates a new
Environment with its own Map, even for a function that never makes a
closure. You’ll measure what that costs in chapter 44.
The bytecode VM in Part IV makes the opposite choice. It keeps locals in a fast stack, which is exactly where a variable can’t survive its call. So it has to spot variables that closures capture and move them off the stack when the call returns. The idea is the same as here; only the bookkeeping changes. Drip, and this two-interpreter plan, follow the Lox language from Crafting Interpreters[1].
Your turn
This Drip program saves three functions from a loop, but every one prints 4. Change it so that f1(), f2() and f3() print 1, 2 and 3. Keep the loop and the three calls at the end.
let f1;
let f2;
let f3;
for (let i = 1; i <= 3; i = i + 1) {
fn show() { print i; }
if (i == 1) f1 = show;
if (i == 2) f2 = show;
if (i == 3) f3 = show;
}
f1();
f2();
f3();A tiny model of the interpreter in plain JavaScript. Env is given. A function value is { params, body, closure }, where body(env) computes the result. Write callFn(f, args): make a new Env whose parent is f.closure, define each parameter in it, and return f.body(env).
class Env {
constructor(parent) {
this.vars = new Map();
this.parent = parent;
}
get(name) {
if (this.vars.has(name)) {
return this.vars.get(name);
}
if (this.parent) {
return this.parent.get(name);
}
throw new Error('no ' + name);
}
}
function callFn(f, args) {
// your code here
}What you learned
- A Drip function value stores its captured environment: the one current when its declaration ran.
- Each call makes a new environment whose parent is that captured environment, not the globals and not the caller’s. That is lexical scope.
- Environments are heap objects, so their lifetime is “as long as something refers to them”. A closure can keep one alive long after its call has returned.
- Closures made in one call share variables, live.
- A Drip
fordeclares its variable once, so closures made in the loop share it. A call per pass gives each its own.
Next: a bug hiding in this design, and a whole extra pass over the tree that fixes it.
Check your understanding
0/6- 1.
When does a Drip function capture its environment?
- 2.
What does this print?
let x = "outer"; fn show() { print x; } fn run() { let x = "inner"; show(); } run(); - 3.
What keeps
makeCounter’s call environment alive after the call returns? - 4.
A function made inside a call is called. What is the parent of its new call environment?
- 5.
Three functions are made in one Drip
for (let i = 1; i <= 3; i = i + 1)loop and each printsi. Called after the loop, what do they print? - 6.
In the tree-walker, which of these is true of environments?
References
- [1] Robert Nystrom (2021). Crafting Interpreters. Free web edition at craftinginterpreters.com (© 2015–2021); source, text and test suite at github.com/munificent/craftinginterpreters. github.com/munificent/craftinginterpreters (checked 2026-10-31)
- [2] MDN contributors. Closures (JavaScript Guide). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures (checked 2026-10-31)