Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/ty_python_semantic/src/semantic_index/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{

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

Expand Down
100 changes: 90 additions & 10 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signat
use crate::types::tuple::TupleSpec;
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
use crate::types::variance::{TypeVarVariance, VarianceInferable};
use crate::types::visitor::any_over_type;
use crate::unpack::EvaluationMode;
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
use crate::{Db, FxOrderSet, Module, Program};
Expand Down Expand Up @@ -602,7 +603,7 @@ impl From<DataclassTransformerParams> for DataclassParams {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
pub enum Type<'db> {
/// The dynamic type: a statically unknown set of values
Dynamic(DynamicType),
Dynamic(DynamicType<'db>),
/// The empty set of values
Never,
/// A specific function object
Expand Down Expand Up @@ -886,14 +887,14 @@ impl<'db> Type<'db> {
}
}

pub(crate) const fn into_dynamic(self) -> Option<DynamicType> {
pub(crate) const fn into_dynamic(self) -> Option<DynamicType<'db>> {
match self {
Type::Dynamic(dynamic_type) => Some(dynamic_type),
_ => None,
}
}

pub(crate) const fn expect_dynamic(self) -> DynamicType {
pub(crate) const fn expect_dynamic(self) -> DynamicType<'db> {
self.into_dynamic()
.expect("Expected a Type::Dynamic variant")
}
Expand Down Expand Up @@ -1856,6 +1857,10 @@ impl<'db> Type<'db> {
}

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

(Type::SubclassOf(first), Type::SubclassOf(second)) => {
Expand Down Expand Up @@ -6565,6 +6570,14 @@ impl<'db> Type<'db> {
_ => None,
}
}

#[allow(unused)]
pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool {
any_over_type(db, self, &|ty| match ty {
Type::Dynamic(DynamicType::Divergent(_)) => ty == div,
_ => false,
})
}
}

impl<'db> From<&Type<'db>> for Type<'db> {
Expand Down Expand Up @@ -6959,8 +6972,19 @@ impl<'db> KnownInstanceType<'db> {
}
}

#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
pub enum DynamicType {
/// A type that is determined to be divergent during recursive type inference.
/// This type must never be eliminated by dynamic type reduction
/// (e.g. `Divergent` is assignable to `@Todo`, but `@Todo | Divergent` must not be reducted to `@Todo`).
/// Otherwise, type inference cannot converge properly.
/// For detailed properties of this type, see the unit test at the end of the file.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
pub struct DivergentType<'db> {
/// The scope where this divergence was detected.
scope: ScopeId<'db>,
}

#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
pub enum DynamicType<'db> {
/// An explicitly annotated `typing.Any`
Any,
/// An unannotated value, or a dynamic type resulting from an error
Expand All @@ -6983,16 +7007,21 @@ pub enum DynamicType {
TodoTypeAlias,
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoUnpack,
/// A type that is determined to be divergent during type inference for a recursive function.
Divergent(DivergentType<'db>),
}

impl DynamicType {
#[expect(clippy::unused_self)]
impl DynamicType<'_> {
fn normalized(self) -> Self {
Self::Any
if matches!(self, Self::Divergent(_)) {
self
} else {
Self::Any
}
}
}

impl std::fmt::Display for DynamicType {
impl std::fmt::Display for DynamicType<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DynamicType::Any => f.write_str("Any"),
Expand Down Expand Up @@ -7021,6 +7050,7 @@ impl std::fmt::Display for DynamicType {
f.write_str("@Todo")
}
}
DynamicType::Divergent(_) => f.write_str("Divergent"),
}
}
}
Expand Down Expand Up @@ -10263,7 +10293,7 @@ impl BoundSuperError<'_> {

