From bab4886346d2416166300c951688b34408a77b4f Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 20 Jun 2026 17:09:26 +0300 Subject: [PATCH] fibers B1.1: per-fiber context root is library-only (no compiler change) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fiber needs its own root Context (the spawner's snapshot), not the ambient one. Probed whether that needs compiler support: it does not. context is an implicit slot-0 *Context param (call-carried, rides the callee's own stack) and push Context allocates on the caller frame — never TLS, never re-read from the __sx_default_context global mid-stack. So the spawn convention is pure library sx: snap := context; // snapshot the spawner's context f := Fiber.{ root = snap }; // store it push f.root { entry(args) } // trampoline installs it as the fiber root examples/1804-concurrency-context-snapshot.sx locks it: a trampoline running under ambient ctx 99 installs a stored snapshot (42); the body reads 42, and the push scope restores 99 on exit. No fiber runtime yet (B1.3) — this proves the plumbing it builds on. The design doc's "lower context as swappable indirection, never raw TLS" guarded a non-problem — context was already param-carried. Suite green (726/0). --- current/CHECKPOINT-FIBERS.md | 74 ++++++++++++++----- current/PLAN-FIBERS.md | 22 +++--- examples/1804-concurrency-context-snapshot.sx | 48 ++++++++++++ .../1804-concurrency-context-snapshot.exit | 1 + .../1804-concurrency-context-snapshot.stderr | 1 + .../1804-concurrency-context-snapshot.stdout | 2 + 6 files changed, 117 insertions(+), 31 deletions(-) create mode 100644 examples/1804-concurrency-context-snapshot.sx create mode 100644 examples/expected/1804-concurrency-context-snapshot.exit create mode 100644 examples/expected/1804-concurrency-context-snapshot.stderr create mode 100644 examples/expected/1804-concurrency-context-snapshot.stdout diff --git a/current/CHECKPOINT-FIBERS.md b/current/CHECKPOINT-FIBERS.md index ce1d129a..b425122a 100644 --- a/current/CHECKPOINT-FIBERS.md +++ b/current/CHECKPOINT-FIBERS.md @@ -4,8 +4,25 @@ Companion to [PLAN-FIBERS.md](PLAN-FIBERS.md). Update after every step (one step per the cadence rule). New corpus category: `18xx` concurrency. ## Last completed step -**B1.0b (`abi(.naked)` real emission) — DONE. B1.0 complete.** Replaced the emit bail with -real LLVM `naked` emission: +**B1.1 (per-fiber `context` root) — DONE. Zero compiler change (confirmed by probe).** The +fiber-spawn context convention works end-to-end with ordinary language features: +- `snap := context` captures the spawner's `Context` as a value; +- the snapshot is stored in a struct (the stand-in `Fiber`); +- a trampoline running under a *different* ambient context installs the fiber's stored root + with `push f.root { … }`, and the body reads the snapshot — not the trampoline's ambient + context — because `context` is an implicit slot-0 `*Context` param (call-carried, rides the + callee's own stack) and `push` allocates on the caller frame (no global, no TLS). +- Locked by `examples/1804-concurrency-context-snapshot.sx`: prints `fiber root: 42` (the + installed snapshot wins over ambient 99) + `ambient after: 99` (the `push` scope restores + the ambient context on exit). No fiber runtime yet (that's B1.3) — this proves the plumbing + it will build on. No `.build` pin (pure sx, host-independent). +- **Probe result:** the design doc's "lower as swappable indirection, never raw TLS" guarded + a non-problem — context was already param-carried, never TLS. No path re-reads + `__sx_default_context` mid-stack, so there is **no compiler obligation** here. +- `zig build && zig build test` green: **726 ran, 0 failed**. + +### Earlier — B1.0 (`abi(.naked)` codegen) — complete +Replaced the emit bail with real LLVM `naked` emission: - `emit_llvm` declaration pass: for `func.is_naked`, add the LLVM `naked` + `noinline` + `nounwind` attributes and **skip** the `frame-pointer=all` attribute (incompatible with a frameless function). Pass 2 now emits the `.naked` body normally — `naked` makes the @@ -42,13 +59,16 @@ body); closed + locked. The review's `.naked`-lambda CRITICAL was a false positi (unparseable — `isLambda` breaks on the `abi` keyword). ## Current state -Stream A (atomics) is feature-complete (✅). Stream B1: **B1.0 complete** — `abi(.naked)` -emits a real LLVM `naked` function end-to-end (decl, generic, pack paths), the substrate for -the fiber context-switch. No fibers/Io/scheduler code yet. Grounded floor facts: -- `context` is already an implicit `*Context` param (slot 0) + `push Context` is a stack - `alloca` ⇒ **fiber-local for free**. Only shared root = `__sx_default_context` global - (entry-point bind). B1.1 expected to be a **library convention** (spawn trampoline - snapshots the spawner's ctx into slot 0), **likely zero compiler change** — probe first. +Stream A (atomics) is feature-complete (✅). Stream B1: **B1.0 + B1.1 complete.** The two +compiler-floor preconditions for the fiber runtime are in place: (1) `abi(.naked)` emits a +real LLVM `naked` function end-to-end (decl, generic, pack paths) — the context-switch +substrate; (2) per-fiber `context` root needs **no compiler change** — the spawn convention +(snapshot `context`, store, `push` it from the trampoline) is pure library sx. No +fibers/Io/scheduler code yet. Grounded floor facts: +- `context` is an implicit slot-0 `*Context` param + `push Context` is a stack `alloca` ⇒ + **fiber-local for free** (confirmed by the B1.1 probe — never TLS, never re-read from the + `__sx_default_context` global mid-stack). A spawn passes the snapshot as the fiber-entry + fn's slot-0 ctx via `push f.root { entry(args) }`. Locked by `1804-...-context-snapshot`. - Inline asm works end-to-end (lower→emit→JIT, aarch64 + x86_64) — the `.naked` body reuses it. - **`.naked` with PARAMS works** (B1.0c, the B1.3 substrate): the param-alloca loop is gated on `fd.abi != .naked` in decl.zig (both paths) + generic.zig — a naked fn's args stay in @@ -60,11 +80,14 @@ the fiber context-switch. No fibers/Io/scheduler code yet. Grounded floor facts: boundary; a sharper sx diagnostic for it is a candidate polish, not a blocker. ## Next step -**B1.1 (per-fiber `context` root) — probe-first.** Per PLAN-FIBERS.md "Phases → B1.1". Write -a probe confirming a spawn trampoline can pass a snapshotted `Context` as slot 0 with no -compiler change (grounded as likely zero-change); lock the behavior with an `18xx` example + -a checkpoint note on the convention. Only if the probe surfaces a real gap (a path re-reads -`__sx_default_context` mid-stack) does this become a compiler step. +**B1.2 (A1 — `Io` interface + `context.io` + `Future` + `cancel()` API).** Per PLAN-FIBERS.md +"Phases → B1.2". Library-only: add an `Io` protocol as a `Context` field (mirror `Allocator` +at field 0; `Context` is currently `{ allocator, data }` — add `io`), plus the `Future` / +`cancel()` surface. Exercise the blocking-`Io` default with an `18xx` example (real suspend +lands in B1.3). No compiler change expected; if a protocol-in-context gap appears, file it. +NOTE: adding a field to `Context` shifts its layout — check whether any `Context` literal / +`push Context.{...}` site or the `__sx_default_context` builder needs the new field (the +allocator precedent shows the pattern). ## Known issues / capability gaps - **Orthogonal (not a B1 blocker):** default VALUES for comptime params don't bind on @@ -100,10 +123,12 @@ a checkpoint note on the convention. Only if the probe surfaces a real gap (a pa x86_64 cross sibling (`.build` target-gated, ir-only on mismatch), like the asm corpus split. The `.ir` proves the `naked` attr + asm emitted, NOT register-save correctness (that's B1.3's stress harness). -- **B1.1 grounded as library-only (pending probe):** push frames are stack-`alloca`'d and - the implicit ctx rides slot 0, so a spawn trampoline can pass a snapshotted ctx with no - compiler change. The design doc's "never raw TLS" guards a non-problem (context is not - TLS). Probe to confirm before sizing any compiler work. +- **B1.1 — per-fiber context is library-only (CONFIRMED by probe):** push frames are + stack-`alloca`'d and the implicit ctx rides slot 0, so the spawn convention — snapshot + `context`, store it, `push f.root { entry(args) }` from the trampoline — installs the + fiber's root with no compiler change. Verified: the body reads the snapshot over a different + ambient context, and `push` restores ambient on exit (`1804-...-context-snapshot`). The + design doc's "never raw TLS" guarded a non-problem (context was never TLS). - **Test keystones (design §10):** the **B1.3 switch-stress harness** gates the context-switch (the one piece the deterministic `Io` can't test — §8.1.1, §10.7); the **B1.4 deterministic-sim `Io`** (calibrated against blocking `Io` — §8.1.3) gates all @@ -141,5 +166,14 @@ a checkpoint note on the convention. Only if the probe surfaces a real gap (a pa error). Gated the param-alloca loop on `fd.abi != .naked` (decl.zig both paths + generic.zig) — naked args stay in registers, read by the asm body (the B1.3 context-switch shape). Locked by `examples/1803-concurrency-naked-asm-param.sx`. Pack `.naked` left unsupported - (loud, nonsensical). **B1.0 complete.** Suite green (725/0). **Next: B1.1 (per-fiber - context, probe-first).** + (loud, nonsensical). **B1.0 complete.** Suite green (725/0). +- **rename** — ABI variant `.pure → .naked` (keyword, `Function.is_naked`, diagnostics, + examples 1800-1803 `*-pure-* → *-naked-*`, docs). "pure" universally means side-effect-free + — wrong for a register-clobbering switch; "naked" matches LLVM/Zig/Rust/GCC/Clang. Pure + cosmetics, no semantic change. Suite green (725/0). +- **B1.1** — per-fiber `context` root: **zero compiler change** (probe-confirmed). The spawn + convention (snapshot `context` → store in a struct → `push f.root { entry() }` from the + trampoline) installs the fiber's root via the implicit slot-0 `*Context` param; the body + reads the snapshot, not the trampoline's ambient ctx, and the `push` scope restores ambient + on exit. Locked by `examples/1804-concurrency-context-snapshot.sx` (prints `fiber root: 42` + / `ambient after: 99`). Suite green (726/0). **Next: B1.2 (Io interface + context.io).** diff --git a/current/PLAN-FIBERS.md b/current/PLAN-FIBERS.md index 42279457..1bbfb10a 100644 --- a/current/PLAN-FIBERS.md +++ b/current/PLAN-FIBERS.md @@ -1,8 +1,8 @@ # PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler) -> **STATUS: 🚧 in progress.** B1.0 (`abi(.naked)` codegen) ✅ complete — emits a real LLVM -> `naked` function end-to-end (decl / generic / pack paths; examples 1800/1801/1802 + unit -> test). Next step = **B1.1** (per-fiber `context` root — probe-first, likely library-only). +> **STATUS: 🚧 in progress.** B1.0 (`abi(.naked)` codegen) ✅ + B1.1 (per-fiber `context` +> root — zero compiler change, library convention) ✅ complete. Next step = **B1.2** (`Io` +> interface + `context.io` + `Future` + `cancel()`). Carved from [PLAN-POST-METATYPE.md](PLAN-POST-METATYPE.md) Stream B (§B1) + the design-of-record [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md) @@ -174,14 +174,14 @@ B1.0 (`.naked`) forces these plumbing sites: *enables* B1.3's `swap_context(from, to)`. Locked by `1803-concurrency-naked-asm-param.sx`. Pack `.naked` (variadic + naked, nonsensical) left unsupported → loud verifier error. -### B1.1 — per-fiber `context` root (probe-first; likely zero compiler change) -- **B1.1a (probe + lock)** — write a probe (`.sx-tmp/`) + an `18xx` example that snapshots a - `Context` (e.g. a custom allocator pushed via `push Context`) and confirms it is carried by - slot 0 across an ordinary call chain (it is — grounded). If the probe shows a fiber-entry - trampoline can pass a snapshotted ctx as slot 0 with **no compiler change**, this phase is a - **library convention doc** (record it in the checkpoint) + a corpus example locking the - behavior. If (and only if) the probe surfaces a real compiler gap (a path re-reads - `__sx_default_context` mid-stack), file it as a step here and size it then. +### B1.1 — per-fiber `context` root — ✅ COMPLETE (zero compiler change) +Probe confirmed the spawn convention works with ordinary language features: snapshot +`context` (`snap := context`), store it in a struct, and `push f.root { entry(args) }` from a +trampoline running under a different ambient context — the body reads the snapshot (via the +implicit slot-0 `*Context` param), not the ambient ctx, and `push` restores ambient on exit. +No path re-reads `__sx_default_context` mid-stack ⇒ **no compiler obligation**; this is a pure +library convention. Locked by `examples/1804-concurrency-context-snapshot.sx` (`fiber root: +42` / `ambient after: 99`). The design doc's "never raw TLS" guarded a non-problem. ### B1.2 — A1: `Io` interface + `context.io` + `Future` + `cancel()` API Library-only. `Io` as a protocol added to `Context` (mirror `Allocator`). `Future`/`cancel` diff --git a/examples/1804-concurrency-context-snapshot.sx b/examples/1804-concurrency-context-snapshot.sx new file mode 100644 index 00000000..a76f6968 --- /dev/null +++ b/examples/1804-concurrency-context-snapshot.sx @@ -0,0 +1,48 @@ +// Stream B1 (fibers) step B1.1 — per-fiber `context` root, via plain +// snapshot + `push` (NO compiler change — the substrate the fiber spawn relies on). +// +// `context` is an implicit `*Context` parameter (slot 0) threaded through every +// sx call, and `push Context` allocates the new context on the caller's stack +// frame — so a function's context is CALL-CARRIED and rides its own stack, never +// read from a global. That is exactly what a fiber needs: each fiber, on its own +// stack, reads its own root context. +// +// This locks the spawn convention end-to-end with ordinary language features: +// 1. capture the spawner's context as a value (`snap := context`) +// 2. store the snapshot in a struct (the stand-in `Fiber`) +// 3. a trampoline, running under a DIFFERENT ambient context, installs the +// fiber's stored root before entering the body (`push f.root { … }`) +// The body sees the snapshot (42), not the trampoline's ambient context (99), +// and the `push` scope restores the ambient context on exit. No fiber runtime +// exists yet (that is B1.3) — this proves the context plumbing it will build on. +#import "modules/std.sx"; + +// Stand-in for a Fiber: stores the spawner's snapshotted root Context. +Fiber :: struct { root: Context; } + +// Reads this call's context sentinel (carried in `context.data` here for the +// test; a real fiber carries its allocator / io the same way). +sentinel :: () -> i64 { return xx context.data; } + +// Trampoline: runs under its own ambient context, but installs the fiber's +// stored root before entering the fiber body. +run_fiber :: (f: *Fiber) -> i64 { + push f.root { + return sentinel(); + } +} + +main :: () { + snap := context; + snap.data = xx 42; // the spawner's sentinel + f := Fiber.{ root = snap }; // snapshot stored in the fiber + + other := context; + other.data = xx 99; // a DIFFERENT ambient context + push other { + // Ambient is 99 here; the trampoline must install f.root (42). + print("fiber root: {}\n", run_fiber(@f)); + // After run_fiber returns, this scope's ambient context is intact. + print("ambient after: {}\n", sentinel()); + } +} diff --git a/examples/expected/1804-concurrency-context-snapshot.exit b/examples/expected/1804-concurrency-context-snapshot.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/1804-concurrency-context-snapshot.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1804-concurrency-context-snapshot.stderr b/examples/expected/1804-concurrency-context-snapshot.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1804-concurrency-context-snapshot.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1804-concurrency-context-snapshot.stdout b/examples/expected/1804-concurrency-context-snapshot.stdout new file mode 100644 index 00000000..356223ab --- /dev/null +++ b/examples/expected/1804-concurrency-context-snapshot.stdout @@ -0,0 +1,2 @@ +fiber root: 42 +ambient after: 99