structs & refs

Value Arrays

Arrays of structs store their elements inline rather than as pointers — and FixedArray embeds a fixed-size array anywhere a value can live.

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;
    }
}

Arrays of values

Both array containers store value-class elements flat, with copy semantics at every boundary. StaticArray<V> is fixed-length: new StaticArray<V>(n) allocates the n elements back to back in one block, and length stays n. Array<V> is growable: sized construction, element places, length reads and assignment, array literals, push/pop, and for..of all work on it, and its capacity starts at eight elements and doubles as it grows. The other members (fill, slice, sort, …) work on any struct element type too.

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

export function demo(): f32 {
    let arr = new Array<Vec2>(2);   // zero-filled
    arr[0] = Vec2(3, 4);            // element place: leaf stores
    arr[1].y += 1;                  // chains through elements resolve statically
    arr.push(Vec2(5, 6));           // grows with the flat stride
    let v = arr.pop();              // copies out
    return arr[0].x + arr[1].y + v.y;   // 3 + 1 + 6
}

Deliberate deviations from reference-class arrays:

Map stores struct values flat in its entries: set copies in, get copies out, and values() iterates by copy. Struct keys (and Set elements) work through key identity — see Keys & Equality.

three births, one rule

V() runs field initializers. Uninitialized locals (let v: V;) and fresh array elements are zeros. Consistent with C — but if a type’s zero state is not meaningful (like a rotation), initialize elements explicitly.

Fixed arrays

FixedArray<T, N> is a first-class fixed-size value type: N elements embedded flat wherever it lives — a struct field, a local, a param, a return, another FixedArray. No separate allocation, no header, no indirection; length is the constant N. This is C’s b2Vec2 points[2] member, expressible directly.

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

struct Manifold {
    points: FixedArray<Vec2, 2>;
    count: i32 = 0;
}

export function demo(): f32 {
    let m = Manifold();
    m.points[0] = Vec2(3, 1);          // element place: leaf stores
    m.points[1].y += 2;                // compound leaf chains work
    let c = m;                         // the owner copies with its elements
    c.points[0].x = 100;               // m.points[0] is untouched
    let row = m.points;                // a FixedArray is a value: whole copies work

    let sum: f32 = 0;
    for (let i = 0; i < m.points.length; i++) {   // length is the constant
        sum += m.points[i].x + row[i].y;
    }
    return sum;                        // 3 + 1 + 0 + 2
}

Elements are structs or primitives (FixedArray<f32, 4>); nesting composes (FixedArray<FixedArray<f32, 2>, 3>). Element access goes through the place machinery — constant indexes fold into the offset, a runtime index on a memory-backed owner costs one bounds check per chain level (arr[i].points[j] composes), and a runtime index over a flattened local dispatches through a bounded compare chain of the element leaves. Lengths are pinned to 1–255 and the flattened fan-out to 1024 leaves — past that the engine’s frame limits would refuse the module, so the compiler refuses first. Fresh elements are zeroed, not field-initialized, like array elements everywhere else.