Step 1 of 3 · Reading · ~2 min
Blocks — Creating New Scopes
Statements & State
Blocks: Where Scopes Actually Come From
The previous lesson assumed you already had scopes to chain — this lesson is where scopes are actually created, by the block statement { ... }. A block is nothing more than "run these statements in a fresh child environment, then discard that environment when the block ends."
Grammar and Parsing
statement → exprStmt | printStmt | block
block → "{" declaration* "}"
A block contains a list of declarations (not just statements) — this is important, because it's what allows var inside a block:
And a new AST node that just wraps a list of statements:
Executing a Block: Swap the Environment, Run, Restore
The interesting part isn't parsing — it's execution. Running a block means: create a new child Environment (whose enclosing is the current environment), execute every statement inside it using that new environment, and then — critically — restore the original environment afterward, regardless of how execution finished:
The try/finally is not optional. Once you add functions and early return, control flow can exit a block abruptly partway through — without the finally, an exception or early return inside a nested block would leave the interpreter's "current environment" pointer stuck on a scope that should have already been discarded, corrupting every variable lookup for the rest of the program.
Nesting: Each Block Gets Its Own Fresh Environment
{
var x = 1;
{
var y = 2;
print x + y; // 3 — inner block's environment can see outer x via the chain
}
// y is not visible here — that environment is gone
}
Every { } you enter — even nested directly inside another block — gets its own new Environment whose parent is whatever environment was active when that block started executing. This is what makes y from the inner block invisible outside it: once execute_block restores the outer environment, the inner one (and everything defined in it) simply has no remaining reference and is unreachable.
The Common Bug: Reusing One Environment
A frequent mistake is calling execute() on the block's statements without swapping in a new environment — i.e., treating a block as "just run these statements in the current scope." This silently breaks shadowing (nested var x would overwrite the outer x instead of creating a new binding) and means variables declared in a block would incorrectly remain visible after the block ends. Always construct a genuinely new Environment(enclosing=current) per block execution, never reuse or skip it.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…