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 statements — print 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).
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:
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:
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).
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…