Skip to content

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

python

Three operations, three different scope-walking behaviors:

  • define only ever touches the current environment — a new var x inside a block always creates a fresh binding in that block's scope, never reaching outward. This is exactly what makes shadowing possible.
  • get and assign both walk outward through the enclosing chain 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.

Up nextBlock Statements & NestingStatements & State

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…