Methods and Vtables
How to lower method calls, interfaces, and virtual dispatch to SBBE IR.
Object-oriented method dispatch is a frontend concern. SBBE provides the building blocks (calli, memory operations, function indices) and the frontend uses them to implement whatever dispatch model the source language requires.
Static dispatch (non-virtual methods)
A non-virtual method is just a regular function where the first parameter is a pointer to the receiver. The frontend resolves the method at compile time and emits a direct call.
Pseudo-code
class Counter {
var count: Int32 = 0
func increment() { count += 1 }
func get() -> Int32 { return count }
}
SBBE lowering
// Counter layout: { i32 count } at offset 0
func $Counter_increment(ptr) {
entry:
ldl 0 // self
dup // keep self for the store
ldm i32 // load self.count
ldi 1
add.s i32 // count + 1
stm i32 // store back to self.count
ret
}
func $Counter_get(ptr) -> i32 {
entry:
ldl 0 // self
ldm i32 // load self.count
ret
}
func $main() -> i32 {
var $c ptr
entry:
// Allocate a Counter (4 bytes)
ldi 4
salloc
str $c
// Initialize count = 0
ld $c
ldi 0
stm i32
// c.increment() — static dispatch, direct call
ld $c
call $Counter_increment
ld $c
call $Counter_increment
// c.get() — returns 2
ld $c
call $Counter_get
ret
}
Name mangling ($Counter_increment) is the frontend’s responsibility. Common schemes include $Type_Method, $module.Type.Method, or C++-style mangling.
Virtual dispatch (vtables)
Virtual methods require runtime dispatch through a vtable (virtual method table). The vtable is an array of function indices stored in memory. Each object has a pointer to its vtable.
Object layout
// Object layout:
// offset 0: ptr vtable_ptr
// offset 8: ... instance fields ...
// Vtable layout (array of i32 function indices):
// slot 0: method_0 function index
// slot 1: method_1 function index
// ...
Pseudo-code
interface Shape {
func area() -> Double // vtable slot 0
func perimeter() -> Double // vtable slot 1
}
class Circle: Shape {
var radius: Double // offset 8
func area() -> Double { return 3.14159 * radius * radius }
func perimeter() -> Double { return 2 * 3.14159 * radius }
}
SBBE lowering
// Circle.area implementation
func $Circle_area(ptr) -> f64 {
entry:
ldl 0
ldi 8
add.s i64
ldm f64 // load self.radius
dup
fmul f64 // radius * radius
ldc f64 3.14159265358979
fmul f64 // pi * r^2
ret
}
// Virtual dispatch: call shape.area() through vtable
func $call_area(ptr) -> f64 {
entry:
// Load vtable pointer from object (offset 0)
ldl 0
ldm ptr // vtable_ptr
// Load function index from vtable slot 0
ldm i32 // vtable[0] = area function index
// Call with self as first argument
ldl 0 // push self
swap // put func_index on top
calli 0 // call vtable[0](self)
ret
}
Setting up the vtable
The vtable is typically stored in a static region of linear memory, initialized once at program startup:
var $circle_vtable ptr = 0
func $init_vtables() {
entry:
// Allocate vtable (2 slots * 4 bytes each)
ldi 8
salloc
str $circle_vtable
// Slot 0: area -> index of $Circle_area
ld $circle_vtable
ldi 2 // function index of $Circle_area
stm i32
// Slot 1: perimeter -> index of $Circle_perimeter
ld $circle_vtable
ldi 4
add.s i64
ldi 3 // function index of $Circle_perimeter
stm i32
ret
}
Interface dispatch (fat pointers)
Some languages (Go, Rust trait objects) use “fat pointers” for interface dispatch: a (data_ptr, vtable_ptr) pair. This avoids storing the vtable pointer inside every object.
// Fat pointer layout:
// offset 0: ptr data_ptr
// offset 8: ptr vtable_ptr
func $call_via_interface(ptr) -> f64 {
// param 0 is a fat pointer to a Shape interface
entry:
// Load vtable
ldl 0
ldi 8
add.s i64
ldm ptr // vtable_ptr
// Load function index from vtable slot 0 (area)
ldm i32 // area function index
// Load data pointer
ldl 0
ldm ptr // data_ptr
// Call: area(data_ptr)
swap
calli 0
ret
}
Key takeaways
- Static dispatch: Direct
callwith the receiver as the first parameter - Virtual dispatch: Load the vtable pointer from the object, index into it, then
calli - Interface dispatch: Fat pointer (data_ptr + vtable_ptr), vtable lookup, then
calli - Name mangling is the frontend’s job
- SBBE never sees classes, interfaces, or inheritance; it only sees pointers, loads, and indirect calls