This commit is contained in:
Alex Waygood 2025-09-05 18:41:36 +01:00
parent 5b547171bc
commit 1a73410486
11 changed files with 86 additions and 609 deletions

View File

@ -315,10 +315,11 @@ static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B])) static_assert(is_equivalent_to(C[B], C[B]))
static_assert(is_equivalent_to(C[B], C[A])) static_assert(is_equivalent_to(C[B], C[A]))
static_assert(is_equivalent_to(C[A], C[B])) static_assert(is_equivalent_to(C[A], C[B]))
static_assert(is_equivalent_to(C[A], C[Any]))
static_assert(is_equivalent_to(C[B], C[Any])) static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(is_equivalent_to(C[Any], C[A])) static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A])) static_assert(not is_equivalent_to(D[A], C[A]))
static_assert(not is_equivalent_to(D[B], C[B])) static_assert(not is_equivalent_to(D[B], C[B]))

View File

@ -1147,10 +1147,10 @@ static_assert(
) )
) )
static_assert(not is_equivalent_to(GenericProto[str], GenericProto[int])) static_assert(not is_equivalent_to(GenericProto[str], GenericProto[int])) # error: [static-assert-error]
static_assert(not is_equivalent_to(GenericProto[str], LegacyGenericProto[int])) static_assert(not is_equivalent_to(GenericProto[str], LegacyGenericProto[int])) # error: [static-assert-error]
static_assert(not is_equivalent_to(GenericProto, GenericProto[int])) static_assert(not is_equivalent_to(GenericProto, GenericProto[int])) # error: [static-assert-error]
static_assert(not is_equivalent_to(LegacyGenericProto, LegacyGenericProto[int])) static_assert(not is_equivalent_to(LegacyGenericProto, LegacyGenericProto[int])) # error: [static-assert-error]
``` ```
## Intersections of protocols ## Intersections of protocols
@ -1543,8 +1543,7 @@ from ty_extensions import is_equivalent_to
class HasMutableXAttr(Protocol): class HasMutableXAttr(Protocol):
x: int x: int
# TODO: should pass static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty))
static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) static_assert(is_subtype_of(HasMutableXAttr, HasXProperty))
static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) static_assert(is_assignable_to(HasMutableXAttr, HasXProperty))
@ -1835,9 +1834,7 @@ class P4(Protocol):
def z(self, value: int) -> None: ... def z(self, value: int) -> None: ...
static_assert(is_equivalent_to(P1, P2)) static_assert(is_equivalent_to(P1, P2))
static_assert(is_equivalent_to(P3, P4))
# TODO: should pass
static_assert(is_equivalent_to(P3, P4)) # error: [static-assert-error]
``` ```
As with protocols that only have non-method members, this also holds true when they appear in As with protocols that only have non-method members, this also holds true when they appear in
@ -1848,9 +1845,7 @@ class A: ...
class B: ... class B: ...
static_assert(is_equivalent_to(A | B | P1, P2 | B | A)) static_assert(is_equivalent_to(A | B | P1, P2 | B | A))
static_assert(is_equivalent_to(A | B | P3, P4 | B | A))
# TODO: should pass
static_assert(is_equivalent_to(A | B | P3, P4 | B | A)) # error: [static-assert-error]
``` ```
## Narrowing of protocols ## Narrowing of protocols
@ -2131,8 +2126,6 @@ class Bar(Protocol):
@property @property
def x(self) -> "Bar": ... def x(self) -> "Bar": ...
# TODO: this should pass
# error: [static-assert-error]
static_assert(is_equivalent_to(Foo, Bar)) static_assert(is_equivalent_to(Foo, Bar))
T = TypeVar("T", bound="TypeVarRecursive") T = TypeVar("T", bound="TypeVarRecursive")

View File

