Skip to content

Fix stack overflow in relater's skipCaching path for recursive tuple types#3171

Open
Copilot wants to merge 2 commits intomainfrom
copilot/fix-ts-issue-63270
Open

Fix stack overflow in relater's skipCaching path for recursive tuple types#3171
Copilot wants to merge 2 commits intomainfrom
copilot/fix-ts-issue-63270

Conversation

Copy link
Contributor

Copilot AI commented Mar 19, 2026

Recursive union types with fewer than 4 constituents (e.g., recursive tuple types with spreads) trigger unbounded recursion through the skipCaching path in isRelatedToEx, which bypasses recursiveTypeRelatedTo's depth protection entirely.

type Recur<T> = (
    T extends (unknown[]) ? {} : { [K in keyof T]?: Recur<T> }
) | [...Recur<T>[number][]];

function a<T>(l: Recur<T>[]): void {
    const x: Recur<T> | undefined = join(l); // goroutine stack overflow
}

The cycle is isRelatedToExunionOrIntersectionRelatedToeachTypeRelatedToTypeisRelatedToEx with no depth check, consuming the full 1GB goroutine stack.

Changes

  • relater.go: Add overallDepth counter to Relater, checked in the skipCaching branch with a limit of 100 (matching recursiveTypeRelatedTo). Sets overflow = true on exceeding, producing proper "Excessive stack depth" diagnostics instead of crashing.
  • inference.go: Add depth counter to InferenceState with a limit of 200 in inferFromTypes as defense-in-depth against unbounded inferFromTypesinferToMultipleTypesinferFromTypes recursion (not protected by invokeOnce).
  • Add compiler test case recursiveTupleTypeWithSpreadOverflow.ts.

📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.

…ements

Add depth limit to the relater's skipCaching path in isRelatedToEx to prevent
infinite recursion when comparing recursive union types with fewer than 4
constituents. The skipCaching optimization bypassed recursiveTypeRelatedTo's
existing depth protection, causing unbounded recursion through
unionOrIntersectionRelatedTo → eachTypeRelatedToType → isRelatedToEx.

Also add a depth limit to inferFromTypes as a safety measure against potential
infinite recursion in the inference code path.

Fixes microsoft/TypeScript#63270

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/f12a8fa0-b738-405a-a4c2-67edd64ba015
Copilot AI changed the title [WIP] Fix TypeScript issue 63270 in current repository Fix stack overflow in relater's skipCaching path for recursive tuple types Mar 19, 2026
Copilot AI requested a review from RyanCavanaugh March 19, 2026 23:25
@RyanCavanaugh RyanCavanaugh marked this pull request as ready for review March 20, 2026 15:44
Copilot AI review requested due to automatic review settings March 20, 2026 15:44
@RyanCavanaugh
Copy link
Member

@ahejlsberg this seems like two fixes when only one is needed, but I'm not sure which is preferable (or maybe there's a third option)

Copy link
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

Prevents stack overflows in the type-relater/inference recursion paths when caching is bypassed (e.g., recursive tuple types with spreads), ensuring the compiler reports “excessive depth” diagnostics instead of crashing.

Changes:

  • Add an overallDepth guard in Relater.isRelatedToEx for the skipCaching path to stop unbounded recursion.
  • Add a depth guard in inferFromTypes for defense-in-depth against recursive inference loops.
  • Add a compiler regression test and baselines for recursive tuple type with spread overflow.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
testdata/tests/cases/compiler/recursiveTupleTypeWithSpreadOverflow.ts Adds a repro case to ensure the compiler doesn’t crash on recursive tuple/spread types.
testdata/baselines/reference/compiler/recursiveTupleTypeWithSpreadOverflow.types Captures expected type baseline output for the new test.
testdata/baselines/reference/compiler/recursiveTupleTypeWithSpreadOverflow.symbols Captures expected symbol baseline output for the new test.
testdata/baselines/reference/compiler/recursiveTupleTypeWithSpreadOverflow.errors.txt Captures expected error output (including excessive depth diagnostics).
internal/checker/relater.go Introduces overallDepth limiting in skipCaching relatedness checks.
internal/checker/inference.go Introduces a recursion depth limiter for inference to avoid unbounded recursion.

Comment on lines +70 to +75
if n.depth > 200 {
n.inferencePriority = min(n.inferencePriority, InferencePriorityCircularity)
return
}
n.depth++
defer func() { n.depth-- }()
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The PR description says there is a depth limit of 200 in inferFromTypes, but this check happens before the increment, which effectively allows reaching a depth of 201 (the 201st call passes when n.depth == 200, increments to 201, and proceeds). If the intended hard cap is 200 active frames, increment first and then compare, or change the pre-check to >= 200 to align behavior with the stated limit.

Copilot uses AI. Check for mistakes.
Comment on lines +2656 to +2663
r.overallDepth++
if r.overallDepth > 100 {
r.overflow = true
result = TernaryFalse
} else {
result = r.unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState)
}
r.overallDepth--
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The overallDepth-- is manually paired with the increment; converting the decrement to a defer immediately after increment would make this depth accounting more robust against future edits (e.g., additional early returns added in this block) and maintain invariants more reliably. Also, consider replacing the magic number 100 with a named constant (ideally shared/derived from the existing recursion-depth limit used by recursiveTypeRelatedTo) to keep these limits consistent over time.

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.

4 participants