Skip to content

Latest commit

 

History

History
199 lines (167 loc) · 9.53 KB

File metadata and controls

199 lines (167 loc) · 9.53 KB

SharedArrayBuffer + Atomics

Status: the single-agent surface and real cross-agent substrate ship. SharedArrayBuffer uses a refcounted shared backing block; $262.agent runs agents on isolated OS-thread realms; Atomics.wait / notify coordinate over the shared store. Exact-count FIFO wake ordering and timer/microtask refinements remain follow-up work.

This document preserves the original scoping and phased design; estimates and future tense below describe the pre-implementation plan. Current aggregate status lives in ROADMAP.md. Sister docs: multi-realm.md (per-realm intrinsics — the same install plumbing SAB/Atomics use) and multi-agent-atomics.md.

Historical motivation and scoping

SharedArrayBuffer / Atomics is the single largest engine-true gap in the binary-scored corpus: ~382 built-ins/Atomics + 104 built-ins/SharedArrayBuffer fixtures fail outright (ReferenceError: SharedArrayBuffer), plus ~148 fixtures under DataView / ArrayBuffer / TypedArray* that reference SAB. Everything else surfaced in triage (docs triage 2026-06) is a deliberate divergence (Annex B, strict-only, eval-strict-this) that counts as an honest fail by design — this is the one big fixable block.

The decisive fact: most of it needs no real concurrency. Counts against the pinned corpus:

tree total use $262.agent (multi-agent) single-agent
built-ins/Atomics 382 112 ~270
built-ins/SharedArrayBuffer 104 0 104
SAB-referencing in DataView/ArrayBuffer/TypedArray* ~148 ~0 ~148

Cynic is single-agent-per-isolate (no Workers, no threads). On a single agent, Atomics.* read-modify-write / load / store / compareExchange / isLockFree are ordinary sequential operations on the backing store; Atomics.notify always returns 0 (no other agent waits); the only genuinely-concurrent surface ($262.agent.* spawning agents, the memory-model litmus tests, cross-agent wait/notify) is the ~112 fixtures we defer.

Prior art

  • QuickJS-ng — the closest reference: ships SAB + Atomics with a single-process backing store; Atomics.wait uses a real futex only when threads exist, else degrades. Smallest faithful implementation to mirror.
  • V8 / JavaScriptCore / SpiderMonkey — SAB backed by a refcounted shared store; wait/notify over a per-buffer futex/condvar table; isLockFree(n) true for the platform's lock-free widths (1,2,4, and 8 where Atomics on 64-bit ints is lock-free). We copy the observable rules (isLockFree results, the §25.4 validation order, the [[CanBlock]] gate on wait) without the threading machinery in phase 1.
  • XS (Moddable) — embedded, single-agent by default; SAB is a non-detachable ArrayBuffer. Confirms the "SAB ≈ ArrayBuffer minus detach, plus grow" shape is enough for the sequential surface.

Spec: §25.2 SharedArrayBuffer, §25.4 Atomics, §9.7 Agents + §9.8 AgentClusters, §25.4.3 (ValidateIntegerTypedArray / ValidateAtomicAccess), §25.4.{11,12} wait/notify, §25.1.3 (shared vs non-shared ArrayBuffer abstract ops).

Historical engine starting point

  • ArrayBuffer is fully implemented in src/runtime/builtins/typed_array.zig (constructor, byteLength, resizable + resize, transfer / transferToFixedLength, slice, species, isView). Backing store is JSObject.array_buffer: ?[]u8 with array_buffer_max_byte_length: ?usize (resizable) and the has_array_buffer_data brand (detached = brand set + slice null) — see src/runtime/object.zig.
  • TypedArray (typed_view) and DataView (data_view) views borrow the backing slice and already work over ArrayBuffer.
  • No SharedArrayBuffer, no Atomics, no shared/agent concept anywhere (git grep clean). Greenfield, with a strong reuse base.

Phase 1 — SharedArrayBuffer (single-agent)

SAB is an ArrayBuffer that is never detachable and grow-only (grow, not resize). Reuse the array_buffer slice + brand; add a discriminator.

  • JSObject: add array_buffer_shared: bool = false (or fold into a small enum on the existing brand). IsSharedArrayBuffer(O) = brand set ∧ shared.
  • Global SharedArrayBuffer constructor + %SharedArrayBuffer.prototype%, installed per-realm alongside ArrayBuffer (mirror the existing install).
    • new SharedArrayBuffer(len [, { maxByteLength }]) → allocate a zeroed store; growable iff maxByteLength given.
    • prototype: byteLength, maxByteLength, growable, grow(n) (grow-only; never shrinks), slice (returns a SAB), @@toStringTag, species (get [Symbol.species]).
    • never detached / transfer / resize (those stay ArrayBuffer-only).
  • Wire the existing ArrayBuffer.prototype this-is-sharedarraybuffer.js guards: methods that step "If IsSharedArrayBuffer(O) throw TypeError" (byteLength/detached/maxByteLength/resizable/resize/slice/ transfer/transferToFixedLength) now reach a real SAB instead of a ReferenceError → those ~9 ArrayBuffer fixtures flip to pass.
  • TypedArray / DataView constructors accept a SAB-backed buffer (the buffer-arg validation currently keys off the AB brand; broaden to "AB or SAB"). A SAB-backed view is otherwise identical (no detach path).

