Skip to content

Commit 79c8f3f

Browse files
committed
[ty] Fall back to Divergent for deeply nested specializations
1 parent 68c1fa8 commit 79c8f3f

File tree

5 files changed

+165
-3
lines changed

5 files changed

+165
-3
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2457,6 +2457,31 @@ 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+
24602485
### Builtin types attributes
24612486

24622487
This test can probably be removed eventually, but we currently include it because we do not yet

crates/ty_python_semantic/src/types.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ use crate::types::signatures::{ParameterForm, walk_signature};
6868
use crate::types::tuple::{TupleSpec, TupleSpecBuilder};
6969
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
7070
use crate::types::variance::{TypeVarVariance, VarianceInferable};
71-
use crate::types::visitor::any_over_type;
71+
use crate::types::visitor::{any_over_type, specialization_depth};
7272
use crate::unpack::EvaluationMode;
7373
use crate::{Db, FxOrderSet, Module, Program};
7474
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
@@ -830,6 +830,10 @@ impl<'db> Type<'db> {
830830
Self::Dynamic(DynamicType::Divergent(DivergentType { scope }))
831831
}
832832

833+
pub(crate) const fn is_divergent(&self) -> bool {
834+
matches!(self, Type::Dynamic(DynamicType::Divergent(_)))
835+
}
836+
833837
pub const fn is_unknown(&self) -> bool {
834838
matches!(self, Type::Dynamic(DynamicType::Unknown))
835839
}

crates/ty_python_semantic/src/types/class.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ 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, infer_definition_types, specialization_depth,
4141
};
4242
use crate::{
4343
Db, FxIndexMap, FxIndexSet, FxOrderSet, Program,
@@ -1607,10 +1607,34 @@ impl<'db> ClassLiteral<'db> {
16071607
db: &'db dyn Db,
16081608
f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>,
16091609
) -> ClassType<'db> {
1610+
// To prevent infinite recursion during type inference for infinite types, we fall back to
1611+
// `C[Divergent]` once a certain amount of levels of specialization have occurred. For
1612+
// example:
1613+
//
1614+
// ```py
1615+
// x = 1
1616+
// while random_bool():
1617+
// x = [x]
1618+
//
1619+
// reveal_type(x) # Unknown | Literal[1] | list[Divergent]
1620+
// ```
1621+
const MAX_SPECIALIZATION_DEPTH: usize = 10;
1622+
16101623
match self.generic_context(db) {
16111624
None => ClassType::NonGeneric(self),
16121625
Some(generic_context) => {
1613-
let specialization = f(generic_context);
1626+
let mut specialization = f(generic_context);
1627+
1628+
for (idx, ty) in specialization.types(db).iter().enumerate() {
1629+
if specialization_depth(db, *ty) > MAX_SPECIALIZATION_DEPTH {
1630+
specialization = specialization.with_replaced_type(
1631+
db,
1632+
idx,
1633+
Type::divergent(self.body_scope(db)),
1634+
);
1635+
}
1636+
}
1637+
16141638
ClassType::Generic(GenericAlias::new(db, self, specialization))
16151639
}
16161640
}

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,27 @@ impl<'db> Specialization<'db> {
12901290
// A tuple's specialization will include all of its element types, so we don't need to also
12911291
// look in `self.tuple`.
12921292
}
1293+
1294+
/// Returns a copy of this specialization with the type at a given index replaced.
1295+
pub(crate) fn with_replaced_type(
1296+
self,
1297+
db: &'db dyn Db,
1298+
index: usize,
1299+
new_type: Type<'db>,
1300+
) -> Self {
1301+
debug_assert!(index < self.types(db).len());
1302+
1303+
let mut new_types: Box<[_]> = self.types(db).to_vec().into_boxed_slice();
1304+
new_types[index] = new_type;
1305+
1306+
Self::new(
1307+
db,
1308+
self.generic_context(db),
1309+
new_types,
1310+
self.materialization_kind(db),
1311+
self.tuple_inner(db),
1312+
)
1313+
}
12931314
}
12941315

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

