Skip to content

Fix #11472: TypeVarTuple escapes method with very nested recursive tuple aliases#11497

Open
rchiodo wants to merge 1 commit into
microsoft:mainfrom
rchiodo:fix11472
Open

Fix #11472: TypeVarTuple escapes method with very nested recursive tuple aliases#11497
rchiodo wants to merge 1 commit into
microsoft:mainfrom
rchiodo:fix11472

Conversation

@rchiodo

@rchiodo rchiodo commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Description

Fixes a bug where a TypeVarTuple could "escape" a method's signature when the receiver's type arguments contained deeply nested recursive tuple type aliases. In these cases, an unsolved TypeVar (e.g. OO from unpack_then) leaked into the inferred type of an assignment, producing a spurious type error.

How you figured out what to do

The repro chained parser combinators (then / unpack_then) on values whose types involved recursive tuple aliases (Whitespace, Implementations, BlockItem). Tracing the failure showed that TypeVarTransformer bailed out early when transforming tuple types whose container depth exceeded a hard-coded limit (maxTupleTypeArgRecursionDepth = 10). When that early-return fired mid-transformation, the method's TypeVars were never substituted, so they escaped into the result.

Implementation

Removed the maxTupleTypeArgRecursionDepth guard in typeUtils.ts:

  • Deleted the maxTupleTypeArgRecursionDepth constant.
  • Removed the getContainerDepth(classType) > maxTupleTypeArgRecursionDepth early-return in TypeVarTransformer's tuple handling, so deeply nested tuples are transformed fully instead of being returned unmodified.

The depth bail-out was originally a guard against effectively-infinite nesting, but the existing recursion-count limit already protects against runaway types, and the early return caused incorrect TypeVar solving in legitimate code.

Testing

Added typeVarTuple31.py sample reproducing the escaping TypeVar with nested recursive tuple aliases, and wired it up as TypeVarTuple31 in typeEvaluator6.test.ts (Python 3.12). The sample asserts zero errors; the final chained assignment previously failed because OO escaped. Ran the type-evaluator test suite to confirm the new test passes and no existing TypeVarTuple tests regressed.

Addresses

Fixes https://github.com/microsoft/pylance-release/issues/11472


Generated by fix_all_my_issues pipeline

The tuple type-var transformer bailed out early (returning the type
unchanged) whenever a tuple's container depth exceeded a fixed limit of
10. This was meant to prevent the analyzer from hanging on tuple types
that grow without bound during constraint solving. However, legitimate
code can produce bounded-but-deeply-nested tuples via recursive type
aliases, and bailing out there silently dropped already-solved TypeVar
substitutions, causing a solved TypeVar (e.g. OO) to escape into the
inferred type and produce a spurious assignment error.

Container depth cannot distinguish a legitimately deep tuple from a
runaway one: a single substitution of a bounded recursive alias can
jump the depth past the limit just as runaway recursion does. The real
protection against unbounded tuple growth is already provided by the
apply() recursion-count limit and the pending-type-var-scope mechanism
(which leaves same-scope recursive type vars unexpanded), so the depth
guard is redundant and is removed.

Fixes microsoft#11472

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

sympy (https://github.com/sympy/sympy)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:315:25 - error: "Set" is not iterable
+     "__iter__" method not defined (reportGeneralTypeIssues)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:315:25 - error: "ConditionSet" is not iterable
+     "__iter__" method not defined (reportGeneralTypeIssues)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2030:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2042:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2055:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2076:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2077:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2270:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2351:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2364:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2370:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2538:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
+       "Set" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/solvers/tests/test_solveset.py:2541:20 - error: Argument of type "Unknown | Basic | Any" cannot be assigned to parameter "obj" of type "Sized" in function "len"
+     Type "Unknown | Basic | Any" is not assignable to type "Sized"
+       "Basic" is incompatible with protocol "Sized"
+         "__len__" is not present (reportArgumentType)
+   .../projects/sympy/sympy/stats/crv_types.py:2544:9 - error: Method "_cdf" overrides class "SingleContinuousDistribution" in an incompatible manner
+     Return type mismatch: base method returns type "None", override returns type "ComplexInfinity | NaN | Rational | Unknown | Zero | Expr"
+       Type "ComplexInfinity | NaN | Rational | Unknown | Zero | Expr" is not assignable to type "None"
+         "Expr" is not assignable to "None" (reportIncompatibleMethodOverride)
+   .../projects/sympy/sympy/stats/crv_types.py:2723:9 - error: Method "_cdf" overrides class "SingleContinuousDistribution" in an incompatible manner
+     Return type mismatch: base method returns type "None", override returns type "Expr | NaN | ComplexInfinity | Rational | Unknown | One | NegativeOne | Zero | Integer"
+       Type "Expr | NaN | ComplexInfinity | Rational | Unknown | One | NegativeOne | Zero | Integer" is not assignable to type "None"
+         "Expr" is not assignable to "None" (reportIncompatibleMethodOverride)
-   .../projects/sympy/sympy/stats/drv.py:269:22 - error: Argument of type "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
+   .../projects/sympy/sympy/stats/drv.py:269:22 - error: Argument of type "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
-     "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
+     "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
-       Type parameter "_T_co@Iterable" is covariant, but "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not a subtype of "_SupportsSumNoDefaultT@sum"
+       Type parameter "_T_co@Iterable" is covariant, but "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not a subtype of "_SupportsSumNoDefaultT@sum"
-         Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+         Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
-           Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+           Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
-   .../projects/sympy/sympy/stats/drv_types.py:293:16 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic"
+   .../projects/sympy/sympy/stats/drv_types.py:293:16 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic"
+   .../projects/sympy/sympy/stats/frv_types.py:139:17 - error: Argument of type "NaN | ComplexInfinity | Rational | Unknown | Expr" cannot be assigned to parameter "value" of type "int" in function "__setitem__"
+     Type "NaN | ComplexInfinity | Rational | Unknown | Expr" is not assignable to type "int"
+       "Expr" is not assignable to "int" (reportArgumentType)
-   .../projects/sympy/sympy/stats/rv.py:473:25 - error: Argument of type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
+   .../projects/sympy/sympy/stats/rv.py:473:25 - error: Argument of type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
-     Type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" is not assignable to type "Expr | complex"
+     Type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" is not assignable to type "Expr | complex"
-   .../projects/sympy/sympy/stats/stochastic_process_types.py:1839:25 - error: Argument of type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | Integral | Any | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"

... (truncated 832 lines) ...

return classType;
}

if (classType.priv.tupleTypeArgs) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Removing this early-return fixes the escaping TypeVar (1→0 errors), but it also deletes a documented performance safeguard whose comment explicitly claimed the recursion-count limit is insufficient ("will effectively hang the analyzer"). Consider a narrower, safe-bailout variant: keep a depth bound but make the bailout safe (e.g. return Unknown or still solve top-level TypeVars) so nothing escapes at any depth while preserving the perf guard. Since this is vendored, upstream-owned Pyright core (keep diffs minimal, coordinate upstream), prefer the safe-bailout variant or coordinate the bound removal upstream against the original motivating repro, and confirm that repro still terminates without the guard.

const configOptions = new ConfigOptions(Uri.empty());

configOptions.defaultPythonVersion = pythonVersion3_12;
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarTuple31.py'], configOptions);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

validateResults(analysisResults, 0) asserts only "no errors" — it would still pass if OO were replaced by some other wrong-but-non-erroring type. For a TypeVar-escape bug, add a reveal_type (or hover) assertion pinning forloop's concrete inferred type so the fix's correctness is self-evident and a future divergent-but-non-error regression is caught.

@rchiodo

rchiodo commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

Fix looks correct and well-targeted. Two non-blocking suggestions: (1) the removed depth guard was a documented performance safeguard in vendored Pyright core — please confirm the original motivating workload still terminates and consider coordinating upstream; (2) strengthen the test with a reveal_type assertion to pin the inferred type rather than only asserting zero errors.

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.

1 participant