Unions and Tagged Variants
How to lower unions, tagged unions, and variant types to SBBE IR.
Untagged unions (C-style)
A C union overlays multiple fields at the same memory address. The size of the union is the size of its largest member. In SBBE, this is trivial: all fields start at offset 0, and the frontend picks the correct ldm/stm type based on which field is being accessed.
union Value {
var asInt: Int32 // offset 0, size 4
var asFloat: Float // offset 0, size 4
}
// total size: 4 bytes
Reading the int interpretation:
ld $val // ptr to union
ldm i32 // read as i32
Reading the float interpretation of the same memory:
ld $val // same ptr
ldm f32 // read as f32
The cast instruction can also reinterpret a value already on the stack without going through memory:
ldi 0x3F800000 // IEEE 754 for 1.0f
cast i32 f32 // reinterpret bits as f32, now 1.0 is on the stack
Tagged unions (discriminated variants)
Many languages have tagged unions (Rust enum, TypeScript discriminated unions, ML variants). The standard lowering is a tag byte (or word) followed by the payload:
enum Shape {
case circle(radius: Double) // tag 0
case rect(width: Double, height: Double) // tag 1
}
// Layout:
// offset 0: i32 tag (0 = circle, 1 = rect)
// offset 8: payload (aligned to 8 for Double)
// total size: 24 bytes (max payload + tag + padding)
Constructing a Circle
func $make_circle(f64) -> ptr {
var $shape ptr
entry:
ldi 24
salloc
str $shape
// Write tag = 0 (Circle)
ld $shape
ldi 0
stm i32
// Write radius at offset 8
ld $shape
ldi 8
add.s i64
ldl 0 // radius parameter
stm f64
ld $shape
ret
}
Dispatching on the tag
func $area(ptr) -> f64 {
entry:
ldl 0 // shape ptr
ldm i32 // load tag
// Check if Circle (tag == 0)
eqz i32
jmp.if circle
jmp rect
circle:
ldl 0
ldi 8
add.s i64
ldm f64 // radius
dup
fmul f64 // radius * radius
ldc f64 3.14159265358979
fmul f64 // pi * r^2
ret
rect:
ldl 0
ldi 8
add.s i64
ldm f64 // width
ldl 0
ldi 16
add.s i64
ldm f64 // height
fmul f64 // width * height
ret
}
Option/Maybe types
An Option<T> is a tagged union with two variants: None (tag 0, no payload) and Some(T) (tag 1, payload follows). For pointer-sized types, a common optimization is to use a null pointer as None:
// Option<ptr> — null = None, non-null = Some
func $unwrap_or(ptr, ptr) -> ptr {
entry:
ldl 0 // the option value (ptr)
eqz i32 // is it null?
jmp.if use_default
jmp use_value
use_value:
ldl 0
ret
use_default:
ldl 1 // default value
ret
}
For non-pointer types, use the tag + payload layout described above.