Skip to content

Commit 9d6cce1

Browse files
committed
[ty] Fall back to Divergent for deeply nested specializations
1 parent 0f21567 commit 9d6cce1

5 files changed

Lines changed: 207 additions & 3 deletions

File tree

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
@@ -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, specialization_depth};
7373
use crate::unpack::EvaluationMode;
7474
use crate::{Db, FxOrderSet, Module, Program};
7575
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
@@ -831,6 +831,10 @@ impl<'db> Type<'db> {
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
}

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,
@@ -1609,10 +1609,34 @@ impl<'db> ClassLiteral<'db> {
16091609
db: &'db dyn Db,
16101610
f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>,
16111611
) -> ClassType<'db> {
1612+
// To prevent infinite recursion during type inference for infinite types, we fall back to
1613+
// `C[Divergent]` once a certain amount of levels of specialization have occurred. For
1614+
// example:
1615+
//
1616+
// ```py
1617+
// x = 1
1618+
// while random_bool():
1619+
// x = [x]
1620+
//
1621+
// reveal_type(x) # Unknown | Literal[1] | list[Divergent]
1622+
// ```
1623+
const MAX_SPECIALIZATION_DEPTH: usize = 10;
1624+
16121625
match self.generic_context(db) {
16131626
None => ClassType::NonGeneric(self),
16141627
Some(generic_context) => {
1615-
let specialization = f(generic_context);
1628+
let mut specialization = f(generic_context);
1629+
1630+
for (idx, ty) in specialization.types(db).iter().enumerate() {
1631+
if specialization_depth(db, *ty) > MAX_SPECIALIZATION_DEPTH {
1632+
specialization = specialization.with_replaced_type(
1633+
db,
1634+
idx,
1635+
Type::divergent(self.body_scope(db)),
1636+
);
1637+
}
1638+
}
1639+
16161640
ClassType::Generic(GenericAlias::new(db, self, specialization))
16171641
}
16181642
}

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,27 @@ 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+
debug_assert!(index < self.types(db).len());
1276+
1277+
let mut new_types: Box<[_]> = self.types(db).to_vec().into_boxed_slice();
1278+
new_types[index] = new_type;
1279+
1280+
Self::new(
1281+
db,
1282+
self.generic_context(db),
1283+
new_types,
1284+
self.materialization_kind(db),
1285+
self.tuple_inner(db),
1286+
)
1287+
}
12671288
}
12681289

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

