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.
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:
- Fields are primitives, other structs, or
FixedArrays — no references, so GC visitors skip value ranges wholesale. - Single inheritance from another struct — the base’s leaves become
the derived prefix and methods inherit with static shadowing (see
Inheritance). No interfaces, no nullability
(
nullrefuses a struct target at compile time). - Methods and operators exist on the same representation — mutating
methods take
thisas an implicitref,readonlymethods take a copy, and operators are C++-styleoperator+(a, b)members over values (see Methods & Constructors). Free-function style (dot(a, b)) stays idiomatic for C ports. - No destructors, ever — destruction is trivially nothing, by construction. Value locals die at scope exit for free; embedded fields and array elements are part of their owner’s allocation and die with it. Copies at every boundary make ownership incoherent, so RAII stays off the table: these are C structs, deliberately.
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
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:
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.