@ -188,10 +188,6 @@ pub(crate) type HasRelationToVisitor<'db, C> =
pub(crate) type IsDisjointVisitor<'db, C> = PairVisitor<'db, IsDisjoint, C>; pub(crate) type IsDisjointVisitor<'db, C> = PairVisitor<'db, IsDisjoint, C>;
pub(crate) struct IsDisjoint; pub(crate) struct IsDisjoint;
/// A [`PairVisitor`] that is used in `is_equivalent` methods.
pub(crate) type IsEquivalentVisitor<'db, C> = PairVisitor<'db, IsEquivalent, C>;
pub(crate) struct IsEquivalent;
/// A [`CycleDetector`] that is used in `find_legacy_typevars` methods. /// A [`CycleDetector`] that is used in `find_legacy_typevars` methods.
pub(crate) type FindLegacyTypeVarsVisitor<'db> = CycleDetector<FindLegacyTypeVars, Type<'db>, ()>; pub(crate) type FindLegacyTypeVarsVisitor<'db> = CycleDetector<FindLegacyTypeVars, Type<'db>, ()>;
pub(crate) struct FindLegacyTypeVars; pub(crate) struct FindLegacyTypeVars;
@ -1422,9 +1418,15 @@ impl<'db> Type<'db> {
// no other type is a subtype of or assignable to `Never`. // no other type is a subtype of or assignable to `Never`.
(_, Type::Never) => C::unsatisfiable(db), (_, Type::Never) => C::unsatisfiable(db),
(Type::Union(union), _) => union.elements(db).iter().when_all(db, |&elem_ty| { (Type::Union(union), _) => union
elem_ty.has_relation_to_impl(db, target, relation, visitor) .elements(db)
}), .iter()
.when_all(db, |&elem_ty| {
elem_ty.has_relation_to_impl(db, target, relation, visitor)
})
.or(db, || {
C::from_bool(db, self.normalized(db) == target.normalized(db))
}),
(_, Type::Union(union)) => union.elements(db).iter().when_any(db, |&elem_ty| { (_, Type::Union(union)) => union.elements(db).iter().when_any(db, |&elem_ty| {
self.has_relation_to_impl(db, elem_ty, relation, visitor) self.has_relation_to_impl(db, elem_ty, relation, visitor)
@ -1465,7 +1467,7 @@ impl<'db> Type<'db> {
(left, Type::AlwaysTruthy) => C::from_bool(db, left.bool(db).is_always_true()), (left, Type::AlwaysTruthy) => C::from_bool(db, left.bool(db).is_always_true()),
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
target.when_equivalent_to(db, Type::object(db)) C::from_bool(db, target.is_equivalent_to(db, Type::object(db)))
} }
// These clauses handle type variants that include function literals. A function // These clauses handle type variants that include function literals. A function
@ -1602,13 +1604,16 @@ impl<'db> Type<'db> {
(Type::Callable(_), _) => C::unsatisfiable(db), (Type::Callable(_), _) => C::unsatisfiable(db),
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.when_equivalent_to(db, target), (Type::BoundSuper(_), Type::BoundSuper(_)) => visitor
.visit((self, target, relation), || {
self.when_equivalent_to_impl(db, target, visitor)
}),
(Type::BoundSuper(_), _) => KnownClass::Super (Type::BoundSuper(_), _) => KnownClass::Super
.to_instance(db) .to_instance(db)
.has_relation_to_impl(db, target, relation, visitor), .has_relation_to_impl(db, target, relation, visitor),
// `Literal[<class 'C'>]` is a subtype of `type[B]` if `C` is a subclass of `B`, // `Literal[<class 'C'>]` 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`. // since `type[B]` describes all possible runtiexime subclasses of the class object `B`.
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
.subclass_of() .subclass_of()
.into_class() .into_class()
@ -1740,95 +1745,31 @@ impl<'db> Type<'db> {
} }
fn when_equivalent_to<C: Constraints<'db>>(self, db: &'db dyn Db, other: Type<'db>) -> C { fn when_equivalent_to<C: Constraints<'db>>(self, db: &'db dyn Db, other: Type<'db>) -> C {
self.is_equivalent_to_impl( let visitor = HasRelationToVisitor::new(C::always_satisfiable(db));
db, self.when_equivalent_to_impl(db, other, &visitor)
other,
&IsEquivalentVisitor::new(C::always_satisfiable(db)),
)
} }
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>( fn when_equivalent_to_impl<C: Constraints<'db>>(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
other: Type<'db>, other: Type<'db>,
visitor: &IsEquivalentVisitor<'db, C>, visitor: &HasRelationToVisitor<'db, C>,
) -> C { ) -> C {
if self == other { let self_relation = self.has_relation_to_impl(db, other, TypeRelation::Subtyping, visitor);
return C::always_satisfiable(db); // println!(
} // "{}, {}, {}",
// self.display(db),
match (self, other) { // other.display(db),
(Type::Dynamic(_), Type::Dynamic(_)) => C::always_satisfiable(db), // self_relation.display(db)
// );
(Type::SubclassOf(first), Type::SubclassOf(second)) => { let other_relation = other.has_relation_to_impl(db, self, TypeRelation::Subtyping, visitor);
match (first.subclass_of(), second.subclass_of()) { // println!(
(first, second) if first == second => C::always_satisfiable(db), // "{}, {}, {}",
(SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { // other.display(db),
C::always_satisfiable(db) // self.display(db),
} // other_relation.display(db)
_ => C::unsatisfiable(db), // );
} self_relation.and(db, || other_relation)
}
(Type::TypeAlias(self_alias), _) => {
let self_alias_ty = self_alias.value_type(db).normalized(db);
visitor.visit((self_alias_ty, other), || {
self_alias_ty.is_equivalent_to_impl(db, other, visitor)
})
}
(_, Type::TypeAlias(other_alias)) => {
let other_alias_ty = other_alias.value_type(db).normalized(db);
visitor.visit((self, other_alias_ty), || {
self.is_equivalent_to_impl(db, other_alias_ty, visitor)
})
}
(Type::NominalInstance(first), Type::NominalInstance(second)) => {
first.is_equivalent_to_impl(db, second, visitor)
}
(Type::Union(first), Type::Union(second)) => {
first.is_equivalent_to_impl(db, second, visitor)
}
(Type::Intersection(first), Type::Intersection(second)) => {
first.is_equivalent_to_impl(db, second, visitor)
}
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
self_function.is_equivalent_to_impl(db, target_function, visitor)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_equivalent_to_impl(db, target_method, visitor)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_equivalent_to_impl(db, target_method, visitor)
}
(Type::Callable(first), Type::Callable(second)) => {
first.is_equivalent_to_impl(db, second, visitor)
}
(Type::ProtocolInstance(first), Type::ProtocolInstance(second)) => {
first.is_equivalent_to_impl(db, second, visitor)
}
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
C::from_bool(db, n.is_object(db) && protocol.normalized(db) == nominal)
}
// An instance of an enum class is equivalent to an enum literal of that class,
// if that enum has only has one member.
(Type::NominalInstance(instance), Type::EnumLiteral(literal))
| (Type::EnumLiteral(literal), Type::NominalInstance(instance)) => {
if literal.enum_class_instance(db) != Type::NominalInstance(instance) {
return C::unsatisfiable(db);
}
let class_literal = instance.class(db).class_literal(db).0;
C::from_bool(db, is_single_member_enum(db, class_literal))
}
_ => C::unsatisfiable(db),
}
} }
/// Return true if this type and `other` have no common elements. /// Return true if this type and `other` have no common elements.
@ -8855,21 +8796,6 @@ impl<'db> BoundMethodType<'db> {
) )
}) })
} }
fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
self.function(db)
.is_equivalent_to_impl(db, other.function(db), visitor)
.and(db, || {
other
.self_instance(db)
.is_equivalent_to_impl(db, self.self_instance(db), visitor)
})
}
} }
/// This type represents the set of all callable objects with a certain, possibly overloaded, /// This type represents the set of all callable objects with a certain, possibly overloaded,
@ -9002,25 +8928,6 @@ impl<'db> CallableType<'db> {
self.signatures(db) self.signatures(db)
.has_relation_to_impl(db, other.signatures(db), relation, visitor) .has_relation_to_impl(db, other.signatures(db), relation, visitor)
} }
/// Check whether this callable type is equivalent to another callable type.
///
/// See [`Type::is_equivalent_to`] for more details.
fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
if self == other {
return C::always_satisfiable(db);
}
C::from_bool(db, self.is_function_like(db) == other.is_function_like(db)).and(db, || {
self.signatures(db)
.is_equivalent_to_impl(db, other.signatures(db), visitor)
})
}
} }
/// Represents a specific instance of `types.MethodWrapperType` /// Represents a specific instance of `types.MethodWrapperType`
@ -9108,44 +9015,6 @@ impl<'db> MethodWrapperKind<'db> {
} }
} }
fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
match (self, other) {
(
MethodWrapperKind::FunctionTypeDunderGet(self_function),
MethodWrapperKind::FunctionTypeDunderGet(other_function),
) => self_function.is_equivalent_to_impl(db, other_function, visitor),
(
MethodWrapperKind::FunctionTypeDunderCall(self_function),
MethodWrapperKind::FunctionTypeDunderCall(other_function),
) => self_function.is_equivalent_to_impl(db, other_function, visitor),
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
C::from_bool(db, self == other)
}
(
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
) => C::unsatisfiable(db),
}
}
fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
match self { match self {
MethodWrapperKind::FunctionTypeDunderGet(function) => { MethodWrapperKind::FunctionTypeDunderGet(function) => {
@ -9640,14 +9509,6 @@ impl<'db> UnionType<'db> {
} }
} }
/// Create a new union type with the elements normalized.
///
/// See [`Type::normalized`] for more details.
#[must_use]
pub(crate) fn normalized(self, db: &'db dyn Db) -> Type<'db> {
self.normalized_impl(db, &NormalizedVisitor::default())
}
pub(crate) fn normalized_impl( pub(crate) fn normalized_impl(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
@ -9664,32 +9525,6 @@ impl<'db> UnionType<'db> {
) )
.build() .build()
} }
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
_visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
if self == other {
return C::always_satisfiable(db);
}
let self_elements = self.elements(db);
let other_elements = other.elements(db);
if self_elements.len() != other_elements.len() {
return C::unsatisfiable(db);
}
let sorted_self = self.normalized(db);
if sorted_self == Type::Union(other) {
return C::always_satisfiable(db);
}
C::from_bool(db, sorted_self == other.normalized(db))
}
} }
#[salsa::interned(debug, heap_size=IntersectionType::heap_size)] #[salsa::interned(debug, heap_size=IntersectionType::heap_size)]
@ -9734,15 +9569,6 @@ impl<'db> IntersectionType<'db> {
.build() .build()
} }
/// Return a new `IntersectionType` instance with the positive and negative types sorted
/// according to a canonical ordering, and other normalizations applied to each element as applicable.
///
/// See [`Type::normalized`] for more details.
#[must_use]
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
self.normalized_impl(db, &NormalizedVisitor::default())
}
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
fn normalized_set<'db>( fn normalized_set<'db>(
db: &'db dyn Db, db: &'db dyn Db,
@ -9765,40 +9591,6 @@ impl<'db> IntersectionType<'db> {
) )
} }
/// Return `true` if `self` represents exactly the same set of possible runtime objects as `other`
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
_visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
if self == other {
return C::always_satisfiable(db);
}
let self_positive = self.positive(db);
let other_positive = other.positive(db);
if self_positive.len() != other_positive.len() {
return C::unsatisfiable(db);
}
let self_negative = self.negative(db);
let other_negative = other.negative(db);
if self_negative.len() != other_negative.len() {
return C::unsatisfiable(db);
}
let sorted_self = self.normalized(db);
if sorted_self == other {
return C::always_satisfiable(db);
}
C::from_bool(db, sorted_self == other.normalized(db))
}
/// Returns an iterator over the positive elements of the intersection. If /// Returns an iterator over the positive elements of the intersection. If
/// there are no positive elements, returns a single `object` type. /// there are no positive elements, returns a single `object` type.
fn positive_elements_or_object(self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> { fn positive_elements_or_object(self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {

View File

@ -27,10 +27,10 @@ use crate::types::typed_dict::typed_dict_params_from_class_def;
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType, ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor,
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation,
TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, UnionBuilder,
UnionBuilder, VarianceInferable, declaration_type, infer_definition_types, VarianceInferable, declaration_type, infer_definition_types,
}; };
use crate::{ use crate::{
Db, FxIndexMap, FxOrderSet, Program, Db, FxIndexMap, FxOrderSet, Program,
@ -584,33 +584,6 @@ impl<'db> ClassType<'db> {
}) })
} }
pub(super) fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: ClassType<'db>,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
if self == other {
return C::always_satisfiable(db);
}
match (self, other) {
// A non-generic class is never equivalent to a generic class.
// Two non-generic classes are only equivalent if they are equal (handled above).
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => C::unsatisfiable(db),
(ClassType::Generic(this), ClassType::Generic(other)) => {
C::from_bool(db, this.origin(db) == other.origin(db)).and(db, || {
this.specialization(db).is_equivalent_to_impl(
db,
other.specialization(db),
visitor,
)
})
}
}
}
/// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred.
pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
let (class_literal, specialization) = self.class_literal(db); let (class_literal, specialization) = self.class_literal(db);

View File

@ -81,7 +81,7 @@ use crate::Db;
use crate::types::{BoundTypeVarInstance, IntersectionType, Type, UnionType}; use crate::types::{BoundTypeVarInstance, IntersectionType, Type, UnionType};
/// Encodes the constraints under which a type property (e.g. assignability) holds. /// Encodes the constraints under which a type property (e.g. assignability) holds.
pub(crate) trait Constraints<'db>: Clone + Sized { pub(crate) trait Constraints<'db>: Clone + Sized + std::fmt::Debug {
/// Returns a constraint set that never holds /// Returns a constraint set that never holds
fn unsatisfiable(db: &'db dyn Db) -> Self; fn unsatisfiable(db: &'db dyn Db) -> Self;

View File

@ -78,9 +78,9 @@ use crate::types::signatures::{CallableSignature, Signature};
use crate::types::visitor::any_over_type; use crate::types::visitor::any_over_type;
use crate::types::{ use crate::types::{
BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType, BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType,
DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, KnownClass,
IsEquivalentVisitor, KnownClass, NormalizedVisitor, SpecialFormType, Truthiness, Type, NormalizedVisitor, SpecialFormType, Truthiness, Type, TypeMapping, TypeRelation, UnionBuilder,
TypeMapping, TypeRelation, UnionBuilder, all_members, binding_type, walk_type_mapping, all_members, binding_type, walk_type_mapping,
}; };
use crate::{Db, FxOrderSet, ModuleName, resolve_module}; use crate::{Db, FxOrderSet, ModuleName, resolve_module};
@ -981,23 +981,6 @@ impl<'db> FunctionType<'db> {
&& self.signature(db).is_assignable_to(db, other.signature(db)) && self.signature(db).is_assignable_to(db, other.signature(db))
} }
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
if self.normalized(db) == other.normalized(db) {
return C::always_satisfiable(db);
}
if self.literal(db) != other.literal(db) {
return C::unsatisfiable(db);
}
let self_signature = self.signature(db);
let other_signature = other.signature(db);
self_signature.is_equivalent_to_impl(db, other_signature, visitor)
}
pub(crate) fn find_legacy_typevars_impl( pub(crate) fn find_legacy_typevars_impl(
self, self,
db: &'db dyn Db, db: &'db dyn Db,

View File

@ -16,9 +16,9 @@ use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeMapping,
Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType,
UnionType, binding_type, declaration_type, binding_type, declaration_type,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
@ -805,7 +805,9 @@ impl<'db> Specialization<'db> {
{ {
match relation { match relation {
TypeRelation::Assignability => continue, TypeRelation::Assignability => continue,
TypeRelation::Subtyping => return C::unsatisfiable(db), TypeRelation::Subtyping => {
return C::from_bool(db, self_type.is_dynamic() && other_type.is_dynamic());
}
} }
} }
@ -841,58 +843,6 @@ impl<'db> Specialization<'db> {
result result
} }
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Specialization<'db>,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
if self.materialization_kind(db) != other.materialization_kind(db) {
return C::unsatisfiable(db);
}
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return C::unsatisfiable(db);
}
let mut result = C::always_satisfiable(db);
for ((bound_typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db))
.zip(other.types(db))
{
// Equivalence 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
// - bivariant: skip, can't make equivalence false
let compatible = match bound_typevar.variance(db) {
TypeVarVariance::Invariant
| TypeVarVariance::Covariant
| TypeVarVariance::Contravariant => {
self_type.is_equivalent_to_impl(db, *other_type, visitor)
}
TypeVarVariance::Bivariant => C::always_satisfiable(db),
};
if result.intersect(db, compatible).is_never_satisfied(db) {
return result;
}
}
match (self.tuple_inner(db), other.tuple_inner(db)) {
(Some(_), None) | (None, Some(_)) => return C::unsatisfiable(db),
(None, None) => {}
(Some(self_tuple), Some(other_tuple)) => {
let compatible = self_tuple.is_equivalent_to_impl(db, other_tuple, visitor);
if result.intersect(db, compatible).is_never_satisfied(db) {
return result;
}
}
}
result
}
pub(crate) fn find_legacy_typevars_impl( pub(crate) fn find_legacy_typevars_impl(
self, self,
db: &'db dyn Db, db: &'db dyn Db,

View File

@ -13,8 +13,7 @@ use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, ClassBase, FindLegacyTypeVarsVisitor, HasRelationToVisitor, ApplyTypeMappingVisitor, ClassBase, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, TypeMapping, TypeRelation, IsDisjointVisitor, NormalizedVisitor, TypeMapping, TypeRelation, VarianceInferable,
VarianceInferable,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
@ -278,24 +277,6 @@ impl<'db> NominalInstanceType<'db> {
} }
} }
pub(super) fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
match (self.0, other.0) {
(
NominalInstanceInner::ExactTuple(tuple1),
NominalInstanceInner::ExactTuple(tuple2),
) => tuple1.is_equivalent_to_impl(db, tuple2, visitor),
(NominalInstanceInner::NonTuple(class1), NominalInstanceInner::NonTuple(class2)) => {
class1.is_equivalent_to_impl(db, class2, visitor)
}
_ => C::unsatisfiable(db),
}
}
pub(super) fn is_disjoint_from_impl<C: Constraints<'db>>( pub(super) fn is_disjoint_from_impl<C: Constraints<'db>>(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
@ -482,13 +463,6 @@ impl<'db> ProtocolInstanceType<'db> {
} }
} }
/// Return a "normalized" version of this `Protocol` type.
///
/// See [`Type::normalized`] for more details.
pub(super) fn normalized(self, db: &'db dyn Db) -> Type<'db> {
self.normalized_impl(db, &NormalizedVisitor::default())
}
/// Return a "normalized" version of this `Protocol` type. /// Return a "normalized" version of this `Protocol` type.
/// ///
/// See [`Type::normalized`] for more details. /// See [`Type::normalized`] for more details.
@ -524,32 +498,21 @@ impl<'db> ProtocolInstanceType<'db> {
self, self,
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
_relation: TypeRelation, relation: TypeRelation,
visitor: &HasRelationToVisitor<'db, C>, visitor: &HasRelationToVisitor<'db, C>,
) -> C { ) -> C {
other other
.inner .inner
.interface(db) .interface(db)
.is_sub_interface_of(db, self.inner.interface(db), visitor) .is_sub_interface_of(db, self.inner.interface(db), visitor)
} .or(db, || {
Type::object(db).has_relation_to_impl(
/// Return `true` if this protocol type is equivalent to the protocol `other`. db,
/// Type::ProtocolInstance(other),
/// TODO: consider the types of the members as well as their existence relation,
pub(super) fn is_equivalent_to_impl<C: Constraints<'db>>( visitor,
self, )
db: &'db dyn Db, })
other: Self,
_visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
if self == other {
return C::always_satisfiable(db);
}
let self_normalized = self.normalized(db);
if self_normalized == Type::ProtocolInstance(other) {
return C::always_satisfiable(db);
}
C::from_bool(db, self_normalized == other.normalized(db))
} }
/// Return `true` if this protocol type is disjoint from the protocol `other`. /// Return `true` if this protocol type is disjoint from the protocol `other`.

View File

@ -173,16 +173,16 @@ mod stable {
forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t])) forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t]))
); );
// Only `Never` is a subtype of `Any`. // Only `Never`/`Any` are subtypes of `Any`.
type_property_test!( type_property_test!(
only_never_is_subtype_of_any, db, only_never_is_subtype_of_any, db,
forall types s. !s.is_equivalent_to(db, Type::Never) => !s.is_subtype_of(db, Type::any()) forall types s. !s.is_equivalent_to(db, Type::Never) => s.is_dynamic() || !s.is_subtype_of(db, Type::any())
); );
// Only `object` is a supertype of `Any`. // Only `object`/`Any` are supertypes of `Any`.
type_property_test!( type_property_test!(
only_object_is_supertype_of_any, db, only_object_is_supertype_of_any, db,
forall types t. !t.is_equivalent_to(db, Type::object(db)) => !Type::any().is_subtype_of(db, t) forall types t. !t.is_equivalent_to(db, Type::object(db)) => t.is_dynamic() || !Type::any().is_subtype_of(db, t)
); );
// Equivalence is commutative. // Equivalence is commutative.

