SBBE has no composite types. Structs, arrays, and unions are all lowered to flat memory operations by the frontend. This guide shows how a compiler would translate struct definitions and field accesses into SBBE IR.

The mental model

A struct is a region of memory with fields at known offsets. The frontend computes the layout (sizes, offsets, alignment) and emits ldm/stm instructions at the correct byte offsets. SBBE never sees the struct as a type; it only sees loads and stores to addresses.

Keep in mind that a ptr doesn’t have to point to heap space. You can use salloc to allocate memory directly on the function’s stack and address it with ptr the same way. Stack-allocated structs are common for small, short-lived values and avoid the overhead of heap management.

Example: a 2D point

Consider a simple struct like the following:

struct Point {
    var x: Int32  // offset 0, size 4
    var y: Int32  // offset 4, size 4
}
// total size: 8 bytes, alignment: 4

Allocating a struct on the stack

func $make_point(i32, i32) -> ptr {
    var $p ptr

entry:
    // Allocate 8 bytes for the struct
    ldi 8
    salloc
    str $p

    // Store x at offset 0
    ld $p                // base address
    ldl 0                // first parameter (x)
    stm i32              // mem[p + 0] = x

    // Store y at offset 4
    ld $p
    ldi 4
    add.s i64            // p + 4
    ldl 1                // second parameter (y)
    stm i32              // mem[p + 4] = y

    ld $p
    ret
}

Reading a field

To read point.y, the frontend emits a load at base + 4:

// Assume $p (ptr to Point) is on the stack
ld $p
ldi 4
add.s i64          // addr = p + 4
ldm i32            // push(point.y)

Writing a field

// Set point.x = 42
ld $p              // base address
ldi 42             // new value
stm i32            // mem[p + 0] = 42

Nested structs

For a struct containing another struct:

struct Line {
    var start: Point  // offset 0, size 8
    var end: Point    // offset 8, size 8
}

Accessing line.end.y is just base + 8 + 4 = base + 12:

ld $line
ldi 12             // offset of end.y
add.s i64
ldm i32            // push(line.end.y)

The frontend can precompute nested offsets at compile time, so nested struct access is always a single add + load, regardless of nesting depth.

Passing structs to functions

Small structs (fitting in 1-2 registers) can be flattened into separate parameters:

// Instead of: func $distance(Point, Point) -> f64
// The frontend emits:
func $distance(i32, i32, i32, i32) -> f64 {
    // params: x1=0, y1=1, x2=2, y2=3
entry:
    ldl 2
    ldl 0
    sub.s i32       // dx = x2 - x1
    // ... compute sqrt(dx*dx + dy*dy) ...
}

Large structs are passed by pointer:

func $process(ptr) {
entry:
    ldl 0           // ptr to struct
    ldm i32         // load first field
    // ...
}

The choice of flattening vs. pointer passing follows the target’s ABI (e.g., System V AMD64 passes structs up to 16 bytes in registers).

Arrays

An array is just a pointer with index arithmetic. To access arr[i] where each element is 4 bytes:

ld $arr
ld $i
ldi 4
mul.s i64          // i * sizeof(element)
add.s i64          // arr + i * 4
ldm i32            // push(arr[i])

A strength reduction optimization can replace the mul with a shl when the element size is a power of two:

ld $arr
ld $i
ldi 2
shl i64            // i << 2 = i * 4
add.s i64
ldm i32