Structs and Fields
How to lower struct types and field access to SBBE IR using flat memory operations.
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
ptrdoesn’t have to point to heap space. You can usesallocto allocate memory directly on the function’s stack and address it withptrthe 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