The B1.2 "blockers" were not real: - Issue 0151 was INVALID: its repro used the non-idiomatic `($A) -> $R` bare-fn-ptr form. The canonical higher-order pack idiom `Closure(..$args) -> $R` + `..$args` (see examples/0543-packs-canonical-map) infers $R fine and runs today with no compiler change. Removed 0151. - The correct async idiom is verified working live (42 42 for homo + hetero args): async :: (io, worker: Closure(..$args) -> $R, ..$args) -> Future($R) with a lambda worker (annotated params) + a `result = ---; result.v = ...` build form. No compiler change needed. Issue 0150 (void struct field -> SIGTRAP exit 133) IS a real bug but is only reached via Future(void) (void-returning worker / timeout) — deferred to B1.4; B1.2 supports non-void workers. Updates the PLAN/CHECKPOINT B1.2 status to UNBLOCKED with the corrected idiom and the resume plan. No compiler/library code changed in this commit.
18 KiB
PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler)
STATUS: 🚧 in progress. B1.0 (
abi(.naked)) ✅ + B1.1 (per-fibercontext) ✅. B1.2 (Iointerface) is UNBLOCKED — the earlier "blockers" were artifacts of non-idiomatic syntax + a worker's dirty binary. Issue 0151 was INVALID (the($A)->$Rbare-fn-ptr form is not idiomatic sx) and is removed. The correctasyncidiom works today, no compiler change:async :: (io, worker: Closure(..$args) -> $R, ..$args) -> Future($R)with a lambda worker + theresult : Future($R) = ---; result.v = worker(..args);build form (mirrors the canonicalexamples/0543-packs-canonical-map.sx). Caveats: lambda params must be annotated; passing a bare named fn as the worker is non-idiomatic (use a lambda). Issue 0150 (voidstruct field SIGTRAP, exit 133) is a real bug but only hit byFuture(void)/timeout— deferred (avoid void Futures in B1.2; revisit in B1.4). Resume B1.2 with the corrected idiom (the WIP at.sx-tmp/b12-wip/has the Io-protocol/Context/ materializer parts that WORK; rewrite the async layer to the pack-lambda form above).
Carved from PLAN-POST-METATYPE.md Stream B (§B1) + the design-of-record ../design/execution-evolution-roadmap.md §4 (async), §7 steps 4–9, §8.1 (risks), §10 (testing). Progress in CHECKPOINT-FIBERS.md. Stream B2 (channels/cancel/stdlib) is a separate carve ([PLAN-CHANNELS.md], when reached) and depends on this + atomics (✅).
Goal: the colorblind, stackful, pure-sx async runtime — fibers behind an Io
interface, an M:1 scheduler, blocking + deterministic-sim + event-loop Io impls. The
compiler floor is small and net-new: make abi(.naked) actually emit an LLVM naked
function (B1.0), and confirm/close the per-fiber context root (B1.1). Everything
else — the context-switch asm, fiber bootstrap, mmap stacks, the scheduler, futures,
the Io vtables — is ordinary sx library code (design §4, §4.4). The irreducible FFI
floor: the per-arch asm context-switch (in .sx), syscall externs, and mmap.
Cadence (IMPASSIBLE): no commit both adds a test AND makes it pass (lock-to-bail, then
flip to green); zig build && zig build test green after every step; never regen snapshots
while red; scope regens with -Dname=examples/NNNN-…sx -Dupdate-goldens + review the diff.
New corpus category: 18xx concurrency. On an unrelated compiler bug → file
issues/NNNN, mark this checkpoint BLOCKED, STOP (CLAUDE.md). The in-session
worker-fix override (delegate a blocker to a worker) applies only with explicit user
authorization.
Design (grounded against the tree)
B1.0 — abi(.naked) codegen (the one genuinely net-new compiler piece in B1)
The design doc spells this callconv(.naked); the real sx surface is abi(.naked) —
written in the postfix slot, name :: (sig) -> Ret abi(.naked) { asm { … }; } (cf.
build_options :: () -> BuildOptions abi(.compiler); in build.sx:28).
The sx-facing name is naked throughout (keyword, field is_naked, diagnostics) —
matching LLVM's naked attribute (the lowering mechanism) and the industry term
(Zig/Rust/GCC/Clang). The ABI variant was renamed .pure → .naked: "pure" universally
means side-effect-free, the opposite of a register-clobbering context switch.
Grounding (verified — do not re-derive):
- The
ABIenum already carries.naked—ABI = enum { default, c, compiler, naked }(ast.zig:142), documented "naked function (inline asm body), no calling-convention prologue/epilogue." So B1.0 is NOT "extend the enum." .nakedis inert today: type_resolver.zig:237 maps.compiler, .naked → .defaultCC, andemit_llvmemits no LLVMnakedattribute. So the net-new work is exactly: carryabi == .nakedinto the IRFunction, emit LLVM'snakedattr, and skip the implicit-Context/ prologue lowering so the body is just the asm block + its ownret.- The IR
Functionstruct (inst.zig:605) carriescall_conv(default/c) +is_compiler_domain, but no naked flag — add one (is_naked: bool). - Attribute API is in-tree:
nounwindis set at emit_llvm.zig:1339 viaLLVMGetEnumAttributeKindForName("nounwind", 8)→LLVMCreateEnumAttribute(ctx, id, 0)→LLVMAddAttributeAtIndex(func, func_idx_attr /* -1 */, attr). The LLVMnakedattr is the same shape:LLVMGetEnumAttributeKindForName("naked", 5). - The
.cABI already skips the implicit ctx at lowering —lam.abi == .c/fd.abi == .cgates (closure.zig:171, decl.zig:515)..nakedmust skip it too (a.nakedfn gets no synthetic__sx_ctx, no stack frame, no prologue — args arrive in ABI registers and are read directly from asm). The implicit-return machinery (lowerValueBody) must also be bypassed: a.nakedbody has no sx return (the asm rets itself), so lower its statements and cap the block withunreachable. - Inline asm already works end-to-end (lower→emit→JIT): aarch64
(examples/1645), x86_64
(examples/1651), global asm, JIT
(1653).
emitInlineAsm/LLVMGetInlineAsmat ops.zig:915. The.nakedbody is a single asm block reusing this path.
.naked ≠ .c (design §4.6 context-switch note): a .c epilogue restores SP from the
frame; a context switch deliberately makes SP-in ≠ SP-out, so the .c epilogue would
restore from the wrong stack. .naked = no prologue/epilogue/frame — the asm emits its
own ret. This is why the switch must be .naked, not .c.
Snapshot story (per the atomics precedent): a .naked fn's body is raw per-arch asm
(it can't be portable — that's the point), while LLVM's naked attribute text is
arch-invariant. B1.0a (lock) needs only one host example locked to the emit bail —
the bail fires at the function level before any asm/instruction selection, so it is
host-independent (no .build target pin). B1.0b (green) adds emission, pins that
example aarch64 (.build {"target": "aarch64-macos"}, end-to-end on a matching host,
ir-only on a mismatch), and adds an x86_64 cross sibling — mirroring the existing asm
corpus split (1645 aarch64 / 1651 x86). The ir-only .ir (only producible once emission
lands in B1.0b) asserts the naked attribute + the asm body. State loudly: the .ir
proves the naked keyword + asm emitted, NOT that any hand-written register save/restore
is correct — that is the B1.3 switch-stress harness's job, never the corpus's.
B1.1 — per-fiber context root (grounding says this is SMALL, likely library-only)
Grounding (verified — closes the design doc's open sizing question):
contextis an implicit*Contextparameter (__sx_ctx, slot 0), threaded through every default-conv sx call (lower.zig:259) — not raw TLS. Inside a functioncurrent_ctx_ref = Ref.fromIndex(0)(the param) → it rides the fiber stack frame for free.push Context.{…}allocates the newContextwith a stackallocaand rebindscurrent_ctx_refto that slot (stmt.zig:1263) — "No global, no walk." So push frames are fiber-local for free.- The only shared root is the
__sx_default_contextglobal, bound at entry-points /abi(.c)fns before any user code runs (decl.zig:2667, :2815).
⇒ The design doc's "lower as swappable indirection, never raw TLS" guards a non-problem
(confirmed). The real, now-sized B1.1 work is purely a library convention: a
freshly-spawned fiber must take its root Context from the spawner's snapshot (passed
as the fiber-entry fn's __sx_ctx slot-0 arg by the spawn trampoline), not the
__sx_default_context global. That is sx-side (the trampoline already controls slot 0) —
expected to be ZERO compiler change. B1.1's first action is a probe confirming this; if
a fiber genuinely re-reads the global root mid-stack (it should not — entry binds once),
then and only then is there a compiler obligation. Ground the probe before sizing any
compiler work. Prerequisite of B1.3 (a fiber needs a valid root before it switches).
B1.2–B1.5 — pure sx over the primitives (design §4)
- B1.2 (A1):
Iointerface +context.io+Future+cancel()— a protocol/vtable threaded exactly likeAllocator(which already lives atContextfield 0; seeallocViaContextcall.zig:1214).Iobecomes anotherContextfield. No compiler change — protocols + context already carry it. - B1.3 (A2): the fiber runtime — naked context-switch asm (per-arch), bootstrap,
mmapstacks with mandatory guard pages. All sx. Highest corruption risk in the stream (§8.1.1) and untestable by the deterministicIo(which tests scheduling, not the switch). Its first deliverable, before the scheduler AND the deterministicIo: a standalone 2-fiber ping-pong switch-stress harness (§10.7) — scribble every callee-saved register + a stack canary before each suspend, deep/recursive chains, verify all survive post-resume. This harness — not B1.4 — is A2's correctness gate. - B1.4 (A3):
Ioimpls in order blocking → deterministic-sim (KEYSTONE) → event-loop (kqueue/epoll/io_uring). Build the deterministicIoright after blocking; calibrate it against blockingIobefore trusting it to gate everything async (§8.1.3, §10.7) — a deterministic-but-wrong scheduler snapshots garbage. (Open, deferred: the event loop does not yet cooperate with a platform UI run loop — CFRunLoop/ALooper; that's a §6 app-target gap, out of B1.) - B1.5 (A5·M:1): the single-thread scheduler — validates the whole colorblind stack
end-to-end.
18xxcorpus runs under the deterministicIo, asserting a program-emitted ordering contract (sequence markers), not raw interleaving, so scheduler-policy tweaks don't churn every snapshot.
Files the compiler floor touches (B1.0 only; B1.1–B1.5 are library + tests)
B1.0 (.naked) forces these plumbing sites:
- ast.zig:142 —
ABI.naked(exists; reference only). - inst.zig:605 — add
is_naked: bool = falsetoFunction. - decl.zig — set
is_nakedfromfd.abi == .naked; gate the implicit-ctx off for.nakedinfuncWantsImplicitCtx(mirror the.cskip at decl.zig:515) and bypasslowerValueBodyfor.nakedbodies (lower statements + cap withunreachable, in both body-lowering paths) — a.nakedfn binds no ctx and has no sx return. - type_resolver.zig:237 — leave CC
.default(a.nakedfn-pointer type has no CC of its own; nakedness is a decl-level emit attribute). - emit_llvm.zig:402 Pass 2 — B1.0a: bail loudly when
func.is_naked(build-gating). B1.0b: instead emit LLVM'snakedattr (shape pernounwindat emit_llvm.zig:1339) + the asm-only body (no prologue). - Any
.op/Function-field switch the Zig build flags — let the build tell you.
Phases (xfail→green steps)
B1.0 — abi(.naked) codegen — ✅ COMPLETE
- B1.0a (lock) — ✅ DONE. Carried
abi == .nakedinto IRFunction.is_naked; threaded throughdecl.zig(funcWantsImplicitCtxskips.nakedlike.c; all body-lowering paths bypasslowerValueBodyfor.naked, lowering the asm body + capping withunreachable) + generic.zig + pack.zig;emit_llvmPass 2 bailed loudly onfunc.is_naked. Locked byexamples/1800-concurrency-naked-asm.sx+ the generic regression (review-found gap). - B1.0b (green) — ✅ DONE.
emit_llvmdeclaration pass adds LLVMnaked+noinline+nounwindforfunc.is_nakedand skipsframe-pointer=all(incompatible with a frameless function); Pass 2 emits the body normally (naked⇒ verbatim asm + ownret, no prologue).1800pinned aarch64 → exit 42 +.ir;1801-concurrency-naked-generic.sx(renamed from-bail) proves the generic path emits a naked body (exit 42);1802-concurrency-naked-asm-x86.sxx86_64 cross sibling (ir-only here,.irlocksnakedmovl $42, %eax). Unit testemit: abi(.naked) function gets the naked attributeassertsnakedpresent +frame-pointerabsent. Suite green (724/0).
- B1.0c (review-hardening) — ✅ DONE. A param-bearing
.nakedfn emitted invalid LLVM (loud verifier error). Gated the param-alloca loop onfd.abi != .naked(decl.zig both paths + generic.zig) so a naked fn's args stay in registers (read by the asm body) — this enables B1.3'sswap_context(from, to). Locked by1803-concurrency-naked-asm-param.sx. Pack.naked(variadic + naked, nonsensical) left unsupported → loud verifier error.
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
API surface. xfail→green via an 18xx example exercising the blocking Io default (real
suspend lands in B1.3). No compiler change expected; if a protocol-in-context gap appears,
file it.
B1.3 — A2: fiber runtime (naked switch + bootstrap + guarded mmap stacks)
- B1.3a (switch-stress harness FIRST) — the standalone 2-fiber ping-pong harness
(register + canary survival, deep chains) per §10.7. This is A2's gate and predates the
scheduler + deterministic
Io. Arch-gated run test (matching-host run; ir-only elsewhere). - B1.3b — fiber bootstrap +
mmapstacks with guard pages (mandatory — §8.1.1). - (Cadence inside B1.3 follows lock→green per sub-piece; the asm switch is the highest-risk artifact — review adversarially, with a worker if authorized.)
B1.4 — A3: Io impls (blocking → deterministic-sim KEYSTONE → event-loop)
Blocking first; then the deterministic-sim Io, calibrated against blocking before any
18xx test trusts it; then the event loop. The deterministic Io is the test harness for
all of B1.5 + Stream B2.
B1.5 — A5: M:1 scheduler
End-to-end validation of the colorblind stack. 18xx corpus under the deterministic Io,
asserting program-emitted ordering contracts.
Gates
- B1.0: unit
emit_llvm.test.zig(thenakedattr present on a.nakedfn); two arch-gated examples (aarch64 + x86_64) run end-to-end on a matching host, ir-only on a mismatch (assertnaked+ asm in.ir). OUT of corpus scope, stated loudly: the correctness of any hand-written register save/restore — that's the B1.3 stress harness. - B1.1: an
18xxexample locking context-carried-by-slot-0 behavior + a checkpoint note on the spawn-trampoline convention. - B1.3: the switch-stress harness is A2's gate (register/canary survival — §10.7), NOT a run/snapshot test; plus arch-gated run tests.
- B1.4: deterministic
Iocalibrated against blockingIo(§8.1.3) before trusting it;18xxunder the deterministicIo. - B1.5:
18xxordering-contract snapshots under the deterministicIo.
Kickoff prompt (B1.0b — paste into a fresh session)
Implement Stream B1 step B1.0b (
abi(.naked)real emission) percurrent/PLAN-FIBERS.md. Verifyzig build && zig build testis green first (B1.0a is already landed:Function.is_nakedplumbed,decl.zigskips ctx + bypasses implicit-return for.naked,emit_llvmPass 2 bails loudly,examples/1800-concurrency-naked-asm.sxlocked to the bail). Then: (1) insrc/ir/emit_llvm.zigPass 2 (~line 402), REPLACE thefunc.is_nakedbail with real emission — set LLVM'snakedattribute on the function (LLVMGetEnumAttributeKindForName("naked", 5)→LLVMCreateEnumAttribute(ctx, id, 0)→LLVMAddAttributeAtIndex(llvm_func, -1, attr); shape per thenounwindset at emit_llvm.zig:1339) and emit the.nakedbody as its asm block only, no prologue/epilogue (the body already lowers to the inline-asm op + anunreachableterminator). (2) Pinexamples/1800-concurrency-naked-asm.sxaarch64 with a.buildsidecar{"target":"aarch64-macos"}; on this aarch64 host it runs end-to-end (exit 42), capture.ir+ regen (-Dname=examples/1800-concurrency-naked-asm.sx -Dupdate-goldens), review the diff (assert the.irshows thenakedattr +mov x0, #42/ret, NO stray error text). (3) Addexamples/1802-concurrency-naked-asm-x86.sx(x86_64 body,.build {"target":"x86_64-linux"}, ir-only on this host — requires its.ir, now producible). (4) Add a unit test insrc/ir/emit_llvm.test.zigasserting thenakedattribute is present on anabi(.naked)function. Confirmzig build testgreen, commit. NOTE: the.irproves the keyword + asm emitted, NOT register-save correctness (that's the B1.3 switch-stress harness). If you hit an UNRELATED compiler bug, fileissues/NNNN, markCHECKPOINT-FIBERS.mdBLOCKED, and STOP.