Canonical Tracker
This is the single canonical issue for JavaScript multithreading in zig-js.
Older duplicate roadmap issues should point here instead of tracking similar
work in parallel.
Authoritative design and status docs live in docs/threads:
docs/threads/index.md - start here and support matrix.
docs/threads/api.md - current shared-realm thread API.
docs/threads/testing.md - exact Zig 0.17-dev verification commands.
docs/threads/limits.md - current GIL semantics and Layer-C blockers.
docs/threads/bindings.md - mutable-state rulings.
docs/threads/P7-gc-design.md, docs/threads/P7-gil-removal.md, and
docs/threads/P8-structs.md - GC, GIL removal, and TC39 structs planning.
Current State
- Layer A isolated agents / workers / shared memory are implemented: real
OS-thread agents, retained SharedArrayBuffer storage, typed-array
Atomics.wait / notify / waitAsync, structured clone, ArrayBuffer
transfer/detach, and Worker APIs are covered by unit and conformance tests.
- Layer B shared-realm
Thread is implemented behind
Context.createWith(.{ .enable_threads = true }): Thread, Lock,
Condition, ThreadLocal, ConcurrentAccessError, property-mode
Atomics.*, and proposal-aligned Atomics.Mutex / Atomics.Condition
entry points.
- Shared-realm threads run on real OS threads, share one realm and object
identity, and are serialized by the context GIL. This is concurrency, not
true parallel JS heap mutation.
- C-API
JSStringRef values are immutable and use atomic retain/release.
GC-enabled JSValueRef wrappers use counted JSValueProtect /
JSValueUnprotect roots; JSValueRef and JSObjectRef access remains
context-affine.
- Shape transition maps are synchronized with a per-shape
transition_lock, so
concurrent same-name transitions converge on one child shape. Ordinary
named-property helper paths, named-property delete/rebuild, and VM
plain-property inline caches are synchronized with Object.property_lock,
covering helper-routed shape publication, slot vector updates, accessor maps,
attribute maps, and key order. Per-promise settlement and reaction-list state
is synchronized with Promise.lock; awaitValue, async thenable guards, and
GC tracing use locked snapshots/marking for that state. Object.elements_lock
is now the indexed-storage synchronization funnel: central dense-array
get/set/delete/length paths, packed reverse/sort/splice fast paths, Map/Set
helper methods, Map/Set forEach slot snapshots, native Set helper scans,
and Map/Set cursors use it.
- Remaining direct
Object.elements side doors, arena allocation around
transitions, microtask queues, async waiter arrays, running stack roots, and
Value atomicity remain Layer-C blockers protected by the context GIL.
- The WebKit PR-249 thread allowlist is currently documented as 209/209
green. Remaining reference-only files are tracked in
docs/threads/testing.md with concrete reasons.
- The GC is opt-in (
enable_gc) and now collects mid-script, including
while peer threads are parked, not only at quiescent points: zig-js uses
the sibling zig-gc collector for precise mark-sweep, driven at the engines'
(steps & 1023) safepoints. Live Values the precise tracer can't see are
rooted by conservative native-stack + register scanning (src/stack_scan.zig):
the collecting thread's own stack, plus every parked peer's published scan
range (the multi-thread safepoint protocol — a parking thread publishes its
range before releasing the GIL; a safety net aborts collection unless every
peer is parked-and-published). VM Exec operand stacks are registered as
precise roots. The GC now reclaims at safepoints under the full threading
model with the GIL held, and marks incrementally (M2): a Dijkstra
insertion write barrier is wired into every post-creation heap→heap
reference-store funnel (Object slots/elements/accessors, Environment bindings,
VM property IC, Promise/Generator, Map/Set, proto reparent), with mid-cycle
allocations born grey and a finish-time root re-scan; collectMidScript steps
startMarking/markStep/finishMarking for GC-on contexts. Layer-C / GIL
removal now needs a Value atomicity story (NaN-boxing, blocker #7), then M3
(drop the GIL, mark concurrently behind this barrier) with a TSan campaign.
Work Still Tracked Here
- Keep
docs/threads and this issue aligned whenever thread behavior, counts,
or blockers change.
- Promote PR-249 files only when the engine implements the behavior and the
file passes reliably under Zig 0.17-dev.
- Finish the GC/root story needed for Layer C: stack roots for arbitrary
running/parked native frames, safe collection around shared-realm threads,
write barriers or stop-the-world coordination, and dependency work in
~/Code/Libraries/zig-gc when the collector mechanism belongs there.
- Design and implement the remaining object/value synchronization before
removing the GIL: remaining direct Object.elements side doors, microtask
queues, async waiter arrays, and ordinary Value slots must have a real
concurrent access story. Keep transition-map mutation funneled through
Shape.transition, named-property metadata funneled through
Object.property_lock, indexed storage funneled through
Object.elements_lock, and Promise state funneled through Promise.lock.
- Decide the final
Value representation / atomicity model before true
parallel heap mutation.
- Keep C-API context affinity rules explicit; do not expose test-only host
knobs as stable embedder APIs until they have a real public contract.
- Track TC39
proposal-structs only through this issue and
docs/threads/P8-structs.md; do not open another parallel
structs/threading tracker.
Verification Gates
Use Zig 0.17-dev:
zig build test
zig build threads-test
zig build threads-test -Dthreads-case=atomics/property-waitasync-timeout.js
zig build test -Dtsan=true
bun run docs:build
Thread work should also run focused PR-249 cases for the touched behavior,
update the allowlist/count docs when promotion is real, and keep
docs/threads/bindings.md current for every new mutable global or threadlocal
state.
Canonical Tracker
This is the single canonical issue for JavaScript multithreading in zig-js.
Older duplicate roadmap issues should point here instead of tracking similar
work in parallel.
Authoritative design and status docs live in
docs/threads:docs/threads/index.md- start here and support matrix.docs/threads/api.md- current shared-realm thread API.docs/threads/testing.md- exact Zig0.17-devverification commands.docs/threads/limits.md- current GIL semantics and Layer-C blockers.docs/threads/bindings.md- mutable-state rulings.docs/threads/P7-gc-design.md,docs/threads/P7-gil-removal.md, anddocs/threads/P8-structs.md- GC, GIL removal, and TC39 structs planning.Current State
OS-thread agents, retained
SharedArrayBufferstorage, typed-arrayAtomics.wait/notify/waitAsync, structured clone, ArrayBuffertransfer/detach, and Worker APIs are covered by unit and conformance tests.
Threadis implemented behindContext.createWith(.{ .enable_threads = true }):Thread,Lock,Condition,ThreadLocal,ConcurrentAccessError, property-modeAtomics.*, and proposal-alignedAtomics.Mutex/Atomics.Conditionentry points.
identity, and are serialized by the context GIL. This is concurrency, not
true parallel JS heap mutation.
JSStringRefvalues are immutable and use atomic retain/release.GC-enabled
JSValueRefwrappers use countedJSValueProtect/JSValueUnprotectroots;JSValueRefandJSObjectRefaccess remainscontext-affine.
transition_lock, soconcurrent same-name transitions converge on one child shape. Ordinary
named-property helper paths, named-property delete/rebuild, and VM
plain-property inline caches are synchronized with
Object.property_lock,covering helper-routed shape publication, slot vector updates, accessor maps,
attribute maps, and key order. Per-promise settlement and reaction-list state
is synchronized with
Promise.lock;awaitValue, async thenable guards, andGC tracing use locked snapshots/marking for that state.
Object.elements_lockis now the indexed-storage synchronization funnel: central dense-array
get/set/delete/length paths, packed reverse/sort/splice fast paths, Map/Set
helper methods, Map/Set
forEachslot snapshots, native Set helper scans,and Map/Set cursors use it.
Object.elementsside doors, arena allocation aroundtransitions, microtask queues, async waiter arrays, running stack roots, and
Valueatomicity remain Layer-C blockers protected by the context GIL.green. Remaining reference-only files are tracked in
docs/threads/testing.mdwith concrete reasons.enable_gc) and now collects mid-script, includingwhile peer threads are parked, not only at quiescent points: zig-js uses
the sibling
zig-gccollector for precise mark-sweep, driven at the engines'(steps & 1023)safepoints. LiveValues the precise tracer can't see arerooted by conservative native-stack + register scanning (
src/stack_scan.zig):the collecting thread's own stack, plus every parked peer's published scan
range (the multi-thread safepoint protocol — a parking thread publishes its
range before releasing the GIL; a safety net aborts collection unless every
peer is parked-and-published). VM
Execoperand stacks are registered asprecise roots. The GC now reclaims at safepoints under the full threading
model with the GIL held, and marks incrementally (M2): a Dijkstra
insertion write barrier is wired into every post-creation heap→heap
reference-store funnel (Object slots/elements/accessors, Environment bindings,
VM property IC, Promise/Generator, Map/Set, proto reparent), with mid-cycle
allocations born grey and a finish-time root re-scan;
collectMidScriptstepsstartMarking/markStep/finishMarkingfor GC-on contexts. Layer-C / GILremoval now needs a
Valueatomicity story (NaN-boxing, blocker #7), then M3(drop the GIL, mark concurrently behind this barrier) with a TSan campaign.
Work Still Tracked Here
docs/threadsand this issue aligned whenever thread behavior, counts,or blockers change.
file passes reliably under Zig
0.17-dev.running/parked native frames, safe collection around shared-realm threads,
write barriers or stop-the-world coordination, and dependency work in
~/Code/Libraries/zig-gcwhen the collector mechanism belongs there.removing the GIL: remaining direct
Object.elementsside doors, microtaskqueues, async waiter arrays, and ordinary
Valueslots must have a realconcurrent access story. Keep transition-map mutation funneled through
Shape.transition, named-property metadata funneled throughObject.property_lock, indexed storage funneled throughObject.elements_lock, and Promise state funneled throughPromise.lock.Valuerepresentation / atomicity model before trueparallel heap mutation.
knobs as stable embedder APIs until they have a real public contract.
proposal-structsonly through this issue anddocs/threads/P8-structs.md; do not open another parallelstructs/threading tracker.
Verification Gates
Use Zig
0.17-dev:Thread work should also run focused PR-249 cases for the touched behavior,
update the allowlist/count docs when promotion is real, and keep
docs/threads/bindings.mdcurrent for every new mutable global or threadlocalstate.