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

@@ -0,0 +1,35 @@
// Stream B2/A1 — a fiber INHERITS the dynamic `context` in force when it was
// spawned. Previously a fiber body ran under the static `__sx_default_context`
// (the `abi(.c)` `fib_dispatch` dropped the implicit context), so a
// `push Context { … }` around `spawn` was invisible inside the fiber. Now
// `Scheduler.spawn` snapshots `context` into the fiber and `fib_dispatch`
// re-pushes it around the body — so a capability installed before `spawn`
// (here a marker in `context.data`) is visible to the worker.
//
// This is the foundation for folding a fiber scheduler behind `context.io`: a
// worker's `context.io.*` must resolve to the scheduler that spawned it, not the
// blocking default. Behavior-preserving for fibers spawned under the default
// context (the snapshot just re-pushes that same default).
//
// 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";
Marker :: struct { id: i64; }
main :: () -> i64 {
mk := Marker.{ id = 7 };
s := sched.Scheduler.init();
ps := @s;
print("outside: marker id = {}\n", mk.id);
push Context.{ allocator = context.allocator, data = xx @mk, io = context.io } {
ps.spawn(() => {
m : *Marker = xx context.data; // inherited from the spawn-time context
print("inside fiber: context.data marker id = {}\n", m.id);
});
ps.run();
}
print("done\n");
return 0;
}

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);
}