Yield estimate: 104 (SharedArrayBuffer) + ~148 (SAB-backed views / ArrayBuffer guards) ≈ ~250 fixtures.

Phase 2 — Atomics (single-agent)

A global Atomics ordinary object (per-realm) with:

  • add, and, or, sub, xor, exchange — §25.4.8 AtomicReadModifyWrite: ValidateIntegerTypedArrayValidateAtomicAccess → ToIntegerOrInfinity/ToBigInt the value → read, op, write back. On a single agent this is a plain sequential read-op-write.
  • compareExchange — §25.4.6, same shape with the expected/replacement compare.
  • load, store — §25.4.{10,13}.
  • isLockFree(n) — §25.4.9: true for 1, 2, 4 (and 8, matching V8 on 64-bit); false otherwise. Pure function of size.
  • notify(ta, index, count) — §25.4.12: validate, then return 0 (no waiters exist on a single agent).
  • wait(ta, index, value, timeout) — §25.4.11: requires an Int32Array/ BigInt64Array over a shared buffer; honor [[CanBlock]] (§9.7 — a TypeError when the surrounding agent can't block). The not-equal fast path ("not-equal") and the validation/throw paths are fully testable single-agent; with no other agent to notify, a matching wait either returns "timed-out" (finite timeout) or is a no-op we bound. Most single-agent wait fixtures exercise validation + not-equal + zero-timeout timed-out.
  • waitAsync (§25.4.x, ES2024) — returns { async: true, value: <promise> } / { async:false, value:"not-equal"|"timed-out" }; single-agent the promise resolves "timed-out".
  • @@toStringTag = "Atomics".

Engine touch-points: a new src/runtime/builtins/atomics.zig; the typed-array element read/write helpers in typed_array.zig are reused for the per-kind load/store. No GC-visible new heap types (Atomics is a plain object; SAB reuses the ArrayBuffer slot).

Yield estimate: ~270 single-agent Atomics fixtures (213 landed).

Single-agent follow-ups — shipped (Atomics now 268 / ~270):

  • Atomics.waitAsync (§25.4, ES2024) — returns the { async, value } record; sync "not-equal" / "timed-out" paths plus a pending Promise for the would-block case (cross-agent resolution deferred).
  • Atomics.pause (TC39 proposal) — no-op hint, validates an optional integral argument, returns undefined.
  • Atomics.store return value is ToIntegerOrInfinity (normalizes -0+0).

Deferred at the end of the single-agent phase:

  • wait / waitAsync cross-agent resolution + the memory-model litmus tests (~112 $262.agent fixtures). The real-agent follow-up subsequently shipped; see multi-agent-atomics.md.
  • wait with [[CanBlock]] = false (browser-main-thread semantics) — 2 fixtures; needs an agent CanBlock model.

Phase 1 + 2 combined ≈ ~500 fixtures → headline ~89.3 % → ~90.3 %+.

Phase 3 — multi-agent (real threads)

Shipped. Each agent has its own realm and heap on an OS thread; agents share only a refcounted data block and futex state. The test262-only $262.agent host hooks provide start, broadcast, report, sleep, and lifecycle operations. The accepted design and residual work are in multi-agent-atomics.md.

SES / hardening notes

  • SharedArrayBuffer + Atomics are frozen with the other primordials at realm init (the existing freeze pass walks them automatically once installed — no extra work, but confirm Object.isFrozen(Atomics) in a test-ses fixture).
  • SAB has no detach/transfer, so it sidesteps the ArrayBuffer capability-revocation surface entirely.
  • A SAB backing store is process-local in phase 1 (single agent), so there's no cross-realm sharing concern yet; phase 3 must revisit how a shared store interacts with per-realm teardown (Heap.pending_realm_teardown).

Verification

  • Unit tests live in atomics_test.zig and shared_array_buffer_test.zig.
  • Run zig build test262 -- --filter=built-ins/SharedArrayBuffer and zig build test262 -- --filter=built-ins/Atomics after shared-memory changes.
  • --filter=built-ins/DataView / ArrayBuffer to confirm the SAB-backed + this-is-sharedarraybuffer flips land.
  • test262-safe --gc-threshold=1 on the SAB tree (new heap-slot usage).
  • Use test262-results.md for current counts; the estimates above are historical.