Problem
After #989 bounded the immutable RegExp lookup table, the programs and matcher scratch behind evicted entries are still arena-owned. Dropping an index entry therefore does not reclaim either allocation domain. Interpreter.regex_matchers also grows once per distinct compiled program, and each Regex.Matcher lazily retains DFA/VM scratch through the compiled regex's arena allocator.
This is a focused child of #480 and #473.
Executed evidence
Measured from exact main commit e3e2ca49119937119d8dbe32bc731f0743a2a5bc with a freshly built test262 runner. Each row is a fresh process; the source uses unique constant-shape patterns token_<i>_(?:alpha|beta)+$. strings builds the source only, construct eagerly validates/compiles with new RegExp, and match additionally calls .test() once. All rows returned the expected checksum.
| patterns | strings RSS | construct RSS | match RSS |
|---|---|---|---|
| 512 | 19,988,480 | 24,756,224 | 28,622,848 |
| 2,048 | 21,495,808 | 38,436,864 | 53,395,456 |
| 8,192 | 27,361,280 | 93,257,728 | 152,748,032 |
| 32,768 | 51,003,392 | 316,293,120 | 557,334,528 |
At 32,768 patterns, construction retains about 265 MB beyond the string control; matching retains another 241 MB beyond construction. The guarded matrix peaked at 497 MB of physical footprint and completed with the host at 44% low-water, so this is measured growth rather than a guard artifact.
Root cause
src/regexp_program_cache.zigbounds only 64 lookup entries. Its contract explicitly says eviction does not free programs or keys because they are context-arena-owned.compileRegexallocates everyregex.Regexand all compiled state fromInterpreter.arena, then stores the pointer on the RegExp object.regexMatcherstores one matcher per compiled pointer in an unbounded identity map.- zig-regex's matcher allocates its lazy DFA and VM scratch through
re.allocator, which is the same context arena; evenMatcher.deinit()cannot reclaim arena pages.
Acceptance
- Give compiled programs and matcher scratch explicit interpreter-local, freeable ownership.
- Bound the number of retained programs and matchers without lowering JavaScript limits or rejecting valid patterns.
- Preserve immutable-program reuse while keeping mutable backtracking state isolated per RegExp/interpreter.
- Make eviction and interpreter teardown reclaim owned allocations with no dangling object pointers or reentrant-use hazard.
- Add focused ownership/eviction tests and tree/VM semantic witnesses.
- Verify affected test262 RegExp subtrees, no-GIL, and TSan with no regressions.
- Publish raw exact-parent before/after memory evidence and update #480 and #473.