v3 Bytecode: Closures and Indexing
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Use v3 callee/target AST fields and lexical binding rules.
What you will practice
- Derive stack operands for closures, collections, and indexing.
- Separate function IDs from slice positions and runtime closure instances.
- Preserve source evaluation order through reverse stack consumption.
In this lesson
Lesson
3 min readv3 Bytecode: Closures and Indexing
The v3 compiler returns Bytecode with Main and a function table. Each function
declaration gets a unique numeric ID, but a closure instance is created later
when the VM executes MAKE_CLOSURE id. Two calls to a factory can create two
closures sharing code ID but capturing different environments.
Worked reconstruction
For let a=[4,5]; a[1]=9; let x=a[1];:
PUSH_NUM 4
PUSH_NUM 5
MAKE_ARRAY 2
DEFINE_VAR a
LOAD a
PUSH_NUM 1
PUSH_NUM 9
SET_INDEX
LOAD a
PUSH_NUM 1
GET_INDEX
DEFINE_VAR x
MAKE_ARRAY consumes two values and pushes one object while preserving element order. SET_INDEX consumes value 9, index 1, then the array target; it mutates the object and pushes nothing. GET_INDEX consumes target/index and pushes the element. Both statements restore the original stack height.
Closure call protocol
f(2,3) compiles as LOAD f; PUSH_NUM 2; PUSH_NUM 3; CALL 2. CALL consumes
the arguments and the callee value, unlike BC1's CALL f 2. A declaration
emits MAKE_CLOSURE id; DEFINE_CONST name in its surrounding code and puts its
body in a separate Function. Creating a closure does not execute that body.
Reserve an ID before recursively compiling nested functions. The inner body may be appended to the function slice before the outer body completes: an ID is a handle, not necessarily its slice index. Keep IDs unique and deterministic; the VM resolves by ID.
Map order and branch invariants
A map with N pairs pushes key then value for each pair in source order and emits MAKE_MAP N. The VM pops pairs in reverse order, so it must preserve the last source pair when keys repeat. The count N is pairs, not operand count 2N.
Jumps remain absolute within their own code slice. Function bodies have no extra block wrapper around their parameter scope; nested explicit blocks do. Append an implicit nil return when needed, while expression statements still emit POP. The compiler assumes analysis has accepted the AST.
Debugging lab
A code generator emits SET_INDEX and then POP for the assignment a[0]=2;. Another uses a function’s slice position as its ID after compiling a nested declaration. Diagnose both failures.
Compare your diagnosis after attempting the lab
SET_INDEX leaves no value, so POP removes an unrelated operand or underflows. Reserved function IDs can differ from append order when nested bodies finish first; reference the explicit ID, not a guessed index.
Transfer task
Lower make()[idx()] = value(); and annotate the evaluation order without executing the calls. Then compile a function containing one nested function and compare reserved IDs with function-table order.
Carry forward
Carry exact stack contracts into the v3 VM. Save a container assignment, a closure call, and a nested-function ID example for BC2 round-trip tests.
Build compile-bytecode-v3. Keep a before/after trace of one failing test. Use the checkpoint to diagnose specific skills; passing it does not replace implementing and testing the artifact.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.