feat: fibers inherit the spawn-time context (Phase 0 of Io unification)

A fiber body previously ran under the static `__sx_default_context`: the
`abi(.c)` `fib_dispatch` frame has no implicit context param, so a
`push Context { … }` around `spawn` was invisible inside the fiber. That
makes it impossible to fold a fiber scheduler behind `context.io` — a
worker's `context.io.*` would resolve to the blocking default, not the
scheduler that spawned it.

`Scheduler.spawn` now snapshots the live `context` into `Fiber.dctx`, and
`fib_dispatch` re-pushes it (`push self.dctx { self.body() }`) around the
body. So a capability installed before `spawn` (allocator, io, data) is
visible to the worker, and a worker spawned under `push Context { io = … }`
sees that `io` as `context.io`.

Behavior-preserving for fibers spawned under the default context (the
snapshot just re-pushes that same default — full suite green). Context is
parameter-threaded per fiber stack, so interleaved fibers with different
contexts don't leak across the `swap_context` (verified: two fibers with
distinct `context.data` each keep their own across a `yield_now`).

Locks: examples/concurrency/1822-concurrency-fiber-context-inherit.sx
(byte-identical aarch64-macOS host + aarch64-linux container).
This commit is contained in:
agra
2026-06-27 06:32:56 +03:00
parent 3d8f9ca094
commit 2f2d7f1db7
2 changed files with 53 additions and 1 deletions

View File

@@ -88,6 +88,16 @@ Fiber :: struct {
stack_len: i64; // GUARD + STACK, for munmap
id: i64;
next: *Fiber; // intrusive FIFO ready-queue link
// The dynamic `context` in force when this fiber was SPAWNED, captured by
// value. `fib_dispatch` re-pushes it around the body so the fiber runs under
// its spawner's context — not the static `__sx_default_context` the `abi(.c)`
// dispatch would otherwise leave in force. This is what lets a fiber-backed
// `Io` scheduler be reached as `context.io` INSIDE a worker (the whole point
// of folding the scheduler behind `context.io`): a worker's
// `context.io.suspend_raw`/`ready` resolve to the scheduler that spawned it,
// not the blocking default. Behavior-preserving for fibers spawned under the
// default context (the capture just re-pushes that same default).
dctx: Context;
}
// A pending virtual-time timer: wake `fiber` once the virtual clock reaches
@@ -190,6 +200,9 @@ Scheduler :: struct {
f.sched = self;
f.id = self.next_id;
f.next = null;
// Snapshot the spawn-time dynamic context (see `Fiber.dctx`).
// `fib_dispatch` re-pushes it so the body inherits it.
f.dctx = context;
self.next_id = self.next_id + 1;
self.n_spawned = self.n_spawned + 1;
@@ -650,7 +663,11 @@ T
// `run()` call site. Fiber bodies do not inherit a caller-scoped allocator; a
// body that needs one must capture it explicitly (the long-lived-container rule).
fib_dispatch :: (self: *Fiber) abi(.c) {
self.body();
// Run the body under the context captured at spawn (`Fiber.dctx`), so the
// fiber inherits its spawner's `context` — notably `context.io` resolves to a
// fiber-backed scheduler installed as `context.io`, not the static
// `__sx_default_context` this `abi(.c)` frame would otherwise leave in force.
push self.dctx { self.body(); }
self.state = .done;
swap_context(@self.ctx, @self.sched.sched_ctx);
}