Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8770b95

Browse files
mtshibacarljm
andauthored
[ty] introduce DivergentType (#20312)
## Summary From #17371 In #17371, `DivergentType` was introduced to prevent type inference for recursive functions from diverging and causing panics. This turned out to be useful for other divergent type inferences (#17371 (comment)), so I extracted the introduction part of `DivergentType` into this PR so that we can use it without waiting for the merge of #17371. Note that this PR only introduces `DivergentType` and does not actually address divergent type inference yet. Please refer to https://github.com/astral-sh/ruff/pull/17371/files#diff-20b910c6e20faa962bb1642e111db1cbad8e66ace089bdd966ac9d7f9fa99ff2R542-R622 etc. when implementing handling of divergence suppression using this type. ## Test Plan --------- Co-authored-by: Carl Meyer <[email protected]>
1 parent 65982a1 commit 8770b95

File tree

6 files changed

+106
-15
lines changed

6 files changed

+106
-15
lines changed

crates/ty_python_semantic/src/semantic_index/scope.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::{
1515

1616
/// A cross-module identifier of a scope that can be used as a salsa query parameter.
1717
#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
18+
#[derive(PartialOrd, Ord)]
1819
pub struct ScopeId<'db> {
1920
pub file: File,
2021

crates/ty_python_semantic/src/types.rs

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signat
6565
use crate::types::tuple::TupleSpec;
6666
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
6767
use crate::types::variance::{TypeVarVariance, VarianceInferable};
68+
use crate::types::visitor::any_over_type;
6869
use crate::unpack::EvaluationMode;
6970
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
7071
use crate::{Db, FxOrderSet, Module, Program};
@@ -602,7 +603,7 @@ impl From<DataclassTransformerParams> for DataclassParams {
602603
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
603604
pub enum Type<'db> {
604605
/// The dynamic type: a statically unknown set of values
605-
Dynamic(DynamicType),
606+
Dynamic(DynamicType<'db>),
606607
/// The empty set of values
607608
Never,
608609
/// A specific function object
@@ -885,14 +886,14 @@ impl<'db> Type<'db> {
885886
}
886887
}
887888

888-
pub(crate) const fn into_dynamic(self) -> Option<DynamicType> {
889+
pub(crate) const fn into_dynamic(self) -> Option<DynamicType<'db>> {
889890
match self {
890891
Type::Dynamic(dynamic_type) => Some(dynamic_type),
891892
_ => None,
892893
}
893894
}
894895

895-
pub(crate) const fn expect_dynamic(self) -> DynamicType {
896+
pub(crate) const fn expect_dynamic(self) -> DynamicType<'db> {
896897
self.into_dynamic()
897898
.expect("Expected a Type::Dynamic variant")
898899
}
@@ -1857,6 +1858,10 @@ impl<'db> Type<'db> {
18571858
}
18581859

18591860
match (self, other) {
1861+
// The `Divergent` type is a special type that is not equivalent to other kinds of dynamic types,
1862+
// which prevents `Divergent` from being eliminated during union reduction.
1863+
(Type::Dynamic(_), Type::Dynamic(DynamicType::Divergent(_)))
1864+
| (Type::Dynamic(DynamicType::Divergent(_)), Type::Dynamic(_)) => C::unsatisfiable(db),
18601865
(Type::Dynamic(_), Type::Dynamic(_)) => C::always_satisfiable(db),
18611866

18621867
(Type::SubclassOf(first), Type::SubclassOf(second)) => {
@@ -6566,6 +6571,14 @@ impl<'db> Type<'db> {
65666571
_ => None,
65676572
}
65686573
}
6574+
6575+
#[allow(unused)]
6576+
pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool {
6577+
any_over_type(db, self, &|ty| match ty {
6578+
Type::Dynamic(DynamicType::Divergent(_)) => ty == div,
6579+
_ => false,
6580+
})
6581+
}
65696582
}
65706583

65716584
impl<'db> From<&Type<'db>> for Type<'db> {
@@ -6960,8 +6973,19 @@ impl<'db> KnownInstanceType<'db> {
69606973
}
69616974
}
69626975

6963-
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
6964-
pub enum DynamicType {
6976+
/// A type that is determined to be divergent during recursive type inference.
6977+
/// This type must never be eliminated by dynamic type reduction
6978+
/// (e.g. `Divergent` is assignable to `@Todo`, but `@Todo | Divergent` must not be reducted to `@Todo`).
6979+
/// Otherwise, type inference cannot converge properly.
6980+
/// For detailed properties of this type, see the unit test at the end of the file.
6981+
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
6982+
pub struct DivergentType<'db> {
6983+
/// The scope where this divergence was detected.
6984+
scope: ScopeId<'db>,
6985+
}
6986+
6987+
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
6988+
pub enum DynamicType<'db> {
69656989
/// An explicitly annotated `typing.Any`
69666990
Any,
69676991
/// An unannotated value, or a dynamic type resulting from an error
@@ -6984,16 +7008,21 @@ pub enum DynamicType {
69847008
TodoTypeAlias,
69857009
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
69867010
TodoUnpack,
7011+
/// A type that is determined to be divergent during type inference for a recursive function.
7012+
Divergent(DivergentType<'db>),
69877013
}
69887014

6989-
impl DynamicType {
6990-
#[expect(clippy::unused_self)]
7015+
impl DynamicType<'_> {
69917016
fn normalized(self) -> Self {
6992-
Self::Any
7017+
if matches!(self, Self::Divergent(_)) {
7018+
self
7019+
} else {
7020+
Self::Any
7021+
}
69937022
}
69947023
}
69957024

6996-
impl std::fmt::Display for DynamicType {
7025+
impl std::fmt::Display for DynamicType<'_> {
69977026
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69987027
match self {
69997028
DynamicType::Any => f.write_str("Any"),
@@ -7022,6 +7051,7 @@ impl std::fmt::Display for DynamicType {
70227051
f.write_str("@Todo")
70237052
}
70247053
}
7054+
DynamicType::Divergent(_) => f.write_str("Divergent"),
70257055
}
70267056
}
70277057
}
@@ -10290,7 +10320,7 @@ impl BoundSuperError<'_> {
1029010320

1029110321
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, get_size2::GetSize)]
1029210322
pub enum SuperOwnerKind<'db> {
10293-
Dynamic(DynamicType),
10323+
Dynamic(DynamicType<'db>),
1029410324
Class(ClassType<'db>),
1029510325
Instance(NominalInstanceType<'db>),
1029610326
}
@@ -10656,6 +10686,7 @@ pub(crate) mod tests {
1065610686
use super::*;
1065710687
use crate::db::tests::{TestDbBuilder, setup_db};
1065810688
use crate::place::{global_symbol, typing_extensions_symbol, typing_symbol};
10689+
use crate::semantic_index::FileScopeId;
1065910690
use ruff_db::files::system_path_to_file;
1066010691
use ruff_db::parsed::parsed_module;
1066110692
use ruff_db::system::DbWithWritableSystem as _;
@@ -10801,4 +10832,53 @@ pub(crate) mod tests {
1080110832
.is_todo()
1080210833
);
1080310834
}
10835+
10836+
#[test]
10837+
fn divergent_type() {
10838+
let mut db = setup_db();
10839+
10840+
db.write_dedented("src/foo.py", "").unwrap();
10841+
let file = system_path_to_file(&db, "src/foo.py").unwrap();
10842+
let file_scope_id = FileScopeId::global();
10843+
let scope = file_scope_id.to_scope_id(&db, file);
10844+
10845+
let div = Type::Dynamic(DynamicType::Divergent(DivergentType { scope }));
10846+
10847+
// The `Divergent` type must not be eliminated in union with other dynamic types,
10848+
// as this would prevent detection of divergent type inference using `Divergent`.
10849+
let union = UnionType::from_elements(&db, [Type::unknown(), div]);
10850+
assert_eq!(union.display(&db).to_string(), "Unknown | Divergent");
10851+
10852+
let union = UnionType::from_elements(&db, [div, Type::unknown()]);
10853+
assert_eq!(union.display(&db).to_string(), "Divergent | Unknown");
10854+
10855+
let union = UnionType::from_elements(&db, [div, Type::unknown(), todo_type!("1")]);
10856+
assert_eq!(union.display(&db).to_string(), "Divergent | Unknown");
10857+
10858+
assert!(div.is_equivalent_to(&db, div));
10859+
assert!(!div.is_equivalent_to(&db, Type::unknown()));
10860+
assert!(!Type::unknown().is_equivalent_to(&db, div));
10861+
10862+
// The `object` type has a good convergence property, that is, its union with all other types is `object`.
10863+
// (e.g. `object | tuple[Divergent] == object`, `object | tuple[object] == object`)
10864+
// So we can safely eliminate `Divergent`.
10865+
let union = UnionType::from_elements(&db, [div, KnownClass::Object.to_instance(&db)]);
10866+
assert_eq!(union.display(&db).to_string(), "object");
10867+
10868+
let union = UnionType::from_elements(&db, [KnownClass::Object.to_instance(&db), div]);
10869+
assert_eq!(union.display(&db).to_string(), "object");
10870+
10871+
// The same can be said about intersections for the `Never` type.
10872+
let intersection = IntersectionBuilder::new(&db)
10873+
.add_positive(Type::Never)
10874+
.add_positive(div)
10875+
.build();
10876+
assert_eq!(intersection.display(&db).to_string(), "Never");
10877+
10878+
let intersection = IntersectionBuilder::new(&db)
10879+
.add_positive(div)
10880+
.add_positive(Type::Never)
10881+
.build();
10882+
assert_eq!(intersection.display(&db).to_string(), "Never");
10883+
}
1080410884
}

crates/ty_python_semantic/src/types/class_base.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::types::{
1818
/// automatically construct the default specialization for that class.
1919
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
2020
pub enum ClassBase<'db> {
21-
Dynamic(DynamicType),
21+
Dynamic(DynamicType<'db>),
2222
Class(ClassType<'db>),
2323
/// Although `Protocol` is not a class in typeshed's stubs, it is at runtime,
2424
/// and can appear in the MRO of a class.
@@ -54,6 +54,7 @@ impl<'db> ClassBase<'db> {
5454
| DynamicType::TodoTypeAlias
5555
| DynamicType::TodoUnpack,
5656
) => "@Todo",
57+
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
5758
ClassBase::Protocol => "Protocol",
5859
ClassBase::Generic => "Generic",
5960
ClassBase::TypedDict => "TypedDict",

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7544,6 +7544,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
75447544

75457545
// Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future,
75467546
// the result would then become Any or Unknown, respectively).
7547+
(div @ Type::Dynamic(DynamicType::Divergent(_)), _, _)
7548+
| (_, div @ Type::Dynamic(DynamicType::Divergent(_)), _) => Some(div),
7549+
75477550
(any @ Type::Dynamic(DynamicType::Any), _, _)
75487551
| (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any),
75497552

crates/ty_python_semantic/src/types/subclass_of.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ impl<'db> VarianceInferable<'db> for SubclassOfType<'db> {
221221
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
222222
pub(crate) enum SubclassOfInner<'db> {
223223
Class(ClassType<'db>),
224-
Dynamic(DynamicType),
224+
Dynamic(DynamicType<'db>),
225225
}
226226

227227
impl<'db> SubclassOfInner<'db> {
@@ -240,7 +240,7 @@ impl<'db> SubclassOfInner<'db> {
240240
}
241241
}
242242

243-
pub(crate) const fn into_dynamic(self) -> Option<DynamicType> {
243+
pub(crate) const fn into_dynamic(self) -> Option<DynamicType<'db>> {
244244
match self {
245245
Self::Class(_) => None,
246246
Self::Dynamic(dynamic) => Some(dynamic),
@@ -271,8 +271,8 @@ impl<'db> From<ClassType<'db>> for SubclassOfInner<'db> {
271271
}
272272
}
273273

274-
impl From<DynamicType> for SubclassOfInner<'_> {
275-
fn from(value: DynamicType) -> Self {
274+
impl<'db> From<DynamicType<'db>> for SubclassOfInner<'db> {
275+
fn from(value: DynamicType<'db>) -> Self {
276276
SubclassOfInner::Dynamic(value)
277277
}
278278
}

crates/ty_python_semantic/src/types/type_ordering.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
275275

276276
(DynamicType::TodoTypeAlias, _) => Ordering::Less,
277277
(_, DynamicType::TodoTypeAlias) => Ordering::Greater,
278+
279+
(DynamicType::Divergent(left), DynamicType::Divergent(right)) => {
280+
left.scope.cmp(&right.scope)
281+
}
282+
(DynamicType::Divergent(_), _) => Ordering::Less,
283+
(_, DynamicType::Divergent(_)) => Ordering::Greater,
278284
}
279285
}
280286

0 commit comments

Comments
 (0)