#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, get_size2::GetSize)]
pub enum SuperOwnerKind<'db> {
Dynamic(DynamicType),
Dynamic(DynamicType<'db>),
Class(ClassType<'db>),
Instance(NominalInstanceType<'db>),
}
Expand Down Expand Up @@ -10629,6 +10659,7 @@ pub(crate) mod tests {
use super::*;
use crate::db::tests::{TestDbBuilder, setup_db};
use crate::place::{global_symbol, typing_extensions_symbol, typing_symbol};
use crate::semantic_index::FileScopeId;
use ruff_db::files::system_path_to_file;
use ruff_db::parsed::parsed_module;
use ruff_db::system::DbWithWritableSystem as _;
Expand Down Expand Up @@ -10774,4 +10805,53 @@ pub(crate) mod tests {
.is_todo()
);
}

#[test]
fn divergent_type() {
let mut db = setup_db();

db.write_dedented("src/foo.py", "").unwrap();
let file = system_path_to_file(&db, "src/foo.py").unwrap();
let file_scope_id = FileScopeId::global();
let scope = file_scope_id.to_scope_id(&db, file);

let div = Type::Dynamic(DynamicType::Divergent(DivergentType { scope }));

// The `Divergent` type must not be eliminated in union with other dynamic types,
// as this would prevent detection of divergent type inference using `Divergent`.
let union = UnionType::from_elements(&db, [Type::unknown(), div]);
assert_eq!(union.display(&db).to_string(), "Unknown | Divergent");

let union = UnionType::from_elements(&db, [div, Type::unknown()]);
assert_eq!(union.display(&db).to_string(), "Divergent | Unknown");

let union = UnionType::from_elements(&db, [div, Type::unknown(), todo_type!("1")]);
assert_eq!(union.display(&db).to_string(), "Divergent | Unknown");

assert!(div.is_equivalent_to(&db, div));
assert!(!div.is_equivalent_to(&db, Type::unknown()));
assert!(!Type::unknown().is_equivalent_to(&db, div));

// The `object` type has a good convergence property, that is, its union with all other types is `object`.
// (e.g. `object | tuple[Divergent] == object`, `object | tuple[object] == object`)
// So we can safely eliminate `Divergent`.
let union = UnionType::from_elements(&db, [div, KnownClass::Object.to_instance(&db)]);
assert_eq!(union.display(&db).to_string(), "object");

let union = UnionType::from_elements(&db, [KnownClass::Object.to_instance(&db), div]);
assert_eq!(union.display(&db).to_string(), "object");

// The same can be said about intersections for the `Never` type.
let intersection = IntersectionBuilder::new(&db)
.add_positive(Type::Never)
.add_positive(div)
.build();
assert_eq!(intersection.display(&db).to_string(), "Never");

let intersection = IntersectionBuilder::new(&db)
.add_positive(div)
.add_positive(Type::Never)
.build();
assert_eq!(intersection.display(&db).to_string(), "Never");
}
}
3 changes: 2 additions & 1 deletion crates/ty_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::types::{
/// automatically construct the default specialization for that class.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub enum ClassBase<'db> {
Dynamic(DynamicType),
Dynamic(DynamicType<'db>),
Class(ClassType<'db>),
/// Although `Protocol` is not a class in typeshed's stubs, it is at runtime,
/// and can appear in the MRO of a class.
Expand Down Expand Up @@ -54,6 +54,7 @@ impl<'db> ClassBase<'db> {
| DynamicType::TodoTypeAlias
| DynamicType::TodoUnpack,
) => "@Todo",
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
ClassBase::Protocol => "Protocol",
ClassBase::Generic => "Generic",
ClassBase::TypedDict => "TypedDict",
Expand Down
3 changes: 3 additions & 0 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7544,6 +7544,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {

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

(any @ Type::Dynamic(DynamicType::Any), _, _)
| (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any),

Expand Down
8 changes: 4 additions & 4 deletions crates/ty_python_semantic/src/types/subclass_of.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ impl<'db> VarianceInferable<'db> for SubclassOfType<'db> {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
pub(crate) enum SubclassOfInner<'db> {
Class(ClassType<'db>),
Dynamic(DynamicType),
Dynamic(DynamicType<'db>),
}

impl<'db> SubclassOfInner<'db> {
Expand All @@ -240,7 +240,7 @@ impl<'db> SubclassOfInner<'db> {
}
}

pub(crate) const fn into_dynamic(self) -> Option<DynamicType> {
pub(crate) const fn into_dynamic(self) -> Option<DynamicType<'db>> {
match self {
Self::Class(_) => None,
Self::Dynamic(dynamic) => Some(dynamic),
Expand Down Expand Up @@ -271,8 +271,8 @@ impl<'db> From<ClassType<'db>> for SubclassOfInner<'db> {
}
}

impl From<DynamicType> for SubclassOfInner<'_> {
fn from(value: DynamicType) -> Self {
impl<'db> From<DynamicType<'db>> for SubclassOfInner<'db> {
fn from(value: DynamicType<'db>) -> Self {
SubclassOfInner::Dynamic(value)
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/ty_python_semantic/src/types/type_ordering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering

(DynamicType::TodoTypeAlias, _) => Ordering::Less,
(_, DynamicType::TodoTypeAlias) => Ordering::Greater,

(DynamicType::Divergent(left), DynamicType::Divergent(right)) => {
left.scope.cmp(&right.scope)
}
(DynamicType::Divergent(_), _) => Ordering::Less,
(_, DynamicType::Divergent(_)) => Ordering::Greater,
}
}

Expand Down
Loading