Files
sx/current/PLAN-RACE.md
agra dea96bd66a docs: PLAN-RACE (race over M:1 tasks) + file issue 0196
Plan the `race` async deliverable (roadmap A1): a structured first-wins over
the M:1 `Task` layer, returning a comptime-synthesized tagged-union mirroring
the input named tuple's labels. Identifies the one net-new compiler primitive
needed — `pointee($P: Type) -> Type` (project `*Task(A)` → `A`); everything else
(tuple reflection, union synthesis, the suspending runtime) is already in place.

Issue 0196: a named-tuple type ALIAS (`NT :: Tuple(a: i64, b: bool)`) loses its
structure — field access and reflection both fail on the alias, while the inline
and `$T`-type-parameter forms work. Not on the race critical path (race reflects
a tuple type parameter), filed for tracking.
2026-06-26 12:34:13 +03:00

4.7 KiB
Raw Blame History

PLAN-RACE — Stream B2/A1: race over the M:1 fiber scheduler

Carved from the async roadmap (../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.

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.asyncFuture 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 waits 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:

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

  • Step 0 (prereq) DONE: issue 0195 (tuple/array/vector field reflection) fixed + committed (8ac6c573). Tuple reflection works on inline + $T-param forms. Issue 0196 (tuple alias) filed, not on the critical path.
  • NEXT: Step 1 — the pointee builtin.