feat: async/await colorblind over the fiber Io (Phase 2 of Io unification)

`context.io.async(worker)` / `await` now run over the `Io` PROTOCOL, so the
same code interleaves under the fiber scheduler or runs inline under the
blocking `CBlockingIo` — one async stack, reached purely through `context.io`.

- Protocol: `suspend_raw(park: *ParkToken)` (was by-value). A suspending impl
  records the parked execution context into `park.handle` before parking, so a
  cross-context `ready(park)` knows whom to resume; `Scheduler.suspend_raw`
  writes `self.current`, `CBlockingIo` ignores it.
- io.sx async layer rewritten colorblind: `async` submits the worker through
  `io.spawn_raw` (inline under blocking, a fiber under the scheduler) and returns
  a HEAP `*Future($R)` the worker fills later; `await` suspends via `suspend_raw`
  until ready, then returns/raises. The generic worker is bridged to spawn_raw's
  raw `(*void)->void` entry via a monomorphic `ThunkBox` (a heap-boxed nullary
  completion closure) — all genericity lives in the closure env. Workers are
  nullary (inputs captured at the call site) because a variadic pack can't cross
  the fiber boundary. `CBlockingIo.spawn_raw` now runs the worker inline.
- Migrated 1805/1806 to the nullary `*Future` form; retrofit 1822/1823 to the
  `push .{ … }` partial-context literal (inherits allocator/data).
- The async machinery adds a few prelude types, shifting the type-name table —
  40 `.ir` snapshots regenerated (no behavior change; only `.exit`/`.stdout`/
  `.stderr` would signal that, and none changed).

Locked by examples/concurrency/1824 — two async tasks under the fiber Io, the
completion log proving deferral (1 2 then 10 20 then 123). Suite 829/0,
byte-identical aarch64-macOS host + aarch64-linux container.
This commit is contained in:
agra
2026-06-27 07:50:29 +03:00
parent c4977247b7
commit 967aed67d4
52 changed files with 210381 additions and 202872 deletions

View File

@@ -1,28 +1,29 @@
// B1.2 — the async ergonomic layer over the `Io` capability, blocking
// default. `context.io.async(worker, ..args)` runs the worker to completion
// inline and returns a `.ready` Future($R); `f.await()` yields the result
// (a value-failable `($R, !IoErr)`, handled with `or`). `context.io.now_ms()`
// reads the monotonic clock through the same capability.
// B1.2 / B2 — the async ergonomic layer over the `Io` capability, blocking
// default. `context.io.async(worker)` submits a NULLARY `worker: Closure() -> $R`
// and returns a `*Future($R)` handle; under the blocking `CBlockingIo` the worker
// runs to completion inline, so the Future is born `.ready`. `f.await()` yields
// the result (a value-failable `($R, !IoErr)`, handled with `or`).
// `context.io.now_ms()` reads the clock through the same capability.
//
// Worker form: a lambda whose params are annotated at the call site
// (`(a: i64, b: i64) -> i64 => …`); `..args` forwards the call-site
// arguments to it.
// Worker form: a nullary lambda capturing any inputs at the CALL SITE
// (`() -> i64 => compute(a, b)`) — the colorblind shape that also works when the
// worker is deferred onto a fiber (a captured variadic pack can't cross the fiber
// boundary), mirroring `sched.go`.
#import "modules/std.sx";
main :: () {
// Homogeneous args.
s := context.io.async((a: i64, b: i64) -> i64 => a + b, 40, 2);
// Inputs captured at the call site.
s := context.io.async(() -> i64 => 40 + 2);
print("sum: {}\n", s.await() or { -1 });
// Single arg.
d := context.io.async((x: i64) -> i64 => x * 2, 21);
d := context.io.async(() -> i64 => 21 * 2);
print("double: {}\n", d.await() or { -1 });
// Nullary worker — the variadic `async` binds an empty pack, so no separate
// `async_void` entry is needed.
n := context.io.async(() -> i64 => 42);
// A worker that closes over a local.
base := 42;
n := context.io.async(() -> i64 => base);
print("nullary: {}\n", n.await() or { -1 });
// The Io capability also carries a monotonic clock.
// The Io capability also carries a clock.
if context.io.now_ms() >= 0 { print("clock ok\n"); }
}

