Bound createAnonymousTypeNodeEx reuse for recursive instantiation expression types#3673
Bound createAnonymousTypeNodeEx reuse for recursive instantiation expression types#3673
createAnonymousTypeNodeEx reuse for recursive instantiation expression types#3673Conversation
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/1d9f5e59-b7a6-4808-8fe4-c0c32da3c2ba Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
- Make wrappingTracker pass-through methods skip directly to the underlying user tracker instead of walking the entire nested-boundary chain (mirrors TS `...oldTracker.inner` spread). - Guard tryReuseExistingNonParameterTypeNode for InstantiationExpressionType with visitedTypes so the recovery-boundary fallback can't re-enter reuse on the same node. - Add regression test symbolToNodeBoundaryNoStackOverflow.ts. Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/1d9f5e59-b7a6-4808-8fe4-c0c32da3c2ba Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Part 1 (wrappingTracker passthrough field) is not needed: the visitedTypes guard alone in createAnonymousTypeNodeEx breaks the recursion that was causing the unbounded chain of nested recovery boundaries (and therefore the stack-overflow chain through their SymbolTrackerImpl/wrappingTracker pass-throughs). Verified the regression test still passes with TS_GO_DEBUG_STACK_LIMIT=2000000 using only part 2. Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/05f377f7-64ce-4026-9c1c-aeae8220c368 Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes an unbounded recursion/stack overflow in node building for recursive typeof X<...> instantiation expression types by bounding reuse attempts with the existing visitedTypes recursion guard, and adds a regression test to prevent reintroduction.
Changes:
- Add a
visitedTypesadd/delete guard aroundtryReuseExistingNonParameterTypeNodewhen reusing an existingtypeofnode for anInstantiationExpressionType. - Add a compiler regression test reproducing the recursive
typeof node.getNextNode<any>scenario. - Add/update baselines (types, symbols, errors) for the new test.
Show a summary per file
| File | Description |
|---|---|
| internal/checker/nodebuilderimpl.go | Bounds recursion during typeof reuse attempts by marking the instantiation type as visited. |
| testdata/tests/cases/compiler/symbolToNodeBoundaryNoStackOverflow.ts | Adds a repro case that previously triggered unbounded recursion/stack overflow. |
| testdata/baselines/reference/compiler/symbolToNodeBoundaryNoStackOverflow.types | Baseline output for types to validate the compiler output for the new test case. |
| testdata/baselines/reference/compiler/symbolToNodeBoundaryNoStackOverflow.symbols | Baseline output for symbols to validate symbol emission for the new test case. |
| testdata/baselines/reference/compiler/symbolToNodeBoundaryNoStackOverflow.errors.txt | Baseline output for the expected error(s) in the new test case. |
Copilot's findings
- Files reviewed: 5/5 changed files
- Comments generated: 2
| typeNode := b.tryReuseExistingNonParameterTypeNode(existing, t, nil, nil) | ||
| b.ctx.visitedTypes.Delete(typeId) |
There was a problem hiding this comment.
Consider using a defer b.ctx.visitedTypes.Delete(typeId) immediately after Add so the visited set is reliably unwound even if this code is refactored later (e.g., additional early returns added) or if tryReuseExistingNonParameterTypeNode ever panics. This keeps the recursion-tracking state consistent and reduces the chance of hard-to-debug downstream behavior.
| typeNode := b.tryReuseExistingNonParameterTypeNode(existing, t, nil, nil) | |
| b.ctx.visitedTypes.Delete(typeId) | |
| defer b.ctx.visitedTypes.Delete(typeId) | |
| typeNode := b.tryReuseExistingNonParameterTypeNode(existing, t, nil, nil) |
| // (e.g. its entity name isn't accessible from this scope) and the recovery boundary's | ||
| // fallback re-enters typeToTypeNode with the very same instantiation type, which would | ||
| // in turn try to reuse the same node again. Mark the type as visited around the reuse | ||
| // attempt so the inner recursion bottoms out via the visitedTypes guard below. |
There was a problem hiding this comment.
The comment says the recursion bottoms out via the "visitedTypes guard below", but the guard is now immediately in this block. Suggest tweaking wording to avoid directional references (e.g., just "via the visitedTypes guard") to keep the comment accurate if surrounding code moves.
| // attempt so the inner recursion bottoms out via the visitedTypes guard below. | |
| // attempt so the inner recursion bottoms out via the visitedTypes guard. |
A recursive
typeof X<...>whose entity name isn't accessible from the current scope sends the node builder into unbounded recursion: whentryReuseExistingNonParameterTypeNodefails for anInstantiationExpressionType, the recovery boundary's fallback re-enterstypeToTypeNodeon the same type, which retries reuse on the same node, stacking another recovery boundary each time until the goroutine stack overflows.Changes
createAnonymousTypeNodeEx(internal/checker/nodebuilderimpl.go): wrap thetryReuseExistingNonParameterTypeNodecall forInstantiationExpressionTypewith avisitedTypesadd/delete so the inner re-entry hits the existingvisitedTypes.Hasguard and returns an elided-information placeholder instead of recursing.testdata/tests/cases/compiler/symbolToNodeBoundaryNoStackOverflow.ts(the repro from the upstream issue).