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