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`.
|
just with the scheduler now reachable as `context.io`.
|
||||||
|
|
||||||
## Status (2026-06-28)
|
## 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
|
- **Phase 3 — TRUE cancellation via `suspend_raw -> !`. DONE.** A cancelled async
|
||||||
worker now abandons its body at its next suspend instead of running to
|
worker now abandons its body at its next suspend instead of running to
|
||||||
completion. Pieces:
|
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
|
// TRUE cancellation (Phase 3): the workers sleep 10/20/30 ms (deterministic
|
||||||
// `*Task(..)` handles, SUSPENDS the calling fiber until the FIRST task is ready,
|
// virtual clock), so `a` wins at t=10. The losers `b`/`c` are parked mid-`sleep`
|
||||||
// and returns a comptime-SYNTHESIZED tagged-union (`RaceResult`) mirroring the
|
// when cancelled; their next `suspend_raw` raises `Canceled` and unwinds the body,
|
||||||
// tuple's labels — variant NAME = the tuple label, payload = that task's result
|
// so their POST-SLEEP `rec(...)` NEVER runs and `race` returns at WINNER-time. The
|
||||||
// type. Here the three tasks return DIFFERENT types (i64 / bool / f64), so the
|
// completion log therefore shows ONLY `a @ 10ms`, and the final virtual clock is
|
||||||
// minted union is `enum { a: i64; b: bool; c: f64 }` and the winner is matched by
|
// 10 — NOT 30 (the old cooperative-join behaviour that let losers run to their
|
||||||
// label. After picking the winner, `race` CANCELS and JOINS every loser, so no
|
// natural end). The losers end `.canceled` with their work stopped.
|
||||||
// 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`.
|
|
||||||
//
|
//
|
||||||
// aarch64-pinned (the scheduler's per-arch asm + per-OS mmap/event constants):
|
// 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.
|
// 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;
|
ps := @s; pl := @lg;
|
||||||
|
|
||||||
// The coordinator runs as a fiber so `race` has a `current` to park.
|
// The coordinator runs as a fiber so `race` has a `current` to park.
|
||||||
s.spawn(() => {
|
push .{ io = xx s } {
|
||||||
// Three async tasks with DIFFERENT result types and sleep durations.
|
ps.spawn(() => {
|
||||||
a := ps.go(() -> i64 => { ps.sleep(10); rec(pl, 1, ps.now_ms()); 111 });
|
// Three async workers, DIFFERENT result types and sleep durations.
|
||||||
b := ps.go(() -> bool => { ps.sleep(20); rec(pl, 2, ps.now_ms()); true });
|
a := context.io.async(() -> (i64, !) => { try context.io.sleep(10); rec(pl, 1, context.io.now_ms()); 111 });
|
||||||
c := ps.go(() -> f64 => { ps.sleep(30); rec(pl, 3, ps.now_ms()); 2.5 });
|
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.
|
// Race them. `a` (sleep 10) wins; `b` and `c` are cancelled — their
|
||||||
winner := ps.race(.(a = a, b = b, c = c));
|
// post-sleep work never runs (true cancellation).
|
||||||
|
winner := context.io.race(.(a = a, b = b, c = c));
|
||||||
if winner == {
|
if winner == {
|
||||||
case .a: (v) { print("winner: a (i64) = {}\n", v); }
|
case .a: (v) { print("winner: a (i64) = {}\n", v); }
|
||||||
case .b: (v) { print("winner: b (bool) = {}\n", v); }
|
case .b: (v) { print("winner: b (bool) = {}\n", v); }
|
||||||
case .c: (v) { print("winner: c (f64) = {}\n", v); }
|
case .c: (v) { print("winner: c (f64) = {}\n", v); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// The losers were cancelled (flag set) and joined (worker finished).
|
// The losers were cancelled; their work was stopped at the suspend.
|
||||||
print("loser b: canceled={} finished={}\n", b.canceled, b.finished);
|
print("loser b: canceled={}\n", b.state == .canceled);
|
||||||
print("loser c: canceled={} finished={}\n", c.canceled, c.finished);
|
print("loser c: canceled={}\n", c.state == .canceled);
|
||||||
});
|
});
|
||||||
s.run();
|
ps.run();
|
||||||
|
}
|
||||||
|
|
||||||
print("completion order (id @ virtual-ms):\n");
|
print("completion order (id @ virtual-ms):\n");
|
||||||
i := 0;
|
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
|
winner: a (i64) = 111
|
||||||
loser b: canceled=1 finished=1
|
loser b: canceled=true
|
||||||
loser c: canceled=1 finished=1
|
loser c: canceled=true
|
||||||
completion order (id @ virtual-ms):
|
completion order (id @ virtual-ms):
|
||||||
task 1 @ 10ms
|
task 1 @ 10ms
|
||||||
task 2 @ 20ms
|
final virtual clock: 10ms
|
||||||
task 3 @ 30ms
|
|
||||||
final virtual clock: 30ms
|
|
||||||
|
|||||||
@@ -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;
|
poll :: (self: *Self, deadline_ms: i64) -> i64;
|
||||||
now_ms :: (self: *Self) -> i64;
|
now_ms :: (self: *Self) -> i64;
|
||||||
arm_timer :: (self: *Self, deadline_ms: i64, park: ParkToken) -> *void;
|
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 ---
|
// --- Context ---
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ impl Io for CBlockingIo {
|
|||||||
arm_timer :: (self: *CBlockingIo, deadline_ms: i64, park: ParkToken) -> *void {
|
arm_timer :: (self: *CBlockingIo, deadline_ms: i64, park: ParkToken) -> *void {
|
||||||
return null;
|
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 ---
|
// --- 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);
|
self.timers.append(t, self.own_allocator);
|
||||||
return null;
|
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 -------------------
|
// --- the context switch (naked) + first-entry trampoline -------------------
|
||||||
@@ -1154,112 +1168,120 @@ cancel :: ufcs (t: *Task($R)) {
|
|||||||
t.state = .canceled;
|
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,
|
// `context.io.race((a: fa, b: fb, …))` starts from N already-spawned `*Future(..)`
|
||||||
// returns when the FIRST completes, and STRUCTURALLY cancels + joins the losers
|
// handles (from `context.io.async`), returns when the FIRST is `.ready`, and
|
||||||
// before returning — no loser fiber outlives the call. The result is a
|
// CANCELS every loser before returning — with Phase-3 TRUE cancellation each loser
|
||||||
// comptime-synthesized tagged-union (`RaceResult`) mirroring the input tuple's
|
// stops at its next suspend, so `race` returns at WINNER-time, not slowest-loser-
|
||||||
// labels: each variant's NAME is the tuple label, its payload is that task's
|
// time. The result is a comptime-synthesized tagged-union (`RaceResult`) mirroring
|
||||||
// result type. The tuple must be NAMED (`(a: ta, b: tb)`); a positional-tuple
|
// the input tuple's labels: each variant's NAME is the tuple label, its payload is
|
||||||
// form (`._0`/`._1` variants) is future work — `field_name` yields "" for an
|
// that future's result type. The tuple must be NAMED (`(a: fa, b: fb)`); a
|
||||||
// unnamed element, which `make_enum` rejects as a duplicate-name collision.
|
// 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)
|
// It runs over the `Io` PROTOCOL (`current_park`/`suspend_raw`/`ready`), so it is
|
||||||
// fb := s.go(() -> B => read_b(conn)); // *Task(B)
|
// colorblind: under the fiber scheduler it really parks/wakes; under the blocking
|
||||||
// winner := s.race((a: fa, b: fb)); // RaceResult = enum { a: A; b: B }
|
// `CBlockingIo` every future is born `.ready`, so the winner scan returns
|
||||||
// if winner == { case .a: (v) {…} case .b: (v) {…} } // loser cancelled+joined
|
// 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.
|
// Synthesize the race RESULT type for a named tuple `$T` of `*Future(..)` handles.
|
||||||
// `*Task(R)` projects to its result `R` via `field_type(pointee(field_type(T, i)), 0)`:
|
// `*Future(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_type(T, i)` = `*Future(R)`, `pointee` strips the pointer to `Future(R)`,
|
||||||
// field 0 of `Task` is `value: R`. One nominal type per distinct `T` (type-fn
|
// and field 0 of `Future` is `value: R` (the `Value :: R` type member is not a
|
||||||
// identity), so the decl, every `make_variant(RaceResult(T), …)` call, and the
|
// data field). One nominal type per distinct `T` (type-fn identity), so the decl,
|
||||||
// `-> RaceResult(T)` return all name the SAME union. (The 0649 composition shape,
|
// every `make_variant(RaceResult(T), …)` call, and the `-> RaceResult(T)` return
|
||||||
// with `Task` in place of the stand-in `Box`.)
|
// all name the SAME union.
|
||||||
RaceResult :: ($T: Type) -> Type {
|
RaceResult :: ($T: Type) -> Type {
|
||||||
vs : [field_count(T)]EnumVariant = ---;
|
vs : [field_count(T)]EnumVariant = ---;
|
||||||
inline for 0..field_count(T) (i) {
|
inline for 0..field_count(T) (i) {
|
||||||
vs[i] = EnumVariant.{
|
vs[i] = EnumVariant.{
|
||||||
name = field_name(T, i), // tuple label → variant name
|
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)]);
|
return make_enum("RaceResult", vs[0..field_count(T)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structured first-wins race. Suspends the calling fiber until the FIRST task is
|
// Structured first-wins race over the `Io` protocol. Suspends the calling fiber
|
||||||
// `.ready`, builds a `RaceResult(T)` carrying that winner's value, then CANCELS
|
// until the FIRST future is `.ready`, builds a `RaceResult(T)` carrying that
|
||||||
// and JOINS every loser before returning.
|
// 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
|
// MUST be called from inside a fiber under a suspending `Io` (there must be a
|
||||||
// `wait`/`sleep`; a null `current` bails loudly rather than dereferencing null.
|
// `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
|
// TRUE-CANCEL SEMANTIC (Phase 3): each loser was parked mid-suspend when cancelled;
|
||||||
// work's first line cannot be preempted — `cancel` sets its flag and the JOIN
|
// `cancel(f)` flips its sticky flag and wakes its worker fiber, whose next
|
||||||
// waits for the worker to reach its natural end (the value is discarded). A loser
|
// `suspend_raw` raises `Canceled` and unwinds the body — its post-suspend work
|
||||||
// that had not yet started skips its work entirely (`go`'s `if t.canceled == 0`
|
// never runs. `race` does NOT join the losers (they unwind on their own next turn),
|
||||||
// guard). Either way `race` returns only once every loser's worker has `finished`,
|
// so it returns at winner-time. The winner's value is taken from `f.value`.
|
||||||
// so no loser fiber is still live past the call (structured concurrency).
|
race :: ufcs (io: Io, futures: $T) -> RaceResult(T) {
|
||||||
race :: ufcs (self: *Scheduler, tasks: $T) -> RaceResult(T) {
|
// Phase 1 — first winner. Scan for an already-`.ready` future (lowest index
|
||||||
cur := self.current;
|
// wins on a same-tick tie → deterministic). If none, register THIS coordinator
|
||||||
if cur == null {
|
// (`current_park`) as the waiter on every still-`.pending` future's `park`
|
||||||
print("sched: race() called outside a fiber (no running fiber)\n");
|
// slot and park once via `suspend_raw`; any completion `ready`s us. On wake
|
||||||
abort();
|
// 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.
|
||||||
// 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.
|
|
||||||
winner_idx : i64 = -1;
|
winner_idx : i64 = -1;
|
||||||
while winner_idx < 0 {
|
while winner_idx < 0 {
|
||||||
inline for 0..field_count(T) (i) {
|
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; }
|
if winner_idx >= 0 { break; }
|
||||||
|
me := io.current_park();
|
||||||
|
any_pending := false;
|
||||||
inline for 0..field_count(T) (i) {
|
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) {
|
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
|
// result type of variant `i` (comptime-cursor tuple indexing), so the matching
|
||||||
// unrolled arm constructs `RaceResult(T)`'s i-th variant directly — no nested
|
// unrolled arm constructs `RaceResult(T)`'s i-th variant directly. `i` is the
|
||||||
// `inline if` to recover the payload type. `i` is the comptime cursor; the
|
// comptime cursor; the runtime `if i == winner_idx` selects the one arm.
|
||||||
// runtime `if i == winner_idx` selects the one arm that fires.
|
|
||||||
result : RaceResult(T) = ---;
|
result : RaceResult(T) = ---;
|
||||||
inline for 0..field_count(T) (i) {
|
inline for 0..field_count(T) (i) {
|
||||||
if i == winner_idx {
|
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;
|
// Phase 3 — cancel every still-IN-FLIGHT loser. With true cancellation a
|
||||||
// the join then ensures the loser's worker fiber has `finished` (not merely
|
// parked loser's next `suspend_raw` raises `Canceled` and unwinds its body;
|
||||||
// been flagged): if it has not, register as ITS sole waiter and park until the
|
// its `park` was cleared above, so its completion `ready`s nobody. No join —
|
||||||
// worker's tail wakes us (the worker sets `finished = 1` and wakes its waiter
|
// `race` returns now. Only `.pending` losers are cancelled: a loser that
|
||||||
// whether it ran the work or skipped it on an early cancel). Checking
|
// already settled (`.ready`/`.failed`) is done — cancelling it would do
|
||||||
// `finished` BEFORE parking avoids a lost wakeup (mirrors `wait` checking
|
// nothing useful and would stomp its real outcome label to `.canceled`.
|
||||||
// `.ready`). Only the loser being joined has a registered waiter, so no other
|
|
||||||
// task's completion can wake us mid-join.
|
|
||||||
inline for 0..field_count(T) (i) {
|
inline for 0..field_count(T) (i) {
|
||||||
if i != winner_idx {
|
if i != winner_idx and futures[i].state == .pending { futures[i].cancel(); }
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user