! each Arm Is a Comprehension
Koru’s ! arm — the effect-handler position every for, map, store query, and channel read shares — grew a grammar. An arm is no longer BINDING when guard — it is a fold: bind, then stages, in text order.
for(1..8)
! each i when i % 2 == 0 { double: i * 2 } |> std/io:print.ln("{{ double:d }}") when filters. { } projects. They interleave, and each { } replaces the
downstream scope with exactly the names it lists. double does not exist
until the stage mints it; i does not survive it unless it is re-listed — { i, double: i * 2 }. The braces are a SELECT list.
The contract
! each BINDING (when EXPR | { FIELDS })* { FIELDS } is the produce literal — name pun or name: expr, the same
grammar the destructure landed earlier this week, doing double duty. name binds the element’s field; name: expr computes in the pre-stage scope. { i: i + 1 } is a legal rebind — the stage boundary means the old i is
unreachable the moment it closes, so LHS and RHS never coexist. { i: i } refuses: the pun { i } already says it, and : is for renames and
computations, never the same name twice.
when takes either position and the position is the meaning. Before the { } it guards the binding scope — it is admission, evaluated where the
produced names cannot yet exist. After, it guards the produced scope — engagement. There is no second guard keyword. An earlier draft paired when with a where for the admission site, and the chain dissolved it: a when before any { } is admission by construction, because the produced
names literally cannot be in scope. Position carries what a keyword would
have only restated.
And ! else continues the run:
for(1..16)
! each i when i % 15 == 0 |> std/io:print.ln("fizzbuzz")
! else i when i % 3 == 0 |> std/io:print.ln("fizz")
! else i when i % 5 == 0 |> std/io:print.ln("buzz")
! else i |> std/io:print.ln("{{ i:d }}") ! each stays parallel — handlers by birth, every matching arm fires. ! else opts the contiguous each/else run into exclusive dispatch: fires only when no
earlier arm did, ! else when … is else-if, bare ! else is the residual.
The exclusivity is spelled in the arm glyph, not inferred from ordering rules
— the reader sees “ordered dispatch” before reading a single guard.
Why the SQL frame is load-bearing
{ double: i * 2 } is SELECT i * 2 AS double. The resemblance is not a
metaphor borrowed for readability — it is the mechanism. A { } stage is a
projection, and a projection is demand: the arm names exactly what the body
will consume, so everything upstream of the list is free to produce only
those fields. SQL calls it projection pushdown; the Koru arm makes it
syntactic — the demand is visible at the stage boundary instead of inferred
after the fact.
Emission stays fused. A when is a guard; a { } is one const per name;
a chain is sequential consts inside one loop iteration. No intermediate
collection materializes — { sum: x + y } is const sum = x + y in the loop
body, not a record allocation — so chaining arm to arm and tor to tor carries
no comprehension tax. LLVM can dead-load-eliminate a field that was fetched
and ignored; a producer that reads the stage list can decline to fetch it
in the first place. That second move — the iterator emitting a stride, a
store query declining a column — is legal by the grammar’s scope rule and is
the reason the grammar was shaped this way. It is not implemented yet, and
the arm does not pretend otherwise: the pins are filter, project, rebind, and else, not pushdown.
What it settles
LINQ is the comparison everyone reaches for, and the honest version of it:
this is not more powerful than Select/Where — it is the same power in one
syntactic position. xs.Where(i => i % 2 == 0).Select(i => new { d = i * 2 }) needs three names and two lambdas for what the arm head says with grammar you
already know — when, { }, : — because the arm is where the loop was
always going to carry a body anyway. The power is in not leaving the position
you were standing in.
The dispatch rule is the same settling cond needed: ! each is parallel, ! else is the explicit opt-in, and an ! else with no ! each group fails
loud rather than guessing a chain head. One { } stage and one when per
position today — { f } { g } and doubled whens refuse with teaching
errors until the stage list generalizes. Zig is the oracle; JS and C#
templates do not lower the grammar yet, and the tree says so in the open
edges rather than by silent omission.
for(1..8)
! each i when i > 2 { sq: i * i } when sq > 10 |> std/io:print.ln("{{ sq:d }}") Bind, filter, project, filter — one arm, one loop, no intermediates. The comprehension is the arm.
The pins
Every claim above is a green regression test, not a sketch: