Skip to content

Bound createAnonymousTypeNodeEx reuse for recursive instantiation expression types#3673

Open
Copilot wants to merge 3 commits intomainfrom
copilot/fix-stack-overflow-issue
Open

Bound createAnonymousTypeNodeEx reuse for recursive instantiation expression types#3673
Copilot wants to merge 3 commits intomainfrom
copilot/fix-stack-overflow-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 30, 2026

A recursive typeof X<...> whose entity name isn't accessible from the current scope sends the node builder into unbounded recursion: when tryReuseExistingNonParameterTypeNode fails for an InstantiationExpressionType, the recovery boundary's fallback re-enters typeToTypeNode on 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 the tryReuseExistingNonParameterTypeNode call for InstantiationExpressionType with a visitedTypes add/delete so the inner re-entry hits the existing visitedTypes.Has guard and returns an elided-information placeholder instead of recursing.
  • Regression test: testdata/tests/cases/compiler/symbolToNodeBoundaryNoStackOverflow.ts (the repro from the upstream issue).
if ast.IsTypeQueryNode(existing) && b.getTypeFromTypeNode(existing, false) == t {
    if b.ctx.visitedTypes.Has(typeId) {
        return b.createElidedInformationPlaceholder()
    }
    b.ctx.visitedTypes.Add(typeId)
    typeNode := b.tryReuseExistingNonParameterTypeNode(existing, t, nil, nil)
    b.ctx.visitedTypes.Delete(typeId)
    if typeNode != nil {
        return typeNode
    }
}

Copilot AI and others added 3 commits April 30, 2026 17:46
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>
@RyanCavanaugh RyanCavanaugh requested a review from weswigham April 30, 2026 19:41
@RyanCavanaugh RyanCavanaugh marked this pull request as ready for review April 30, 2026 19:41
Copilot AI review requested due to automatic review settings April 30, 2026 19:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 visitedTypes add/delete guard around tryReuseExistingNonParameterTypeNode when reusing an existing typeof node for an InstantiationExpressionType.
  • 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

Comment on lines 2730 to +2731
typeNode := b.tryReuseExistingNonParameterTypeNode(existing, t, nil, nil)
b.ctx.visitedTypes.Delete(typeId)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
// (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.
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// attempt so the inner recursion bottoms out via the visitedTypes guard below.
// attempt so the inner recursion bottoms out via the visitedTypes guard.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants