fibers B1.1: per-fiber context root is library-only (no compiler change)
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).
This commit is contained in:
@@ -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).**
|
||||
|
||||
@@ -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`
|
||||
|
||||
48
examples/1804-concurrency-context-snapshot.sx
Normal file
48
examples/1804-concurrency-context-snapshot.sx
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
1
examples/expected/1804-concurrency-context-snapshot.exit
Normal file
1
examples/expected/1804-concurrency-context-snapshot.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
fiber root: 42
|
||||
ambient after: 99
|
||||
Reference in New Issue
Block a user