gather { … }: one ownership mechanism, and par moves to the prelude
A frame owns the workers born under it, and there is one mechanism for
that: the brood. gather { … } gives it syntax. A gather installs a brood
on the current run exactly as a worker’s base does: a spawn or watch inside
it is the gather’s, joined at its return and cancelled and drained on a halt.
A worker’s body is an implicit gather. The second ownership machine — a
Litter over call-owned births, behind the Rust par and the all form — is
deleted. par is prelude code over gather, spawn, a new pure observation
first-settled, poll and await, and keeps every property it had; all is
removed, both covering the two-ary case; first-of races fresh blocks;
race stays in Rust for handles already held.
This amends await-is-the-block-run-here: it amends I2, replaces I3–I5 and adds I4′. I1 and I6–I10 stand in letter; the owner they name changes.
Decision
-
I2, two data observations.
pollis the data-valued observation of one handle, total on an unclaimed one, andfirst-settledthe data-valued observation of several: the index of the first to settle. Neither writes, neither claims. Both treat a claimed handle asawaitdoes:pollof one fails,first-settledskips it and fails only when every handle it was given is claimed. -
I3, three owners, one of them a scope. Every concurrent block has exactly one owner, fixed at birth:
- a gather — a
gather { … }form, or a worker’s body. Its births hang under the scope current when the gather was entered (awithininside the body does not re-parent them), so a foreground interrupt or wall on the run reaches them, and it ends them at its own exit; - the session —
spawn/watchwith no brood in scope (a bare run, an aside): hangs at the session root, ended by completion,cancel, or the session’s end; - nobody —
detach.
There is no fourth owner and no lease. A
watchinside a gather is the gather’s, exactly as aspawnis. - a gather — a
-
I4, a gather ends with its children. The body ends, then each child no one has claimed is claimed and awaited in birth order with I1’s semantics, a child another claimer won being skipped; the first failure is the gather’s, and the rest are cancelled and drained within the cleanup grace. A body that halts cancels and drains them; one that panics cancels them without waiting. A worker’s base is this rule applied to its own brood. A normal return joins; it never cancels.
-
I4′, the exit drain is total. A gather’s exit also waits, bounded by the grace, for every child it started whose scope is cancelled and whose thread has not ended, however it was cancelled: the halt rule, a
cancelin the body, or an outsidecancelthroughworker <id>. A child an outside party claimed and is legitimately awaiting, uncancelled, is left to that party. -
I5, hereditary cancellation. A gather-owned birth’s node is a child of the gather’s entry scope; a session birth’s node a child of the session root.
cancel $hreaches everything$hstarted; an interrupt or a wall on a run reaches that run’s gathers’ children, never a session worker.
I6 is untouched: a gather is not a worker base and converts nothing; a gather’s child is a worker and converts as any worker does.
The surface, after:
gather BODY : control operator. BODY : U(F^ε α); the form : F^ε α
spawn [TITLE] BODY : owned by the enclosing gather if one is in scope, else the session
first-settled : ∀ε α. [Handle^ε α] → F Int index of the first to settle; claims nothing
race : ∀ε α. [Handle^ε α] → F^ε α kept; drains losers it does not own
workers : F [[…, owner: <session | worker: Int | gather>, …]]
par : ∀α β. U(α → F β) → [α] → Int → F [β] prelude
both : U(F α) → U(F β) → F [fst: α, snd: β] prelude
first-of: [U(F^ε α)] → F^ε α preludeWhy one mechanism
- The two machines were one idea. A call-owned birth already hung under
the caller’s current scope, used the run’s live structured surface, was
registered and announced nothing, and buffered its bytes per handle — what
a gather-owned birth needs.
Birth::Callis subsumed: a birth has an owner when a brood is in scope, and the owner, not the verb, decides how it is announced and delivered. - The owner announces and delivers, uniformly. A session birth is the
session’s errand: announced, its structured surface buffered to the
deferred sink, one
`doneat completion. A gather-owned birth — a worker’s child included — is registered (the cap bounds it,workerslists it), uses the run’s live surface, and emits no`done: its result reaches the session through its owner. One unit, one notice. parneeds no output-order compromise. Scheduling and claiming are different acts.first-settledsays which child settled andpollhow, neither claiming; a settled child’s bytes sit in its handle until claimed. So preludeparstarts the next item the moment any slot frees, raises the first failure the moment it sees it, and still awaits every child in input order at the end: bytes and values in input order.racestays Rust. Over handles started earlier it must drain losers that no gather owns;cancelis fire-and-forget, so onlyrace’s ownend_unclaimedbounds that drain.first-ofis the race over fresh blocks.allgoes. Not for typing — it was a form, typed field by field — but for surface economy: two syntaxes for one mechanism is what this removes.bothand thegatherpattern cover its uses.- A handle that escapes its gather is left as it is. The gather claims
the child at exit, so a later
awaitfails as already claimed: truthful and actionable, without the handle carrying its gather’s identity.
Reversed
- “A
gather { … }delimiter” was rejected by await-is-the-block-run-here as a fourth owner, redundant withpar/all/race. Both halves fall: a gather is not a fourth owner but the worker’s own scope given syntax, the same brood under a second entrance, so it replaces the call owner rather than joining it; and the redundancy argument assumedpar/all/raceas primitives, which they no longer are onceparand its siblings are prelude code overgather. One ownership mechanism beats two.
Alternatives rejected
- Deleting
race. Racing held handles would silently stop draining its losers. - A sharper refusal for an escaped handle, which needs the handle to carry its gather’s identity for one diagnostic.
allas a desugaring to thegatherpattern. Possible, and a small separate change if the record form is missed; not taken now.Option<WorkerId>orOwneras a brood’s owner. The first encodes “gather” as an absence, the second admitsSessionin a brood;BroodOwnerhas exactly the two cases.
Consequences
- A
spawninside a worker announces no birth and flushes no`doneof its own; a long worker’s internal child is visible only throughworkersuntil the worker joins it. - A brood prunes a child only once it is claimed and ended, so a cancelled-but-running child stays visible to I4′.
- I4′ is cooperative: a child that ignores cancellation outlives its gather
by the grace, as a
guardcleanup and the session wipe already accept. par’s failures name the builtin that met them (await:,spawn:), notpar: the cost of being library code.gatherjoins the reserved words;allleaves.- Amends await-is-the-block-run-here
and delimiters-are-handlers-for-poll
(its construct table); law 9 and
convert_own_stopare unchanged.
Where
Frame::Gather in core/src/evaluator/machine.rs; Brood, BroodOwner,
Cradle (Cradle::owning), Brood::wait_cancelled in
core/src/types/handle/brood.rs; exit_brood in
core/src/builtins/concurrency/birth.rs; first_ready,
builtin_first_settled in core/src/builtins/concurrency/eliminate.rs;
Owner (Owner::Gather) in core/src/types/shell/workers.rs; spawn_form
in core/src/builtins/concurrency.rs; par, both, first-of in
core/src/prelude.ral. The invariants are
a-worker-ends-with-its-children
and await-is-the-block-run-here;
the runtime narrative is cancellation; the
normative text is docs/SPEC.md §11.