docs: PLAN-RACE — race runtime DONE (GAP 1/2 fixed, 826/0, both platforms)

This commit is contained in:
agra
2026-06-26 18:08:03 +03:00
parent 9099735e88
commit 3d8f9ca094

View File

@@ -90,23 +90,49 @@ Prereqs DONE (each committed + adversarially reviewed + suite-green):
runtime; its label can't be a literal). Pure sx (writes the i64 tag@0 + payload@8). Verified for
complex payloads (struct / string / 40-byte struct). **This resolves the variant-construction gap.**
**ONE remaining blocker for the runtime: GAP 1 — comptime-cursor indexing of a named-tuple VALUE.**
`race(tasks: $T)` must read the i-th task `*Task(T_i)` with its concrete type where `i` is the
`inline for` cursor. Today `tasks[i]`*"cannot index a value of type '(…)'"* and
`field_value(tasks, i)` returns a **void** Any (tuple field-VALUE reflection is unimplemented — the
0195 family covered count/name/type but not value). Fix options: (a) make `tasks[i]` work with a
comptime cursor on a named-tuple value (mirror packs' `xs[i]`), or (b) implement `field_value` for
tuples + recover the concrete type via `field_type(T, i)`. (a) is cleaner for `race` (direct typed
access). This is the last thing between here and the pure-sx runtime.
**GAP 1 — comptime-cursor indexing of a named-tuple VALUE — DONE** (`fee86adf`). `tasks[i]` with a
comptime cursor now reads the i-th element with its concrete type (option (a), a `structGet`).
**THEN (pure-sx, unblocked once GAP 1 lands): the runtime in `sched.sx`.**
- `RaceResult :: ($T) -> Type` over `*Task(..)` (the 0649 shape, with `Task` instead of `Box`).
- `race :: ufcs (self: *Scheduler, tasks: $T) -> RaceResult(T)`: suspend the caller until the FIRST
task is `.ready` (register caller as waiter on all pending; on wake DEREGISTER from all to avoid a
double-wake of the merge — the queue-corruption hazard `wake` guards); winner = first ready;
build it with `make_variant` in the matching `inline for` arm; then `cancel` + JOIN every loser
(needs a `Task.finished` flag set at the end of the `go` body so the joiner distinguishes a
finished worker from a merely-flagged-cancelled one, checked before parking like `wait` checks
`.ready`). Reuses `suspend_self`/`wake`.
- Lock with a 2/3-task example (deterministic winner via `sleep` ordering; assert losers cancelled).
- Validate byte-identical on aarch64-macOS host AND aarch64-linux container; full `zig build test`.
**GAP 2 — surfaced during the runtime, all DONE** (`6a976287`): three compiler enablers the runtime
needed beyond the read path. (1) a named-tuple LITERAL passed directly as `$T` lost its element names
(`field_name` → ""), breaking `RaceResult`'s `make_enum`; (2) tuple-element L-VALUES by comptime index
(`tasks[i].waiter = …`) panicked at LLVM emit (an `index_gep` with `ptrTo(.unresolved)`); (3) a user
`($X) -> Type` call couldn't bind a `$E: Type` arg, blocking `make_variant(RaceResult(T), …)`. All
fixed + adversarially reviewed; OOB comptime tuple indices now diagnose loudly on every L-value path.
**Runtime in `sched.sx` — DONE** (`9099735e`).
- `RaceResult :: ($T) -> Type` over `*Task(..)` (the 0649 shape, with `Task`).
- `race :: ufcs (self: *Scheduler, tasks: $T) -> RaceResult(T)`: Phase 1 suspend until the FIRST
`.ready` (register waiter on all pending; on wake DEREGISTER from all; lowest-index winner). Phase 2
build the winner with `make_variant`. Phase 3 cancel + JOIN each loser one-at-a-time (only the joined
loser carries a waiter → no mid-join double-wake). Join rides a new `Task.finished` flag (set at the
end of the `go` body, checked before parking). Cooperative-cancel: a loser parked mid-`sleep` runs to
its natural end before `race` returns.
- Locked by `examples/concurrency/1821-concurrency-fiber-race.sx` (3 tasks i64/bool/f64, sleep 10/20/30,
shortest wins, losers cancelled + joined). Byte-identical on aarch64-macOS host AND aarch64-linux
container; full `zig build test` green (826/0).
**Remaining (step 5, future work):**
- POSITIONAL tuple form (`._0`/`._1`): `field_name` yields "" for an unnamed element → `make_enum`
rejects the duplicate. Needs a `_N` fallback in `RaceResult` (a comptime int→string) or a compiler
`field_name` default for positional tuples.
- Bare named-arg call form `s.race(a = ta, b = tb)` (no `.(…)` wrapper) — see the note below.
- Edge cases: already-ready-at-call (works today), all-cancelled (would deadlock-abort loudly).
- By-design caveat to document: `race` returns only when the SLOWEST loser finishes (cooperative
cancel can't preempt a mid-`sleep` loser; a loser blocked on `block_on_fd` that never fires blocks
the join forever — `cancel` doesn't unblock an fd waiter).
### Future work — bare named-arg call form `s.race(a = ta, b = tb)`
Today the result-tuple is passed as a named-tuple LITERAL: `s.race(.(a = ta, b = tb))`. The user asked
about dropping the `.(…)` so it reads `s.race(a = ta, b = tb)`. That is a CALL-SITE syntax/binding
feature, independent of the race runtime:
- The parser would have to accept `name = expr` call arguments and, for a `(tasks: $T)` parameter whose
type is inferred, MATERIALIZE them into a single named-tuple value bound to `$T` (rather than treating
each `name = expr` as a separate positional/keyword arg). Effectively "collect trailing `k = v` call
args into one anonymous named-tuple when the callee has a single inferred aggregate param".
- Risk: `name = expr` in call position currently has no meaning (or would collide with a future
keyword-argument feature). Decide whether `k = v` call args are (a) always tuple-materialized for an
inferred aggregate param, or (b) a dedicated `..` / sugar. Option (a) is the least new syntax.
- Once the call site produces the same named-tuple value, the entire race runtime is unchanged (it
already reflects `$T`). So this is purely front-end sugar — schedule it with the keyword-args work,
not the concurrency stream.