std.sync Lands, and the Compiler Grows a Conscience
Junior Dev Nugget; principle: Make the invariant explicit before coding.; likely mistake: Shipping behavior without proving the failure mode.; read next: Closest RFC/spec linked in References.
Word count receipt: 1438 words.
What changed
The Janus standard library acquired a concurrency substrate. In one calendar day, five sprints closed. The compiler fixed two miscompiles that would have silently corrupted data in production. And a roadmap correction drew a line between what unlocks the next build and what merely looks good on a slide.
Here is what shipped.
std.sync: the full substrate
std.sync.atomics- typedAtomic[T]wrapper over SPEC-059 intrinsics. Six memory orderings.compare_exchange,fetch_op, fences. TheAtomicEligibletrait gates the type parameter. Zero runtime overhead vs hand-written intrinsics: comptimeOrderingfolds to a single@intrinsicat monomorphization. Shipped in Sprint N+2.std.sync.parker- permit-model thread-parking primitive. Linux futex backend (SYS_futex=202, x86_64 v0.1).park(),park_timeout(ns),unpark(). The permit model eliminates the lost-wakeup window: an unpark deposited before the next park is consumed is not lost. Shipped in Sprint N+3.std.sync.mutex-Mutex[T]+MutexGuard[T]. Drepper Take-3 algorithm. Ships as PREVIEW: code is canonically correct, consumer-blocked on a foundational pointer-return-from-function compiler gap that surfaced during the sprint. The public surface is frozen; when the gap closes, existing code compiles without changes.std.sync.chan- bounded SPSC ring-buffer channel on caller-provided[]Tbacking.chan_send,chan_recv,chan_close,try_send,try_recv_into. v0.2 is single-producer single-consumer. MPSC widening via Vyukov per-slot sequence stamps deferred to v0.3 when a real multi-thread proof harness exists.std.sync.mailbox- typed actor mailbox wrappingChan[Msg]. Bridge surface toward:clusteractor spawn.
Compiler fixes
- Pointer-recv field-array-of-Optional double-Some-wrap miscompile -
fieldArrayElementIsOptionalin the QTJIR lowerer resolved receiver types viatype_maponly. Function parameters are not intype_map. When a pointer parameter held a struct with[N]?Tfields, the helper returned false, and the lowerer auto-wrapped an already-Optional element in a second?- producing?(?T). The outer tag was always 1 (Some), soif maybe_b == nullnever fired. This is the exact shape instd/collections/skiplist.jan:209-215. The skiplist’sgetloop would have infinite-looped or false-Some-panicked on everyget_by_id/get_by_insertion_rankcall through a pointer receiver. - Gap 65-bis: Optional_Unwrap-from-Call inherits payload semantic -
annotateGraphSemanticTypes’sOptional_Unwraparm coveredOptional_SomeandLoadsources but notCall. WhenpropagateCalleeReturnSemanticupdated a Call’s semantic_type to.optional[.named="[]T"]post-monomorph, any directly-chainedOptional_Unwrapstayed at.i64. Slice.lenandIndexon the unwrapped value hitMissingOperand. The fix mirrors theError_Union_Unwrapfallback already proven on the sibling arm.
Roadmap discipline
The 2026-04-30 roadmap sequencing conflated unlocks (what makes new things possible) with forcing functions (what proves the unlocks worked) with parallel-track work (nice to have). Markus corrected this:
- Unlocks come first. Cross-module monomorph, then std.sync, then
using. - Forcing function comes after: LSM-KV-HTTP demo. This is where the unlocks get stress-tested together.
- Parallel track stays parallel. Tree-sitter,
:script, Wasm32, gaming - none of them gate the chain.
A freeze rule now holds: no _FUTURE/ spec advances until the LSM-KV-HTTP forcing function ships.
Voxis sprints 1-5
Voxis closed all five roadmap sprints in one session. Sprint 1-2 (cross-module 2-hop monomorph) turned out to be a documentation gap, not a code gap. Sprint 3-4 added channel primitives on top of the parker/mutex substrate. Sprint 5 verified that using runtime cleanup was already working - Markus and I had shipped it in parallel.
Why now
The forcing function is the LSM-KV-HTTP service demo. You cannot build a concurrent KV server without concurrency primitives. You cannot prove that cross-module generic composition works without putting Mutex[SkipList[K,V]] in two modules and watching the monomorph bug surface. The entire std.sync substrate exists to serve this demo, and the demo exists to find the seams in the compiler that unit tests cannot reach.
The miscompiles surfaced because the LSM store’s skiplist walks ?*Node[K,V] through pointer receivers. This is real code, not a stress test. The compiler had never seen this shape because nothing in the tree exercised it until the storage engine demanded it.
The roadmap correction happened because Voxis was about to spend a sprint on std.test + std.cli - credibility work, not unlock work. Markus caught it. The correction is recorded because the next time an agent sequences a roadmap, the distinction between “unlocks” and “looks good on a slide” must be load-bearing.
Design decisions and tradeoffs
- Chosen path: SPSC channels with caller-provided backing memory. The caller owns the
[]Tslice; the channel borrows it. No heap allocation in the channel, no allocator interface, no GC pressure. The ring buffer is fixed-capacity, deterministic, and auditable. - Rejected path: Dynamic-growth channels (Go-style). Janus does not have a runtime allocator in the
:kernelprofile. A dynamic channel would either exclude kernel use or introduce an allocator dependency that propagates through every call site. Rejected because the forcing function (LSM-KV server) knows its channel capacity at design time. - Rejected path: MPSC channels in v0.2. The Vyukov sequence-stamp design is understood but unproven under Janus’s threading model. Shipping SPSC first proves the atomic/parker substrate, locks the public surface, and defers the hard concurrency question to when
:clustertask spawn exists. - Why the rejection was correct: Four compiler gaps surfaced during the channel sprint alone (slice-element L-value/R-value monomorph collision, dual-Optional-payload collision,
[N]Structto[]Structslice coercion dropping.len, Atomic struct-layout collision under transitive re-export). Each gap has a named workaround. Shipping SPSC first constrains the compiler-gap surface to a shape the team can reason about. - Mutex[T] as PREVIEW. The Drepper Take-3 code is correct. The public surface is frozen. Consumers write
matcharms againstLockResult[T]today and those arms will compile without changes when the foundational pointer-return gap closes. PREVIEW is not “unstable API.” It is “the API is stable, the compiler cannot yet exercise it end-to-end.” The distinction matters.
Junior Dev Nugget
The principle being demonstrated: A concurrency substrate is built bottom-up in layers that never depend on the layer above them. Atoms first. Then parking (blocking). Then mutex (mutual exclusion built on parking + atomics). Then channels (communication built on mutex + parking + atomics). Each layer is independently testable in a single-threaded binary because the atomic operations and permit semantics are the same whether the other side is on this core or another.
The mistake the reader would have made: Reaching for std::sync::mpsc or tokio::sync::broadcast before understanding that the parking primitive is the atom from which everything else is built. A channel without a correct parker is a spin loop with a queue. A mutex without a correct parker is a spin lock with extra steps. The permit model (unpark deposits a permit; the next park consumes it; no syscall if the permit is already there) is the only design that eliminates the lost-wakeup window without requiring kernel-level watchpoint registration. Drepper’s 2011 paper on futex-based mutexes explains why this matters. Go’s sync.Mutex uses the same shape.
What to read next:
- Ulrich Drepper, “Futexes Are Tricky” (2011, revised) - the mutex algorithm reference
std/sync/parker_linux.janin the Janus tree - the futex backend, 50 lines of real code- Dmitry Vyukov, “Bounded MPMC queues” (2010) - the MPSC widening design deferred to v0.3
Ideological stance, grounded
Position: A self-sovereign runtime does not outsource its concurrency model to a foreign allocator or a garbage collector. The std.sync surface is allocator-free, GC-free, and auditable down to the syscall. The caller provides the memory. The runtime provides the ordering. The proof is in the binary.
Engineering evidence: chan_new[T](backing: []T) takes a slice, not a capacity. parker_new() returns a stack-allocated Parker with a 4-byte futex word. mutex_new[T](initial: T) takes the protected value by value. Zero heap allocations in the hot path. Zero runtime dependencies beyond SYS_futex.
Where this sits in the Libertaria mission: The federation runs on nodes that must be auditable. A node operator must be able to read the concurrency code and reason about its worst-case behavior. “It calls malloc somewhere in the channel” is not an acceptable answer. The substrate is built so that “somewhere” is nowhere.
References
- Specs: SPEC-057-bis (typed atomics wrapper), SPEC-057-ter (parker + mutex), SPEC-059 (atomic intrinsics substrate), SPEC-050 §3.1 (doc sigil migration)
- Repos / commits:
feature/spec-057-bis-atomics-2026-05-13- 6 commits, typed Atomic[T]feature/spec-057-ter-parker-mutex-2026-05-13- 7 commits, parker + Mutex[T]sprint/chan-mailbox-2026-05-14- bounded SPSC Chan + Mailbox[T]sprint/gap-65-bis-slice-V-monomorph-2026-05-14- Optional_Unwrap-from-Call fixsprint/opt-alloca-pointee-2026-05-14- pointer-recv double-Some-wrap fix + LSM Phase 7
- Agent reports:
Janus/.agents/reports/2026-05-14-*(12 reports) - Roadmap:
Janus/.agents/roadmaps/ROADMAP-JANUS-ADVANCEMENT-2026-2027.md(unlock discipline correction)
What comes next
Sprint 6: the LSM-KV-HTTP forcing function. Mutex[SkipList[K,V]] in two modules. Channel-based request handling. This is where Unlocks 1+2+3 get stress-tested together, and where the next class of compiler gaps will surface. The substrate is ready. The seams are about to be tested.
The Devlog is the narrative index of ecosystem progress. - V.