Use ??= for rule-options dispatch (fix tsc stack overflow on parser.hera)#86
Use ??= for rule-options dispatch (fix tsc stack overflow on parser.hera)#86STRd6 wants to merge 2 commits into
??= for rule-options dispatch (fix tsc stack overflow on parser.hera)#86Conversation
The `||` chain put every alternative call into a single contextually-typed expression that TS resolves all-at-once. Combined with rule bodies that materialize `typeof $$r` (the const-ternary-with-comma form for $skip-using handlers), TS exceeds the call stack typechecking Civet's parser.hera (~9.5k lines, ~290 multi-alternative rules). Per-statement `??=` lets each call's return type be resolved independently. Verified: Civet's parser.hera now typechecks end-to-end through hera.compile + civet.compile + tsc. Hera's own test suite (146 tests) and typed-parser- samples (strict tsc on the bundled grammars) still pass. Generated parser size is ~+0.5% vs 0.9.6 on parser.hera, ~-12.6% vs 0.9.5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR fixes a TypeScript stack overflow regression introduced in 0.9.6 by replacing the Confidence Score: 5/5Safe to merge — single, well-reasoned change with verified semantic equivalence and a full passing test suite. The switch from No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["compileRule(options, name, rule)"] --> B{Top-level choice?}
B -- No --> C["compileRuleBodyInline → single function"]
B -- Yes --> D["Emit per-alternative helper functions\nX$0, X$1, … X$N"]
D --> E["Build finalWideDecl\n(MaybeResult union type annotation)"]
E --> F["Build tryBlock via args.map"]
F --> G["i === 0 →\nlet $$final<type> = X$0($$ctx, $$state)"]
F --> H["i > 0 →\n$$final ??= X$i($$ctx, $$state)"]
G --> I["Emit dispatcher function:\nreturn $$final"]
H --> I
I --> J["MaybeResult<T> = ParseResult<T> | undefined\n??= ≡ || for this type:\nParseResult → truthy & non-nullish\nundefined → falsy & nullish"]
Reviews (1): Last reviewed commit: "Use `??=` instead of `||` chain for rule..." | Re-trigger Greptile |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #86 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 9 9
Lines 1605 1611 +6
Branches 256 259 +3
=========================================
+ Hits 1605 1611 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Actually still investigating this, it's a complex interaction with the hera types being emitted and the deeply recursive Civet grammar. |
Generates a 200-rule mutually-recursive grammar in-memory, runs it through
the same pipeline Civet's plugin uses (`hera.compile({ module: true })` →
`civet.compile({ js: false })` → tsc), and asserts no `Maximum call stack
size exceeded`.
The synthetic threshold for the overflow on the broken compiler is N≈175
rules; testing at N=200 leaves a small margin so minor TS-version drift
doesn't make the test flaky. TS lands the overflow in different internals
(`isConstContext`, `resolveEntityName`, `instantiateTypeWithSingleGeneric-
CallSignature`, …) depending on which frame happens to run out of stack
first — the regex match on the shared error message is robust to that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
This can likely be fixed on the Civet side with more / better annotations in |
|
I'm surprised it makes a difference - the const that the Just checking: did you turn on |
I think it only makes a difference because our current 0.9.3 also had a change that exploded as well so I think it's not really this particular change but any type inference that gets too recursive without enough types to break parser cycles.. |
Summary
const $$final = X$0() || X$1() || …rule-options dispatcher introduced a regression:tscstack-overflows when typechecking heavily mutually-recursive grammars, including Civet'sparser.hera(~9.5k lines, ~290 multi-alternative rules).??=reassignment chain. Same runtime semantics, same control flow, but each alternative call is typechecked independently instead of being unified into one contextually-typed||expression.$$finaland the type-narrowing rationale (the wideMaybeResult<X | Y | …>annotation, computed viaUnwrap<ReturnType<typeof X$i>>) intact.Root cause
With the
||-chain const initializer, TS resolves the entire chain's contextual type in one pass. Combined with rule bodies that materializetypeof $$r(the const-ternary-with-comma form for$skip-using handlers), the recursion through Civet's mutually-recursive rule graph blows the stack. Per-statement??=short-circuits this: each rule call's return type is resolved on its own line.The fix is a single
tryBlock :=swap incompileRule. See the comment block in the diff for the analysis.Test plan
pnpm test— 146 unit tests pass (incl. the runtime$skiptest that actually executes a generated parser).pnpm test:typed-parser-samples— strict-modetscon the bundled sample grammars still passes.parser.heraend-to-end (hera.compile→civet.compile→tsc -p tsconfig.json) completes withoutMaximum call stack size exceeded. Repro is wired up at/tmp/hera-civet-repro/(a smallbuild.mjsharness + stub modules +tsconfig.json); happy to upstream it underperf/or as a regression test if useful.Size impact (Civet's parser.hera, post-Civet TS output)
main, broken)Almost all of 0.9.6's size win is preserved.
🤖 Generated with Claude Code