concepts

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.

closures.as
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:

capture.as
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:

listeners.as
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

iteration.as
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:

codepoints.as
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

spread.as
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

generators.as
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.