refactor: retire bespoke Task async; one stack behind context.io (Phase 5)
Converge the Io unification (PLAN-IO-UNIFY Phase 5). The bespoke fiber-task layer in sched.sx — Task / TaskState / TaskErr / go / wait / cancel(Task), plus Scheduler.task_allocs and its deinit bookkeeping (~130 lines) — is removed. There is now ONE async stack: context.io.async / await / cancel / race / sleep over the Io protocol, with the Scheduler as the fiber Io's engine + driver (spawn / yield_now / suspend_self / wake / run / block_on_fd remain as the raw primitives; race stays in sched.sx because it needs meta.sx's make_enum/make_variant). Migrated the four go/wait users to context.io: - 1813 — interleave + cancel (sequence 1 2 3 42 100 -99) - 1817 — m1 end-to-end (completion in deadline order, sum 123) - 1819 — double-AWAIT loud-abort via the Future one-awaiter guard - 1820 — deinit: dropped the go/task_allocs tasks; now exercises timers/io_waiters/ kq cleanup (freed=2, live=3 = the documented per-spawn closure-env residual) Updated readme.md (the user-facing async section documents context.io.async / await / race / sleep) and the stale sched.go/sched.Task comments in io.sx. Suite 854/0; no .ir churn (Task removal touched no snapshotted IR); migrated examples byte-identical on aarch64-macOS + aarch64-linux. PLAN-IO-UNIFY Phases 0-5 all complete — the two parallel async stacks are now one, behind context.io.
This commit is contained in:
59
readme.md
59
readme.md
@@ -573,12 +573,15 @@ fence(.seq_cst); // standalone memory fence
|
||||
combinations are compile errors. The same operations run at compile time (`#run`)
|
||||
under single-threaded semantics.
|
||||
|
||||
### Async / Concurrency (`modules/std/sched.sx`)
|
||||
### Async / Concurrency (`context.io`, `modules/std/sched.sx`)
|
||||
|
||||
A pure-sx cooperative fiber runtime — **colorblind async**, with no `async` /
|
||||
`await` keywords and no function coloring. Any function can suspend; a `Scheduler`
|
||||
drives any number of stackful fibers, each on its own guard-paged stack. The
|
||||
high-level API is `go` to spawn a task and `wait` to suspend until it completes:
|
||||
A pure-sx cooperative fiber runtime — **colorblind async**, with no function
|
||||
coloring. The async API rides the `Io` capability carried implicitly in
|
||||
`context`: `context.io.async` spawns a worker, `await` suspends until it
|
||||
completes. The SAME code runs under the default blocking `Io` (workers run inline)
|
||||
or under the fiber `Scheduler` installed as `context.io` (workers are real fibers
|
||||
that interleave). A `Scheduler` drives any number of stackful fibers, each on its
|
||||
own guard-paged stack:
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
@@ -588,31 +591,37 @@ main :: () {
|
||||
s := sched.Scheduler.init();
|
||||
ps := @s; // closures capture by value — capture a pointer to the scheduler
|
||||
|
||||
// The coordinator runs as a fiber so `wait` has a fiber to park.
|
||||
s.spawn(() => {
|
||||
a := ps.go(() -> i64 => { ps.sleep(30); 100 }); // launch async tasks
|
||||
b := ps.go(() -> i64 => { ps.sleep(10); 20 });
|
||||
c := ps.go(() -> i64 => { ps.sleep(20); 3 });
|
||||
// Install the fiber scheduler as `context.io`; the coordinator runs as a
|
||||
// fiber so `await` has a fiber to park.
|
||||
push .{ io = xx s } {
|
||||
ps.spawn(() => {
|
||||
a := context.io.async(() -> (i64, !) => { try context.io.sleep(30); 100 });
|
||||
b := context.io.async(() -> (i64, !) => { try context.io.sleep(10); 20 });
|
||||
c := context.io.async(() -> (i64, !) => { try context.io.sleep(20); 3 });
|
||||
|
||||
sum := (a.wait() or 0) + (b.wait() or 0) + (c.wait() or 0); // 123
|
||||
print("sum: {}\n", sum);
|
||||
});
|
||||
|
||||
s.run(); // drive the scheduler until all fibers finish
|
||||
sum := (a.await() or 0) + (b.await() or 0) + (c.await() or 0); // 123
|
||||
print("sum: {}\n", sum);
|
||||
});
|
||||
ps.run(); // drive the scheduler until all fibers finish
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tasks complete in deadline order, not spawn or await order. The runtime offers:
|
||||
Workers complete in deadline order, not spawn or await order. The runtime offers:
|
||||
|
||||
- **`go(work) -> *Task($R)`** / **`wait() -> (R, !TaskErr)`** / **`cancel()`** — the
|
||||
task layer. `wait` rides the `!` error channel so a cancel surfaces as
|
||||
`error.Canceled`.
|
||||
- **`spawn`**, **`yield_now`**, **`suspend_self`**, **`wake`** — the raw fiber
|
||||
primitives the task layer is built on.
|
||||
- **`sleep(ms)`** / **`now_ms()`** — timer-driven suspension on a virtual clock
|
||||
(deterministic, no real wall time).
|
||||
- **`block_on_fd(fd, want_read)`** — suspend until a file descriptor is ready,
|
||||
backed by kqueue (darwin) or epoll (linux).
|
||||
- **`context.io.async(worker) -> *Future($R)`** / **`await() -> (R, !IoErr)`** /
|
||||
**`cancel()`** — the async layer over the `Io` protocol. `await` rides the `!`
|
||||
error channel; a `cancel` makes the worker abandon its body at its next suspend
|
||||
(true cancellation) and surfaces as `error.Canceled`.
|
||||
- **`context.io.race(.(a = fa, b = fb, …))`** — structured first-wins over a named
|
||||
tuple of `*Future`s; returns a synthesized tagged-union of the winner, cancels
|
||||
the losers (which stop at their next suspend, so `race` returns at winner-time).
|
||||
- **`context.io.sleep(ms)`** / **`context.io.now_ms()`** — timer-driven suspension
|
||||
on a virtual clock (deterministic, no real wall time).
|
||||
- **`Scheduler.spawn`**, **`yield_now`**, **`suspend_self`**, **`wake`**,
|
||||
**`run`** — the raw fiber primitives + driver loop the async layer is built on.
|
||||
- **`Scheduler.block_on_fd(fd, want_read)`** — suspend until a file descriptor is
|
||||
ready, backed by kqueue (darwin) or epoll (linux).
|
||||
|
||||
It's an M:1 model (cooperative, no preemption — so no data races between fibers
|
||||
and no atomics needed across them), built on `abi(.naked)` context switching over
|
||||
|
||||
Reference in New Issue
Block a user