Step 1 of 3 · Reading · ~3 min
Lexical Scoping and Environment Chains
Statements & State
One Environment Isn't Enough
A single flat Environment works fine for a program with no nested blocks, but real languages have lexical scope: a variable declared inside { ... } shouldn't leak out, and an inner scope should be able to temporarily shadow an outer variable of the same name without permanently overwriting it. The fix is to chain environments together, one per scope, each pointing to its enclosing scope.
The Environment Chain
Three operations, three different scope-walking behaviors:
defineonly ever touches the current environment — a newvar xinside a block always creates a fresh binding in that block's scope, never reaching outward. This is exactly what makes shadowing possible.getandassignboth walk outward through theenclosingchain until they find the name — but neither ever creates a new binding along the way. If the name isn't found anywhere in the chain, it's undefined, all the way up to the global scope.
Shadowing, Traced Through
var x = 1;
{
var x = 2;
print x; // 2 — inner `x` shadows outer `x`
}
print x; // 1 — outer `x` was never touched
When the block's var x = 2; runs, it calls define on the block's own environment — a brand-new dictionary entry, distinct from the outer scope's x. print x; inside the block calls get, which finds x in the current (innermost) environment first and stops there, never even looking at the enclosing one. Once the block ends and that inner environment is discarded, the outer x is untouched — it was shadowed, not overwritten.
Why This Matters for Assignment Too
Shadowing only ever applies to var (declaration). A plain assignment (x = 2;, no var) inside a block, where x was declared outside the block, does not create a new inner binding — assign walks up the chain and mutates the outer variable in place, because there's no local x to assign into. This is the precise difference between:
var x = 1;
{ var x = 2; } // shadowing: outer x still 1 afterward
{ x = 2; } // mutation: outer x is now 2
Both look similar at a glance, but only the first one creates a new binding — get this distinction wrong and shadowing tests will fail in confusing, hard-to-spot ways.
Undefined Variables Stay a Runtime Error
Looking up a name that doesn't exist anywhere in the environment chain (all the way to the global scope) is still a runtime error, exactly as in the previous lesson — the interpreter should catch it internally, stop evaluating that statement, and exit cleanly rather than crashing or printing a stack trace.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…