structs & refs

Layout & Performance

Where struct globals live, the sizeof/alignof/offsetof builtins, and where copying actually costs.

Examples on this page import this shared module — it is live too:

vec2.as
export struct Vec2 {
    x: f32 = 0;
    y: f32 = 0;
    constructor(x: f32, y: f32) {
        this.x = x;
        this.y = y;
    }
}

Value globals

A module-level let or const of struct type lives in static memory, not in a wasm global — the compiler reserves the flat bytes at a fixed address and initializes them in the start function. Every place operation works on it: field writes, compound assignment, methods, and binding a ref into it. Statics never move, so the ref fence that guards growable arrays does not apply.

globals.as
import { Vec2 } from "./vec2";

export const gravity = Vec2(0, -10);
let counter = Vec2(0, 0);

function bump(v: ref Vec2): void { v.x += 1; }

export function demo(): f32 {
    counter.x = 5;
    counter.y += 2;                   // compound leaf write
    bump(counter);                    // ref into the static place
    return counter.x + counter.y + gravity.y;   // 6 + 2 - 10
}

Layout

sizeof<T>() and alignof<T>() report payload size and alignment for structs; offsetof<T>("field") works through the flat layout. Flattened locals have no address of their own — taking a ref to one spills it to a per-frame scratch zone, so aliasing works and costs only where used.

Bit-fields

A width in parentheses after a field's type packs it into bits, like a C++ bit-field: visible: bool(1). Bit-fields may be bool (width 1) or u8, u16, u32 (width up to the type's bits), in structs and reference classes alike. Each one takes the next free bits unless that would straddle an aligned unit of its own type, so nine flags fit in two bytes and a bool(1) beside a u16(15) fits in two.

flags.as
struct Flags {
    visible: bool(1) = true;
    dirty: bool(1);
    kind: u8(4);
}

export function demo(): i32 {
    let f = Flags();
    f.kind = 0x1f;   // stored as 0xf: writes keep the low bits
    f.dirty = true;  // neighbors survive the write
    return sizeof<Flags>() + f.kind;   // 1 + 15
}

A write keeps only the field's bits, wherever the value lives, so reads always see an in-range value. In memory it reads the storage unit, merges the new bits and writes it back. A struct held in locals keeps each bit-field in its own local, so packing costs nothing until the value lands in memory. A full-width integer such as u8(8) is a plain field.

Widths belong to class and struct fields only: locals, parameters, return types and interface properties reject them. Bit-fields have no address, so offsetof and ref reject them too.

Performance notes