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:
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.
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.
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
- Copies are silent; an oversized value class pays hidden per-boundary leaf traffic. Keep value classes small (vectors, rotations, transforms, colors) — big aggregates belong in reference classes.
- A resource-handle field in a value class duplicates on copy with no warning — inherent to POD semantics. Keep handles in reference classes.
- On the Box2D port, value classes plus SIMD cut solve time ~22% and module size ~28%, and deleted six hundred lines of hand-flattening workarounds — the representation expresses directly what the port had been doing by hand.
- For reference classes that stay reference classes, allocation sinking
(automatic under
-O) scalarizes non-escapingnewanyway —structis the opt-in guaranteed representation; sinking is best-effort for everything else.