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.