crates/ty_python_semantic/src/types/visitor.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,91 @@ pub(super) fn any_over_type<'db>(
295295
visitor.visit_type(db, ty);
296296
visitor.found_matching_type.get()
297297
}
298+
299+
/// Returns the number of layers of generic specializations for a given type.
300+
///
301+
/// For example, `int` has a depth of `0`, `list[int]` has a depth of `1`, and
302+
/// `list[set[int]]` has a depth of `2`.
303+
pub(super) fn specialization_depth(db: &dyn Db, ty: Type<'_>) -> usize {
304+
struct SpecializationDepthVisitor<'db> {
305+
seen_types: RefCell<FxIndexSet<NonAtomicType<'db>>>,
306+
max_depth: Cell<usize>,
307+
}
308+
309+
impl<'db> TypeVisitor<'db> for SpecializationDepthVisitor<'db> {
310+
fn should_visit_lazy_type_attributes(&self) -> bool {
311+
false
312+
}
313+
314+
fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
315+
match TypeKind::from(ty) {
316+
TypeKind::Atomic => {
317+
if ty.is_divergent() {
318+
self.max_depth.set(usize::MAX);
319+
}
320+
}
321+
TypeKind::NonAtomic(non_atomic_type) => {
322+
if !self.seen_types.borrow_mut().insert(non_atomic_type) {
323+
return;
324+
}
325+
326+
walk_non_atomic_type(db, non_atomic_type, self);
327+
let child_max_depth = self.max_depth.get();
328+
329+
let self_depth: usize =
330+
matches!(non_atomic_type, NonAtomicType::GenericAlias(_)).into();
331+
332+
self.max_depth.set(
333+
self.max_depth
334+
.get()
335+
.max(child_max_depth.saturating_add(self_depth)),
336+
);
337+
}
338+
}
339+
}
340+
}
341+
342+
let visitor = SpecializationDepthVisitor {
343+
seen_types: RefCell::new(FxIndexSet::default()),
344+
max_depth: Cell::new(0),
345+
};
346+
visitor.visit_type(db, ty);
347+
visitor.max_depth.get()
348+
}
349+
350+
#[cfg(test)]
351+
mod tests {
352+
use super::*;
353+
use crate::{db::tests::setup_db, types::KnownClass};
354+
355+
#[test]
356+
fn test_generics_layering_depth() {
357+
let db = setup_db();
358+
359+
let list_of_int =
360+
KnownClass::List.to_specialized_instance(&db, [KnownClass::Int.to_instance(&db)]);
361+
assert_eq!(specialization_depth(&db, list_of_int), 1);
362+
363+
let list_of_list_of_int = KnownClass::List.to_specialized_instance(&db, [list_of_int]);
364+
assert_eq!(specialization_depth(&db, list_of_list_of_int), 2);
365+
366+
let list_of_list_of_list_of_int =
367+
KnownClass::List.to_specialized_instance(&db, [list_of_list_of_int]);
368+
assert_eq!(specialization_depth(&db, list_of_list_of_list_of_int), 3);
369+
370+
let set_of_dict_of_str_and_list_of_int = KnownClass::Set.to_specialized_instance(
371+
&db,
372+
[KnownClass::Dict
373+
.to_specialized_instance(&db, [KnownClass::Str.to_instance(&db), list_of_int])],
374+
);
375+
assert_eq!(
376+
specialization_depth(&db, set_of_dict_of_str_and_list_of_int),
377+
3
378+
);
379+
380+
// list[list[int]] | list[list[list[int]]]
381+
let union_type =
382+
UnionType::from_elements(&db, [list_of_list_of_int, list_of_list_of_list_of_int]);
383+
assert_eq!(specialization_depth(&db, union_type), 3);
384+
}
385+
}

0 commit comments

Comments
 (0)