Adversarial review of B1.0b found a param-bearing abi(.pure) function
emitted invalid LLVM ("cannot use argument of naked function" — loud
verifier error, not silent) because the param-alloca loop spilled the
args to stack slots, which a naked function cannot have.
Fixed forward — this ENABLES the B1.3 context-switch use case rather
than rejecting it: gate the param-alloca loop on fd.abi != .pure in
decl.zig (both body-lowering paths) and generic.zig. A naked function's
args stay in their ABI registers and are read directly by the asm body
(e.g. swap_context reads from/to from x0/x1); the LLVM args are
declared-but-unused, which the verifier allows.
examples/1803-concurrency-pure-asm-param.sx: naked add(a, b) reads x0/x1
(add x0, x0, x1; ret) -> 40 + 2 = 42. aarch64-pinned.
Pack abi(.pure) (variadic + naked — nonsensical, can't read a runtime
pack from registers) left unsupported: pack.zig's param loop is
intertwined with comptime-param/#insert handling, so that case still
hits the loud verifier error. Documented in the checkpoint.
Also updates PLAN-FIBERS / CHECKPOINT-FIBERS for B1.0 completion.
B1.0 complete. Suite green (725/0).
10 KiB
CHECKPOINT-FIBERS — Stream B1 (fibers + Io + M:1 scheduler)
Companion to PLAN-FIBERS.md. Update after every step (one step at a time,
per the cadence rule). New corpus category: 18xx concurrency.
Last completed step
B1.0b (abi(.pure) real emission) — DONE. B1.0 complete. Replaced the emit bail with
real LLVM naked emission:
emit_llvmdeclaration pass: forfunc.is_pure, add the LLVMnaked+noinline+nounwindattributes and skip theframe-pointer=allattribute (incompatible with a frameless function). Pass 2 now emits the.purebody normally —nakedmakes the backend emit it verbatim (the inline asm + its ownret) with no prologue/epilogue.- IR shape (verified):
; Function Attrs: naked noinline nounwind/define internal i64 @answer() #0 { entry: call void asm sideeffect "…ret…", ""() unreachable }/attributes #0 = { naked noinline nounwind }. The caller invokes it as an ordinary() -> i64call (.pureiscall_conv == .default). examples/1800-concurrency-pure-asm.sx— now GREEN, aarch64-pinned (.build {"target": "macos"}): runs end-to-end → exit 42 on this host, ir-only on a mismatch;.irsnapshot captured.examples/1801-concurrency-pure-generic.sx(renamed from-bail) — the generic.purenow emits a correct nakedanswer__i64(exit 42), proving generic.zig produces a naked body, not a framed one. aarch64-pinned.examples/1802-concurrency-pure-asm-x86.sx— x86_64 cross sibling (.build {"target": "x86_64-linux"}, ir-only here):.irlocksnaked+movl $42, %eax/ret.- Unit test
emit: abi(.pure) function gets the naked attribute (no frame-pointer)inemit_llvm.test.zig(assertsnakedpresent,frame-pointerabsent). - B1.0c (review-hardening): a param-bearing
.purefn emitted invalid LLVM (loud verifier error "cannot use argument of naked function") because the param-alloca loop wasn't gated. Fixed forward (this enables the B1.3 context-switch use case rather than rejecting it): gated the param-alloca loop onfd.abi != .purein decl.zig (both paths) + generic.zig; a naked fn's args stay in registers (read by asm), declared-but-unused in LLVM. Locked byexamples/1803-concurrency-pure-asm-param.sx(add(a,b)→ x0+x1 → 42). zig build && zig build testgreen: 725 ran, 0 failed + unit tests.
Earlier — B1.0a (lock + review hardening)
Plumbed Function.is_pure (set from fd.abi == .pure at both decl sites + generic.zig +
pack.zig); funcWantsImplicitCtx skips .pure (no synthetic ctx, like .c); all
body-lowering paths bypass lowerValueBody for .pure (asm body + unreachable cap — no sx
return); emit_llvm Pass 2 bailed loudly (since flipped to real emission). Adversarial
review caught the generic/pack is_pure gap (a generic .pure silently shipped a framed
body); closed + locked. The review's .pure-lambda CRITICAL was a false positive
(unparseable — isLambda breaks on the abi keyword).
Current state
Stream A (atomics) is feature-complete (✅). Stream B1: B1.0 complete — abi(.pure)
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:
contextis already an implicit*Contextparam (slot 0) +push Contextis a stackalloca⇒ fiber-local for free. Only shared root =__sx_default_contextglobal (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.- Inline asm works end-to-end (lower→emit→JIT, aarch64 + x86_64) — the
.purebody reuses it. .purewith PARAMS works (B1.0c, the B1.3 substrate): the param-alloca loop is gated onfd.abi != .purein decl.zig (both paths) + generic.zig — a naked fn's args stay in ABI registers (read by the asm body), declared-but-unused in LLVM (verifier-legal). Example1803-concurrency-pure-asm-param.sx(add(a,b)reads x0/x1). Unsupported (loud, not silent): a.purevariadic-pack fn (pack.zig's param loop is intertwined with comptime-param/#inserthandling, and a naked fn can't read a runtime-sized pack from registers anyway) → loud LLVM-verifier error for that nonsensical construct. Acceptable 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.
Known issues / capability gaps
- Orthogonal (not a B1 blocker): default VALUES for comptime params don't bind on generic-struct methods (free-fn defaults DO work) — inherited from Stream A. Only matters if a B2 lib type wants a defaulted comptime param; atomics/fibers require explicit, so unaffected.
- Issue 0144 (open, independent): calling an unrecognized bodiless
#builtinsilently returns 0 / exit 0 — a silent-fallback footgun in the generic builtin-call path. Filed; leave for its own fix session unless prioritized. Not a B1 blocker. - Deferred design gap (documented): the B1.4 event-loop
Iodoes not yet cooperate with a platform UI run loop (CFRunLoop/NSRunLoop/ALooper); pinning gives thread-affinity, not run-loop integration — a §6 app-target concern, out of B1 scope.
Decisions (Stream B1 specifics; surface locked in design §4 / §4.6)
- The async runtime is sx LIBRARY code. The compiler provides only: the general
primitives (inline asm ✅,
abi(.pure)naked [B1.0], atomics ✅) + fiber-safe codegen (contextalready fiber-local — B1.1). Schedulers, fibers, channels, futures,Iovtables,mmapstacks are all sx. abi(.pure)is the real spelling of the design'scallconv(.naked)— postfix slot,name :: (sig) -> Ret abi(.pure) { asm { … }; }. B1.0 = carry it into IR + emit LLVMnaked+ skip prologue/ctx (mirror the existing.cskip), NOT extend the enum (it's already there, just inert)..pure≠.c: a.cepilogue would restore SP from the wrong stack across a context switch (SP-in ≠ SP-out by design)..pure= no prologue/epilogue/frame; the asm emits its ownret. This is why the switch must be.pure.- Naming: sx-facing name is
pure(fieldis_pure, the diagnostic). LLVM'snakedfunction attribute is only the lowering mechanism (B1.0b) — do not call the function "naked" (user direction). - B1.0 snapshot scope: a
.purebody is raw per-arch asm; LLVM'snakedattr text is arch-invariant. B1.0a = one host example locked to the emit bail (host-independent — fires before instruction selection; no.buildpin). B1.0b = pin aarch64 + add an x86_64 cross sibling (.buildtarget-gated, ir-only on mismatch), like the asm corpus split. The.irproves thenakedattr + 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. - Test keystones (design §10): the B1.3 switch-stress harness gates the
context-switch (the one piece the deterministic
Iocan't test — §8.1.1, §10.7); the B1.4 deterministic-simIo(calibrated against blockingIo— §8.1.3) gates all scheduling tests. Both must exist + be calibrated before the async tests they gate are trusted.18xxasserts program-emitted ordering contracts, not raw interleaving.
Log
- carve — wrote PLAN-FIBERS.md + CHECKPOINT-FIBERS.md. Grounded the B1 compiler floor:
ABI.pureinert (type_resolver.zig:237), IRFunctionhas no naked flag (inst.zig:605), attribute API pattern (emit_llvm.zig:1339 nounwind),.cctx-skip precedent (decl.zig:515),push Contextstack-alloca + slot-0 implicit ctx (stmt.zig:1263, lower.zig:259),__sx_default_contextroot (decl.zig:2667/2815), inline-asm corpus (1645/1651). Corrected the design'scallconv(.naked)→ realabi(.pure)spelling and the B1.0 snapshot story. B1.1 grounded as likely library-only. Baseline green (721/0). - B1.0a — plumbed
Function.is_pure(set fromfd.abi == .pureat both decl sites);funcWantsImplicitCtxskips.pure(no implicit ctx, like.c); both body-lowering paths bypasslowerValueBodyfor.pure(asm body +unreachablecap — no sx return);emit_llvmPass 2 bails loudly onfunc.is_pure.examples/1800-concurrency-pure-asm.sxlocked to the bail (exit 1 + diagnostic). Renamedis_naked→is_pureper user direction (sx sayspure, not "naked"; LLVMnakedattr is only the B1.0b mechanism). Suite green (722/0). - B1.0a review-hardening — adversarial review found generic/pack Function-creation paths
left
is_purefalse (silent framed body for a generic.pureinstance — returned 42 but corrupted the stack). Fixed generic.zig + pack.zig (setis_pure+ asm-onlyunreachablecap); locked byexamples/1801-concurrency-pure-generic-bail.sx. The review's.pure- lambda CRITICAL was a false positive (unparseable —isLambdabreaks onabi). Suite green (723/0). - B1.0b — real
nakedemission: emit_llvm declaration pass adds LLVMnaked/noinline/nounwind+ skipsframe-pointerforfunc.is_pure; Pass 2 emits the body verbatim (no prologue).1800green aarch64-pinned (exit 42 +.ir); renamed1801→-generic(generic.pureemits a naked body, exit 42); added x86_64 sibling1802(ir-only,.irlocksnaked+movl $42, %eax). Unit test assertsnakedpresent +frame-pointerabsent. Suite green (724/0). - B1.0c — review-hardening: param-bearing
.pureemitted invalid LLVM (loud verifier error). Gated the param-alloca loop onfd.abi != .pure(decl.zig both paths + generic.zig) — naked args stay in registers, read by the asm body (the B1.3 context-switch shape). Locked byexamples/1803-concurrency-pure-asm-param.sx. Pack.pureleft unsupported (loud, nonsensical). B1.0 complete. Suite green (725/0). Next: B1.1 (per-fiber context, probe-first).