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.
36 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.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 errorat the guard page (region+GUARD); the sx crash handler turns it into exit 134. Documented in the example header. - x86_64
swap_contextsibling DEFERRED:sx build --target x86_64-macosmislinks on this arm64 host (object is x86_64 but the link step targets arm64), and--target x86_64-linuxcan'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:
bootnow 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$Tinfers through generic-struct / pointer / UFCS-pack params. Regression0214+0215. - 0152 fixed (
e5586f6):Atomic(bool)load/store byte-promoted toi8in the codegen emitters. Regression1705. - 0153 fixed (
68c1991):inferGenericReturnTypenow pins return-type resolution to the fn's DEFINING module (mirroringmonomorphizeFunction), so a re-exported value-failable's!Eresolves to the real.error_setTypeId — the failable channel survives the re-export alias. Regression1058-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()→awaitraises.Canceled→ordefault;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 aparameterized_type_exprarm (recover the arg instance's recorded per-param bindings viastruct_instance_bindings+ the template's orderedtype_params, recurse positionally; this also fixes*Box($T)— it recurses into itsBox($T)pointee); (2) thepointer_type_exprarm now falls through to match the pointee against a non-pointer arg (auto-address-of: a*Box($T)param accepts a by-valueBox($T), e.g. a UFCS receiverb.m()); (3)ExprTyper.inferTypegot a.lambdaarm 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 bindClosure(..) -> $Rfrom the worker's declared return type); (4) a pack UFCS target routes through the SAMElowerPackFnCallthe direct call uses, with the receiver spliced in asargs[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) +iofield onContext({allocator; data; io}, io LAST); BOTH__sx_default_contextmaterializers (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); thepush 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.irregens (review: pure layout/type-table, no error text, zero .exit/.stdout/.stderr change). - BLOCKED — async surface non-functional:
await/canceltake*Future($R)and are uncallable in EVERY form (not just UFCS) — sx can't infer a generic$Tfrom a pointer-wrapped arg (*Future($R)).async(...)(create) works via explicit call and produces a correct.readyFuture, but you can'tawaitit. Root bug = issue 0151 (WIDENED): infer$Tfrom*T-wrapped params + closure-return-via-pack + UFCS dispatch. Minimal repro:unbox :: (b: *Box($T)) -> $Tfails to inferT. - 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) + add1806(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 := contextcaptures the spawner'sContextas 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 — becausecontextis an implicit slot-0*Contextparam (call-carried, rides the callee's own stack) andpushallocates on the caller frame (no global, no TLS). - Locked by
examples/1804-concurrency-context-snapshot.sx: printsfiber root: 42(the installed snapshot wins over ambient 99) +ambient after: 99(thepushscope restores the ambient context on exit). No fiber runtime yet (that's B1.3) — this proves the plumbing it will build on. No.buildpin (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_contextmid-stack, so there is no compiler obligation here. zig build && zig build testgreen: 726 ran, 0 failed.
Earlier — B1.0 (abi(.naked) codegen) — complete
Replaced the emit bail with real LLVM naked emission:
emit_llvmdeclaration pass: forfunc.is_naked, add the LLVMnaked+noinline+nounwindattributes and skip theframe-pointer=allattribute (incompatible with a frameless function). Pass 2 now emits the.nakedbody 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 (.nakediscall_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;.irsnapshot captured.examples/1801-concurrency-naked-generic.sx(renamed from-bail) — the generic.nakednow emits a correct nakedanswer__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):.irlocksnaked+movl $42, %eax/ret.- Unit test
emit: abi(.naked) function gets the naked attribute (no frame-pointer)inemit_llvm.test.zig(assertsnakedpresent,frame-pointerabsent). - B1.0c (review-hardening): a param-bearing
.nakedfn 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 != .nakedin 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-naked-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_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$Tthrough generic-struct / pointer / UFCS-pack params. Regression0214+0215. - 0152 fixed (
e5586f6):Atomic(bool)byte-promoted toi8in the load/store emitters. Regression1705. - 0153 fixed (
68c1991):inferGenericReturnTypepins return-type resolution to the fn's defining module, so a re-exported value-failable keeps its!channel. Regression1058. - Issue 0150 (
voidstruct field → SIGTRAP) DEFERRED — onlyFuture(void)/timeout, which are B1.4.
The async examples are landed + green: 1805 (async/await + now_ms → sum: 42 /
double: 42 / clock ok) + 1806 (cancel → await raises .Canceled → or 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; }incore.sxnext toAllocator, withSpawnOpts{ pin: PinTarget }+ParkToken{ handle }. Six methods, each justified by a downstream consumer (B1.3-B1.5).Context :: struct { allocator; data; io: Io; }—ioappended LAST soallocatorstays index 0 (thecall.zig:1229hardcode) anddatakeeps index 1 (minimal VM-fallback churn).- Both
__sx_default_contextmaterializers updated in lockstep + verified:protocol.zigemitDefaultContextGlobal(extendedctx_fields2→3, built theCBlockingIo→Ioinline 7-word vtable{null-ctx, fn0..fn5}viagetOrCreateThunks("Io","CBlockingIo")) andcomptime_vm.zigmaterializeDefaultContextfallback (wrote the 6 thunk func-refs atio_base = addr + 4*ps, offset+ (i+1)*ps). The global path auto-followed the 3-field Context type.context.io.now_ms()printedclock oklive — the capability threads + the vtable dispatches correctly. - Stateless
CBlockingIo :: struct {}+impl Io for CBlockingIo(mirror ofCAllocator): blocking semantics —spawn_raw/ready/poll/arm_timerno-op/0,now_ms→time.mono_ms(). - push-inherit-omitted fix (
stmt.ziglowerPush): apush 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 newLowering.impl_method_namesset populated inprotocols.zigregisterImplBlock): 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
$Tthrough generic-struct / pointer / UFCS-pack params).async/await/cancelare callable. See "Last completed step". - issue 0152 — NEW, the current blocker (
Atomic(bool)→ sub-byte i1 atomic; LLVM reject). Blocks the async examples viaFuture.canceled: Atomic(bool). Filed; codegen-level fix. - issue 0150 —
voidstruct field SIGTRAP; onlyFuture(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:
contextis an implicit slot-0*Contextparam +push Contextis a stackalloca⇒ fiber-local for free (confirmed by the B1.1 probe — never TLS, never re-read from the__sx_default_contextglobal mid-stack). A spawn passes the snapshot as the fiber-entry fn's slot-0 ctx viapush f.root { entry(args) }. Locked by1804-...-context-snapshot.- Inline asm works end-to-end (lower→emit→JIT, aarch64 + x86_64) — the
.nakedbody reuses it. .nakedwith PARAMS works (B1.0c, the B1.3 substrate): the param-alloca loop is gated onfd.abi != .nakedin 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-naked-asm-param.sx(add(a,b)reads x0/x1). Unsupported (loud, not silent): a.nakedvariadic-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
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:inferGenericReturnTypenow 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. UnblockedFuture.canceled. - ✅ issue 0151 — FIXED (generic
$Tthrough generic-struct / pointer / UFCS-pack params). Regression:examples/0214+0215. Was the original B1.2 surface blocker. - issue 0150 (deferred) — a
voidstruct field crashes the compiler (unsized-type SIGTRAP in LLVMgetTypeSizeInBits). BlocksFuture(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
#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(.naked)naked [B1.0], atomics ✅) + fiber-safe codegen (contextalready fiber-local — B1.1). Schedulers, fibers, channels, futures,Iovtables,mmapstacks are all sx. abi(.naked)is the real spelling of the design'scallconv(.naked)— postfix slot,name :: (sig) -> Ret abi(.naked) { 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)..naked≠.c: a.cepilogue 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 ownret. This is why the switch must be.naked.- Naming: sx-facing name is
naked(keywordabi(.naked), fieldis_naked, the diagnostic), matching LLVM'snakedattribute 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
.nakedbody 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 — 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 — snapshotcontext, 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, andpushrestores 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
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.nakedinert (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(.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 fromfd.abi == .nakedat both decl sites);funcWantsImplicitCtxskips.naked(no implicit ctx, like.c); both body-lowering paths bypasslowerValueBodyfor.naked(asm body +unreachablecap — no sx return);emit_llvmPass 2 bails loudly onfunc.is_naked.examples/1800-concurrency-naked-asm.sxlocked to the bail (exit 1 + diagnostic). Suite green (722/0). (ABI variant later renamed.pure → .naked— see the Naming decision above — so allis_*/abi(.*)/example names here readnaked.) - B1.0a review-hardening — adversarial review found generic/pack Function-creation paths
left
is_nakedfalse (silent framed body for a generic.nakedinstance — returned 42 but corrupted the stack). Fixed generic.zig + pack.zig (setis_naked+ asm-onlyunreachablecap); locked byexamples/1801-concurrency-naked-generic-bail.sx. The review's.naked- 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_naked; Pass 2 emits the body verbatim (no prologue).1800green aarch64-pinned (exit 42 +.ir); renamed1801→-generic(generic.nakedemits 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
.nakedemitted invalid LLVM (loud verifier error). Gated the param-alloca loop onfd.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 byexamples/1803-concurrency-naked-asm-param.sx. Pack.nakedleft 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
contextroot: zero compiler change (probe-confirmed). The spawn convention (snapshotcontext→ store in a struct →push f.root { entry() }from the trampoline) installs the fiber's root via the implicit slot-0*Contextparam; the body reads the snapshot, not the trampoline's ambient ctx, and thepushscope restores ambient on exit. Locked byexamples/1804-concurrency-context-snapshot.sx(printsfiber root: 42/ambient after: 99). Suite green (726/0). Next: B1.2 (Io interface + context.io). - B1.2 (BLOCKED) — built the full
Iocapability (protocol onContext, statelessCBlockingIoblocking default, both__sx_default_contextmaterializers, 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 theasync/await/timeoutlayer: 0150 (voidstruct field → unsized SIGTRAP, blocksFuture(void)) and 0151 (type-var from a fn-ptr param's return type not bound in the body, blocksasync'sFuture(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
$Tthrough 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_exprarm inextractTypeParam/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.lambdaarm (closure type from annotations — UFCS types args from raw AST pre-lowering); pack UFCS target routes throughlowerPackFnCallwith the receiver spliced in asargs[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.canceledhits 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 (bool→i1) access to itsi8storage type andtrunc/zextthe value at the boundary (newatomicByteTypehelper). rmw/cmpxchg left as-is (aboolrmw/CAS is rejected at the sx level — integer-only — so a sub-byte element never reaches them; comments record this). Regressionexamples/1705-atomics-bool-byte-promoted.sx(load/store round-trip). Issue 0152 marked RESOLVED. Suite green 729/0. WithAtomic(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 "secondaryorPHI" symptom was this, NOT anAtomiccascade — 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_setTypeId, soerrorChannelOf(lower/error.zig:148) misses the channel. Filedissues/0153-...with a minimal co-located 2-file repro + a single-file stdlib-awaitrepro + investigation prompt. Per the STOP rule: shipped the 0152 fix, filed 0153, STOPPED. Resume the async examples after 0153. - 0153 FIXED → B1.2 COMPLETE —
inferGenericReturnType(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_setalias and the planned call-result was a plain tuple (channel lost). Fix: pin the source tofd.body.source_filearound the return-type resolution, exactly asmonomorphizeFunctiondoes — the!Enow 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(+ companionlib.sx). With the channel preserved, landed the async examples:1805(async/await+now_ms→sum: 42/double: 42/clock ok) +1806(cancel→awaitraises.Canceled→ordefault;ok: 7/canceled: -99). B1.2 (Io capability + M:1 async surface) is COMPLETE. Next: B1.3 (fiber runtime) on the.nakedcontext-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,retontoto's stack) + by-hand fiber bootstrap (SP = top of analloc_bytesstack, LR = a.global _fib_trampglobal-asm trampoline that doesmov x0, x19; bl _fib_body, x19 =*Fiber). Proven via a probe (main↔fiber), then locked byexamples/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). Thebl _fib_bodyreaches the sx body viaexport "fib_body"(the 1655 asm→sx pattern); runs under JIT, ir-only on a non-arm host (.ircaptured —swap_contextshowsnaked 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_contextto the COMPLETE AAPCS64 callee-saved set (added FP d8-d15 → 21-slot ctx) and wrote a nakedscribble_verifythat 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 byexamples/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:bootzeroes 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
mmapa[guard | usable]region andmprotectthe low 16KB pagePROT_NONE, so a stack overflow faults at the guard boundary instead of silently corrupting a neighbor (§8.1.1). Locked byexamples/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 erroratregion+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_64swap_contextsibling was DEFERRED:--target x86_64-macosmislinks on this arm64 host andx86_64-linuxcan'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 (Ioimpls / scheduler) on the proven aarch64 substrate.