View File

@ -21,8 +21,8 @@ use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsE
use crate::types::generics::{GenericContext, walk_generic_context}; use crate::types::generics::{GenericContext, walk_generic_context};
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, NormalizedVisitor, HasRelationToVisitor, KnownClass, MaterializationKind, NormalizedVisitor, TypeMapping,
TypeMapping, TypeRelation, VarianceInferable, todo_type, TypeRelation, VarianceInferable, todo_type,
}; };
use crate::{Db, FxOrderSet}; use crate::{Db, FxOrderSet};
use ruff_python_ast::{self as ast, name::Name}; use ruff_python_ast::{self as ast, name::Name};
@ -197,31 +197,6 @@ impl<'db> CallableSignature<'db> {
}), }),
} }
} }
/// Check whether this callable type is equivalent to another callable type.
///
/// See [`Type::is_equivalent_to`] for more details.
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>(
&self,
db: &'db dyn Db,
other: &Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
match (self.overloads.as_slice(), other.overloads.as_slice()) {
([self_signature], [other_signature]) => {
// Common case: both callable types contain a single signature, use the custom
// equivalence check instead of delegating it to the subtype check.
self_signature.is_equivalent_to_impl(db, other_signature, visitor)
}
(_, _) => {
if self == other {
return C::always_satisfiable(db);
}
self.is_subtype_of_impl::<C>(db, other)
.and(db, || other.is_subtype_of_impl(db, self))
}
}
}
} }
impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> {
@ -517,91 +492,6 @@ impl<'db> Signature<'db> {
} }
} }
/// Return `true` if `self` has exactly the same set of possible static materializations as
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
/// `other`).
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>(
&self,
db: &'db dyn Db,
other: &Signature<'db>,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
let mut result = C::always_satisfiable(db);
let mut check_types = |self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| {
let self_type = self_type.unwrap_or(Type::unknown());
let other_type = other_type.unwrap_or(Type::unknown());
!result
.intersect(db, self_type.is_equivalent_to_impl(db, other_type, visitor))
.is_never_satisfied(db)
};
if self.parameters.is_gradual() != other.parameters.is_gradual() {
return C::unsatisfiable(db);
}
if self.parameters.len() != other.parameters.len() {
return C::unsatisfiable(db);
}
if !check_types(self.return_ty, other.return_ty) {
return result;
}
for (self_parameter, other_parameter) in self.parameters.iter().zip(&other.parameters) {
match (self_parameter.kind(), other_parameter.kind()) {
(
ParameterKind::PositionalOnly {
default_type: self_default,
..
},
ParameterKind::PositionalOnly {
default_type: other_default,
..
},
) if self_default.is_some() == other_default.is_some() => {}
(
ParameterKind::PositionalOrKeyword {
name: self_name,
default_type: self_default,
},
ParameterKind::PositionalOrKeyword {
name: other_name,
default_type: other_default,
},
) if self_default.is_some() == other_default.is_some()
&& self_name == other_name => {}
(ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => {}
(
ParameterKind::KeywordOnly {
name: self_name,
default_type: self_default,
},
ParameterKind::KeywordOnly {
name: other_name,
default_type: other_default,
},
) if self_default.is_some() == other_default.is_some()
&& self_name == other_name => {}
(ParameterKind::KeywordVariadic { .. }, ParameterKind::KeywordVariadic { .. }) => {}
_ => return C::unsatisfiable(db),
}
if !check_types(
self_parameter.annotated_type(),
other_parameter.annotated_type(),
) {
return result;
}
}
result
}
/// Implementation of subtyping and assignability for signature. /// Implementation of subtyping and assignability for signature.
fn has_relation_to_impl<C: Constraints<'db>>( fn has_relation_to_impl<C: Constraints<'db>>(
&self, &self,
@ -701,8 +591,15 @@ impl<'db> Signature<'db> {
} }
// If either of the parameter lists is gradual (`...`), then it is assignable to and from // 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. // any other parameter list, but only a subtype or supertype of another parameter list if
if self.parameters.is_gradual() || other.parameters.is_gradual() { // that parameter list is also gradual.
if self.parameters.is_gradual() {
return if other.parameters.is_gradual() {
C::always_satisfiable(db)
} else {
C::from_bool(db, relation.is_assignability())
};
} else if other.parameters.is_gradual() {
return C::from_bool(db, relation.is_assignability()); return C::from_bool(db, relation.is_assignability());
} }

View File

@ -27,8 +27,7 @@ use crate::types::class::{ClassType, KnownClass};
use crate::types::constraints::{Constraints, IteratorConstraintsExtension}; use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping, TypeRelation, IsDisjointVisitor, NormalizedVisitor, Type, TypeMapping, TypeRelation, UnionBuilder, UnionType,
UnionBuilder, UnionType,
}; };
use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError};
use crate::{Db, FxOrderSet, Program}; use crate::{Db, FxOrderSet, Program};
@ -264,16 +263,6 @@ impl<'db> TupleType<'db> {
.has_relation_to_impl(db, other.tuple(db), relation, visitor) .has_relation_to_impl(db, other.tuple(db), relation, visitor)
} }
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
self.tuple(db)
.is_equivalent_to_impl(db, other.tuple(db), visitor)
}
pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool {
self.tuple(db).is_single_valued(db) self.tuple(db).is_single_valued(db)
} }
@ -468,21 +457,6 @@ impl<'db> FixedLengthTuple<Type<'db>> {
} }
} }
fn is_equivalent_to_impl<C: Constraints<'db>>(
&self,
db: &'db dyn Db,
other: &Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
C::from_bool(db, self.0.len() == other.0.len()).and(db, || {
(self.0.iter())
.zip(&other.0)
.when_all(db, |(self_ty, other_ty)| {
self_ty.is_equivalent_to_impl(db, *other_ty, visitor)
})
})
}
fn is_single_valued(&self, db: &'db dyn Db) -> bool { fn is_single_valued(&self, db: &'db dyn Db) -> bool {
self.0.iter().all(|ty| ty.is_single_valued(db)) self.0.iter().all(|ty| ty.is_single_valued(db))
} }
@ -871,36 +845,6 @@ impl<'db> VariableLengthTuple<Type<'db>> {
} }
} }
} }
fn is_equivalent_to_impl<C: Constraints<'db>>(
&self,
db: &'db dyn Db,
other: &Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
self.variable
.is_equivalent_to_impl(db, other.variable, visitor)
.and(db, || {
(self.prenormalized_prefix_elements(db, None))
.zip_longest(other.prenormalized_prefix_elements(db, None))
.when_all(db, |pair| match pair {
EitherOrBoth::Both(self_ty, other_ty) => {
self_ty.is_equivalent_to_impl(db, other_ty, visitor)
}
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => C::unsatisfiable(db),
})
})
.and(db, || {
(self.prenormalized_suffix_elements(db, None))
.zip_longest(other.prenormalized_suffix_elements(db, None))
.when_all(db, |pair| match pair {
EitherOrBoth::Both(self_ty, other_ty) => {
self_ty.is_equivalent_to_impl(db, other_ty, visitor)
}
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => C::unsatisfiable(db),
})
})
}
} }
impl<'db> PyIndex<'db> for &VariableLengthTuple<Type<'db>> { impl<'db> PyIndex<'db> for &VariableLengthTuple<Type<'db>> {
@ -1093,25 +1037,6 @@ impl<'db> Tuple<Type<'db>> {
} }
} }
fn is_equivalent_to_impl<C: Constraints<'db>>(
&self,
db: &'db dyn Db,
other: &Self,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
match (self, other) {
(Tuple::Fixed(self_tuple), Tuple::Fixed(other_tuple)) => {
self_tuple.is_equivalent_to_impl(db, other_tuple, visitor)
}
(Tuple::Variable(self_tuple), Tuple::Variable(other_tuple)) => {
self_tuple.is_equivalent_to_impl(db, other_tuple, visitor)
}
(Tuple::Fixed(_), Tuple::Variable(_)) | (Tuple::Variable(_), Tuple::Fixed(_)) => {
C::unsatisfiable(db)
}
}
}
pub(super) fn is_disjoint_from_impl<C: Constraints<'db>>( pub(super) fn is_disjoint_from_impl<C: Constraints<'db>>(
&self, &self,
db: &'db dyn Db, db: &'db dyn Db,