Repository navigation
Fix quadratic const-context checking for deeply nested literals - #64956
祈雨 (qiyuhuating) wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Cached results can become stale across overload candidates, changing const-generic inference.
1 open issue
What changed in this PR
Addresses #64955 by caching const-context checks in the native TypeScript compiler to avoid repeated ancestor traversal for deeply nested literals.
Changes:
- Adds computed/value flags for per-node const-context caching.
- Updates
isConstContextto reuse cached results. - Adds a depth-20,000 array-literal regression test.
| File | Description |
|---|---|
tsc/internal/checker/types.go |
Defines const-context cache flags. |
tsc/internal/checker/checker.go |
Caches const-context results in node links. |
tsc/internal/checker/checker_test.go |
Adds the deeply nested array regression test. |
Review effort: Balanced
| links := c.nodeLinks.Get(node) | ||
| if links.flags&NodeCheckFlagsIsConstContextComputed == 0 { |
There was a problem hiding this comment.
Good catch — confirmed and fixed in the updated commit.
I reproduced the staleness exactly as described: with the two overloads, the rejected non-const candidate's isConstContext result survived into the selected <const T> candidate, so f({ a: 1 }, "s") inferred { a: number } instead of { readonly a: 1 }, and the same staleness propagated to nested descendants.
The fix scopes the cache: the per-node result is now only reused while no non-cache contextual type is on the stack (nonCacheContextualDepth == 0). Non-cache entries are exactly the contextual-typing/inference attempts (individual overload candidates, contextual re-checks), so within those attempts the answer is recomputed per candidate as before; the cached fast path only applies while the stack holds only the snapshot-style cache entries pushed by literal checking, which is the deeply-nested case that needs it. Depth-4000/8000 array literals and depth-4000 object literals still check in ~0.2s (vs 5.3s / >30s / 10.8s uncached).
Also added TestConstContextAcrossOverloadCandidates covering this scenario including the nested-descendant variant, and re-ran the stock-vs-fixed diagnostic diff over 2000 test files from tsc/testdata/tests/cases: zero differences.
|
@microsoft-github-policy-service agree |
56e1d7f to
7193def
Compare

Fixes #64955
Checker.isConstContextrecurses up through array/object/parenthesized/property-assignment parents and is invoked once per nesting level, with no caching, so checking a deeply nested array or object literal is quadratic in the depth and can effectively hang on valid input (a depth-8000 array literal, which the Go parser handles fine but the JavaScript parser's stack never allowed, takes >30 seconds on main; parse time stays flat at ~25ms). See #64955 for measurements.This caches the per-node result in
NodeLinksusing the same computed-flag pattern asNodeCheckFlagsInitializerIsUndefinedComputed, making the const-context computation linear. With this change:--noEmit --target esnext)as const/<const T>inference / overload casesScoping the cache (correctness)
The const-context of a node is not invariant across contextual-typing and inference attempts: overload resolution re-checks the same argument under different parameter types, and a rejected
<const T>-less candidate must not decide the answer for the const type parameter candidate that is selected (previouslyf({ a: 1 }, "s")with a non-const first overload could infer{ a: number }instead of{ readonly a: 1 }). The per-node cache is therefore only consulted while no non-cache contextual type is on the stack (nonCacheContextualDepth == 0); within those attempts the answer is recomputed exactly as before, while the deeply nested case that needs the fast path runs with only snapshot-style cache entries on the stack. This was flagged by Copilot's review and is covered byTestConstContextAcrossOverloadCandidates, including the nested-descendant variant.Validation
go test ./internal/checker(including the newTestDeeplyNestedArrayLiteralsandTestConstContextAcrossOverloadCandidates).tsc/testdata/tests/cases(every case compiled separately): zero differences.