Skip to content

Step 1 of 3 · Reading · ~3 min

Compiling Statements and Managing State

Compiling to Bytecode

Statements Don't Produce Values

Where the last chapter compiled expressions (things that leave a value on the stack), this lesson compiles statementsprint x;, x;, and var x = 1;. Statements are compiled top to bottom, and the compiler's job is to make sure the stack is exactly as tall after a statement as it was before (statements are executed for effect, not for their value).

python

That trailing OP_POP matters: 1 + 2; as a statement still evaluates the expression (pushing 3), but since nothing uses the result, it must be popped or the stack grows forever across statements.

Globals: Late-Bound, Name-Keyed

A top-level var x = 1; compiles to: push the initializer's value, then OP_DEFINE_GLOBAL <name-constant>. At runtime the VM stores the value in a hash table keyed by the variable's name (itself a heap string). OP_GET_GLOBAL / OP_SET_GLOBAL look the name up by string comparison every time. This is slow (a table lookup per access) but simple, and it's the only option for globals — they can be declared after they're used in a REPL, so you can't resolve them to fixed slots at compile time.

Locals: Stack Slots, Not a Hash Table

Local variables are the interesting optimization. Since a local's lifetime is a well-defined region of the compiler's control flow — not something that can be redefined out of order — the compiler can track locals in a plain list and resolve every reference to a stack slot index at compile time. No runtime name lookup at all:

python

When the compiler sees var x = ...; inside a block, it doesn't emit OP_DEFINE_GLOBAL — it just appends a Local and leaves the value sitting on the stack. The stack slot is the variable; there's nothing else to do. Reading x later becomes:

python

OP_GET_LOCAL <slot> / OP_SET_LOCAL <slot> read/write the VM's stack array directly at frame_base + slot — an array index, not a hash lookup.

Scope Depth and Shadowing

Each Local remembers the scope_depth it was declared at. Leaving a block (end_scope) pops every local whose depth exceeds the new depth — that's the while loop above, and it's also what makes { var x = 1; } followed by more code correctly not see x anymore. Redeclaring the same name within the same scope should be a compile error; redeclaring it in a nested scope is shadowing and is fine — resolve_local naturally finds the innermost one first because it searches from the end of the list backward.

What to build

Add print and bare-expression statement compilation, a var declaration that branches between global and local depending on scope_depth, block statements with { }, and local-slot resolution with shadowing. Test that { var a = 1; { var a = 2; print a; } print a; } prints 2 then 1, and that popping locals at end_scope actually removes them from the stack (check OP_POP counts against block nesting depth).

Up nextCompiling Control Flow (Jumps)Compiling to Bytecode

Discussion

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

Sign in to post a comment or reply.

Loading…