crates/ty_python_semantic/src/types/visitor.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use rustc_hash::FxHashMap;
2+
13
use crate::{
24
Db, FxIndexSet,
35
types::{
@@ -295,3 +297,131 @@ pub(super) fn any_over_type<'db>(
295297
visitor.visit_type(db, ty);
296298
visitor.found_matching_type.get()
297299
}
300+
301+
/// Returns the maximum number of layers of generic specializations for a given type.
302+
///
303+
/// For example, `int` has a depth of `0`, `list[int]` has a depth of `1`, and `list[set[int]]`
304+
/// has a depth of `2`. A set-theoretic type like `list[int] | list[list[int]]` has a maximum
305+
/// depth of `2`.
306+
pub(super) fn specialization_depth(db: &dyn Db, ty: Type<'_>) -> usize {
307+
struct SpecializationDepthVisitor<'db> {
308+
seen_types: RefCell<FxHashMap<NonAtomicType<'db>, Option<usize>>>,
309+
max_depth: Cell<usize>,
310+
}
311+
312+
impl<'db> TypeVisitor<'db> for SpecializationDepthVisitor<'db> {
313+
fn should_visit_lazy_type_attributes(&self) -> bool {
314+
false
315+
}
316+
317+
fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
318+
match TypeKind::from(ty) {
319+
TypeKind::Atomic => {
320+
if ty.is_divergent() {
321+
self.max_depth.set(usize::MAX);
322+
}
323+
}
324+
TypeKind::NonAtomic(non_atomic_type) => {
325+
if let Some(cached_depth) = self.seen_types.borrow().get(&non_atomic_type) {
326+
self.max_depth
327+
.update(|current| current.max(cached_depth.unwrap_or(0)));
328+
return;
329+
}
330+
self.seen_types.borrow_mut().insert(non_atomic_type, None);
331+
332+
let self_depth: usize =
333+
matches!(non_atomic_type, NonAtomicType::GenericAlias(_)).into();
334+
335+
let previous_max_depth = self.max_depth.get();
336+
337+
self.max_depth.set(0);
338+
walk_non_atomic_type(db, non_atomic_type, self);
339+
340+
self.max_depth.update(|max_child_depth| {
341+
previous_max_depth.max(max_child_depth.saturating_add(self_depth))
342+
});
343+
344+
self.seen_types
345+
.borrow_mut()
346+
.insert(non_atomic_type, Some(self.max_depth.get()));
347+
}
348+
}
349+
}
350+
}
351+
352+
let visitor = SpecializationDepthVisitor {
353+
seen_types: RefCell::new(FxHashMap::default()),
354+
max_depth: Cell::new(0),
355+
};
356+
visitor.visit_type(db, ty);
357+
visitor.max_depth.get()
358+
}
359+
360+
#[cfg(test)]
361+
mod tests {
362+
use super::*;
363+
use crate::{db::tests::setup_db, types::KnownClass};
364+
365+
#[test]
366+
fn test_generics_layering_depth() {
367+
let db = setup_db();
368+
369+
let list_of_int =
370+
KnownClass::List.to_specialized_instance(&db, [KnownClass::Int.to_instance(&db)]);
371+
assert_eq!(specialization_depth(&db, list_of_int), 1);
372+
373+
let list_of_list_of_int = KnownClass::List.to_specialized_instance(&db, [list_of_int]);
374+
assert_eq!(specialization_depth(&db, list_of_list_of_int), 2);
375+
376+
let list_of_list_of_list_of_int =
377+
KnownClass::List.to_specialized_instance(&db, [list_of_list_of_int]);
378+
assert_eq!(specialization_depth(&db, list_of_list_of_list_of_int), 3);
379+
380+
let set_of_dict_of_str_and_list_of_int = KnownClass::Set.to_specialized_instance(
381+
&db,
382+
[KnownClass::Dict
383+
.to_specialized_instance(&db, [KnownClass::Str.to_instance(&db), list_of_int])],
384+
);
385+
assert_eq!(
386+
specialization_depth(&db, set_of_dict_of_str_and_list_of_int),
387+
3
388+
);
389+
390+
let union_type_1 =
391+
UnionType::from_elements(&db, [list_of_list_of_list_of_int, list_of_list_of_int]);
392+
assert_eq!(specialization_depth(&db, union_type_1), 3);
393+
394+
let union_type_2 =
395+
UnionType::from_elements(&db, [list_of_list_of_int, list_of_list_of_list_of_int]);
396+
assert_eq!(specialization_depth(&db, union_type_2), 3);
397+
398+
let tuple_of_tuple_of_int = Type::heterogeneous_tuple(
399+
&db,
400+
[Type::heterogeneous_tuple(
401+
&db,
402+
[KnownClass::Int.to_instance(&db)],
403+
)],
404+
);
405+
assert_eq!(specialization_depth(&db, tuple_of_tuple_of_int), 2);
406+
407+
let tuple_of_list_of_int_and_str = KnownClass::Tuple
408+
.to_specialized_instance(&db, [list_of_int, KnownClass::Str.to_instance(&db)]);
409+
assert_eq!(specialization_depth(&db, tuple_of_list_of_int_and_str), 1);
410+
411+
let list_of_union_of_lists = KnownClass::List.to_specialized_instance(
412+
&db,
413+
[UnionType::from_elements(
414+
&db,
415+
[
416+
KnownClass::List
417+
.to_specialized_instance(&db, [KnownClass::Int.to_instance(&db)]),
418+
KnownClass::List
419+
.to_specialized_instance(&db, [KnownClass::Str.to_instance(&db)]),
420+
KnownClass::List
421+
.to_specialized_instance(&db, [KnownClass::Bytes.to_instance(&db)]),
422+
],
423+
)],
424+
);
425+
assert_eq!(specialization_depth(&db, list_of_union_of_lists), 2);
426+
}
427+
}

0 commit comments

Comments
 (0)