Skip to content

Commit e871de4

Browse files
[ty] Make Divergent a top-level type variant (#24252)
## Summary This PR follows #24245 (comment), making `Divergent` a top-level type rather than a `DynamicType` variant.
1 parent 16cc932 commit e871de4

26 files changed

Lines changed: 243 additions & 100 deletions

crates/ty_ide/src/completion.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,6 +2497,7 @@ fn completion_kind_from_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Comp
24972497
.iter_positive(db)
24982498
.find_map(|ty| imp(db, ty, visitor))?,
24992499
Type::Dynamic(_)
2500+
| Type::Divergent(_)
25002501
| Type::Never
25012502
| Type::SpecialForm(_)
25022503
| Type::KnownInstance(_)

crates/ty_python_semantic/resources/mdtest/directives/cast.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ def f(x: Any, y: Unknown, z: Any | str | int):
8181
e = cast(str | int | Any, z) # error: [redundant-cast]
8282
```
8383

84+
Recursive aliases that fall back to `Divergent` should not trigger `redundant-cast`.
85+
86+
```toml
87+
[environment]
88+
python-version = "3.12"
89+
```
90+
91+
```py
92+
from typing import cast
93+
94+
RecursiveAlias = list["RecursiveAlias | None"]
95+
96+
def f(x: RecursiveAlias):
97+
cast(RecursiveAlias, x)
98+
```
99+
84100
## Diagnostic snapshots
85101

86102
<!-- snapshot-diagnostics -->

crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,29 @@ static_assert(has_member(C(), "static_method"))
108108
static_assert(not has_member(C(), "non_existent"))
109109
```
110110

111+
Recursive attribute inference can fall back to `Divergent`, but should still preserve members that
112+
were available before the cycle was introduced:
113+
114+
```py
115+
from ty_extensions import has_member, static_assert
116+
117+
class Base:
118+
def flip(self) -> "Base":
119+
return Base()
120+
121+
class Sub(Base):
122+
pass
123+
124+
class C:
125+
def __init__(self, x: Sub):
126+
self.x = [x]
127+
128+
def replace_with(self, other: "C"):
129+
self.x = [self.x[0].flip()]
130+
131+
static_assert(has_member(C(Sub()).x[0], "flip"))
132+
```
133+
111134
### Class objects
112135

113136
```toml

crates/ty_python_semantic/resources/mdtest/typed_dict.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,17 +1508,24 @@ _ = cast(Bar2, foo) # error: [redundant-cast]
15081508
```py
15091509
from typing import TypedDict, Final, Literal, Any
15101510

1511+
RecursiveKey = list["RecursiveKey | None"]
1512+
15111513
class Person(TypedDict):
15121514
name: str
15131515
age: int | None
15141516

15151517
class Animal(TypedDict):
15161518
name: str
15171519

1520+
class Movie(TypedDict):
1521+
name: str
1522+
15181523
NAME_FINAL: Final = "name"
15191524
AGE_FINAL: Final[Literal["age"]] = "age"
15201525

15211526
def _(
1527+
recursive_key: RecursiveKey,
1528+
movie: Movie,
15221529
person: Person,
15231530
animal: Animal,
15241531
being: Person | Animal,
@@ -1546,6 +1553,8 @@ def _(
15461553
# No error here:
15471554
reveal_type(person[unknown_key]) # revealed: Unknown
15481555

1556+
reveal_type(movie[recursive_key[0]]) # revealed: Unknown
1557+
15491558
# error: [invalid-key] "Unknown key "anything" for TypedDict `Animal`"
15501559
reveal_type(animal["anything"]) # revealed: Unknown
15511560

crates/ty_python_semantic/src/types.rs

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,8 @@ impl<'db> DataclassParams<'db> {
641641
pub enum Type<'db> {
642642
/// The dynamic type: a statically unknown set of values
643643
Dynamic(DynamicType<'db>),
644+
/// A cycle marker used during recursive type inference.
645+
Divergent(DivergentType),
644646
/// The empty set of values
645647
Never,
646648
/// A specific function object
@@ -777,11 +779,11 @@ impl<'db> Type<'db> {
777779
}
778780

779781
pub(crate) fn divergent(id: salsa::Id) -> Self {
780-
Self::Dynamic(DynamicType::Divergent(DivergentType { id }))
782+
Self::Divergent(DivergentType { id })
781783
}
782784

783785
pub(crate) const fn is_divergent(&self) -> bool {
784-
matches!(self, Type::Dynamic(DynamicType::Divergent(_)))
786+
matches!(self, Type::Divergent(_))
785787
}
786788

787789
pub const fn is_unknown(&self) -> bool {
@@ -925,7 +927,6 @@ impl<'db> Type<'db> {
925927
DynamicType::Any
926928
| DynamicType::Unknown
927929
| DynamicType::UnknownGeneric(_)
928-
| DynamicType::Divergent(_)
929930
| DynamicType::UnspecializedTypeVar => false,
930931
DynamicType::Todo(_)
931932
| DynamicType::TodoStarredExpression
@@ -976,7 +977,7 @@ impl<'db> Type<'db> {
976977
}
977978

978979
pub(crate) const fn is_dynamic(&self) -> bool {
979-
matches!(self, Type::Dynamic(_))
980+
matches!(self, Type::Dynamic(_) | Type::Divergent(_))
980981
}
981982

982983
const fn is_non_divergent_dynamic(&self) -> bool {
@@ -1551,7 +1552,7 @@ impl<'db> Type<'db> {
15511552
match self {
15521553
Type::Never => Type::object(),
15531554

1554-
Type::Dynamic(_) => *self,
1555+
Type::Dynamic(_) | Type::Divergent(_) => *self,
15551556

15561557
Type::NominalInstance(instance) if instance.is_object() => Type::Never,
15571558

@@ -1619,6 +1620,7 @@ impl<'db> Type<'db> {
16191620
| Type::TypeAlias(_)
16201621
| Type::SubclassOf(_)=> true,
16211622
Type::Intersection(_)
1623+
| Type::Divergent(_)
16221624
| Type::SpecialForm(_)
16231625
| Type::BoundSuper(_)
16241626
| Type::BoundMethod(_)
@@ -1814,6 +1816,7 @@ impl<'db> Type<'db> {
18141816
Type::TypeGuard(type_guard) => {
18151817
recursive_type_normalize_type_guard_like(db, type_guard, div, nested)
18161818
}
1819+
Type::Divergent(_) => Some(self),
18171820
Type::Dynamic(dynamic) => Some(Type::Dynamic(dynamic.recursive_type_normalized())),
18181821
Type::TypedDict(_) => {
18191822
// TODO: Normalize TypedDicts
@@ -1925,7 +1928,7 @@ impl<'db> Type<'db> {
19251928
/// for more complicated types that are actually singletons.
19261929
pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool {
19271930
match self {
1928-
Type::Dynamic(_) | Type::Never => false,
1931+
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => false,
19291932

19301933
Type::LiteralValue(literal) => match literal.kind() {
19311934
LiteralValueTypeKind::Int(..)
@@ -2114,6 +2117,7 @@ impl<'db> Type<'db> {
21142117
Type::TypeAlias(alias) => alias.value_type(db).is_single_valued(db),
21152118

21162119
Type::Dynamic(_)
2120+
| Type::Divergent(_)
21172121
| Type::Never
21182122
| Type::Union(..)
21192123
| Type::Intersection(..)
@@ -2161,7 +2165,7 @@ impl<'db> Type<'db> {
21612165
}))
21622166
}
21632167

2164-
Type::Dynamic(_) | Type::Never => Some(Place::bound(self).into()),
2168+
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Some(Place::bound(self).into()),
21652169

21662170
Type::ClassLiteral(class) if class.is_typed_dict(db) => {
21672171
Some(class.typed_dict_member(db, None, name, policy))
@@ -2363,7 +2367,7 @@ impl<'db> Type<'db> {
23632367
Type::Intersection(intersection) => intersection
23642368
.map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)),
23652369

2366-
Type::Dynamic(_) | Type::Never => Place::bound(self).into(),
2370+
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Place::bound(self).into(),
23672371

23682372
Type::NominalInstance(instance) => instance.class(db).instance_member(db, name),
23692373
Type::NewTypeInstance(newtype) => {
@@ -2587,7 +2591,7 @@ impl<'db> Type<'db> {
25872591
PlaceAndQualifiers {
25882592
place:
25892593
Place::Defined(DefinedPlace {
2590-
ty: Type::Dynamic(_) | Type::Never,
2594+
ty: Type::Dynamic(_) | Type::Divergent(_) | Type::Never,
25912595
..
25922596
}),
25932597
qualifiers: _,
@@ -2906,7 +2910,7 @@ impl<'db> Type<'db> {
29062910
elem.member_lookup_with_policy(db, name_str.into(), policy)
29072911
}),
29082912

2909-
Type::Dynamic(..) | Type::Never => Place::bound(self).into(),
2913+
Type::Dynamic(..) | Type::Divergent(_) | Type::Never => Place::bound(self).into(),
29102914

29112915
Type::FunctionLiteral(function) if name == "__get__" => Place::bound(
29122916
Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet(function)),
@@ -3775,7 +3779,7 @@ impl<'db> Type<'db> {
37753779

37763780
// Dynamic types are callable, and the return type is the same dynamic type. Similarly,
37773781
// `Never` is always callable and returns `Never`.
3778-
Type::Dynamic(_) | Type::Never => {
3782+
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => {
37793783
Binding::single(self, Signature::dynamic(self)).into()
37803784
}
37813785

@@ -4870,7 +4874,7 @@ impl<'db> Type<'db> {
48704874
return_ty: return_builder.map(IntersectionBuilder::build),
48714875
})
48724876
}
4873-
ty @ (Type::Dynamic(_) | Type::Never) => Some(GeneratorTypes {
4877+
ty @ (Type::Dynamic(_) | Type::Divergent(_) | Type::Never) => Some(GeneratorTypes {
48744878
yield_ty: Some(ty),
48754879
send_ty: Some(ty),
48764880
return_ty: Some(ty),
@@ -4892,7 +4896,7 @@ impl<'db> Type<'db> {
48924896
#[must_use]
48934897
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option<Type<'db>> {
48944898
match self {
4895-
Type::Dynamic(_) | Type::Never => Some(self),
4899+
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Some(self),
48964900
Type::ClassLiteral(class) => Some(Type::instance(db, class.default_specialization(db))),
48974901
Type::GenericAlias(alias) => Some(Type::instance(db, ClassType::from(alias))),
48984902
Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance(db)),
@@ -5109,7 +5113,7 @@ impl<'db> Type<'db> {
51095113
}
51105114
}
51115115

5112-
Type::Dynamic(_) => Ok(*self),
5116+
Type::Dynamic(_) | Type::Divergent(_) => Ok(*self),
51135117

51145118
Type::NominalInstance(instance) => match instance.known_class(db) {
51155119
Some(KnownClass::NoneType) => Ok(Type::none(db)),
@@ -5191,6 +5195,7 @@ impl<'db> Type<'db> {
51915195
Type::GenericAlias(alias) => ClassType::from(alias).metaclass(db),
51925196
Type::SubclassOf(subclass_of_ty) => subclass_of_ty.to_meta_type(db),
51935197
Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)),
5198+
Type::Divergent(_) => self,
51945199
// TODO intersections
51955200
Type::Intersection(_) => {
51965201
SubclassOfType::try_from_type(db, todo_type!("Intersection meta-type"))
@@ -5525,19 +5530,15 @@ impl<'db> Type<'db> {
55255530
TypeMapping::ReplaceParameterDefaults |
55265531
TypeMapping::EagerExpansion |
55275532
TypeMapping::RescopeReturnCallables(_) => self,
5528-
TypeMapping::Materialize(materialization_kind) => match self {
5529-
// `Divergent` is an internal cycle marker rather than a gradual type like
5530-
// `Any` or `Unknown`. Materializing it away would destroy the marker we rely
5531-
// on for recursive alias convergence.
5532-
// TODO: We elsewhere treat `Divergent` as a dynamic type, so failing to
5533-
// materialize it away here could lead to odd behavior.
5534-
Type::Dynamic(DynamicType::Divergent(_)) => self,
5535-
_ => match materialization_kind {
5536-
MaterializationKind::Top => Type::object(),
5537-
MaterializationKind::Bottom => Type::Never,
5538-
},
5533+
TypeMapping::Materialize(materialization_kind) => match materialization_kind {
5534+
MaterializationKind::Top => Type::object(),
5535+
MaterializationKind::Bottom => Type::Never,
55395536
}
55405537
}
5538+
// `Divergent` is an internal cycle marker rather than a gradual type like `Any` or
5539+
// `Unknown`. Materializing it away would destroy the marker we rely on for recursive
5540+
// alias convergence.
5541+
Type::Divergent(_) => self,
55415542

55425543
Type::Never
55435544
| Type::AlwaysTruthy
@@ -5613,6 +5614,7 @@ impl<'db> Type<'db> {
56135614
typevars.insert(bound_typevar);
56145615
}
56155616
}
5617+
Type::Divergent(_) => {}
56165618

56175619
Type::FunctionLiteral(function) => {
56185620
visitor.visit(self, || {
@@ -5970,9 +5972,9 @@ impl<'db> Type<'db> {
59705972
Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db),
59715973

59725974
// These types have no definition
5973-
Self::Dynamic(
5974-
DynamicType::Divergent(_)
5975-
| DynamicType::Todo(_)
5975+
Self::Divergent(_)
5976+
| Self::Dynamic(
5977+
DynamicType::Todo(_)
59765978
| DynamicType::TodoUnpack
59775979
| DynamicType::TodoStarredExpression
59785980
| DynamicType::TodoTypeVarTuple
@@ -6149,6 +6151,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
61496151
Type::TypeGuard(type_guard_type) => type_guard_type.variance_of(db, typevar),
61506152
Type::KnownInstance(known_instance) => known_instance.variance_of(db, typevar),
61516153
Type::Dynamic(_)
6154+
| Type::Divergent(_)
61526155
| Type::Never
61536156
| Type::WrapperDescriptor(_)
61546157
| Type::KnownBoundMethod(_)
@@ -6459,8 +6462,6 @@ pub enum DynamicType<'db> {
64596462
TodoStarredExpression,
64606463
/// A special Todo-variant for `TypeVarTuple` instances encountered in type expressions
64616464
TodoTypeVarTuple,
6462-
/// A type that is determined to be divergent during recursive type inference.
6463-
Divergent(DivergentType),
64646465
}
64656466

64666467
impl DynamicType<'_> {
@@ -6485,7 +6486,6 @@ impl std::fmt::Display for DynamicType<'_> {
64856486
DynamicType::TodoUnpack => f.write_str("@Todo(typing.Unpack)"),
64866487
DynamicType::TodoStarredExpression => f.write_str("@Todo(StarredExpression)"),
64876488
DynamicType::TodoTypeVarTuple => f.write_str("@Todo(TypeVarTuple)"),
6488-
DynamicType::Divergent(_) => f.write_str("Divergent"),
64896489
}
64906490
}
64916491
}

crates/ty_python_semantic/src/types/bool.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ impl<'db> Type<'db> {
207207

208208
let truthiness = match self {
209209
Type::Dynamic(_)
210+
| Type::Divergent(_)
210211
| Type::Never
211212
| Type::Callable(_)
212213
| Type::TypeIs(_)

0 commit comments

Comments
 (0)