Skip to content

[finding] The permanent fix for the registry.ts parser cliff: step17.rationale is a 970-operand + chain (AST depth 976) — a template literal is depth ~4 #10122

Description

@os-steve

Filed unassigned while implementing #10030 (PR raises the lint parser's stack headroom). That PR moves the ceiling; this card is the fix that removes the problem instead of deferring it.

The measurement that reframes the class

Lint & Repo Gates was flipping red on packages/spec/src/migrations/registry.ts with 0:0 error Parsing error: Maximum call stack size exceeded. The received framing (in #10030, #10071 and #10121) is "the file is 525 KB / 6795 lines, machine-generated, and growing". Measured on origin/main @ 1800ffac2, that framing is wrong about the mechanism:

@typescript-eslint/parser recurses over the AST, so what it costs is depth, not size. The depth here is one expression:

whatvalue
registry.ts max AST depth976
longest + chain in it970 operands, lines 397–1367
what that expression isstep17.rationale
nearest <os-generated …> markerline 1432 — i.e. the chain is entirely outside the generated regions
runner-up AST depth across all 4659 linted files71

Two consequences:

  1. The generated regions are innocent. They are flat arrays of string literals — shallow. The file's own header says it plainly: "Everything OUTSIDE the markers — this header, each step's rationale and conversionIds … — is still hand-written and still merges as text."rationale is hand-written, and it is the whole cost.
  2. Depth does not track size. The two largest linted files in the repo — 236 KB (packages/cli/src/commands/serve.ts) and 183 KB (scripts/check-type-check-coverage.mjs) — sit at depth 44 and 40 and are nowhere near the ceiling. Size is not the predictor; a single left-nested operator chain is.

So the two directions proposed in #10030 that target the generated output — "split the generated output across several files" and "exempt the generated regions from parsing" — would not have fixed this. The depth would travel with step17 wherever it went, and it is not between the markers to begin with.

Why it is worth its own card

The stack bump shipped for #10030 buys headroom, and the headroom was measured, not guessed: each appended + '…' fragment costs ~1.10 KB of stack, need is 1085 KB, and the shipped setting is 4000 KB — about 2650 more fragments. That is years at the current rate, not months. But:

  • it is still a growing quantity against a fixed ceiling, and the ceiling cannot be raised indefinitely: --stack-size at or above the OS thread stack (ulimit -s = 8192 KB on ubuntu-latest and locally) makes V8 run off the real stack and SIGSEGV instead of throwing (measured: clean RangeError up to 8000, rc=139 at 8192 and above). So the total remaining budget is bounded, not open-ended;
  • rationale is appended to by retirement cards as routine, so the growth is one-directional by design.

The fix

Rebuild that one string without a 970-deep left-nested + chain. Either spelling collapses the depth from 976 to ~4:

  • a template literal, or
  • an array of fragments with .join('').

No behaviour changes — it is the same string value. Cheap to verify: parse the file and assert max AST depth, or simply confirm the minimum --stack-size drops back under the 984 KB default.

⚠️ Sequencing note: this is a ~970-line diff in a file that retirement cards append to, so it will conflict with anything in flight against step17. Worth landing on a quiet moment, and worth doing as a pure mechanical re-spelling with the resulting string asserted byte-identical.

⚠️ Scope note: the same shape exists at much smaller scale in packages/spec/src/migrations/entries/semantic/*.ts (depths 71, 64, 49, 49, 47 …). Those are harmless today and are not part of this card — but they are the same authoring habit, so if a lint rule capping operator-chain length is ever considered, that is the population it would touch.

Refs: #10030 (the flake card this came out of) · #10071, #10121 (duplicate reports of the same signature) · #7297 (the per-entry generation mechanism).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions