diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 07c6aa12c1d7b..b64cdbb308bd6 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -11,10 +11,10 @@ use crate::{ BoundTypeVarInstance, ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, MemberLookupPolicy, NominalInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, Type, TypeVarBoundOrConstraints, UnionBuilder, - constraints::{ConstraintSet, ConstraintSetBuilder}, + constraints::ConstraintSet, context::InferContext, diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS}, - relation::{HasRelationToVisitor, IsDisjointVisitor}, + relation::EquivalenceChecker, todo_type, typevar::{TypeVarConstraints, TypeVarInstance}, visitor, @@ -740,98 +740,74 @@ impl<'db> BoundSuperType<'db> { .recursive_type_normalized_impl(db, div, nested)?, )) } +} +impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { /// Check whether two `BoundSuperType`s are equivalent by recursing into /// their fields. /// - /// Despite the name, this is called from `Type::has_relation_to_impl`, - /// not from `Type::is_equivalent_to_impl`. `Type::has_relation_to_impl` - /// cannot simply delegate to `Type::is_equivalent_to_impl` for this - /// case, because `Type::is_equivalent_to_impl` itself delegates back to - /// `Type::has_relation_to_impl`, which would cause an infinite loop. - pub(crate) fn is_equivalent_to_impl<'c>( - self, + /// This method is necessary because [`super::relation::TypeRelationChecker::check_type_pair`] + /// should only return an always-satisfied constraint set for two + /// `Type::BoundSuper` types if the two types are exactly equivalent. But + /// `TypeRelationChecker::check_type_pair` cannot simply delegate to + /// [`EquivalenceChecker::check_type_pair`] for this case, because + /// `EquivalenceChecker::check_type_pair` itself delegates back to + /// `TypeRelationChecker::check_type_pair`, which would cause an infinite loop. + pub(super) fn check_bound_super_pair( + &self, db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + left: BoundSuperType<'db>, + right: BoundSuperType<'db>, ) -> ConstraintSet<'db, 'c> { - let mut class_equivalence = match (self.pivot_class(db), other.pivot_class(db)) { - (ClassBase::Class(left), ClassBase::Class(right)) => Type::from(left) - .when_equivalent_to_impl( - db, - Type::from(right), - constraints, - relation_visitor, - disjointness_visitor, - ), - (ClassBase::Class(_), _) => ConstraintSet::from_bool(constraints, false), + let mut class_equivalence = match (left.pivot_class(db), right.pivot_class(db)) { + (ClassBase::Class(left), ClassBase::Class(right)) => { + self.check_type_pair(db, Type::from(left), Type::from(right)) + } + + (ClassBase::Class(_), _) => self.never(), // A `Divergent` type is only equivalent to itself ( ClassBase::Dynamic(DynamicType::Divergent(l)), ClassBase::Dynamic(DynamicType::Divergent(r)), - ) => ConstraintSet::from_bool(constraints, l == r), + ) => ConstraintSet::from_bool(self.constraints, l == r), (ClassBase::Dynamic(DynamicType::Divergent(_)), _) - | (_, ClassBase::Dynamic(DynamicType::Divergent(_))) => { - ConstraintSet::from_bool(constraints, false) - } - (ClassBase::Dynamic(_), ClassBase::Dynamic(_)) => { - ConstraintSet::from_bool(constraints, true) - } - (ClassBase::Dynamic(_), _) => ConstraintSet::from_bool(constraints, false), + | (_, ClassBase::Dynamic(DynamicType::Divergent(_))) => self.never(), + (ClassBase::Dynamic(_), ClassBase::Dynamic(_)) => self.always(), + (ClassBase::Dynamic(_), _) => self.never(), - (ClassBase::Generic, ClassBase::Generic) => ConstraintSet::from_bool(constraints, true), - (ClassBase::Generic, _) => ConstraintSet::from_bool(constraints, false), + (ClassBase::Generic, ClassBase::Generic) => self.always(), + (ClassBase::Generic, _) => self.never(), - (ClassBase::Protocol, ClassBase::Protocol) => { - ConstraintSet::from_bool(constraints, true) - } - (ClassBase::Protocol, _) => ConstraintSet::from_bool(constraints, false), + (ClassBase::Protocol, ClassBase::Protocol) => self.always(), + (ClassBase::Protocol, _) => self.never(), - (ClassBase::TypedDict, ClassBase::TypedDict) => { - ConstraintSet::from_bool(constraints, true) - } - (ClassBase::TypedDict, _) => ConstraintSet::from_bool(constraints, false), + (ClassBase::TypedDict, ClassBase::TypedDict) => self.always(), + (ClassBase::TypedDict, _) => self.never(), }; if class_equivalence.is_never_satisfied(db) { - return ConstraintSet::from_bool(constraints, false); + return self.never(); } - let owner_equivalence = match (self.owner(db), other.owner(db)) { - (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => Type::from(left) - .when_equivalent_to_impl( - db, - Type::from(right), - constraints, - relation_visitor, - disjointness_visitor, - ), - (SuperOwnerKind::Class(_), _) => ConstraintSet::from_bool(constraints, false), - - (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => Type::from(left) - .when_equivalent_to_impl( - db, - Type::from(right), - constraints, - relation_visitor, - disjointness_visitor, - ), - (SuperOwnerKind::Instance(_), _) => ConstraintSet::from_bool(constraints, false), + let owner_equivalence = match (left.owner(db), right.owner(db)) { + (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => { + self.check_type_pair(db, Type::from(left), Type::from(right)) + } + (SuperOwnerKind::Class(_), _) => self.never(), + + (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { + self.check_type_pair(db, Type::from(left), Type::from(right)) + } + (SuperOwnerKind::Instance(_), _) => self.never(), // A `Divergent` type is only equivalent to itself ( SuperOwnerKind::Dynamic(DynamicType::Divergent(l)), SuperOwnerKind::Dynamic(DynamicType::Divergent(r)), - ) => ConstraintSet::from_bool(constraints, l == r), + ) => ConstraintSet::from_bool(self.constraints, l == r), (SuperOwnerKind::Dynamic(DynamicType::Divergent(_)), _) - | (_, SuperOwnerKind::Dynamic(DynamicType::Divergent(_))) => { - ConstraintSet::from_bool(constraints, false) - } - (SuperOwnerKind::Dynamic(_), SuperOwnerKind::Dynamic(_)) => { - ConstraintSet::from_bool(constraints, true) - } - (SuperOwnerKind::Dynamic(_), _) => ConstraintSet::from_bool(constraints, false), + | (_, SuperOwnerKind::Dynamic(DynamicType::Divergent(_))) => self.never(), + (SuperOwnerKind::Dynamic(_), SuperOwnerKind::Dynamic(_)) => self.always(), + (SuperOwnerKind::Dynamic(_), _) => self.never(), ( SuperOwnerKind::InstanceTypeVar(l_typevar, l_class), @@ -840,27 +816,16 @@ impl<'db> BoundSuperType<'db> { | ( SuperOwnerKind::ClassTypeVar(l_typevar, l_class), SuperOwnerKind::ClassTypeVar(r_typevar, r_class), - ) => Type::TypeVar(l_typevar) - .when_equivalent_to_impl( - db, - Type::TypeVar(r_typevar), - constraints, - relation_visitor, - disjointness_visitor, - ) - .and(db, constraints, || { - Type::from(l_class).when_equivalent_to_impl( - db, - Type::from(r_class), - constraints, - relation_visitor, - disjointness_visitor, - ) + ) => self + .check_type_pair(db, Type::TypeVar(l_typevar), Type::TypeVar(r_typevar)) + .and(db, self.constraints, || { + self.check_type_pair(db, Type::from(l_class), Type::from(r_class)) }), + (SuperOwnerKind::InstanceTypeVar(..) | SuperOwnerKind::ClassTypeVar(..), _) => { - ConstraintSet::from_bool(constraints, false) + self.never() } }; - class_equivalence.intersect(db, constraints, owner_equivalence) + class_equivalence.intersect(db, self.constraints, owner_equivalence) } } diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index 0e09e38daf29e..1fd91e64d3b0d 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -10,9 +10,8 @@ use crate::{ KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, Parameter, Parameters, Signature, SubclassOfInner, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, UnionType, - constraints::{ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension}, - generics::InferableTypeVars, - relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}, + constraints::{ConstraintSet, IteratorConstraintsExtension}, + relation::{TypeRelation, TypeRelationChecker}, signatures::CallableSignature, visitor, walk_signature, }, @@ -432,35 +431,6 @@ impl<'db> CallableType<'db> { self.signatures(db) .find_legacy_typevars_impl(db, binding_context, typevars, visitor); } - - /// Check whether this callable type has the given relation to another callable type. - /// - /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. - #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - if other.is_function_like(db) && !self.is_function_like(db) { - return ConstraintSet::from_bool(constraints, false); - } - - self.signatures(db).has_relation_to_impl( - db, - other.signatures(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } } /// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually, @@ -504,6 +474,10 @@ impl<'db> CallableTypes<'db> { self.0 } + pub(super) fn iter(&self) -> std::slice::Iter<'_, CallableType<'db>> { + self.0.iter() + } + pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { match self.0.as_slice() { [] => unreachable!("CallableTypes should not be empty"), @@ -515,28 +489,41 @@ impl<'db> CallableTypes<'db> { pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self { Self::from_elements(self.0.iter().map(|element| f(*element))) } +} - #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( - self, +impl<'a, 'db> IntoIterator for &'a CallableTypes<'db> { + type IntoIter = std::slice::Iter<'a, CallableType<'db>>; + type Item = &'a CallableType<'db>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + /// Check whether one callable type has the given relation to another callable type. + /// + /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. + pub(super) fn check_callable_pair( + &self, db: &'db dyn Db, - other: CallableType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + source: CallableType<'db>, + target: CallableType<'db>, ) -> ConstraintSet<'db, 'c> { - self.0.iter().when_all(db, constraints, |element| { - element.has_relation_to_impl( - db, - other, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + if target.is_function_like(db) && !source.is_function_like(db) { + return self.never(); + } + self.check_callable_signature_pair(db, source.signatures(db), target.signatures(db)) + } + + pub(super) fn check_callables_vs_callable( + &self, + db: &'db dyn Db, + source: &CallableTypes<'db>, + target: CallableType<'db>, + ) -> ConstraintSet<'db, 'c> { + source.iter().when_all(db, self.constraints, |element| { + self.check_callable_pair(db, *element, target) }) } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index bc74889549eff..f7279399d3f72 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -26,7 +26,9 @@ use crate::types::generics::{ }; use crate::types::known_instance::DeprecatedInstance; use crate::types::member::Member; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::relation::{ + HasRelationToVisitor, IsDisjointVisitor, TypeRelation, TypeRelationChecker, +}; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::tuple::TupleSpec; use crate::types::{ @@ -1044,93 +1046,20 @@ impl<'db> ClassType<'db> { } /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.when_subclass_of( - db, - other, - &ConstraintSetBuilder::new(), + pub(super) fn is_subclass_of(self, db: &'db dyn Db, target: ClassType<'db>) -> bool { + let constraints = ConstraintSetBuilder::new(); + let relation_visitor = HasRelationToVisitor::default(&constraints); + let disjointness_visitor = IsDisjointVisitor::default(&constraints); + let checker = TypeRelationChecker::new( + &constraints, InferableTypeVars::None, - ) - .is_always_satisfied(db) - } - - pub(super) fn when_subclass_of<'c>( - self, - db: &'db dyn Db, - other: ClassType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - ) -> ConstraintSet<'db, 'c> { - self.has_relation_to_impl( - db, - other, - constraints, - inferable, TypeRelation::Subtyping, - &HasRelationToVisitor::default(constraints), - &IsDisjointVisitor::default(constraints), - ) - } - - #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - self.iter_mro(db).when_any(db, constraints, |base| { - match base { - ClassBase::Dynamic(_) => match relation { - TypeRelation::Subtyping - | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming => { - ConstraintSet::from_bool(constraints, other.is_object(db)) - } - TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { - ConstraintSet::from_bool(constraints, !other.is_final(db)) - } - }, - - // Protocol, Generic, and TypedDict are special bases that don't match ClassType. - ClassBase::Protocol | ClassBase::Generic | ClassBase::TypedDict => { - ConstraintSet::from_bool(constraints, false) - } - - ClassBase::Class(base) => match (base, other) { - // Two non-generic classes match if they have the same class literal. - (ClassType::NonGeneric(base_literal), ClassType::NonGeneric(other_literal)) => { - ConstraintSet::from_bool(constraints, base_literal == other_literal) - } - - // Two generic classes match if they have the same origin and compatible specializations. - (ClassType::Generic(base), ClassType::Generic(other)) => { - ConstraintSet::from_bool(constraints, base.origin(db) == other.origin(db)) - .and(db, constraints, || { - base.specialization(db).has_relation_to_impl( - db, - other.specialization(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } - - // Generic and non-generic classes don't match. - (ClassType::Generic(_), ClassType::NonGeneric(_)) - | (ClassType::NonGeneric(_), ClassType::Generic(_)) => { - ConstraintSet::from_bool(constraints, false) - } - }, - } - }) + &relation_visitor, + &disjointness_visitor, + ); + checker + .check_class_pair(db, self, target) + .is_always_satisfied(db) } /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. @@ -1907,6 +1836,62 @@ impl<'db> VarianceInferable<'db> for ClassType<'db> { } } +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + pub(super) fn check_class_pair( + &self, + db: &'db dyn Db, + source: ClassType<'db>, + target: ClassType<'db>, + ) -> ConstraintSet<'db, 'c> { + source.iter_mro(db).when_any(db, self.constraints, |base| { + match base { + ClassBase::Dynamic(_) => match self.relation { + TypeRelation::Subtyping + | TypeRelation::Redundancy { .. } + | TypeRelation::SubtypingAssuming => { + ConstraintSet::from_bool(self.constraints, target.is_object(db)) + } + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { + ConstraintSet::from_bool(self.constraints, !target.is_final(db)) + } + }, + + // Protocol, Generic, and TypedDict are special bases that don't match ClassType. + ClassBase::Protocol | ClassBase::Generic | ClassBase::TypedDict => self.never(), + + ClassBase::Class(source) => match (source, target) { + // Two non-generic classes match if they have the same class literal. + ( + ClassType::NonGeneric(source_literal), + ClassType::NonGeneric(target_literal), + ) => { + ConstraintSet::from_bool(self.constraints, source_literal == target_literal) + } + + // Two generic classes match if they have the same origin and compatible specializations. + (ClassType::Generic(source), ClassType::Generic(target)) => { + ConstraintSet::from_bool( + self.constraints, + source.origin(db) == target.origin(db), + ) + .and(db, self.constraints, || { + self.check_specialization_pair( + db, + source.specialization(db), + target.specialization(db), + ) + }) + } + + // Generic and non-generic classes don't match. + (ClassType::Generic(_), ClassType::NonGeneric(_)) + | (ClassType::NonGeneric(_), ClassType::Generic(_)) => self.never(), + }, + } + }) + } +} + #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)] pub(super) struct AbstractMethod<'db> { pub(super) defining_class: ClassType<'db>, diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index d279f0f93c207..ea7a69ba7d466 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -1,23 +1,24 @@ //! Cycle detection for recursive types. //! -//! The visitors here (`TypeTransformer` and `PairVisitor`) are used in methods that recursively -//! visit types to transform them (e.g. `Type::normalize`) or to decide a relation between a pair -//! of types (e.g. `Type::has_relation_to`). +//! The visitors here ([`TypeTransformer`] and [`PairVisitor`]) are used in methods that +//! recursively visit types to transform them (e.g. [`Type::apply_type_mapping`]) or to +//! decide a relation between a pair of types (e.g. [`Type::has_relation_to`]). //! -//! The typical pattern is that the "entry" method (e.g. `Type::has_relation_to`) will create a -//! visitor and pass it to the recursive method (e.g. `Type::has_relation_to_impl`). Rust types -//! that form part of a complex type (e.g. tuples, protocols, nominal instances, etc) should -//! usually just implement the recursive method, and all recursive calls should call the recursive -//! method and pass along the visitor. +//! The typical pattern is that the "entry" method (e.g. [`Type::apply_type_mapping`]) will create +//! a visitor and pass it to the recursive method (e.g. [`Type::apply_type_mapping_impl`]). +//! Rust types that form part of a complex type (e.g. tuples, protocols, nominal instances, etc) +//! should usually just implement the recursive method, and all recursive calls should call the +//! recursive method and pass along the visitor. //! //! Not all recursive calls need to actually call `.visit` on the visitor; only when visiting types //! that can create a recursive relationship (this includes, for example, type aliases and //! protocols). //! -//! There is a risk of double-visiting, for example if `Type::has_relation_to_impl` calls -//! `visitor.visit` when visiting a protocol type, and then internal `has_relation_to_impl` methods -//! of the Rust types implementing protocols also call `visitor.visit`. The best way to avoid this -//! is to prefer always calling `visitor.visit` only in the main recursive method on `Type`. +//! There is a risk of double-visiting, for example if [`Type::apply_type_mapping_impl`] calls +//! `visitor.visit` when visiting a protocol type, and then internal `apply_type_mapping_impl` +//! methods of the Rust types implementing protocols also call `visitor.visit`. The best way to +//! avoid this is to prefer always calling `visitor.visit` only in the main recursive method on +//! `Type`. use std::cell::{Cell, RefCell}; use std::cmp::Eq; diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 9450ea7e34542..7220943ea9d80 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -66,7 +66,7 @@ use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{FileScopeId, SemanticIndex, semantic_index}; use crate::types::call::{Binding, CallArguments}; use crate::types::callable::CallableTypeKind; -use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; +use crate::types::constraints::ConstraintSet; use crate::types::context::InferContext; use crate::types::diagnostic::{ ASSERT_TYPE_UNSPELLABLE_SUBTYPE, INVALID_ARGUMENT_TYPE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, @@ -77,12 +77,12 @@ use crate::types::diagnostic::{ report_runtime_check_against_typed_dict, }; use crate::types::display::DisplaySettings; -use crate::types::generics::{GenericContext, InferableTypeVars, typing_self}; +use crate::types::generics::{GenericContext, typing_self}; use crate::types::infer::nearest_enclosing_class; use crate::types::known_instance::DeprecatedInstance; use crate::types::list_members::all_members; use crate::types::narrow::ClassInfoConstraintFunction; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::relation::TypeRelationChecker; use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; use crate::types::{ @@ -1189,34 +1189,6 @@ impl<'db> FunctionType<'db> { BoundMethodType::new(db, self, self_instance) } - #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - if self.literal(db) != other.literal(db) { - return ConstraintSet::from_bool(constraints, false); - } - - let self_signature = self.signature(db); - let other_signature = other.signature(db); - self_signature.has_relation_to_impl( - db, - other_signature, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } - pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, @@ -1262,6 +1234,20 @@ impl<'db> FunctionType<'db> { } } +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + pub(super) fn check_function_pair( + &self, + db: &'db dyn Db, + source: FunctionType<'db>, + target: FunctionType<'db>, + ) -> ConstraintSet<'db, 'c> { + if source.literal(db) != target.literal(db) { + return self.never(); + } + self.check_callable_signature_pair(db, source.signature(db), target.signature(db)) + } +} + /// Check the second argument to `isinstance()` or `issubclass()` for types that cannot be used /// at runtime (protocol classes, typed dicts, `typing.Any` in `isinstance`, and invalid /// `UnionType` elements). Handles class literals, tuples (including nested tuples), and diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 8f91a332e4c75..cf8cf1ddb9bd2 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -18,7 +18,9 @@ use crate::types::class_base::ClassBase; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, Solutions, }; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::relation::{ + DisjointnessChecker, HasRelationToVisitor, IsDisjointVisitor, TypeRelation, TypeRelationChecker, +}; use crate::types::signatures::{CallableSignature, Parameters}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::type_alias::{walk_manual_pep_695_type_alias, walk_pep_695_type_alias}; @@ -1005,215 +1007,6 @@ pub(super) fn walk_specialization<'db, V: TypeVisitor<'db> + ?Sized>( } } -#[expect(clippy::too_many_arguments)] -fn is_subtype_in_invariant_position<'db, 'c>( - db: &'db dyn Db, - derived_type: &Type<'db>, - derived_materialization: MaterializationKind, - base_type: &Type<'db>, - base_materialization: MaterializationKind, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, -) -> ConstraintSet<'db, 'c> { - let derived_top = derived_type.top_materialization(db); - let derived_bottom = derived_type.bottom_materialization(db); - let base_top = base_type.top_materialization(db); - let base_bottom = base_type.bottom_materialization(db); - - let is_subtype_of = |derived: Type<'db>, base: Type<'db>| { - // TODO: - // This should be removed and properly handled in the respective - // `(Type::TypeVar(_), _) | (_, Type::TypeVar(_))` branch of - // `Type::has_relation_to_impl`. Right now, we cannot generally - // return `ConstraintSet::from_bool(constraints,true)` from that branch, as that - // leads to union simplification, which means that we lose track - // of type variables without recording the constraints under which - // the relation holds. - if matches!(base, Type::TypeVar(_)) || matches!(derived, Type::TypeVar(_)) { - return ConstraintSet::from_bool(constraints, true); - } - - derived.has_relation_to_impl( - db, - base, - constraints, - inferable, - TypeRelation::Subtyping, - relation_visitor, - disjointness_visitor, - ) - }; - match (derived_materialization, base_materialization) { - // `Derived` is a subtype of `Base` if the range of materializations covered by `Derived` - // is a subset of the range covered by `Base`. - (MaterializationKind::Top, MaterializationKind::Top) => { - is_subtype_of(base_bottom, derived_bottom) - .and(db, constraints, || is_subtype_of(derived_top, base_top)) - } - // One bottom is a subtype of another if it covers a strictly larger set of materializations. - (MaterializationKind::Bottom, MaterializationKind::Bottom) => { - is_subtype_of(derived_bottom, base_bottom) - .and(db, constraints, || is_subtype_of(base_top, derived_top)) - } - // The bottom materialization of `Derived` is a subtype of the top materialization - // of `Base` if there is some type that is both within the - // range of types covered by derived and within the range covered by base, because if such a type - // exists, it's a subtype of `Top[base]` and a supertype of `Bottom[derived]`. - (MaterializationKind::Bottom, MaterializationKind::Top) => { - is_subtype_of(base_bottom, derived_bottom) - .and(db, constraints, || is_subtype_of(derived_bottom, base_top)) - .or(db, constraints, || { - is_subtype_of(base_bottom, derived_top) - .and(db, constraints, || is_subtype_of(derived_top, base_top)) - }) - .or(db, constraints, || { - is_subtype_of(base_top, derived_top) - .and(db, constraints, || is_subtype_of(derived_bottom, base_top)) - }) - } - // A top materialization is a subtype of a bottom materialization only if both original - // un-materialized types are the same fully static type. - (MaterializationKind::Top, MaterializationKind::Bottom) => { - is_subtype_of(derived_top, base_bottom) - .and(db, constraints, || is_subtype_of(base_top, derived_bottom)) - } - } -} - -/// Whether two types encountered in an invariant position -/// have a relation (subtyping or assignability), taking into account -/// that the two types may come from a top or bottom materialization. -#[expect(clippy::too_many_arguments)] -fn has_relation_in_invariant_position<'db, 'c>( - db: &'db dyn Db, - derived_type: &Type<'db>, - derived_materialization: Option, - base_type: &Type<'db>, - base_materialization: Option, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, -) -> ConstraintSet<'db, 'c> { - match (derived_materialization, base_materialization, relation) { - // Top and bottom materializations are fully static types, so subtyping - // is the same as assignability. - (Some(derived_mat), Some(base_mat), _) => is_subtype_in_invariant_position( - db, - derived_type, - derived_mat, - base_type, - base_mat, - constraints, - inferable, - relation_visitor, - disjointness_visitor, - ), - // Subtyping between invariant type parameters without a top/bottom materialization necessitates - // checking the subtyping relation both ways: `A` must be a subtype of `B` *and* `B` must be a - // subtype of `A`. The same applies to assignability. - // - // For subtyping between fully static types, this is the same as equivalence. However, we cannot - // use `is_equivalent_to` (or `when_equivalent_to`) here, because we (correctly) understand - // `list[Any]` as being equivalent to `list[Any]`, but we don't want `list[Any]` to be - // considered a subtype of `list[Any]`. For assignability, we would have the opposite issue if - // we simply checked for equivalence here: `Foo[Any]` should be considered assignable to - // `Foo[list[Any]]` even if `Foo` is invariant, and even though `Any` is not equivalent to - // `list[Any]`, because `Any` is assignable to `list[Any]` and `list[Any]` is assignable to - // `Any`. - (None, None, relation) => derived_type - .has_relation_to_impl( - db, - *base_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - .and(db, constraints, || { - base_type.has_relation_to_impl( - db, - *derived_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), - // For gradual types, A <: B (subtyping) is defined as Top[A] <: Bottom[B] - ( - None, - Some(base_mat), - TypeRelation::Subtyping - | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming, - ) => is_subtype_in_invariant_position( - db, - derived_type, - MaterializationKind::Top, - base_type, - base_mat, - constraints, - inferable, - relation_visitor, - disjointness_visitor, - ), - ( - Some(derived_mat), - None, - TypeRelation::Subtyping - | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming, - ) => is_subtype_in_invariant_position( - db, - derived_type, - derived_mat, - base_type, - MaterializationKind::Bottom, - constraints, - inferable, - relation_visitor, - disjointness_visitor, - ), - // And A <~ B (assignability) is Bottom[A] <: Top[B] - ( - None, - Some(base_mat), - TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability, - ) => is_subtype_in_invariant_position( - db, - derived_type, - MaterializationKind::Bottom, - base_type, - base_mat, - constraints, - inferable, - relation_visitor, - disjointness_visitor, - ), - ( - Some(derived_mat), - None, - TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability, - ) => is_subtype_in_invariant_position( - db, - derived_type, - derived_mat, - base_type, - MaterializationKind::Top, - constraints, - inferable, - relation_visitor, - disjointness_visitor, - ), - } -} - impl<'db> Specialization<'db> { /// Restricts this specialization to only include the typevars in a generic context. If the /// specialization does not include all of those typevars, returns `None`. @@ -1489,180 +1282,310 @@ impl<'db> Specialization<'db> { ) } - #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( + pub(crate) fn is_disjoint_from<'c>( self, db: &'db dyn Db, other: Self, constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, ) -> ConstraintSet<'db, 'c> { - let generic_context = self.generic_context(db); - if generic_context != other.generic_context(db) { - return ConstraintSet::from_bool(constraints, false); + let relation_visitor = HasRelationToVisitor::default(constraints); + let disjointness_visitor = IsDisjointVisitor::default(constraints); + let checker = DisjointnessChecker::new( + constraints, + inferable, + &relation_visitor, + &disjointness_visitor, + ); + checker.check_specialization_pair(db, self, other) + } + + pub(crate) fn find_legacy_typevars_impl( + self, + db: &'db dyn Db, + binding_context: Option>, + typevars: &mut FxOrderSet>, + visitor: &FindLegacyTypeVarsVisitor<'db>, + ) { + for ty in self.types(db) { + ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + } + // A tuple's specialization will include all of its element types, so we don't need to also + // look in `self.tuple`. + } +} + +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + pub(super) fn check_specialization_pair( + &self, + db: &'db dyn Db, + source: Specialization<'db>, + target: Specialization<'db>, + ) -> ConstraintSet<'db, 'c> { + let generic_context = source.generic_context(db); + if generic_context != target.generic_context(db) { + return self.never(); } - if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db)) + if let (Some(source_tuple), Some(target_tuple)) = + (source.tuple_inner(db), target.tuple_inner(db)) { - return self_tuple.has_relation_to_impl( - db, - other_tuple, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); + return self.check_tuple_type_pair(db, source_tuple, target_tuple); } - let self_materialization_kind = self.materialization_kind(db); - let other_materialization_kind = other.materialization_kind(db); + let source_materialization_kind = source.materialization_kind(db); + let target_materialization_kind = target.materialization_kind(db); let types = itertools::izip!( generic_context.variables(db), - self.types(db), - other.types(db) + source.types(db), + target.types(db) ); - types.when_all(db, constraints, |(bound_typevar, self_type, other_type)| { - // Subtyping/assignability of each type in the specialization depends on the variance - // of the corresponding typevar: - // - covariant: verify that self_type <: other_type - // - contravariant: verify that other_type <: self_type - // - invariant: verify that self_type <: other_type AND other_type <: self_type - // - bivariant: skip, can't make subtyping/assignability false - match bound_typevar.variance(db) { - TypeVarVariance::Invariant => has_relation_in_invariant_position( - db, - self_type, - self_materialization_kind, - other_type, - other_materialization_kind, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - TypeVarVariance::Covariant => self_type.has_relation_to_impl( - db, - *other_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - TypeVarVariance::Contravariant => other_type.has_relation_to_impl( - db, - *self_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - TypeVarVariance::Bivariant => ConstraintSet::from_bool(constraints, true), + types.when_all( + db, + self.constraints, + |(bound_typevar, source_type, target_type)| { + // Subtyping/assignability of each type in the specialization depends on the variance + // of the corresponding typevar: + // - covariant: verify that source_type <: target_type + // - contravariant: verify that target_type <: source_type + // - invariant: verify that source_type <: target_type AND target_type <: source_type + // - bivariant: skip, can't make subtyping/assignability false + match bound_typevar.variance(db) { + TypeVarVariance::Invariant => self.check_relation_in_invariant_position( + db, + *source_type, + source_materialization_kind, + *target_type, + target_materialization_kind, + ), + TypeVarVariance::Covariant => { + self.check_type_pair(db, *source_type, *target_type) + } + TypeVarVariance::Contravariant => { + self.check_type_pair(db, *target_type, *source_type) + } + TypeVarVariance::Bivariant => self.always(), + } + }, + ) + } + + /// Whether two types encountered in an invariant position + /// have a relation (subtyping or assignability), taking into account + /// that the two types may come from a top or bottom materialization. + fn check_relation_in_invariant_position( + &self, + db: &'db dyn Db, + source_type: Type<'db>, + source_materialization: Option, + target_type: Type<'db>, + target_materialization: Option, + ) -> ConstraintSet<'db, 'c> { + match ( + source_materialization, + target_materialization, + self.relation, + ) { + // Top and bottom materializations are fully static types, so subtyping + // is the same as assignability. + (Some(source_mat), Some(target_mat), _) => self.check_subtyping_in_invariant_position( + db, + source_type, + source_mat, + target_type, + target_mat, + ), + // Subtyping between invariant type parameters without a top/bottom materialization necessitates + // checking the subtyping relation both ways: `A` must be a subtype of `B` *and* `B` must be a + // subtype of `A`. The same applies to assignability. + // + // For subtyping between fully static types, this is the same as equivalence. However, we cannot + // use `is_equivalent_to` (or `when_equivalent_to`) here, because we (correctly) understand + // `list[Any]` as being equivalent to `list[Any]`, but we don't want `list[Any]` to be + // considered a subtype of `list[Any]`. For assignability, we would have the opposite issue if + // we simply checked for equivalence here: `Foo[Any]` should be considered assignable to + // `Foo[list[Any]]` even if `Foo` is invariant, and even though `Any` is not equivalent to + // `list[Any]`, because `Any` is assignable to `list[Any]` and `list[Any]` is assignable to + // `Any`. + (None, None, _) => { + self.check_type_pair(db, target_type, source_type) + .and(db, self.constraints, || { + self.check_type_pair(db, source_type, target_type) + }) } - }) + // For gradual types, A <: B (subtyping) is defined as Top[A] <: Bottom[B] + ( + None, + Some(target_mat), + TypeRelation::Subtyping + | TypeRelation::Redundancy { .. } + | TypeRelation::SubtypingAssuming, + ) => self.check_subtyping_in_invariant_position( + db, + source_type, + MaterializationKind::Top, + target_type, + target_mat, + ), + ( + Some(source_mat), + None, + TypeRelation::Subtyping + | TypeRelation::Redundancy { .. } + | TypeRelation::SubtypingAssuming, + ) => self.check_subtyping_in_invariant_position( + db, + source_type, + source_mat, + target_type, + MaterializationKind::Bottom, + ), + // And A <~ B (assignability) is Bottom[A] <: Top[B] + ( + None, + Some(target_mat), + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability, + ) => self.check_subtyping_in_invariant_position( + db, + source_type, + MaterializationKind::Bottom, + target_type, + target_mat, + ), + ( + Some(source_mat), + None, + TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability, + ) => self.check_subtyping_in_invariant_position( + db, + source_type, + source_mat, + target_type, + MaterializationKind::Top, + ), + } } - pub(crate) fn is_disjoint_from<'c>( - self, + fn check_subtyping_in_invariant_position( + &self, db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, + source_type: Type<'db>, + source_materialization: MaterializationKind, + target_type: Type<'db>, + target_materialization: MaterializationKind, ) -> ConstraintSet<'db, 'c> { - self.is_disjoint_from_impl( - db, - other, - constraints, - inferable, - &IsDisjointVisitor::default(constraints), - &HasRelationToVisitor::default(constraints), - ) + let source_top = source_type.top_materialization(db); + let source_bottom = source_type.bottom_materialization(db); + let target_top = target_type.top_materialization(db); + let target_bottom = target_type.bottom_materialization(db); + + let is_subtype_of = |source: Type<'db>, target: Type<'db>| { + // TODO: + // This should be removed and properly handled in the respective + // `(Type::TypeVar(_), _) | (_, Type::TypeVar(_))` branch of + // `TypeRelationChecker::check_type_pair`. Right now, we cannot generally + // return `self.always()` from that branch, as that leads to union + // simplification, which means that we lose track of type variables + // without recording the constraints under which the relation holds. + if matches!(target, Type::TypeVar(_)) || matches!(source, Type::TypeVar(_)) { + return self.always(); + } + + self.check_type_pair(db, source, target) + }; + match (source_materialization, target_materialization) { + // `source` is a subtype of `target` if the range of materializations covered by `source` + // is a subset of the range covered by `target`. + (MaterializationKind::Top, MaterializationKind::Top) => { + is_subtype_of(target_bottom, source_bottom).and(db, self.constraints, || { + is_subtype_of(source_top, target_top) + }) + } + // One bottom is a subtype of another if it covers a strictly larger set of materializations. + (MaterializationKind::Bottom, MaterializationKind::Bottom) => { + is_subtype_of(source_bottom, target_bottom).and(db, self.constraints, || { + is_subtype_of(target_top, source_top) + }) + } + // The bottom materialization of `source` is a subtype of the top materialization + // of `target` if there is some type that is both within the + // range of types covered by derived and within the range covered by base, because if such a type + // exists, it's a subtype of `Top[target]` and a supertype of `Bottom[source]`. + (MaterializationKind::Bottom, MaterializationKind::Top) => { + is_subtype_of(target_bottom, source_bottom) + .and(db, self.constraints, || { + is_subtype_of(source_bottom, target_top) + }) + .or(db, self.constraints, || { + is_subtype_of(target_bottom, source_top).and(db, self.constraints, || { + is_subtype_of(source_top, target_top) + }) + }) + .or(db, self.constraints, || { + is_subtype_of(target_top, source_top).and(db, self.constraints, || { + is_subtype_of(source_bottom, target_top) + }) + }) + } + // A top materialization is a subtype of a bottom materialization only if both original + // un-materialized types are the same fully static type. + (MaterializationKind::Top, MaterializationKind::Bottom) => { + is_subtype_of(source_top, target_bottom).and(db, self.constraints, || { + is_subtype_of(target_top, source_bottom) + }) + } + } } +} - pub(crate) fn is_disjoint_from_impl<'c>( - self, +impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { + pub(super) fn check_specialization_pair( + &self, db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, + left: Specialization<'db>, + right: Specialization<'db>, ) -> ConstraintSet<'db, 'c> { - let generic_context = self.generic_context(db); - if generic_context != other.generic_context(db) { - return ConstraintSet::from_bool(constraints, true); + let generic_context = left.generic_context(db); + if generic_context != right.generic_context(db) { + return self.always(); } - if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db)) + if let (Some(left_tuple), Some(right_tuple)) = (left.tuple_inner(db), right.tuple_inner(db)) { - return self_tuple.is_disjoint_from_impl( - db, - other_tuple, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ); + return self.check_tuple_type_pair(db, left_tuple, right_tuple); } let types = itertools::izip!( generic_context.variables(db), - self.types(db), - other.types(db) + left.types(db), + right.types(db) ); types.when_all( db, - constraints, - |(bound_typevar, self_type, other_type)| match bound_typevar.variance(db) { + self.constraints, + |(bound_typevar, left_type, right_type)| match bound_typevar.variance(db) { // TODO: This check can lead to false negatives. // // For example, `Foo[int]` and `Foo[bool]` are disjoint, even though `bool` is a subtype // of `int`. However, given two non-inferable type variables `T` and `U`, `Foo[T]` and // `Foo[U]` should not be considered disjoint, as `T` and `U` could be specialized to the // same type. We don't currently have a good typing relationship to represent this. - TypeVarVariance::Invariant => self_type.is_disjoint_from_impl( - db, - *other_type, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ), + TypeVarVariance::Invariant => self.check_type_pair(db, *left_type, *right_type), // If `Foo[T]` is covariant in `T`, `Foo[Never]` is a subtype of `Foo[A]` and `Foo[B]` - TypeVarVariance::Covariant => ConstraintSet::from_bool(constraints, false), + TypeVarVariance::Covariant => self.never(), // If `Foo[T]` is contravariant in `T`, `Foo[A | B]` is a subtype of `Foo[A]` and `Foo[B]` - TypeVarVariance::Contravariant => ConstraintSet::from_bool(constraints, false), + TypeVarVariance::Contravariant => self.never(), // If `Foo[T]` is bivariant in `T`, `Foo[A]` and `Foo[B]` are mutual subtypes. - TypeVarVariance::Bivariant => ConstraintSet::from_bool(constraints, false), + TypeVarVariance::Bivariant => self.never(), }, ) } - - pub(crate) fn find_legacy_typevars_impl( - self, - db: &'db dyn Db, - binding_context: Option>, - typevars: &mut FxOrderSet>, - visitor: &FindLegacyTypeVarsVisitor<'db>, - ) { - for ty in self.types(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); - } - // A tuple's specialization will include all of its element types, so we don't need to also - // look in `self.tuple`. - } } /// A mapping between type variables and types. diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 48ec1a0a16191..cc5c220d9f8cb 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -17,7 +17,9 @@ use crate::types::constraints::{ use crate::types::enums::is_single_member_enum; use crate::types::generics::{InferableTypeVars, walk_specialization}; use crate::types::protocol_class::{ProtocolClass, walk_protocol_interface}; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::relation::{ + DisjointnessChecker, HasRelationToVisitor, IsDisjointVisitor, TypeRelation, TypeRelationChecker, +}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::{ ApplyTypeMappingVisitor, ClassBase, ClassLiteral, FindLegacyTypeVarsVisitor, @@ -137,94 +139,6 @@ impl<'db> Type<'db> { SynthesizedProtocolType::new(ProtocolInterface::with_property_members(db, members)), )) } - - /// Return `true` if `self` conforms to the interface described by `protocol`. - #[expect(clippy::too_many_arguments)] - pub(super) fn satisfies_protocol<'c>( - self, - db: &'db dyn Db, - protocol: ProtocolInstanceType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - // `self` might satisfy the protocol nominally, if `protocol` is a class-based protocol and - // `self` has the protocol class in its MRO. This is a much cheaper check than the - // structural check we perform below, so we do it first to avoid the structural check when - // we can. - let mut result = ConstraintSet::from_bool(constraints, false); - if let Some(nominal_instance) = protocol.to_nominal_instance() { - // if `self` and `other` are *both* protocols, we also need to treat `self` as if it - // were a nominal type, or we won't consider a protocol `P` that explicitly inherits - // from a protocol `Q` to be a subtype of `Q` to be a subtype of `Q` if it overrides - // `Q`'s members in a Liskov-incompatible way. - let type_to_test = self - .as_protocol_instance() - .and_then(ProtocolInstanceType::to_nominal_instance) - .map(Type::NominalInstance) - .unwrap_or(self); - let nominally_satisfied = type_to_test.has_relation_to_impl( - db, - Type::NominalInstance(nominal_instance), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - if result - .union(db, constraints, nominally_satisfied) - .is_always_satisfied(db) - { - return result; - } - } - - // `Generator` special case: Prior to 3.13, the `_ReturnT_co` type didn't appear in any - // methods (except `__iter__`, but that returns the self type recursively, so it can't rule - // out assignability). We don't want generators with different return types to be - // assignable to each other. In this case we use the result of the nominal check above. - if let Some(self_protocol) = self.as_protocol_instance() - && let Protocol::FromClass(self_class) = self_protocol.inner - && let Protocol::FromClass(proto_class) = protocol.inner - && self_class.known(db) == Some(KnownClass::Generator) - && proto_class.known(db) == Some(KnownClass::Generator) - && Program::get(db).python_version(db) < PythonVersion::PY313 - { - return result; - } - - let structurally_satisfied = if let Type::ProtocolInstance(self_protocol) = self { - self_protocol.interface(db).has_relation_to_impl( - db, - protocol.interface(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } else { - protocol - .inner - .interface(db) - .members(db) - .when_all(db, constraints, |member| { - member.is_satisfied_by( - db, - self, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - }; - result.or(db, constraints, || structurally_satisfied) - } } /// A type representing the set of runtime objects which are instances of a certain nominal class. @@ -437,85 +351,6 @@ impl<'db> NominalInstanceType<'db> { } } - #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match (self.0, other.0) { - (_, NominalInstanceInner::Object) => ConstraintSet::from_bool(constraints, true), - ( - NominalInstanceInner::ExactTuple(tuple1), - NominalInstanceInner::ExactTuple(tuple2), - ) => tuple1.has_relation_to_impl( - db, - tuple2, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - _ => self.class(db).has_relation_to_impl( - db, - other.class(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - } - } - - pub(super) fn is_disjoint_from_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - if self.is_object() || other.is_object() { - return ConstraintSet::from_bool(constraints, false); - } - let mut result = ConstraintSet::from_bool(constraints, false); - if let Some(self_spec) = self.tuple_spec(db) { - if let Some(other_spec) = other.tuple_spec(db) { - let compatible = self_spec.is_disjoint_from_impl( - db, - &other_spec, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ); - if result - .union(db, constraints, compatible) - .is_always_satisfied(db) - { - return result; - } - } - } - - result.or(db, constraints, || { - ConstraintSet::from_bool( - constraints, - !self - .class(db) - .could_coexist_in_mro_with(db, other.class(db), constraints), - ) - }) - } - pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool { match self.0 { // The empty tuple is a singleton on CPython and PyPy, but not on other Python @@ -592,6 +427,138 @@ impl<'db> From> for Type<'db> { } } +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + /// Return `true` if `ty` conforms to the interface described by `protocol`. + pub(super) fn check_type_satisfies_protocol( + &self, + db: &'db dyn Db, + ty: Type<'db>, + protocol: ProtocolInstanceType<'db>, + ) -> ConstraintSet<'db, 'c> { + // `ty` might satisfy the protocol nominally, if `protocol` is a class-based protocol and + // `ty` has the protocol class in its MRO. This is a much cheaper check than the + // structural check we perform below, so we do it first to avoid the structural check when + // we can. + let mut result = self.never(); + + if let Some(nominal_instance) = protocol.to_nominal_instance() { + // if `ty` and `protocol` are *both* protocols, we also need to treat `ty` as if it + // were a nominal type, or we won't consider a protocol `P` that explicitly inherits + // from a protocol `Q` to be a subtype of `Q` to be a subtype of `Q` if it overrides + // `Q`'s members in a Liskov-incompatible way. + let type_to_test = ty + .as_protocol_instance() + .and_then(ProtocolInstanceType::to_nominal_instance) + .map(Type::NominalInstance) + .unwrap_or(ty); + + let nominally_satisfied = + self.check_type_pair(db, type_to_test, Type::NominalInstance(nominal_instance)); + + if result + .union(db, self.constraints, nominally_satisfied) + .is_always_satisfied(db) + { + return result; + } + } + + // `Generator` special case: Prior to 3.13, the `_ReturnT_co` type didn't appear in any + // methods (except `__iter__`, but that returns the self type recursively, so it can't rule + // out assignability). We don't want generators with different return types to be + // assignable to each other. In this case we use the result of the nominal check above. + if let Some(source_protocol) = ty.as_protocol_instance() + && let Protocol::FromClass(source_class) = source_protocol.inner + && let Protocol::FromClass(proto_class) = protocol.inner + && source_class.is_known(db, KnownClass::Generator) + && proto_class.is_known(db, KnownClass::Generator) + && Program::get(db).python_version(db) < PythonVersion::PY313 + { + return result; + } + + let structurally_satisfied = if let Type::ProtocolInstance(source_protocol) = ty { + self.check_protocol_interface_pair( + db, + source_protocol.interface(db), + protocol.interface(db), + ) + } else { + protocol + .inner + .interface(db) + .members(db) + .when_all(db, self.constraints, |member| { + self.type_satisfies_protocol_member(db, ty, &member) + }) + }; + result.or(db, self.constraints, || structurally_satisfied) + } + + pub(super) fn check_nominal_instance_pair( + &self, + db: &'db dyn Db, + source: NominalInstanceType<'db>, + target: NominalInstanceType<'db>, + ) -> ConstraintSet<'db, 'c> { + match (source.0, target.0) { + (_, NominalInstanceInner::Object) => self.always(), + ( + NominalInstanceInner::ExactTuple(source_tuple), + NominalInstanceInner::ExactTuple(target_tuple), + ) => self.check_tuple_type_pair(db, source_tuple, target_tuple), + _ => self.check_class_pair(db, source.class(db), target.class(db)), + } + } +} + +impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { + /// Return `true` if this protocol type is disjoint from the protocol `other`. + /// + /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y` + /// have a member with the same name but disjoint types + pub(super) fn check_protocol_instance_pair( + &self, + _db: &'db dyn Db, + _left: ProtocolInstanceType<'db>, + _right: ProtocolInstanceType<'db>, + ) -> ConstraintSet<'db, 'c> { + self.never() + } + + pub(super) fn check_nominal_instance_pair( + &self, + db: &'db dyn Db, + left: NominalInstanceType<'db>, + right: NominalInstanceType<'db>, + ) -> ConstraintSet<'db, 'c> { + let mut result = self.never(); + if left.is_object() || right.is_object() { + return result; + } + if let Some(left_spec) = left.tuple_spec(db) { + if let Some(right_spec) = right.tuple_spec(db) { + let compatible = self.check_tuple_spec_pair(db, &left_spec, &right_spec); + if result + .union(db, self.constraints, compatible) + .is_always_satisfied(db) + { + return result; + } + } + } + + result.or(db, self.constraints, || { + ConstraintSet::from_bool( + self.constraints, + !left + .class(db) + .could_coexist_in_mro_with(db, right.class(db), self.constraints), + ) + }) + } +} + /// [`NominalInstanceType`] is split into two variants internally as a pure /// optimization to avoid having to materialize the [`ClassType`] for tuple /// instances where it would be unnecessary (this is somewhat expensive!). @@ -730,16 +697,17 @@ impl<'db> ProtocolInstanceType<'db> { _: (), ) -> bool { let constraints = ConstraintSetBuilder::new(); - Type::object() - .satisfies_protocol( - db, - protocol, - &constraints, - InferableTypeVars::None, - TypeRelation::Subtyping, - &HasRelationToVisitor::default(&constraints), - &IsDisjointVisitor::default(&constraints), - ) + let relation_visitor = HasRelationToVisitor::default(&constraints); + let disjointness_visitor = IsDisjointVisitor::default(&constraints); + let checker = TypeRelationChecker::new( + &constraints, + InferableTypeVars::None, + TypeRelation::Subtyping, + &relation_visitor, + &disjointness_visitor, + ); + checker + .check_type_satisfies_protocol(db, Type::object(), protocol) .is_always_satisfied(db) } @@ -758,22 +726,6 @@ impl<'db> ProtocolInstanceType<'db> { }) } - /// Return `true` if this protocol type is disjoint from the protocol `other`. - /// - /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y` - /// have a member with the same name but disjoint types - #[expect(clippy::unused_self)] - pub(super) fn is_disjoint_from_impl<'c>( - self, - _db: &'db dyn Db, - _other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - _inferable: InferableTypeVars<'_, 'db>, - _visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - ConstraintSet::from_bool(constraints, false) - } - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { match self.inner { Protocol::FromClass(class) => class.instance_member(db, name), diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index ae12232bf494c..9173f69683824 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -6,14 +6,9 @@ use crate::{ types::{ CallableType, KnownClass, LiteralValueType, LiteralValueTypeKind, Parameter, Parameters, PropertyInstanceType, Signature, StringLiteralType, Type, UnionType, - callable::CallableTypeKind, - constraints::{ConstraintSet, ConstraintSetBuilder}, - function::FunctionType, - generics::InferableTypeVars, - known_instance::InternedConstraintSet, - relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}, - signatures::CallableSignature, - visitor, + callable::CallableTypeKind, constraints::ConstraintSet, function::FunctionType, + known_instance::InternedConstraintSet, relation::TypeRelationChecker, + signatures::CallableSignature, visitor, }, }; @@ -104,42 +99,22 @@ impl<'db> BoundMethodType<'db> { .recursive_type_normalized_impl(db, div, true)?, )) } +} - #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( - self, +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + pub(super) fn check_bound_method_pair( + &self, db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + source: BoundMethodType<'db>, + target: BoundMethodType<'db>, ) -> ConstraintSet<'db, 'c> { // A bound method is a typically a subtype of itself. However, we must explicitly verify // the subtyping of the underlying function signatures (since they might be specialized // differently), and of the bound self parameter (taking care that parameters, including a // bound self parameter, are contravariant.) - self.function(db) - .has_relation_to_impl( - db, - other.function(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - .and(db, constraints, || { - other.self_instance(db).has_relation_to_impl( - db, - self.self_instance(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_function_pair(db, source.function(db), target.function(db)) + .and(db, self.constraints, || { + self.check_type_pair(db, target.self_instance(db), source.self_instance(db)) }) } } @@ -208,117 +183,6 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size } impl<'db> KnownBoundMethodType<'db> { - #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match (self, other) { - ( - KnownBoundMethodType::FunctionTypeDunderGet(self_function), - KnownBoundMethodType::FunctionTypeDunderGet(other_function), - ) => self_function.has_relation_to_impl( - db, - other_function, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - - ( - KnownBoundMethodType::FunctionTypeDunderCall(self_function), - KnownBoundMethodType::FunctionTypeDunderCall(other_function), - ) => self_function.has_relation_to_impl( - db, - other_function, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - - ( - KnownBoundMethodType::PropertyDunderGet(self_property), - KnownBoundMethodType::PropertyDunderGet(other_property), - ) - | ( - KnownBoundMethodType::PropertyDunderSet(self_property), - KnownBoundMethodType::PropertyDunderSet(other_property), - ) => Type::PropertyInstance(self_property).has_relation_to_impl( - db, - Type::PropertyInstance(other_property), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - - (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { - ConstraintSet::from_bool(constraints, self == other) - } - - ( - KnownBoundMethodType::ConstraintSetRange, - KnownBoundMethodType::ConstraintSetRange, - ) - | ( - KnownBoundMethodType::ConstraintSetAlways, - KnownBoundMethodType::ConstraintSetAlways, - ) - | ( - KnownBoundMethodType::ConstraintSetNever, - KnownBoundMethodType::ConstraintSetNever, - ) - | ( - KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), - KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), - ) - | ( - KnownBoundMethodType::ConstraintSetSatisfies(_), - KnownBoundMethodType::ConstraintSetSatisfies(_), - ) - | ( - KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - ) => ConstraintSet::from_bool(constraints, true), - - ( - KnownBoundMethodType::FunctionTypeDunderGet(_) - | KnownBoundMethodType::FunctionTypeDunderCall(_) - | KnownBoundMethodType::PropertyDunderGet(_) - | KnownBoundMethodType::PropertyDunderSet(_) - | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - KnownBoundMethodType::FunctionTypeDunderGet(_) - | KnownBoundMethodType::FunctionTypeDunderCall(_) - | KnownBoundMethodType::PropertyDunderGet(_) - | KnownBoundMethodType::PropertyDunderSet(_) - | KnownBoundMethodType::StrStartswith(_) - | KnownBoundMethodType::ConstraintSetRange - | KnownBoundMethodType::ConstraintSetAlways - | KnownBoundMethodType::ConstraintSetNever - | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) - | KnownBoundMethodType::ConstraintSetSatisfies(_) - | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), - ) => ConstraintSet::from_bool(constraints, false), - } - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -558,6 +422,90 @@ impl<'db> KnownBoundMethodType<'db> { } } +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + pub(super) fn check_known_bound_method_pair( + &self, + db: &'db dyn Db, + source: KnownBoundMethodType<'db>, + target: KnownBoundMethodType<'db>, + ) -> ConstraintSet<'db, 'c> { + match (source, target) { + ( + KnownBoundMethodType::FunctionTypeDunderGet(source_function), + KnownBoundMethodType::FunctionTypeDunderGet(target_function), + ) => self.check_function_pair(db, source_function, target_function), + + ( + KnownBoundMethodType::FunctionTypeDunderCall(source_function), + KnownBoundMethodType::FunctionTypeDunderCall(target_function), + ) => self.check_function_pair(db, source_function, target_function), + + ( + KnownBoundMethodType::PropertyDunderGet(source_property), + KnownBoundMethodType::PropertyDunderGet(target_property), + ) + | ( + KnownBoundMethodType::PropertyDunderSet(source_property), + KnownBoundMethodType::PropertyDunderSet(target_property), + ) => self.check_property_instance_pair(db, source_property, target_property), + + (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { + ConstraintSet::from_bool(self.constraints, source == target) + } + + ( + KnownBoundMethodType::ConstraintSetRange, + KnownBoundMethodType::ConstraintSetRange, + ) + | ( + KnownBoundMethodType::ConstraintSetAlways, + KnownBoundMethodType::ConstraintSetAlways, + ) + | ( + KnownBoundMethodType::ConstraintSetNever, + KnownBoundMethodType::ConstraintSetNever, + ) + | ( + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_), + ) + | ( + KnownBoundMethodType::ConstraintSetSatisfies(_), + KnownBoundMethodType::ConstraintSetSatisfies(_), + ) + | ( + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + ) => self.always(), + + ( + KnownBoundMethodType::FunctionTypeDunderGet(_) + | KnownBoundMethodType::FunctionTypeDunderCall(_) + | KnownBoundMethodType::PropertyDunderGet(_) + | KnownBoundMethodType::PropertyDunderSet(_) + | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + KnownBoundMethodType::FunctionTypeDunderGet(_) + | KnownBoundMethodType::FunctionTypeDunderCall(_) + | KnownBoundMethodType::PropertyDunderGet(_) + | KnownBoundMethodType::PropertyDunderSet(_) + | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + ) => self.never(), + } + } +} + /// Represents a specific instance of `types.WrapperDescriptorType` #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub enum WrapperDescriptorKind { diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index 08cc882ee4afb..ac086f4ea646e 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -1,6 +1,7 @@ use crate::Db; use crate::semantic_index::definition::{Definition, DefinitionKind}; -use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; +use crate::types::constraints::ConstraintSet; +use crate::types::relation::{DisjointnessChecker, TypeRelation, TypeRelationChecker}; use crate::types::{ClassType, KnownUnion, Type, definition_expression_type, visitor}; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; @@ -107,53 +108,13 @@ impl<'db> NewType<'db> { Type::object() } - pub(crate) fn is_equivalent_to_impl(self, db: &'db dyn Db, other: Self) -> bool { + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { // Two instances of the "same" `NewType` won't compare == if one of them has an eagerly // evaluated base (or a normalized base, etc.) and the other doesn't, so we only check for // equality of the `definition`. self.definition(db) == other.definition(db) } - // Since a regular class can't inherit from a newtype, the only way for one newtype to be a - // subtype of another is to have the other in its chain of newtype bases. Once we reach the - // base class, we don't have to keep looking. - pub(crate) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - ) -> ConstraintSet<'db, 'c> { - if self.is_equivalent_to_impl(db, other) { - return ConstraintSet::from_bool(constraints, true); - } - for base in self.iter_bases(db) { - if let NewTypeBase::NewType(base_newtype) = base { - if base_newtype.is_equivalent_to_impl(db, other) { - return ConstraintSet::from_bool(constraints, true); - } - } - } - ConstraintSet::from_bool(constraints, false) - } - - pub(crate) fn is_disjoint_from_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - ) -> ConstraintSet<'db, 'c> { - // Two NewTypes are disjoint if they're not equal and neither inherits from the other. - // NewTypes have single inheritance, and a regular class can't inherit from a NewType, so - // it's not possible for some third type to multiply-inherit from both. - let mut self_not_subtype_of_other = self - .has_relation_to_impl(db, other, constraints) - .negate(db, constraints); - let other_not_subtype_of_self = other - .has_relation_to_impl(db, self, constraints) - .negate(db, constraints); - self_not_subtype_of_other.intersect(db, constraints, other_not_subtype_of_self) - } - /// Create a new `NewType` by mapping the underlying `ClassType`. This descends through any /// number of nested `NewType` layers and rebuilds the whole chain. In the rare case of cyclic /// `NewType`s with no underlying `ClassType`, this has no effect and does not call `f`. @@ -216,6 +177,50 @@ impl<'db> NewType<'db> { } } +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + pub(super) fn check_newtype_pair( + &self, + db: &'db dyn Db, + source: NewType<'db>, + target: NewType<'db>, + ) -> ConstraintSet<'db, 'c> { + // Since a regular class can't inherit from a newtype, the only way for one newtype to be a + // subtype of another is to have the other in its chain of newtype bases. Once we reach the + // base class, we don't have to keep looking. + if source.is_equivalent_to(db, target) { + return self.always(); + } + for base in source.iter_bases(db) { + if let NewTypeBase::NewType(base_newtype) = base + && base_newtype.is_equivalent_to(db, target) + { + return self.always(); + } + } + self.never() + } +} + +impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { + pub(super) fn check_newtype_pair( + &self, + db: &'db dyn Db, + left: NewType<'db>, + right: NewType<'db>, + ) -> ConstraintSet<'db, 'c> { + // Two NewTypes are disjoint if they're not equal and neither inherits from the other. + // NewTypes have single inheritance, and a regular class can't inherit from a NewType, so + // it's not possible for some third type to multiply-inherit from both. + let relation_checker = self.as_relation_checker(TypeRelation::Subtyping); + relation_checker + .check_newtype_pair(db, left, right) + .or(db, self.constraints, || { + relation_checker.check_newtype_pair(db, right, left) + }) + .negate(db, self.constraints) + } +} + pub(crate) fn walk_newtype_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, newtype: NewType<'db>, diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 16bb91c089ae8..4c518e84e64c4 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -7,7 +7,7 @@ use ruff_python_ast::name::Name; use rustc_hash::FxHashMap; use crate::types::callable::CallableTypeKind; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::relation::{DisjointnessChecker, TypeRelationChecker}; use crate::types::{TypeContext, UpcastPolicy}; use crate::{ Db, FxOrderSet, @@ -21,13 +21,9 @@ use crate::{ FindLegacyTypeVarsVisitor, InstanceFallbackShadowsNonDataDescriptor, KnownFunction, MemberLookupPolicy, PropertyInstanceType, Signature, StaticClassLiteral, Type, TypeMapping, TypeQualifiers, TypeVarVariance, VarianceInferable, - constraints::{ - ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, - OptionConstraintsExtension, - }, + constraints::{ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension}, context::InferContext, diagnostic::report_undeclared_protocol_member, - generics::InferableTypeVars, signatures::{Parameter, Parameters}, todo_type, }, @@ -291,119 +287,6 @@ impl<'db> ProtocolInterface<'db> { .unwrap_or_else(|| Type::object().member(db, name)) } - #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - other.members(db).when_all(db, constraints, |other_member| { - self.member_by_name(db, other_member.name).when_some_and( - db, - constraints, - |our_member| { - match (our_member.kind, other_member.kind) { - // Method members are always immutable; - // they can never be subtypes of/assignable to mutable attribute members. - (ProtocolMemberKind::Method(_), ProtocolMemberKind::Other(_)) => { - ConstraintSet::from_bool(constraints, false) - } - - // A property member can only be a subtype of an attribute member - // if the property is readable *and* writable. - // - // TODO: this should also consider the types of the members on both sides. - (ProtocolMemberKind::Property(property), ProtocolMemberKind::Other(_)) => { - ConstraintSet::from_bool( - constraints, - property.getter(db).is_some() && property.setter(db).is_some(), - ) - } - - // A `@property` member can never be a subtype of a method member, as it is not necessarily - // accessible on the meta-type, whereas a method member must be. - (ProtocolMemberKind::Property(_), ProtocolMemberKind::Method(_)) => { - ConstraintSet::from_bool(constraints, false) - } - - // But an attribute member *can* be a subtype of a method member, - // providing it is marked `ClassVar` - ( - ProtocolMemberKind::Other(our_type), - ProtocolMemberKind::Method(other_type), - ) => ConstraintSet::from_bool( - constraints, - our_member.qualifiers.contains(TypeQualifiers::CLASS_VAR), - ) - .and(db, constraints, || { - our_type.has_relation_to_impl( - db, - Type::Callable(protocol_bind_self(db, other_type, None)), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), - - ( - ProtocolMemberKind::Method(our_method), - ProtocolMemberKind::Method(other_method), - ) => our_method.bind_self(db, None).has_relation_to_impl( - db, - protocol_bind_self(db, other_method, None), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - - ( - ProtocolMemberKind::Other(our_type), - ProtocolMemberKind::Other(other_type), - ) => our_type - .has_relation_to_impl( - db, - other_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - .and(db, constraints, || { - other_type.has_relation_to_impl( - db, - our_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), - - // TODO: finish assignability/subtyping between two `@property` members, - // and between a `@property` member and a member of a different kind. - ( - ProtocolMemberKind::Property(_) - | ProtocolMemberKind::Method(_) - | ProtocolMemberKind::Other(_), - ProtocolMemberKind::Property(_), - ) => ConstraintSet::from_bool(constraints, true), - } - }, - ) - }) - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -696,46 +579,18 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { ProtocolMemberKind::Other(ty) => *ty, } } +} - pub(super) fn has_disjoint_type_from<'c>( - &self, - db: &'db dyn Db, - other: Type<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match &self.kind { - // TODO: implement disjointness for property/method members as well as attribute members - ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => { - ConstraintSet::from_bool(constraints, false) - } - ProtocolMemberKind::Other(ty) => ty.is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ), - } - } - +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { /// Return `true` if `other` contains an attribute/method/property that satisfies /// the part of the interface defined by this protocol member. - #[expect(clippy::too_many_arguments)] - pub(super) fn is_satisfied_by<'c>( + pub(super) fn type_satisfies_protocol_member( &self, db: &'db dyn Db, - other: Type<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + ty: Type<'db>, + member: &ProtocolMember<'_, 'db>, ) -> ConstraintSet<'db, 'c> { - match &self.kind { + match &member.kind { ProtocolMemberKind::Method(method) => { // `__call__` members must be special cased for several reasons: // @@ -746,24 +601,24 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { // 3. Looking up `__call__` on the meta-type of a class-literal, generic-alias or subclass-of type is // unfortunately not sufficient to obtain the `Callable` supertypes of these types, due to the // complex interaction between `__new__`, `__init__` and metaclass `__call__`. - let attribute_type = if self.name == "__call__" { - other + let attribute_type = if member.name == "__call__" { + ty } else { let Place::Defined(DefinedPlace { ty: attribute_type, definedness: Definedness::AlwaysDefined, .. - }) = other + }) = ty .invoke_descriptor_protocol( db, - self.name, + member.name, Place::Undefined.into(), InstanceFallbackShadowsNonDataDescriptor::No, MemberLookupPolicy::default(), ) .place else { - return ConstraintSet::from_bool(constraints, false); + return self.never(); }; attribute_type }; @@ -779,28 +634,22 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { // // With the new solver, we should be to replace all of this with an additional // constraint that enforces what `Self` can specialize to. - let fallback_other = other.literal_fallback_instance(db).unwrap_or(other); + let fallback_other = ty.literal_fallback_instance(db).unwrap_or(ty); attribute_type - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(relation)) - .when_some_and(db, constraints, |callables| { - callables - .map(|callable| callable.apply_self(db, fallback_other)) - .has_relation_to_impl( - db, - protocol_bind_self(db, *method, Some(fallback_other)), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) + .when_some_and(db, self.constraints, |callables| { + self.check_callables_vs_callable( + db, + &callables.map(|callable| callable.apply_self(db, fallback_other)), + protocol_bind_self(db, *method, Some(fallback_other)), + ) }) } // TODO: consider the types of the attribute on `other` for property members ProtocolMemberKind::Property(_) => ConstraintSet::from_bool( - constraints, + self.constraints, matches!( - other.member(db, self.name).place, + ty.member(db, member.name).place, Place::Defined(DefinedPlace { definedness: Definedness::AlwaysDefined, .. @@ -812,34 +661,120 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { ty: attribute_type, definedness: Definedness::AlwaysDefined, .. - }) = other.member(db, self.name).place + }) = ty.member(db, member.name).place else { - return ConstraintSet::from_bool(constraints, false); + return self.never(); }; - member_type - .has_relation_to_impl( - db, - attribute_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - .and(db, constraints, || { - attribute_type.has_relation_to_impl( - db, - *member_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + self.check_type_pair(db, *member_type, attribute_type).and( + db, + self.constraints, + || self.check_type_pair(db, attribute_type, *member_type), + ) } } } + + pub(super) fn check_protocol_interface_pair( + &self, + db: &'db dyn Db, + source: ProtocolInterface<'db>, + target: ProtocolInterface<'db>, + ) -> ConstraintSet<'db, 'c> { + target + .members(db) + .when_all(db, self.constraints, |target_member| { + source.member_by_name(db, target_member.name).when_some_and( + db, + self.constraints, + |source_member| { + match (source_member.kind, target_member.kind) { + // Method members are always immutable; + // they can never be subtypes of/assignable to mutable attribute members. + (ProtocolMemberKind::Method(_), ProtocolMemberKind::Other(_)) => { + self.never() + } + + // A property member can only be a subtype of an attribute member + // if the property is readable *and* writable. + // + // TODO: this should also consider the types of the members on both sides. + ( + ProtocolMemberKind::Property(property), + ProtocolMemberKind::Other(_), + ) => ConstraintSet::from_bool( + self.constraints, + property.getter(db).is_some() && property.setter(db).is_some(), + ), + + // A `@property` member can never be a subtype of a method member, as it is not necessarily + // accessible on the meta-type, whereas a method member must be. + (ProtocolMemberKind::Property(_), ProtocolMemberKind::Method(_)) => { + self.never() + } + + // But an attribute member *can* be a subtype of a method member, + // providing it is marked `ClassVar` + ( + ProtocolMemberKind::Other(source_type), + ProtocolMemberKind::Method(target_callable), + ) => ConstraintSet::from_bool( + self.constraints, + source_member.qualifiers.contains(TypeQualifiers::CLASS_VAR), + ) + .and(db, self.constraints, || { + self.check_type_pair( + db, + source_type, + Type::Callable(protocol_bind_self(db, target_callable, None)), + ) + }), + + ( + ProtocolMemberKind::Method(source_method), + ProtocolMemberKind::Method(target_method), + ) => self.check_callable_pair( + db, + source_method.bind_self(db, None), + protocol_bind_self(db, target_method, None), + ), + + ( + ProtocolMemberKind::Other(source_type), + ProtocolMemberKind::Other(target_type), + ) => self.check_type_pair(db, source_type, target_type).and( + db, + self.constraints, + || self.check_type_pair(db, target_type, source_type), + ), + + // TODO: finish assignability/subtyping between two `@property` members, + // and between a `@property` member and a member of a different kind. + ( + ProtocolMemberKind::Property(_) + | ProtocolMemberKind::Method(_) + | ProtocolMemberKind::Other(_), + ProtocolMemberKind::Property(_), + ) => self.always(), + } + }, + ) + }) + } +} + +impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { + pub(super) fn protocol_member_has_disjoint_type_from_ty( + &self, + db: &'db dyn Db, + member: &ProtocolMember<'_, 'db>, + ty: Type<'db>, + ) -> ConstraintSet<'db, 'c> { + match &member.kind { + // TODO: implement disjointness for property/method members as well as attribute members + ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => self.never(), + ProtocolMemberKind::Other(other_type) => self.check_type_pair(db, ty, *other_type), + } + } } /// 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/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index b07ca0835b413..34fbde3bb0856 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -225,121 +225,6 @@ impl TypeRelation { } } -#[expect(clippy::too_many_arguments)] -fn optional_property_method_has_relation<'db, 'c>( - db: &'db dyn Db, - left: Option>, - right: Option>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, -) -> ConstraintSet<'db, 'c> { - match (left, right) { - (None, None) => ConstraintSet::from_bool(constraints, true), - (Some(left), Some(right)) => left.has_relation_to_impl( - db, - right, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - (None | Some(_), None | Some(_)) => ConstraintSet::from_bool(constraints, false), - } -} - -fn optional_property_method_is_disjoint<'db, 'c>( - db: &'db dyn Db, - left: Option>, - right: Option>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, -) -> ConstraintSet<'db, 'c> { - match (left, right) { - (None, None) => ConstraintSet::from_bool(constraints, false), - (Some(left), Some(right)) => left.is_disjoint_from_impl( - db, - right, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ), - (None | Some(_), None | Some(_)) => ConstraintSet::from_bool(constraints, true), - } -} - -#[expect(clippy::too_many_arguments)] -fn property_instance_has_relation<'db, 'c>( - db: &'db dyn Db, - left: PropertyInstanceType<'db>, - right: PropertyInstanceType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, -) -> ConstraintSet<'db, 'c> { - optional_property_method_has_relation( - db, - left.getter(db), - right.getter(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - .and(db, constraints, || { - optional_property_method_has_relation( - db, - left.setter(db), - right.setter(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) -} - -fn property_instance_is_disjoint<'db, 'c>( - db: &'db dyn Db, - left: PropertyInstanceType<'db>, - right: PropertyInstanceType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, -) -> ConstraintSet<'db, 'c> { - optional_property_method_is_disjoint( - db, - left.getter(db), - right.getter(db), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - .or(db, constraints, || { - optional_property_method_is_disjoint( - db, - left.setter(db), - right.setter(db), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) -} - #[salsa::tracked] impl<'db> Type<'db> { /// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for @@ -434,15 +319,14 @@ impl<'db> Type<'db> { constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db, 'c> { - self.has_relation_to_impl( - db, - target, + let checker = TypeRelationChecker { constraints, inferable, - TypeRelation::SubtypingAssuming, - &HasRelationToVisitor::with_given(constraints, assuming), - &IsDisjointVisitor::default(constraints), - ) + relation: TypeRelation::SubtypingAssuming, + relation_visitor: &HasRelationToVisitor::with_given(constraints, assuming), + disjointness_visitor: &IsDisjointVisitor::default(constraints), + }; + checker.check_type_pair(db, self, target) } /// Return true if this type is assignable to type `target`. @@ -507,12 +391,11 @@ impl<'db> Type<'db> { self_ty: Type<'db>, other: Type<'db>, ) -> bool { - let constraints = ConstraintSetBuilder::new(); self_ty .has_relation_to( db, other, - &constraints, + &ConstraintSetBuilder::new(), InferableTypeVars::None, TypeRelation::Redundancy { pure: false }, ) @@ -534,58 +417,203 @@ impl<'db> Type<'db> { inferable: InferableTypeVars<'_, 'db>, relation: TypeRelation, ) -> ConstraintSet<'db, 'c> { - self.has_relation_to_impl( - db, - target, + let checker = TypeRelationChecker { constraints, inferable, relation, - &HasRelationToVisitor::default(constraints), - &IsDisjointVisitor::default(constraints), - ) + relation_visitor: &HasRelationToVisitor::default(constraints), + disjointness_visitor: &IsDisjointVisitor::default(constraints), + }; + checker.check_type_pair(db, self, target) + } + + /// Return true if this type is [equivalent to] type `other`. + /// + /// Two equivalent types represent the same sets of values. + /// + /// > Two gradual types `A` and `B` are equivalent + /// > (that is, the same gradual type, not merely consistent with one another) + /// > if and only if all materializations of `A` are also materializations of `B`, + /// > and all materializations of `B` are also materializations of `A`. + /// > + /// > — [Summary of type relations] + /// + /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { + self.when_equivalent_to(db, other, &ConstraintSetBuilder::new()) + .is_always_satisfied(db) } - #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( + pub(crate) fn when_equivalent_to<'c>( self, db: &'db dyn Db, - target: Type<'db>, + other: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, + ) -> ConstraintSet<'db, 'c> { + let checker = EquivalenceChecker { + constraints, + relation_visitor: &HasRelationToVisitor::default(constraints), + disjointness_visitor: &IsDisjointVisitor::default(constraints), + }; + checker.check_type_pair(db, self, other) + } + + /// Return true if `self & other` should simplify to `Never`: + /// if the intersection of the two types could never be inhabited by any + /// possible runtime value. + /// + /// Our implementation of disjointness for non-fully-static types only + /// returns true if the *top materialization* of `self` has no overlap with + /// the *top materialization* of `other`. + /// + /// For example, `list[int]` is disjoint from `list[str]`: the two types have + /// no overlap. But `list[Any]` is not disjoint from `list[str]`: there exists + /// a fully static materialization of `list[Any]` (`list[str]`) that is a + /// subtype of `list[str]` + /// + /// This function aims to have no false positives, but might return wrong + /// `false` answers in some cases. + pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { + let constraints = ConstraintSetBuilder::new(); + self.when_disjoint_from(db, other, &constraints, InferableTypeVars::None) + .is_always_satisfied(db) + } + + pub(crate) fn when_disjoint_from<'c>( + self, + db: &'db dyn Db, + other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, + ) -> ConstraintSet<'db, 'c> { + let checker = DisjointnessChecker { + constraints, + inferable, + disjointness_visitor: &IsDisjointVisitor::default(constraints), + relation_visitor: &HasRelationToVisitor::default(constraints), + }; + checker.check_type_pair(db, self, other) + } +} + +/// A [`PairVisitor`] that is used in `has_relation_to` methods. +pub(crate) type HasRelationToVisitor<'db, 'c> = CycleDetector< + TypeRelation, + (Type<'db>, Type<'db>, TypeRelation), + ConstraintSet<'db, 'c>, + ConstraintSet<'db, 'c>, +>; + +impl<'db, 'c> HasRelationToVisitor<'db, 'c> { + pub(crate) fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { + HasRelationToVisitor::with_given(constraints, ConstraintSet::from_bool(constraints, false)) + } + + pub(crate) fn with_given( + constraints: &'c ConstraintSetBuilder<'db>, + given: ConstraintSet<'db, 'c>, + ) -> Self { + let fallback = ConstraintSet::from_bool(constraints, true); + HasRelationToVisitor::with_extra(fallback, given) + } +} + +/// A [`PairVisitor`] that is used in `is_disjoint_from` methods. +pub(crate) type IsDisjointVisitor<'db, 'c> = PairVisitor<'db, IsDisjoint, ConstraintSet<'db, 'c>>; + +#[derive(Debug)] +pub(crate) struct IsDisjoint; + +impl<'db, 'c> IsDisjointVisitor<'db, 'c> { + pub(crate) fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { + IsDisjointVisitor::new(ConstraintSet::from_bool(constraints, false)) + } +} + +#[derive(Clone)] +pub(super) struct TypeRelationChecker<'a, 'c, 'db> { + pub(super) constraints: &'c ConstraintSetBuilder<'db>, + pub(super) inferable: InferableTypeVars<'a, 'db>, + pub(super) relation: TypeRelation, + + // N.B. these fields are private to reduce the risk of + // "double-visiting" a given pair of types. You should + // generally only ever call `self.relation_visitor.visit()` + // or `self.disjointness_visitor.visit()` from + // `check_type_pair`, never from `check_typeddict_pair` or + // any other more "low-level" method. + relation_visitor: &'a HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, +} + +impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { + pub(super) fn new( + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'a, 'db>, relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &'a HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, + ) -> Self { + Self { + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + } + } + + pub(super) fn with_inferable_typevars(&self, inferable: InferableTypeVars<'a, 'db>) -> Self { + Self { + inferable, + ..self.clone() + } + } + + pub(super) fn always(&self) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(self.constraints, true) + } + + pub(super) fn never(&self) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(self.constraints, false) + } + + pub(super) fn check_type_pair( + &self, + db: &'db dyn Db, + source: Type<'db>, + target: Type<'db>, ) -> ConstraintSet<'db, 'c> { // Subtyping implies assignability, so if subtyping is reflexive and the two types are // equal, it is both a subtype and assignable. Assignability is always reflexive. // // Note that we could do a full equivalence check here, but that would be both expensive // and unnecessary. This early return is only an optimisation. - if relation.can_safely_assume_reflexivity(self) && self == target { - return ConstraintSet::from_bool(constraints, true); + if self.relation.can_safely_assume_reflexivity(source) && source == target { + return self.always(); } - // Handle constraint implication first. If either `self` or `target` is a typevar, check + // Handle constraint implication first. If either `source` or `target` is a typevar, check // the constraint set to see if the corresponding constraint is satisfied. - if relation == TypeRelation::SubtypingAssuming - && (self.is_type_var() || target.is_type_var()) + if self.relation == TypeRelation::SubtypingAssuming + && (source.is_type_var() || target.is_type_var()) { - let given = relation_visitor.extra; - return given.implies_subtype_of(db, constraints, self, target); + let given = self.relation_visitor.extra; + return given.implies_subtype_of(db, self.constraints, source, target); } - // Handle the new constraint-set-based assignability relation next. Comparisons with a + // Handle the constraint-set-based assignability relation next. Comparisons with a // typevar are translated directly into a constraint set. - if relation.is_constraint_set_assignability() { + if self.relation.is_constraint_set_assignability() { // A typevar satisfies a relation when...it satisfies the relation. Yes that's a // tautology! We're moving the caller's subtyping/assignability requirement into a // constraint set. If the typevar has an upper bound or constraints, then the relation // only has to hold when the typevar has a valid specialization (i.e., one that // satisfies the upper bound/constraints). - if let Type::TypeVar(bound_typevar) = self { + if let Type::TypeVar(bound_typevar) = source { return ConstraintSet::constrain_typevar( db, - constraints, + self.constraints, bound_typevar, Type::Never, target, @@ -593,31 +621,29 @@ impl<'db> Type<'db> { } else if let Type::TypeVar(bound_typevar) = target { return ConstraintSet::constrain_typevar( db, - constraints, + self.constraints, bound_typevar, - self, + source, Type::object(), ); } } - match (self, target) { + match (source, target) { // Everything is a subtype of `object`. - (_, Type::NominalInstance(instance)) if instance.is_object() => { - ConstraintSet::from_bool(constraints, true) - } + (_, Type::NominalInstance(target)) if target.is_object() => self.always(), (_, Type::ProtocolInstance(target)) if target.is_equivalent_to_object(db) => { - ConstraintSet::from_bool(constraints, true) + self.always() } // `Never` is the bottom type, the empty set. // It is a subtype of all other types. - (Type::Never, _) => ConstraintSet::from_bool(constraints, true), + (Type::Never, _) => self.always(), - (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) - if self_typevar.is_same_typevar_as(db, other_typevar) => + (Type::TypeVar(source_typevar), Type::TypeVar(target_typevar)) + if source_typevar.is_same_typevar_as(db, target_typevar) => { - ConstraintSet::from_bool(constraints, true) + self.always() } // In some specific situations, `Any`/`Unknown`/`@Todo` can be simplified out of unions and intersections, @@ -625,65 +651,41 @@ impl<'db> Type<'db> { // "too many cycle iterations" panics). (Type::Dynamic(DynamicType::Divergent(_)), _) | (_, Type::Dynamic(DynamicType::Divergent(_))) => { - ConstraintSet::from_bool(constraints, relation.is_assignability()) + ConstraintSet::from_bool(self.constraints, self.relation.is_assignability()) } - (Type::TypeAlias(self_alias), _) => { - relation_visitor.visit((self, target, relation), || { - self_alias.value_type(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } + (Type::TypeAlias(source_alias), _) => self + .relation_visitor + .visit((source, target, self.relation), || { + self.check_type_pair(db, source_alias.value_type(db), target) + }), - (_, Type::TypeAlias(target_alias)) => { - relation_visitor.visit((self, target, relation), || { - self.has_relation_to_impl( - db, - target_alias.value_type(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } + (_, Type::TypeAlias(target_alias)) => self + .relation_visitor + .visit((source, target, self.relation), || { + self.check_type_pair(db, source, target_alias.value_type(db)) + }), // Pretend that instances of `dataclasses.Field` are assignable to their default type. // This allows field definitions like `name: str = field(default="")` in dataclasses // to pass the assignability check of the inferred type to the declared type. (Type::KnownInstance(KnownInstanceType::Field(field)), right) - if relation.is_assignability() => + if self.relation.is_assignability() => { field .default_type(db) - .when_none_or(db, constraints, |default_type| { - default_type.has_relation_to_impl( - db, - right, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_none_or(db, self.constraints, |default_type| { + self.check_type_pair(db, default_type, target) }) } // Dynamic is only a subtype of `object` and only a supertype of `Never`; both were // handled above. It's always assignable, though. // - // Union simplification sits in between subtyping and assignability. `Any <: T` only - // holds true if `T` is also a dynamic type or a union that contains a dynamic type. - // Similarly, `T <: Any` only holds true if `T` is a dynamic type or an intersection - // that contains a dynamic type. + // Redundancy sits in between subtyping and assignability. `Any <: T` only holds true + // if `T` is also a dynamic type or a union that contains a dynamic type. Similarly, + // `T <: Any` only holds true if `T` is a dynamic type or an intersection that + // contains a dynamic type. (Type::Dynamic(dynamic), _) => { // If a `Divergent` type is involved, it must not be eliminated. debug_assert!( @@ -691,8 +693,8 @@ impl<'db> Type<'db> { "DynamicType::Divergent should have been handled in an earlier branch" ); ConstraintSet::from_bool( - constraints, - match relation { + self.constraints, + match self.relation { TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => false, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { true @@ -706,11 +708,11 @@ impl<'db> Type<'db> { ) } (_, Type::Dynamic(_)) => ConstraintSet::from_bool( - constraints, - match relation { + self.constraints, + match self.relation { TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => false, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => true, - TypeRelation::Redundancy { .. } => match self { + TypeRelation::Redundancy { .. } => match source { Type::Dynamic(_) => true, Type::Intersection(intersection) => { // If a `Divergent` type is involved, it must not be eliminated. @@ -732,33 +734,33 @@ impl<'db> Type<'db> { // However, there is one exception to this general rule: for any given typevar `T`, // `T` will always be a subtype of any union containing `T`. (_, Type::Union(union)) - if relation.can_safely_assume_reflexivity(self) - && union.elements(db).contains(&self) => + if self.relation.can_safely_assume_reflexivity(source) + && union.elements(db).contains(&source) => { - ConstraintSet::from_bool(constraints, true) + self.always() } // A similar rule applies in reverse to intersection types. (Type::Intersection(intersection), _) - if relation.can_safely_assume_reflexivity(target) + if self.relation.can_safely_assume_reflexivity(target) && intersection.positive(db).contains(&target) => { - ConstraintSet::from_bool(constraints, true) + self.always() } (Type::Intersection(intersection), _) - if relation.is_assignability() + if self.relation.is_assignability() && intersection.positive(db).iter().any(Type::is_dynamic) => { // If the intersection contains `Any`/`Unknown`/`@Todo`, it is assignable to any type. // `Any` could materialize to `Never`, `Never & T & ~S` simplifies to `Never` for any // `T` and any `S`, and `Never` is a subtype of all types. - ConstraintSet::from_bool(constraints, true) + self.always() } (Type::Intersection(intersection), _) - if relation.can_safely_assume_reflexivity(target) + if self.relation.can_safely_assume_reflexivity(target) && intersection.negative(db).contains(&target) => { - ConstraintSet::from_bool(constraints, false) + self.never() } // `type[T]` is a subtype of the class object `A` if every instance of `T` is a subtype of an instance @@ -767,16 +769,8 @@ impl<'db> Type<'db> { if !subclass_of .into_type_var() .zip(target.to_instance(db)) - .when_some_and(db, constraints, |(this_instance, other_instance)| { - Type::TypeVar(this_instance).has_relation_to_impl( - db, - other_instance, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_some_and(db, self.constraints, |(source_i, target_i)| { + self.check_type_pair(db, Type::TypeVar(source_i), target_i) }) .is_never_satisfied(db) => { @@ -784,50 +778,26 @@ impl<'db> Type<'db> { subclass_of .into_type_var() .zip(target.to_instance(db)) - .when_some_and(db, constraints, |(this_instance, other_instance)| { - Type::TypeVar(this_instance).has_relation_to_impl( - db, - other_instance, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_some_and(db, self.constraints, |(source_i, target_i)| { + self.check_type_pair(db, Type::TypeVar(source_i), target_i) }) } (_, Type::SubclassOf(subclass_of)) if !subclass_of .into_type_var() - .zip(self.to_instance(db)) - .when_some_and(db, constraints, |(other_instance, this_instance)| { - this_instance.has_relation_to_impl( - db, - Type::TypeVar(other_instance), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .zip(source.to_instance(db)) + .when_some_and(db, self.constraints, |(target_i, source_i)| { + self.check_type_pair(db, source_i, Type::TypeVar(target_i)) }) .is_never_satisfied(db) => { // TODO: The repetition here isn't great, but we need the fallthrough logic. subclass_of .into_type_var() - .zip(self.to_instance(db)) - .when_some_and(db, constraints, |(other_instance, this_instance)| { - this_instance.has_relation_to_impl( - db, - Type::TypeVar(other_instance), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .zip(source.to_instance(db)) + .when_some_and(db, self.constraints, |(target_i, source_i)| { + self.check_type_pair(db, source_i, Type::TypeVar(target_i)) }) } @@ -835,36 +805,19 @@ impl<'db> Type<'db> { // the union of its constraints. An unbound, unconstrained, fully static typevar has an // implicit upper bound of `object` (which is handled above). (Type::TypeVar(bound_typevar), _) - if !bound_typevar.is_inferable(db, inferable) + if !bound_typevar.is_inferable(db, self.inferable) && bound_typevar.typevar(db).bound_or_constraints(db).is_some() => { match bound_typevar.typevar(db).bound_or_constraints(db) { None => unreachable!(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound - .has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + self.check_type_pair(db, bound, target) + } Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { typevar_constraints.elements(db).iter().when_all( db, - constraints, - |constraint| { - constraint.has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }, + self.constraints, + |constraint| self.check_type_pair(db, *constraint, target), ) } } @@ -874,24 +827,14 @@ impl<'db> Type<'db> { // might be specialized to any one of them. However, the constraints do not have to be // disjoint, which means an lhs type might be a subtype of all of the constraints. (_, Type::TypeVar(bound_typevar)) - if !bound_typevar.is_inferable(db, inferable) + if !bound_typevar.is_inferable(db, self.inferable) && !bound_typevar .typevar(db) .constraints(db) - .when_some_and(db, constraints, |typevar_constraints| { - typevar_constraints - .iter() - .when_all(db, constraints, |constraint| { - self.has_relation_to_impl( - db, - *constraint, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + .when_some_and(db, self.constraints, |constraints| { + constraints.iter().when_all(db, self.constraints, |c| { + self.check_type_pair(db, source, *c) + }) }) .is_never_satisfied(db) => { @@ -901,32 +844,22 @@ impl<'db> Type<'db> { // able to simplify the typevar logic. bound_typevar.typevar(db).constraints(db).when_some_and( db, - constraints, - |typevar_constraints| { - typevar_constraints - .iter() - .when_all(db, constraints, |constraint| { - self.has_relation_to_impl( - db, - *constraint, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + self.constraints, + |constraints| { + constraints.iter().when_all(db, self.constraints, |c| { + self.check_type_pair(db, source, *c) + }) }, ) } - (Type::TypeVar(bound_typevar), _) if bound_typevar.is_inferable(db, inferable) => { + (Type::TypeVar(bound_typevar), _) if bound_typevar.is_inferable(db, self.inferable) => { // The implicit lower bound of a typevar is `Never`, which means // that it is always assignable to any other type. // TODO: record the unification constraints - ConstraintSet::from_bool(constraints, true) + self.always() } // Fast path for various types that we know `object` is never a subtype of @@ -938,21 +871,21 @@ impl<'db> Type<'db> { | Type::SubclassOf(_) | Type::Callable(_) | Type::ProtocolInstance(_), - ) if source.is_object() => ConstraintSet::from_bool(constraints, false), + ) if source.is_object() => self.never(), // Fast path: `object` is not a subtype of any non-inferable type variable, since the // type variable could be specialized to a type smaller than `object`. (Type::NominalInstance(source), Type::TypeVar(typevar)) - if source.is_object() && !typevar.is_inferable(db, inferable) => + if source.is_object() && !typevar.is_inferable(db, self.inferable) => { - ConstraintSet::from_bool(constraints, false) + self.never() } // `Never` is the bottom type, the empty set. - (_, Type::Never) => ConstraintSet::from_bool(constraints, false), + (_, Type::Never) => self.never(), - (Type::NewTypeInstance(self_newtype), Type::NewTypeInstance(target_newtype)) => { - self_newtype.has_relation_to_impl(db, target_newtype, constraints) + (Type::NewTypeInstance(source_newtype), Type::NewTypeInstance(target_newtype)) => { + self.check_newtype_pair(db, source_newtype, target_newtype) } // In the special cases of `NewType`s of `float` or `complex`, the concrete base type // can be a union (`int | float` or `int | float | complex`). For that reason, @@ -978,39 +911,23 @@ impl<'db> Type<'db> { // To handle both cases, we have to check that *either* `Foo` as a whole is assignable // (or subtypeable etc.) *or* that its concrete base type is. Note that this match arm // needs to take precedence over the `Type::Union` arms immediately below. - (Type::NewTypeInstance(self_newtype), Type::Union(union)) => { + (Type::NewTypeInstance(source_newtype), Type::Union(union)) => { // First the normal "assign to union" case, unfortunately duplicated from below. union .elements(db) .iter() - .when_any(db, constraints, |&elem_ty| { - self.has_relation_to_impl( - db, - elem_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_any(db, self.constraints, |&elem_ty| { + self.check_type_pair(db, source, elem_ty) }) // Failing that, if the concrete base type is a union, try delegating to that. // Otherwise, this would be equivalent to what we just checked, and we // shouldn't waste time checking it twice. - .or(db, constraints, || { - let concrete_base = self_newtype.concrete_base_type(db); + .or(db, self.constraints, || { + let concrete_base = source_newtype.concrete_base_type(db); if matches!(concrete_base, Type::Union(_)) { - concrete_base.has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_type_pair(db, concrete_base, target) } else { - ConstraintSet::from_bool(constraints, false) + self.never() } }) } @@ -1019,16 +936,8 @@ impl<'db> Type<'db> { union .elements(db) .iter() - .when_all(db, constraints, |&elem_ty| { - elem_ty.has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_all(db, self.constraints, |&elem_ty| { + self.check_type_pair(db, elem_ty, target) }) } @@ -1036,16 +945,8 @@ impl<'db> Type<'db> { union .elements(db) .iter() - .when_any(db, constraints, |&elem_ty| { - self.has_relation_to_impl( - db, - elem_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_any(db, self.constraints, |&elem_ty| { + self.check_type_pair(db, source, elem_ty) }) } @@ -1055,19 +956,11 @@ impl<'db> Type<'db> { (_, Type::Intersection(intersection)) => intersection .positive(db) .iter() - .when_all(db, constraints, |&pos_ty| { - self.has_relation_to_impl( - db, - pos_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_all(db, self.constraints, |&pos_ty| { + self.check_type_pair(db, source, pos_ty) }) - .and(db, constraints, || { - // For subtyping, we would want to check whether the *top materialization* of `self` + .and(db, self.constraints, || { + // For subtyping, we would want to check whether the *top materialization* of `source` // is disjoint from the *top materialization* of `neg_ty`. As an optimization, however, // we can avoid this explicit transformation here, since our `Type::is_disjoint_from` // implementation already only returns true for `T.is_disjoint_from(U)` if the *top @@ -1080,19 +973,19 @@ impl<'db> Type<'db> { // redundancy implementation: a fully complete implementation of redundancy may lead // to non-transitivity (highly undesirable); and pragmatically, a full implementation // of redundancy may not generally lead to simpler types in many situations. - let self_ty = match relation { + let source_ty = match self.relation { TypeRelation::Subtyping | TypeRelation::Redundancy { .. } - | TypeRelation::SubtypingAssuming => self, + | TypeRelation::SubtypingAssuming => source, TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => { - self.bottom_materialization(db) + source.bottom_materialization(db) } }; intersection .negative(db) .iter() - .when_all(db, constraints, |&neg_ty| { - let neg_ty = match relation { + .when_all(db, self.constraints, |&neg_ty| { + let neg_ty = match self.relation { TypeRelation::Subtyping | TypeRelation::Redundancy { .. } | TypeRelation::SubtypingAssuming => neg_ty, @@ -1101,14 +994,8 @@ impl<'db> Type<'db> { neg_ty.bottom_materialization(db) } }; - self_ty.is_disjoint_from_impl( - db, - neg_ty, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.as_disjointness_checker() + .check_type_pair(db, source_ty, neg_ty) }) }), @@ -1117,19 +1004,11 @@ impl<'db> Type<'db> { // positive elements is a subtype of that type. If there are no positive elements, // we treat `object` as the implicit positive element (e.g., `~str` is semantically // `object & ~str`). - intersection - .positive_elements_or_object(db) - .when_any(db, constraints, |elem_ty| { - elem_ty.has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + intersection.positive_elements_or_object(db).when_any( + db, + self.constraints, + |elem_ty| self.check_type_pair(db, elem_ty, target), + ) } // Other than the special cases checked above, no other types are a subtype of a @@ -1137,24 +1016,18 @@ impl<'db> Type<'db> { // (If the typevar is bounded, it might be specialized to a smaller type than the // bound. This is true even if the bound is a final class, since the typevar can still // be specialized to `Never`.) - (_, Type::TypeVar(bound_typevar)) if !bound_typevar.is_inferable(db, inferable) => { - ConstraintSet::from_bool(constraints, false) + (_, Type::TypeVar(bound_typevar)) + if !bound_typevar.is_inferable(db, self.inferable) => + { + self.never() } (_, Type::TypeVar(typevar)) - if typevar.is_inferable(db, inferable) - && relation.is_assignability() + if typevar.is_inferable(db, self.inferable) + && self.relation.is_assignability() && typevar.typevar(db).upper_bound(db).is_none_or(|bound| { !self - .has_relation_to_impl( - db, - bound, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .check_type_pair(db, source, bound) .is_never_satisfied(db) }) => { @@ -1163,126 +1036,79 @@ impl<'db> Type<'db> { typevar .typevar(db) .upper_bound(db) - .when_none_or(db, constraints, |bound| { - self.has_relation_to_impl( - db, - bound, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_none_or(db, self.constraints, |bound| { + self.check_type_pair(db, source, bound) }) } // TODO: Infer specializations here - (_, Type::TypeVar(bound_typevar)) if bound_typevar.is_inferable(db, inferable) => { - ConstraintSet::from_bool(constraints, false) + (_, Type::TypeVar(bound_typevar)) if bound_typevar.is_inferable(db, self.inferable) => { + self.never() } (Type::TypeVar(bound_typevar), _) => { // All inferable cases should have been handled above - assert!(!bound_typevar.is_inferable(db, inferable)); - ConstraintSet::from_bool(constraints, false) + assert!(!bound_typevar.is_inferable(db, self.inferable)); + self.never() } // All other `NewType` assignments fall back to the concrete base type. // This case must come after the TypeVar cases above, so that when checking // `NewType <: TypeVar`, we use the TypeVar handling rather than falling back // to the NewType's concrete base type. - (Type::NewTypeInstance(self_newtype), _) => { - self_newtype.concrete_base_type(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + (Type::NewTypeInstance(source_newtype), _) => { + self.check_type_pair(db, source_newtype.concrete_base_type(db), target) } // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. - (left, Type::AlwaysFalsy) => { - ConstraintSet::from_bool(constraints, left.bool(db).is_always_false()) + (_, Type::AlwaysFalsy) => { + ConstraintSet::from_bool(self.constraints, source.bool(db).is_always_false()) } - (left, Type::AlwaysTruthy) => { - ConstraintSet::from_bool(constraints, left.bool(db).is_always_true()) + (_, Type::AlwaysTruthy) => { + ConstraintSet::from_bool(self.constraints, source.bool(db).is_always_true()) } // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). - (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { - relation_visitor.visit((self, target, relation), || { - Type::object().has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } + (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => self + .relation_visitor + .visit((source, target, self.relation), || { + self.check_type_pair(db, Type::object(), target) + }), // These clauses handle type variants that include function literals. A function // literal is the subtype of itself, and not of any other function literal. However, // our representation of a function literal includes any specialization that should be // applied to the signature. Different specializations of the same function literal are // only subtypes of each other if they result in the same signature. - (Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => { - self_function.has_relation_to_impl( - db, - target_function, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + (Type::FunctionLiteral(source_function), Type::FunctionLiteral(target_function)) => { + self.check_function_pair(db, source_function, target_function) } - (Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => self_method - .has_relation_to_impl( - db, - target_method, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - (Type::KnownBoundMethod(self_method), Type::KnownBoundMethod(target_method)) => { - self_method.has_relation_to_impl( - db, - target_method, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + (Type::BoundMethod(source_method), Type::BoundMethod(target_method)) => { + self.check_bound_method_pair(db, source_method, target_method) + } + (Type::KnownBoundMethod(source_method), Type::KnownBoundMethod(target_method)) => { + self.check_known_bound_method_pair(db, source_method, target_method) } // All `StringLiteral` types are a subtype of `LiteralString`. - (Type::LiteralValue(this), Type::LiteralValue(target)) - if this.is_string() && target.is_literal_string() => + (Type::LiteralValue(source), Type::LiteralValue(target)) + if source.is_string() && target.is_literal_string() => { - ConstraintSet::from_bool(constraints, true) + self.always() } // For union simplification, we want to preserve the unpromotable form of a literal value, // and so redundancy is not symmetric. - (Type::LiteralValue(this), Type::LiteralValue(target)) - if matches!(relation, TypeRelation::Redundancy { pure: false }) => + (Type::LiteralValue(source), Type::LiteralValue(target)) + if matches!(self.relation, TypeRelation::Redundancy { pure: false }) => { ConstraintSet::from_bool( - constraints, - this.kind() == target.kind() && this.is_promotable(), + self.constraints, + source.kind() == target.kind() && source.is_promotable(), ) } - (Type::LiteralValue(this), Type::LiteralValue(target)) => { - ConstraintSet::from_bool(constraints, this.kind() == target.kind()) + (Type::LiteralValue(source), Type::LiteralValue(target)) => { + ConstraintSet::from_bool(self.constraints, source.kind() == target.kind()) } // No literal type is a subtype of any other literal type, unless they are the same @@ -1298,36 +1124,26 @@ impl<'db> Type<'db> { | Type::ClassLiteral(_) | Type::FunctionLiteral(_) | Type::ModuleLiteral(_), - ) => ConstraintSet::from_bool(constraints, false), + ) => self.never(), - (Type::Callable(self_callable), Type::Callable(other_callable)) => relation_visitor - .visit((self, target, relation), || { - self_callable.has_relation_to_impl( - db, - other_callable, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + (Type::Callable(source_callable), Type::Callable(target_callable)) => self + .relation_visitor + .visit((source, target, self.relation), || { + self.check_callable_pair(db, source_callable, target_callable) }), - (_, Type::Callable(other_callable)) => { - relation_visitor.visit((self, target, relation), || { - self.try_upcast_to_callable_with_policy(db, UpcastPolicy::from(relation)) - .when_some_and(db, constraints, |callables| { - callables.has_relation_to_impl( + (_, Type::Callable(target_callable)) => { + self.relation_visitor + .visit((source, target, self.relation), || { + source + .try_upcast_to_callable_with_policy( db, - other_callable, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, + UpcastPolicy::from(self.relation), ) - }) - }) + .when_some_and(db, self.constraints, |callables| { + self.check_callables_vs_callable(db, &callables, target_callable) + }) + }) } // `type[Any]` is assignable to arbitrary protocols as it has arbitrary attributes @@ -1336,71 +1152,43 @@ impl<'db> Type<'db> { // always be assignable to any protocol if `type[]` is assignable // to that protocol (handled lower down), but it is only a subtype of that protocol // if `type` is a subtype of that protocol. - (Type::SubclassOf(self_subclass_ty), Type::ProtocolInstance(_)) - if (self_subclass_ty.is_dynamic() || self_subclass_ty.is_type_var()) - && !relation.is_assignability() => + (Type::SubclassOf(source_subclass_ty), Type::ProtocolInstance(_)) + if (source_subclass_ty.is_dynamic() || source_subclass_ty.is_type_var()) + && !self.relation.is_assignability() => { - KnownClass::Type.to_instance(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_type_pair(db, KnownClass::Type.to_instance(db), target) } - (_, Type::ProtocolInstance(protocol)) => { - relation_visitor.visit((self, target, relation), || { - self.satisfies_protocol( - db, - protocol, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } + (_, Type::ProtocolInstance(target_proto)) => self + .relation_visitor + .visit((source, target, self.relation), || { + self.check_type_satisfies_protocol(db, source, target_proto) + }), // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`. - (Type::ProtocolInstance(_), _) => ConstraintSet::from_bool(constraints, false), + (Type::ProtocolInstance(_), _) => self.never(), - (Type::TypedDict(self_typeddict), Type::TypedDict(other_typeddict)) => relation_visitor - .visit((self, target, relation), || { - self_typeddict.has_relation_to_impl( - db, - other_typeddict, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + (Type::TypedDict(source_td), Type::TypedDict(target_td)) => self + .relation_visitor + .visit((source, target, self.relation), || { + self.check_typeddict_pair(db, source_td, target_td) }), // TODO: When we support `closed` and/or `extra_items`, we could allow assignments to other // compatible `Mapping`s. `extra_items` could also allow for some assignments to `dict`, as // long as `total=False`. (But then again, does anyone want a non-total `TypedDict` where all // key types are a supertype of the extra items type?) - (Type::TypedDict(_), _) => relation_visitor.visit((self, target, relation), || { - KnownClass::Mapping - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::object()]) - .has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), + (Type::TypedDict(_), _) => { + self.relation_visitor + .visit((source, target, self.relation), || { + let spec = &[KnownClass::Str.to_instance(db), Type::object()]; + let str_object_map = KnownClass::Mapping.to_specialized_instance(db, spec); + self.check_type_pair(db, str_object_map, target) + }) + } // A non-`TypedDict` cannot subtype a `TypedDict` - (_, Type::TypedDict(_)) => ConstraintSet::from_bool(constraints, false), + (_, Type::TypedDict(_)) => self.never(), // A string literal `Literal["abc"]` is assignable to `str` *and* to // `Sequence[Literal["a", "b", "c"]]` because strings are sequences of their characters. @@ -1408,10 +1196,10 @@ impl<'db> Type<'db> { if literal.is_string() => { let value = literal.as_string().unwrap(); - let other_class = instance.class(db); + let target_class = instance.class(db); - if other_class.is_known(db, KnownClass::Str) { - return ConstraintSet::from_bool(constraints, true); + if target_class.is_known(db, KnownClass::Str) { + return self.always(); } if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db) @@ -1419,9 +1207,9 @@ impl<'db> Type<'db> { .iter_mro(db, None) .filter_map(ClassBase::into_class) .map(|class| class.class_literal(db)) - .contains(&other_class.class_literal(db)) + .contains(&target_class.class_literal(db)) { - return ConstraintSet::from_bool(constraints, false); + return self.never(); } let chars: FxHashSet = value.value(db).chars().collect(); @@ -1443,22 +1231,12 @@ impl<'db> Type<'db> { KnownClass::Sequence .to_specialized_class_type(db, &[spec]) - .when_some_and(db, constraints, |sequence| { - sequence.has_relation_to_impl( - db, - other_class, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_some_and(db, self.constraints, |sequence| { + self.check_class_pair(db, sequence, target_class) }) } - (Type::LiteralValue(literal), _) if literal.is_string() => { - ConstraintSet::from_bool(constraints, false) - } + (Type::LiteralValue(literal), _) if literal.is_string() => self.never(), // A bytes literal `Literal[b"abc"]` is assignable to `bytes` *and* to // `Sequence[Literal[97, 98, 99]]` because bytes are sequences of integers. @@ -1466,10 +1244,10 @@ impl<'db> Type<'db> { if literal.is_bytes() => { let value = literal.as_bytes().unwrap(); - let other_class = instance.class(db); + let target_class = instance.class(db); - if other_class.is_known(db, KnownClass::Bytes) { - return ConstraintSet::from_bool(constraints, true); + if target_class.is_known(db, KnownClass::Bytes) { + return self.always(); } if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db) @@ -1477,9 +1255,9 @@ impl<'db> Type<'db> { .iter_mro(db, None) .filter_map(ClassBase::into_class) .map(|class| class.class_literal(db)) - .contains(&other_class.class_literal(db)) + .contains(&target_class.class_literal(db)) { - return ConstraintSet::from_bool(constraints, false); + return self.never(); } let ints: FxHashSet = value @@ -1500,335 +1278,175 @@ impl<'db> Type<'db> { KnownClass::Sequence .to_specialized_class_type(db, &[spec]) - .when_some_and(db, constraints, |sequence| { - sequence.has_relation_to_impl( - db, - other_class, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_some_and(db, self.constraints, |sequence| { + self.check_class_pair(db, sequence, target_class) }) } - (Type::LiteralValue(literal), _) if literal.is_bytes() => { - ConstraintSet::from_bool(constraints, false) - } + (Type::LiteralValue(literal), _) if literal.is_bytes() => self.never(), // An instance is a subtype of an enum literal, if it is an instance of the enum class // and the enum has only one member. (Type::NominalInstance(_), Type::LiteralValue(literal)) if literal.is_enum() => { let target_enum_literal = literal.as_enum().unwrap(); - if target_enum_literal.enum_class_instance(db) != self { - return ConstraintSet::from_bool(constraints, false); + if target_enum_literal.enum_class_instance(db) != source { + return self.never(); } ConstraintSet::from_bool( - constraints, + self.constraints, is_single_member_enum(db, target_enum_literal.enum_class(db)), ) } // Except for the special `BytesLiteral`, `LiteralString`, and string literal cases above, // most `Literal` types delegate to their instance fallbacks - // unless `self` is exactly equivalent to `target` (handled above) + // unless `source` is exactly equivalent to `target` (handled above) (Type::ModuleLiteral(_) | Type::LiteralValue(_) | Type::FunctionLiteral(_), _) => { - (self.literal_fallback_instance(db)).when_some_and(db, constraints, |instance| { - instance.has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + source.literal_fallback_instance(db).when_some_and( + db, + self.constraints, + |source_instance| self.check_type_pair(db, source_instance, target), + ) } // The same reasoning applies for these special callable types: (Type::BoundMethod(_), _) => { - KnownClass::MethodType.to_instance(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_type_pair(db, KnownClass::MethodType.to_instance(db), target) } (Type::KnownBoundMethod(method), _) => { - method.class().to_instance(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_type_pair(db, method.class().to_instance(db), target) } - (Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType - .to_instance(db) - .has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), + (Type::WrapperDescriptor(_), _) => self.check_type_pair( + db, + KnownClass::WrapperDescriptorType.to_instance(db), + target, + ), (Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => { // TODO: Implement subtyping using an equivalent `Callable` type. - ConstraintSet::from_bool(constraints, false) + self.never() } // `TypeIs` is invariant. - (Type::TypeIs(left), Type::TypeIs(right)) => left - .return_type(db) - .has_relation_to_impl( + (Type::TypeIs(source), Type::TypeIs(target)) => { + let source_return = source.return_type(db); + let target_return = target.return_type(db); + self.check_type_pair(db, source_return, target_return).and( db, - right.return_type(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, + self.constraints, + || self.check_type_pair(db, target_return, source_return), ) - .and(db, constraints, || { - right.return_type(db).has_relation_to_impl( - db, - left.return_type(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), + } // `TypeGuard` is covariant. - (Type::TypeGuard(left), Type::TypeGuard(right)) => { - left.return_type(db).has_relation_to_impl( - db, - right.return_type(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + (Type::TypeGuard(source), Type::TypeGuard(target)) => { + self.check_type_pair(db, source.return_type(db), target.return_type(db)) } // `TypeIs[T]` and `TypeGuard[T]` are subtypes of `bool`. (Type::TypeIs(_) | Type::TypeGuard(_), _) => { - KnownClass::Bool.to_instance(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_type_pair(db, KnownClass::Bool.to_instance(db), target) } // Function-like callables are subtypes of `FunctionType` (Type::Callable(callable), _) if callable.is_function_like(db) => { - KnownClass::FunctionType - .to_instance(db) - .has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_type_pair(db, KnownClass::FunctionType.to_instance(db), target) } - (Type::Callable(_), _) => ConstraintSet::from_bool(constraints, false), + (Type::Callable(_), _) => self.never(), - (Type::BoundSuper(left), Type::BoundSuper(right)) => left.is_equivalent_to_impl( - db, - right, - constraints, - relation_visitor, - disjointness_visitor, - ), - (Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), + (Type::BoundSuper(source), Type::BoundSuper(target)) => self + .as_equivalence_checker() + .check_bound_super_pair(db, source, target), + + (Type::BoundSuper(_), _) => { + self.check_type_pair(db, KnownClass::Super.to_instance(db), target) + } (Type::SubclassOf(subclass_of), _) | (_, Type::SubclassOf(subclass_of)) if subclass_of.is_type_var() => { - ConstraintSet::from_bool(constraints, false) + self.never() } // `Literal[]` is a subtype of `type[B]` if `C` is a subclass of `B`, // since `type[B]` describes all possible runtime subclasses of the class object `B`. - (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty - .subclass_of() - .into_class(db) - .map(|subclass_of_class| { - class.default_specialization(db).has_relation_to_impl( - db, - subclass_of_class, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - .unwrap_or_else(|| { - ConstraintSet::from_bool(constraints, relation.is_assignability()) - }), + (Type::ClassLiteral(source_cls), Type::SubclassOf(target_subclass_ty)) => { + target_subclass_ty + .subclass_of() + .into_class(db) + .map(|target_cls| { + self.check_class_pair(db, source_cls.default_specialization(db), target_cls) + }) + .unwrap_or_else(|| { + ConstraintSet::from_bool(self.constraints, self.relation.is_assignability()) + }) + } // Similarly, `` is assignable to `` (a generic-alias type) // if the default specialization of `C` is assignable to `C[...]`. This scenario occurs // with final generic types, where `type[C[...]]` is simplified to the generic-alias // type ``, due to the fact that `C[...]` has no subclasses. - (Type::ClassLiteral(class), Type::GenericAlias(target_alias)) => { - class.default_specialization(db).has_relation_to_impl( + (Type::ClassLiteral(source_cls), Type::GenericAlias(target_alias)) => self + .check_class_pair( db, + source_cls.default_specialization(db), ClassType::Generic(target_alias), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } + ), // For generic aliases, we delegate to the underlying class type. - (Type::GenericAlias(self_alias), Type::GenericAlias(target_alias)) => { - ClassType::Generic(self_alias).has_relation_to_impl( + (Type::GenericAlias(source_alias), Type::GenericAlias(target_alias)) => self + .check_class_pair( db, + ClassType::Generic(source_alias), ClassType::Generic(target_alias), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } + ), - (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty - .subclass_of() - .into_class(db) - .map(|subclass_of_class| { - ClassType::Generic(alias).has_relation_to_impl( - db, - subclass_of_class, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - .unwrap_or_else(|| { - ConstraintSet::from_bool(constraints, relation.is_assignability()) - }), + (Type::GenericAlias(source_alias), Type::SubclassOf(target_subclass_ty)) => { + target_subclass_ty + .subclass_of() + .into_class(db) + .map(|target_cls| { + self.check_class_pair(db, ClassType::Generic(source_alias), target_cls) + }) + .unwrap_or_else(|| { + ConstraintSet::from_bool(self.constraints, self.relation.is_assignability()) + }) + } // This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`? - (Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => { - self_subclass_ty.has_relation_to_impl( - db, - target_subclass_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + (Type::SubclassOf(source), Type::SubclassOf(target)) => { + self.check_subclassof_pair(db, source, target) } // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object // is an instance of its metaclass `abc.ABCMeta`. - (Type::ClassLiteral(class), _) => { - class.metaclass_instance_type(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + (Type::ClassLiteral(source_class), _) => { + self.check_type_pair(db, source_class.metaclass_instance_type(db), target) } - (Type::GenericAlias(alias), _) => ClassType::from(alias) - .metaclass_instance_type(db) - .has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), + (Type::GenericAlias(source_alias), _) => self.check_type_pair( + db, + ClassType::Generic(source_alias).metaclass_instance_type(db), + target, + ), // `type[Any]` is a subtype of `type[object]`, and is assignable to any `type[...]` - (Type::SubclassOf(subclass_of_ty), other) if subclass_of_ty.is_dynamic() => { - KnownClass::Type - .to_instance(db) - .has_relation_to_impl( - db, - other, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - .or(db, constraints, || { - ConstraintSet::from_bool(constraints, relation.is_assignability()).and( - db, - constraints, - || { - other.has_relation_to_impl( - db, - KnownClass::Type.to_instance(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }, - ) - }) - } + (Type::SubclassOf(subclass_of_ty), _) if subclass_of_ty.is_dynamic() => self + .check_type_pair(db, KnownClass::Type.to_instance(db), target) + .or(db, self.constraints, || { + ConstraintSet::from_bool(self.constraints, self.relation.is_assignability()) + .and(db, self.constraints, || { + self.check_type_pair(db, target, KnownClass::Type.to_instance(db)) + }) + }), // Any `type[...]` type is assignable to `type[Any]` - (other, Type::SubclassOf(subclass_of_ty)) - if subclass_of_ty.is_dynamic() && relation.is_assignability() => + (_, Type::SubclassOf(subclass_of_ty)) + if subclass_of_ty.is_dynamic() && self.relation.is_assignability() => { - other.has_relation_to_impl( - db, - KnownClass::Type.to_instance(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_type_pair(db, source, KnownClass::Type.to_instance(db)) } // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses @@ -1838,298 +1456,262 @@ impl<'db> Type<'db> { // Similarly `type[enum.Enum]` is a subtype of `enum.EnumMeta` because `enum.Enum` // is an instance of `enum.EnumMeta`. `type[Any]` and `type[Unknown]` do not participate in subtyping, // however, as they are not fully static types. - (Type::SubclassOf(subclass_of_ty), _) => subclass_of_ty - .subclass_of() - .into_class(db) - .map(|class| class.metaclass_instance_type(db)) - .unwrap_or_else(|| KnownClass::Type.to_instance(db)) - .has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), + (Type::SubclassOf(subclass_of_ty), _) => self.check_type_pair( + db, + subclass_of_ty + .subclass_of() + .into_class(db) + .map(|source_class| source_class.metaclass_instance_type(db)) + .unwrap_or_else(|| KnownClass::Type.to_instance(db)), + target, + ), // For example: `Type::SpecialForm(SpecialFormType::Type)` is a subtype of `Type::NominalInstance(_SpecialForm)`, // because `Type::SpecialForm(SpecialFormType::Type)` is a set with exactly one runtime value in it // (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime. - (Type::SpecialForm(left), right) => left.instance_fallback(db).has_relation_to_impl( - db, - right, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), + (Type::SpecialForm(source_form), _) => { + self.check_type_pair(db, source_form.instance_fallback(db), target) + } - (Type::KnownInstance(left), right) => left.instance_fallback(db).has_relation_to_impl( - db, - right, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), + (Type::KnownInstance(source), _) => { + self.check_type_pair(db, source.instance_fallback(db), target) + } // `bool` is a subtype of `int`, because `bool` subclasses `int`, // which means that all instances of `bool` are also instances of `int` - (Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => { - relation_visitor.visit((self, target, relation), || { - self_instance.has_relation_to_impl( - db, - target_instance, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } + (Type::NominalInstance(source_i), Type::NominalInstance(target_i)) => self + .relation_visitor + .visit((source, target, self.relation), || { + self.check_nominal_instance_pair(db, source_i, target_i) + }), - (Type::PropertyInstance(self_property), Type::PropertyInstance(target_property)) => { - relation_visitor.visit((self, target, relation), || { - property_instance_has_relation( - db, - self_property, - target_property, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } + (Type::PropertyInstance(source_p), Type::PropertyInstance(target_p)) => self + .relation_visitor + .visit((source, target, self.relation), || { + self.check_property_instance_pair(db, source_p, target_p) + }), (Type::PropertyInstance(_), _) => { - KnownClass::Property.to_instance(db).has_relation_to_impl( - db, - target, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_type_pair(db, KnownClass::Property.to_instance(db), target) + } + (_, Type::PropertyInstance(_)) => { + self.check_type_pair(db, source, KnownClass::Property.to_instance(db)) } - (_, Type::PropertyInstance(_)) => self.has_relation_to_impl( - db, - KnownClass::Property.to_instance(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - // Other than the special cases enumerated above, nominal-instance types are never // subtypes of any other variants - (Type::NominalInstance(_), _) => ConstraintSet::from_bool(constraints, false), + (Type::NominalInstance(_), _) => self.never(), } } - /// Return true if this type is [equivalent to] type `other`. - /// - /// Two equivalent types represent the same sets of values. - /// - /// > Two gradual types `A` and `B` are equivalent - /// > (that is, the same gradual type, not merely consistent with one another) - /// > if and only if all materializations of `A` are also materializations of `B`, - /// > and all materializations of `B` are also materializations of `A`. - /// > - /// > — [Summary of type relations] - /// - /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { - let constraints = ConstraintSetBuilder::new(); - self.when_equivalent_to(db, other, &constraints) - .is_always_satisfied(db) + pub(super) fn check_property_instance_pair( + &self, + db: &'db dyn Db, + source: PropertyInstanceType<'db>, + target: PropertyInstanceType<'db>, + ) -> ConstraintSet<'db, 'c> { + self.check_optional_property_method_pair(db, source.getter(db), target.getter(db)) + .and(db, self.constraints, || { + self.check_optional_property_method_pair(db, source.setter(db), target.setter(db)) + }) } - pub(crate) fn when_equivalent_to<'c>( - self, + fn check_optional_property_method_pair( + &self, db: &'db dyn Db, - other: Type<'db>, - constraints: &'c ConstraintSetBuilder<'db>, + source: Option>, + target: Option>, ) -> ConstraintSet<'db, 'c> { - let relation_visitor = HasRelationToVisitor::default(constraints); - let disjointness_visitor = IsDisjointVisitor::default(constraints); - self.when_equivalent_to_impl( - db, - other, - constraints, - &relation_visitor, - &disjointness_visitor, - ) + match (source, target) { + (None, None) => self.always(), + (Some(source), Some(target)) => self.check_type_pair(db, source, target), + (None | Some(_), None | Some(_)) => self.never(), + } } - pub(crate) fn when_equivalent_to_impl<'c>( - self, + pub(super) fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { + EquivalenceChecker { + constraints: self.constraints, + relation_visitor: self.relation_visitor, + disjointness_visitor: self.disjointness_visitor, + } + } + + pub(super) fn as_disjointness_checker(&self) -> DisjointnessChecker<'_, 'c, 'db> { + DisjointnessChecker { + constraints: self.constraints, + inferable: self.inferable, + relation_visitor: self.relation_visitor, + disjointness_visitor: self.disjointness_visitor, + } + } +} + +pub(super) struct EquivalenceChecker<'a, 'c, 'db> { + pub(super) constraints: &'c ConstraintSetBuilder<'db>, + + // N.B. these fields are private to reduce the risk of + // "double-visiting" a given pair of types. You should + // generally only ever call `self.relation_visitor.visit()` + // or `self.disjointness_visitor.visit()` from + // `check_type_pair`, never from `check_typeddict_pair` or + // any other more "low-level" method. + relation_visitor: &'a HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, +} + +impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { + fn as_relation_checker(&self) -> TypeRelationChecker<'_, 'c, 'db> { + TypeRelationChecker { + relation: TypeRelation::Redundancy { pure: true }, + constraints: self.constraints, + inferable: InferableTypeVars::None, + relation_visitor: self.relation_visitor, + disjointness_visitor: self.disjointness_visitor, + } + } + + pub(super) fn always(&self) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(self.constraints, true) + } + + pub(super) fn never(&self) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(self.constraints, false) + } + + pub(super) fn check_type_pair( + &self, db: &'db dyn Db, - other: Type<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + left: Type<'db>, + right: Type<'db>, ) -> ConstraintSet<'db, 'c> { - self.has_relation_to_impl( - db, - other, + let relation_checker = self.as_relation_checker(); + relation_checker + .check_type_pair(db, left, right) + .and(db, self.constraints, || { + relation_checker.check_type_pair(db, right, left) + }) + } +} + +pub(super) struct DisjointnessChecker<'a, 'c, 'db> { + pub(super) constraints: &'c ConstraintSetBuilder<'db>, + pub(super) inferable: InferableTypeVars<'a, 'db>, + + // N.B. these fields are private to reduce the risk of + // "double-visiting" a given pair of types. You should + // generally only ever call `self.relation_visitor.visit()` + // or `self.disjointness_visitor.visit()` from + // `check_type_pair`, never from `check_typeddict_pair` or + // any other more "low-level" method. + disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, + relation_visitor: &'a HasRelationToVisitor<'db, 'c>, +} + +impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { + pub(super) fn new( + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'a, 'db>, + relation_visitor: &'a HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, + ) -> Self { + Self { constraints, - InferableTypeVars::None, - TypeRelation::Redundancy { pure: true }, - relation_visitor, + inferable, disjointness_visitor, - ) - .and(db, constraints, || { - other.has_relation_to_impl( - db, - self, - constraints, - InferableTypeVars::None, - TypeRelation::Redundancy { pure: true }, - relation_visitor, - disjointness_visitor, - ) - }) + relation_visitor, + } } - /// Return true if `self & other` should simplify to `Never`: - /// if the intersection of the two types could never be inhabited by any - /// possible runtime value. - /// - /// Our implementation of disjointness for non-fully-static types only - /// returns true if the *top materialization* of `self` has no overlap with - /// the *top materialization* of `other`. - /// - /// For example, `list[int]` is disjoint from `list[str]`: the two types have - /// no overlap. But `list[Any]` is not disjoint from `list[str]`: there exists - /// a fully static materialization of `list[Any]` (`list[str]`) that is a - /// subtype of `list[str]` - /// - /// This function aims to have no false positives, but might return wrong - /// `false` answers in some cases. - pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { - let constraints = ConstraintSetBuilder::new(); - self.when_disjoint_from(db, other, &constraints, InferableTypeVars::None) - .is_always_satisfied(db) + pub(super) fn as_relation_checker( + &self, + relation: TypeRelation, + ) -> TypeRelationChecker<'_, 'c, 'db> { + TypeRelationChecker { + relation, + constraints: self.constraints, + inferable: self.inferable, + relation_visitor: self.relation_visitor, + disjointness_visitor: self.disjointness_visitor, + } } - pub(crate) fn when_disjoint_from<'c>( - self, + pub(super) fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { + EquivalenceChecker { + constraints: self.constraints, + relation_visitor: self.relation_visitor, + disjointness_visitor: self.disjointness_visitor, + } + } + + fn any_protocol_members_absent_or_disjoint( + &self, db: &'db dyn Db, + protocol: ProtocolInstanceType<'db>, other: Type<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db, 'c> { - self.is_disjoint_from_impl( - db, - other, - constraints, - inferable, - &IsDisjointVisitor::default(constraints), - &HasRelationToVisitor::default(constraints), - ) + protocol + .interface(db) + .members(db) + .when_any(db, self.constraints, |member| { + other + .member(db, member.name()) + .place + .ignore_possibly_undefined() + .when_none_or(db, self.constraints, |attribute_type| { + self.protocol_member_has_disjoint_type_from_ty(db, &member, attribute_type) + }) + }) } - pub(crate) fn is_disjoint_from_impl<'c>( - self, + pub(super) fn always(&self) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(self.constraints, true) + } + + pub(super) fn never(&self) -> ConstraintSet<'db, 'c> { + ConstraintSet::from_bool(self.constraints, false) + } + + pub(super) fn check_type_pair( + &self, db: &'db dyn Db, - other: Type<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, + left: Type<'db>, + right: Type<'db>, ) -> ConstraintSet<'db, 'c> { - fn any_protocol_members_absent_or_disjoint<'db, 'c>( - db: &'db dyn Db, - protocol: ProtocolInstanceType<'db>, - other: Type<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - protocol - .interface(db) - .members(db) - .when_any(db, constraints, |member| { - other - .member(db, member.name()) - .place - .ignore_possibly_undefined() - .when_none_or(db, constraints, |attribute_type| { - member.has_disjoint_type_from( - db, - attribute_type, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) - }) - } - - match (self, other) { - (Type::Never, _) | (_, Type::Never) => ConstraintSet::from_bool(constraints, true), + match (left, right) { + (Type::Never, _) | (_, Type::Never) => self.always(), - (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => { - ConstraintSet::from_bool(constraints, false) - } + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => self.never(), (Type::TypeAlias(alias), _) => { - let self_alias_ty = alias.value_type(db); - disjointness_visitor.visit((self, other), || { - self_alias_ty.is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + let left_alias_ty = alias.value_type(db); + self.disjointness_visitor.visit((left, right), || { + self.check_type_pair(db, left_alias_ty, right) }) } (_, Type::TypeAlias(alias)) => { - let other_alias_ty = alias.value_type(db); - disjointness_visitor.visit((self, other), || { - self.is_disjoint_from_impl( - db, - other_alias_ty, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + let right_alias_ty = alias.value_type(db); + self.disjointness_visitor.visit((left, right), || { + self.check_type_pair(db, left, right_alias_ty) }) } // `type[T]` is disjoint from a callable or protocol instance if its upper bound or constraints are. - (Type::SubclassOf(subclass_of), Type::Callable(_) | Type::ProtocolInstance(_)) - | (Type::Callable(_) | Type::ProtocolInstance(_), Type::SubclassOf(subclass_of)) - if subclass_of.is_type_var() => - { + ( + Type::SubclassOf(subclass_of), + other @ (Type::Callable(_) | Type::ProtocolInstance(_)), + ) + | ( + other @ (Type::Callable(_) | Type::ProtocolInstance(_)), + Type::SubclassOf(subclass_of), + ) if subclass_of.is_type_var() => { let type_var = subclass_of .subclass_of() .with_transposed_type_var(db) .into_type_var() .unwrap(); - Type::TypeVar(type_var).is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, Type::TypeVar(type_var), other) } // `type[T]` is disjoint from a class object `A` if every instance of `T` is disjoint from an instance of `A`. @@ -2137,15 +1719,8 @@ impl<'db> Type<'db> { if !subclass_of .into_type_var() .zip(other.to_instance(db)) - .when_none_or(db, constraints, |(this_instance, other_instance)| { - Type::TypeVar(this_instance).is_disjoint_from_impl( - db, - other_instance, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + .when_none_or(db, self.constraints, |(this_instance, other_instance)| { + self.check_type_pair(db, Type::TypeVar(this_instance), other_instance) }) .is_always_satisfied(db) => { @@ -2153,15 +1728,8 @@ impl<'db> Type<'db> { subclass_of .into_type_var() .zip(other.to_instance(db)) - .when_none_or(db, constraints, |(this_instance, other_instance)| { - Type::TypeVar(this_instance).is_disjoint_from_impl( - db, - other_instance, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + .when_none_or(db, self.constraints, |(this_instance, other_instance)| { + self.check_type_pair(db, Type::TypeVar(this_instance), other_instance) }) } @@ -2169,170 +1737,111 @@ impl<'db> Type<'db> { // be specialized to the same type. (This is an important difference between typevars // and `Any`!) Different typevars might be disjoint, depending on their bounds and // constraints, which are handled below. - (Type::TypeVar(self_bound_typevar), Type::TypeVar(other_bound_typevar)) - if !self_bound_typevar.is_inferable(db, inferable) - && self_bound_typevar.is_same_typevar_as(db, other_bound_typevar) => + (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar)) + if !left_tvar.is_inferable(db, self.inferable) + && left_tvar.is_same_typevar_as(db, right_tvar) => { - ConstraintSet::from_bool(constraints, false) + self.never() } - (tvar @ Type::TypeVar(bound_typevar), Type::Intersection(intersection)) - | (Type::Intersection(intersection), tvar @ Type::TypeVar(bound_typevar)) - if !bound_typevar.is_inferable(db, inferable) - && intersection.negative(db).contains(&tvar) => + (Type::TypeVar(tvar), Type::Intersection(intersection)) + | (Type::Intersection(intersection), Type::TypeVar(tvar)) + if !tvar.is_inferable(db, self.inferable) + && intersection.negative(db).contains(&Type::TypeVar(tvar)) => { - ConstraintSet::from_bool(constraints, true) + self.always() } // An unbounded typevar is never disjoint from any other type, since it might be // specialized to any type. A bounded typevar is not disjoint from its bound, and is // only disjoint from other types if its bound is. A constrained typevar is disjoint // from a type if all of its constraints are. - (Type::TypeVar(bound_typevar), other) | (other, Type::TypeVar(bound_typevar)) - if !bound_typevar.is_inferable(db, inferable) => + (Type::TypeVar(tvar), other) | (other, Type::TypeVar(tvar)) + if !tvar.is_inferable(db, self.inferable) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => ConstraintSet::from_bool(constraints, false), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound - .is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ), + match tvar.typevar(db).bound_or_constraints(db) { + None => self.never(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + self.check_type_pair(db, bound, other) + } Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { typevar_constraints.elements(db).iter().when_all( db, - constraints, - |constraint| { - constraint.is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - }, + self.constraints, + |constraint| self.check_type_pair(db, *constraint, other), ) } } } // TODO: Infer specializations here - (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => { - ConstraintSet::from_bool(constraints, false) - } + (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => self.never(), - (Type::Union(union), other) | (other, Type::Union(union)) => { - union.elements(db).iter().when_all(db, constraints, |e| { - e.is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) - } + (Type::Union(union), other) | (other, Type::Union(union)) => union + .elements(db) + .iter() + .when_all(db, self.constraints, |e| { + self.check_type_pair(db, *e, other) + }), // If we have two intersections, we test the positive elements of each one against the other intersection // Negative elements need a positive element on the other side in order to be disjoint. // This is similar to what would happen if we tried to build a new intersection that combines the two - (Type::Intersection(self_intersection), Type::Intersection(other_intersection)) => { - disjointness_visitor.visit((self, other), || { - self_intersection + (Type::Intersection(left_intersection), Type::Intersection(right_intersection)) => { + self.disjointness_visitor.visit((left, right), || { + left_intersection .positive(db) .iter() - .when_any(db, constraints, |p| { - p.is_disjoint_from_impl( + .when_any(db, self.constraints, |&pos_ty| { + self.check_type_pair(db, pos_ty, right) + }) + .or(db, self.constraints, || { + right_intersection.positive(db).iter().when_any( db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, + self.constraints, + |&pos_ty| self.check_type_pair(db, pos_ty, left), ) }) - .or(db, constraints, || { - other_intersection - .positive(db) - .iter() - .when_any(db, constraints, |p| { - p.is_disjoint_from_impl( - db, - self, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) - }) }) } - (Type::Intersection(intersection), non_intersection) - | (non_intersection, Type::Intersection(intersection)) => { - disjointness_visitor.visit((self, other), || { + (Type::Intersection(intersection), other) + | (other, Type::Intersection(intersection)) => { + self.disjointness_visitor.visit((left, right), || { intersection .positive(db) .iter() - .when_any(db, constraints, |p| { - p.is_disjoint_from_impl( - db, - non_intersection, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + .when_any(db, self.constraints, |&pos_ty| { + self.check_type_pair(db, pos_ty, other) }) // A & B & Not[C] is disjoint from C - .or(db, constraints, || { - intersection - .negative(db) - .iter() - .when_any(db, constraints, |&neg_ty| { - non_intersection.has_relation_to_impl( - db, - neg_ty, - constraints, - inferable, - TypeRelation::Subtyping, - relation_visitor, - disjointness_visitor, - ) - }) + .or(db, self.constraints, || { + intersection.negative(db).iter().when_any( + db, + self.constraints, + |&neg_ty| { + self.as_relation_checker(TypeRelation::Subtyping) + .check_type_pair(db, other, neg_ty) + }, + ) }) }) } - (Type::LiteralValue(this), Type::LiteralValue(target)) - if this.is_literal_string() && target.is_literal_string() - || (this.is_string() && target.is_literal_string()) - || (this.is_literal_string() && target.is_string()) => + (Type::LiteralValue(left), Type::LiteralValue(right)) + if left.is_literal_string() && right.is_literal_string() + || (left.is_string() && right.is_literal_string()) + || (left.is_literal_string() && right.is_string()) => { - ConstraintSet::from_bool(constraints, false) + self.never() } (Type::LiteralValue(left), Type::LiteralValue(right)) => { - ConstraintSet::from_bool(constraints, left.kind() != right.kind()) + ConstraintSet::from_bool(self.constraints, left.kind() != right.kind()) } (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { - property_instance_is_disjoint( - db, - left, - right, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_property_instance_pair(db, left, right) } ( @@ -2342,15 +1851,7 @@ impl<'db> Type<'db> { | ( Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(left)), Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(right)), - ) => property_instance_is_disjoint( - db, - left, - right, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ), + ) => self.check_property_instance_pair(db, left, right), // any single-valued type is disjoint from another single-valued type // iff the two types are nonequal @@ -2372,7 +1873,7 @@ impl<'db> Type<'db> { | Type::ClassLiteral(..) | Type::SpecialForm(..) | Type::KnownInstance(..)), - ) => ConstraintSet::from_bool(constraints, left != right), + ) => ConstraintSet::from_bool(self.constraints, left != right), ( Type::SubclassOf(_), @@ -2391,55 +1892,42 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..), Type::SubclassOf(_), - ) => ConstraintSet::from_bool(constraints, true), + ) => self.always(), (Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => { // `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint. // Thus, they are only disjoint if `ty.bool() == AlwaysFalse`. - ConstraintSet::from_bool(constraints, ty.bool(db).is_always_false()) + ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_false()) } (Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => { // Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`. - ConstraintSet::from_bool(constraints, ty.bool(db).is_always_true()) + ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_true()) } - (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => disjointness_visitor - .visit((self, other), || { - left.is_disjoint_from_impl( - db, - right, - constraints, - inferable, - disjointness_visitor, - ) - }), + (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => { + self.disjointness_visitor.visit((left, right), || { + self.check_protocol_instance_pair(db, left_proto, right_proto) + }) + } (Type::ProtocolInstance(protocol), Type::SpecialForm(special_form)) | (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => { - disjointness_visitor.visit((self, other), || { - any_protocol_members_absent_or_disjoint( + self.disjointness_visitor.visit((left, right), || { + self.any_protocol_members_absent_or_disjoint( db, protocol, special_form.instance_fallback(db), - constraints, - inferable, - disjointness_visitor, - relation_visitor, ) }) } (Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance)) | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => { - disjointness_visitor.visit((self, other), || { - any_protocol_members_absent_or_disjoint( + self.disjointness_visitor.visit((left, right), || { + self.any_protocol_members_absent_or_disjoint( db, protocol, known_instance.instance_fallback(db), - constraints, - inferable, - disjointness_visitor, - relation_visitor, ) }) } @@ -2486,57 +1974,42 @@ impl<'db> Type<'db> { | Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::GenericAlias(..)), - ) => disjointness_visitor.visit((self, other), || { - any_protocol_members_absent_or_disjoint( - db, - protocol, - ty, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + ) => self.disjointness_visitor.visit((left, right), || { + self.any_protocol_members_absent_or_disjoint(db, protocol, ty) }), // This is the same as the branch above -- // once guard patterns are stabilised, it could be unified with that branch // () - (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) - | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) - if n.class(db).is_final(db) => + (Type::ProtocolInstance(protocol), Type::NominalInstance(nominal)) + | (Type::NominalInstance(nominal), Type::ProtocolInstance(protocol)) + if nominal.class(db).is_final(db) => { - disjointness_visitor.visit((self, other), || { - any_protocol_members_absent_or_disjoint( + self.disjointness_visitor.visit((left, right), || { + self.any_protocol_members_absent_or_disjoint( db, protocol, - nominal, - constraints, - inferable, - disjointness_visitor, - relation_visitor, + Type::NominalInstance(nominal), ) }) } (Type::ProtocolInstance(protocol), other) | (other, Type::ProtocolInstance(protocol)) => { - disjointness_visitor.visit((self, other), || { + self.disjointness_visitor.visit((left, right), || { protocol .interface(db) .members(db) - .when_any(db, constraints, |member| { + .when_any(db, self.constraints, |member| { match other.member(db, member.name()).place { Place::Defined(DefinedPlace { ty: attribute_type, .. - }) => member.has_disjoint_type_from( + }) => self.protocol_member_has_disjoint_type_from_ty( db, + &member, attribute_type, - constraints, - inferable, - disjointness_visitor, - relation_visitor, ), - Place::Undefined => ConstraintSet::from_bool(constraints, false), + Place::Undefined => self.never(), } }) }) @@ -2545,51 +2018,41 @@ impl<'db> Type<'db> { (Type::SubclassOf(subclass_of_ty), _) | (_, Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_type_var() => { - ConstraintSet::from_bool(constraints, true) + self.always() } (Type::GenericAlias(left_alias), Type::GenericAlias(right_alias)) => { ConstraintSet::from_bool( - constraints, + self.constraints, left_alias.origin(db) != right_alias.origin(db), ) - .or(db, constraints, || { - left_alias.specialization(db).is_disjoint_from_impl( + .or(db, self.constraints, || { + self.check_specialization_pair( db, + left_alias.specialization(db), right_alias.specialization(db), - constraints, - inferable, - disjointness_visitor, - relation_visitor, ) }) } - (Type::ClassLiteral(class_literal), other @ Type::GenericAlias(_)) - | (other @ Type::GenericAlias(_), Type::ClassLiteral(class_literal)) => class_literal + (Type::ClassLiteral(class), Type::GenericAlias(alias_b)) + | (Type::GenericAlias(alias_b), Type::ClassLiteral(class)) => class .default_specialization(db) .into_generic_alias() - .when_none_or(db, constraints, |alias| { - other.is_disjoint_from_impl( - db, - Type::GenericAlias(alias), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + .when_none_or(db, self.constraints, |alias| { + self.check_type_pair(db, Type::GenericAlias(alias_b), Type::GenericAlias(alias)) }), (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { - SubclassOfInner::Dynamic(_) => ConstraintSet::from_bool(constraints, false), + SubclassOfInner::Dynamic(_) => self.never(), SubclassOfInner::Class(class_a) => ConstraintSet::from_bool( - constraints, + self.constraints, !class_a.could_exist_in_mro_of( db, ClassType::NonGeneric(class_b), - constraints, + self.constraints, ), ), SubclassOfInner::TypeVar(_) => unreachable!(), @@ -2599,13 +2062,13 @@ impl<'db> Type<'db> { (Type::SubclassOf(subclass_of_ty), Type::GenericAlias(alias_b)) | (Type::GenericAlias(alias_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { - SubclassOfInner::Dynamic(_) => ConstraintSet::from_bool(constraints, false), + SubclassOfInner::Dynamic(_) => self.never(), SubclassOfInner::Class(class_a) => ConstraintSet::from_bool( - constraints, + self.constraints, !class_a.could_exist_in_mro_of( db, ClassType::Generic(alias_b), - constraints, + self.constraints, ), ), SubclassOfInner::TypeVar(_) => unreachable!(), @@ -2613,7 +2076,7 @@ impl<'db> Type<'db> { } (Type::SubclassOf(left), Type::SubclassOf(right)) => { - left.is_disjoint_from_impl(db, right, constraints, inferable, disjointness_visitor) + self.check_subclassof_pair(db, left, right) } // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`, @@ -2621,24 +2084,10 @@ impl<'db> Type<'db> { (Type::SubclassOf(subclass_of_ty), other) | (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() { SubclassOfInner::Dynamic(_) => { - KnownClass::Type.to_instance(db).is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, KnownClass::Type.to_instance(db), other) } SubclassOfInner::Class(class) => { - class.metaclass_instance_type(db).is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, class.metaclass_instance_type(db), other) } SubclassOfInner::TypeVar(_) => unreachable!(), }, @@ -2646,7 +2095,7 @@ impl<'db> Type<'db> { (Type::SpecialForm(special_form), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => { ConstraintSet::from_bool( - constraints, + self.constraints, !special_form.is_instance_of(db, instance.class(db)), ) } @@ -2654,41 +2103,35 @@ impl<'db> Type<'db> { (Type::KnownInstance(known_instance), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { ConstraintSet::from_bool( - constraints, + self.constraints, !known_instance.is_instance_of(db, instance.class(db)), ) } (Type::LiteralValue(literal), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::LiteralValue(literal)) => { - match literal.kind() { - LiteralValueTypeKind::Int(_) => KnownClass::Int - .when_subclass_of(db, instance.class(db), constraints) - .negate(db, constraints), - LiteralValueTypeKind::Bool(_) => KnownClass::Bool - .when_subclass_of(db, instance.class(db), constraints) - .negate(db, constraints), + let positive_relation_holds = match literal.kind() { + LiteralValueTypeKind::Int(_) => { + KnownClass::Int.when_subclass_of(db, instance.class(db), self.constraints) + } + LiteralValueTypeKind::Bool(_) => { + KnownClass::Bool.when_subclass_of(db, instance.class(db), self.constraints) + } LiteralValueTypeKind::LiteralString | LiteralValueTypeKind::String(_) => { - KnownClass::Str - .when_subclass_of(db, instance.class(db), constraints) - .negate(db, constraints) + KnownClass::Str.when_subclass_of(db, instance.class(db), self.constraints) } - LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes - .when_subclass_of(db, instance.class(db), constraints) - .negate(db, constraints), - LiteralValueTypeKind::Enum(enum_literal) => enum_literal - .enum_class_instance(db) - .has_relation_to_impl( + LiteralValueTypeKind::Bytes(_) => { + KnownClass::Bytes.when_subclass_of(db, instance.class(db), self.constraints) + } + LiteralValueTypeKind::Enum(enum_literal) => self + .as_relation_checker(TypeRelation::Subtyping) + .check_type_pair( db, + enum_literal.enum_class_instance(db), Type::NominalInstance(instance), - constraints, - inferable, - TypeRelation::Subtyping, - relation_visitor, - disjointness_visitor, - ) - .negate(db, constraints), - } + ), + }; + positive_relation_holds.negate(db, self.constraints) } (Type::TypeIs(_) | Type::TypeGuard(_), Type::NominalInstance(instance)) @@ -2696,85 +2139,58 @@ impl<'db> Type<'db> { // A boolean literal must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) KnownClass::Bool - .when_subclass_of(db, instance.class(db), constraints) - .negate(db, constraints) + .when_subclass_of(db, instance.class(db), self.constraints) + .negate(db, self.constraints) } (Type::TypeIs(_) | Type::TypeGuard(_), _) - | (_, Type::TypeIs(_) | Type::TypeGuard(_)) => { - ConstraintSet::from_bool(constraints, true) - } + | (_, Type::TypeIs(_) | Type::TypeGuard(_)) => self.always(), - (Type::LiteralValue(_), _) | (_, Type::LiteralValue(_)) => { - ConstraintSet::from_bool(constraints, true) - } + (Type::LiteralValue(_), _) | (_, Type::LiteralValue(_)) => self.always(), // A class-literal type `X` is always disjoint from an instance type `Y`, // unless the type expressing "all instances of `Z`" is a subtype of of `Y`, // where `Z` is `X`'s metaclass. - (Type::ClassLiteral(class), instance @ Type::NominalInstance(_)) - | (instance @ Type::NominalInstance(_), Type::ClassLiteral(class)) => class + (Type::ClassLiteral(class), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::ClassLiteral(class)) => class .metaclass_instance_type(db) - .when_subtype_of(db, instance, constraints, inferable) - .negate(db, constraints), - (Type::GenericAlias(alias), instance @ Type::NominalInstance(_)) - | (instance @ Type::NominalInstance(_), Type::GenericAlias(alias)) => { - ClassType::from(alias) - .metaclass_instance_type(db) - .has_relation_to_impl( - db, - instance, - constraints, - inferable, - TypeRelation::Subtyping, - relation_visitor, - disjointness_visitor, - ) - .negate(db, constraints) - } + .when_subtype_of( + db, + Type::NominalInstance(instance), + self.constraints, + self.inferable, + ) + .negate(db, self.constraints), + + (Type::GenericAlias(alias), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::GenericAlias(alias)) => self + .as_relation_checker(TypeRelation::Subtyping) + .check_type_pair( + db, + ClassType::Generic(alias).metaclass_instance_type(db), + Type::NominalInstance(instance), + ) + .negate(db, self.constraints), (Type::FunctionLiteral(..), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::FunctionLiteral(..)) => { // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // (it cannot be an instance of a `types.FunctionType` subclass) KnownClass::FunctionType - .when_subclass_of(db, instance.class(db), constraints) - .negate(db, constraints) + .when_subclass_of(db, instance.class(db), self.constraints) + .negate(db, self.constraints) } - (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType - .to_instance(db) - .is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ), + (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => { + self.check_type_pair(db, KnownClass::MethodType.to_instance(db), other) + } (Type::KnownBoundMethod(method), other) | (other, Type::KnownBoundMethod(method)) => { - method.class().to_instance(db).is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, method.class().to_instance(db), other) } (Type::WrapperDescriptor(_), other) | (other, Type::WrapperDescriptor(_)) => { - KnownClass::WrapperDescriptorType - .to_instance(db) - .is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, KnownClass::WrapperDescriptorType.to_instance(db), other) } (Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_)) @@ -2782,7 +2198,7 @@ impl<'db> Type<'db> { // No two callable types are ever disjoint because // `(*args: object, **kwargs: object) -> Never` is a subtype of all fully static // callable types. - ConstraintSet::from_bool(constraints, false) + self.never() } (Type::Callable(_), Type::SpecialForm(special_form)) @@ -2791,17 +2207,17 @@ impl<'db> Type<'db> { // that are callable (like TypedDict and collection constructors). // Most special forms are type constructors/annotations (like `typing.Literal`, // `typing.Union`, etc.) that are subscripted, not called. - ConstraintSet::from_bool(constraints, !special_form.is_callable()) + ConstraintSet::from_bool(self.constraints, !special_form.is_callable()) } ( Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), - instance @ Type::NominalInstance(nominal), + Type::NominalInstance(nominal), ) | ( - instance @ Type::NominalInstance(nominal), + Type::NominalInstance(nominal), Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), - ) if nominal.class(db).is_final(db) => instance + ) if nominal.class(db).is_final(db) => Type::NominalInstance(nominal) .member_lookup_with_policy( db, Name::new_static("__call__"), @@ -2809,18 +2225,10 @@ impl<'db> Type<'db> { ) .place .ignore_possibly_undefined() - .when_none_or(db, constraints, |dunder_call| { - dunder_call - .has_relation_to_impl( - db, - Type::Callable(CallableType::unknown(db)), - constraints, - inferable, - TypeRelation::Assignability, - relation_visitor, - disjointness_visitor, - ) - .negate(db, constraints) + .when_none_or(db, self.constraints, |dunder_call| { + self.as_relation_checker(TypeRelation::Assignability) + .check_type_pair(db, dunder_call, Type::Callable(CallableType::unknown(db))) + .negate(db, self.constraints) }), ( @@ -2832,93 +2240,50 @@ impl<'db> Type<'db> { Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), ) => { // TODO: Implement disjointness for general callable type with other types - ConstraintSet::from_bool(constraints, false) + self.never() } - (Type::ModuleLiteral(..), other @ Type::NominalInstance(..)) - | (other @ Type::NominalInstance(..), Type::ModuleLiteral(..)) => { + (Type::ModuleLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::ModuleLiteral(..)) => { // Modules *can* actually be instances of `ModuleType` subclasses - other.is_disjoint_from_impl( + self.check_type_pair( db, + Type::NominalInstance(instance), KnownClass::ModuleType.to_instance(db), - constraints, - inferable, - disjointness_visitor, - relation_visitor, ) } - (Type::NominalInstance(left), Type::NominalInstance(right)) => disjointness_visitor - .visit((self, other), || { - left.is_disjoint_from_impl( - db, - right, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - }), + (Type::NominalInstance(left_i), Type::NominalInstance(right_i)) => { + self.disjointness_visitor.visit((left, right), || { + self.check_nominal_instance_pair(db, left_i, right_i) + }) + } (Type::NewTypeInstance(left), Type::NewTypeInstance(right)) => { - left.is_disjoint_from_impl(db, right, constraints) + self.check_newtype_pair(db, left, right) } (Type::NewTypeInstance(newtype), other) | (other, Type::NewTypeInstance(newtype)) => { - newtype.concrete_base_type(db).is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, newtype.concrete_base_type(db), other) } (Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => { - KnownClass::Property.to_instance(db).is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, KnownClass::Property.to_instance(db), other) } - (Type::BoundSuper(left), Type::BoundSuper(right)) => left - .is_equivalent_to_impl( - db, - right, - constraints, - relation_visitor, - disjointness_visitor, - ) - .negate(db, constraints), + (Type::BoundSuper(left), Type::BoundSuper(right)) => self + .as_equivalence_checker() + .check_bound_super_pair(db, left, right) + .negate(db, self.constraints), + (Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => { - KnownClass::Super.to_instance(db).is_disjoint_from_impl( - db, - other, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, KnownClass::Super.to_instance(db), other) } - (Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => { - ConstraintSet::from_bool(constraints, true) - } + (Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => self.always(), - (Type::TypedDict(self_typeddict), Type::TypedDict(other_typeddict)) => { - disjointness_visitor.visit((self, other), || { - self_typeddict.is_disjoint_from_impl( - db, - other_typeddict, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + (Type::TypedDict(left_td), Type::TypedDict(right_td)) => { + self.disjointness_visitor.visit((left, right), || { + self.check_typeddict_pair(db, left_td, right_td) }) } @@ -2926,52 +2291,39 @@ impl<'db> Type<'db> { // types will always be disjoint from `T`. This doesn't cover all cases -- in fact // `dict` *itself* is almost always disjoint from `TypedDict` -- but it's a good // approximation, and some false negatives are acceptable. - (Type::TypedDict(_), other) | (other, Type::TypedDict(_)) => KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]) - .has_relation_to_impl( - db, - other, - constraints, - inferable, - TypeRelation::Assignability, - relation_visitor, - disjointness_visitor, - ) - .negate(db, constraints), - } - } -} - -/// A [`PairVisitor`] that is used in `has_relation_to` methods. -pub(crate) type HasRelationToVisitor<'db, 'c> = CycleDetector< - TypeRelation, - (Type<'db>, Type<'db>, TypeRelation), - ConstraintSet<'db, 'c>, - ConstraintSet<'db, 'c>, ->; + (Type::TypedDict(_), other) | (other, Type::TypedDict(_)) => { + let dict_str_any = KnownClass::Dict + .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); -impl<'db, 'c> HasRelationToVisitor<'db, 'c> { - pub(crate) fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { - HasRelationToVisitor::with_given(constraints, ConstraintSet::from_bool(constraints, false)) + self.as_relation_checker(TypeRelation::Assignability) + .check_type_pair(db, dict_str_any, other) + .negate(db, self.constraints) + } + } } - pub(crate) fn with_given( - constraints: &'c ConstraintSetBuilder<'db>, - given: ConstraintSet<'db, 'c>, - ) -> Self { - let fallback = ConstraintSet::from_bool(constraints, true); - HasRelationToVisitor::with_extra(fallback, given) + fn check_property_instance_pair( + &self, + db: &'db dyn Db, + left: PropertyInstanceType<'db>, + right: PropertyInstanceType<'db>, + ) -> ConstraintSet<'db, 'c> { + self.check_optional_property_method_pair(db, left.getter(db), right.getter(db)) + .or(db, self.constraints, || { + self.check_optional_property_method_pair(db, left.setter(db), right.setter(db)) + }) } -} - -/// A [`PairVisitor`] that is used in `is_disjoint_from` methods. -pub(crate) type IsDisjointVisitor<'db, 'c> = PairVisitor<'db, IsDisjoint, ConstraintSet<'db, 'c>>; - -#[derive(Debug)] -pub(crate) struct IsDisjoint; -impl<'db, 'c> IsDisjointVisitor<'db, 'c> { - pub(crate) fn default(constraints: &'c ConstraintSetBuilder<'db>) -> Self { - IsDisjointVisitor::new(ConstraintSet::from_bool(constraints, false)) + fn check_optional_property_method_pair( + &self, + db: &'db dyn Db, + left: Option>, + right: Option>, + ) -> ConstraintSet<'db, 'c> { + match (left, right) { + (None, None) => self.never(), + (Some(left), Some(right)) => self.check_type_pair(db, left, right), + (None | Some(_), None | Some(_)) => self.always(), + } } } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index a48c2f430bc63..16444be23d40a 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -24,7 +24,9 @@ use crate::types::constraints::{ }; use crate::types::generics::{GenericContext, InferableTypeVars, walk_generic_context}; use crate::types::infer::infer_deferred_types; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::relation::{ + HasRelationToVisitor, IsDisjointVisitor, TypeRelation, TypeRelationChecker, +}; use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, ParamSpecAttrKind, SelfBinding, @@ -292,29 +294,6 @@ impl<'db> CallableSignature<'db> { } } - #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( - &self, - db: &'db dyn Db, - other: &Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - Self::has_relation_to_inner( - db, - &self.overloads, - &other.overloads, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } - pub(crate) fn is_single_paramspec(&self) -> Option<(BoundTypeVarInstance<'db>, Type<'db>)> { Self::signatures_is_single_paramspec(&self.overloads) } @@ -342,333 +321,16 @@ impl<'db> CallableSignature<'db> { constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db, 'c> { - self.has_relation_to_impl( - db, - other, + let relation_visitor = HasRelationToVisitor::default(constraints); + let disjointness_visitor = IsDisjointVisitor::default(constraints); + let checker = TypeRelationChecker::new( constraints, inferable, TypeRelation::ConstraintSetAssignability, - &HasRelationToVisitor::default(constraints), - &IsDisjointVisitor::default(constraints), - ) - } - - /// Fast path for unary callable assignability: compare overload sets by aggregating - /// overlapping parameter domains and return types. - /// - /// This is intentionally accept-only. If the probe does not definitely succeed, it returns - /// `None` and callers should fall back to legacy per-overload relation checks. - #[expect(clippy::too_many_arguments)] - fn try_unary_overload_aggregate_relation<'c>( - db: &'db dyn Db, - constraints: &'c ConstraintSetBuilder<'db>, - self_signatures: &[Signature<'db>], - other_signature: &Signature<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> Option> { - let single_required_positional_parameter_type = |signature: &Signature<'db>| { - if signature.parameters().len() != 1 { - return None; - } - let parameter = signature.parameters().get(0)?; - - match parameter.kind() { - ParameterKind::PositionalOnly { - default_type: None, .. - } - | ParameterKind::PositionalOrKeyword { - default_type: None, .. - } => Some(parameter.annotated_type()), - _ => None, - } - }; - - let is_unary_overload_aggregate_candidate_type = |ty: Type<'db>| { - // Keep aggregate probing away from inference-sensitive shapes and defer them to the - // legacy path, which already handles dynamic/typevar interactions. - !ty.has_dynamic(db) && !ty.has_typevar_or_typevar_instance(db) - }; - - let other_parameter_type = single_required_positional_parameter_type(other_signature)?; - // Keep this aggregate path narrowly scoped to unary target callables whose parameter - // domain is an explicit union. - // - // Broader overload-set assignability (non-union unary domains, higher arity, - // typevars/dynamic interactions) needs dedicated relation logic. - if !matches!(other_parameter_type, Type::Union(_)) - || !is_unary_overload_aggregate_candidate_type(other_parameter_type) - || !is_unary_overload_aggregate_candidate_type(other_signature.return_ty) - { - return None; - } - - let mut parameter_type_union = UnionBuilder::new(db); - let mut return_type_union = UnionBuilder::new(db); - let mut has_overlapping_domain = false; - - for self_signature in self_signatures { - let self_parameter_type = single_required_positional_parameter_type(self_signature)?; - if !is_unary_overload_aggregate_candidate_type(self_parameter_type) - || !is_unary_overload_aggregate_candidate_type(self_signature.return_ty) - { - return None; - } - let signatures_are_disjoint = self_parameter_type - .is_disjoint_from_impl( - db, - other_parameter_type, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - .is_always_satisfied(db); - - if signatures_are_disjoint { - continue; - } - - has_overlapping_domain = true; - parameter_type_union = parameter_type_union.add(self_parameter_type); - return_type_union = return_type_union.add(self_signature.return_ty); - } - - if !has_overlapping_domain { - return None; - } - - // Function assignability here is parameter-contravariant and return-covariant. - let parameters_cover_target = other_parameter_type.has_relation_to_impl( - db, - parameter_type_union.build(), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - let returns_match_target = return_type_union.build().has_relation_to_impl( - db, - other_signature.return_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, + &relation_visitor, + &disjointness_visitor, ); - let aggregate_relation = - parameters_cover_target.and(db, constraints, || returns_match_target); - aggregate_relation - .is_always_satisfied(db) - .then_some(aggregate_relation) - } - - /// Implementation of subtyping and assignability between two, possible overloaded, callable - /// types. - #[expect(clippy::too_many_arguments)] - fn has_relation_to_inner<'c>( - db: &'db dyn Db, - self_signatures: &[Signature<'db>], - other_signatures: &[Signature<'db>], - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - if relation.is_constraint_set_assignability() { - // TODO: Oof, maybe ParamSpec needs to live at CallableSignature, not Signature? - let self_is_single_paramspec = Self::signatures_is_single_paramspec(self_signatures); - let other_is_single_paramspec = Self::signatures_is_single_paramspec(other_signatures); - - // If either callable is a ParamSpec, the constraint set should bind the ParamSpec to - // the other callable's signature. We also need to compare the return types — for - // instance, to verify in `Callable[P, int]` that the return type is assignable to - // `int`, or in `Callable[P, T]` to bind `T` to the return type of the other callable. - match (self_is_single_paramspec, other_is_single_paramspec) { - ( - Some((self_bound_typevar, self_return_type)), - Some((other_bound_typevar, other_return_type)), - ) => { - let param_spec_matches = ConstraintSet::constrain_typevar( - db, - constraints, - self_bound_typevar, - Type::TypeVar(other_bound_typevar), - Type::TypeVar(other_bound_typevar), - ); - let return_types_match = self_return_type.has_relation_to_impl( - db, - other_return_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - return param_spec_matches.and(db, constraints, || return_types_match); - } - - (Some((self_bound_typevar, self_return_type)), None) => { - let upper = Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(other_signatures.iter().map( - |signature| { - Signature::new_generic( - signature.generic_context, - signature.parameters().clone(), - Type::unknown(), - ) - }, - )), - CallableTypeKind::ParamSpecValue, - )); - let param_spec_matches = ConstraintSet::constrain_typevar( - db, - constraints, - self_bound_typevar, - Type::Never, - upper, - ); - let return_types_match = other_signatures - .iter() - .map(|signature| signature.return_ty) - .when_any(db, constraints, |other_return_type| { - self_return_type.has_relation_to_impl( - db, - other_return_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }); - return param_spec_matches.and(db, constraints, || return_types_match); - } - - (None, Some((other_bound_typevar, other_return_type))) => { - let lower = Type::Callable(CallableType::new( - db, - CallableSignature::from_overloads(self_signatures.iter().map( - |signature| { - Signature::new_generic( - signature.generic_context, - signature.parameters().clone(), - Type::unknown(), - ) - }, - )), - CallableTypeKind::ParamSpecValue, - )); - let param_spec_matches = ConstraintSet::constrain_typevar( - db, - constraints, - other_bound_typevar, - lower, - Type::object(), - ); - let return_types_match = self_signatures - .iter() - .map(|signature| signature.return_ty) - .when_any(db, constraints, |self_return_type| { - self_return_type.has_relation_to_impl( - db, - other_return_type, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }); - return param_spec_matches.and(db, constraints, || return_types_match); - } - - (None, None) => {} - } - } - - match (self_signatures, other_signatures) { - ([self_signature], [other_signature]) => { - // Base case: both callable types contain a single signature. - self_signature.has_relation_to_impl( - db, - other_signature, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } - - // `self` is possibly overloaded while `other` is definitely not overloaded. - (_, [other_signature]) => { - if let Some(aggregate_relation) = Self::try_unary_overload_aggregate_relation( - db, - constraints, - self_signatures, - other_signature, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) { - return aggregate_relation; - } - - self_signatures - .iter() - .when_any(db, constraints, |self_signature| { - Self::has_relation_to_inner( - db, - std::slice::from_ref(self_signature), - other_signatures, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } - - // `self` is definitely not overloaded while `other` is possibly overloaded. - ([_], _) => other_signatures - .iter() - .when_all(db, constraints, |other_signature| { - Self::has_relation_to_inner( - db, - self_signatures, - std::slice::from_ref(other_signature), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), - - // `self` is definitely overloaded while `other` is possibly overloaded. - (_, _) => other_signatures - .iter() - .when_all(db, constraints, |other_signature| { - Self::has_relation_to_inner( - db, - self_signatures, - std::slice::from_ref(other_signature), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }), - } + checker.check_callable_signature_pair_inner(db, &self.overloads, &other.overloads) } } @@ -1122,28 +784,310 @@ impl<'db> Signature<'db> { constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db, 'c> { - self.has_relation_to_impl( - db, - other, + let relation_visitor = HasRelationToVisitor::default(constraints); + let disjointness_visitor = IsDisjointVisitor::default(constraints); + let checker = TypeRelationChecker::new( constraints, inferable, TypeRelation::ConstraintSetAssignability, - &HasRelationToVisitor::default(constraints), - &IsDisjointVisitor::default(constraints), + &relation_visitor, + &disjointness_visitor, + ); + checker.check_signature_pair(db, self, other) + } + + /// Create a new signature with the given definition. + pub(crate) fn with_definition(self, definition: Option>) -> Self { + Self { definition, ..self } + } + + /// Create a new signature with the given return type. + pub(crate) fn with_return_type(self, return_ty: Type<'db>) -> Self { + Self { return_ty, ..self } + } +} + +impl<'db> VarianceInferable<'db> for &Signature<'db> { + fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { + tracing::trace!( + "Checking variance of `{tvar}` in `{self:?}`", + tvar = typevar.typevar(db).name(db) + ); + itertools::chain( + self.parameters + .iter() + .filter_map(|parameter| match parameter.form { + ParameterForm::Type => None, + ParameterForm::Value => Some( + parameter + .annotated_type() + .with_polarity(TypeVarVariance::Contravariant) + .variance_of(db, typevar), + ), + }), + Some(self.return_ty.variance_of(db, typevar)), ) + .collect() + } +} + +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + /// Fast path for unary callable assignability: compare overload sets by aggregating + /// overlapping parameter domains and return types. + /// + /// This is intentionally accept-only. If the probe does not definitely succeed, it returns + /// `None` and callers should fall back to legacy per-overload relation checks. + fn try_unary_overload_aggregate_relation( + &self, + db: &'db dyn Db, + source_signatures: &[Signature<'db>], + target_signature: &Signature<'db>, + ) -> Option> { + let single_required_positional_parameter_type = |signature: &Signature<'db>| { + if signature.parameters().len() != 1 { + return None; + } + let parameter = signature.parameters().get(0)?; + + match parameter.kind() { + ParameterKind::PositionalOnly { + default_type: None, .. + } + | ParameterKind::PositionalOrKeyword { + default_type: None, .. + } => Some(parameter.annotated_type()), + _ => None, + } + }; + + let is_unary_overload_aggregate_candidate_type = |ty: Type<'db>| { + // Keep aggregate probing away from inference-sensitive shapes and defer them to the + // legacy path, which already handles dynamic/typevar interactions. + !ty.has_dynamic(db) && !ty.has_typevar_or_typevar_instance(db) + }; + + let other_parameter_type = single_required_positional_parameter_type(target_signature)?; + // Keep this aggregate path narrowly scoped to unary target callables whose parameter + // domain is an explicit union. + // + // Broader overload-set assignability (non-union unary domains, higher arity, + // typevars/dynamic interactions) needs dedicated relation logic. + if !matches!(other_parameter_type, Type::Union(_)) + || !is_unary_overload_aggregate_candidate_type(other_parameter_type) + || !is_unary_overload_aggregate_candidate_type(target_signature.return_ty) + { + return None; + } + + let mut parameter_type_union = UnionBuilder::new(db); + let mut return_type_union = UnionBuilder::new(db); + let mut has_overlapping_domain = false; + + for self_signature in source_signatures { + let self_parameter_type = single_required_positional_parameter_type(self_signature)?; + if !is_unary_overload_aggregate_candidate_type(self_parameter_type) + || !is_unary_overload_aggregate_candidate_type(self_signature.return_ty) + { + return None; + } + let signatures_are_disjoint = self + .as_disjointness_checker() + .check_type_pair(db, self_parameter_type, other_parameter_type) + .is_always_satisfied(db); + + if signatures_are_disjoint { + continue; + } + + has_overlapping_domain = true; + parameter_type_union = parameter_type_union.add(self_parameter_type); + return_type_union = return_type_union.add(self_signature.return_ty); + } + + if !has_overlapping_domain { + return None; + } + + // Function assignability here is parameter-contravariant and return-covariant. + let parameters_cover_target = + self.check_type_pair(db, other_parameter_type, parameter_type_union.build()); + let returns_match_target = + || self.check_type_pair(db, return_type_union.build(), target_signature.return_ty); + let aggregate_relation = + parameters_cover_target.and(db, self.constraints, returns_match_target); + aggregate_relation + .is_always_satisfied(db) + .then_some(aggregate_relation) + } + + pub(super) fn check_callable_signature_pair( + &self, + db: &'db dyn Db, + source: &CallableSignature<'db>, + target: &CallableSignature<'db>, + ) -> ConstraintSet<'db, 'c> { + self.check_callable_signature_pair_inner(db, &source.overloads, &target.overloads) + } + + /// Implementation of subtyping and assignability between two, possible overloaded, callable + /// types. + fn check_callable_signature_pair_inner( + &self, + db: &'db dyn Db, + source_overloads: &[Signature<'db>], + target_overloads: &[Signature<'db>], + ) -> ConstraintSet<'db, 'c> { + if self.relation.is_constraint_set_assignability() { + // TODO: Oof, maybe ParamSpec needs to live at CallableSignature, not Signature? + let source_is_single_paramspec = + CallableSignature::signatures_is_single_paramspec(source_overloads); + let target_is_single_paramspec = + CallableSignature::signatures_is_single_paramspec(target_overloads); + + // If either callable is a ParamSpec, the constraint set should bind the ParamSpec to + // the other callable's signature. We also need to compare the return types — for + // instance, to verify in `Callable[P, int]` that the return type is assignable to + // `int`, or in `Callable[P, T]` to bind `T` to the return type of the other callable. + match (source_is_single_paramspec, target_is_single_paramspec) { + (Some((source_tvar, source_return)), Some((target_tvar, target_return))) => { + let param_spec_matches = ConstraintSet::constrain_typevar( + db, + self.constraints, + source_tvar, + Type::TypeVar(target_tvar), + Type::TypeVar(target_tvar), + ); + let return_types_match = self.check_type_pair(db, source_return, target_return); + return param_spec_matches.and(db, self.constraints, || return_types_match); + } + + (Some((source_tvar, source_return)), None) => { + let upper = Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(target_overloads.iter().map( + |signature| { + Signature::new_generic( + signature.generic_context, + signature.parameters().clone(), + Type::unknown(), + ) + }, + )), + CallableTypeKind::ParamSpecValue, + )); + let param_spec_matches = ConstraintSet::constrain_typevar( + db, + self.constraints, + source_tvar, + Type::Never, + upper, + ); + let return_types_match = || { + target_overloads + .iter() + .map(|signature| signature.return_ty) + .when_any(db, self.constraints, |target_return| { + self.check_type_pair(db, source_return, target_return) + }) + }; + return param_spec_matches.and(db, self.constraints, return_types_match); + } + + (None, Some((target_tvar, target_return))) => { + let lower = Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(source_overloads.iter().map( + |signature| { + Signature::new_generic( + signature.generic_context, + signature.parameters().clone(), + Type::unknown(), + ) + }, + )), + CallableTypeKind::ParamSpecValue, + )); + let param_spec_matches = ConstraintSet::constrain_typevar( + db, + self.constraints, + target_tvar, + lower, + Type::object(), + ); + let return_types_match = || { + source_overloads + .iter() + .map(|signature| signature.return_ty) + .when_any(db, self.constraints, |source_return| { + self.check_type_pair(db, source_return, target_return) + }) + }; + return param_spec_matches.and(db, self.constraints, return_types_match); + } + + (None, None) => {} + } + } + + match (source_overloads, target_overloads) { + ([self_signature], [other_signature]) => { + // Base case: both callable types contain a single signature. + self.check_signature_pair(db, self_signature, other_signature) + } + + // `self` is possibly overloaded while `other` is definitely not overloaded. + (_, [other_signature]) => { + if let Some(aggregate_relation) = self.try_unary_overload_aggregate_relation( + db, + source_overloads, + other_signature, + ) { + return aggregate_relation; + } + + source_overloads + .iter() + .when_any(db, self.constraints, |self_signature| { + self.check_callable_signature_pair_inner( + db, + std::slice::from_ref(self_signature), + target_overloads, + ) + }) + } + + // `self` is definitely not overloaded while `other` is possibly overloaded. + ([_], _) => { + target_overloads + .iter() + .when_all(db, self.constraints, |target_signature| { + self.check_callable_signature_pair_inner( + db, + source_overloads, + std::slice::from_ref(target_signature), + ) + }) + } + + // `self` is definitely overloaded while `other` is possibly overloaded. + (_, _) => target_overloads + .iter() + .when_all(db, self.constraints, |target_signature| { + self.check_callable_signature_pair_inner( + db, + source_overloads, + std::slice::from_ref(target_signature), + ) + }), + } } /// Implementation of subtyping and assignability for signature. - #[expect(clippy::too_many_arguments)] - fn has_relation_to_impl<'c>( + fn check_signature_pair( &self, db: &'db dyn Db, - other: &Signature<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + source: &Signature<'db>, + target: &Signature<'db>, ) -> ConstraintSet<'db, 'c> { // If either signature is generic, their typevars should also be considered inferable when // checking whether one signature is a subtype/etc of the other, since we only need to find @@ -1157,21 +1101,14 @@ impl<'db> Signature<'db> { // # Here, TypeOf[identity2] is a generic callable that should consider T to be // # inferable, even though other uses of T in the function body are non-inferable. // return t - let self_inferable = self.inferable_typevars(db); - let other_inferable = other.inferable_typevars(db); - let inferable = inferable.merge(&self_inferable); - let inferable = inferable.merge(&other_inferable); + let source_inferable = source.inferable_typevars(db); + let target_inferable = target.inferable_typevars(db); + let inferable = self.inferable.merge(&source_inferable); + let inferable = inferable.merge(&target_inferable); // `inner` will create a constraint set that references these newly inferable typevars. - let when = self.has_relation_to_inner( - db, - other, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); + let checker = self.with_inferable_typevars(inferable); + let when = checker.check_signature_pair_inner(db, source, target); // But the caller does not need to consider those extra typevars. Whatever constraint set // we produce, we reduce it back down to the inferable set that the caller asked about. @@ -1179,67 +1116,62 @@ impl<'db> Signature<'db> { // before returning. when.reduce_inferable( db, - constraints, - self_inferable.iter().chain(other_inferable.iter()), + self.constraints, + source_inferable.iter().chain(target_inferable.iter()), ) } - #[expect(clippy::too_many_arguments)] - fn has_relation_to_inner<'c>( + fn check_signature_pair_inner( &self, db: &'db dyn Db, - other: &Signature<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + source: &Signature<'db>, + target: &Signature<'db>, ) -> ConstraintSet<'db, 'c> { /// A helper struct to zip two slices of parameters together that provides control over the /// two iterators individually. It also keeps track of the current parameter in each /// iterator. struct ParametersZip<'a, 'db> { - current_self: Option<&'a Parameter<'db>>, - current_other: Option<&'a Parameter<'db>>, - iter_self: Iter<'a, Parameter<'db>>, - iter_other: Iter<'a, Parameter<'db>>, + current_source: Option<&'a Parameter<'db>>, + current_target: Option<&'a Parameter<'db>>, + source_iter: Iter<'a, Parameter<'db>>, + target_iter: Iter<'a, Parameter<'db>>, } impl<'a, 'db> ParametersZip<'a, 'db> { - /// Move to the next parameter in both the `self` and `other` parameter iterators, + /// Move to the next parameter in both the `source` and `target` parameter iterators, /// [`None`] if both iterators are exhausted. fn next(&mut self) -> Option, &'a Parameter<'db>>> { - match (self.next_self(), self.next_other()) { - (Some(self_param), Some(other_param)) => { - Some(EitherOrBoth::Both(self_param, other_param)) + match (self.next_source(), self.next_target()) { + (Some(source_param), Some(target_param)) => { + Some(EitherOrBoth::Both(source_param, target_param)) } - (Some(self_param), None) => Some(EitherOrBoth::Left(self_param)), - (None, Some(other_param)) => Some(EitherOrBoth::Right(other_param)), + (Some(source_param), None) => Some(EitherOrBoth::Left(source_param)), + (None, Some(target_param)) => Some(EitherOrBoth::Right(target_param)), (None, None) => None, } } - /// Move to the next parameter in the `self` parameter iterator, [`None`] if the + /// Move to the next parameter in the `source` parameter iterator, [`None`] if the /// iterator is exhausted. - fn next_self(&mut self) -> Option<&'a Parameter<'db>> { - self.current_self = self.iter_self.next(); - self.current_self + fn next_source(&mut self) -> Option<&'a Parameter<'db>> { + self.current_source = self.source_iter.next(); + self.current_source } - /// Move to the next parameter in the `other` parameter iterator, [`None`] if the + /// Move to the next parameter in the `target` parameter iterator, [`None`] if the /// iterator is exhausted. - fn next_other(&mut self) -> Option<&'a Parameter<'db>> { - self.current_other = self.iter_other.next(); - self.current_other + fn next_target(&mut self) -> Option<&'a Parameter<'db>> { + self.current_target = self.target_iter.next(); + self.current_target } - /// Peek at the next parameter in the `other` parameter iterator without consuming it. - fn peek_other(&mut self) -> Option<&'a Parameter<'db>> { - self.iter_other.clone().next() + /// Peek at the next parameter in the `target` parameter iterator without consuming it. + fn peek_target(&mut self) -> Option<&'a Parameter<'db>> { + self.target_iter.clone().next() } /// Consumes the `ParametersZip` and returns a two-element tuple containing the - /// remaining parameters in the `self` and `other` iterators respectively. + /// remaining parameters in the `source` and `target` iterators respectively. /// /// The returned iterators starts with the current parameter, if any, followed by the /// remaining parameters in the respective iterators. @@ -1250,13 +1182,13 @@ impl<'db> Signature<'db> { impl Iterator>, ) { ( - self.current_self.into_iter().chain(self.iter_self), - self.current_other.into_iter().chain(self.iter_other), + self.current_source.into_iter().chain(self.source_iter), + self.current_target.into_iter().chain(self.target_iter), ) } } - let mut result = ConstraintSet::from_bool(constraints, true); + let mut result = self.always(); let mut check_types = |type1: Type<'db>, type2: Type<'db>| { match (type1, type2) { @@ -1265,16 +1197,16 @@ impl<'db> Signature<'db> { // position. // // `ParamSpec` type variables can only occur in parameter lists so this special case - // is present here instead of in `Type::has_relation_to_impl`. + // is present here instead of in `TypeRelationChecker::check_type_pair`. (Type::TypeVar(typevar1), Type::TypeVar(typevar2)) if typevar1.paramspec_attr(db).is_some() && typevar1.paramspec_attr(db) == typevar2.paramspec_attr(db) && typevar1 .without_paramspec_attr(db) - .is_inferable(db, inferable) + .is_inferable(db, self.inferable) && typevar2 .without_paramspec_attr(db) - .is_inferable(db, inferable) => + .is_inferable(db, self.inferable) => { return true; } @@ -1282,128 +1214,114 @@ impl<'db> Signature<'db> { } !result - .intersect( - db, - constraints, - type1.has_relation_to_impl( - db, - type2, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - ) + .intersect(db, self.constraints, self.check_type_pair(db, type1, type2)) .is_never_satisfied(db) }; // Return types are covariant. - if !check_types(self.return_ty, other.return_ty) { + if !check_types(source.return_ty, target.return_ty) { return result; } // A gradual parameter list is a supertype of the "bottom" parameter list (*args: object, // **kwargs: object). - if other.parameters.is_gradual() - && !self.parameters.is_top() - && self + if target.parameters.is_gradual() + && !source.parameters.is_top() + && source .parameters .variadic() .is_some_and(|(_, param)| param.annotated_type().is_object()) - && self + && source .parameters .keyword_variadic() .is_some_and(|(_, param)| param.annotated_type().is_object()) { - return ConstraintSet::from_bool(constraints, true); + return self.always(); } // The top signature is supertype of (and assignable from) all other signatures. It is a // subtype of no signature except itself, and assignable only to the gradual signature. - if other.parameters.is_top() { - return ConstraintSet::from_bool(constraints, true); - } else if self.parameters.is_top() && !other.parameters.is_gradual() { - return ConstraintSet::from_bool(constraints, false); + if target.parameters.is_top() { + return self.always(); + } else if source.parameters.is_top() && !target.parameters.is_gradual() { + return self.never(); } // If either of the parameter lists is gradual (`...`), then it is assignable to and from // any other parameter list, but not a subtype or supertype of any other parameter list. - if self.parameters.is_gradual() || other.parameters.is_gradual() { - return match relation { - TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => { - ConstraintSet::from_bool(constraints, false) - } + if source.parameters.is_gradual() || target.parameters.is_gradual() { + return match self.relation { + TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => self.never(), TypeRelation::Redundancy { .. } => result.intersect( db, - constraints, + self.constraints, ConstraintSet::from_bool( - constraints, - self.parameters.is_gradual() && other.parameters.is_gradual(), + self.constraints, + source.parameters.is_gradual() && target.parameters.is_gradual(), ), ), TypeRelation::Assignability | TypeRelation::ConstraintSetAssignability => result, }; } - if relation.is_constraint_set_assignability() { - let self_is_paramspec = self.parameters.as_paramspec(); - let other_is_paramspec = other.parameters.as_paramspec(); + if self.relation.is_constraint_set_assignability() { + let source_is_paramspec = source.parameters.as_paramspec(); + let target_is_paramspec = target.parameters.as_paramspec(); // If either signature is a ParamSpec, the constraint set should bind the ParamSpec to // the other signature. - match (self_is_paramspec, other_is_paramspec) { - (Some(self_bound_typevar), Some(other_bound_typevar)) => { + match (source_is_paramspec, target_is_paramspec) { + (Some(source_tvar), Some(target_tvar)) => { let param_spec_matches = ConstraintSet::constrain_typevar( db, - constraints, - self_bound_typevar, - Type::TypeVar(other_bound_typevar), - Type::TypeVar(other_bound_typevar), + self.constraints, + source_tvar, + Type::TypeVar(target_tvar), + Type::TypeVar(target_tvar), ); - result.intersect(db, constraints, param_spec_matches); + result.intersect(db, self.constraints, param_spec_matches); return result; } - (Some(self_bound_typevar), None) => { + (Some(source_tvar), None) => { let upper = Type::Callable(CallableType::new( db, CallableSignature::single(Signature::new_generic( - other.generic_context, - other.parameters.clone(), + target.generic_context, + target.parameters.clone(), Type::unknown(), )), CallableTypeKind::ParamSpecValue, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, - constraints, - self_bound_typevar, + self.constraints, + source_tvar, Type::Never, upper, ); - result.intersect(db, constraints, param_spec_matches); + result.intersect(db, self.constraints, param_spec_matches); return result; } - (None, Some(other_bound_typevar)) => { + (None, Some(target_tvar)) => { let lower = Type::Callable(CallableType::new( db, CallableSignature::single(Signature::new_generic( - self.generic_context, - self.parameters.clone(), + source.generic_context, + source.parameters.clone(), Type::unknown(), )), CallableTypeKind::ParamSpecValue, )); let param_spec_matches = ConstraintSet::constrain_typevar( db, - constraints, - other_bound_typevar, + self.constraints, + target_tvar, lower, Type::object(), ); - result.intersect(db, constraints, param_spec_matches); + result.intersect(db, self.constraints, param_spec_matches); return result; } @@ -1412,33 +1330,33 @@ impl<'db> Signature<'db> { } let mut parameters = ParametersZip { - current_self: None, - current_other: None, - iter_self: self.parameters.iter(), - iter_other: other.parameters.iter(), + current_source: None, + current_target: None, + source_iter: source.parameters.iter(), + target_iter: target.parameters.iter(), }; // Collect all the standard parameters that have only been matched against a variadic // parameter which means that the keyword variant is still unmatched. - let mut other_keywords = Vec::new(); + let mut target_keywords = Vec::new(); loop { let Some(next_parameter) = parameters.next() else { - if other_keywords.is_empty() { + if target_keywords.is_empty() { // All parameters have been checked or both the parameter lists were empty. - // In either case, `self` is a subtype of `other`. + // In either case, `source` is a subtype of `target`. return result; } - // There are keyword parameters in `other` that were only matched positionally - // against a variadic parameter in `self`. We need to verify that they can also + // There are keyword parameters in `target` that were only matched positionally + // against a variadic parameter in `source`. We need to verify that they can also // be matched as keyword arguments, which is done after this loop. break; }; match next_parameter { - EitherOrBoth::Left(self_parameter) => match self_parameter.kind() { + EitherOrBoth::Left(source_parameter) => match source_parameter.kind() { ParameterKind::KeywordOnly { .. } | ParameterKind::KeywordVariadic { .. } - if !other_keywords.is_empty() => + if !target_keywords.is_empty() => { // If there are any unmatched keyword parameters in `other`, they need to // be checked against the keyword-only / keyword-variadic parameters that @@ -1448,11 +1366,11 @@ impl<'db> Signature<'db> { ParameterKind::PositionalOnly { default_type, .. } | ParameterKind::PositionalOrKeyword { default_type, .. } | ParameterKind::KeywordOnly { default_type, .. } => { - // For `self <: other` to be valid, if there are no more parameters in - // `other`, then the non-variadic parameters in `self` must have a default + // For `source <: target` to be valid, if there are no more parameters in + // `target`, then the non-variadic parameters in `source` must have a default // value. if default_type.is_none() { - return ConstraintSet::from_bool(constraints, false); + return self.never(); } } ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => { @@ -1462,33 +1380,33 @@ impl<'db> Signature<'db> { }, EitherOrBoth::Right(_) => { - // If there are more parameters in `other` than in `self`, then `self` is not a - // subtype of `other`. - return ConstraintSet::from_bool(constraints, false); + // If there are more parameters in `target` than in `source`, then `source` is + // not a subtype of `target`. + return self.never(); } - EitherOrBoth::Both(self_parameter, other_parameter) => { - match (self_parameter.kind(), other_parameter.kind()) { + EitherOrBoth::Both(source_param, target_param) => { + match (source_param.kind(), target_param.kind()) { ( ParameterKind::PositionalOnly { - default_type: self_default, + default_type: source_default, .. } | ParameterKind::PositionalOrKeyword { - default_type: self_default, + default_type: source_default, .. }, ParameterKind::PositionalOnly { - default_type: other_default, + default_type: target_default, .. }, ) => { - if self_default.is_none() && other_default.is_some() { - return ConstraintSet::from_bool(constraints, false); + if source_default.is_none() && target_default.is_some() { + return self.never(); } if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), + target_param.annotated_type(), + source_param.annotated_type(), ) { return result; } @@ -1496,24 +1414,24 @@ impl<'db> Signature<'db> { ( ParameterKind::PositionalOrKeyword { - name: self_name, - default_type: self_default, + name: source_name, + default_type: source_default, }, ParameterKind::PositionalOrKeyword { - name: other_name, - default_type: other_default, + name: target_name, + default_type: target_default, }, ) => { - if self_name != other_name { - return ConstraintSet::from_bool(constraints, false); + if source_name != target_name { + return self.never(); } // The following checks are the same as positional-only parameters. - if self_default.is_none() && other_default.is_some() { - return ConstraintSet::from_bool(constraints, false); + if source_default.is_none() && target_default.is_some() { + return self.never(); } if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), + target_param.annotated_type(), + source_param.annotated_type(), ) { return result; } @@ -1525,36 +1443,36 @@ impl<'db> Signature<'db> { | ParameterKind::PositionalOrKeyword { .. }, ) => { if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), + target_param.annotated_type(), + source_param.annotated_type(), ) { return result; } if matches!( - other_parameter.kind(), + target_param.kind(), ParameterKind::PositionalOrKeyword { .. } ) { - other_keywords.push(other_parameter); + target_keywords.push(target_param); } - // We've reached a variadic parameter in `self` which means there can + // We've reached a variadic parameter in `source` which means there can // be no more positional parameters after this in a valid AST. But, the - // current parameter in `other` is a positional-only which means there + // current parameter in `target` is a positional-only which means there // can be more positional parameters after this which could be either // more positional-only parameters, standard parameters or a variadic // parameter. // - // So, any remaining positional parameters in `other` would need to be - // checked against the variadic parameter in `self`. This loop does + // So, any remaining positional parameters in `target` would need to be + // checked against the variadic parameter in `source`. This loop does // that by only moving the `other` iterator forward. loop { - let Some(other_parameter) = parameters.peek_other() else { + let Some(target_parameter) = parameters.peek_target() else { break; }; - match other_parameter.kind() { + match target_parameter.kind() { ParameterKind::PositionalOrKeyword { .. } => { - other_keywords.push(other_parameter); + target_keywords.push(target_parameter); } ParameterKind::PositionalOnly { .. } | ParameterKind::Variadic { .. } => {} @@ -1565,19 +1483,19 @@ impl<'db> Signature<'db> { } } if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), + target_parameter.annotated_type(), + source_param.annotated_type(), ) { return result; } - parameters.next_other(); + parameters.next_target(); } } (ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => { if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), + target_param.annotated_type(), + source_param.annotated_type(), ) { return result; } @@ -1594,153 +1512,116 @@ impl<'db> Signature<'db> { break; } - _ => return ConstraintSet::from_bool(constraints, false), + _ => return self.never(), } } } } - // At this point, the remaining parameters in `other` are keyword-only or keyword variadic. - // But, `self` could contain any unmatched positional parameters. - let (self_parameters, other_parameters) = parameters.into_remaining(); + // At this point, the remaining parameters in `target` are keyword-only or keyword-variadic. + // But, `source` could contain any unmatched positional parameters. + let (source_params, target_params) = parameters.into_remaining(); // Collect all the keyword-only parameters and the unmatched standard parameters. - let mut self_keywords = FxHashMap::default(); + let mut source_keywords = FxHashMap::default(); - // Type of the variadic keyword parameter in `self`. + // Type of the variadic keyword parameter in `source`. // - // This is an option representing the presence (and annotated type) of a keyword variadic - // parameter in `self`. - let mut self_keyword_variadic: Option> = None; + // This is an option representing the presence (and annotated type) of a keyword-variadic + // parameter in `source`. + let mut source_keyword_variadic: Option> = None; - for self_parameter in self_parameters { - match self_parameter.kind() { + for source_param in source_params { + match source_param.kind() { ParameterKind::KeywordOnly { name, .. } | ParameterKind::PositionalOrKeyword { name, .. } => { - self_keywords.insert(name.as_str(), self_parameter); + source_keywords.insert(name.as_str(), source_param); } ParameterKind::KeywordVariadic { .. } => { - self_keyword_variadic = Some(self_parameter.annotated_type()); + source_keyword_variadic = Some(source_param.annotated_type()); } ParameterKind::PositionalOnly { default_type, .. } => { - // These are the unmatched positional-only parameters in `self` from the - // previous loop. They cannot be matched against any parameter in `other` which + // These are the unmatched positional-only parameters in `source` from the + // previous loop. They cannot be matched against any parameter in `target` which // only contains keyword-only and keyword-variadic parameters. However, if the // parameter has a default, it's valid because callers don't need to provide it. if default_type.is_none() { - return ConstraintSet::from_bool(constraints, false); + return self.never(); } } ParameterKind::Variadic { .. } => {} } } - for other_parameter in other_keywords.into_iter().chain(other_parameters) { - match other_parameter.kind() { + for target_param in target_keywords.into_iter().chain(target_params) { + match target_param.kind() { ParameterKind::KeywordOnly { - name: other_name, - default_type: other_default, + name: target_name, + default_type: target_default, } | ParameterKind::PositionalOrKeyword { - name: other_name, - default_type: other_default, + name: target_name, + default_type: target_default, } => { - if let Some(self_parameter) = self_keywords.remove(other_name.as_str()) { - match self_parameter.kind() { + if let Some(source_param) = source_keywords.remove(&**target_name) { + match source_param.kind() { ParameterKind::PositionalOrKeyword { - default_type: self_default, + default_type: source_default, .. } | ParameterKind::KeywordOnly { - default_type: self_default, + default_type: source_default, .. } => { - if self_default.is_none() && other_default.is_some() { - return ConstraintSet::from_bool(constraints, false); + if source_default.is_none() && target_default.is_some() { + return self.never(); } if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), + target_param.annotated_type(), + source_param.annotated_type(), ) { return result; } } _ => unreachable!( - "`self_keywords` should only contain keyword-only or standard parameters" + "`source_keywords` should only contain keyword-only or standard parameters" ), } - } else if let Some(self_keyword_variadic_type) = self_keyword_variadic { - if !check_types( - other_parameter.annotated_type(), - self_keyword_variadic_type, - ) { + } else if let Some(source_keyword_variadic) = source_keyword_variadic { + if !check_types(target_param.annotated_type(), source_keyword_variadic) { return result; } } else { - return ConstraintSet::from_bool(constraints, false); + return self.never(); } } ParameterKind::KeywordVariadic { .. } => { - let Some(self_keyword_variadic_type) = self_keyword_variadic else { - // For a `self <: other` relationship, if `other` has a keyword variadic - // parameter, `self` must also have a keyword variadic parameter. - return ConstraintSet::from_bool(constraints, false); + let Some(source_keyword_variadic) = source_keyword_variadic else { + // For a `source <: target` relationship, if `target` has a keyword variadic + // parameter, `source` must also have a keyword variadic parameter. + return self.never(); }; - if !check_types(other_parameter.annotated_type(), self_keyword_variadic_type) { + if !check_types(target_param.annotated_type(), source_keyword_variadic) { return result; } } _ => { // This can only occur in case of a syntax error. - return ConstraintSet::from_bool(constraints, false); + return self.never(); } } } - // If there are still unmatched keyword parameters from `self`, then they should be + // If there are still unmatched keyword parameters from `source`, then they should be // optional otherwise the subtype relation is invalid. - for (_, self_parameter) in self_keywords { - if self_parameter.default_type().is_none() { - return ConstraintSet::from_bool(constraints, false); + for (_, source_param) in source_keywords { + if source_param.default_type().is_none() { + return self.never(); } } result } - - /// Create a new signature with the given definition. - pub(crate) fn with_definition(self, definition: Option>) -> Self { - Self { definition, ..self } - } - - /// Create a new signature with the given return type. - pub(crate) fn with_return_type(self, return_ty: Type<'db>) -> Self { - Self { return_ty, ..self } - } -} - -impl<'db> VarianceInferable<'db> for &Signature<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { - tracing::trace!( - "Checking variance of `{tvar}` in `{self:?}`", - tvar = typevar.typevar(db).name(db) - ); - itertools::chain( - self.parameters - .iter() - .filter_map(|parameter| match parameter.form { - ParameterForm::Type => None, - ParameterForm::Value => Some( - parameter - .annotated_type() - .with_polarity(TypeVarVariance::Contravariant) - .variance_of(db, typevar), - ), - }), - Some(self.return_ty.variance_of(db, typevar)), - ) - .collect() - } } // TODO: the spec also allows signatures like `Concatenate[int, ...]` or `Concatenate[int, P]`, diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index c8e0e8fadcc4a..46e9836058370 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,10 +1,9 @@ use crate::place::PlaceAndQualifiers; use crate::semantic_index::definition::Definition; use crate::types::class::DynamicClassLiteral; -use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; -use crate::types::generics::InferableTypeVars; +use crate::types::constraints::ConstraintSet; use crate::types::protocol_class::ProtocolClass; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::relation::{DisjointnessChecker, TypeRelationChecker}; use crate::types::variance::VarianceInferable; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, ClassType, DynamicType, @@ -216,79 +215,6 @@ impl<'db> SubclassOfType<'db> { class_like.find_name_in_mro_with_policy(db, name, policy) } - /// Return `true` if `self` has a certain relation to `other`. - #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( - self, - db: &'db dyn Db, - other: SubclassOfType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match (self.subclass_of, other.subclass_of) { - (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from_bool(constraints, !relation.is_subtyping()) - } - (SubclassOfInner::Dynamic(_), SubclassOfInner::Class(other_class)) => { - ConstraintSet::from_bool( - constraints, - other_class.is_object(db) || relation.is_assignability(), - ) - } - (SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from_bool(constraints, relation.is_assignability()) - } - - // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, - // and `type[int]` describes all possible runtime subclasses of the class `int`. - // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. - (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => self_class - .has_relation_to_impl( - db, - other_class, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - - (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { - unreachable!() - } - } - } - - /// Return` true` if `self` is a disjoint type from `other`. - /// - /// See [`Type::is_disjoint_from`] for more details. - pub(crate) fn is_disjoint_from_impl<'c>( - self, - db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - _inferable: InferableTypeVars<'_, 'db>, - _visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match (self.subclass_of, other.subclass_of) { - (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from_bool(constraints, false) - } - (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { - ConstraintSet::from_bool( - constraints, - !self_class.could_coexist_in_mro_with(db, other_class, constraints), - ) - } - (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { - unreachable!() - } - } - } - pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, @@ -355,6 +281,69 @@ impl<'db> VarianceInferable<'db> for SubclassOfType<'db> { } } +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + /// Return `true` if `source` has a certain relation to `other`. + pub(crate) fn check_subclassof_pair( + &self, + db: &'db dyn Db, + source: SubclassOfType<'db>, + target: SubclassOfType<'db>, + ) -> ConstraintSet<'db, 'c> { + match (source.subclass_of, target.subclass_of) { + (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { + ConstraintSet::from_bool(self.constraints, !self.relation.is_subtyping()) + } + (SubclassOfInner::Dynamic(_), SubclassOfInner::Class(target_class)) => { + ConstraintSet::from_bool( + self.constraints, + target_class.is_object(db) || self.relation.is_assignability(), + ) + } + (SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => { + ConstraintSet::from_bool(self.constraints, self.relation.is_assignability()) + } + + // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, + // and `type[int]` describes all possible runtime subclasses of the class `int`. + // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. + (SubclassOfInner::Class(source), SubclassOfInner::Class(target)) => { + self.check_class_pair(db, source, target) + } + + (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { + unreachable!() + } + } + } +} + +impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { + /// Return` true` if `left` is a disjoint type from `right`. + /// + /// See [`Type::is_disjoint_from`] for more details. + pub(super) fn check_subclassof_pair( + &self, + db: &'db dyn Db, + left: SubclassOfType<'db>, + right: SubclassOfType<'db>, + ) -> ConstraintSet<'db, 'c> { + match (left.subclass_of, right.subclass_of) { + (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => { + ConstraintSet::from_bool(self.constraints, false) + } + (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => { + ConstraintSet::from_bool( + self.constraints, + !left.could_coexist_in_mro_with(db, right, self.constraints), + ) + } + (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { + unreachable!() + } + } + } +} + /// An enumeration of the different kinds of `type[]` types that a [`SubclassOfType`] can represent: /// /// 1. A "subclass of a class": `type[C]` for any class object `C` diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 4c602584db19d..e197814f3989c 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -25,11 +25,8 @@ use smallvec::{SmallVec, smallvec_inline}; use crate::semantic_index::definition::Definition; use crate::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; use crate::types::class::{ClassType, KnownClass}; -use crate::types::constraints::{ - ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, -}; -use crate::types::generics::InferableTypeVars; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; +use crate::types::relation::{DisjointnessChecker, TypeRelationChecker}; use crate::types::set_theoretic::RecursivelyDefined; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, IntersectionType, @@ -258,49 +255,311 @@ impl<'db> TupleType<'db> { .find_legacy_typevars_impl(db, binding_context, typevars, visitor); } - #[expect(clippy::too_many_arguments)] - pub(crate) fn has_relation_to_impl<'c>( - self, + pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { + self.tuple(db).is_single_valued(db) + } +} + +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { + pub(super) fn check_tuple_type_pair( + &self, db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + source: TupleType<'db>, + target: TupleType<'db>, ) -> ConstraintSet<'db, 'c> { - self.tuple(db).has_relation_to_impl( - db, - other.tuple(db), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.check_tuple_spec_pair(db, source.tuple(db), target.tuple(db)) } - pub(crate) fn is_disjoint_from_impl<'c>( - self, + fn check_tuple_spec_pair( + &self, db: &'db dyn Db, - other: Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, + source: &TupleSpec<'db>, + target: &TupleSpec<'db>, ) -> ConstraintSet<'db, 'c> { - self.tuple(db).is_disjoint_from_impl( - db, - other.tuple(db), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + match source { + Tuple::Fixed(source) => self.check_fixed_length_tuple_vs_tuple_spec(db, source, target), + Tuple::Variable(source) => self.check_variable_length_vs_tuple_spec(db, source, target), + } } - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { - self.tuple(db).is_single_valued(db) + fn check_fixed_length_tuple_vs_tuple_spec( + &self, + db: &'db dyn Db, + source: &FixedLengthTuple>, + target: &TupleSpec<'db>, + ) -> ConstraintSet<'db, 'c> { + match target { + Tuple::Fixed(target) => ConstraintSet::from_bool( + self.constraints, + source.0.len() == target.0.len(), + ) + .and(db, self.constraints, || { + (source.0.iter().zip(&target.0)).when_all( + db, + self.constraints, + |(&source, &target)| self.check_type_pair(db, source, target), + ) + }), + + Tuple::Variable(target) => { + // This tuple must have enough elements to match up with the other tuple's prefix + // and suffix, and each of those elements must pairwise satisfy the relation. + let mut result = self.always(); + let mut source_iter = source.0.iter(); + for &target_ty in target.prefix_elements() { + let Some(&source_ty) = source_iter.next() else { + return self.never(); + }; + let element_constraints = self.check_type_pair(db, source_ty, target_ty); + if result + .intersect(db, self.constraints, element_constraints) + .is_never_satisfied(db) + { + return result; + } + } + for target_ty in target.iter_suffix_elements().rev() { + let Some(&source_ty) = source_iter.next_back() else { + return self.never(); + }; + let element_constraints = self.check_type_pair(db, source_ty, target_ty); + if result + .intersect(db, self.constraints, element_constraints) + .is_never_satisfied(db) + { + return result; + } + } + + // In addition, any remaining elements in this tuple must satisfy the + // variable-length portion of the other tuple. + result.and(db, self.constraints, || { + source_iter.when_all(db, self.constraints, |&source_ty| { + self.check_type_pair(db, source_ty, target.variable()) + }) + }) + } + } + } + + fn check_variable_length_vs_tuple_spec( + &self, + db: &'db dyn Db, + source: &VariableLengthTuple>, + target: &TupleSpec<'db>, + ) -> ConstraintSet<'db, 'c> { + match target { + Tuple::Fixed(target) => { + // The `...` length specifier of a variable-length tuple type is interpreted + // differently depending on the type of the variable-length elements. + // + // It typically represents the _union_ of all possible lengths. That means that a + // variable-length tuple type is not a subtype of _any_ fixed-length tuple type. + // + // However, as a special case, if the variable-length portion of the tuple is `Any` + // (or any other dynamic type), then the `...` is the _gradual choice_ of all + // possible lengths. This means that `tuple[Any, ...]` can match any tuple of any + // length. + if !self.relation.is_assignability() || !source.variable().is_dynamic() { + return self.never(); + } + + // In addition, the other tuple must have enough elements to match up with this + // tuple's prefix and suffix, and each of those elements must pairwise satisfy the + // relation. + let mut result = self.always(); + let mut target_iter = target.iter_all_elements(); + for source_ty in source.prenormalized_prefix_elements(db, None) { + let Some(target_ty) = target_iter.next() else { + return self.never(); + }; + let element_constraints = self.check_type_pair(db, source_ty, target_ty); + if result + .intersect(db, self.constraints, element_constraints) + .is_never_satisfied(db) + { + return result; + } + } + let suffix: Vec<_> = source.prenormalized_suffix_elements(db, None).collect(); + for &source_ty in suffix.iter().rev() { + let Some(target_ty) = target_iter.next_back() else { + return self.never(); + }; + let element_constraints = self.check_type_pair(db, source_ty, target_ty); + if result + .intersect(db, self.constraints, element_constraints) + .is_never_satisfied(db) + { + return result; + } + } + + result + } + + Tuple::Variable(target) => { + // When prenormalizing below, we assume that a dynamic variable-length portion of + // one tuple materializes to the variable-length portion of the other tuple. + let source_prenormalize_variable = match source.variable() { + Type::Dynamic(_) => Some(target.variable()), + _ => None, + }; + let target_prenormalize_variable = match target.variable() { + Type::Dynamic(_) => Some(source.variable()), + _ => None, + }; + + // The overlapping parts of the prefixes and suffixes must satisfy the relation. + // Any remaining parts must satisfy the relation with the other tuple's + // variable-length part. + let mut result = self.always(); + let pairwise = source + .prenormalized_prefix_elements(db, source_prenormalize_variable) + .zip_longest( + target.prenormalized_prefix_elements(db, target_prenormalize_variable), + ); + for pair in pairwise { + let pair_constraints = match pair { + EitherOrBoth::Both(self_ty, other_ty) => { + self.check_type_pair(db, self_ty, other_ty) + } + EitherOrBoth::Left(self_ty) => { + self.check_type_pair(db, self_ty, target.variable()) + } + EitherOrBoth::Right(other_ty) => { + // The rhs has a required element that the lhs is not guaranteed to + // provide, unless the lhs has a dynamic variable-length portion + // that can materialize to provide it (for assignability only), + // as in `tuple[Any, ...]` matching `tuple[int, int]`. + if !self.relation.is_assignability() || !source.variable().is_dynamic() + { + return self.never(); + } + self.check_type_pair(db, source.variable(), other_ty) + } + }; + if result + .intersect(db, self.constraints, pair_constraints) + .is_never_satisfied(db) + { + return result; + } + } + + let source_suffix: Vec<_> = source + .prenormalized_suffix_elements(db, source_prenormalize_variable) + .collect(); + let target_suffix: Vec<_> = target + .prenormalized_suffix_elements(db, target_prenormalize_variable) + .collect(); + let pairwise = source_suffix + .iter() + .rev() + .zip_longest(target_suffix.iter().rev()); + for pair in pairwise { + let pair_constraints = match pair { + EitherOrBoth::Both(&source_ty, &target_ty) => { + self.check_type_pair(db, source_ty, target_ty) + } + EitherOrBoth::Left(&source_ty) => { + self.check_type_pair(db, source_ty, target.variable()) + } + EitherOrBoth::Right(&target_ty) => { + // The rhs has a required element that the lhs is not guaranteed to + // provide, unless the lhs has a dynamic variable-length portion + // that can materialize to provide it (for assignability only), + // as in `tuple[Any, ...]` matching `tuple[int, int]`. + if !self.relation.is_assignability() || !source.variable().is_dynamic() + { + return self.never(); + } + self.check_type_pair(db, source.variable(), target_ty) + } + }; + if result + .intersect(db, self.constraints, pair_constraints) + .is_never_satisfied(db) + { + return result; + } + } + + // And lastly, the variable-length portions must satisfy the relation. + result.and(db, self.constraints, || { + self.check_type_pair(db, source.variable(), target.variable()) + }) + } + } + } +} + +impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { + pub(super) fn check_tuple_type_pair( + &self, + db: &'db dyn Db, + left: TupleType<'db>, + right: TupleType<'db>, + ) -> ConstraintSet<'db, 'c> { + self.check_tuple_spec_pair(db, left.tuple(db), right.tuple(db)) + } + + pub(super) fn check_tuple_spec_pair( + &self, + db: &'db dyn Db, + left: &TupleSpec<'db>, + right: &TupleSpec<'db>, + ) -> ConstraintSet<'db, 'c> { + // Two tuples with an incompatible number of required elements must always be disjoint. + let (self_min, self_max) = left.len().size_hint(); + let (other_min, other_max) = right.len().size_hint(); + if self_max.is_some_and(|max| max < other_min) { + return self.always(); + } + if other_max.is_some_and(|max| max < self_min) { + return self.always(); + } + + // If any of the required elements are pairwise disjoint, the tuples are disjoint as well. + let any_disjoint = |a: &[Type<'db>], b: &[Type<'db>], rev: bool| { + if rev { + std::iter::zip(a.iter().rev(), b.iter().rev()).when_any( + db, + self.constraints, + |(&left_elem, &right_elem)| self.check_type_pair(db, left_elem, right_elem), + ) + } else { + std::iter::zip(a, b).when_any(db, self.constraints, |(&left_elem, &right_elem)| { + self.check_type_pair(db, left_elem, right_elem) + }) + } + }; + + match (left, right) { + (Tuple::Fixed(left), Tuple::Fixed(right)) => { + any_disjoint(left.all_elements(), right.all_elements(), false) + } + + // Note that we don't compare the variable-length portions; two pure homogeneous tuples + // `tuple[A, ...]` and `tuple[B, ...]` can never be disjoint even if A and B are + // disjoint, because `tuple[()]` would be assignable to both. + (Tuple::Variable(left), Tuple::Variable(right)) => { + any_disjoint(left.prefix_elements(), right.prefix_elements(), false).or( + db, + self.constraints, + || any_disjoint(left.suffix_elements(), right.suffix_elements(), true), + ) + } + + (Tuple::Fixed(fixed), Tuple::Variable(variable)) + | (Tuple::Variable(variable), Tuple::Fixed(fixed)) => { + any_disjoint(fixed.all_elements(), variable.prefix_elements(), false).or( + db, + self.constraints, + || any_disjoint(fixed.all_elements(), variable.suffix_elements(), true), + ) + } + } } } @@ -478,101 +737,6 @@ impl<'db> FixedLengthTuple> { } } - #[expect(clippy::too_many_arguments)] - fn has_relation_to_impl<'c>( - &self, - db: &'db dyn Db, - other: &Tuple>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match other { - Tuple::Fixed(other) => ConstraintSet::from_bool( - constraints, - self.0.len() == other.0.len(), - ) - .and(db, constraints, || { - (self.0.iter().zip(&other.0)).when_all(db, constraints, |(self_ty, other_ty)| { - self_ty.has_relation_to_impl( - db, - *other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - }), - - Tuple::Variable(other) => { - // This tuple must have enough elements to match up with the other tuple's prefix - // and suffix, and each of those elements must pairwise satisfy the relation. - let mut result = ConstraintSet::from_bool(constraints, true); - let mut self_iter = self.0.iter(); - for other_ty in other.prefix_elements() { - let Some(self_ty) = self_iter.next() else { - return ConstraintSet::from_bool(constraints, false); - }; - let element_constraints = self_ty.has_relation_to_impl( - db, - *other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - if result - .intersect(db, constraints, element_constraints) - .is_never_satisfied(db) - { - return result; - } - } - for other_ty in other.iter_suffix_elements().rev() { - let Some(self_ty) = self_iter.next_back() else { - return ConstraintSet::from_bool(constraints, false); - }; - let element_constraints = self_ty.has_relation_to_impl( - db, - other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - if result - .intersect(db, constraints, element_constraints) - .is_never_satisfied(db) - { - return result; - } - } - - // In addition, any remaining elements in this tuple must satisfy the - // variable-length portion of the other tuple. - result.and(db, constraints, || { - self_iter.when_all(db, constraints, |self_ty| { - self_ty.has_relation_to_impl( - db, - other.variable(), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - }) - } - } - } - fn is_single_valued(&self, db: &'db dyn Db) -> bool { self.0.iter().all(|ty| ty.is_single_valued(db)) } @@ -966,224 +1130,6 @@ impl<'db> VariableLengthTuple> { ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); } } - - #[expect(clippy::too_many_arguments)] - fn has_relation_to_impl<'c>( - &self, - db: &'db dyn Db, - other: &Tuple>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match other { - Tuple::Fixed(other) => { - // The `...` length specifier of a variable-length tuple type is interpreted - // differently depending on the type of the variable-length elements. - // - // It typically represents the _union_ of all possible lengths. That means that a - // variable-length tuple type is not a subtype of _any_ fixed-length tuple type. - // - // However, as a special case, if the variable-length portion of the tuple is `Any` - // (or any other dynamic type), then the `...` is the _gradual choice_ of all - // possible lengths. This means that `tuple[Any, ...]` can match any tuple of any - // length. - if !relation.is_assignability() || !self.variable().is_dynamic() { - return ConstraintSet::from_bool(constraints, false); - } - - // In addition, the other tuple must have enough elements to match up with this - // tuple's prefix and suffix, and each of those elements must pairwise satisfy the - // relation. - let mut result = ConstraintSet::from_bool(constraints, true); - let mut other_iter = other.iter_all_elements(); - for self_ty in self.prenormalized_prefix_elements(db, None) { - let Some(other_ty) = other_iter.next() else { - return ConstraintSet::from_bool(constraints, false); - }; - let element_constraints = self_ty.has_relation_to_impl( - db, - other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - if result - .intersect(db, constraints, element_constraints) - .is_never_satisfied(db) - { - return result; - } - } - let suffix: Vec<_> = self.prenormalized_suffix_elements(db, None).collect(); - for self_ty in suffix.iter().rev() { - let Some(other_ty) = other_iter.next_back() else { - return ConstraintSet::from_bool(constraints, false); - }; - let element_constraints = self_ty.has_relation_to_impl( - db, - other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ); - if result - .intersect(db, constraints, element_constraints) - .is_never_satisfied(db) - { - return result; - } - } - - result - } - - Tuple::Variable(other) => { - // When prenormalizing below, we assume that a dynamic variable-length portion of - // one tuple materializes to the variable-length portion of the other tuple. - let self_prenormalize_variable = match self.variable() { - Type::Dynamic(_) => Some(other.variable()), - _ => None, - }; - let other_prenormalize_variable = match other.variable() { - Type::Dynamic(_) => Some(self.variable()), - _ => None, - }; - - // The overlapping parts of the prefixes and suffixes must satisfy the relation. - // Any remaining parts must satisfy the relation with the other tuple's - // variable-length part. - let mut result = ConstraintSet::from_bool(constraints, true); - let pairwise = self - .prenormalized_prefix_elements(db, self_prenormalize_variable) - .zip_longest( - other.prenormalized_prefix_elements(db, other_prenormalize_variable), - ); - for pair in pairwise { - let pair_constraints = match pair { - EitherOrBoth::Both(self_ty, other_ty) => self_ty.has_relation_to_impl( - db, - other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - EitherOrBoth::Left(self_ty) => self_ty.has_relation_to_impl( - db, - other.variable(), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - EitherOrBoth::Right(other_ty) => { - // The rhs has a required element that the lhs is not guaranteed to - // provide, unless the lhs has a dynamic variable-length portion - // that can materialize to provide it (for assignability only), - // as in `tuple[Any, ...]` matching `tuple[int, int]`. - if !relation.is_assignability() || !self.variable().is_dynamic() { - return ConstraintSet::from_bool(constraints, false); - } - self.variable().has_relation_to_impl( - db, - other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } - }; - if result - .intersect(db, constraints, pair_constraints) - .is_never_satisfied(db) - { - return result; - } - } - - let self_suffix: Vec<_> = self - .prenormalized_suffix_elements(db, self_prenormalize_variable) - .collect(); - let other_suffix: Vec<_> = other - .prenormalized_suffix_elements(db, other_prenormalize_variable) - .collect(); - let pairwise = self_suffix - .iter() - .rev() - .zip_longest(other_suffix.iter().rev()); - for pair in pairwise { - let pair_constraints = match pair { - EitherOrBoth::Both(self_ty, other_ty) => self_ty.has_relation_to_impl( - db, - *other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - EitherOrBoth::Left(self_ty) => self_ty.has_relation_to_impl( - db, - other.variable(), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - EitherOrBoth::Right(other_ty) => { - // The rhs has a required element that the lhs is not guaranteed to - // provide, unless the lhs has a dynamic variable-length portion - // that can materialize to provide it (for assignability only), - // as in `tuple[Any, ...]` matching `tuple[int, int]`. - if !relation.is_assignability() || !self.variable().is_dynamic() { - return ConstraintSet::from_bool(constraints, false); - } - self.variable().has_relation_to_impl( - db, - *other_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } - }; - if result - .intersect(db, constraints, pair_constraints) - .is_never_satisfied(db) - { - return result; - } - } - - // And lastly, the variable-length portions must satisfy the relation. - result.and(db, constraints, || { - self.variable().has_relation_to_impl( - db, - other.variable(), - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - } - } - } } impl<'db> PyIndex<'db> for &VariableLengthTuple> { @@ -1396,167 +1342,6 @@ impl<'db> Tuple> { } } - #[expect(clippy::too_many_arguments)] - fn has_relation_to_impl<'c>( - &self, - db: &'db dyn Db, - other: &Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - match self { - Tuple::Fixed(self_tuple) => self_tuple.has_relation_to_impl( - db, - other, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - Tuple::Variable(self_tuple) => self_tuple.has_relation_to_impl( - db, - other, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ), - } - } - - pub(super) fn is_disjoint_from_impl<'c>( - &self, - db: &'db dyn Db, - other: &Self, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - ) -> ConstraintSet<'db, 'c> { - // Two tuples with an incompatible number of required elements must always be disjoint. - let (self_min, self_max) = self.len().size_hint(); - let (other_min, other_max) = other.len().size_hint(); - if self_max.is_some_and(|max| max < other_min) { - return ConstraintSet::from_bool(constraints, true); - } - if other_max.is_some_and(|max| max < self_min) { - return ConstraintSet::from_bool(constraints, true); - } - - // If any of the required elements are pairwise disjoint, the tuples are disjoint as well. - #[allow(clippy::items_after_statements)] - #[expect(clippy::too_many_arguments)] - fn any_disjoint<'s, 'db, 'c>( - db: &'db dyn Db, - a: &'s [Type<'db>], - b: &'s [Type<'db>], - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - rev: bool, - ) -> ConstraintSet<'db, 'c> - where - 'db: 's, - { - if rev { - std::iter::zip(a.iter().rev(), b.iter().rev()).when_any( - db, - constraints, - |(self_element, other_element)| { - self_element.is_disjoint_from_impl( - db, - *other_element, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - }, - ) - } else { - std::iter::zip(a, b).when_any(db, constraints, |(self_element, other_element)| { - self_element.is_disjoint_from_impl( - db, - *other_element, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) - }) - } - } - - match (self, other) { - (Tuple::Fixed(self_tuple), Tuple::Fixed(other_tuple)) => any_disjoint( - db, - self_tuple.all_elements(), - other_tuple.all_elements(), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - false, - ), - - // Note that we don't compare the variable-length portions; two pure homogeneous tuples - // `tuple[A, ...]` and `tuple[B, ...]` can never be disjoint even if A and B are - // disjoint, because `tuple[()]` would be assignable to both. - (Tuple::Variable(self_tuple), Tuple::Variable(other_tuple)) => any_disjoint( - db, - self_tuple.prefix_elements(), - other_tuple.prefix_elements(), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - false, - ) - .or(db, constraints, || { - any_disjoint( - db, - self_tuple.suffix_elements(), - other_tuple.suffix_elements(), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - true, - ) - }), - - (Tuple::Fixed(fixed), Tuple::Variable(variable)) - | (Tuple::Variable(variable), Tuple::Fixed(fixed)) => any_disjoint( - db, - fixed.all_elements(), - variable.prefix_elements(), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - false, - ) - .or(db, constraints, || { - any_disjoint( - db, - fixed.all_elements(), - variable.suffix_elements(), - constraints, - inferable, - disjointness_visitor, - relation_visitor, - true, - ) - }), - } - } - pub(crate) fn is_single_valued(&self, db: &'db dyn Db) -> bool { match self { Tuple::Fixed(tuple) => tuple.is_single_valued(db), diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 5624532d5dc17..865f51c9000b5 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -22,11 +22,8 @@ use crate::semantic_index::definition::Definition; use crate::types::TypeContext; use crate::types::TypeDefinition; use crate::types::class::FieldKind; -use crate::types::constraints::{ - ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, -}; -use crate::types::generics::InferableTypeVars; -use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; +use crate::types::relation::{DisjointnessChecker, TypeRelation, TypeRelationChecker}; bitflags! { /// Used for `TypedDict` class parameters. @@ -154,42 +151,49 @@ impl<'db> TypedDictType<'db> { Self::from_patch_items(db, items) } + pub fn definition(self, db: &'db dyn Db) -> Option> { + match self { + TypedDictType::Class(defining_class) => defining_class.definition(db), + TypedDictType::Synthesized(_) => None, + } + } + + pub fn type_definition(self, db: &'db dyn Db) -> Option> { + match self { + TypedDictType::Class(defining_class) => defining_class.type_definition(db), + TypedDictType::Synthesized(_) => None, + } + } +} + +impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // Subtyping between `TypedDict`s follows the algorithm described at: // https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types - #[expect(clippy::too_many_arguments)] - pub(super) fn has_relation_to_impl<'c>( - self, + pub(super) fn check_typeddict_pair( + &self, db: &'db dyn Db, + source: TypedDictType<'db>, target: TypedDictType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - relation: TypeRelation, - relation_visitor: &HasRelationToVisitor<'db, 'c>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, ) -> ConstraintSet<'db, 'c> { if let TypedDictType::Synthesized(synthesized_target) = target && synthesized_target.is_patch(db) { - let self_items = self.items(db); + let source_items = source.items(db); let target_items = synthesized_target.items(db); - let mut result = ConstraintSet::from_bool(constraints, true); + let mut result = self.always(); - for (self_item_name, self_item_field) in self_items { - let Some(target_item_field) = target_items.get(self_item_name) else { + for (source_item_name, source_item_field) in source_items { + let Some(target_item_field) = target_items.get(source_item_name) else { continue; }; result.intersect( db, - constraints, - self_item_field.declared_ty.has_relation_to_impl( + self.constraints, + self.check_type_pair( db, + source_item_field.declared_ty, target_item_field.declared_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, ), ); @@ -202,99 +206,81 @@ impl<'db> TypedDictType<'db> { } // First do a quick nominal check that (if it succeeds) means that we can avoid - // materializing the full `TypedDict` schema for either `self` or `target`. + // materializing the full `TypedDict` schema for either `source` or `target`. // This should be cheaper in many cases, and also helps us avoid some cycles. - if let Some(defining_class) = self.defining_class() + if let Some(defining_class) = source.defining_class() && let Some(target_defining_class) = target.defining_class() && defining_class.is_subclass_of(db, target_defining_class) { - return ConstraintSet::from_bool(constraints, true); + return self.always(); } - let self_items = self.items(db); + let source_items = source.items(db); let target_items = target.items(db); // Many rules violations short-circuit with "never", but asking whether one field is // [relation] to/of another can produce more complicated constraints, and we collect those. - let mut result = ConstraintSet::from_bool(constraints, true); + let mut result = self.always(); for (target_item_name, target_item_field) in target_items { let field_constraints = if target_item_field.is_required() { // required target fields - let Some(self_item_field) = self_items.get(target_item_name) else { + let Some(source_item_field) = source_items.get(target_item_name) else { // Self is missing a required field. - return ConstraintSet::from_bool(constraints, false); + return self.never(); }; - if !self_item_field.is_required() { + if !source_item_field.is_required() { // A required field is not required in self. - return ConstraintSet::from_bool(constraints, false); + return self.never(); } if target_item_field.is_read_only() { // For `ReadOnly[]` fields in the target, the corresponding fields in // self need to have the same assignability/subtyping/etc relation // individually that we're looking for overall between the // `TypedDict`s. - self_item_field.declared_ty.has_relation_to_impl( + self.check_type_pair( db, + source_item_field.declared_ty, target_item_field.declared_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, ) } else { - if self_item_field.is_read_only() { + if source_item_field.is_read_only() { // A read-only field can't be assigned to a mutable target. - return ConstraintSet::from_bool(constraints, false); + return self.never(); } // For mutable fields in the target, the relation needs to apply both // ways, or else mutating the target could violate the structural // invariants of self. For fully-static types, this is "equivalence". // For gradual types, it depends on the relation, but mutual // assignability is "consistency". - self_item_field - .declared_ty - .has_relation_to_impl( + self.check_type_pair( + db, + source_item_field.declared_ty, + target_item_field.declared_ty, + ) + .and(db, self.constraints, || { + self.check_type_pair( db, target_item_field.declared_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, + source_item_field.declared_ty, ) - .and(db, constraints, || { - target_item_field.declared_ty.has_relation_to_impl( - db, - self_item_field.declared_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + }) } } else { // `NotRequired[]` target fields if target_item_field.is_read_only() { // As above, for `NotRequired[]` + `ReadOnly[]` fields in the target. It's // tempting to refactor things and unify some of these calls to - // `has_relation_to_impl`, but this branch will get more complicated when we + // `check_typeddict_pair`, but this branch will get more complicated when we // add support for `closed` and `extra_items` (which is why the rules in the // spec are structured like they are), and following the structure of the spec // makes it easier to check the logic here. - if let Some(self_item_field) = self_items.get(target_item_name) { - self_item_field.declared_ty.has_relation_to_impl( + if let Some(source_item_field) = source_items.get(target_item_name) { + self.check_type_pair( db, + source_item_field.declared_ty, target_item_field.declared_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, ) } else { - // Self is missing this not-required, read-only item. However, since all + // `source` is missing this not-required, read-only item. However, since all // `TypedDict`s by default are allowed to have "extra items" of any type // (until we support `closed` and explicit `extra_items`), this key could // actually turn out to have a value. To make sure this is type-safe, the @@ -303,82 +289,61 @@ impl<'db> TypedDictType<'db> { Type::object().when_assignable_to( db, target_item_field.declared_ty, - constraints, - inferable, + self.constraints, + self.inferable, ) } } else { // As above, for `NotRequired[]` mutable fields in the target. Again the logic // is largely the same for now, but it will get more complicated with `closed` // and `extra_items`. - if let Some(self_item_field) = self_items.get(target_item_name) { - if self_item_field.is_read_only() { + if let Some(source_item_field) = source_items.get(target_item_name) { + if source_item_field.is_read_only() { // A read-only field can't be assigned to a mutable target. - return ConstraintSet::from_bool(constraints, false); + return self.never(); } - if self_item_field.is_required() { + if source_item_field.is_required() { // A required field can't be assigned to a not-required, mutable field // in the target, because `del` is allowed on the target field. - return ConstraintSet::from_bool(constraints, false); + return self.never(); } // As above, for mutable fields in the target, the relation needs // to apply both ways. - self_item_field - .declared_ty - .has_relation_to_impl( + self.check_type_pair( + db, + source_item_field.declared_ty, + target_item_field.declared_ty, + ) + .and(db, self.constraints, || { + self.check_type_pair( db, target_item_field.declared_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, + source_item_field.declared_ty, ) - .and(db, constraints, || { - target_item_field.declared_ty.has_relation_to_impl( - db, - self_item_field.declared_ty, - constraints, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + }) } else { - // Self is missing this not-required, mutable field. This isn't ok if self - // has read-only extra items, which all `TypedDict`s effectively do until - // we support `closed` and explicit `extra_items`. See "A subtle + // `source` is missing this not-required, mutable field. This isn't OK if + // `source has read-only extra items, which all `TypedDict`s effectively + // do until we support `closed` and explicit `extra_items`. See "A subtle // interaction between two structural assignability rules prevents // unsoundness" in `typed_dict.md`. + // // TODO: `closed` and `extra_items` support will go here. - ConstraintSet::from_bool(constraints, false) + self.never() } } }; - result.intersect(db, constraints, field_constraints); + result.intersect(db, self.constraints, field_constraints); if result.is_never_satisfied(db) { return result; } } result } +} - pub fn definition(self, db: &'db dyn Db) -> Option> { - match self { - TypedDictType::Class(defining_class) => defining_class.definition(db), - TypedDictType::Synthesized(_) => None, - } - } - - pub fn type_definition(self, db: &'db dyn Db) -> Option> { - match self { - TypedDictType::Class(defining_class) => defining_class.type_definition(db), - TypedDictType::Synthesized(_) => None, - } - } - +impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { /// Two `TypedDict`s `A` and `B` are disjoint if it's impossible to come up with a third /// `TypedDict` `C` that's fully-static and assignable to both of them. /// @@ -393,8 +358,8 @@ impl<'db> TypedDictType<'db> { /// in `C`, which we want to be assignable to both `A` and `B`) given a destination field (for /// us that means in either `A` or `B`). For completeness we'll also include the possibility /// that the source field is missing entirely, though we'll soon see that we can ignore that - /// case. This table is essentially what `has_relation_to_impl` implements above. Here - /// "equivalent" means the source and destination types must be equivalent/compatible, + /// case. This table is essentially what [`TypeRelationChecker::check_typeddict_pair`] implements + /// above. Here "equivalent" means the source and destination types must be equivalent/compatible, /// "assignable" means the source must be assignable to the destination, and "-" means the /// assignment is never allowed: /// @@ -438,91 +403,51 @@ impl<'db> TypedDictType<'db> { /// be assignable to both.) /// /// TODO: Adding support for `closed` and `extra_items` will complicate this. - pub(crate) fn is_disjoint_from_impl<'c>( - self, + pub(super) fn check_typeddict_pair( + &self, db: &'db dyn Db, - other: TypedDictType<'db>, - constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, - disjointness_visitor: &IsDisjointVisitor<'db, 'c>, - relation_visitor: &HasRelationToVisitor<'db, 'c>, + left: TypedDictType<'db>, + right: TypedDictType<'db>, ) -> ConstraintSet<'db, 'c> { - let fields_in_common = btreemap_values_with_same_key(self.items(db), other.items(db)); - fields_in_common.when_any(db, constraints, |(self_field, other_field)| { + let fields_in_common = btreemap_values_with_same_key(left.items(db), right.items(db)); + fields_in_common.when_any(db, self.constraints, |(left_field, right_field)| { // Condition 1 above. - if self_field.is_required() || other_field.is_required() { - if (!self_field.is_required() && !self_field.is_read_only()) - || (!other_field.is_required() && !other_field.is_read_only()) + if left_field.is_required() || right_field.is_required() { + if (!left_field.is_required() && !left_field.is_read_only()) + || (!right_field.is_required() && !right_field.is_read_only()) { // One side demands a `Required` source field, while the other side demands a // `NotRequired` one. They must be disjoint. - return ConstraintSet::from_bool(constraints, true); + return self.always(); } } - if !self_field.is_read_only() && !other_field.is_read_only() { + if !left_field.is_read_only() && !right_field.is_read_only() { // Condition 2 above. This field is mutable on both sides, so the so the types must // be compatible, i.e. mutually assignable. - self_field - .declared_ty - .has_relation_to_impl( - db, - other_field.declared_ty, - constraints, - inferable, - TypeRelation::Assignability, - relation_visitor, - disjointness_visitor, - ) - .and(db, constraints, || { - other_field.declared_ty.has_relation_to_impl( + let relation_checker = self.as_relation_checker(TypeRelation::Assignability); + relation_checker + .check_type_pair(db, left_field.declared_ty, right_field.declared_ty) + .and(db, self.constraints, || { + relation_checker.check_type_pair( db, - self_field.declared_ty, - constraints, - inferable, - TypeRelation::Assignability, - relation_visitor, - disjointness_visitor, + right_field.declared_ty, + left_field.declared_ty, ) }) - .negate(db, constraints) - } else if !self_field.is_read_only() { + .negate(db, self.constraints) + } else if !left_field.is_read_only() { // Half of condition 3 above. - self_field - .declared_ty - .has_relation_to_impl( - db, - other_field.declared_ty, - constraints, - inferable, - TypeRelation::Assignability, - relation_visitor, - disjointness_visitor, - ) - .negate(db, constraints) - } else if !other_field.is_read_only() { + self.as_relation_checker(TypeRelation::Assignability) + .check_type_pair(db, left_field.declared_ty, right_field.declared_ty) + .negate(db, self.constraints) + } else if !right_field.is_read_only() { // The other half of condition 3 above. - other_field - .declared_ty - .has_relation_to_impl( - db, - self_field.declared_ty, - constraints, - inferable, - TypeRelation::Assignability, - relation_visitor, - disjointness_visitor, - ) - .negate(db, constraints) + self.as_relation_checker(TypeRelation::Assignability) + .check_type_pair(db, right_field.declared_ty, left_field.declared_ty) + .negate(db, self.constraints) } else { // Condition 4 above. - self_field.declared_ty.is_disjoint_from_impl( - db, - other_field.declared_ty, - constraints, - inferable, - disjointness_visitor, - relation_visitor, - ) + self.check_type_pair(db, left_field.declared_ty, right_field.declared_ty) } }) }