Skip to content

Commit 58a68f1

Browse files
authored
[ty] Fall back to Divergent for deeply nested specializations (#20988)
## Summary Fall back to `C[Divergent]` if we are trying to specialize `C[T]` with a type that itself already contains deeply nested specialized generic classes. This is a way to prevent infinite recursion for cases like `self.x = [self.x]` where type inference for the implicit instance attribute would not converge. closes astral-sh/ty#1383 closes astral-sh/ty#837 ## Test Plan Regression tests.
1 parent 2c94337 commit 58a68f1

File tree

10 files changed

+317
-26
lines changed

10 files changed

+317
-26
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2457,6 +2457,48 @@ class Counter:
24572457
reveal_type(Counter().count) # revealed: Unknown | int
24582458
```
24592459

2460+
We also handle infinitely nested generics:
2461+
2462+
```py
2463+
class NestedLists:
2464+
def __init__(self: "NestedLists"):
2465+
self.x = 1
2466+
2467+
def f(self: "NestedLists"):
2468+
self.x = [self.x]
2469+
2470+
reveal_type(NestedLists().x) # revealed: Unknown | Literal[1] | list[Divergent]
2471+
2472+
class NestedMixed:
2473+
def f(self: "NestedMixed"):
2474+
self.x = [self.x]
2475+
2476+
def g(self: "NestedMixed"):
2477+
self.x = {self.x}
2478+
2479+
def h(self: "NestedMixed"):
2480+
self.x = {"a": self.x}
2481+
2482+
reveal_type(NestedMixed().x) # revealed: Unknown | list[Divergent] | set[Divergent] | dict[Unknown | str, Divergent]
2483+
```
2484+
2485+
And cases where the types originate from annotations:
2486+
2487+
```py
2488+
from typing import TypeVar
2489+
2490+
T = TypeVar("T")
2491+
2492+
def make_list(value: T) -> list[T]:
2493+
return [value]
2494+
2495+
class NestedLists2:
2496+
def f(self: "NestedLists2"):
2497+
self.x = make_list(self.x)
2498+
2499+
reveal_type(NestedLists2().x) # revealed: Unknown | list[Divergent]
2500+
```
2501+
24602502
### Builtin types attributes
24612503

24622504
This test can probably be removed eventually, but we currently include it because we do not yet
@@ -2551,13 +2593,54 @@ reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown]
25512593
## Divergent inferred implicit instance attribute types
25522594

25532595
```py
2554-
# TODO: This test currently panics, see https://github.com/astral-sh/ty/issues/837
2596+
class C:
2597+
def f(self, other: "C"):
2598+
self.x = (other.x, 1)
25552599

2556-
# class C:
2557-
# def f(self, other: "C"):
2558-
# self.x = (other.x, 1)
2559-
#
2560-
# reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
2600+
reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
2601+
```
2602+
2603+
This also works if the tuple is not constructed directly:
2604+
2605+
```py
2606+
from typing import TypeVar, Literal
2607+
2608+
T = TypeVar("T")
2609+
2610+
def make_tuple(x: T) -> tuple[T, Literal[1]]:
2611+
return (x, 1)
2612+
2613+
class D:
2614+
def f(self, other: "D"):
2615+
self.x = make_tuple(other.x)
2616+
2617+
reveal_type(D().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
2618+
```
2619+
2620+
The tuple type may also expand exponentially "in breadth":
2621+
2622+
```py
2623+
def duplicate(x: T) -> tuple[T, T]:
2624+
return (x, x)
2625+
2626+
class E:
2627+
def f(self: "E"):
2628+
self.x = duplicate(self.x)
2629+
2630+
reveal_type(E().x) # revealed: Unknown | tuple[Divergent, Divergent]
2631+
```
2632+
2633+
And it also works for homogeneous tuples:
2634+
2635+
```py
2636+
def make_homogeneous_tuple(x: T) -> tuple[T, ...]:
2637+
return (x, x)
2638+
2639+
class E:
2640+
def f(self, other: "E"):
2641+
self.x = make_homogeneous_tuple(other.x)
2642+
2643+
reveal_type(E().x) # revealed: Unknown | tuple[Divergent, ...]
25612644
```
25622645

25632646
## Attributes of standard library modules that aren't yet defined
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# PEP 613 type aliases
2+
3+
We do not support PEP 613 type aliases yet. For now, just make sure that we don't panic:
4+
5+
```py
6+
from typing import TypeAlias
7+
8+
RecursiveTuple: TypeAlias = tuple[int | "RecursiveTuple", str]
9+
10+
def _(rec: RecursiveTuple):
11+
reveal_type(rec) # revealed: tuple[Divergent, str]
12+
13+
RecursiveHomogeneousTuple: TypeAlias = tuple[int | "RecursiveHomogeneousTuple", ...]
14+
15+
def _(rec: RecursiveHomogeneousTuple):
16+
reveal_type(rec) # revealed: tuple[Divergent, ...]
17+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ use crate::types::tuple::{TupleSpec, TupleSpecBuilder};
6969
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
7070
pub use crate::types::variance::TypeVarVariance;
7171
use crate::types::variance::VarianceInferable;
72-
use crate::types::visitor::any_over_type;
72+
use crate::types::visitor::{any_over_type, exceeds_max_specialization_depth};
7373
use crate::unpack::EvaluationMode;
7474
use crate::{Db, FxOrderSet, Module, Program};
7575
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
@@ -827,10 +827,14 @@ impl<'db> Type<'db> {
827827
Self::Dynamic(DynamicType::Unknown)
828828
}
829829

830-
pub(crate) fn divergent(scope: ScopeId<'db>) -> Self {
830+
pub(crate) fn divergent(scope: Option<ScopeId<'db>>) -> Self {
831831
Self::Dynamic(DynamicType::Divergent(DivergentType { scope }))
832832
}
833833

834+
pub(crate) const fn is_divergent(&self) -> bool {
835+
matches!(self, Type::Dynamic(DynamicType::Divergent(_)))
836+
}
837+
834838
pub const fn is_unknown(&self) -> bool {
835839
matches!(self, Type::Dynamic(DynamicType::Unknown))
836840
}
@@ -6652,7 +6656,7 @@ impl<'db> Type<'db> {
66526656
match self {
66536657
Type::TypeVar(bound_typevar) => match type_mapping {
66546658
TypeMapping::Specialization(specialization) => {
6655-
specialization.get(db, bound_typevar).unwrap_or(self)
6659+
specialization.get(db, bound_typevar).unwrap_or(self).fallback_to_divergent(db)
66566660
}
66576661
TypeMapping::PartialSpecialization(partial) => {
66586662
partial.get(db, bound_typevar).unwrap_or(self)
@@ -7214,6 +7218,16 @@ impl<'db> Type<'db> {
72147218
pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool {
72157219
any_over_type(db, self, &|ty| ty == div, false)
72167220
}
7221+
7222+
/// If the specialization depth of `self` exceeds the maximum limit allowed,
7223+
/// return `Divergent`. Otherwise, return `self`.
7224+
pub(super) fn fallback_to_divergent(self, db: &'db dyn Db) -> Type<'db> {
7225+
if exceeds_max_specialization_depth(db, self) {
7226+
Type::divergent(None)
7227+
} else {
7228+
self
7229+
}
7230+
}
72177231
}
72187232

72197233
impl<'db> From<&Type<'db>> for Type<'db> {
@@ -7659,7 +7673,7 @@ impl<'db> KnownInstanceType<'db> {
76597673
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
76607674
pub struct DivergentType<'db> {
76617675
/// The scope where this divergence was detected.
7662-
scope: ScopeId<'db>,
7676+
scope: Option<ScopeId<'db>>,
76637677
}
76647678

76657679
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
@@ -11772,7 +11786,7 @@ pub(crate) mod tests {
1177211786
let file_scope_id = FileScopeId::global();
1177311787
let scope = file_scope_id.to_scope_id(&db, file);
1177411788

11775-
let div = Type::Dynamic(DynamicType::Divergent(DivergentType { scope }));
11789+
let div = Type::Dynamic(DynamicType::Divergent(DivergentType { scope: Some(scope) }));
1177611790

1177711791
// The `Divergent` type must not be eliminated in union with other dynamic types,
1177811792
// as this would prevent detection of divergent type inference using `Divergent`.

crates/ty_python_semantic/src/types/class.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ use crate::types::{
3737
IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType,
3838
MaterializationKind, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType,
3939
TypeContext, TypeMapping, TypeRelation, TypedDictParams, UnionBuilder, VarianceInferable,
40-
declaration_type, determine_upper_bound, infer_definition_types,
40+
declaration_type, determine_upper_bound, exceeds_max_specialization_depth,
41+
infer_definition_types,
4142
};
4243
use crate::{
4344
Db, FxIndexMap, FxIndexSet, FxOrderSet, Program,
@@ -1612,7 +1613,18 @@ impl<'db> ClassLiteral<'db> {
16121613
match self.generic_context(db) {
16131614
None => ClassType::NonGeneric(self),
16141615
Some(generic_context) => {
1615-
let specialization = f(generic_context);
1616+
let mut specialization = f(generic_context);
1617+
1618+
for (idx, ty) in specialization.types(db).iter().enumerate() {
1619+
if exceeds_max_specialization_depth(db, *ty) {
1620+
specialization = specialization.with_replaced_type(
1621+
db,
1622+
idx,
1623+
Type::divergent(Some(self.body_scope(db))),
1624+
);
1625+
}
1626+
}
1627+
16161628
ClassType::Generic(GenericAlias::new(db, self, specialization))
16171629
}
16181630
}

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,25 @@ impl<'db> Specialization<'db> {
12641264
// A tuple's specialization will include all of its element types, so we don't need to also
12651265
// look in `self.tuple`.
12661266
}
1267+
1268+
/// Returns a copy of this specialization with the type at a given index replaced.
1269+
pub(crate) fn with_replaced_type(
1270+
self,
1271+
db: &'db dyn Db,
1272+
index: usize,
1273+
new_type: Type<'db>,
1274+
) -> Self {
1275+
let mut new_types: Box<[_]> = self.types(db).to_vec().into_boxed_slice();
1276+
new_types[index] = new_type;
1277+
1278+
Self::new(
1279+
db,
1280+
self.generic_context(db),
1281+
new_types,
1282+
self.materialization_kind(db),
1283+
self.tuple_inner(db),
1284+
)
1285+
}
12671286
}
12681287

12691288
/// A mapping between type variables and types.

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ impl<'db> CycleRecovery<'db> {
567567
fn fallback_type(self) -> Type<'db> {
568568
match self {
569569
Self::Initial => Type::Never,
570-
Self::Divergent(scope) => Type::divergent(scope),
570+
Self::Divergent(scope) => Type::divergent(Some(scope)),
571571
}
572572
}
573573
}

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5968,16 +5968,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
59685968
let mut annotated_elt_tys = annotated_tuple.as_ref().map(Tuple::all_elements);
59695969

59705970
let db = self.db();
5971-
let divergent = Type::divergent(self.scope());
59725971
let element_types = elts.iter().map(|element| {
59735972
let annotated_elt_ty = annotated_elt_tys.as_mut().and_then(Iterator::next).copied();
5974-
let element_type = self.infer_expression(element, TypeContext::new(annotated_elt_ty));
5975-
5976-
if element_type.has_divergent_type(self.db(), divergent) {
5977-
divergent
5978-
} else {
5979-
element_type
5980-
}
5973+
self.infer_expression(element, TypeContext::new(annotated_elt_ty))
59815974
});
59825975

59835976
Type::heterogeneous_tuple(db, element_types)

crates/ty_python_semantic/src/types/infer/builder/type_expression.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
2222
/// Infer the type of a type expression.
2323
pub(super) fn infer_type_expression(&mut self, expression: &ast::Expr) -> Type<'db> {
2424
let mut ty = self.infer_type_expression_no_store(expression);
25-
let divergent = Type::divergent(self.scope());
25+
let divergent = Type::divergent(Some(self.scope()));
2626
if ty.has_divergent_type(self.db(), divergent) {
2727
ty = divergent;
2828
}
@@ -588,7 +588,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
588588
// TODO: emit a diagnostic
589589
}
590590
} else {
591-
element_types.push(element_ty);
591+
element_types.push(element_ty.fallback_to_divergent(self.db()));
592592
}
593593
}
594594

crates/ty_python_semantic/src/types/instance.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ impl<'db> Type<'db> {
7272
{
7373
Type::tuple(TupleType::heterogeneous(
7474
db,
75-
elements.into_iter().map(Into::into),
75+
elements
76+
.into_iter()
77+
.map(Into::into)
78+
.map(|element| element.fallback_to_divergent(db)),
7679
))
7780
}
7881

0 commit comments

Comments
 (0)