Open and answer this issue on GitHub.
ready · opened 2026-10-30 · updated 2026-11-03 · 5 comments
Decided (owner, 2026-10-30, third and fourth design rounds; the design round is closed). Records: ADR-async-1 (Accepted; its forms changed, see the notes under each D), ADR-async-2 (Accepted, option D; D9), ADR-async-3 (Accepted, option C; D7), ADR-async-4 (Accepted, the surface; D13, D14); ADR-346 D3, D4 changed. Specification: Part I 8.1-8.3, Part II 11-12, Part III (the version raise is the supervisor’s).
task::OverBudget, which goes to the group’s receiver. Forms state only their group’s policy: async, par_iter, select, supervisor::run, serve, async(detached) (async-3).async is an expression prefix; no async let. let x = async e, f(async g(), async h()), async a() + async b(): waited where the value is first needed; no handle, no cancel; nothing beside it is NK2110 (async-4 D1-D3).async(handle) fn { … } (group-bound task::ChildHandle, join/join(within:)/cancel, NK2122 if it leaves the group), async(deferred) (runs from h.start(), NK2121), async(detached) (free TaskHandle, = spawn) (async-4 D4-D6).h.cancel(); task::cancel, NK2113, NK2114 removed (async-4 D10).within: on async(…) is a deadline counted from the start, on every form, value or handle: when it runs out the task is cancelled and task::TimedOut arrives where the value is first needed or at the join (async-3 D7).h.join(within: d) is a limit on this wait (task::OnTimeout { Keep, Cancel }, Keep by default): by default it throws task::TimedOut, the task keeps running and the handle stays live (join again, cancel, detach); h.join(within: d, on_timeout: task::OnTimeout::Cancel) cancels the task and uses the handle up. NK2109: without on_timeout: task::OnTimeout::Cancel the handle is taken only on the success path, and in the task::TimedOut handler it must be taken on every path (async-4 D14; changes ADR-346 D3, D4).catch; a catch where it arrives (mention, join); the fallback after the async expression; the receiver. Only a cancellation the program asks for (h.cancel(), a losing select arm) runs no fallback; a deadline’s task::TimedOut is a failure and runs it (async-4 D11, D15).par_iter, whose map/filter/for_each lambdas may pause: the compiler runs pieces or one task per element; one rule set (input order, every failure kept, NK2104, NK2107); limit: replaces workers:; NK2119 warns; costs only where needed. The lambdas of fold, par_pieces, par_fold never pause (NK2209; its help on a fold names map (pausing) then fold, async-2 D9). The async loop is removed; async inside a loop body is NK2120 (async-2).async, sync, handle, deferred, detached, on_timeout, ChildHandle, start(). spawn stays reserved until #991. Part I 8.1 is “Pausing Without Marking Functions”, Part II 11.1 “Implicit Pausing and the Scheduler”; 12.4 “Racing Tasks” stays (an arm is a task).NK2113, NK2114, NK2116, NK2117; new NK2119-NK2122; NK2109 widened for join(within:).Nothing of the design. Outside it: a pending value or a handle as a select arm (ADR-async-1 §6); cancelling siblings at the first failure (ADR-292 §6).
join(within:) / on_timeout included (ready; memory half waits on #490)NK2109 for join(within:) and the NK2209 help included (needs #984)--overlaps, the two measurements (needs #986, #987)spawn → async(detached) included (needs #988, #989)overlap, task::scope, spawn (needs #990)#985 (the SharedMut bug) is independent.
Found on the way: develop’s default dev profile does not build nikaia (winnow recursion limit since 4853408); --profile fast and --release do.
Generated by Claude Code
Back to decision (owner, 2026-10-30): the concurrency concepts are not yet round. #984 and #987 are set to blocked on this issue so nobody builds a shape that may change. #985 (the SharedMut bug) stays ready; it does not depend on this.
The main session reviewed the concurrency vocabulary as it stands on develop at 0.0.1303. The owner finds async beside spawn confusing, and wants to see whether the concepts can be unified first, before any naming: fewer forms are more, and differences in meaning should be inferred by the compiler or hang on options.
| Concept | Written | Lifetime | Captures | Waited at |
|---|---|---|---|---|
| pausing (implicit) | nothing | - | - | - |
sync |
fn f() sync |
a function property: never pauses | - | - |
| async child with a value | async let x = e / = fn { … } |
its group | borrows | first mention |
| async group with a value | async let xs = for … { … } |
its group | borrows | first mention |
| async child, no value | async e, async fn { … }, async for … |
its group | borrows | group end |
| async group | async { … } |
its block | - | } |
| task | spawn fn { … } → TaskHandle |
may outlive the scope | moves | h.join(), or detached |
| race | select { p = e => … } |
the block | borrows | block, first wins |
| data parallelism | xs.par_iter().map/fold … |
the call | borrows, may not pause | the call |
| channel | channel::bounded(n) |
a value | - | recv |
| supervisor | supervisor::run([child(…)]) |
until it gives up | duplicates | - |
| connection tasks | net::serve |
per connection | - | - |
Plus task::cancel(x), h.cancel(), task::Crashed, task::OverBudget, task::TimedOut, task::Admission and task::cores().
sync and async look like a pair and are not one. sync is a function property (never pauses); async means run beside. A reader from JS, Rust or C# reads them as “colored / not colored”. Part I 8.1 is still titled “Async by Default” in the old sense (“pauses”). The owner puts naming last.task:: and for memory it is treated as a task. Part II 12.4 is titled “Racing Tasks” though a select arm is no task. Part I 8.1 says “a task exists only where the program writes one (spawn, par_iter)” and “a pause never forks one”, which async now contradicts. A possible reading, as in Swift: everything is a task, either a child task (bound to its scope, borrows) or a free task (moves, may outlive).h.cancel() and task::cancel(x) use the same mechanism.async let xs = for … and par_iter. Both are parallel loops that yield a list. With a body that never pauses, the child already runs on the pool. par_iter cuts into pieces and has fold/merge and order:; async for makes one child, with its own memory block, per element, which is expensive for a million pixels. The boundary is clear but drawn nowhere.spawn + join in the same scope versus async let. They are nearly the same. spawn must move, and its handle can be returned or stored, so it is not redundant, but within one scope async would be the better form.async fn { … } and spawn fn { … } share one shape. within: means the same on join and async. select, channels, supervisor and serve each have their own job. The boundary errors (Crashed, OverBudget, TimedOut, “only a bug cancels siblings”) are uniform.
scope.spawn fn { … } (Part II 12.5): a remnant of task::scope, and the wrong section.par_iter and async for (and further, async children and spawn tasks) become one mechanism, with the differences (pieces or one child each, borrows or moves, bound or detached, pausing or computing) inferred by the compiler or set by options?sync/async, spawn) come after that.Generated by Claude Code
Decision papers for the owner’s two questions (architect, 2026-10-30). Two records with status Open, one per question, each answerable on its own; no specification text changed, no version raise. Two records because the ADR template says a record holds one question, and the owner asked for the two to be answered one at a time.
par_iter and the async loop as one mechanism. Options: A two forms, boundary written down; B one rule set and runtime, the body’s pause property picks pieces or one child per element, par_iter stays as the chain for filter/fold/par_pieces (recommended); C only the loop, fold by a Cilk-style reducer; D only par_iter, taking pausing lambdas.select, supervisor::run, serve, the process for spawn); captures, failures, memory by one rule each; one task::cancel; one meaning of within: (recommended); C B plus one word, spawn as async(detached) (for the naming round); D lifetime inferred from use (not recommended: the unbound case cannot be inferred, captures would hang on a later line).Found while checking: xs.par_iter().for_each fn(x) { println(f"{x}") } compiles and runs with the current binary, while the same body in async for is NK2104 by Part I 8.1.2. Whatever the owner answers, the two must agree.
Measurements owed (none taken; callgrind counts only): per-element cost of par_iter with kept failures and a block reset per element; one child per element against pieces (needs #984); block set-up alone; the cliff when a body starts to pause; spawn+join against async let. Details in each record’s section 6.
Leftovers to fix once decided (in the deciding round, with the specification text):
scope.spawn fn { … } (Part II 12.5) is a task::scope remnant and the wrong section.spawn, par_iter)” contradict async.NK2104 row: “A child is not a task”.spawn returns has cancel()” (cancel spelling).NK2104: not asked of par_iter lambdas (above).NK2110 against async let xs = for … named in the next statement: ambiguous today.within: counts from the start on async (ADR-async-1 D6), from the call on join (ADR-346 D4).@detached row (“Used by spawn, defer”) if ADR-async-3 C is taken.Label stays decision. #984 and #987 stay blocked on this.
Generated by Claude Code
Recorded (architect, 2026-10-30). The owner’s third-round decisions are now in ADR-async-2 (Accepted, D), ADR-async-3 (Accepted, C), and the new ADR-async-4 (Accepted, the surface). ADR-async-1’s changed Ds have notes, and the specification is rewritten (develop 798e412, 1ead8ae). The body above has the summary and the build order. The sub-issues #984 and #986-#992 are rewritten for the new surface. #983, #984 and #987 are ready; the rest are blocked on their predecessors.
Still open: where within: counts from (ADR-async-3 §7, recommended: from where the wait starts), plus the naming round.
Generated by Claude Code
The design round is closed (owner, 2026-10-30; develop a6d1a0d). The last decisions are recorded: ADR-async-3 D7 (within: on async is a deadline from the start), ADR-async-4 D13 (names final, headings) and D14 (join(within:) limits the wait, on_timeout: cancel, the NK2109 rule; changes ADR-346 D3, D4), ADR-async-2 D9 (fold/par_pieces/par_fold lambdas never pause). Part I-III carry them; no naming placeholder is left.
Next is building, starting with #984 (parser) and #987 (std and runtime), which touch different files. The owner says when it starts.
Generated by Claude Code
ADR-async-5 is now the record for this whole topic. The sub-issues #984–#992 still cite async-4 in their titles and are built against async-5. By #1214, the async lane is built now, in Rust where the code it changes is Rust. Lane briefs: docs/briefs/async/.
Generated by Claude Code