diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 1c322419e0039b..f58f0013b1cfe2 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -492,6 +492,10 @@ impl File { .map_or(PySourceType::Python, PySourceType::from_extension), } } + + pub fn structural_ordering(self, db: &dyn Db, other: Self) -> std::cmp::Ordering { + self.path(db).cmp(other.path(db)) + } } impl fmt::Debug for File { diff --git a/crates/ruff_db/src/files/path.rs b/crates/ruff_db/src/files/path.rs index 557a70cdf97f3d..5738c40073e745 100644 --- a/crates/ruff_db/src/files/path.rs +++ b/crates/ruff_db/src/files/path.rs @@ -11,7 +11,7 @@ use std::fmt::{Display, Formatter}; /// * a file stored on the [host system](crate::system::System). /// * a virtual file stored on the [host system](crate::system::System). /// * a vendored file stored in the [vendored file system](crate::vendored::VendoredFileSystem). -#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, get_size2::GetSize)] pub enum FilePath { /// Path to a file on the [host system](crate::system::System). System(SystemPathBuf), diff --git a/crates/ruff_db/src/vendored/path.rs b/crates/ruff_db/src/vendored/path.rs index 86cdd5057e1e19..d93a4bb978b453 100644 --- a/crates/ruff_db/src/vendored/path.rs +++ b/crates/ruff_db/src/vendored/path.rs @@ -88,7 +88,7 @@ impl ToOwned for VendoredPath { } #[repr(transparent)] -#[derive(Debug, Eq, PartialEq, Clone, Hash)] +#[derive(Debug, Eq, PartialEq, Clone, Hash, PartialOrd, Ord)] pub struct VendoredPathBuf(Utf8PathBuf); impl get_size2::GetSize for VendoredPathBuf { diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index f2eb8862237aba..1c708bbb6593aa 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2325,7 +2325,7 @@ class C: def copy(self, other: "C"): self.x = other.x -reveal_type(C().x) # revealed: Unknown | Literal[1] +reveal_type(C().x) # revealed: Literal[1] | Unknown ``` If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute: @@ -2381,8 +2381,8 @@ class B: def copy(self, other: "A"): self.x = other.x -reveal_type(B().x) # revealed: Unknown | Literal[1] -reveal_type(A().x) # revealed: Unknown | Literal[1] +reveal_type(B().x) # revealed: Literal[1] | Unknown +reveal_type(A().x) # revealed: Literal[1] | Unknown class Base: def flip(self) -> "Sub": @@ -2400,7 +2400,7 @@ class C2: def replace_with(self, other: "C2"): self.x = other.x.flip() -reveal_type(C2(Sub()).x) # revealed: Unknown | Base +reveal_type(C2(Sub()).x) # revealed: Base | Unknown class C3: def __init__(self, x: Sub): @@ -2409,8 +2409,8 @@ class C3: def replace_with(self, other: "C3"): self.x = [self.x[0].flip()] -# TODO: should be `Unknown | list[Unknown | Sub] | list[Unknown | Base]` -reveal_type(C3(Sub()).x) # revealed: Unknown | list[Unknown | Sub] | list[Divergent] +# TODO: should be `list[Unknown | Sub] | list[Unknown | Base] | Unknown` +reveal_type(C3(Sub()).x) # revealed: list[Divergent] | list[Unknown | Sub] | Unknown ``` And cycles between many attributes: @@ -2453,13 +2453,13 @@ class ManyCycles: self.x6 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x7 self.x7 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x6 - reveal_type(self.x1) # revealed: Unknown | int - reveal_type(self.x2) # revealed: Unknown | int - reveal_type(self.x3) # revealed: Unknown | int - reveal_type(self.x4) # revealed: Unknown | int - reveal_type(self.x5) # revealed: Unknown | int - reveal_type(self.x6) # revealed: Unknown | int - reveal_type(self.x7) # revealed: Unknown | int + reveal_type(self.x1) # revealed: int | Unknown + reveal_type(self.x2) # revealed: int | Unknown + reveal_type(self.x3) # revealed: int | Unknown + reveal_type(self.x4) # revealed: int | Unknown + reveal_type(self.x5) # revealed: int | Unknown + reveal_type(self.x6) # revealed: int | Unknown + reveal_type(self.x7) # revealed: int | Unknown class ManyCycles2: def __init__(self: "ManyCycles2"): @@ -2468,8 +2468,8 @@ class ManyCycles2: self.x3 = [1] def f1(self: "ManyCycles2"): - # TODO: should be Unknown | list[Unknown | int] | list[Divergent] - reveal_type(self.x3) # revealed: Unknown | list[Unknown | int] | list[Divergent] | list[Divergent] + # TODO: should be list[Unknown | int] | list[Divergent] | Unknown + reveal_type(self.x3) # revealed: list[Divergent] | list[Divergent] | list[Unknown | int] | Unknown self.x1 = [self.x2] + [self.x3] self.x2 = [self.x1] + [self.x3] @@ -2528,7 +2528,7 @@ class Counter: def increment(self: "Counter"): self.count = self.count + 1 -reveal_type(Counter().count) # revealed: Unknown | int +reveal_type(Counter().count) # revealed: int | Unknown ``` We also handle infinitely nested generics: @@ -2541,7 +2541,7 @@ class NestedLists: def f(self: "NestedLists"): self.x = [self.x] -reveal_type(NestedLists().x) # revealed: Unknown | Literal[1] | list[Divergent] +reveal_type(NestedLists().x) # revealed: Literal[1] | list[Divergent] | Unknown class NestedMixed: def f(self: "NestedMixed"): @@ -2550,7 +2550,7 @@ class NestedMixed: def g(self: "NestedMixed"): self.x = {self.x} -reveal_type(NestedMixed().x) # revealed: Unknown | list[Divergent] | set[Divergent] +reveal_type(NestedMixed().x) # revealed: list[Divergent] | set[Divergent] | Unknown ``` And cases where the types originate from annotations: @@ -2567,7 +2567,7 @@ class NestedLists2: def f(self: "NestedLists2"): self.x = make_list(self.x) -reveal_type(NestedLists2().x) # revealed: Unknown | list[Divergent] +reveal_type(NestedLists2().x) # revealed: list[Divergent] | Unknown ``` ### Builtin types attributes @@ -2673,7 +2673,7 @@ class C: def f(self, other: "C"): self.x = (other.x, 1) -reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]] +reveal_type(C().x) # revealed: tuple[Divergent, Literal[1]] | Unknown reveal_type(C().x[0]) # revealed: Unknown | Divergent ``` @@ -2691,7 +2691,7 @@ class D: def f(self, other: "D"): self.x = make_tuple(other.x) -reveal_type(D().x) # revealed: Unknown | tuple[Divergent, Literal[1]] +reveal_type(D().x) # revealed: tuple[Divergent, Literal[1]] | Unknown ``` The tuple type may also expand exponentially "in breadth": @@ -2704,7 +2704,7 @@ class E: def f(self: "E"): self.x = duplicate(self.x) -reveal_type(E().x) # revealed: Unknown | tuple[Divergent, Divergent] +reveal_type(E().x) # revealed: tuple[Divergent, Divergent] | Unknown ``` And it also works for homogeneous tuples: @@ -2717,7 +2717,7 @@ class F: def f(self, other: "F"): self.x = make_homogeneous_tuple(other.x) -reveal_type(F().x) # revealed: Unknown | tuple[Divergent, ...] +reveal_type(F().x) # revealed: tuple[Divergent, ...] | Unknown ``` ## Attributes of standard library modules that aren't yet defined diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index 7d1686fb2dbbc4..a1af27e3c933fc 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -28,8 +28,8 @@ class Point: self.x, self.y = other.x, other.y p = Point() -reveal_type(p.x) # revealed: Unknown | int -reveal_type(p.y) # revealed: Unknown | int +reveal_type(p.x) # revealed: int | Unknown +reveal_type(p.y) # revealed: int | Unknown ``` ## Self-referential bare type alias diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index d4e4fafc737fa0..a8dbae5a2e4f57 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -248,9 +248,9 @@ IntOrStr = TypeAliasType(get_name(), int | str) type OptNestedInt = int | tuple[OptNestedInt, ...] | None def f(x: OptNestedInt) -> None: - reveal_type(x) # revealed: int | tuple[OptNestedInt, ...] | None + reveal_type(x) # revealed: tuple[OptNestedInt, ...] | None | int if x is not None: - reveal_type(x) # revealed: int | tuple[OptNestedInt, ...] + reveal_type(x) # revealed: tuple[OptNestedInt, ...] | int ``` ### Invalid self-referential @@ -327,7 +327,7 @@ class B(A[Alias]): def f(b: B): reveal_type(b) # revealed: B - reveal_type(b.attr) # revealed: list[Alias] | int + reveal_type(b.attr) # revealed: int | list[Alias] ``` ### Mutually recursive @@ -450,5 +450,5 @@ type Y = X | str | dict[str, Y] def _(y: Y): if isinstance(y, dict): - reveal_type(y) # revealed: dict[str, X] | dict[str, Y] + reveal_type(y) # revealed: dict[str, Y] | dict[str, X] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index cfa4c689145709..a5888995bc1bb0 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -3029,7 +3029,7 @@ class B(A[P[int]]): def f(b: B): reveal_type(b) # revealed: B reveal_type(b.attr) # revealed: P[int] - reveal_type(b.attr.attr) # revealed: P[int] | int + reveal_type(b.attr.attr) # revealed: int | P[int] ``` ### Recursive generic protocols with property members diff --git a/crates/ty_python_semantic/src/module_resolver/module.rs b/crates/ty_python_semantic/src/module_resolver/module.rs index 6927c3b89f5a20..4218a93e7246e9 100644 --- a/crates/ty_python_semantic/src/module_resolver/module.rs +++ b/crates/ty_python_semantic/src/module_resolver/module.rs @@ -102,6 +102,17 @@ impl<'db> Module<'db> { .as_deref() .unwrap_or_default() } + + pub(crate) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self, other) { + (Module::File(left), Module::File(right)) => left.structural_ordering(db, right), + (Module::Namespace(left), Module::Namespace(right)) => { + left.name(db).cmp(right.name(db)) + } + (Module::File(_), Module::Namespace(_)) => std::cmp::Ordering::Less, + (Module::Namespace(_), Module::File(_)) => std::cmp::Ordering::Greater, + } + } } impl std::fmt::Debug for Module<'_> { @@ -274,6 +285,14 @@ pub struct FileModule<'db> { pub(super) known: Option, } +impl FileModule<'_> { + pub(crate) fn structural_ordering(self, db: &dyn Db, other: Self) -> std::cmp::Ordering { + self.name(db) + .cmp(other.name(db)) + .then_with(|| self.file(db).structural_ordering(db, other.file(db))) + } +} + /// A namespace package. /// /// Namespace packages are special because there are diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 2659e75493b0ce..d368ad8017d56e 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -122,6 +122,17 @@ impl<'db> Definition<'db> { _ => None, } } + + pub(crate) fn structural_ordering( + self, + db: &'db dyn Db, + other: Definition<'db>, + ) -> std::cmp::Ordering { + self.file(db) + .cmp(&other.file(db)) + .then_with(|| self.file_scope(db).cmp(&other.file_scope(db))) + .then_with(|| self.place(db).cmp(&other.place(db))) + } } /// Get the module-level docstring for the given file diff --git a/crates/ty_python_semantic/src/semantic_index/member.rs b/crates/ty_python_semantic/src/semantic_index/member.rs index 8511c7b2951595..f4d8d1c41b2bc8 100644 --- a/crates/ty_python_semantic/src/semantic_index/member.rs +++ b/crates/ty_python_semantic/src/semantic_index/member.rs @@ -347,7 +347,7 @@ impl Hash for MemberExprRef<'_> { /// Uniquely identifies a member in a scope. #[newtype_index] -#[derive(get_size2::GetSize, salsa::Update)] +#[derive(PartialOrd, Ord, get_size2::GetSize, salsa::Update)] pub struct ScopedMemberId; /// The members of a scope. Allows lookup by member path and [`ScopedMemberId`]. diff --git a/crates/ty_python_semantic/src/semantic_index/place.rs b/crates/ty_python_semantic/src/semantic_index/place.rs index 04bea976264039..21674628426981 100644 --- a/crates/ty_python_semantic/src/semantic_index/place.rs +++ b/crates/ty_python_semantic/src/semantic_index/place.rs @@ -133,7 +133,9 @@ impl std::fmt::Display for PlaceExprRef<'_> { } /// ID that uniquely identifies a place inside a [`Scope`](super::FileScopeId). -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, get_size2::GetSize, salsa::Update)] +#[derive( + Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, get_size2::GetSize, salsa::Update, +)] pub enum ScopedPlaceId { Symbol(ScopedSymbolId), Member(ScopedMemberId), diff --git a/crates/ty_python_semantic/src/semantic_index/scope.rs b/crates/ty_python_semantic/src/semantic_index/scope.rs index c7c42241a34871..bb5cb0e580781e 100644 --- a/crates/ty_python_semantic/src/semantic_index/scope.rs +++ b/crates/ty_python_semantic/src/semantic_index/scope.rs @@ -64,11 +64,21 @@ impl<'db> ScopeId<'db> { NodeWithScopeKind::GeneratorExpression(_) => "", } } + + pub(crate) fn structural_ordering( + self, + db: &'db dyn Db, + other: ScopeId<'db>, + ) -> std::cmp::Ordering { + self.file(db) + .cmp(&other.file(db)) + .then_with(|| self.file_scope_id(db).cmp(&other.file_scope_id(db))) + } } /// ID that uniquely identifies a scope inside of a module. #[newtype_index] -#[derive(salsa::Update, get_size2::GetSize)] +#[derive(salsa::Update, get_size2::GetSize, PartialOrd, Ord)] pub struct FileScopeId; impl FileScopeId { diff --git a/crates/ty_python_semantic/src/semantic_index/symbol.rs b/crates/ty_python_semantic/src/semantic_index/symbol.rs index 8aea606f597bf9..f29734a1ce84a3 100644 --- a/crates/ty_python_semantic/src/semantic_index/symbol.rs +++ b/crates/ty_python_semantic/src/semantic_index/symbol.rs @@ -8,7 +8,7 @@ use std::ops::{Deref, DerefMut}; /// Uniquely identifies a symbol in a given scope. #[newtype_index] -#[derive(get_size2::GetSize)] +#[derive(PartialOrd, Ord, get_size2::GetSize)] pub struct ScopedSymbolId; /// A symbol in a given scope. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ff41b2d06dd270..df8f6e70e9e924 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -19,7 +19,7 @@ use ruff_python_ast::name::Name; use ruff_text_size::{Ranged, TextRange}; use smallvec::{SmallVec, smallvec}; -use type_ordering::union_or_intersection_elements_ordering; +use type_ordering::{structural_type_ordering, union_or_intersection_elements_ordering}; pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder}; pub use self::cyclic::CycleDetector; @@ -44,6 +44,7 @@ use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{imported_modules, place_table, semantic_index}; use crate::suppression::check_suppressions; use crate::types::bound_super::BoundSuperType; +use crate::types::builder::RecursivelyDefined; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::constraints::{ @@ -652,6 +653,26 @@ impl<'db> PropertyInstanceType<'db> { getter_equivalence.and(db, setter_equivalence) } + + fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let getter_ord = match (self.getter(db), other.getter(db)) { + (Some(left), Some(right)) => structural_type_ordering(db, &left, &right), + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + (None, None) => std::cmp::Ordering::Equal, + }; + + if getter_ord != std::cmp::Ordering::Equal { + return getter_ord; + } + + match (self.setter(db), other.setter(db)) { + (Some(left), Some(right)) => structural_type_ordering(db, &left, &right), + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + (None, None) => std::cmp::Ordering::Equal, + } + } } bitflags! { @@ -755,6 +776,30 @@ impl<'db> DataclassParams<'db> { params.field_specifiers(db), ) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let flag_ord = self.flags(db).bits().cmp(&other.flags(db).bits()); + if flag_ord != std::cmp::Ordering::Equal { + return flag_ord; + } + + let self_fields = self.field_specifiers(db); + let other_fields = other.field_specifiers(db); + + let fields_count = self_fields.len().cmp(&other_fields.len()); + if fields_count != std::cmp::Ordering::Equal { + return fields_count; + } + + for (self_field, other_field) in self_fields.iter().zip(other_fields.iter()) { + let field_ord = structural_type_ordering(db, self_field, other_field); + if field_ord != std::cmp::Ordering::Equal { + return field_ord; + } + } + + std::cmp::Ordering::Equal + } } /// Representation of a type: a set of possible values at runtime. @@ -8763,6 +8808,61 @@ impl<'db> KnownInstanceType<'db> { fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { self.display_with(db, DisplaySettings::default()) } + + fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self, other) { + (Self::SubscriptedProtocol(left), Self::SubscriptedProtocol(right)) + | (Self::SubscriptedGeneric(left), Self::SubscriptedGeneric(right)) + | (Self::GenericContext(left), Self::GenericContext(right)) => { + left.structural_ordering(db, right) + } + (Self::TypeVar(left), Self::TypeVar(right)) => left + .identity(db) + .structural_ordering(db, right.identity(db)), + (Self::TypeAliasType(left), Self::TypeAliasType(right)) => { + left.structural_ordering(db, right) + } + (Self::Deprecated(left), Self::Deprecated(right)) => { + left.structural_ordering(db, right) + } + (Self::Field(left), Self::Field(right)) => left.structural_ordering(db, right), + // No need to compare structurally, they are used only in debugging contexts + (Self::ConstraintSet(left), Self::ConstraintSet(right)) => left.cmp(&right), + (Self::Specialization(left), Self::Specialization(right)) => { + left.structural_ordering(db, right) + } + (Self::UnionType(left), Self::UnionType(right)) => left.structural_ordering(db, right), + (Self::Literal(left), Self::Literal(right)) + | (Self::Annotated(left), Self::Annotated(right)) + | (Self::TypeGenericAlias(left), Self::TypeGenericAlias(right)) + | (Self::LiteralStringAlias(left), Self::LiteralStringAlias(right)) => { + structural_type_ordering(db, &left.inner(db), &right.inner(db)) + } + (Self::Callable(left), Self::Callable(right)) => left.structural_ordering(db, right), + (Self::NewType(left), Self::NewType(right)) => left.structural_ordering(db, right), + (left, right) => { + let index = |instance| match instance { + Self::SubscriptedProtocol(_) => 0, + Self::SubscriptedGeneric(_) => 1, + Self::TypeVar(_) => 2, + Self::TypeAliasType(_) => 3, + Self::Deprecated(_) => 4, + Self::Field(_) => 5, + Self::ConstraintSet(_) => 6, + Self::GenericContext(_) => 7, + Self::Specialization(_) => 8, + Self::UnionType(_) => 9, + Self::Literal(_) => 10, + Self::Annotated(_) => 11, + Self::TypeGenericAlias(_) => 12, + Self::Callable(_) => 13, + Self::LiteralStringAlias(_) => 14, + Self::NewType(_) => 15, + }; + index(left).cmp(&index(right)) + } + } + } } /// A type that is determined to be divergent during recursive type inference. @@ -8841,7 +8941,7 @@ impl std::fmt::Display for DynamicType { bitflags! { /// Type qualifiers that appear in an annotation expression. - #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, salsa::Update, Hash)] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, salsa::Update, Hash, PartialOrd, Ord)] pub(crate) struct TypeQualifiers: u8 { /// `typing.ClassVar` const CLASS_VAR = 1 << 0; @@ -9160,6 +9260,17 @@ pub struct DeprecatedInstance<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for DeprecatedInstance<'_> {} +impl DeprecatedInstance<'_> { + fn structural_ordering(self, db: &dyn Db, other: DeprecatedInstance<'_>) -> std::cmp::Ordering { + match (self.message(db), other.message(db)) { + (Some(left), Some(right)) => left.structural_ordering(db, right), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + } +} + /// Contains information about instances of `dataclasses.Field`, typically created using /// `dataclasses.field()`. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -9217,6 +9328,24 @@ impl<'db> FieldInstance<'db> { self.alias(db), )) } + + fn structural_ordering(self, db: &dyn Db, other: FieldInstance<'_>) -> std::cmp::Ordering { + match (self.default_type(db), other.default_type(db)) { + (Some(left), Some(right)) => { + let ord = structural_type_ordering(db, &left, &right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + (None, Some(_)) => return std::cmp::Ordering::Less, + (Some(_), None) => return std::cmp::Ordering::Greater, + (None, None) => {} + } + self.init(db) + .cmp(&other.init(db)) + .then_with(|| self.kw_only(db).cmp(&other.kw_only(db))) + .then_with(|| self.alias(db).cmp(&other.alias(db))) + } } /// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax, @@ -9270,6 +9399,19 @@ pub struct TypeVarIdentity<'db> { impl get_size2::GetSize for TypeVarIdentity<'_> {} +impl TypeVarIdentity<'_> { + fn structural_ordering(self, db: &dyn Db, other: TypeVarIdentity<'_>) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + match (self.definition(db), other.definition(db)) { + (Some(left), Some(right)) => left.structural_ordering(db, right), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + }) + } +} + /// A specific instance of a type variable that has not been bound to a generic context yet. /// /// This is usually not the type that you want; if you are working with a typevar, in a generic @@ -9568,6 +9710,7 @@ impl<'db> TypeVarInstance<'db> { .skip(1) .map(|arg| definition_expression_type(db, definition, arg)) .collect::>(), + RecursivelyDefined::No, ) } _ => return None, @@ -9855,6 +9998,17 @@ impl<'db> BoundTypeVarInstance<'db> { } } } + + fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.typevar(db) + .identity(db) + .structural_ordering(db, other.typevar(db).identity(db)) + .then_with(|| { + self.binding_context(db) + .definition() + .cmp(&other.binding_context(db).definition()) + }) + } } fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -9998,6 +10152,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> { .iter() .map(|ty| ty.normalized_impl(db, visitor)) .collect::>(), + constraints.recursively_defined(db), )) } } @@ -10021,6 +10176,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> { .iter() .map(|ty| ty.materialize(db, materialization_kind, visitor)) .collect::>(), + RecursivelyDefined::No, )) } } @@ -10180,6 +10336,27 @@ impl<'db> UnionTypeInstance<'db> { Some(Self::new(db, value_expr_types, union_type)) } + + fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self._value_expr_types(db), other._value_expr_types(db)) { + (Some(left_tys), Some(right_tys)) => { + let len_count = left_tys.len().cmp(&right_tys.len()); + if len_count != std::cmp::Ordering::Equal { + return len_count; + } + for (left, right) in left_tys.iter().zip(right_tys.iter()) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + std::cmp::Ordering::Equal + } + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + } } /// A salsa-interned `Type` @@ -11585,6 +11762,14 @@ impl<'db> BoundMethodType<'db> { ) }) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.function(db) + .structural_ordering(db, other.function(db)) + .then_with(|| { + structural_type_ordering(db, &self.self_instance(db), &other.self_instance(db)) + }) + } } /// This type represents the set of all callable objects with a certain, possibly overloaded, @@ -11773,6 +11958,15 @@ impl<'db> CallableType<'db> { .is_equivalent_to_impl(db, other.signatures(db), inferable, visitor) }) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.is_function_like(db) + .cmp(&other.is_function_like(db)) + .then_with(|| { + self.signatures(db) + .structural_ordering(db, other.signatures(db)) + }) + } } /// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually, @@ -12387,6 +12581,48 @@ impl<'db> KnownBoundMethodType<'db> { } } } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self, other) { + ( + KnownBoundMethodType::FunctionTypeDunderGet(self_function), + KnownBoundMethodType::FunctionTypeDunderGet(other_function), + ) => self_function.structural_ordering(db, other_function), + + ( + KnownBoundMethodType::FunctionTypeDunderCall(self_function), + KnownBoundMethodType::FunctionTypeDunderCall(other_function), + ) => self_function.structural_ordering(db, other_function), + + ( + KnownBoundMethodType::PropertyDunderGet(self_property), + KnownBoundMethodType::PropertyDunderGet(other_property), + ) => self_property.structural_ordering(db, other_property), + + ( + KnownBoundMethodType::PropertyDunderSet(self_property), + KnownBoundMethodType::PropertyDunderSet(other_property), + ) => self_property.structural_ordering(db, other_property), + + (left, right) => { + let index = |known| match known { + KnownBoundMethodType::FunctionTypeDunderGet(_) => 0, + KnownBoundMethodType::FunctionTypeDunderCall(_) => 1, + KnownBoundMethodType::PropertyDunderGet(_) => 2, + KnownBoundMethodType::PropertyDunderSet(_) => 3, + KnownBoundMethodType::StrStartswith(_) => 4, + KnownBoundMethodType::ConstraintSetRange => 5, + KnownBoundMethodType::ConstraintSetAlways => 6, + KnownBoundMethodType::ConstraintSetNever => 7, + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => 8, + KnownBoundMethodType::ConstraintSetSatisfies(_) => 9, + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => 10, + KnownBoundMethodType::GenericContextSpecializeConstrained(_) => 11, + }; + index(left).cmp(&index(right)) + } + } + } } /// Represents a specific instance of `types.WrapperDescriptorType` @@ -12642,6 +12878,14 @@ impl<'db> ModuleLiteralType<'db> { place_and_qualifiers } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: ModuleLiteralType<'db>, + ) -> std::cmp::Ordering { + self.module(db).structural_ordering(db, other.module(db)) + } } /// # Ordering @@ -12755,6 +12999,17 @@ impl<'db> PEP695TypeAliasType<'db> { fn normalized_impl(self, _db: &'db dyn Db, _visitor: &NormalizedVisitor<'db>) -> Self { self } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: PEP695TypeAliasType<'db>, + ) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + self.rhs_scope(db) + .structural_ordering(db, other.rhs_scope(db)) + }) + } } fn generic_context_cycle_initial<'db>( @@ -12828,6 +13083,21 @@ impl<'db> ManualPEP695TypeAliasType<'db> { .recursive_type_normalized_impl(db, div, true)?, )) } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: ManualPEP695TypeAliasType<'db>, + ) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + match (self.definition(db), other.definition(db)) { + (Some(left), Some(right)) => left.structural_ordering(db, right), + (None, None) => std::cmp::Ordering::Equal, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + } + }) + } } #[derive( @@ -12948,6 +13218,21 @@ impl<'db> TypeAliasType<'db> { TypeAliasType::ManualPEP695(_) => self, } } + + fn structural_ordering(self, db: &'db dyn Db, other: TypeAliasType<'db>) -> std::cmp::Ordering { + match (self, other) { + (TypeAliasType::PEP695(left), TypeAliasType::PEP695(right)) => { + left.structural_ordering(db, right) + } + (TypeAliasType::ManualPEP695(left), TypeAliasType::ManualPEP695(right)) => { + left.structural_ordering(db, right) + } + (TypeAliasType::PEP695(_), TypeAliasType::ManualPEP695(_)) => std::cmp::Ordering::Less, + (TypeAliasType::ManualPEP695(_), TypeAliasType::PEP695(_)) => { + std::cmp::Ordering::Greater + } + } + } } /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. @@ -12962,6 +13247,9 @@ pub struct UnionType<'db> { /// The union type includes values in any of these types. #[returns(deref)] pub elements: Box<[Type<'db>]>, + /// Whether the value pointed to by this type is recursively defined. + /// If `Yes`, union literal widening is performed early. + recursively_defined: RecursivelyDefined, } pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -13046,7 +13334,14 @@ impl<'db> UnionType<'db> { db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, ) -> Type<'db> { - Self::from_elements(db, self.elements(db).iter().map(transform_fn)) + self.elements(db) + .iter() + .map(transform_fn) + .fold(UnionBuilder::new(db), |builder, element| { + builder.add(element) + }) + .recursively_defined(self.recursively_defined(db)) + .build() } /// A fallible version of [`UnionType::map`]. @@ -13061,7 +13356,12 @@ impl<'db> UnionType<'db> { db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Option>, ) -> Option> { - Self::try_from_elements(db, self.elements(db).iter().map(transform_fn)) + let mut builder = UnionBuilder::new(db); + for element in self.elements(db).iter().map(transform_fn) { + builder = builder.add(element?); + } + builder = builder.recursively_defined(self.recursively_defined(db)); + Some(builder.build()) } pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> { @@ -13073,7 +13373,14 @@ impl<'db> UnionType<'db> { db: &'db dyn Db, mut f: impl FnMut(&Type<'db>) -> bool, ) -> Type<'db> { - Self::from_elements(db, self.elements(db).iter().filter(|ty| f(ty))) + self.elements(db) + .iter() + .filter(|ty| f(ty)) + .fold(UnionBuilder::new(db), |builder, element| { + builder.add(*element) + }) + .recursively_defined(self.recursively_defined(db)) + .build() } pub(crate) fn map_with_boundness( @@ -13108,7 +13415,9 @@ impl<'db> UnionType<'db> { Place::Undefined } else { Place::Defined( - builder.build(), + builder + .recursively_defined(self.recursively_defined(db)) + .build(), origin, if possibly_unbound { Definedness::PossiblyUndefined @@ -13156,7 +13465,9 @@ impl<'db> UnionType<'db> { Place::Undefined } else { Place::Defined( - builder.build(), + builder + .recursively_defined(self.recursively_defined(db)) + .build(), origin, if possibly_unbound { Definedness::PossiblyUndefined @@ -13191,6 +13502,7 @@ impl<'db> UnionType<'db> { .unpack_aliases(true), UnionBuilder::add, ) + .recursively_defined(self.recursively_defined(db)) .build() } @@ -13203,7 +13515,8 @@ impl<'db> UnionType<'db> { let mut builder = UnionBuilder::new(db) .order_elements(false) .unpack_aliases(false) - .cycle_recovery(true); + .cycle_recovery(true) + .recursively_defined(self.recursively_defined(db)); let mut empty = true; for ty in self.elements(db) { if nested { @@ -13218,6 +13531,7 @@ impl<'db> UnionType<'db> { // `Divergent` in a union type does not mean true divergence, so we skip it if not nested. // e.g. T | Divergent == T | (T | (T | (T | ...))) == T if ty == &div { + builder = builder.recursively_defined(RecursivelyDefined::Yes); continue; } builder = builder.add( @@ -13565,6 +13879,14 @@ impl<'db> StringLiteralType<'db> { pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { self.value(db).chars().count() } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: StringLiteralType<'db>, + ) -> std::cmp::Ordering { + self.value(db).cmp(other.value(db)) + } } /// # Ordering @@ -13584,6 +13906,14 @@ impl<'db> BytesLiteralType<'db> { pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { self.value(db).len() } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: BytesLiteralType<'db>, + ) -> std::cmp::Ordering { + self.value(db).cmp(other.value(db)) + } } /// A singleton type corresponding to a specific enum member. @@ -13612,6 +13942,17 @@ impl<'db> EnumLiteralType<'db> { pub(crate) fn enum_class_instance(self, db: &'db dyn Db) -> Type<'db> { self.enum_class(db).to_non_generic_instance(db) } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: EnumLiteralType<'db>, + ) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + self.enum_class(db) + .structural_ordering(db, other.enum_class(db)) + }) + } } #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -13672,6 +14013,23 @@ impl<'db> TypeIsType<'db> { pub(crate) fn is_bound(self, db: &'db dyn Db) -> bool { self.place_info(db).is_some() } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: TypeIsType<'db>, + ) -> std::cmp::Ordering { + structural_type_ordering(db, &self.return_type(db), &other.return_type(db)).then_with( + || match (self.place_info(db), other.place_info(db)) { + (Some((left_scope, left_place)), Some((right_scope, right_place))) => left_scope + .structural_ordering(db, right_scope) + .then_with(|| left_place.cmp(&right_place)), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + }, + ) + } } impl<'db> VarianceInferable<'db> for TypeIsType<'db> { diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 0618682837f1a3..2240a2aa2885bb 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -41,7 +41,7 @@ use crate::types::enums::{enum_member_literals, enum_metadata}; use crate::types::type_ordering::union_or_intersection_elements_ordering; use crate::types::{ BytesLiteralType, IntersectionType, KnownClass, StringLiteralType, Type, - TypeVarBoundOrConstraints, UnionType, + TypeVarBoundOrConstraints, UnionType, structural_type_ordering, }; use crate::{Db, FxOrderSet}; use rustc_hash::FxHashSet; @@ -202,6 +202,25 @@ enum ReduceResult<'db> { Type(Type<'db>), } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum RecursivelyDefined { + Yes, + No, +} + +impl RecursivelyDefined { + const fn is_yes(self) -> bool { + matches!(self, RecursivelyDefined::Yes) + } + + const fn or(self, other: RecursivelyDefined) -> RecursivelyDefined { + match (self, other) { + (RecursivelyDefined::Yes, _) | (_, RecursivelyDefined::Yes) => RecursivelyDefined::Yes, + _ => RecursivelyDefined::No, + } + } +} + // TODO increase this once we extend `UnionElement` throughout all union/intersection // representations, so that we can make large unions of literals fast in all operations. // @@ -217,6 +236,7 @@ pub(crate) struct UnionBuilder<'db> { // This is enabled when joining types in a `cycle_recovery` function. // Since a cycle cannot be created within a `cycle_recovery` function, execution of `is_redundant_with` is skipped. cycle_recovery: bool, + recursively_defined: RecursivelyDefined, } impl<'db> UnionBuilder<'db> { @@ -227,6 +247,7 @@ impl<'db> UnionBuilder<'db> { unpack_aliases: true, order_elements: false, cycle_recovery: false, + recursively_defined: RecursivelyDefined::No, } } @@ -248,6 +269,11 @@ impl<'db> UnionBuilder<'db> { self } + pub(crate) fn recursively_defined(mut self, val: RecursivelyDefined) -> Self { + self.recursively_defined = val; + self + } + pub(crate) fn is_empty(&self) -> bool { self.elements.is_empty() } @@ -277,6 +303,9 @@ impl<'db> UnionBuilder<'db> { for element in new_elements { self.add_in_place_impl(*element, seen_aliases); } + self.recursively_defined = self + .recursively_defined + .or(union.recursively_defined(self.db)); } // Adding `Never` to a union is a no-op. Type::Never => {} @@ -560,7 +589,13 @@ impl<'db> UnionBuilder<'db> { self.try_build().unwrap_or(Type::Never) } - pub(crate) fn try_build(self) -> Option> { + pub(crate) fn try_build(mut self) -> Option> { + // If the type is defined recursively, the union type is sorted and normalized. + // This is because the execution order of the queries is not deterministic and may result in a different order of elements. + // The order of the union type does not affect the type check result, but unstable output is undesirable. + if self.recursively_defined.is_yes() { + self.order_elements = true; + } let mut types = vec![]; for element in self.elements { match element { @@ -577,7 +612,13 @@ impl<'db> UnionBuilder<'db> { } } if self.order_elements { - types.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(self.db, l, r)); + types.sort_unstable_by(|l, r| { + if self.recursively_defined.is_yes() { + structural_type_ordering(self.db, l, r) + } else { + union_or_intersection_elements_ordering(self.db, l, r) + } + }); } match types.len() { 0 => None, @@ -585,6 +626,7 @@ impl<'db> UnionBuilder<'db> { _ => Some(Type::Union(UnionType::new( self.db, types.into_boxed_slice(), + self.recursively_defined, ))), } } @@ -696,6 +738,7 @@ impl<'db> IntersectionBuilder<'db> { enum_member_literals(db, instance.class_literal(db), None) .expect("Calling `enum_member_literals` on an enum class") .collect::>(), + RecursivelyDefined::No, )), seen_aliases, ) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 1285fb00937b41..1dd3f7995f8c76 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -332,6 +332,19 @@ impl<'db> GenericAlias<'db> { pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool { self.origin(db).is_typed_dict(db) } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: GenericAlias<'db>, + ) -> std::cmp::Ordering { + self.origin(db) + .structural_ordering(db, other.origin(db)) + .then_with(|| { + self.specialization(db) + .structural_ordering(db, other.specialization(db)) + }) + } } impl<'db> From> for Type<'db> { @@ -1278,6 +1291,19 @@ impl<'db> ClassType<'db> { pub(super) fn header_span(self, db: &'db dyn Db) -> Span { self.class_literal(db).0.header_span(db) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self, other) { + (ClassType::NonGeneric(left), ClassType::NonGeneric(right)) => { + left.structural_ordering(db, right) + } + (ClassType::Generic(left), ClassType::Generic(right)) => { + left.structural_ordering(db, right) + } + (ClassType::NonGeneric(_), ClassType::Generic(_)) => std::cmp::Ordering::Less, + (ClassType::Generic(_), ClassType::NonGeneric(_)) => std::cmp::Ordering::Greater, + } + } } fn into_callable_cycle_initial<'db>( @@ -3769,6 +3795,20 @@ impl<'db> ClassLiteral<'db> { pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { QualifiedClassName { db, class: self } } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: ClassLiteral<'db>, + ) -> std::cmp::Ordering { + self.name(db) + .cmp(other.name(db)) + .then_with(|| self.known(db).cmp(&other.known(db))) + .then_with(|| { + self.body_scope(db) + .structural_ordering(db, other.body_scope(db)) + }) + } } impl<'db> From> for Type<'db> { @@ -4050,7 +4090,7 @@ pub(super) enum DisjointBaseKind { /// Feel free to expand this enum if you ever find yourself using the same class in multiple /// places. /// Note: good candidates are any classes in `[crate::module_resolver::module::KnownModule]` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] #[cfg_attr(test, derive(strum_macros::EnumIter))] pub enum KnownClass { // To figure out where an stdlib symbol is defined, you can go into `crates/ty_vendored` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 189520fa52e1cb..15069d196bc6b0 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1129,6 +1129,21 @@ impl<'db> FunctionType<'db> { updated_last_definition_signature, )) } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: FunctionType<'db>, + ) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + match (self.updated_signature(db), other.updated_signature(db)) { + (Some(left_sig), Some(right_sig)) => left_sig.structural_ordering(db, right_sig), + (None, None) => std::cmp::Ordering::Equal, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + } + }) + } } /// Evaluate an `isinstance` call. Return `Truthiness::AlwaysTrue` if we can definitely infer that diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index de357dfbce2c58..d3c72dc1733b05 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -21,7 +21,8 @@ use crate::types::{ FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, - TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_bound_type_var_type, + TypeVarKind, TypeVarVariance, UnionType, declaration_type, structural_type_ordering, + walk_bound_type_var_type, }; use crate::{Db, FxOrderMap, FxOrderSet}; @@ -602,6 +603,26 @@ impl<'db> GenericContext<'db> { ) -> usize { ruff_memory_usage::order_map_heap_size(variables) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let left = self.variables_inner(db); + let right = other.variables_inner(db); + let variables_count = left.len().cmp(&right.len()); + if variables_count != std::cmp::Ordering::Equal { + return variables_count; + } + for (left_key, left_value) in left { + if let Some(right_value) = right.get(left_key) { + let ord = left_value.structural_ordering(db, *right_value); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } else { + return std::cmp::Ordering::Greater; + } + } + std::cmp::Ordering::Equal + } } fn inferable_typevars_cycle_initial<'db>( @@ -1282,6 +1303,20 @@ impl<'db> Specialization<'db> { // A tuple's specialization will include all of its element types, so we don't need to also // look in `self.tuple`. } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let types_count = self.types(db).len().cmp(&other.types(db).len()); + if types_count != std::cmp::Ordering::Equal { + return types_count; + } + for (left, right) in self.types(db).iter().zip(other.types(db)) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + std::cmp::Ordering::Equal + } } /// A mapping between type variables and types. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 4c97ad8c606284..b4c9b8e8e0dff9 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -49,6 +49,7 @@ use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, }; use crate::subscript::{PyIndex, PySlice}; +use crate::types::builder::RecursivelyDefined; use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex}; use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator}; @@ -3270,6 +3271,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { elts.iter() .map(|expr| self.infer_type_expression(expr)) .collect::>(), + RecursivelyDefined::No, )); self.store_expression_type(expr, ty); } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index fb53f10ef4d509..802f38944c98ef 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -547,6 +547,25 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::Object => {} } } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self.0, other.0) { + (NominalInstanceInner::Object, NominalInstanceInner::Object) => { + std::cmp::Ordering::Equal + } + (NominalInstanceInner::Object, _) => std::cmp::Ordering::Less, + (_, NominalInstanceInner::Object) => std::cmp::Ordering::Greater, + ( + NominalInstanceInner::ExactTuple(tuple1), + NominalInstanceInner::ExactTuple(tuple2), + ) => tuple1.structural_ordering(db, tuple2), + (NominalInstanceInner::ExactTuple(_), _) => std::cmp::Ordering::Less, + (_, NominalInstanceInner::ExactTuple(_)) => std::cmp::Ordering::Greater, + (NominalInstanceInner::NonTuple(class1), NominalInstanceInner::NonTuple(class2)) => { + class1.structural_ordering(db, class2) + } + } + } } impl<'db> From> for Type<'db> { @@ -835,6 +854,19 @@ impl<'db> ProtocolInstanceType<'db> { pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { self.inner.interface(db) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self.inner, other.inner) { + (Protocol::FromClass(left), Protocol::FromClass(right)) => { + left.structural_ordering(db, *right) + } + (Protocol::Synthesized(left), Protocol::Synthesized(right)) => { + left.structural_ordering(db, right) + } + (Protocol::FromClass(_), Protocol::Synthesized(_)) => std::cmp::Ordering::Less, + (Protocol::Synthesized(_), Protocol::FromClass(_)) => std::cmp::Ordering::Greater, + } + } } impl<'db> VarianceInferable<'db> for ProtocolInstanceType<'db> { @@ -963,6 +995,14 @@ mod synthesized_protocol { self.0.recursive_type_normalized_impl(db, div, nested)?, )) } + + pub(in crate::types) fn structural_ordering( + self, + db: &'db dyn Db, + other: Self, + ) -> std::cmp::Ordering { + self.0.structural_ordering(db, other.0) + } } impl<'db> VarianceInferable<'db> for SynthesizedProtocolType<'db> { diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index 84a6e18f50aca5..48bb1df20f75a5 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -194,6 +194,13 @@ impl<'db> NewType<'db> { self.try_map_base_class_type(db, |class_type| Some(f(class_type))) .unwrap() } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + self.definition(db) + .structural_ordering(db, other.definition(db)) + }) + } } pub(crate) fn walk_newtype_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 862349ea40010d..286c6e1f56dafb 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use ruff_python_ast::name::Name; use rustc_hash::FxHashMap; -use crate::types::TypeContext; +use crate::types::{TypeContext, structural_type_ordering}; use crate::{ Db, FxOrderSet, place::{Definedness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, @@ -461,6 +461,22 @@ impl<'db> ProtocolInterface<'db> { interface: self, } } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let members_count = self.members(db).len().cmp(&other.members(db).len()); + if members_count != std::cmp::Ordering::Equal { + return members_count; + } + + for (left, right) in self.members(db).zip(other.members(db)) { + let ord = left.structural_ordering(db, &right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + std::cmp::Ordering::Equal + } } impl<'db> VarianceInferable<'db> for ProtocolInterface<'db> { @@ -813,6 +829,42 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { } } } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Self) -> std::cmp::Ordering { + let name_ordering = self.name.cmp(other.name); + if name_ordering != std::cmp::Ordering::Equal { + return name_ordering; + } + let qualifiers_ordering = self.qualifiers.cmp(&other.qualifiers); + if qualifiers_ordering != std::cmp::Ordering::Equal { + return qualifiers_ordering; + } + + match (&self.kind, &other.kind) { + (ProtocolMemberKind::Method(left), ProtocolMemberKind::Method(right)) => { + left.structural_ordering(db, *right) + } + (ProtocolMemberKind::Property(left), ProtocolMemberKind::Property(right)) => { + left.structural_ordering(db, *right) + } + (ProtocolMemberKind::Other(left), ProtocolMemberKind::Other(right)) => { + structural_type_ordering(db, left, right) + } + (left, right) => { + let left_index = match left { + ProtocolMemberKind::Method(_) => 0, + ProtocolMemberKind::Property(_) => 1, + ProtocolMemberKind::Other(_) => 2, + }; + let right_index = match right { + ProtocolMemberKind::Method(_) => 0, + ProtocolMemberKind::Property(_) => 1, + ProtocolMemberKind::Other(_) => 2, + }; + left_index.cmp(&right_index) + } + } + } } /// Returns `true` if a declaration or binding to a given name in a protocol class body diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 738fcfb971d75d..b4cb81bda6f171 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -31,7 +31,8 @@ use crate::types::infer::nearest_enclosing_class; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, - NormalizedVisitor, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, + NormalizedVisitor, TypeContext, TypeMapping, TypeRelation, VarianceInferable, + structural_type_ordering, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -367,6 +368,23 @@ impl<'db> CallableSignature<'db> { } } } + + pub(super) fn structural_ordering( + &self, + db: &'db dyn Db, + other: &CallableSignature<'db>, + ) -> std::cmp::Ordering { + if self.overloads.len() != other.overloads.len() { + return self.overloads.len().cmp(&other.overloads.len()); + } + for (left_sig, right_sig) in self.overloads.iter().zip(&other.overloads) { + let ord = left_sig.structural_ordering(db, right_sig); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + std::cmp::Ordering::Equal + } } impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { @@ -1294,6 +1312,26 @@ impl<'db> Signature<'db> { pub(crate) fn with_definition(self, definition: Option>) -> Self { Self { definition, ..self } } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Signature<'db>) -> std::cmp::Ordering { + let parameters_count = self.parameters.len().cmp(&other.parameters.len()); + if parameters_count != std::cmp::Ordering::Equal { + return parameters_count; + } + for (left, right) in self.parameters.iter().zip(&other.parameters) { + let ord = left.structural_ordering(db, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + match (self.return_ty.as_ref(), other.return_ty.as_ref()) { + (Some(left_ty), Some(right_ty)) => structural_type_ordering(db, left_ty, right_ty), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + } } impl<'db> VarianceInferable<'db> for &Signature<'db> { @@ -2074,6 +2112,17 @@ impl<'db> Parameter<'db> { ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => None, } } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Parameter<'db>) -> std::cmp::Ordering { + self.kind.cmp(&other.kind).then_with(|| { + match (self.annotated_type.as_ref(), other.annotated_type.as_ref()) { + (Some(left_ty), Some(right_ty)) => structural_type_ordering(db, left_ty, right_ty), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + }) + } } #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] @@ -2115,6 +2164,46 @@ pub(crate) enum ParameterKind<'db> { }, } +impl Ord for ParameterKind<'_> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + ( + Self::PositionalOnly { name: l_name, .. }, + Self::PositionalOnly { name: r_name, .. }, + ) => l_name.cmp(r_name), + ( + Self::PositionalOrKeyword { name: l_name, .. }, + Self::PositionalOrKeyword { name: r_name, .. }, + ) => l_name.cmp(r_name), + (Self::Variadic { name: l_name }, Self::Variadic { name: r_name }) => { + l_name.cmp(r_name) + } + (Self::KeywordOnly { name: l_name, .. }, Self::KeywordOnly { name: r_name, .. }) => { + l_name.cmp(r_name) + } + (Self::KeywordVariadic { name: l_name }, Self::KeywordVariadic { name: r_name }) => { + l_name.cmp(r_name) + } + (left, right) => { + let index = |param: &_| match param { + Self::PositionalOnly { .. } => 0, + Self::PositionalOrKeyword { .. } => 1, + Self::Variadic { .. } => 2, + Self::KeywordOnly { .. } => 3, + Self::KeywordVariadic { .. } => 4, + }; + index(left).cmp(&index(right)) + } + } + } +} + +impl PartialOrd for ParameterKind<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl<'db> ParameterKind<'db> { fn apply_type_mapping_impl<'a>( &self, diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 0e3deed233ebaf..ac3ff574dfb318 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -416,7 +416,7 @@ impl<'db> SubclassOfInner<'db> { ) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let constraints = constraints + let constraints_types = constraints .elements(db) .iter() .map(|constraint| { @@ -425,7 +425,11 @@ impl<'db> SubclassOfInner<'db> { }) .collect::>(); - TypeVarBoundOrConstraints::Constraints(UnionType::new(db, constraints)) + TypeVarBoundOrConstraints::Constraints(UnionType::new( + db, + constraints_types, + constraints.recursively_defined(db), + )) } }) }); diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index d2b96f284989e2..577abc3f73a730 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -23,13 +23,14 @@ use itertools::{Either, EitherOrBoth, Itertools}; use crate::semantic_index::definition::Definition; use crate::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; +use crate::types::builder::RecursivelyDefined; use crate::types::class::{ClassType, KnownClass}; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::generics::InferableTypeVars; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping, TypeRelation, - UnionBuilder, UnionType, + UnionBuilder, UnionType, structural_type_ordering, }; use crate::types::{Truthiness, TypeContext}; use crate::{Db, FxOrderSet, Program}; @@ -301,6 +302,10 @@ impl<'db> TupleType<'db> { pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { self.tuple(db).is_single_valued(db) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.tuple(db).structural_ordering(db, other.tuple(db)) + } } fn to_class_type_cycle_initial<'db>( @@ -582,6 +587,22 @@ impl<'db> FixedLengthTuple> { fn is_single_valued(&self, db: &'db dyn Db) -> bool { self.0.iter().all(|ty| ty.is_single_valued(db)) } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Self) -> std::cmp::Ordering { + let len_count = self.0.len().cmp(&other.0.len()); + if len_count != std::cmp::Ordering::Equal { + return len_count; + } + + for (left, right) in self.0.iter().zip(&other.0) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + std::cmp::Ordering::Equal + } } impl<'db> PyIndex<'db> for &FixedLengthTuple> { @@ -1095,6 +1116,32 @@ impl<'db> VariableLengthTuple> { }) }) } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Self) -> std::cmp::Ordering { + let prefix_count = self.prefix.len().cmp(&other.prefix.len()); + if prefix_count != std::cmp::Ordering::Equal { + return prefix_count; + } + let suffix_count = self.suffix.len().cmp(&other.suffix.len()); + if suffix_count != std::cmp::Ordering::Equal { + return suffix_count; + } + + for (left, right) in self.prefix.iter().zip(&other.prefix) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + for (left, right) in self.suffix.iter().zip(&other.suffix) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + structural_type_ordering(db, &self.variable, &other.variable) + } } impl<'db> PyIndex<'db> for &VariableLengthTuple> { @@ -1458,7 +1505,7 @@ impl<'db> Tuple> { // those techniques ensure that union elements are deduplicated and unions are eagerly simplified // into other types where necessary. Here, however, we know that there are no duplicates // in this union, so it's probably more efficient to use `UnionType::new()` directly. - Type::Union(UnionType::new(db, elements)) + Type::Union(UnionType::new(db, elements, RecursivelyDefined::No)) }; TupleSpec::heterogeneous([ @@ -1469,6 +1516,19 @@ impl<'db> Tuple> { int_instance_ty, ]) } + + pub(super) fn structural_ordering(&self, db: &'db dyn Db, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (Tuple::Fixed(self_tuple), Tuple::Fixed(other_tuple)) => { + self_tuple.structural_ordering(db, other_tuple) + } + (Tuple::Variable(self_tuple), Tuple::Variable(other_tuple)) => { + self_tuple.structural_ordering(db, other_tuple) + } + (Tuple::Fixed(_), Tuple::Variable(_)) => std::cmp::Ordering::Less, + (Tuple::Variable(_), Tuple::Fixed(_)) => std::cmp::Ordering::Greater, + } + } } impl From> for Tuple { diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index d2c9a7120820d6..0b90fe7644ba18 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -8,6 +8,262 @@ use super::{ DynamicType, TodoType, Type, TypeIsType, class_base::ClassBase, subclass_of::SubclassOfInner, }; +pub(super) fn structural_type_ordering<'db>( + db: &'db dyn Db, + left: &Type<'db>, + right: &Type<'db>, +) -> Ordering { + match (left, right) { + (Type::Never, _) => Ordering::Less, + (_, Type::Never) => Ordering::Greater, + + (Type::LiteralString, _) => Ordering::Less, + (_, Type::LiteralString) => Ordering::Greater, + + (Type::BooleanLiteral(left), Type::BooleanLiteral(right)) => left.cmp(right), + (Type::BooleanLiteral(_), _) => Ordering::Less, + (_, Type::BooleanLiteral(_)) => Ordering::Greater, + + (Type::IntLiteral(left), Type::IntLiteral(right)) => left.cmp(right), + (Type::IntLiteral(_), _) => Ordering::Less, + (_, Type::IntLiteral(_)) => Ordering::Greater, + + (Type::StringLiteral(left), Type::StringLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::StringLiteral(_), _) => Ordering::Less, + (_, Type::StringLiteral(_)) => Ordering::Greater, + + (Type::BytesLiteral(left), Type::BytesLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::BytesLiteral(_), _) => Ordering::Less, + (_, Type::BytesLiteral(_)) => Ordering::Greater, + + (Type::EnumLiteral(left), Type::EnumLiteral(right)) => left.structural_ordering(db, *right), + (Type::EnumLiteral(_), _) => Ordering::Less, + (_, Type::EnumLiteral(_)) => Ordering::Greater, + + (Type::FunctionLiteral(left), Type::FunctionLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::FunctionLiteral(_), _) => Ordering::Less, + (_, Type::FunctionLiteral(_)) => Ordering::Greater, + + (Type::BoundMethod(left), Type::BoundMethod(right)) => left.structural_ordering(db, *right), + (Type::BoundMethod(_), _) => Ordering::Less, + (_, Type::BoundMethod(_)) => Ordering::Greater, + + (Type::KnownBoundMethod(left), Type::KnownBoundMethod(right)) => { + left.structural_ordering(db, *right) + } + (Type::KnownBoundMethod(_), _) => Ordering::Less, + (_, Type::KnownBoundMethod(_)) => Ordering::Greater, + + (Type::WrapperDescriptor(left), Type::WrapperDescriptor(right)) => left.cmp(right), + (Type::WrapperDescriptor(_), _) => Ordering::Less, + (_, Type::WrapperDescriptor(_)) => Ordering::Greater, + + (Type::DataclassDecorator(left), Type::DataclassDecorator(right)) => { + left.structural_ordering(db, *right) + } + (Type::DataclassDecorator(_), _) => Ordering::Less, + (_, Type::DataclassDecorator(_)) => Ordering::Greater, + + (Type::DataclassTransformer(left), Type::DataclassTransformer(right)) => left.cmp(right), + (Type::DataclassTransformer(_), _) => Ordering::Less, + (_, Type::DataclassTransformer(_)) => Ordering::Greater, + + (Type::Callable(left), Type::Callable(right)) => left.structural_ordering(db, *right), + (Type::Callable(_), _) => Ordering::Less, + (_, Type::Callable(_)) => Ordering::Greater, + + (Type::ModuleLiteral(left), Type::ModuleLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::ModuleLiteral(_), _) => Ordering::Less, + (_, Type::ModuleLiteral(_)) => Ordering::Greater, + + (Type::ClassLiteral(left), Type::ClassLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::ClassLiteral(_), _) => Ordering::Less, + (_, Type::ClassLiteral(_)) => Ordering::Greater, + + (Type::GenericAlias(left), Type::GenericAlias(right)) => { + left.structural_ordering(db, *right) + } + (Type::GenericAlias(_), _) => Ordering::Less, + (_, Type::GenericAlias(_)) => Ordering::Greater, + + (Type::SubclassOf(left), Type::SubclassOf(right)) => { + match (left.subclass_of(), right.subclass_of()) { + (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => { + left.structural_ordering(db, right) + } + (SubclassOfInner::Class(_), _) => Ordering::Less, + (_, SubclassOfInner::Class(_)) => Ordering::Greater, + (SubclassOfInner::Dynamic(left), SubclassOfInner::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + (SubclassOfInner::TypeVar(left), SubclassOfInner::TypeVar(right)) => { + left.structural_ordering(db, right) + } + (SubclassOfInner::TypeVar(_), _) => Ordering::Less, + (_, SubclassOfInner::TypeVar(_)) => Ordering::Greater, + } + } + + (Type::SubclassOf(_), _) => Ordering::Less, + (_, Type::SubclassOf(_)) => Ordering::Greater, + + (Type::TypeIs(left), Type::TypeIs(right)) => left.structural_ordering(db, *right), + (Type::TypeIs(_), _) => Ordering::Less, + (_, Type::TypeIs(_)) => Ordering::Greater, + + (Type::NominalInstance(left), Type::NominalInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::NominalInstance(_), _) => Ordering::Less, + (_, Type::NominalInstance(_)) => Ordering::Greater, + + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::ProtocolInstance(_), _) => Ordering::Less, + (_, Type::ProtocolInstance(_)) => Ordering::Greater, + + (Type::TypeVar(left), Type::TypeVar(right)) => left.structural_ordering(db, *right), + (Type::TypeVar(_), _) => Ordering::Less, + (_, Type::TypeVar(_)) => Ordering::Greater, + + (Type::AlwaysTruthy, _) => Ordering::Less, + (_, Type::AlwaysTruthy) => Ordering::Greater, + + (Type::AlwaysFalsy, _) => Ordering::Less, + (_, Type::AlwaysFalsy) => Ordering::Greater, + + (Type::BoundSuper(left), Type::BoundSuper(right)) => { + (match (left.pivot_class(db), right.pivot_class(db)) { + (ClassBase::Class(left), ClassBase::Class(right)) => { + left.structural_ordering(db, right) + } + (ClassBase::Class(_), _) => Ordering::Less, + (_, ClassBase::Class(_)) => Ordering::Greater, + + (ClassBase::Protocol, _) => Ordering::Less, + (_, ClassBase::Protocol) => Ordering::Greater, + + (ClassBase::Generic, _) => Ordering::Less, + (_, ClassBase::Generic) => Ordering::Greater, + + (ClassBase::TypedDict, _) => Ordering::Less, + (_, ClassBase::TypedDict) => Ordering::Greater, + + (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + }) + .then_with(|| match (left.owner(db), right.owner(db)) { + (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => { + left.structural_ordering(db, right) + } + (SuperOwnerKind::Class(_), _) => Ordering::Less, + (_, SuperOwnerKind::Class(_)) => Ordering::Greater, + (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { + left.structural_ordering(db, right) + } + (SuperOwnerKind::Instance(_), _) => Ordering::Less, + (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, + (SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + }) + } + (Type::BoundSuper(_), _) => Ordering::Less, + (_, Type::BoundSuper(_)) => Ordering::Greater, + + (Type::SpecialForm(left), Type::SpecialForm(right)) => left.cmp(right), + (Type::SpecialForm(_), _) => Ordering::Less, + (_, Type::SpecialForm(_)) => Ordering::Greater, + + (Type::KnownInstance(left), Type::KnownInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::KnownInstance(_), _) => Ordering::Less, + (_, Type::KnownInstance(_)) => Ordering::Greater, + + (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::PropertyInstance(_), _) => Ordering::Less, + (_, Type::PropertyInstance(_)) => Ordering::Greater, + + (Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right), + (Type::Dynamic(_), _) => Ordering::Less, + (_, Type::Dynamic(_)) => Ordering::Greater, + + (Type::TypeAlias(left), Type::TypeAlias(right)) => left.structural_ordering(db, *right), + (Type::TypeAlias(_), _) => Ordering::Less, + (_, Type::TypeAlias(_)) => Ordering::Greater, + + (Type::TypedDict(left), Type::TypedDict(right)) => left + .defining_class() + .structural_ordering(db, right.defining_class()), + (Type::TypedDict(_), _) => Ordering::Less, + (_, Type::TypedDict(_)) => Ordering::Greater, + + (Type::NewTypeInstance(left), Type::NewTypeInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::NewTypeInstance(_), _) => Ordering::Less, + (_, Type::NewTypeInstance(_)) => Ordering::Greater, + + (Type::Union(left), Type::Union(right)) => { + if left.elements(db).len() != right.elements(db).len() { + return left.elements(db).len().cmp(&right.elements(db).len()); + } + for (left, right) in left.elements(db).iter().zip(right.elements(db)) { + let ordering = structural_type_ordering(db, left, right); + if ordering != Ordering::Equal { + return ordering; + } + } + Ordering::Equal + } + (Type::Union(_), _) => Ordering::Less, + (_, Type::Union(_)) => Ordering::Greater, + + (Type::Intersection(left), Type::Intersection(right)) => { + // Lexicographically compare the elements of the two unequal intersections. + let left_positive = left.positive(db); + let right_positive = right.positive(db); + if left_positive.len() != right_positive.len() { + return left_positive.len().cmp(&right_positive.len()); + } + let left_negative = left.negative(db); + let right_negative = right.negative(db); + if left_negative.len() != right_negative.len() { + return left_negative.len().cmp(&right_negative.len()); + } + for (left, right) in left_positive.iter().zip(right_positive) { + let ordering = structural_type_ordering(db, left, right); + if ordering != Ordering::Equal { + return ordering; + } + } + for (left, right) in left_negative.iter().zip(right_negative) { + let ordering = structural_type_ordering(db, left, right); + if ordering != Ordering::Equal { + return ordering; + } + } + + Ordering::Equal + } + } +} + /// Return an [`Ordering`] that describes the canonical order in which two types should appear /// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them /// to be compared for equivalence.