Skip to content

Blocks

A block is a function value that captures variables from its enclosing scope. Declare one like a variable, call it like a function:

block add u32(u16 x, u16 y) = { return (u32)x + (u32)y; }
u32 five = add(2, 3);

The declaration reads by the same rule as every other declaration in the language — the name is the second token, mirroring u32 y = …: block is the kind, add is the name, and the signature u32(u16 x, u16 y) is a qualifier on the declaration, the way a function’s parameter list qualifies its name.

// Declaration with a body.
block add u32(u16 x, u16 y) = { return (u32)x + (u32)y; }
// Declaration only; assign later. Parameter NAMES are part of the
// declared signature…
block op u32(u16 x, u16 y);
op = add;
// …which is what lets a bare body re-bind it with nothing repeated:
op = { return (u32)x * (u32)y; };
// Inline literal in argument position.
each(block u32(u32 v) { return v * (u32)2; }, (u32)5);
// A block as a parameter — the name rides inside the type, like any
// block declaration.
u32 each(block cb u32(u32), u32 n) { … cb(i) … }
// A block as a return type, a block in `auto`, a null block:
block cb u32(u32) makeAdder(u32 base) { … }
auto add5 = makeAdder((u32)5);
block maybe u32(u32);
maybe = (block u32(u32))0;
if (maybe) { … } // test before calling

A named literal may call itself — recursion without a forward declaration:

auto fact = block f u32(u32 n) {
if (n <= (u32)1) { return (u32)1; }
return n * f(n - (u32)1);
};

A literal captures the locals and parameters its body names, by value, at the moment the literal runs:

u32 base = (u32)100;
block plus u32(u32 n) = { return base + n; }
base = (u32)999; // too late
plus(7); // 107 — the snapshot holds 100

A captured class pointer copies the reference (the object is shared and retained; the binding is not). Two rules keep captures honest:

  • Locals and parameters only. A block cannot capture self or an ivar directly — copy it into a local first (auto me = self;), which makes the capture visible and deliberate.
  • No auto captures. A capture needs the declared type at the point of capture; give the variable an explicit type.

A plain capture is a snapshot; mutating it inside the block changes the block’s own copy. When a callback must hand a result home, qualify the variable with block::

block:u32 total = (u32)0;
each(block u32(u32 v) { total = total + v; return v; }, (u32)5);
// total == 10 right here — no cell, no box

The semantics are copy-in / copy-out (Ada’s value-result; in practice what Swift’s inout does): the block takes a private copy at creation and writes it back at every invocation exit — including an exit by throw. Between calls, the variable holds the latest written-back value; while the block runs, neither side sees the other’s half-done state. The variable itself stays an ordinary local — block: licenses the capture, nothing else.

Because write-back targets the enclosing stack frame, a block carrying block: captures must not outlive it. The compiler rejects returning one and storing one into an ivar, global field or element; block: captures inside a nested literal are also rejected. Passing such a block down — to each, a sort, any callee that calls it and returns — is the intended shape. (A callee that stows its argument for later — a thread spawn, a registry — is outside what the compiler can see; results that must survive the frame belong in an object.) v1 limits block: to scalars and pointers.

A block is a heap object: the captures are its fields, ARC owns it, and calling it is a method call. That means everything you know about objects applies — blocks pass and return like any reference, if (blk) is a null test, storing a (non-block:) block in an ivar keeps it alive, and a block that captures the only reference to an object keeps that alive too. Blocks work on every target, including xt6502, under -falloc=heap.

Current limits, beyond the capture rules above: &obj.method does not yet convert to a block (planned); an inline literal cannot be invoked in the same expression ((block …{…})(5) — bind it to a name first); and a block-typed ivar can be called with the cb(…) sugar only when it is declared before the method that calls it — self.cb.invoke(…) always works.

examples/compiler/blocks.xc, compiled and run by the docs harness:

// blocks.xc — blocks: function values with captures (xtc 0.4).
#import "Stdio.xc"
#import "Foundation.xc"
// A block as a parameter: `each` drives it, the caller supplies it.
u32 each(block cb u32(u32), u32 n)
{
u32 acc = (u32)0;
for (u32 i = (u32)0; i < n; i++) { acc = acc + cb(i); }
return acc;
}
// A block as a return value: the captures live in the block, so they
// outlive this frame — ARC owns the block like any other object.
block cb u32(u32) makeAdder(u32 base)
{
block a u32(u32 n) = { return base + n; }
return a;
}
i32 main(void)
{
// Declaration with a body; the call reads like any function call.
block add u32(u16 x, u16 y) = { return (u32)x + (u32)y; }
Stdio.printf("add(2,3) = %ld\n", add(2, 3));
// Captures are BY-VALUE snapshots, taken when the literal runs.
u32 base = (u32)100;
block plus u32(u32 n) = { return base + n; }
base = (u32)999; // too late: plus still sees 100
Stdio.printf("plus(7) = %ld\n", plus(7));
// Re-assignment; a bare body inherits the DECLARED signature.
block op u32(u16 x, u16 y);
op = add;
Stdio.printf("op=add = %ld\n", op(10, 20));
op = { return (u32)x * (u32)y; };
Stdio.printf("op={x*y} = %ld\n", op(6, 7));
// Inline literal in argument position; auto infers block types.
Stdio.printf("each(2n) = %ld\n", each(block u32(u32 v) { return v * (u32)2; }, (u32)5));
auto add5 = makeAdder((u32)5);
Stdio.printf("add5(10) = %ld\n", add5((u32)10));
// A NAMED literal can call itself — recursion without a declaration.
auto fact = block f u32(u32 n) { if (n <= (u32)1) { return (u32)1; } return n * f(n - (u32)1); };
Stdio.printf("fact(5) = %ld\n", fact((u32)5));
// `block:` — write-back captures: copy in at creation, copy out at
// every invocation exit. The loop below never sees the variable; it
// still holds the total the moment `each` returns.
block:u32 total = (u32)0;
u32 r = each(block u32(u32 v) { total = total + v; return v; }, (u32)5);
Stdio.printf("total,r = %ld %ld\n", total, r);
// Null is a first-class block value; test before calling.
block maybe u32(u32);
maybe = (block u32(u32))0;
if (maybe) { Stdio.printf("unreachable\n"); }
else { Stdio.printf("null block = ok\n"); }
return 0;
}
add(2,3) = 5
plus(7) = 107
op=add = 30
op={x*y} = 42
each(2n) = 20
add5(10) = 15
fact(5) = 120
total,r = 10 10
null block = ok