ReviewOS

zig-utils/zig-js

Array join: bound and account active-receiver scratch

#1023
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Parent: #480 Related: #473, #941 Follows: #1022

Executed evidence

On exact main 2723311b, a temporary same-module witness drove the production Array.prototype.join / toLocaleString entry and unwind paths. The probe was removed after collection.

ArrayJoinActive is 32 bytes and each RuntimeObjectIdentity slot is 16 bytes. After every completed or caught invocation, logical length returned to zero but allocator capacity remained:

  • shallow [1, 2].join("-"): capacity 9, retaining 176 logical bytes;
  • successful 40-level nested join: capacity 44, retaining 736 logical bytes;
  • successful 20-level nested toLocaleString: still capacity 44 / 736 bytes;
  • self/mutual cycles and aliases: length 0, capacity 44 after unwind;
  • throwing element toString, followed by successful retry: length 0, capacity 44 after unwind.

The owner and vector currently allocate from the realm arena. Interpreter.deinit does not reclaim either, and the active-set pointer dies with the temporary interpreter, so the backing remains until realm teardown. The native stack guard bounds executable nesting, but the retained domain is neither freeable at interpreter teardown nor represented in the memory inventory.

These are deterministic allocator-capacity observations, not timing or RSS claims.

Required change

  • Keep ordinary shallow joins allocation-free or inline-first.
  • Give fallback active-receiver storage an explicitly freeable interpreter-local owner.
  • Release one-off deep high-water backing while preserving useful repeated-depth reuse.
  • Publish exact current/peak receiver count, capacity, bytes, growths, releases, and released bytes through the machine-readable Context inventory without double-counting Context backing.
  • Preserve entry-order identity checks and the check-before-separator-coercion behavior established by #941.

Acceptance

  • Deterministic shallow/deep/locale/cycle/throw witnesses prove inline use, fallback growth, one-off release, repeated-depth reuse, shallow invalidation, and teardown zero.
  • Self-cycles, mutual cycles, aliases, reentrant toString, throwing coercions, Proxies, separators, and toLocaleString keep exact observable order.
  • Exhaustive allocation-failure rollback leaves no active receiver, stale lease, or leaked fallback.
  • Moving-GC, no-GIL, and unsuppressed TSan witnesses pass against the collecting zig-gc checkout.
  • Affected test262 Array join/toLocaleString subtrees remain clean and forced tree/required-VM outcomes agree exactly.
  • Versioned inventory/tool validators and teardown reconciliation pass.
  • Deterministic capacity evidence is published; no noisy wall-time claim is used.

Guardrails

No reduced nesting, disabled cycle handling, global visited set, receiver/source recognition, arena-only fallback, changed separator coercion order, or cached final strings.

9 days ago

Runtime slice pushed as 3b51022.

Verified before landing:

  • direct interpreter module: 2/2 pass, including exhaustive allocation-failure unwind
  • focused runtime cycle/re-entry witness: 1/1 pass (10 semantic shapes)
  • common path: 8 receivers inline
  • first one-off depth-40 spill releases its fallback
  • repeated depth-40 spills retain and reuse without another growth
  • a later shallow invocation releases retained deep backing

Next: wire the coherent overlap counters into Context memory inventory and its machine-readable sidecar.

9 days ago

Completed on main in four direct commits:

  • 3b51022 — eight inline receivers, freeable fallback, one-off release / repeated-depth reuse, exhaustive OOM
  • 420154a — schema-v5 Context inventory and schema-v17 representative sidecar
  • c559c7c — V46 benchmark documentation and verified BunPress manifest
  • cf52f1a — moving-GC proxy cycles, required-VM/no-GIL/TSan coverage, deterministic evidence

Closing evidence:

  • direct owner/OOM: 2/2 pass
  • inventory/teardown: 2/2 pass
  • moving GC + proxy cycles in tree and required VM: 2/2 pass
  • shared no-GIL required VM: 2/2 pass
  • same no-GIL witness under unsuppressed TSan: 2/2 pass
  • test262 Array.prototype.join: 23/23 before and after
  • test262 Array.prototype.toLocaleString: 12/12 before and after
  • forced tree/required-VM: 35/35 agree, 0 diverge
  • representative matrix V46 and attribution fail-closed self-test: pass
  • BunPress: 51 pages, manifest exact

Deterministic report: docs/.data/array-join-active-receiver-memory-2026-10-02.md. Shallow uses no fallback; one-off depth 40 releases capacity 44 / 704 fallback bytes; repeated depth reuses its established working set; all measured stages finish at zero current owners, receivers, fallback capacity, and bytes.

Sign in to comment on this issue.