v3 Bytecode: Closures and Indexing

Work through examples, try the reasoning yourself, then put it into practice.

View Slides

Before 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 read

v3 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.

Join Discord