Multi-Value Returns
Functions return whole structs through WebAssembly’s native multi-value feature — at any size, with no hidden pointer plumbing.
When a function returns a struct, rasc emits a wasm signature
whose results are the struct’s fields, flattened in declaration order —
its leaves, in the sense of
Structs & Refs. A
Vec2-returning function is
(func (param ...) (result f32 f32)); a
Transform-returning function carries four results. The caller
receives the leaves directly on the operand stack — no allocation, no scratch
memory, no copy through linear memory.
struct Vec2 {
x: f32 = 0;
y: f32 = 0;
constructor(x: f32, y: f32) { this.x = x; this.y = y; }
}
function polar(r: f32, theta: f32): Vec2 {
// (result f32 f32) — leaves on the stack, no out-pointer
return Vec2(r * Mathf.cos(theta), r * Mathf.sin(theta));
}
export function demo(): f32 {
// Chained value expressions stay on the stack end to end:
let p = polar(2.0, 0.5);
let speed = polar(1.0, 0.25).x; // field of a call result: rvalue spill
return p.x + speed;
}
No size cap
There is deliberately no leaf-count ceiling. Early designs capped multi-value at four leaves with an out-pointer fallback; the landed representation handles every size uniformly instead, because two conventions means two code paths to verify and a performance cliff to document. Larger structs return more results. Both production engines — WAMR and V8 — execute the fixtures bit-identically.
Toolchain interplay
Multi-value is a wasm feature with uneven toolchain support, so rasc is conservative at the edges:
-O2passes--enable-multivalueto wasm-opt only when the module actually carries a signature with several results — modules that never use it keep byte-identical output.- The host binding ABI never uses multi-value: functions in the
rive_*_v1import namespaces that produce several values take caller-provided out-pointers into guest scratch instead, because out-params are uniform across every guest toolchain. Multi-value is an intra-module mechanism.
Under the hood
Flattening runs in the schema’s normative order — declaration order,
depth-first, with FixedArray elements expanding in index order —
so a Transform is
p.x, p.y, q.c, q.s everywhere: params, returns, locals, and the
debugger’s locals map. A field access on a bare call result
(add(a, b).y) spills the returned leaves to locals and reads the
one it needs; assignments to call results are compile errors, since the result
is a temporary copy.