Skip to content

Canonical: JavaScript multithreading support #1

@chrisbbreuer

Description

@chrisbbreuer

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.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No fields configured for Task.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions