Files
sx/current/PLAN-RACE.md

94 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PLAN-RACE — Stream B2/A1: `race` over the M:1 fiber scheduler
Carved from the async roadmap ([../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md)
§4.5, §4.6, §7 step 3). The headline A1 feature still missing after Stream B1: **`race`** — start N
async tasks, return when the FIRST completes, and **structurally** cancel + join the losers before
returning. The result is a synthesized tagged-union mirroring the input named tuple's labels.
```sx
fa := s.go(() -> A => read_a(conn)); // *Task(A)
fb := s.go(() -> B => read_b(conn)); // *Task(B)
winner := s.race((a: fa, b: fb)); // RaceResult = enum { a: A; b: B }
if winner == {
case .a: (v) { handle_a(v); } // v : A (fb cancelled + joined)
case .b: (v) { handle_b(v); } // v : B (fa cancelled + joined)
}
// positional form: s.race((fa, fb)) → tags ._0 / ._1
```
## Design decisions (grounded against the tree, 2026-06-26)
- **Built over the M:1 `Task` layer, NOT `context.io`/`Future`.** The suspending async is
`sched.go`/`wait`/`cancel` over `*Task($R)` (B1.4a); `context.io.async``Future` is the BLOCKING
impl (workers run inline → racing is meaningless there). The roadmap's "Future" maps to our
`*Task`. `race` is a `Scheduler`/`Task` UFCS function in `library/modules/std/sched.sx`.
- **The result is a comptime-synthesized nominal tagged-union** (`RaceResult`), one variant per input
tuple element: variant NAME = the tuple label (positional → `_0`/`_1`), payload = the task's result
type. Synthesis uses the proven `declare`/`define`/`make_enum` + `field_count`/`field_name`/
`field_type` reflection (examples 06190623, 0646). The input arrives as an inferred type PARAMETER
`$T` (a named tuple of `*Task(..)`), which reflects correctly (issue 0195 fixed; the tuple-*alias*
gap is issue 0196, NOT on this path).
- **Cancellation rides the existing cooperative `Task.cancel`** (sets the flag + `.canceled` state).
`race` cancels every loser, then `wait`s each (joins) so no loser fiber outlives the `race` call —
structured. Reuses `suspend_self`/`wake`; no new scheduler machinery.
## The one net-new compiler primitive (step 1)
**`pointee($P: Type) -> Type #builtin`** — given a pointer type `*X`, return `X`. This is the only
missing reflection capability: `race` must project each tuple element `*Task(A)` to its result type
`A`, and there is currently NO way to get a pointer's target type at comptime (`field_count(*X)`=0,
`type_info` has no pointer variant). With it the projection is pure sx:
```sx
TaskResult :: ($P: Type) -> Type { // P = *Task(A) → A
return field_type(pointee(P), 0); // pointee → Task(A); field 0 = `value: A`
}
```
Small + generally useful (reflection is currently complete for aggregates but blind to pointer
targets). Mirror `field_type`'s `#builtin` plumbing (`src/ir/lower/call.zig` + `src/ir/calls.zig`),
backed by the pointer TypeInfo's pointee TypeId (`src/ir/types.zig`). Lock with a comptime example.
## Steps (each: implement → lock with an example → `zig build test` green → both platforms)
1. **`pointee` reflection builtin.** Add `pointee($P: Type) -> Type` (core.sx + compiler). Example:
`pointee(*i64)` = `i64`, `field_type(pointee(*Task(i64)), 0)` = the task value type. (worker+review)
2. **`RaceResult($T) -> Type` synthesis.** Type-fn: reflect the named-tuple `$T` of `*Task(..)`,
project each element via `TaskResult`, mint the tagged-union (labels → variants). Comptime-only
example asserting the minted type's `field_count`/`field_name` match the input tuple.
3. **`Task.Value` projection + result construction.** Confirm a winner's value can be boxed into the
minted variant by label/index (uses the existing variant-construction path).
4. **Runtime `race(tasks: $T) -> RaceResult(T)`.** Suspend the caller until the first task is
`.ready`; build the winner variant; then cancel + `wait`-join every loser before returning.
Single-winner (first by completion order; FIFO tiebreak). Example: 2 tasks, deterministic winner
via `sleep` ordering (like 1817), asserting the loser is cancelled + joined.
5. **Positional tuple form** (`._0`/`._1`) + edge cases (already-ready task → immediate, single-task
race, all-cancelled). Examples.
6. **Validate** every new example byte-identical on aarch64-macOS host AND aarch64-linux container;
full `zig build test` green; adversarially review each step.
## Status
Prereqs DONE (each committed + adversarially reviewed + suite-green):
- **issue 0195** (tuple/array/vector field reflection) fixed (`8ac6c573`). Tuple reflection works on
inline + `$T`-param forms. Issue 0196 (tuple *alias*) filed, not on the critical path.
- **`pointee($P) -> Type`** builtin added (`f1d29876`) — projects `*Task(A)``Task(A)`.
- **`field_count`/`size_of`/`align_of` fold as comptime constants** (`2a6ef398`) — so a generic
`($T) -> Type` builder can `inline for 0..field_count(T)` and size `[field_count(T)]EnumVariant`.
Verified: the variable-arity loop + array dim now work inside `RaceResult`.
- **comptime type-call composition** fixed (`eb18bbc6`) — a `field_type(...)`/`pointee(...)` result is
now usable as a `Type`-typed struct-field value, a generic `$P: Type` arg, and a nested type-call
arg (incl. with an `inline for` loop-var index). The **variable-arity `RaceResult` synthesis works
end-to-end** (proven by `examples/comptime/0649-comptime-typecall-composition.sx`: reflect a named
tuple of `*Box(..)` handles → mint a tagged-union with the tuple's labels, projecting `*Box(A)``A`).
**All comptime prerequisites are now in place — the rest is pure-sx library work in sched.sx.**
**NEXT (step 24): the actual `race` in `library/modules/std/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`; build the winner variant (construct the minted union by the winning index/label —
watch for a possible variant-construction-by-dynamic-index gap); then `cancel` + `wait`-join every
loser before returning (structured). Reuses `suspend_self`/`wake` + `Task.cancel`/`wait`.
- Lock with a 2/3-task example (deterministic winner via `sleep` ordering, asserting losers cancelled).
- Validate byte-identical on aarch64-macOS host AND aarch64-linux container; full `zig build test`.