language

Structs & Refs

Structs bring C’s plain-old-data semantics: values that copy on assignment, embed flat in their containers, and never touch the heap.

Declare a struct and it is never a heap object. Its fields become leaves: locals when the value is a local, stack values when it is an expression, a flat byte range when it is embedded in a reference class or a StaticArray. Assignment, argument passing, and returning copy by construction — a value expression is its leaves.

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

export function add(a: Vec2, b: Vec2): Vec2 {
    return Vec2(a.x + b.x, a.y + b.y);
}

export function dot(a: Vec2, b: Vec2): f32 {
    return a.x * b.x + a.y * b.y;
}

export function demo(): f32 {
    return dot(add(Vec2(1, 2), Vec2(3, 4)), Vec2(1, 1));   // 4 + 6
}

Vec2() never allocates — it pushes leaves with field initializers applied. Locals, params, and returns flatten to leaves in the schema’s normative order; returns use wasm multi-value for every size, with no hidden out-pointer fallback (see Multi-Value Returns).

The shape

Structs are deliberately pinned to plain-old-data:

Everything outside this shape is an explicit compile error, not a silent fallback — a multi-slot value in the wrong context would otherwise silently corrupt the wasm stack, so every such context diagnoses instead. On the declaration: constructors, accessors, static methods, index signatures, field cycles (static primitive fields are legal). At use sites: ternaries on values, value globals, static value fields, closure captures, generator locals, params, and yields, function-typed uses, instanceof, stores into rvalue temporaries (make().x = 9), value-typed setters, virtual or interface member access, switch values, conditions (if (v), !v, &&/||), for..of elements, template literal ${}, changetype/select/assert, Map keys or values, and the unowned std surface of Array<V> (see Value Arrays). On the declaration, accessors, non-operator statics, and index signatures are grammar errors.

Copy semantics

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

export function demo(): f32 {
    let a = Vec2(1, 2);
    let b = a;        // copy
    b.x = 10;         // a.x is still 1

    let t = Transform();
    t.p = Vec2(3, 4); // copy into the embedded field
    let v = t.p;      // copy out
    v.x = 100;        // t.p.x is still 3

    return a.x + b.x + t.p.x;   // 1 + 10 + 3
}

Getters that return a value type return a copy too. This is the C rule — let v = arr[i] copies, arr[i].x = 5 writes through — and it has one famous consequence:

the alias-mutation hazard

Code written against reference semantics — let v = poly.vertices[i]; v.x = ... — silently mutates a copy under value semantics. Copy-then-mutate is legal, so the language keeps it; but when no path reads the copy again the mutation is provably dead, and the compiler warns (W3001: “this writes to a copy of poly.vertices[i]… write through it, store the copy back, or bind it with ref”). Mutating a copy and then using it is idiomatic and stays silent — that variant is on you. When porting, convert with copy/store-back first, then tighten hot loops with ref bindings, which alias the place instead of copying.

The rest of the chapter: Places & Refs covers writing through chains and ref bindings, Value Arrays the flat containers and FixedArray, Methods & Constructors the member surface, Inheritance the prefix layout, Keys & Equality struct identity in maps and sets, and Layout & Performance the value globals, layout builtins, and cost model.