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.
The forms
Section titled “The forms”// 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 callingA 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);};Captures are by-value snapshots
Section titled “Captures are by-value snapshots”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 lateplus(7); // 107 — the snapshot holds 100A 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
selfor an ivar directly — copy it into a local first (auto me = self;), which makes the capture visible and deliberate. - No
autocaptures. A capture needs the declared type at the point of capture; give the variable an explicit type.
block: — write-back captures
Section titled “block: — write-back captures”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 boxThe 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.
What a block is underneath
Section titled “What a block is underneath”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.
Worked example
Section titled “Worked example”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) = 5plus(7) = 107op=add = 30op={x*y} = 42each(2n) = 20add5(10) = 15fact(5) = 120total,r = 10 10null block = ok