Skip to content
Compiling Functions & Closures
step 1/3

Reading — step 1 of 3

Functions and Closures in Bytecode

~2 min readCompiling to Bytecode

Compiling Functions & Closures

Functions are the most complex feature to compile. Each function gets its own Chunk of bytecode, and closures require a mechanism to capture variables from enclosing scopes.

Separate Chunks

Each function is compiled into its own Chunk:

fun add(a, b) { return a + b; }

The compiler creates a new Chunk for add, compiles its body into it, then wraps it in a function object stored in the enclosing chunk's constant pool.

Call Frames

The VM needs a call stack to track nested function calls:

CallFrame {
    function:  Function     // the function being called
    ip:        int          // instruction pointer within this function's chunk
    slots:     int          // base index in the value stack for this frame
}

OP_CALL pushes a new CallFrame. OP_RETURN pops it and restores the caller's frame.

Compiling Closures and Upvalues

When a function references a variable from an enclosing scope, the compiler emits OP_CLOSURE instead of OP_CONSTANT:

OP_CLOSURE [function_index] [upvalue_count] [upvalue_descriptors...]

Each upvalue descriptor specifies:

  • isLocal: is the captured variable a local in the immediately enclosing function?
  • index: the stack slot (if local) or the upvalue index (if already captured)

Upvalue Resolution

fun outer() {
    var x = 10;
    fun middle() {
        fun inner() {
            print x;      // x is two scopes out
        }
        inner();
    }
    middle();
}

inner captures x from outer. But middle is in between. The compiler makes middle capture x as an upvalue too (even though it does not use it directly), so that inner can capture it from middle.

Closing Upvalues

When a local variable goes out of scope, any closure that captured it still needs it. We close the upvalue by moving the value from the stack to the heap:

OP_CLOSE_UPVALUE   // move stack local to heap-allocated upvalue

This is Lua's elegant solution, adopted directly in Crafting Interpreters. Before closing, the upvalue points to the stack slot. After closing, it points to its own heap copy.

Your Task

Compile function declarations, implement call frames, and handle closures with upvalue capture and closing.

Discussion

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

Sign in to post a comment or reply.

Loading…