Files
sx/current/CHECKPOINT-FIBERS.md
agra a7fe165684 fibers: rename ABI variant .pure -> .naked
"pure" universally means side-effect-free (GCC __attribute__((pure)),
FP purity, D's pure) — the opposite of a register-clobbering context
switch. The concept is "naked": no compiler-generated prologue/epilogue,
body is raw asm that emits its own ret. That is the established term
everywhere (LLVM's naked function attribute — which we literally emit —
plus Zig callconv(.naked), Rust #[naked], GCC/Clang __attribute__
((naked))). Rename the keyword + everything keyed off it so concept,
surface, field, and the emitted LLVM attribute all agree.

- ast.zig: ABI enum variant pure -> naked (+ doc).
- parser: accept abi(.naked); error text updated.
- IR Function.is_pure -> is_naked; type_resolver/decl/generic/pack/
  emit_llvm references updated; diagnostics say abi(.naked).
- examples 1800-1803 renamed *-pure-* -> *-naked-* (source + expected/
  snapshots; .ir/.exit/.stdout/.stderr are byte-identical — the emitted
  IR is unchanged, only the keyword spelling differs).
- docs (PLAN-FIBERS, CHECKPOINT-FIBERS, PLAN-POST-METATYPE, the design
  roadmap, the compiler-API checkpoint/design) updated; the naming
  rationale now records why .naked over .pure.

No semantic change — pure cosmetics. Suite green (725/0).
2026-06-20 17:01:09 +03:00

11 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(.naked) real emission) — DONE. B1.0 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 backend emit it verbatim (the inline asm + its own ret) 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 () -> i64 call (.naked is call_conv == .default).
  • examples/1800-concurrency-naked-asm.sx — now GREEN, aarch64-pinned (.build {"target": "macos"}): runs end-to-end → exit 42 on this host, ir-only on a mismatch; .ir snapshot captured.
  • examples/1801-concurrency-naked-generic.sx (renamed from -bail) — the generic .naked now emits a correct naked answer__i64 (exit 42), proving generic.zig produces a naked body, not a framed one. aarch64-pinned.
  • examples/1802-concurrency-naked-asm-x86.sx — x86_64 cross sibling (.build {"target": "x86_64-linux"}, ir-only here): .ir locks naked + movl $42, %eax / ret.
  • Unit test emit: abi(.naked) function gets the naked attribute (no frame-pointer) in emit_llvm.test.zig (asserts naked present, frame-pointer absent).
  • B1.0c (review-hardening): a param-bearing .naked fn 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 on fd.abi != .naked in decl.zig (both paths) + generic.zig; a naked fn's args stay in registers (read by asm), declared-but-unused in LLVM. Locked by examples/1803-concurrency-naked-asm-param.sx (add(a,b) → x0+x1 → 42).
  • zig build && zig build test green: 725 ran, 0 failed + unit tests.

Earlier — B1.0a (lock + review hardening)

Plumbed Function.is_naked (set from fd.abi == .naked at both decl sites + generic.zig + pack.zig); funcWantsImplicitCtx skips .naked (no synthetic ctx, like .c); all body-lowering paths bypass lowerValueBody for .naked (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_naked gap (a generic .naked silently shipped a framed body); closed + locked. The review's .naked-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 completeabi(.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 allocafiber-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.
  • 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 ABI registers (read by the asm body), declared-but-unused in LLVM (verifier-legal). Example 1803-concurrency-naked-asm-param.sx (add(a,b) reads x0/x1). Unsupported (loud, not silent): a .naked variadic-pack fn (pack.zig's param loop is intertwined with comptime-param/#insert handling, 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 #builtin silently 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 Io does 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(.naked) naked [B1.0], atomics ) + fiber-safe codegen (context already fiber-local — B1.1). Schedulers, fibers, channels, futures, Io vtables, mmap stacks are all sx.
  • abi(.naked) is the real spelling of the design's callconv(.naked) — postfix slot, name :: (sig) -> Ret abi(.naked) { asm { … }; }. B1.0 = carry it into IR + emit LLVM naked + skip prologue/ctx (mirror the existing .c skip), NOT extend the enum (it's already there, just inert).
  • .naked.c: a .c epilogue would restore SP from the wrong stack across a context switch (SP-in ≠ SP-out by design). .naked = no prologue/epilogue/frame; the asm emits its own ret. This is why the switch must be .naked.
  • Naming: sx-facing name is naked (keyword abi(.naked), field is_naked, the diagnostic), matching LLVM's naked attribute and the industry term (Zig/Rust/GCC/Clang). The ABI variant was renamed .pure → .naked (user direction): "pure" universally means side-effect-free, the opposite of a register-clobbering context switch.
  • B1.0 snapshot scope: a .naked body is raw per-arch asm; LLVM's naked attr text is arch-invariant. B1.0a = one host example locked to the emit bail (host-independent — fires before instruction selection; no .build pin). B1.0b = pin aarch64 + add an 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.
  • 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 scheduling tests. Both must exist + be calibrated before the async tests they gate are trusted. 18xx asserts program-emitted ordering contracts, not raw interleaving.

Log

  • carve — wrote PLAN-FIBERS.md + CHECKPOINT-FIBERS.md. Grounded the B1 compiler floor: ABI.naked inert (type_resolver.zig:237), IR Function has no naked flag (inst.zig:605), attribute API pattern (emit_llvm.zig:1339 nounwind), .c ctx-skip precedent (decl.zig:515), push Context stack-alloca + slot-0 implicit ctx (stmt.zig:1263, lower.zig:259), __sx_default_context root (decl.zig:2667/2815), inline-asm corpus (1645/1651). Corrected the design's callconv(.naked) → real abi(.naked) spelling and the B1.0 snapshot story. B1.1 grounded as likely library-only. Baseline green (721/0).
  • B1.0a — plumbed Function.is_naked (set from fd.abi == .naked at both decl sites); funcWantsImplicitCtx skips .naked (no implicit ctx, like .c); both body-lowering paths bypass lowerValueBody for .naked (asm body + unreachable cap — no sx return); emit_llvm Pass 2 bails loudly on func.is_naked. examples/1800-concurrency-naked-asm.sx locked to the bail (exit 1 + diagnostic). Suite green (722/0). (ABI variant later renamed .pure → .naked — see the Naming decision above — so all is_*/abi(.*)/example names here read naked.)
  • B1.0a review-hardening — adversarial review found generic/pack Function-creation paths left is_naked false (silent framed body for a generic .naked instance — returned 42 but corrupted the stack). Fixed generic.zig + pack.zig (set is_naked + asm-only unreachable cap); locked by examples/1801-concurrency-naked-generic-bail.sx. The review's .naked- lambda CRITICAL was a false positive (unparseable — isLambda breaks on abi). Suite green (723/0).
  • B1.0b — real naked emission: emit_llvm declaration pass adds LLVM naked/noinline/ nounwind + skips frame-pointer for func.is_naked; Pass 2 emits the body verbatim (no prologue). 1800 green aarch64-pinned (exit 42 + .ir); renamed 1801-generic (generic .naked emits a naked body, exit 42); added x86_64 sibling 1802 (ir-only, .ir locks naked + movl $42, %eax). Unit test asserts naked present + frame-pointer absent. Suite green (724/0).
  • B1.0c — review-hardening: param-bearing .naked emitted invalid LLVM (loud verifier 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).