From cbfecfaf41c62f3f4b88e1e88c0b0f5946814ea3 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 15 Dec 2025 10:25:33 -0500 Subject: [PATCH] [ty] Avoid stack overflow when calculating inferable typevars (#21971) When we calculate which typevars are inferable in a generic context, the result might include more than the typevars bound by the generic context. The canonical example is a generic method of a generic class: ```py class C[A]: def method[T](self, t: T): ... ``` Here, the inferable typevar set of `method` contains `Self` and `T`, as you'd expect. (Those are the typevars bound by the method.) But it also contains `A@C`, since the implicit `Self` typevar is defined as `Self: C[A]`. That means when we call `method`, we need to mark `A@C` as inferable, so that we can determine the correct mapping for `A@C` at the call site. Fixes https://github.com/astral-sh/ty/issues/1874 --- .../mdtest/generics/pep695/classes.md | 23 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 21 ++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 71e05171e8..06680d2168 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -800,6 +800,29 @@ def func(x: D): ... func(G()) # error: [invalid-argument-type] ``` +### Self-referential protocol with different specialization + +This is a minimal reproduction for [ty#1874](https://github.com/astral-sh/ty/issues/1874). + +```py +from __future__ import annotations +from typing import Protocol +from ty_extensions import generic_context + +class A[S, R](Protocol): + def get(self, s: S) -> R: ... + def set(self, s: S, r: R) -> S: ... + def merge[R2](self, other: A[S, R2]) -> A[S, tuple[R, R2]]: ... + +class Impl[S, R](A[S, R]): + def foo(self, s: S) -> S: + return self.set(s, self.get(s)) + +reveal_type(generic_context(A.get)) # revealed: ty_extensions.GenericContext[Self@get] +reveal_type(generic_context(A.merge)) # revealed: ty_extensions.GenericContext[Self@merge, R2@merge] +reveal_type(generic_context(Impl.foo)) # revealed: ty_extensions.GenericContext[Self@foo] +``` + ## Tuple as a PEP-695 generic class Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 58ccd28ca0..d4e6c7a446 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -23,7 +23,7 @@ use crate::types::{ KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Signature, Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type, - walk_bound_type_var_type, + walk_type_var_bounds, }; use crate::{Db, FxOrderMap, FxOrderSet}; @@ -290,6 +290,18 @@ impl<'db> GenericContext<'db> { ) } + /// Returns the typevars that are inferable in this generic context. This set might include + /// more typevars than the ones directly bound by the generic context. For instance, consider a + /// method of a generic class: + /// + /// ```py + /// class C[A]: + /// def method[T](self, t: T): + /// ``` + /// + /// In this example, `method`'s generic context binds `Self` and `T`, but its inferable set + /// also includes `A@C`. This is needed because at each call site, we need to infer the + /// specialized class instance type whose method is being invoked. pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> { #[derive(Default)] struct CollectTypeVars<'db> { @@ -299,7 +311,7 @@ impl<'db> GenericContext<'db> { impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { fn should_visit_lazy_type_attributes(&self) -> bool { - true + false } fn visit_bound_type_var_type( @@ -310,7 +322,10 @@ impl<'db> GenericContext<'db> { self.typevars .borrow_mut() .insert(bound_typevar.identity(db)); - walk_bound_type_var_type(db, bound_typevar, self); + let typevar = bound_typevar.typevar(db); + if let Some(bound_or_constraints) = typevar.bound_or_constraints(db) { + walk_type_var_bounds(db, bound_or_constraints, self); + } } fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {