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. poll is the data-valued observation of one handle, total on an unclaimed one, and first-settled the data-valued observation of several: the index of the first to settle. Neither writes, neither claims. Both treat a claimed handle as await does: poll of one fails, first-settled skips 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 (a within inside 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/watch with 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 watch inside a gather is the gather’s, exactly as a spawn is.

  • 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 cancel in the body, or an outside cancel through worker <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 $h reaches everything $h started; 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^ε α                    prelude

Why 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::Call is 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 `done at completion. A gather-owned birth — a worker’s child included — is registered (the cap bounds it, workers lists it), uses the run’s live surface, and emits no `done: its result reaches the session through its owner. One unit, one notice.
  • par needs no output-order compromise. Scheduling and claiming are different acts. first-settled says which child settled and poll how, neither claiming; a settled child’s bytes sit in its handle until claimed. So prelude par starts 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.
  • race stays Rust. Over handles started earlier it must drain losers that no gather owns; cancel is fire-and-forget, so only race’s own end_unclaimed bounds that drain. first-of is the race over fresh blocks.
  • all goes. Not for typing — it was a form, typed field by field — but for surface economy: two syntaxes for one mechanism is what this removes. both and the gather pattern cover its uses.
  • A handle that escapes its gather is left as it is. The gather claims the child at exit, so a later await fails 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 with par/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 assumed par/all/race as primitives, which they no longer are once par and its siblings are prelude code over gather. 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.
  • all as a desugaring to the gather pattern. Possible, and a small separate change if the record form is missed; not taken now.
  • Option<WorkerId> or Owner as a brood’s owner. The first encodes “gather” as an absence, the second admits Session in a brood; BroodOwner has exactly the two cases.

Consequences

  • A spawn inside a worker announces no birth and flushes no `done of its own; a long worker’s internal child is visible only through workers until 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 guard cleanup and the session wipe already accept.
  • par’s failures name the builtin that met them (await:, spawn:), not par: the cost of being library code.
  • gather joins the reserved words; all leaves.
  • Amends await-is-the-block-run-here and delimiters-are-handlers-for-poll (its construct table); law 9 and convert_own_stop are 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.