5.9 KiB
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
Tasklayer, NOTcontext.io/Future. The suspending async issched.go/wait/cancelover*Task($R)(B1.4a);context.io.async→Futureis the BLOCKING impl (workers run inline → racing is meaningless there). The roadmap's "Future" maps to our*Task.raceis aScheduler/TaskUFCS function inlibrary/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 provendeclare/define/make_enum+field_count/field_name/field_typereflection (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 +.canceledstate).racecancels every loser, thenwaits each (joins) so no loser fiber outlives theracecall — structured. Reusessuspend_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)
pointeereflection builtin. Addpointee($P: Type) -> Type(core.sx + compiler). Example:pointee(*i64)=i64,field_type(pointee(*Task(i64)), 0)= the task value type. (worker+review)RaceResult($T) -> Typesynthesis. Type-fn: reflect the named-tuple$Tof*Task(..), project each element viaTaskResult, mint the tagged-union (labels → variants). Comptime-only example asserting the minted type'sfield_count/field_namematch the input tuple.Task.Valueprojection + result construction. Confirm a winner's value can be boxed into the minted variant by label/index (uses the existing variant-construction path).- 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 viasleepordering (like 1817), asserting the loser is cancelled + joined. - Positional tuple form (
._0/._1) + edge cases (already-ready task → immediate, single-task race, all-cancelled). Examples. - Validate every new example byte-identical on aarch64-macOS host AND aarch64-linux container;
full
zig build testgreen; 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) -> Typebuiltin added (f1d29876) — projects*Task(A)→Task(A).field_count/size_of/align_offold as comptime constants (2a6ef398) — so a generic($T) -> Typebuilder caninline for 0..field_count(T)and size[field_count(T)]EnumVariant. Verified: the variable-arity loop + array dim now work insideRaceResult.
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- arityRaceResultpayloads can be built. Everything up to the payload projection works.