[GH-ISSUE #7130] JavaScript analysis: a self store such as s.c = s.c.p never closes its member-path cycle and exhausts the worker heap (CodeMirror 5 stylus mode) #4282

Closed
opened 2026-10-31 00:48:30 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @kaoru0822-kitauji on GitHub (Oct 25, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7130

Originally assigned to: @kaoru0822-kitauji on GitHub.

Observed behavior

Since 6.8.0, JavaScript semantic analysis never terminates on a member slot that is overwritten with a value read from the same slot, when the receiver is a parameter or another opaque origin. Memory grows until the owned worker exhausts its heap, and the file comes back as resource_constraint.

Smallest trigger (3 lines):

function f(s) {
  s.c = s.c.p;
  s.c.i = 1;
}

analyzeParsedJavaScriptSemantics on this source runs out of a 512 MiB heap after about 9 s. Variants that fail the same way: s.a.b = s.a.b.next; s.a.b.x = 1, s[k] = s[k].p; s[k].i = 1, s.c = s.c.p.q; s.c.i = 1, and alternating slots s.c = s.d.p; s.d = s.c.q; s.c.i = 1.

Real-world trigger: CodeMirror 5's parser context stack (state.context = state.context.prev; state.context.indent = ...). Example: codemirror@5.65.18 mode/stylus/stylus.js (42,246 bytes).

node scripts/rea.mjs analyze-javascript-application <dir with stylus.js> --format json --analysis-timeout-ms 120000 on main fc43e285e (macOS arm64, Node 26.5.0, one cold run, /usr/bin/time -l):

Input Result Wall CPU Max RSS (largest process)
stylus.js resource_constraint: "Owned JavaScript worker exhausted its 1024 MiB V8 old-space heap (max_heap_mb) while analyzing stylus.js during static" 20.3 s 24.3 s 1.19 GB
641-byte context-stack fixture, --max-heap-mb 256 resource_constraint (256 MiB) 4.1 s 5.0 s 354 MB

Where it allocates

A V8 tick profile of the semantic phase puts 47% of ticks in markValue (collectSemanticMemberMutationsSteps, src/domain/javascript/javascriptSemanticMemberMutations.ts) and most of the rest in semanticProjectedSourceFactsAtRead and GC beneath it. Tracing the work-list on the 3-line trigger shows a cycle that never repeats a path:

s       ["c","i"]
s.c.p   ["i"]          stored source of slot c, selected by the receiver query
s.c     ["p","i"]
s       ["c","p","i"]
s.c.p   ["p","i"]
s       ["c","p","p","i"]
...                    one more "p" after "c" on each turn

Each turn copies the alias-history bindings set, and its keys contain the whole path, so memory grows superlinearly.

Root cause (source-derived)

eb721ebbf fix(javascript): retain stored receiver capture authority (first released in 6.8.0) made markValue follow receiver.storedSources for a write below a stored slot. Bisecting with the trigger above: d3a33fcfc finishes in 8 ms and eb721ebbf runs out of heap.

Each call can see the previous call's store, so s.c may legitimately be c0.p^k for any k. The existing cycle closure only recognizes a self alias such as node = node[key], which adds its repeated segment at the front of the path. A self store adds the segment after the stored key ([c] + p^k + [i]), so the closure never fires.

Proposed fix

At the same owner, extend the repeated-prefix check to a contiguous segment inserted after a stable base. If the revisited capture's path is an ancestor path on this chain plus one inserted segment, and repeating that segment after the base can never be pruned (repeatsUnpruned), escape base + segment once. This is the same widening the existing self-alias rule uses. The front-insertion case keeps its current order and result.

Originally created by @kaoru0822-kitauji on GitHub (Oct 25, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7130 Originally assigned to: @kaoru0822-kitauji on GitHub. ### Observed behavior Since 6.8.0, JavaScript semantic analysis never terminates on a member slot that is overwritten with a value read from the same slot, when the receiver is a parameter or another opaque origin. Memory grows until the owned worker exhausts its heap, and the file comes back as `resource_constraint`. Smallest trigger (3 lines): ```js function f(s) { s.c = s.c.p; s.c.i = 1; } ``` `analyzeParsedJavaScriptSemantics` on this source runs out of a 512 MiB heap after about 9 s. Variants that fail the same way: `s.a.b = s.a.b.next; s.a.b.x = 1`, `s[k] = s[k].p; s[k].i = 1`, `s.c = s.c.p.q; s.c.i = 1`, and alternating slots `s.c = s.d.p; s.d = s.c.q; s.c.i = 1`. Real-world trigger: CodeMirror 5's parser context stack (`state.context = state.context.prev; state.context.indent = ...`). Example: `codemirror@5.65.18` `mode/stylus/stylus.js` (42,246 bytes). `node scripts/rea.mjs analyze-javascript-application <dir with stylus.js> --format json --analysis-timeout-ms 120000` on main fc43e285e (macOS arm64, Node 26.5.0, one cold run, `/usr/bin/time -l`): | Input | Result | Wall | CPU | Max RSS (largest process) | |---|---|---|---|---| | `stylus.js` | `resource_constraint`: "Owned JavaScript worker exhausted its 1024 MiB V8 old-space heap (max_heap_mb) while analyzing stylus.js during static" | 20.3 s | 24.3 s | 1.19 GB | | 641-byte context-stack fixture, `--max-heap-mb 256` | `resource_constraint` (256 MiB) | 4.1 s | 5.0 s | 354 MB | ### Where it allocates A V8 tick profile of the semantic phase puts 47% of ticks in `markValue` (`collectSemanticMemberMutationsSteps`, `src/domain/javascript/javascriptSemanticMemberMutations.ts`) and most of the rest in `semanticProjectedSourceFactsAtRead` and GC beneath it. Tracing the work-list on the 3-line trigger shows a cycle that never repeats a path: ``` s ["c","i"] s.c.p ["i"] stored source of slot c, selected by the receiver query s.c ["p","i"] s ["c","p","i"] s.c.p ["p","i"] s ["c","p","p","i"] ... one more "p" after "c" on each turn ``` Each turn copies the alias-history `bindings` set, and its keys contain the whole path, so memory grows superlinearly. ### Root cause (source-derived) `eb721ebbf fix(javascript): retain stored receiver capture authority` (first released in 6.8.0) made `markValue` follow `receiver.storedSources` for a write below a stored slot. Bisecting with the trigger above: `d3a33fcfc` finishes in 8 ms and `eb721ebbf` runs out of heap. Each call can see the previous call's store, so `s.c` may legitimately be `c0.p^k` for any k. The existing cycle closure only recognizes a self alias such as `node = node[key]`, which adds its repeated segment at the **front** of the path. A self store adds the segment **after the stored key** (`[c] + p^k + [i]`), so the closure never fires. ### Proposed fix At the same owner, extend the repeated-prefix check to a contiguous segment inserted after a stable base. If the revisited capture's path is an ancestor path on this chain plus one inserted segment, and repeating that segment after the base can never be pruned (`repeatsUnpruned`), escape `base + segment` once. This is the same widening the existing self-alias rule uses. The front-insertion case keeps its current order and result.
bsakaguchi 2026-10-31 00:48:30 +00:00
  • closed this issue
  • added the
    bug
    label
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4282
No description provided.