Files
sx/current/PLAN-RACE.md

89 lines
5.9 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`.
**NEXT BLOCKER (step 2, not yet resolved): nested comptime-type-call composition.** Building each
variant payload needs `*Task(A) → A`, i.e. `field_type(pointee(field_type(T, i)), 0)`. Today:
- passing a `field_type(...)` RESULT as the type-arg to a generic (`TaskResult(field_type(T, i))`) →
*"cannot infer generic type parameter 'P'"*;
- the same nested inline (`field_type(pointee(field_type(T, i)), 0)`) → *"cannot infer 'T' for
field_type"*;
- a `::` alias bound to a comptime-type-call result (`P0 :: field_type(NT, 0)`) → *"unresolved type
'P0'"* (kin to issue 0196).
So a comptime-type-call's Type result isn't usable as a generic type-arg / `::` alias / nested
type-call arg. This composition gap is the next thing to fix (or design around) before the variable-
arity `RaceResult` payloads can be built. Everything up to the payload projection works.