v3 Bytecode: Closures and Indexing

Lesson, slides, and applied problem sets.

View Slides

Lesson

v3 Bytecode: Closures + Collections

Why this module exists

v3 compiler output is now richer than expression math. It must model:

  • lexical closures
  • composite heap values
  • index operations on mutable containers

This means bytecode is not just arithmetic anymore; it is a compact contract between compiler and runtime.


1) Closure-oriented bytecode model

v3 introduces explicit closure creation and call flow:

  • function declarations compile into function objects in the code section
  • MAKE_CLOSURE <id> creates a callable value in the operand stack
  • CALL <arity> executes whatever callable is on the stack

If there is no closure object at call time, VM should fail with a runtime error.

Design implication:

  • compile-time does not execute closure contents
  • runtime owns lifetime of captured values and callable identity

2) Function object identity and environment binding

Even before GC is introduced, treat function values as identity-bearing values.

At compile time, each function declaration should:

  • have stable entry index/id
  • compile body as separate function bytecode unit
  • be referenced through id in closure/call instructions

MAKE_CLOSURE can be viewed as:

  1. lookup function metadata by <id>
  2. capture current lexical references (in later stages, this becomes closure env)
  3. push a callable object to stack

This keeps callsites uniform regardless of whether callee is named or passed around.


3) Collections as first-class bytecoded operations

MAKE_ARRAY <n>

  • pops n values in reverse push order
  • returns one array value on stack

MAKE_MAP <n>

  • pops 2n operands: k1 v1 k2 v2 ... kn vn
  • stores by pair order

GET_INDEX

  • pops container + index
  • reads by key/index semantics configured by VM
  • returns looked-up value or nil/error per semantics

SET_INDEX

  • pops container + index + value (or value + index + container depending on VM ordering)
  • mutates container in place
  • pushes updated container or nothing depending on design choice

For this course, explicit ordering should be deterministic and taught once, not rediscovered by students in implementation.


4) Bytecode stack discipline for container ops

Index and collection instructions are easy to get subtly wrong. Recommended mental model:

  • arithmetic and call ops are stack-consuming in a fixed arity
  • container ops should mirror this predictability:
    • create ops consume source values and return 1 container result
    • access ops consume exactly the operands needed and push result/error signal

Avoid implicit popping patterns that depend on implementation internals; make operand order visible in the spec.


5) Control flow and scope op ordering (v3 continuation)

JUMP and JUMP_IF_FALSE remain the same branching core from earlier:

  • absolute target addresses
  • JUMP_IF_FALSE pops one condition value first
  • explicit ENTER_SCOPE/EXIT_SCOPE

What changes with closures/collections is that values on these instructions can now be containers or callable values; operand stack traces become more important.


6) Error behavior as compiler/VM contract

v3 bytecode execution should make failure explicit:

  • calling a non-closure value
  • indexing non-indexable type
  • map key arity mismatch in map literal creation
  • bad CALL arity when target is known function

A robust instruction contract is more valuable than clever op additions.


7) Practice checkpoints (almost solved)

1) Array construction ordering + +Input bytecode: PUSH 1, PUSH 2, PUSH 3, MAKE_ARRAY 3 + +Expected stack: one array value containing [1,2,3] in that order. + +2) Map construction ordering + +Input: PUSH "a", PUSH 1, PUSH "b", PUSH 2, MAKE_MAP 2 + +Expected: map with keys "a"->1, "b"->2. + +3) Closure call flow + +Compile-time expectation: MAKE_CLOSURE places callable; CALL n consumes it and n args and pushes return value. + +4) Non-callable dispatch + +Stack contains 1 then CALL 0 executes + +Expected: runtime error "call target is not callable" (or equivalent). + +5) Indexing contract + +Given map m, op sequence should consistently consume operands and return either value or error by defined policy. + +This module should give students a clean contract for rich bytecode before moving into runtime GC behavior.


Module Items

  • Compile to Bytecode v3

    Lower a v3 program AST into stack-based bytecode.

    hard Upgrade to Pro to access hard problems
Join Discord