Files
sx/current/CHECKPOINT-FIBERS.md
agra dd532ab7b2 fibers B1.3b: mmap guard-page fiber stacks (x86_64 switch sibling deferred)
Fiber stacks are now mmap'd with a PROT_NONE guard page at the low end: mmap a
[guard | usable] region and mprotect the low 16KB page PROT_NONE, so a stack
overflow faults at the guard boundary instead of silently corrupting a neighbor
(design 8.1.1 — fixed stacks without a guard corrupt silently on overflow).

Locked by examples/1809-concurrency-fiber-guard-stack.sx (aarch64-macos-pinned):
guard armed: 1 (mprotect -> 0) + sum: 20100 (a fiber runs real recursion on the
guarded stack and yields). The guard FIRING is validated manually (a fiber
recursing past its 128KB stack faults with Bus error at region+GUARD, exit 134
via the sx crash handler) — not corpus-pinned, since a deliberate-overflow crash
is host-fragile and a 'child faulted' fork test would not prove the boundary
catch specifically.

The x86_64 swap_context sibling is DEFERRED: sx build --target x86_64-macos
mislinks on this arm64 host (object x86_64, link step arm64) and x86_64-linux
can't run here, so it could only ship IR-only / unrun. For the highest-
corruption-risk asm, shipping un-run / un-negative-controlled code violates the
design 10.7 'correctness not existence' rule. SysV target notes (rbx/rbp/r12-r15
/rsp, no callee-saved XMM, rsp-carried return address) recorded for a future
x86_64 host. Suite green 735/0.
2026-06-21 06:51:29 +03:00

36 KiB
Raw Blame History

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.3b (mmap guard-page stacks) — DONE. x86_64 switch sibling DEFERRED (not runnable on this host). Fiber stacks are now mmap'd with a PROT_NONE GUARD PAGE at the low end (§8.1.1: a fixed stack without a guard silently corrupts neighbors on overflow). mmap the [guard | usable] region, mprotect the low 16KB page PROT_NONE; SP descends into the guard and faults loudly at the boundary instead of corrupting a neighbor. Locked by examples/1809-concurrency-fiber-guard-stack.sx (aarch64-macos-pinned): guard armed: 1 (mprotect→0) + sum: 20100 (a fiber runs real recursion on the guarded stack + yields).

  • Guard FIRING validated (manually, not corpus-pinned — a deliberate overflow crash is host-fragile): a fiber recursing past its 128KB stack faults with Bus error at the guard page (region+GUARD); the sx crash handler turns it into exit 134. Documented in the example header.
  • x86_64 swap_context sibling DEFERRED: sx build --target x86_64-macos mislinks on this arm64 host (object is x86_64 but the link step targets arm64), and --target x86_64-linux can't run here either — so the x86_64 switch could only ship IR-only, UNRUN. For the single highest-corruption-risk asm, shipping un-run/un-negative-controlled asm violates the §10.7 "correctness not existence" rule. Deferred until an x86_64 host (or working cross-run) is available. The aarch64 switch + its §10.7 gate are complete and reviewed; portability is the only gap. SysV target notes recorded in Next step.

Earlier — B1.3a-2 — the context-switch STRESS GATE (design §10.7) — DONE + adversarially reviewed

