Closures and Callbacks
How to implement closures, function pointers, and callback systems in SBBE IR.
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:
- Allocate an environment (a struct in linear memory) containing the captured values
- Pass the environment pointer as a hidden first parameter to the closure function
- 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
- A closure is a (function_index, environment_pointer) pair
- The environment is allocated in linear memory (via
sallocor the heap) - The closure function receives the environment as a hidden first parameter
callihandles the indirect call; the frontend manages the calling convention- This pattern works for closures, callbacks, vtable dispatch, and coroutine resume functions