From 2f2d7f1db7f1a2325327e0c4197945e9d1053585 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 27 Jun 2026 06:32:56 +0300 Subject: [PATCH] feat: fibers inherit the spawn-time context (Phase 0 of Io unification) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../1822-concurrency-fiber-context-inherit.sx | 35 +++++++++++++++++++ library/modules/std/sched.sx | 19 +++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 examples/concurrency/1822-concurrency-fiber-context-inherit.sx diff --git a/examples/concurrency/1822-concurrency-fiber-context-inherit.sx b/examples/concurrency/1822-concurrency-fiber-context-inherit.sx new file mode 100644 index 00000000..3a81cbdd --- /dev/null +++ b/examples/concurrency/1822-concurrency-fiber-context-inherit.sx @@ -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; +} diff --git a/library/modules/std/sched.sx b/library/modules/std/sched.sx index 886a5c02..07f40c97 100644 --- a/library/modules/std/sched.sx +++ b/library/modules/std/sched.sx @@ -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); }