ReviewOS

zig-utils/zig-js

test262: exhaustive non-whitespace RegExp case exceeds the 3 GiB Debug guard

#989
Closed glennmichael123 opened this 8 days ago ·
8 days ago

Reproduction

Follow-up qualification for #988 found an existing Debug resource limitation: the unmodified pinned test262 case test/built-ins/RegExp/character-class-escape-non-whitespace.js exceeds this machine's 3072 MB process-tree guard on both the exact dependency parent and the capture-rollback candidate.

  • Engine source baseline: bcc32b717ee2765d2d1143ff0f697390eda642e7.
  • zig-regex: parent 1d3e5c9e3f2fb1c421de6433cab503220988617f versus 601edb49ba0579f1a30e4318290a7d7fb919aeed.
  • Real zig-gc: 0285ef801dd221a4ae8e88b31a09ae31627657a3, not the local stub.
  • test262: 4249661388e5d3f92a85186213da140a6481490f, clean.
  • Zig 0.17.0-dev.1441+d5181a9c9, Debug, macOS M2 Pro / 16 GiB.
  • Each invocation used the existing shared machine lock and HOME_RUN_MAX_MB=3072.

The stopped 25-case baseline batch reported indices 1636..1647 then exceeded the cap at 3469 MB. A fresh process containing ONLY case 1648 reproduced it:

BuildGuard exitMeasured footprint at terminationTest result
Exact parent1253461 MBIncomplete; no outcome
Capture-rollback candidate1253421 MBIncomplete; no outcome

Those sampled values are termination observations, not precise peaks or a before/after memory improvement claim. The guard samples periodically, so it can observe an overshoot before terminating the process tree.

Executed command from zig-js's repository root:

HOME_RUN_MAX_MB=3072 HOME_RUN_LOCK="$HOME/.cache/home-run.lock" \
  perl ../home/scripts/run-bounded.pl 900 \
  /path/to/preserved/test262 --worker test/built-ins/RegExp 1648 1

Worker indices depend on filesystem traversal; do not assume 1648 elsewhere. LLDB stopped the preserved baseline at test262.runOne before execution and confirmed its abs_path was the file above. For another checkout, target that exact file through the runner's file-subtree interface.

The upstream test performs its full 65,536-code-unit loop, replacing with /\S+/g and checking whitespace behavior. Its inputs and assertions were not reduced. All binaries, logs and receipts are retained under /private/tmp/zig-js-sep23-verification.xkalXk on the measurement host: regexp-native-1636-before.{json,log}, regexp-native-1648-before.{json,log}, regexp-isolated-1648-after.json, and memory-stop-1648.md.

Scope and unknowns

This is a resource-limited test, not an observed assertion failure, and not evidence of a regression introduced by #988. The allocation/retention cause is not yet identified; calling it a leak would be premature. No GC or regex workaround, weaker test, ReleaseFast substitution, or cap increase was applied.

Related: #473 (owned string/RegExp pipeline memory), #988 (qualification), #933 (machine resource boundary). Broader zig-utils/zig-regex#30 semantic work is separate from this resource limitation.

Done when

  • Profile the unmodified standalone test, attributing the retained allocations.
  • Fix the underlying lifetime/allocation issue without changing test semantics or weakening the user's guard.
  • Demonstrate completion of the full case within the 3072 MB ceiling and run the relevant before/after correctness gates. Do not claim a full RegExp pass until all corpus cases actually complete.
8 days ago

Allocation diagnosis is now measured; the fix is in progress, not yet qualified.

  • The unchanged exhaustive case retains distinct compiled-program matcher entries (254 entries at an early LLDB checkpoint).
  • Controlled 2,048-iteration probes, same Debug binary and shared 3072 MB guard: fresh literal + replacement peaked at 195,641,848 bytes; construction alone at 163,512,776 bytes; hoisted literal + identical replacement inputs/results at 12,042,576 bytes. These are /usr/bin/time -l process peak-footprint diagnostics, not benchmark claims or replacements for the upstream test.
  • A library-only arena probe attributes ~96 KB of initial arena capacity to compiling \\S+ (116 NFA states), before matcher use. Recompiling the same pattern is the dominant measured retained allocation domain.

Candidate change reuses immutable compiled programs via a bounded interpreter-local source/flags cache; RegExp object identity, lastIndex, eager syntax validation, .compile() replacement and interpreter-local mutable matchers remain separate. The cache does not claim to bound all arena allocations from arbitrary distinct patterns.

The exact-main Debug baseline build passed under the existing cap (sampled tree peak 2201 MB). Focused cache/semantic checks and an isolated candidate build are underway. The full unchanged exhaustive case and complete affected corpus are still required before closing this. No cap, GC-mode, corpus input or assertion changes.

Preserved diagnostic files and build receipts: /private/tmp/zig-js-989.YwhlHo on the measurement host. No source change landed yet.

8 days ago

Code landed directly on main in 517a538, authored and committed as glennmichael123, without trailers. Four cache unit tests and all 30 focused runtime cases passed. The unchanged exhaustive non-whitespace testcase completes in Debug at 145801648 bytes peak process footprint; the exact baseline still reaches the unchanged 3072 MB guard and exits 125. The bounded cache excludes mutable backtracking programs. Complete affected-corpus verification and the dated evidence report remain pending, so this issue stays open.

8 days ago

Resolved on main by 517a5385 and evidence commit 56419e1f.

The root cause was repeated eager compilation of identical immutable RegExp programs into the Context arena. The fix keeps fresh JS RegExp identity and independent lastIndex while reusing only immutable Thompson/one-pass programs through a bounded 64-entry interpreter-local source+flags cache; mutable backtracking programs remain excluded.

Verification under the unchanged 3072 MiB process-tree guard:

  • exact parent: unchanged upstream witness terminated after a sampled 3249 MiB tree reading; no completed outcome/peak claimed
  • candidate: the unchanged witness passed 3/3 times, each at 145,818,056 bytes peak process footprint
  • 56 alternating fresh-process samples: fresh replace 186.58 → 12.78 MiB median; construction-only 155.94 → 10.30 MiB; hoisted and 2,048-unique-pattern controls unchanged at displayed precision
  • 2,456 inventoried affected corpus cases completed, plus 70 baseline/candidate Annex B cases
  • zero regressions among 1,878 previously completed baseline RegExp outcomes; the one prior resource-incomplete case now passes
  • 4 cache unit tests, 30 focused runtime cases, 18 Node/tree/VM semantic checks, direct-root interpreter test, 341-file docs link check, and 51-page BunPress manifest gate passed

Report: regexp-retention-2026-09-25.md

Raw bundle: regexp-retention-2026-09-25.json

The report explicitly does not compare the killed baseline guard reading to the candidate per-process peak, does not claim a throughput/JSC result, and does not claim a general Context-arena bound.

Sign in to comment on this issue.