Skip to content

Commit 36c628f

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

File tree

5 files changed

+205
-3
lines changed

5 files changed

+205
-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
@@ -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: 128 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,129 @@ 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>, 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.update(|current| current.max(*cached_depth));
327+
return;
328+
}
329+
330+
let self_depth: usize =
331+
matches!(non_atomic_type, NonAtomicType::GenericAlias(_)).into();
332+
333+
let previous_max_depth = self.max_depth.get();
334+
335+
self.max_depth.set(0);
336+
walk_non_atomic_type(db, non_atomic_type, self);
337+
338+
self.max_depth.update(|child_max_depth| {
339+
previous_max_depth.max(child_max_depth.saturating_add(self_depth))
340+
});
341+
342+
self.seen_types
343+
.borrow_mut()
344+
.insert(non_atomic_type, self.max_depth.get());
345+
}
346+
}
347+
}
348+
}
349+
350+
let visitor = SpecializationDepthVisitor {
351+
seen_types: RefCell::new(FxHashMap::default()),
352+
max_depth: Cell::new(0),
353+
};
354+
visitor.visit_type(db, ty);
355+
visitor.max_depth.get()
356+
}
357+
358+
#[cfg(test)]
359+
mod tests {
360+
use super::*;
361+
use crate::{db::tests::setup_db, types::KnownClass};
362+
363+
#[test]
364+
fn test_generics_layering_depth() {
365+
let db = setup_db();
366+
367+
let list_of_int =
368+
KnownClass::List.to_specialized_instance(&db, [KnownClass::Int.to_instance(&db)]);
369+
assert_eq!(specialization_depth(&db, list_of_int), 1);
370+
371+
let list_of_list_of_int = KnownClass::List.to_specialized_instance(&db, [list_of_int]);
372+
assert_eq!(specialization_depth(&db, list_of_list_of_int), 2);
373+
374+
let list_of_list_of_list_of_int =
375+
KnownClass::List.to_specialized_instance(&db, [list_of_list_of_int]);
376+
assert_eq!(specialization_depth(&db, list_of_list_of_list_of_int), 3);
377+
378+
let set_of_dict_of_str_and_list_of_int = KnownClass::Set.to_specialized_instance(
379+
&db,
380+
[KnownClass::Dict
381+
.to_specialized_instance(&db, [KnownClass::Str.to_instance(&db), list_of_int])],
382+
);
383+
assert_eq!(
384+
specialization_depth(&db, set_of_dict_of_str_and_list_of_int),
385+
3
386+
);
387+
388+
let union_type_1 =
389+
UnionType::from_elements(&db, [list_of_list_of_list_of_int, list_of_list_of_int]);
390+
assert_eq!(specialization_depth(&db, union_type_1), 3);
391+
392+
let union_type_2 =
393+
UnionType::from_elements(&db, [list_of_list_of_int, list_of_list_of_list_of_int]);
394+
assert_eq!(specialization_depth(&db, union_type_2), 3);
395+
396+
let tuple_of_tuple_of_int = Type::heterogeneous_tuple(
397+
&db,
398+
[Type::heterogeneous_tuple(
399+
&db,
400+
[KnownClass::Int.to_instance(&db)],
401+
)],
402+
);
403+
assert_eq!(specialization_depth(&db, tuple_of_tuple_of_int), 2);
404+
405+
let tuple_of_list_of_int_and_str = KnownClass::Tuple
406+
.to_specialized_instance(&db, [list_of_int, KnownClass::Str.to_instance(&db)]);
407+
assert_eq!(specialization_depth(&db, tuple_of_list_of_int_and_str), 1);
408+
409+
let list_of_union_of_lists = KnownClass::List.to_specialized_instance(
410+
&db,
411+
[UnionType::from_elements(
412+
&db,
413+
[
414+
KnownClass::List
415+
.to_specialized_instance(&db, [KnownClass::Int.to_instance(&db)]),
416+
KnownClass::List
417+
.to_specialized_instance(&db, [KnownClass::Str.to_instance(&db)]),
418+
KnownClass::List
419+
.to_specialized_instance(&db, [KnownClass::Bytes.to_instance(&db)]),
420+
],
421+
)],
422+
);
423+
assert_eq!(specialization_depth(&db, list_of_union_of_lists), 2);
424+
}
425+
}

0 commit comments

Comments
 (0)