structs & refs

Places & Refs

Writing through nested fields and array elements directly — and ref, a binding that aliases a location instead of copying it.

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

export struct Rot {
    c: f32 = 1;   // identity rotation
    s: f32 = 0;
}

export struct Transform {
    p: Vec2 = Vec2(0, 0);   // structs nest
    q: Rot = Rot();
}

Places: writing through chains

Anything that names a memory location — an embedded field, a StaticArray element, a chain of both — is a place, and stores through places compile to direct writes at computed addresses:

places.as
import { Vec2, Transform } from "./vec2";

class Body {
    position: Vec2 = Vec2();      // embedded flat: no pointer, no header
    transform: Transform = Transform();
    mass: f32 = 1;
}

export function demo(): f32 {
    let body = new Body();
    body.transform.p.x = 3;           // one store at a constant offset
    body.transform.p.x += 2;          // compound leaf assignment works

    let arr = new StaticArray<Transform>(8);
    arr[1].p = Vec2(1, 2);            // element place: leaf stores
    arr[1].q.s += 0.25;               // chains through elements resolve statically

    return body.transform.p.x + arr[1].p.y + arr[1].q.s;   // 5 + 2 + 0.25
}

Reads chain to single loads the same way — body.transform.p.x is one load at a constant offset. This flat embedding is the payoff shape ports care about: a Box2D body is one allocation, not a tree of five.

Refs: places as values

A ref is a first-class place: a pointer plus a static element type, formed from anything that names a location — value locals and params (taking a ref spills them to a scratch frame so both names alias), struct fields in objects, StaticArray<V> and FixedArray elements including runtime indexes, and single leaves like v.x. Reads copy out, writes go through, and a ref forwards to other ref params without copying.

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

function bump(v: ref Vec2, dx: f32): void {
    v.x += dx;                     // writes the caller's place
}

class Poly {
    vertices: StaticArray<Vec2> = new StaticArray<Vec2>(4);
}

export function demo(): f32 {
    let poly = new Poly();
    ref p = poly.vertices[1];      // binds the element, no copy
    p.x += 3;                      // W3001's hazard, done right
    bump(poly.vertices[1], 2);     // place straight into a ref param

    let v = Vec2(1, 0);
    ref q = v;                     // spills v; q and v alias
    q.x += 4;
    return poly.vertices[1].x + v.x;   // 5 + 5
}

Refs are deliberately second-class: locals and params only. No ref fields, returns, closures, generators, or arrays of refs — all explicit errors — so a ref’s lifetime is lexical and the compiler pairs every ref rooted in a managed object with a hidden root for its scope. One fence to know: ref p = arr[i] into a growable Array<V> refuses at any chain depth (“may reallocate; copy or index directly”) — push can move the buffer. StaticArray and FixedArray elements never move, so they bind freely.