The explicit every-callee-saved-register scribble that B1.3a-1 owed. swap_context now saves the COMPLETE AAPCS64 callee-saved set — integer x19-x28 + fp/lr + sp AND FP d8-d15 (per §6.1.2 only the low 64 bits of v8-v15 are callee-saved, so d8-d15 is exactly sufficient; x18 is Apple's reserved platform reg, untouched). A naked scribble_verify(self_ctx, peer, base) loads a unique sentinel into all 18 callee-saved regs, yields, and on resume counts the ones that didn't survive (honoring its own caller ABI via a 176-byte frame that saves+restores the caller's callee-saved; base reloaded from the frame post-swap; the original lr round-trips through the swap). The gate is a 2-fiber MUTUAL scribble (A and B scribble DISTINCT sentinels into the same physical regs, so each survives only if swap_context saved+restored it — a lone fiber yielding to an idle peer would NOT exercise preservation). Locked by examples/1808-concurrency-fiber-switch-stress.sx (aarch64-pinned): A mismatches: 0 / B mismatches: 0.

  • Validity proven by NEGATIVE controls: dropping the d8-d15 save/restore → 8/8 mismatches (exactly the FP regs); dropping x27/x28 → 2/2. The gate genuinely catches a broken switch.
  • Adversarial review (worker, per the plan): no CRITICAL bugs. Verified the callee-saved set is complete + correct, all frame offsets/16-alignment, the lr/sp dance, and swap read-ordering against AAPCS64. Applied its one recommendation: boot now zeroes the FP ctx slots [13..20] so a first switch-to loads 0 (not garbage) into d8-d15. Residual gaps it flagged (all spec-correct for a call-boundary swap, documented in the example header): NZCV/FPSR not swapped; FPCR (rounding mode — thread-global, bleeds across fibers if changed) and TPIDR_EL0/TLS (errno, allocator thread-caches — shared by same-thread fibers) not swapped; fp=0 bootstrap blocks unwind/signal walking past a fiber trampoline. These bite at the N×M:1 / signals stages, not the single-thread switch.
  • Suite green 734/0, master clean. WIP probes: .sx-tmp/scribble2.sx (+ _broken/_gp).

Earlier — B1.3a-1 — the foundational stackful context switch (commit b234b7d)

Pure sx over abi(.naked): naked swap_context (GP-only 13-slot save) + by-hand fiber bootstrap (SP = alloc_bytes stack top, LR = global-asm trampoline, x19 = *Fiber). Locked by examples/1807-concurrency-fiber-context-switch.sx: 2-fiber ping-pong (rounds: 6 / canary fails: 0) + 64-frame deep recursion (frames verified: 64 / depth fails: 0). Indirect register/stack survival; 1808 supersedes its switch with the complete GP+FP save area + the explicit gate.

Earlier — B1.2 COMPLETE — the async surface works end-to-end

All three surface blockers (0151, 0152, 0153) FIXED + committed; async examples landed + green.

  • 0151 fixed (362674f): generic $T infers through generic-struct / pointer / UFCS-pack params. Regression 0214 + 0215.
  • 0152 fixed (e5586f6): Atomic(bool) load/store byte-promoted to i8 in the codegen emitters. Regression 1705.
  • 0153 fixed (68c1991): inferGenericReturnType now pins return-type resolution to the fn's DEFINING module (mirroring monomorphizeFunction), so a re-exported value-failable's !E resolves to the real .error_set TypeId — the failable channel survives the re-export alias. Regression 1058-errors-reexport-value-failable-channel.sx.
  • Async examples landed: examples/1805-concurrency-io-blocking-async.sx (context.io.async((a,b)->i64 => a+b, 40, 2).await() or {…}sum: 42 / double: 42 / clock ok) + examples/1806-concurrency-io-cancel.sx (f.cancel()await raises .Canceledor default; ok: 7 / canceled: -99). Both green, snapshots captured.

Earlier — the three B1.2 surface fixes (committed)

Generic $T inference, Atomic(bool) byte-promotion, and re-export failable-channel pin — details below.

  • 0151 fix (committed): four gaps closed on the inference + UFCS-dispatch path — (1) extractTypeParam/matchTypeParam(Static) got a parameterized_type_expr arm (recover the arg instance's recorded per-param bindings via struct_instance_bindings + the template's ordered type_params, recurse positionally; this also fixes *Box($T) — it recurses into its Box($T) pointee); (2) the pointer_type_expr arm now falls through to match the pointee against a non-pointer arg (auto-address-of: a *Box($T) param accepts a by-value Box($T), e.g. a UFCS receiver b.m()); (3) ExprTyper.inferType got a .lambda arm building the closure type from the lambda's annotations (the UFCS binder types args from the raw AST before they're lowered, so it can now bind Closure(..) -> $R from the worker's declared return type); (4) a pack UFCS target routes through the SAME lowerPackFnCall the direct call uses, with the receiver spliced in as args[0].
  • Regression tests: examples/0214-generics-ufcs-closure-return-pack.sx (direct + UFCS closure-return pack) + examples/0215-generics-infer-through-pointer.sx (by-value / pointer / multi-param / nested / UFCS-auto-ref struct-head inference). Issue 0151 marked RESOLVED; repro moved into the suite.

Earlier — B1.2 (Io capability) — LANDED + adversarially reviewed

Commits a1b14f0 (lock) + 45d869d (Io capability) + 3eeb965 (issue 0151 lock).

  • LANDED + review-confirmed correct (commit 45d869d): Io :: protocol #inline (spawn_raw/suspend_raw/ready/poll/now_ms/arm_timer) + io field on Context ({allocator; data; io}, io LAST); BOTH __sx_default_context materializers (protocol.zig + comptime_vm.zig) build an identical CBlockingIo→Io vtable (review verified byte-for-byte agreement; context.io.now_ms() dispatches at runtime AND comptime); the push Context.{…} omitted-field-inherits-ambient fix (review: correct, right fix, no bad blast radius); library/modules/std/io.sx (Future($R), CBlockingIo, async/await/cancel); the !-protocol-impl-lint suppression; 37 .ir regens (review: pure layout/type-table, no error text, zero .exit/.stdout/.stderr change).
  • BLOCKED — async surface non-functional: await/cancel take *Future($R) and are uncallable in EVERY form (not just UFCS) — sx can't infer a generic $T from a pointer-wrapped arg (*Future($R)). async(...) (create) works via explicit call and produces a correct .ready Future, but you can't await it. Root bug = issue 0151 (WIDENED): infer $T from *T-wrapped params + closure-return-via-pack + UFCS dispatch. Minimal repro: unbox :: (b: *Box($T)) -> $T fails to infer T.
  • No async example in the corpus (1805 was removed because it needs the blocked surface) → the green suite does NOT cover async. Restore 1805 (async/await) + add 1806 (cancel) once 0151 is fixed.

Earlier — 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 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

B1.2 COMPLETE. The full async surface (Io capability on Context + async/await/cancel + blocking CBlockingIo) works end-to-end. Master GREEN (732/0), installed sx clean. All four B1.2 surface bugs resolved or deferred:

  • 0151 fixed (362674f): generic $T through generic-struct / pointer / UFCS-pack params. Regression 0214 + 0215.
  • 0152 fixed (e5586f6): Atomic(bool) byte-promoted to i8 in the load/store emitters. Regression 1705.
  • 0153 fixed (68c1991): inferGenericReturnType pins return-type resolution to the fn's defining module, so a re-exported value-failable keeps its ! channel. Regression 1058.
  • Issue 0150 (void struct field → SIGTRAP) DEFERRED — only Future(void) / timeout, which are B1.4.

The async examples are landed + green: 1805 (async/await + now_mssum: 42 / double: 42 / clock ok) + 1806 (cancelawait raises .Canceledor default). The 18xx concurrency category now covers naked-asm (1800-1803), context-snapshot (1804), and the async surface (1805-1806).

B1.2 Io capability — what is LANDED + verified (commit 45d869d)

  • Io :: protocol #inline { spawn_raw; suspend_raw -> !; ready; poll; now_ms; arm_timer; } in core.sx next to Allocator, with SpawnOpts{ pin: PinTarget } + ParkToken{ handle }. Six methods, each justified by a downstream consumer (B1.3-B1.5).
  • Context :: struct { allocator; data; io: Io; }io appended LAST so allocator stays index 0 (the call.zig:1229 hardcode) and data keeps index 1 (minimal VM-fallback churn).
  • Both __sx_default_context materializers updated in lockstep + verified: protocol.zig emitDefaultContextGlobal (extended ctx_fields 2→3, built the CBlockingIo→Io inline 7-word vtable {null-ctx, fn0..fn5} via getOrCreateThunks("Io","CBlockingIo")) and comptime_vm.zig materializeDefaultContext fallback (wrote the 6 thunk func-refs at io_base = addr + 4*ps, offset + (i+1)*ps). The global path auto-followed the 3-field Context type. context.io.now_ms() printed clock ok live — the capability threads + the vtable dispatches correctly.
  • Stateless CBlockingIo :: struct {} + impl Io for CBlockingIo (mirror of CAllocator): blocking semantics — spawn_raw/ready/poll/arm_timer no-op/0, now_mstime.mono_ms().
  • push-inherit-omitted fix (stmt.zig lowerPush): a push Context.{...} now SEEDS the new slot from the ambient context (load+store), then overwrites ONLY the literal's named fields — so omitted fields (now incl. io) are INHERITED, never zero-inited to a null vtable. Eliminates the omitted-field footgun globally (zero per-site churn across the 17 partial-literal sites). This is the correct capability-bag semantics; it compiled clean.
  • !-protocol-method warning fix (error_analysis.zig + a new Lowering.impl_method_names set populated in protocols.zig registerImplBlock): a protocol impl method may be declared ! by contract (e.g. Io.suspend_raw) yet never raise; the "declared ! but never errors — drop the !" hint is a false positive for impl methods, now suppressed for them.

Status of the blockers that originally stopped B1.2:

  • issue 0151 — FIXED this session (generic $T through generic-struct / pointer / UFCS-pack params). async/await/cancel are callable. See "Last completed step".
  • issue 0152 — NEW, the current blocker (Atomic(bool) → sub-byte i1 atomic; LLVM reject). Blocks the async examples via Future.canceled: Atomic(bool). Filed; codegen-level fix.
  • issue 0150void struct field SIGTRAP; only Future(void)/timeout (B1.4). DEFERRED.

Per the IMPASSABLE STOP rule: 0151 fix shipped (suite green 728/0), 0152 filed, STOPPED. Resume B1.2's async examples once 0152 lands.

Earlier — B1.0 + B1.1 complete

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 allocafiber-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 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

Two open threads — pick by host availability:

(A) x86_64 swap_context sibling — needs an x86_64 host (or a working cross-run). The per-arch switch. SysV-AMD64 callee-saved = rbx, rbp, r12, r13, r14, r15 + rsp (6 GP + sp; no callee-saved XMM on SysV, unlike Win64) — so a 7-slot ctx and a different scribble_verify reg set. No link register: the return address rides each fiber's stack, so the switch is mov [from],regs… ; mov rsp,[to+48] ; ret (the final ret pops to's saved return addr). Bootstrap: push &_fib_tramp onto the new stack and set saved rsp to it (16-align: at the trampoline's call, rsp must be ≡0 mod 16). Args rdi/rsi/rdx; result rax. Carry the SAME 2-fiber mutual-scribble gate

  • negative-control discipline + adversarial review. Must be RUN + negative-controlled on a matching host — do NOT ship it ir-only/unrun (§10.7). (1802 is the x86_64 naked-asm template.)

(B) B1.4 — Io impls (blocking → deterministic-sim KEYSTONE → event-loop). The aarch64 substrate (switch + §10.7 gate + guarded stacks) is enough to build the scheduler on. B1.4 builds the deterministic-sim Io (calibrated against blocking Io before trusting it — §8.1.3), then B1.5 (M:1 scheduler) replaces the hand-bootstrapped ping-pong with real spawn/yield/ resume over the switch. The §10.7 gate (1808) + the guarded-stack path (1809) must keep passing as the switch is wrapped into the scheduler.

Deferred (do NOT block on these): issue 0150 (void struct field SIGTRAP) — only Future(void)/timeout (B1.4). The :: callable-parameter feature (named-fn async workers async(read_a, conn)) — WIP at .sx-tmp/wip-callable-params/patch.diff (parser done, inference incomplete); a dedicated effort; lambda workers are the idiom meanwhile.

Context layout settled: { allocator; data; io; } (allocator index 0 fixed by call.zig:1229, io last). Io protocol + materializers + push-inherit are LANDED + reviewed.

Known issues / capability gaps

  • issue 0153 — FIXED (re-exported generic value-failable ($R, !E) kept its ! channel: inferGenericReturnType now pins return-type resolution to the fn's defining module). Regression: examples/1058. Was the LAST B1.2 surface blocker.
  • issue 0152 — FIXED (Atomic(bool) sub-byte i1 atomic → byte-promoted to i8 in the load/store emitters). Regression: examples/1705. Unblocked Future.canceled.
  • issue 0151 — FIXED (generic $T through generic-struct / pointer / UFCS-pack params). Regression: examples/0214 + 0215. Was the original B1.2 surface blocker.
  • issue 0150 (deferred) — a void struct field crashes the compiler (unsized-type SIGTRAP in LLVM getTypeSizeInBits). Blocks Future(void)timeout (B1.4). Repro: issues/0150-....
    • (Note: issue 0149, filed by another session against an earlier dirty binary, was a manifestation of the pre-fix 0151 — now moot.)
  • 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 — 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 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).
  • 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).
  • B1.2 (BLOCKED) — built the full Io capability (protocol on Context, stateless CBlockingIo blocking default, both __sx_default_context materializers, push-inherit-omitted fix, !-impl-method warning fix) and VERIFIED the core works live (context.io.now_ms()clock ok). Two independent compiler bugs blocked the async/await/timeout layer: 0150 (void struct field → unsized SIGTRAP, blocks Future(void)) and 0151 (type-var from a fn-ptr param's return type not bound in the body, blocks async's Future(R)). Both filed with standalone repros + investigation prompts. Per the STOP rule: reverted ALL B1.2 working changes (master green again, 726/0; the dirty binary had broken the photo project — see the now-moot 0149), saved WIP to .sx-tmp/b12-wip/, STOPPED. Resume after 0150 + 0151.
  • 0151 FIXED — generic inference now binds $T through a generic-struct param head, a pointer (*Box($T), incl. UFCS auto-ref), and a closure-return-via-pack on the UFCS path. Four gaps closed: parameterized_type_expr arm in extractTypeParam/matchTypeParam(Static) (recovers the arg instance's recorded per-param bindings, recurses positionally); pointer arm falls through to match a value arg (auto-address-of); ExprTyper.inferType .lambda arm (closure type from annotations — UFCS types args from raw AST pre-lowering); pack UFCS target routes through lowerPackFnCall with the receiver spliced in as args[0]. Issue 0151 marked RESOLVED; repro → examples/0214-generics-ufcs-closure-return-pack.sx; widened cases → examples/0215-generics-infer-through-pointer.sx. Suite green 728/0. The now-callable async surface immediately exposed a SEPARATE codegen bug — issue 0152 (Atomic(bool) → sub-byte i1 atomic, LLVM reject; Future.canceled hits it). Filed with standalone repro + fix prompt. Per the STOP rule: shipped the 0151 fix, filed 0152, STOPPED. Resume the async examples (1805/1806) after 0152.
  • 0152 FIXED — the atomic load/store emitters (src/backend/llvm/ops.zig) byte-promote a sub-byte (booli1) access to its i8 storage type and trunc/zext the value at the boundary (new atomicByteType helper). rmw/cmpxchg left as-is (a bool rmw/CAS is rejected at the sx level — integer-only — so a sub-byte element never reaches them; comments record this). Regression examples/1705-atomics-bool-byte-promoted.sx (load/store round-trip). Issue 0152 marked RESOLVED. Suite green 729/0. With Atomic(bool) working, the async surface exposed the TRUE remaining blocker — issue 0153: a re-exported generic value-failable ($R, !E) loses its ! channel at the call site (the earlier "secondary or PHI" symptom was this, NOT an Atomic cascade — confirmed it persists after 0152). Narrowed to the generic+re-export co-requirement (non-generic re-export OK; direct generic import OK; only the combination drops !). Root cause: the monomorphized return-type's error-set, reached via the re-export alias, resolves to a non-.error_set TypeId, so errorChannelOf (lower/error.zig:148) misses the channel. Filed issues/0153-... with a minimal co-located 2-file repro + a single-file stdlib-await repro + investigation prompt. Per the STOP rule: shipped the 0152 fix, filed 0153, STOPPED. Resume the async examples after 0153.
  • 0153 FIXED → B1.2 COMPLETEinferGenericReturnType (src/ir/generics.zig) resolved the return-type AST in the CALL-SITE module, so a re-exported error set (LE :: lib.LE) resolved to a non-.error_set alias and the planned call-result was a plain tuple (channel lost). Fix: pin the source to fd.body.source_file around the return-type resolution, exactly as monomorphizeFunction does — the !E now resolves to the real .error_set. One-function change; full suite green (732/0), no regression. Issue 0153 RESOLVED; repro → examples/1058-errors-reexport-value-failable-channel.sx (+ companion lib.sx). With the channel preserved, landed the async examples: 1805 (async/await + now_mssum: 42 / double: 42 / clock ok) + 1806 (cancelawait raises .Canceledor default; ok: 7 / canceled: -99). B1.2 (Io capability + M:1 async surface) is COMPLETE. Next: B1.3 (fiber runtime) on the .naked context-switch substrate.
  • B1.3a-1 — context switch works. Implemented the stackful switch in pure sx over abi(.naked): swap_context(from, to) (save callee-saved x19-x28 + fp/lr + sp into *from, load from *to, ret onto to's stack) + by-hand fiber bootstrap (SP = top of an alloc_bytes stack, LR = a .global _fib_tramp global-asm trampoline that does mov x0, x19; bl _fib_body, x19 = *Fiber). Proven via a probe (main↔fiber), then locked by examples/1807-concurrency-fiber-context-switch.sx (aarch64-pinned): a 2-fiber ping-pong (rounds: 6, canary fails: 0 — a per-fiber stack canary survives every switch) + a 64-frame deep recursive chain suspended at the bottom and resumed (frames verified: 64 / depth fails: 0). The bl _fib_body reaches the sx body via export "fib_body" (the 1655 asm→sx pattern); runs under JIT, ir-only on a non-arm host (.ir captured — swap_context shows naked noinline nounwind). Suite green 733/0. Honest scope: indirect register/stack survival only; the EXPLICIT every-callee-saved + FP scribble (§10.7) is B1.3a-2, still owed. Next: B1.3a-2.
  • B1.3a-2 — the §10.7 stress gate, adversarially reviewed. Extended swap_context to the COMPLETE AAPCS64 callee-saved set (added FP d8-d15 → 21-slot ctx) and wrote a naked scribble_verify that loads a unique sentinel into all 18 callee-saved regs, yields, and counts non-survivors on resume (176-byte frame saves/restores the caller's callee-saved + base; lr round-trips the swap). The gate is a 2-fiber MUTUAL scribble (each clobbers the other's regs, so survival ⇒ the switch saved+restored them). Locked by examples/1808-concurrency-fiber-switch-stress.sx (A/B mismatches: 0). Validity proven by negative controls (drop d8-d15 → 8/8; drop x27/x28 → 2/2). Spawned an adversarial-review worker (per the plan + user request): NO critical bugs — callee-saved set complete (x18 rightly excluded; d8-d15 suffices per §6.1.2), offsets/alignment/lr-sp dance all verified. Applied its one rec: boot zeroes FP ctx slots so first-entry loads 0, not garbage. Honest residual gaps (spec-correct for a call-boundary swap; in the example header): FPCR/FPSR/NZCV + TPIDR/TLS not swapped, fp=0 blocks unwind — relevant at N×M:1 / signals, not here. Suite green 734/0. Next: B1.3b (x86_64 sibling + mmap guard-page stacks).
  • B1.3b — mmap guard-page stacks (x86_64 sibling deferred). Fiber stacks now mmap a [guard | usable] region and mprotect the low 16KB page PROT_NONE, so a stack overflow faults at the guard boundary instead of silently corrupting a neighbor (§8.1.1). Locked by examples/1809-concurrency-fiber-guard-stack.sx (aarch64-macos-pinned): guard armed: 1 (mprotect→0) + sum: 20100 (a fiber runs real recursion on the guarded stack + yields). Guard FIRING validated manually (overflow → Bus error at region+GUARD, exit 134 via the sx crash handler) — not corpus-pinned because a deliberate-overflow crash is host-fragile (and a mere "child faulted" fork test wouldn't prove the BOUNDARY catch). The x86_64 swap_context sibling was DEFERRED: --target x86_64-macos mislinks on this arm64 host and x86_64-linux can't run here, so it could only ship un-run/un-negative-controlled — which §10.7 forbids for the highest-risk asm. SysV target notes (rbx/rbp/r12-r15/rsp, no callee-saved XMM, rsp-carried return addr) recorded in Next step. Suite green 735/0. Next: x86_64 sibling (needs an x86_64 host) OR B1.4 (Io impls / scheduler) on the proven aarch64 substrate.