Closures & Iteration
Functions that capture their surroundings, for..of loops,
spread arguments, and generators — the extensions upstream AssemblyScript
never shipped.
Closures
Upstream AssemblyScript’s oldest open request (issue #798): functions cannot capture enclosing locals. rasc implements them natively. Every function value is a managed closure object — a function table index plus captured fields — passed as a hidden context argument; plain function references get forwarding thunks, so they mix freely.
export function counter(): i32 {
let count = 10;
let inc = (by: i32): i32 => { count += by; return count; };
inc(5); // 15
return inc(3); // 18 — mutation persists across the closure's calls
}
The semantics to internalize: capture is by value at closure creation. Assigning to a captured variable inside the closure updates the closure’s own context and persists across its calls — but the enclosing scope does not observe it, and later writes to the outer local are not seen by the closure:
export function demo(): i32 {
let n = 1;
let f = (): i32 => n;
n = 5;
return f(); // 1 — the closure snapshotted n at creation
}
Contexts are synthesized managed classes: GC-traced, safe to escape through
globals, arrays, fields, and returns; captured strings and objects survive
collection. Two limits: lambdas cannot capture this (copy needed
fields to a local first), and lambda parameters need type annotations unless the
context supplies a function type. Note that closures are suite-tested under the
GC runtimes; under the stub runtime prefer class-based listeners.
Function-typed fields and accessors call directly — the closure loads off the receiver and dispatches indirectly, no copy to a local needed:
function echo(id: i32): i32 { return id; }
class Button {
onTap: (id: i32) => i32 = echo;
}
export function demo(): i32 {
let b = new Button();
b.onTap = (id: i32): i32 => id * 2;
return b.onTap(21); // 42 — plain functions and closures mix freely
}
for..of
for (let x of arr) { total += x; } // Array/StaticArray, typed arrays: index loop
for (let k of map.keys()) { use(k); } // arrays from keys()/values()
for (let c of name) { last = c; } // strings: one code point per step, as String
for (let v of new Range(10)) { ... } // protocol path: values()/iterator(), then next()
Array, StaticArray, the typed arrays, and any class
with a length getter and operator[] lower to an index
loop — under -O the reads go unchecked. Strings iterate the way JS
does: one code point per step, surrogate pairs kept whole, each yielded as a
String:
export function wideChars(): i32 {
let wide = 0;
for (let c of "wide \u{1F642} char") {
if (c.length == 2) { wide++; } // the emoji stays whole
}
return wide; // 1
}
Everything else uses the protocol path: the compiler calls
values() or iterator() once if present, then
next() repeatedly, which must return a class with done
and value fields — and the protocol outranks length
plus [] when a class declares both. All of it resolves statically
per monomorphized type — plain direct calls, no dynamic iterator objects, no
Symbol.iterator. The binding must be a single let;
break/continue work.
Spread arguments
function blend(r: f32, g: f32, b: f32): f32 {
return r * 0.3 + g * 0.6 + b * 0.1;
}
export function demo(): f32 {
let c = FixedArray<f32, 3>();
c[0] = 1; c[1] = 0.5; c[2] = 0.25;
return blend(...c) + blend(...[1, 1, 1]);
}
Spread sources are static-length only — array literals and
FixedArray — so the element count is a compile-time fact and every
check stays at compile time: arity, implicit conversions, and defaults behave
exactly as if the elements were written inline. A trailing spread fills the
remaining parameters; literal elements evaluate left to right, and a
FixedArray spread unpacks value copies, struct elements included.
Spreading a dynamic array or a string, spreading into a constructor, a
non-final spread, and supplying a ref parameter are all clean
compile errors.
Generators
function* counter(n: i32): i32 { // return type = yielded element type
for (let i = 0; i < n; i++) {
yield i;
}
}
export function demo(): i32 {
let sum = 0;
for (let x of counter(3)) { // generators satisfy the for..of protocol
sum += x;
}
return sum + counter(9).next().value; // 0+1+2, plus a fresh .next().value
}
Calling a generator allocates a managed state object holding done,
value, parameters, and hoisted locals; next() returns
the object itself, and the body is flattened into state segments driven by a
dispatch loop. Instances are independent and GC-traced.
yield is statement-only, by construction: no wasm
operand-stack value ever lives across a suspend. let x = yield ...,
(yield 1) + 2, and yield* are syntax errors, and a
switch or for..of containing a yield is rejected.
Generator locals need type annotations when not inferable.