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:
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
Reference in New Issue
Block a user