feat: race over Futures via context.io.race (PLAN-IO-UNIFY Phase 4)
Re-home the proven first-wins race from sched.race(*Task) onto *Future handles
+ the Io protocol; the old Task-based race is REPLACED (ufcs overload-by-receiver
is rejected, and only 1821 used it).
- Protocol: add Io.current_park() -> ParkToken — the running fiber as a token,
captured WITHOUT parking — so race can register the SAME coordinator across N
futures' park slots, then park once via suspend_raw; any completion readies it.
Scheduler returns {self.current} (bails outside a fiber); CBlockingIo returns
{null} (race never parks there — futures are born .ready).
- race :: ufcs (io: Io, futures: $T) -> RaceResult(T), kept in sched.sx (it needs
meta.sx's make_enum/make_variant; pulling that into the io.sx prelude part-file
would cycle). Winner scan -> register/park/deregister -> make_variant the winner
-> Phase-3 cancel each still-.pending loser (no join). RaceResult reused
unchanged (*Future(R) projects field 0 'value' -> R).
- TRUE-cancel: parked losers stop at their next suspend (timers evicted by cancel's
wake), so race returns at WINNER-time, not slowest-loser-time.
- Adversarial review fixes: (1) an all-failing/all-cancelling racer set no longer
deadlock-aborts the scheduler — race bails loudly ('all futures settled without
a winner') when nothing is .ready and nothing is still .pending; (2) only
.pending losers are cancelled, so a loser that already .failed keeps its real
outcome label instead of being stomped to .canceled.
Re-point 1821 to context.io.async + context.io.race (winner a=111, losers
.canceled, completion log only 'task 1 @ 10ms', final clock 10ms — was 30 under
the old cooperative join). New 1826 locks the failing-loser case. Byte-identical
on aarch64-macOS + aarch64-linux. Suite 853/0; .ir churn is the current_park
vtable method.
This commit is contained in:
@@ -36,6 +36,30 @@ installed via `push Context { io = xx scheduler } { … s.run(); }` — exactly
|
||||
just with the scheduler now reachable as `context.io`.
|
||||
|
||||
## Status (2026-06-28)
|
||||
- **Phase 4 — `race` over Futures via `context.io.race`. DONE.** Re-homed the
|
||||
proven first-wins race from `sched.race(*Task)` onto `*Future` handles + the
|
||||
`Io` protocol; the old Task-based `race` is REPLACED (ufcs overload-by-receiver
|
||||
is rejected — "duplicate top-level decl" — and only 1821 used it).
|
||||
- **Protocol affordance:** added `Io.current_park() -> ParkToken` (the running
|
||||
fiber as a token, captured WITHOUT parking) so race can register the SAME
|
||||
coordinator across N futures' `park` slots, then park once via `suspend_raw`;
|
||||
any completion `ready`s it. Scheduler returns `{self.current}` (bails outside
|
||||
a fiber); CBlockingIo returns `{null}` (race never parks there — futures born
|
||||
`.ready`). The await comment already anticipated this fan-in.
|
||||
- **race** (`ufcs (io: Io, futures: $T) -> RaceResult(T)`, in sched.sx — it
|
||||
needs meta.sx's `make_enum`/`make_variant`, and pulling that into the io.sx
|
||||
prelude part-file would cycle): winner scan → register+park → deregister →
|
||||
`make_variant` the winner → Phase-3 `cancel` each loser (NO join). `RaceResult`
|
||||
reused unchanged (`*Future(R)` projects field 0 `value` → R).
|
||||
- **Winner-time return:** with true cancellation the parked losers stop at their
|
||||
next suspend (their timers evicted by cancel's wake), so race returns at the
|
||||
winner's virtual time, not the slowest loser's. 1821 re-pointed to
|
||||
`context.io.async` + `context.io.race`: `winner a=111`, losers `.canceled`,
|
||||
completion log ONLY `task 1 @ 10ms`, final clock `10ms` (was 30 under the old
|
||||
cooperative join). Byte-identical on aarch64-macOS + aarch64-linux. Suite
|
||||
853/0; `.ir` churn (current_park vtable method) regenerated, only 1821 stdout
|
||||
changed otherwise.
|
||||
|
||||
- **Phase 3 — TRUE cancellation via `suspend_raw -> !`. DONE.** A cancelled async
|
||||
worker now abandons its body at its next suspend instead of running to
|
||||
completion. Pieces:
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,19 @@
|
||||
// Stream B2/A1 — structured first-wins `race` over the M:1 fiber scheduler.
|
||||
// Stream B2 — structured first-wins `race` over `context.io` (PLAN-IO-UNIFY
|
||||
// Phase 4). `context.io.race(.(a = fa, b = fb, c = fc))` takes a named tuple of
|
||||
// already-spawned `*Future(..)` handles (from `context.io.async`), SUSPENDS the
|
||||
// calling fiber until the FIRST is `.ready`, and returns a comptime-SYNTHESIZED
|
||||
// tagged-union (`RaceResult`) mirroring the tuple's labels — variant NAME = the
|
||||
// tuple label, payload = that future's result type. Here the three workers return
|
||||
// DIFFERENT types (i64 / bool / f64), so the minted union is
|
||||
// `enum { a: i64; b: bool; c: f64 }` and the winner is matched by label.
|
||||
//
|
||||
// `s.race((a: ta, b: tb, c: tc))` takes a named tuple of already-spawned
|
||||
// `*Task(..)` handles, SUSPENDS the calling fiber until the FIRST task is ready,
|
||||
// and returns a comptime-SYNTHESIZED tagged-union (`RaceResult`) mirroring the
|
||||
// tuple's labels — variant NAME = the tuple label, payload = that task's result
|
||||
// type. Here the three tasks return DIFFERENT types (i64 / bool / f64), so the
|
||||
// minted union is `enum { a: i64; b: bool; c: f64 }` and the winner is matched by
|
||||
// label. After picking the winner, `race` CANCELS and JOINS every loser, so no
|
||||
// loser fiber outlives the call (structured concurrency).
|
||||
//
|
||||
// Deterministic by virtual time (like 1817 — no real clock): the tasks sleep
|
||||
// 10/20/30 ms, so `a` (shortest) wins at t=10. Cancellation is COOPERATIVE (M:1,
|
||||
// no preemption): the losers were already parked mid-`sleep` when cancelled, so
|
||||
// they cannot be preempted — `race` joins them, letting each run to its natural
|
||||
// end (its value discarded) before returning. The completion log therefore shows
|
||||
// all three finishing (a@10 winner, b@20, c@30 joined) and the final virtual
|
||||
// clock is 30. Each loser's `canceled` flag is set and its worker `finished`.
|
||||
// TRUE cancellation (Phase 3): the workers sleep 10/20/30 ms (deterministic
|
||||
// virtual clock), so `a` wins at t=10. The losers `b`/`c` are parked mid-`sleep`
|
||||
// when cancelled; their next `suspend_raw` raises `Canceled` and unwinds the body,
|
||||
// so their POST-SLEEP `rec(...)` NEVER runs and `race` returns at WINNER-time. The
|
||||
// completion log therefore shows ONLY `a @ 10ms`, and the final virtual clock is
|
||||
// 10 — NOT 30 (the old cooperative-join behaviour that let losers run to their
|
||||
// natural end). The losers end `.canceled` with their work stopped.
|
||||
//
|
||||
// aarch64-pinned (the scheduler's per-arch asm + per-OS mmap/event constants):
|
||||
// runs end-to-end on a matching host (macOS + linux), ir-only on a mismatch.
|
||||
@@ -31,25 +29,28 @@ main :: () -> i64 {
|
||||
ps := @s; pl := @lg;
|
||||
|
||||
// The coordinator runs as a fiber so `race` has a `current` to park.
|
||||
s.spawn(() => {
|
||||
// Three async tasks with DIFFERENT result types and sleep durations.
|
||||
a := ps.go(() -> i64 => { ps.sleep(10); rec(pl, 1, ps.now_ms()); 111 });
|
||||
b := ps.go(() -> bool => { ps.sleep(20); rec(pl, 2, ps.now_ms()); true });
|
||||
c := ps.go(() -> f64 => { ps.sleep(30); rec(pl, 3, ps.now_ms()); 2.5 });
|
||||
push .{ io = xx s } {
|
||||
ps.spawn(() => {
|
||||
// Three async workers, DIFFERENT result types and sleep durations.
|
||||
a := context.io.async(() -> (i64, !) => { try context.io.sleep(10); rec(pl, 1, context.io.now_ms()); 111 });
|
||||
b := context.io.async(() -> (bool, !) => { try context.io.sleep(20); rec(pl, 2, context.io.now_ms()); true });
|
||||
c := context.io.async(() -> (f64, !) => { try context.io.sleep(30); rec(pl, 3, context.io.now_ms()); 2.5 });
|
||||
|
||||
// Race them. `a` (sleep 10) wins; `b` and `c` are cancelled + joined.
|
||||
winner := ps.race(.(a = a, b = b, c = c));
|
||||
if winner == {
|
||||
case .a: (v) { print("winner: a (i64) = {}\n", v); }
|
||||
case .b: (v) { print("winner: b (bool) = {}\n", v); }
|
||||
case .c: (v) { print("winner: c (f64) = {}\n", v); }
|
||||
}
|
||||
// Race them. `a` (sleep 10) wins; `b` and `c` are cancelled — their
|
||||
// post-sleep work never runs (true cancellation).
|
||||
winner := context.io.race(.(a = a, b = b, c = c));
|
||||
if winner == {
|
||||
case .a: (v) { print("winner: a (i64) = {}\n", v); }
|
||||
case .b: (v) { print("winner: b (bool) = {}\n", v); }
|
||||
case .c: (v) { print("winner: c (f64) = {}\n", v); }
|
||||
}
|
||||
|
||||
// The losers were cancelled (flag set) and joined (worker finished).
|
||||
print("loser b: canceled={} finished={}\n", b.canceled, b.finished);
|
||||
print("loser c: canceled={} finished={}\n", c.canceled, c.finished);
|
||||
});
|
||||
s.run();
|
||||
// The losers were cancelled; their work was stopped at the suspend.
|
||||
print("loser b: canceled={}\n", b.state == .canceled);
|
||||
print("loser c: canceled={}\n", c.state == .canceled);
|
||||
});
|
||||
ps.run();
|
||||
}
|
||||
|
||||
print("completion order (id @ virtual-ms):\n");
|
||||
i := 0;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Stream B2 — `context.io.race` tolerates a FAILING racer (PLAN-IO-UNIFY Phase 4).
|
||||
// A `race` is first-SUCCESS-wins: a racer that ends `.failed` is simply not a
|
||||
// winner candidate; as long as ANOTHER racer succeeds, `race` returns that winner.
|
||||
// Here `a` raises at t=5 and `b` succeeds (42) at t=10, so `b` wins. The failed
|
||||
// racer keeps its real outcome label (`.failed`) — `race` only cancels still-
|
||||
// in-flight (`.pending`) losers, so it never stomps `a`'s `.failed` to `.canceled`.
|
||||
//
|
||||
// (Regression: an all-FAILING racer set instead bails loudly — "race — all
|
||||
// futures settled without a winner" — rather than dead-locking the scheduler.)
|
||||
//
|
||||
// aarch64-pinned (the scheduler's per-arch asm): runs end-to-end on a matching
|
||||
// host (macOS + linux), ir-only on a mismatch.
|
||||
#import "modules/std.sx";
|
||||
sched :: #import "modules/std/sched.sx";
|
||||
|
||||
main :: () -> i64 {
|
||||
s := sched.Scheduler.init();
|
||||
ps := @s;
|
||||
push .{ io = xx s } {
|
||||
ps.spawn(() => {
|
||||
a := context.io.async(() -> (i64, !) => { try context.io.sleep(5); raise error.Boom; });
|
||||
b := context.io.async(() -> (i64, !) => { try context.io.sleep(10); 42 });
|
||||
winner := context.io.race(.(a = a, b = b));
|
||||
if winner == {
|
||||
case .a: (v) { print("winner: a = {}\n", v); }
|
||||
case .b: (v) { print("winner: b = {}\n", v); }
|
||||
}
|
||||
// The failing loser keeps its real outcome — not stomped to .canceled.
|
||||
print("a: failed={} canceled={}\n", a.state == .failed, a.state == .canceled);
|
||||
});
|
||||
ps.run();
|
||||
}
|
||||
print("final clock: {}ms\n", s.now_ms());
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
||||
winner: a (i64) = 111
|
||||
loser b: canceled=1 finished=1
|
||||
loser c: canceled=1 finished=1
|
||||
loser b: canceled=true
|
||||
loser c: canceled=true
|
||||
completion order (id @ virtual-ms):
|
||||
task 1 @ 10ms
|
||||
task 2 @ 20ms
|
||||
task 3 @ 30ms
|
||||
final virtual clock: 30ms
|
||||
final virtual clock: 10ms
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "target": "macos" }
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
winner: b = 42
|
||||
a: failed=true canceled=false
|
||||
final clock: 10ms
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -128,6 +128,13 @@ Io :: protocol #inline {
|
||||
poll :: (self: *Self, deadline_ms: i64) -> i64;
|
||||
now_ms :: (self: *Self) -> i64;
|
||||
arm_timer :: (self: *Self, deadline_ms: i64, park: ParkToken) -> *void;
|
||||
// `current_park()` — a `ParkToken` identifying the CURRENTLY-running execution
|
||||
// context, so a fan-in waiter (`race`) can register the SAME awaiter across
|
||||
// several futures' `park` slots before parking once. A suspending impl
|
||||
// returns `{ handle = <current fiber> }`; the blocking impl has no fiber and
|
||||
// returns `{ handle = null }` (race never parks there — its futures are born
|
||||
// `.ready`). Unlike `suspend_raw`, this captures the awaiter WITHOUT parking.
|
||||
current_park :: (self: *Self) -> ParkToken;
|
||||
}
|
||||
|
||||
// --- Context ---
|
||||
|
||||
@@ -73,6 +73,12 @@ impl Io for CBlockingIo {
|
||||
arm_timer :: (self: *CBlockingIo, deadline_ms: i64, park: ParkToken) -> *void {
|
||||
return null;
|
||||
}
|
||||
// No fibers in the blocking model — there is no current execution context to
|
||||
// register as a fan-in waiter. `race`'s futures are born `.ready` here, so it
|
||||
// finds a winner without ever parking; this null token is never consulted.
|
||||
current_park :: (self: *CBlockingIo) -> ParkToken {
|
||||
return .{ handle = null };
|
||||
}
|
||||
}
|
||||
|
||||
// --- Future($R): the handle to an async task's eventual result ---
|
||||
|
||||
@@ -765,6 +765,20 @@ impl Io for Scheduler {
|
||||
self.timers.append(t, self.own_allocator);
|
||||
return null;
|
||||
}
|
||||
|
||||
// The running fiber as a `ParkToken`, for fan-in registration (`race`): the
|
||||
// caller stamps this handle into several futures' `park` slots so ANY of
|
||||
// their completions `ready`s it, then parks once via `suspend_raw`. MUST be
|
||||
// called from inside a fiber (race parks `current`); a null current would
|
||||
// register a `null` waiter no completion can wake — bail loudly, mirroring
|
||||
// `suspend_self` / `sleep` / `arm_timer`.
|
||||
current_park :: (self: *Scheduler) -> ParkToken {
|
||||
if self.current == null {
|
||||
print("sched: current_park() called outside a fiber (no running fiber)\n");
|
||||
abort();
|
||||
}
|
||||
return .{ handle = xx self.current };
|
||||
}
|
||||
}
|
||||
|
||||
// --- the context switch (naked) + first-entry trampoline -------------------
|
||||
@@ -1154,112 +1168,120 @@ cancel :: ufcs (t: *Task($R)) {
|
||||
t.state = .canceled;
|
||||
}
|
||||
|
||||
// --- B2/A1: structured first-wins `race` over the M:1 Task layer -------------
|
||||
// --- B2/A1: structured first-wins `race` over `context.io` Futures -----------
|
||||
//
|
||||
// `race((a: ta, b: tb, …))` starts from N already-spawned `*Task(..)` handles,
|
||||
// returns when the FIRST completes, and STRUCTURALLY cancels + joins the losers
|
||||
// before returning — no loser fiber outlives the call. The result is a
|
||||
// comptime-synthesized tagged-union (`RaceResult`) mirroring the input tuple's
|
||||
// labels: each variant's NAME is the tuple label, its payload is that task's
|
||||
// result type. The tuple must be NAMED (`(a: ta, b: tb)`); a positional-tuple
|
||||
// form (`._0`/`._1` variants) is future work — `field_name` yields "" for an
|
||||
// unnamed element, which `make_enum` rejects as a duplicate-name collision.
|
||||
// `context.io.race((a: fa, b: fb, …))` starts from N already-spawned `*Future(..)`
|
||||
// handles (from `context.io.async`), returns when the FIRST is `.ready`, and
|
||||
// CANCELS every loser before returning — with Phase-3 TRUE cancellation each loser
|
||||
// stops at its next suspend, so `race` returns at WINNER-time, not slowest-loser-
|
||||
// time. The result is a comptime-synthesized tagged-union (`RaceResult`) mirroring
|
||||
// the input tuple's labels: each variant's NAME is the tuple label, its payload is
|
||||
// that future's result type. The tuple must be NAMED (`(a: fa, b: fb)`); a
|
||||
// positional-tuple form (`._0`/`._1`) is future work — `field_name` yields "" for
|
||||
// an unnamed element, which `make_enum` rejects as a duplicate-name collision.
|
||||
//
|
||||
// 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) {…} case .b: (v) {…} } // loser cancelled+joined
|
||||
// It runs over the `Io` PROTOCOL (`current_park`/`suspend_raw`/`ready`), so it is
|
||||
// colorblind: under the fiber scheduler it really parks/wakes; under the blocking
|
||||
// `CBlockingIo` every future is born `.ready`, so the winner scan returns
|
||||
// immediately and it never parks. Lives here (not io.sx) because the `RaceResult`
|
||||
// synthesis needs the metatype WRITE side (`make_enum`/`make_variant`), and
|
||||
// meta.sx imports only std.sx — pulling it into the io.sx prelude part-file would
|
||||
// cycle.
|
||||
//
|
||||
// fa := context.io.async(() -> (A, !) => read_a(conn)); // *Future(A)
|
||||
// fb := context.io.async(() -> (B, !) => read_b(conn)); // *Future(B)
|
||||
// winner := context.io.race(.(a = fa, b = fb)); // enum { a: A; b: B }
|
||||
// if winner == { case .a: (v) {…} case .b: (v) {…} } // losers cancelled
|
||||
|
||||
// Synthesize the race RESULT type for a named tuple `$T` of `*Task(..)` handles.
|
||||
// `*Task(R)` projects to its result `R` via `field_type(pointee(field_type(T, i)), 0)`:
|
||||
// `field_type(T, i)` = `*Task(R)`, `pointee` strips the pointer to `Task(R)`, and
|
||||
// field 0 of `Task` is `value: R`. One nominal type per distinct `T` (type-fn
|
||||
// identity), so the decl, every `make_variant(RaceResult(T), …)` call, and the
|
||||
// `-> RaceResult(T)` return all name the SAME union. (The 0649 composition shape,
|
||||
// with `Task` in place of the stand-in `Box`.)
|
||||
// Synthesize the race RESULT type for a named tuple `$T` of `*Future(..)` handles.
|
||||
// `*Future(R)` projects to its result `R` via `field_type(pointee(field_type(T, i)), 0)`:
|
||||
// `field_type(T, i)` = `*Future(R)`, `pointee` strips the pointer to `Future(R)`,
|
||||
// and field 0 of `Future` is `value: R` (the `Value :: R` type member is not a
|
||||
// data field). One nominal type per distinct `T` (type-fn identity), so the decl,
|
||||
// every `make_variant(RaceResult(T), …)` call, and the `-> RaceResult(T)` return
|
||||
// all name the SAME union.
|
||||
RaceResult :: ($T: Type) -> Type {
|
||||
vs : [field_count(T)]EnumVariant = ---;
|
||||
inline for 0..field_count(T) (i) {
|
||||
vs[i] = EnumVariant.{
|
||||
name = field_name(T, i), // tuple label → variant name
|
||||
payload = field_type(pointee(field_type(T, i)), 0), // *Task(R) -> Task(R) -> R
|
||||
payload = field_type(pointee(field_type(T, i)), 0), // *Future(R) -> Future(R) -> R
|
||||
};
|
||||
}
|
||||
return make_enum("RaceResult", vs[0..field_count(T)]);
|
||||
}
|
||||
|
||||
// Structured first-wins race. Suspends the calling fiber until the FIRST task is
|
||||
// `.ready`, builds a `RaceResult(T)` carrying that winner's value, then CANCELS
|
||||
// and JOINS every loser before returning.
|
||||
// Structured first-wins race over the `Io` protocol. Suspends the calling fiber
|
||||
// until the FIRST future is `.ready`, builds a `RaceResult(T)` carrying that
|
||||
// winner's value, then CANCELS every loser and returns immediately.
|
||||
//
|
||||
// MUST be called from inside a fiber (there must be a `current` to park), like
|
||||
// `wait`/`sleep`; a null `current` bails loudly rather than dereferencing null.
|
||||
// MUST be called from inside a fiber under a suspending `Io` (there must be a
|
||||
// `current` to park) — `current_park` bails loudly on a null current. Under
|
||||
// `CBlockingIo` the futures are already `.ready`, so the winner scan returns
|
||||
// without ever calling `current_park`/`suspend_raw`.
|
||||
//
|
||||
// COOPERATIVE-CANCEL SEMANTIC (M:1, no preemption): a loser already past its
|
||||
// work's first line cannot be preempted — `cancel` sets its flag and the JOIN
|
||||
// waits for the worker to reach its natural end (the value is discarded). A loser
|
||||
// that had not yet started skips its work entirely (`go`'s `if t.canceled == 0`
|
||||
// guard). Either way `race` returns only once every loser's worker has `finished`,
|
||||
// so no loser fiber is still live past the call (structured concurrency).
|
||||
race :: ufcs (self: *Scheduler, tasks: $T) -> RaceResult(T) {
|
||||
cur := self.current;
|
||||
if cur == null {
|
||||
print("sched: race() called outside a fiber (no running fiber)\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
// Phase 1 — first winner. Scan for an already-`.ready` task (lowest index
|
||||
// wins on a same-tick tie → deterministic). If none, register the caller as
|
||||
// the waiter on every still-`.pending` task and park. On wake DEREGISTER from
|
||||
// ALL of them: a later loser completion must never wake the caller again — by
|
||||
// the time it fires the caller may be running or parked on a different join,
|
||||
// and a stale waiter-wake would be a spurious/lost wakeup (the queue-corruption
|
||||
// hazard `wake` guards). A spurious wake with nothing ready re-registers and
|
||||
// re-parks.
|
||||
// TRUE-CANCEL SEMANTIC (Phase 3): each loser was parked mid-suspend when cancelled;
|
||||
// `cancel(f)` flips its sticky flag and wakes its worker fiber, whose next
|
||||
// `suspend_raw` raises `Canceled` and unwinds the body — its post-suspend work
|
||||
// never runs. `race` does NOT join the losers (they unwind on their own next turn),
|
||||
// so it returns at winner-time. The winner's value is taken from `f.value`.
|
||||
race :: ufcs (io: Io, futures: $T) -> RaceResult(T) {
|
||||
// Phase 1 — first winner. Scan for an already-`.ready` future (lowest index
|
||||
// wins on a same-tick tie → deterministic). If none, register THIS coordinator
|
||||
// (`current_park`) as the waiter on every still-`.pending` future's `park`
|
||||
// slot and park once via `suspend_raw`; any completion `ready`s us. On wake
|
||||
// DEREGISTER from ALL of them (clear our handle): a later loser completion must
|
||||
// never `ready` a coordinator that has since moved on (the spurious/lost-wakeup
|
||||
// hazard `wake` guards). A spurious wake with nothing ready re-registers + re-parks.
|
||||
winner_idx : i64 = -1;
|
||||
while winner_idx < 0 {
|
||||
inline for 0..field_count(T) (i) {
|
||||
if winner_idx < 0 and tasks[i].state == .ready { winner_idx = i; }
|
||||
if winner_idx < 0 and futures[i].state == .ready { winner_idx = i; }
|
||||
}
|
||||
if winner_idx >= 0 { break; }
|
||||
me := io.current_park();
|
||||
any_pending := false;
|
||||
inline for 0..field_count(T) (i) {
|
||||
if tasks[i].state == .pending { tasks[i].waiter = xx cur; }
|
||||
if futures[i].state == .pending { futures[i].park.handle = me.handle; any_pending = true; }
|
||||
}
|
||||
self.suspend_self();
|
||||
// No `.ready` winner and nothing still `.pending` → every racer settled
|
||||
// `.failed`/`.canceled` with no success. `race` is first-SUCCESS-wins and
|
||||
// its `RaceResult` carries only success values, so there is no winner to
|
||||
// return. Parking here would deadlock (no future can ever `ready` us);
|
||||
// bail loudly with a specific message instead. (A recoverable all-fail —
|
||||
// a failable `race` that raises — is a deliberate future refinement.)
|
||||
if !any_pending {
|
||||
print("sched: race — all futures settled without a winner (all failed/canceled)\n");
|
||||
abort();
|
||||
}
|
||||
// The coordinator is the user's fiber (no cancel flag), so `suspend_raw`
|
||||
// never raises here; the `catch {}` just discards the `!` for the type.
|
||||
pk : ParkToken = .{ handle = null };
|
||||
io.suspend_raw(@pk) catch {};
|
||||
inline for 0..field_count(T) (i) {
|
||||
if tasks[i].waiter == (xx cur) { tasks[i].waiter = null; }
|
||||
if futures[i].park.handle == me.handle { futures[i].park.handle = null; }
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2 — build the winner variant. `tasks[i].value` carries the CONCRETE
|
||||
// Phase 2 — build the winner variant. `futures[i].value` carries the CONCRETE
|
||||
// result type of variant `i` (comptime-cursor tuple indexing), so the matching
|
||||
// unrolled arm constructs `RaceResult(T)`'s i-th variant directly — no nested
|
||||
// `inline if` to recover the payload type. `i` is the comptime cursor; the
|
||||
// runtime `if i == winner_idx` selects the one arm that fires.
|
||||
// unrolled arm constructs `RaceResult(T)`'s i-th variant directly. `i` is the
|
||||
// comptime cursor; the runtime `if i == winner_idx` selects the one arm.
|
||||
result : RaceResult(T) = ---;
|
||||
inline for 0..field_count(T) (i) {
|
||||
if i == winner_idx {
|
||||
result = make_variant(RaceResult(T), i, tasks[i].value);
|
||||
result = make_variant(RaceResult(T), i, futures[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3 — cancel + JOIN every loser, one at a time. `cancel` sets the flag;
|
||||
// the join then ensures the loser's worker fiber has `finished` (not merely
|
||||
// been flagged): if it has not, register as ITS sole waiter and park until the
|
||||
// worker's tail wakes us (the worker sets `finished = 1` and wakes its waiter
|
||||
// whether it ran the work or skipped it on an early cancel). Checking
|
||||
// `finished` BEFORE parking avoids a lost wakeup (mirrors `wait` checking
|
||||
// `.ready`). Only the loser being joined has a registered waiter, so no other
|
||||
// task's completion can wake us mid-join.
|
||||
// Phase 3 — cancel every still-IN-FLIGHT loser. With true cancellation a
|
||||
// parked loser's next `suspend_raw` raises `Canceled` and unwinds its body;
|
||||
// its `park` was cleared above, so its completion `ready`s nobody. No join —
|
||||
// `race` returns now. Only `.pending` losers are cancelled: a loser that
|
||||
// already settled (`.ready`/`.failed`) is done — cancelling it would do
|
||||
// nothing useful and would stomp its real outcome label to `.canceled`.
|
||||
inline for 0..field_count(T) (i) {
|
||||
if i != winner_idx {
|
||||
tasks[i].cancel();
|
||||
if tasks[i].finished == 0 {
|
||||
tasks[i].waiter = xx cur;
|
||||
self.suspend_self();
|
||||
if tasks[i].waiter == (xx cur) { tasks[i].waiter = null; }
|
||||
}
|
||||
}
|
||||
if i != winner_idx and futures[i].state == .pending { futures[i].cancel(); }
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user