View File

@@ -7,11 +7,11 @@
main :: () {
// Not canceled → await yields the value.
ok := context.io.async((n: i64) -> i64 => n, 7);
ok := context.io.async(() -> i64 => 7);
print("ok: {}\n", ok.await() or { -1 });
// Canceled → await raises .Canceled → the `or` default is taken.
c := context.io.async((n: i64) -> i64 => n, 7);
c := context.io.async(() -> i64 => 7);
c.cancel();
print("canceled: {}\n", c.await() or { -99 });
}

View File

@@ -23,7 +23,7 @@ main :: () -> i64 {
s := sched.Scheduler.init();
ps := @s;
print("outside: marker id = {}\n", mk.id);
push Context.{ allocator = context.allocator, data = xx @mk, io = context.io } {
push .{ data = xx @mk } {
ps.spawn(() => {
m : *Marker = xx context.data; // inherited from the spawn-time context
print("inside fiber: context.data marker id = {}\n", m.id);

View File

@@ -23,7 +23,7 @@ sleeper :: (arg: *void) {
n : *i64 = xx arg;
tok : ParkToken = .{ handle = null };
context.io.arm_timer(context.io.now_ms() + n.*, tok);
context.io.suspend_raw(tok) catch {};
context.io.suspend_raw(@tok) catch {};
print("worker(sleep {}) resumed at now_ms = {}\n", n.*, context.io.now_ms());
}
@@ -32,7 +32,7 @@ main :: () -> i64 {
ps := @s;
d1 : i64 = 20;
d2 : i64 = 10;
push Context.{ allocator = context.allocator, data = null, io = xx s } {
push .{ io = xx s } {
context.io.spawn_raw(xx sleeper, xx @d1, .{});
context.io.spawn_raw(xx sleeper, xx @d2, .{});
ps.run();

View File

@@ -0,0 +1,42 @@
// Stream B2 — `async`/`await` (the io.sx ergonomic layer) running COLORBLIND over
// the fiber `Io` scheduler. The SAME `context.io.async(worker)` that runs inline
// under the blocking `CBlockingIo` (1805) here spawns the worker as a real fiber
// and returns a PENDING `*Future`; `await` suspends the calling fiber until the
// worker completes. No bespoke `go`/`wait` — this is the unified async stack
// (io.sx async over the `Io` protocol), reaching the fiber scheduler purely
// through `context.io`.
//
// The completion log makes the deferral visible: the coordinator records 1,2
// BEFORE either worker runs (async only SPAWNS them), then `await` parks it while
// the workers run (10,20), then it resumes and sums (123). Deterministic.
//
// 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";
Log :: struct { seq: [8]i64; n: i64; }
rec :: (l: *Log, v: i64) { l.seq[l.n] = v; l.n = l.n + 1; }
main :: () -> i64 {
lg : Log = ---; lg.n = 0;
s := sched.Scheduler.init();
ps := @s; pl := @lg;
push .{ io = xx s } {
ps.spawn(() => {
rec(pl, 1); // coordinator starts
a := context.io.async(() -> i64 => { rec(pl, 10); 100 }); // worker A — deferred
b := context.io.async(() -> i64 => { rec(pl, 20); 23 }); // worker B — deferred
rec(pl, 2); // both spawned, neither has run
va := a.await() or { -1 }; // park; A runs, wakes us
vb := b.await() or { -1 };
rec(pl, va + vb); // 123
});
ps.run();
}
print("sequence:");
i := 0;
while i < lg.n { print(" {}", lg.seq[i]); i = i + 1; }
print("\n");
return 0;
}

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 one or more lines are too long

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1 @@
sequence: 1 2 10 20 123