Skip to content

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:

python

And a new AST node that just wraps a list of statements:

python

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:

python

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.

Up nextIf/Else StatementsControl Flow & Functions

Discussion

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

Sign in to post a comment or reply.

Loading…