SBBE has no built-in closure type. Closures are lowered by the frontend into a function pointer paired with an environment pointer, using calli for the indirect call. This guide shows the common patterns.

Simple function pointers

A function pointer is just an index into the unit’s function table. The frontend knows the index at compile time.

func $double(i32) -> i32 {
entry:
    ldl 0
    ldi 2
    mul.s i32
    ret
}

func $triple(i32) -> i32 {
entry:
    ldl 0
    ldi 3
    mul.s i32
    ret
}

func $apply(i32, i32) -> i32 {
    // params: fn_index=0, value=1
entry:
    ldl 1          // push the argument
    ldl 0          // push the function index
    calli 0        // indirect call
    ret
}

func $main() -> i32 {
entry:
    ldi 0          // index of $double
    ldi 21         // value
    call $apply    // apply($double, 21) = 42
    ret
}

Closures with captured variables

A closure captures variables from its enclosing scope. The standard lowering is:

  1. Allocate an environment (a struct in linear memory) containing the captured values
  2. Pass the environment pointer as a hidden first parameter to the closure function
  3. Store the (function_index, env_pointer) pair as the closure value

Pseudo-code

func makeAdder(_ n: Int32) -> (Int32) -> Int32 {
    // captures n
    return { x in x + n }
}
let add5 = makeAdder(5)
add5(10)  // returns 15

SBBE lowering

The closure function takes the environment pointer as its first parameter:

// The closure body: takes (env_ptr, x) and returns x + env.n
func $adder_closure(ptr, i32) -> i32 {
entry:
    ldl 1          // x
    ldl 0          // env ptr
    ldm i32        // load n from env[0]
    add.s i32      // x + n
    ret
}

The factory function allocates the environment and returns a closure pair. The caller is responsible for the calling convention (storing the pair, passing the env pointer).

// make_adder(n) -> returns (func_index, env_ptr) as two values
func $make_adder(i32) -> ptr {
    var $env ptr

entry:
    // Allocate environment: 4 bytes for one i32
    ldi 4
    salloc
    str $env

    // Store captured variable n into env
    ld $env
    ldl 0          // n parameter
    stm i32        // env[0] = n

    // Return env pointer
    // (caller stores this alongside the function index)
    ld $env
    ret
}

Calling the closure:

func $main() -> i32 {
    var $env ptr

entry:
    // make_adder(5)
    ldi 5
    call $make_adder
    str $env

    // Call the closure: adder_closure(env, 10)
    ld $env        // env pointer (first arg)
    ldi 10         // x (second arg)
    ldi 0          // function index of $adder_closure
    calli 0        // indirect call -> returns 15
    ret
}

Callback registration

For callback systems (event handlers, plugin hooks), the pattern is a function index + context pointer stored in a struct:

// Callback struct layout:
//   offset 0: i32 func_index
//   offset 4: (padding)
//   offset 8: ptr context

func $register_callback(ptr, i32, ptr) {
    // params: callback_slot=0, func_index=1, context=2
entry:
    // Store func_index
    ldl 0
    ldl 1
    stm i32

    // Store context at offset 8
    ldl 0
    ldi 8
    add.s i64
    ldl 2
    stm ptr

    ret
}

func $invoke_callback(ptr) -> i32 {
entry:
    // Load context (first arg to the callback)
    ldl 0
    ldi 8
    add.s i64
    ldm ptr

    // Load func_index
    ldl 0
    ldm i32

    // Call: callback(context)
    calli 0
    ret
}

Key takeaways