# 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 0619–0623, 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`). - **`return` inside `inline if` fixed** (`84c2ae4f`) — the natural early-return-per-arm pattern (a `return` in an `inline if`/comptime-`case` branch inside an `inline for`) no longer drops the function's trailing statements. Lets `race` build the winner variant with a clean `inline if i == { case 0: … else: … }` per-arm form. - **`make_variant($E, idx, payload)`** added to `modules/std/meta.sx` (`1c26944e`) — the WRITE side of the metatype triad: construct a minted tagged-union value by variant INDEX (the winner is chosen at 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. **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`.