[ty] Unify Type::is_subtype_of() and Type::is_assignable_to() (#18430)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Alex Waygood 2025-06-06 18:28:55 +01:00 committed by GitHub
parent 1274521f9f
commit 6e785867c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 394 additions and 588 deletions

View file

@ -305,10 +305,13 @@ simplify to `Never`, even in the presence of other types:
```py
from ty_extensions import Intersection, Not
from typing import Any
from typing import Any, Generic, TypeVar
T_co = TypeVar("T_co", covariant=True)
class P: ...
class Q: ...
class R(Generic[T_co]): ...
def _(
i1: Intersection[P, Not[P]],
@ -317,6 +320,8 @@ def _(
i4: Intersection[Not[P], Q, P],
i5: Intersection[P, Any, Not[P]],
i6: Intersection[Not[P], Any, P],
i7: Intersection[R[P], Not[R[P]]],
i8: Intersection[R[P], Not[R[Q]]],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
@ -324,6 +329,8 @@ def _(
reveal_type(i4) # revealed: Never
reveal_type(i5) # revealed: Never
reveal_type(i6) # revealed: Never
reveal_type(i7) # revealed: Never
reveal_type(i8) # revealed: R[P] & ~R[Q]
```
### Union of a type and its negation
@ -332,20 +339,28 @@ Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `o
```py
from ty_extensions import Intersection, Not
from typing import Generic, TypeVar
T_co = TypeVar("T_co", covariant=True)
class P: ...
class Q: ...
class R(Generic[T_co]): ...
def _(
i1: P | Not[P],
i2: Not[P] | P,
i3: P | Q | Not[P],
i4: Not[P] | Q | P,
i5: R[P] | Not[R[P]],
i6: R[P] | Not[R[Q]],
) -> None:
reveal_type(i1) # revealed: object
reveal_type(i2) # revealed: object
reveal_type(i3) # revealed: object
reveal_type(i4) # revealed: object
reveal_type(i5) # revealed: object
reveal_type(i6) # revealed: R[P] | ~R[Q]
```
### Negation is an involution

View file

@ -902,8 +902,7 @@ from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf
class HasX(Protocol):
x: int
# TODO: this should pass
static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error]
static_assert(is_subtype_of(TypeOf[module], HasX))
static_assert(is_assignable_to(TypeOf[module], HasX))
class ExplicitProtocolSubtype(HasX, Protocol):

View file

@ -209,6 +209,34 @@ class AnyMeta(metaclass=Any): ...
static_assert(is_assignable_to(type[AnyMeta], type))
static_assert(is_assignable_to(type[AnyMeta], type[object]))
static_assert(is_assignable_to(type[AnyMeta], type[Any]))
from typing import TypeVar, Generic, Any
T_co = TypeVar("T_co", covariant=True)
class Foo(Generic[T_co]): ...
class Bar(Foo[T_co], Generic[T_co]): ...
static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types)
static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error]
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error]
```
## `type[]` is not assignable to types disjoint from `builtins.type`
```py
from typing import Any
from ty_extensions import is_assignable_to, static_assert
static_assert(not is_assignable_to(type[Any], None))
```
## Class-literals that inherit from `Any`
@ -717,6 +745,53 @@ def f(x: int, y: str) -> None: ...
c1: Callable[[int], None] = partial(f, y="a")
```
### Generic classes with `__call__`
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import Callable, Any, Generic, TypeVar, ParamSpec
from ty_extensions import static_assert, is_assignable_to
T = TypeVar("T")
P = ParamSpec("P")
class Foo[T]:
def __call__(self): ...
class FooLegacy(Generic[T]):
def __call__(self): ...
class Bar[T, **P]:
def __call__(self): ...
# TODO: should not error
class BarLegacy(Generic[T, P]): # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
def __call__(self): ...
static_assert(is_assignable_to(Foo, Callable[..., Any]))
static_assert(is_assignable_to(FooLegacy, Callable[..., Any]))
static_assert(is_assignable_to(Bar, Callable[..., Any]))
static_assert(is_assignable_to(BarLegacy, Callable[..., Any]))
class Spam[T]: ...
class SpamLegacy(Generic[T]): ...
class Eggs[T, **P]: ...
# TODO: should not error
class EggsLegacy(Generic[T, P]): ... # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
static_assert(not is_assignable_to(Spam, Callable[..., Any]))
static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any]))
static_assert(not is_assignable_to(Eggs, Callable[..., Any]))
# TODO: should pass
static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [static-assert-error]
```
### Classes with `__call__` as attribute
An instance type is assignable to a compatible callable type if the instance type's class has a

View file

@ -611,6 +611,10 @@ impl<'db> Type<'db> {
matches!(self, Type::GenericAlias(_))
}
const fn is_dynamic(&self) -> bool {
matches!(self, Type::Dynamic(_))
}
/// Replace references to the class `class` with a self-reference marker. This is currently
/// used for recursive protocols, but could probably be extended to self-referential type-
/// aliases and similar.
@ -1050,34 +1054,26 @@ impl<'db> Type<'db> {
///
/// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
// Two equivalent types are always subtypes of each other.
//
// "Equivalent to" here means that the two types are both fully static
// and describe exactly the same set of possible runtime objects.
// For example, `int` is a subtype of `int` because `int` and `int` are equivalent to each other.
// Equally, `type[object]` is a subtype of `type`,
// because the former type expresses "all subclasses of `object`"
// while the latter expresses "all instances of `type`",
// and these are exactly the same set of objects at runtime.
if self.is_equivalent_to(db, target) {
self.has_relation_to(db, target, TypeRelation::Subtyping)
}
/// Return true if this type is [assignable to] type `target`.
///
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.has_relation_to(db, target, TypeRelation::Assignability)
}
fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool {
if !relation.applies_to(db, self, target) {
return false;
}
if relation.are_equivalent(db, self, target) {
return true;
}
// Non-fully-static types do not participate in subtyping.
//
// Type `A` can only be a subtype of type `B` if the set of possible runtime objects
// that `A` represents is a subset of the set of possible runtime objects that `B` represents.
// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown,
// so the question is simply unanswerable for non-fully-static types.
if !self.is_fully_static(db) || !target.is_fully_static(db) {
return false;
}
match (self, target) {
// We should have handled these immediately above.
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => {
unreachable!("Non-fully-static types do not participate in subtyping!")
}
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => true,
// `Never` is the bottom type, the empty set.
// It is a subtype of all other fully static types.
@ -1115,12 +1111,12 @@ impl<'db> Type<'db> {
match typevar.bound_or_constraints(db) {
None => unreachable!(),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.is_subtype_of(db, target)
bound.has_relation_to(db, target, relation)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_subtype_of(db, target)),
.all(|constraint| constraint.has_relation_to(db, target, relation)),
}
}
@ -1131,7 +1127,7 @@ impl<'db> Type<'db> {
if typevar.constraints(db).is_some_and(|constraints| {
constraints
.iter()
.all(|constraint| self.is_subtype_of(db, *constraint))
.all(|constraint| self.has_relation_to(db, *constraint, relation))
}) =>
{
true
@ -1140,12 +1136,12 @@ impl<'db> Type<'db> {
(Type::Union(union), _) => union
.elements(db)
.iter()
.all(|&elem_ty| elem_ty.is_subtype_of(db, target)),
.all(|&elem_ty| elem_ty.has_relation_to(db, target, relation)),
(_, Type::Union(union)) => union
.elements(db)
.iter()
.any(|&elem_ty| self.is_subtype_of(db, elem_ty)),
.any(|&elem_ty| self.has_relation_to(db, elem_ty, relation)),
// If both sides are intersections we need to handle the right side first
// (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B,
@ -1154,7 +1150,7 @@ impl<'db> Type<'db> {
intersection
.positive(db)
.iter()
.all(|&pos_ty| self.is_subtype_of(db, pos_ty))
.all(|&pos_ty| self.has_relation_to(db, pos_ty, relation))
&& intersection
.negative(db)
.iter()
@ -1164,7 +1160,7 @@ impl<'db> Type<'db> {
(Type::Intersection(intersection), _) => intersection
.positive(db)
.iter()
.any(|&elem_ty| elem_ty.is_subtype_of(db, target)),
.any(|&elem_ty| elem_ty.has_relation_to(db, target, relation)),
// Other than the special cases checked above, no other types are a subtype of a
// typevar, since there's no guarantee what type the typevar will be specialized to.
@ -1179,7 +1175,7 @@ impl<'db> Type<'db> {
(left, Type::AlwaysTruthy) => left.bool(db).is_always_true(),
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
target.is_equivalent_to(db, Type::object(db))
relation.are_equivalent(db, target, Type::object(db))
}
// These clauses handle type variants that include function literals. A function
@ -1188,13 +1184,13 @@ impl<'db> Type<'db> {
// 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.is_subtype_of(db, target_function)
self_function.has_relation_to(db, target_function, relation)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_subtype_of(db, target_method)
self_method.has_relation_to(db, target_method, relation)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_subtype_of(db, target_method)
self_method.has_relation_to(db, target_method, relation)
}
// No literal type is a subtype of any other literal type, unless they are the same
@ -1216,6 +1212,31 @@ impl<'db> Type<'db> {
| Type::ModuleLiteral(_),
) => false,
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place;
// If the type of __call__ is a subtype of a callable type, this instance is.
// Don't add other special cases here; our subtyping of a callable type
// shouldn't get out of sync with the calls we will actually allow.
if let Place::Type(t, Boundness::Bound) = call_symbol {
t.has_relation_to(db, target, relation)
} else {
false
}
}
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.has_relation_to(db, right, relation)
}
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
(Type::ProtocolInstance(_), _) => false,
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
// All `StringLiteral` types are a subtype of `LiteralString`.
(Type::StringLiteral(_), Type::LiteralString) => true,
@ -1231,45 +1252,37 @@ impl<'db> Type<'db> {
| Type::ModuleLiteral(_),
_,
) => (self.literal_fallback_instance(db))
.is_some_and(|instance| instance.is_subtype_of(db, target)),
// Function-like callables are subtypes of `FunctionType`
(Type::Callable(callable), Type::NominalInstance(target))
if callable.is_function_like(db)
&& target.class.is_known(db, KnownClass::FunctionType) =>
{
true
}
.is_some_and(|instance| instance.has_relation_to(db, target, relation)),
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
self_function_literal
.into_callable_type(db)
.is_subtype_of(db, target)
.has_relation_to(db, target, relation)
}
(Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method
.into_callable_type(db)
.is_subtype_of(db, target),
.has_relation_to(db, target, relation),
// A `FunctionLiteral` type is a single-valued type like the other literals handled above,
// so it also, for now, just delegates to its instance fallback.
(Type::FunctionLiteral(_), _) => KnownClass::FunctionType
.to_instance(db)
.is_subtype_of(db, target),
.has_relation_to(db, target, relation),
// The same reasoning applies for these special callable types:
(Type::BoundMethod(_), _) => KnownClass::MethodType
.to_instance(db)
.is_subtype_of(db, target),
.has_relation_to(db, target, relation),
(Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType
.to_instance(db)
.is_subtype_of(db, target),
.has_relation_to(db, target, relation),
(Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType
.to_instance(db)
.is_subtype_of(db, target),
.has_relation_to(db, target, relation),
(Type::Callable(self_callable), Type::Callable(other_callable)) => {
self_callable.is_subtype_of(db, other_callable)
self_callable.has_relation_to(db, other_callable, relation)
}
(Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => {
@ -1277,29 +1290,15 @@ impl<'db> Type<'db> {
false
}
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place;
// If the type of __call__ is a subtype of a callable type, this instance is.
// Don't add other special cases here; our subtyping of a callable type
// shouldn't get out of sync with the calls we will actually allow.
if let Place::Type(t, Boundness::Bound) = call_symbol {
t.is_subtype_of(db, target)
} else {
false
}
// Function-like callables are subtypes of `FunctionType`
(Type::Callable(callable), _)
if callable.is_function_like(db)
&& KnownClass::FunctionType
.to_instance(db)
.has_relation_to(db, target, relation) =>
{
true
}
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.is_subtype_of(db, right)
}
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
(Type::ProtocolInstance(_), _) => false,
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
(Type::Callable(_), _) => {
// TODO: Implement subtyping between callable types and other types like
@ -1319,54 +1318,81 @@ impl<'db> Type<'db> {
self_elements.len() == target_elements.len()
&& self_elements.iter().zip(target_elements).all(
|(self_element, target_element)| {
self_element.is_subtype_of(db, *target_element)
self_element.has_relation_to(db, *target_element, relation)
},
)
}
// `tuple[A, B, C]` is a subtype of `tuple[A | B | C, ...]`
(Type::Tuple(tuple), _) => tuple.homogeneous_supertype(db).is_subtype_of(db, target),
(Type::Tuple(tuple), _) => tuple
.homogeneous_supertype(db)
.has_relation_to(db, target, relation),
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target),
(Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).is_subtype_of(db, target),
(Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target),
(Type::BoundSuper(_), _) => KnownClass::Super
.to_instance(db)
.has_relation_to(db, target, relation),
// `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`.
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
.subclass_of()
.into_class()
.is_some_and(|target_class| class.is_subclass_of(db, None, target_class)),
.is_none_or(|subclass_of_class| {
ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation)
}),
(Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
.subclass_of()
.into_class()
.is_some_and(|target_class| {
ClassType::from(alias).is_subclass_of(db, target_class)
.is_none_or(|subclass_of_class| {
ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation)
}),
// 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.is_subtype_of(db, target_subclass_ty)
self_subclass_ty.has_relation_to(db, target_subclass_ty, relation)
}
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
ClassType::NonGeneric(class_literal)
.into_callable(db)
.is_subtype_of(db, target)
.has_relation_to(db, target, relation)
}
(Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias)
.into_callable(db)
.is_subtype_of(db, target),
.has_relation_to(db, target, relation),
// `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).is_subtype_of(db, target)
}
(Type::ClassLiteral(class), _) => class
.metaclass_instance_type(db)
.has_relation_to(db, target, relation),
(Type::GenericAlias(alias), _) => ClassType::from(alias)
.metaclass_instance_type(db)
.is_subtype_of(db, target),
.has_relation_to(db, target, relation),
// This branch upholds two properties:
// - For any type `T` that is assignable to `type`, `T` shall be assignable to `type[Any]`.
// - For any type `T` that is assignable to `type`, `type[Any]` shall be assignable to `T`.
//
// This is really the same as the very first branch in this `match` statement that handles dynamic types.
// That branch upholds two properties:
// - For any type `S` that is assignable to `object` (which is _all_ types), `S` shall be assignable to `Any`
// - For any type `S` that is assignable to `object` (which is _all_ types), `Any` shall be assignable to `S`.
//
// The only difference between this branch and the first branch is that the first branch deals with the type
// `object & Any` (which simplifies to `Any`!) whereas this branch deals with the type `type & Any`.
//
// See also: <https://github.com/astral-sh/ty/issues/222>
(Type::SubclassOf(subclass_of_ty), other)
| (other, Type::SubclassOf(subclass_of_ty))
if subclass_of_ty.is_dynamic()
&& other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) =>
{
true
}
// `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses
// of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str`
@ -1379,30 +1405,31 @@ impl<'db> Type<'db> {
.subclass_of()
.into_class()
.map(|class| class.metaclass_instance_type(db))
.is_some_and(|metaclass_instance_type| {
metaclass_instance_type.is_subtype_of(db, target)
}),
.unwrap_or_else(|| KnownClass::Type.to_instance(db))
.has_relation_to(db, target, relation),
// 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).is_subtype_of(db, right),
(Type::SpecialForm(left), right) => left
.instance_fallback(db)
.has_relation_to(db, right, relation),
(Type::KnownInstance(left), right) => {
left.instance_fallback(db).is_subtype_of(db, right)
}
(Type::KnownInstance(left), right) => left
.instance_fallback(db)
.has_relation_to(db, right, relation),
// `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)) => {
self_instance.is_subtype_of(db, target_instance)
self_instance.has_relation_to(db, target_instance, relation)
}
(Type::PropertyInstance(_), _) => KnownClass::Property
.to_instance(db)
.is_subtype_of(db, target),
.has_relation_to(db, target, relation),
(_, Type::PropertyInstance(_)) => {
self.is_subtype_of(db, KnownClass::Property.to_instance(db))
self.has_relation_to(db, KnownClass::Property.to_instance(db), relation)
}
// Other than the special cases enumerated above, `Instance` types and typevars are
@ -1411,292 +1438,6 @@ impl<'db> Type<'db> {
}
}
/// Return true if this type is [assignable to] type `target`.
///
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
if self.is_gradual_equivalent_to(db, target) {
return true;
}
match (self, target) {
// Never can be assigned to any type.
(Type::Never, _) => true,
// The dynamic type is assignable-to and assignable-from any type.
(Type::Dynamic(_), _) => true,
(_, Type::Dynamic(_)) => true,
// All types are assignable to `object`.
// TODO this special case might be removable once the below cases are comprehensive
(_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true,
// In general, a TypeVar `T` is not assignable to a type `S` unless one of the two conditions is satisfied:
// 1. `T` is a bound TypeVar and `T`'s upper bound is assignable to `S`.
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
// 2. `T` is a constrained TypeVar and all of `T`'s constraints are assignable to `S`.
//
// However, there is one exception to this general rule: for any given typevar `T`,
// `T` will always be assignable to any union containing `T`.
// A similar rule applies in reverse to intersection types.
(Type::TypeVar(_), Type::Union(union)) if union.elements(db).contains(&self) => true,
(Type::Intersection(intersection), Type::TypeVar(_))
if intersection.positive(db).contains(&target) =>
{
true
}
(Type::Intersection(intersection), Type::TypeVar(_))
if intersection.negative(db).contains(&target) =>
{
false
}
// A typevar is assignable to its upper bound, and to something similar to the union of
// its constraints. An unbound, unconstrained typevar has an implicit upper bound of
// `object` (which is handled above).
(Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => {
match typevar.bound_or_constraints(db) {
None => unreachable!(),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.is_assignable_to(db, target)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_assignable_to(db, target)),
}
}
// If the typevar is constrained, there must be multiple constraints, and the typevar
// might be specialized to any one of them. However, the constraints do not have to be
// disjoint, which means an lhs type might be assignable to all of the constraints.
(_, Type::TypeVar(typevar))
if typevar.constraints(db).is_some_and(|constraints| {
constraints
.iter()
.all(|constraint| self.is_assignable_to(db, *constraint))
}) =>
{
true
}
// A union is assignable to a type T iff every element of the union is assignable to T.
(Type::Union(union), ty) => union
.elements(db)
.iter()
.all(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
// A type T is assignable to a union iff T is assignable to any element of the union.
(ty, Type::Union(union)) => union
.elements(db)
.iter()
.any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
// If both sides are intersections we need to handle the right side first
// (A & B & C) is assignable to (A & B) because the left is assignable to both A and B,
// but none of A, B, or C is assignable to (A & B).
//
// A type S is assignable to an intersection type T if
// S is assignable to all positive elements of T (e.g. `str & int` is assignable to `str & Any`), and
// S is disjoint from all negative elements of T (e.g. `int` is not assignable to Intersection[int, Not[Literal[1]]]).
(ty, Type::Intersection(intersection)) => {
intersection
.positive(db)
.iter()
.all(|&elem_ty| ty.is_assignable_to(db, elem_ty))
&& intersection
.negative(db)
.iter()
.all(|&neg_ty| ty.is_disjoint_from(db, neg_ty))
}
// An intersection type S is assignable to a type T if
// Any element of S is assignable to T (e.g. `A & B` is assignable to `A`)
// Negative elements do not have an effect on assignability - if S is assignable to T then S & ~P is also assignable to T.
(Type::Intersection(intersection), ty) => intersection
.positive(db)
.iter()
.any(|&elem_ty| elem_ty.is_assignable_to(db, ty)),
// Other than the special cases checked above, no other types are assignable to a
// typevar, since there's no guarantee what type the typevar will be specialized to.
// (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(_)) => false,
// A tuple type S is assignable to a tuple type T if their lengths are the same, and
// each element of S is assignable to the corresponding element of T.
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
let self_elements = self_tuple.elements(db);
let target_elements = target_tuple.elements(db);
self_elements.len() == target_elements.len()
&& self_elements.iter().zip(target_elements).all(
|(self_element, target_element)| {
self_element.is_assignable_to(db, *target_element)
},
)
}
// This special case is required because the left-hand side tuple might be a
// gradual type, so we can not rely on subtyping. This allows us to assign e.g.
// `tuple[Any, int]` to `tuple`.
//
// `tuple[A, B, C]` is assignable to `tuple[A | B | C, ...]`
(Type::Tuple(tuple), _)
if tuple.homogeneous_supertype(db).is_assignable_to(db, target) =>
{
true
}
// These clauses handle type variants that include function literals. A function
// literal is assignable to itself, and not to 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 assignable to each other if they result in the same signature.
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
self_function.is_assignable_to(db, target_function)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_assignable_to(db, target_method)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_assignable_to(db, target_method)
}
// `type[Any]` is assignable to any `type[...]` type, because `type[Any]` can
// materialize to any `type[...]` type.
(Type::SubclassOf(subclass_of_ty), Type::SubclassOf(_))
if subclass_of_ty.is_dynamic() =>
{
true
}
(Type::ClassLiteral(class), Type::SubclassOf(_))
if class
.iter_mro(db, None)
.any(class_base::ClassBase::is_dynamic) =>
{
true
}
// Every `type[...]` is assignable to `type`
(Type::SubclassOf(_), _)
if KnownClass::Type
.to_instance(db)
.is_assignable_to(db, target) =>
{
true
}
// All `type[...]` types are assignable to `type[Any]`, because `type[Any]` can
// materialize to any `type[...]` type.
//
// Every class literal type is also assignable to `type[Any]`, because the class
// literal type for a class `C` is a subtype of `type[C]`, and `type[C]` is assignable
// to `type[Any]`.
(
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_),
Type::SubclassOf(target_subclass_of),
) if target_subclass_of.is_dynamic() => true,
// `type[Any]` is assignable to any type that `type[object]` is assignable to, because
// `type[Any]` can materialize to `type[object]`.
//
// `type[Any]` is also assignable to any subtype of `type[object]`, because all
// subtypes of `type[object]` are `type[...]` types (or `Never`), and `type[Any]` can
// materialize to any `type[...]` type (or to `type[Never]`, which is equivalent to
// `Never`.)
(Type::SubclassOf(subclass_of_ty), Type::NominalInstance(_))
if subclass_of_ty.is_dynamic()
&& (KnownClass::Type
.to_instance(db)
.is_assignable_to(db, target)
|| target.is_subtype_of(db, KnownClass::Type.to_instance(db))) =>
{
true
}
// Any type that is assignable to `type[object]` is also assignable to `type[Any]`,
// because `type[Any]` can materialize to `type[object]`.
(Type::NominalInstance(_), Type::SubclassOf(subclass_of_ty))
if subclass_of_ty.is_dynamic()
&& self.is_assignable_to(db, KnownClass::Type.to_instance(db)) =>
{
true
}
(Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => {
self_instance.is_assignable_to(db, target_instance)
}
(Type::Callable(self_callable), Type::Callable(target_callable)) => {
self_callable.is_assignable_to(db, target_callable)
}
(Type::NominalInstance(instance), Type::Callable(_))
if instance.class.is_subclass_of_any_or_unknown(db) =>
{
true
}
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
let call_symbol = self
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place;
// shouldn't get out of sync with the calls we will actually allow.
if let Place::Type(t, Boundness::Bound) = call_symbol {
t.is_assignable_to(db, target)
} else {
false
}
}
_ if self
.literal_fallback_instance(db)
.is_some_and(|instance| instance.is_assignable_to(db, target)) =>
{
true
}
(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
ClassType::NonGeneric(class_literal)
.into_callable(db)
.is_assignable_to(db, target)
}
(Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias)
.into_callable(db)
.is_assignable_to(db, target),
(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
self_function_literal
.into_callable_type(db)
.is_assignable_to(db, target)
}
(Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method
.into_callable_type(db)
.is_assignable_to(db, target),
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.is_assignable_to(db, right)
}
// Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above,
// a protocol instance can never be assignable to a nominal type,
// with the *sole* exception of `object`.
(Type::ProtocolInstance(_), _) => false,
(_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol),
// TODO other types containing gradual forms
_ => self.is_subtype_of(db, target),
}
}
/// Return true if this type is [equivalent to] type `other`.
///
/// This method returns `false` if either `self` or `other` is not fully static.
@ -7027,6 +6768,45 @@ impl<'db> ConstructorCallError<'db> {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) enum TypeRelation {
Subtyping,
Assignability,
}
impl TypeRelation {
/// Non-fully-static types do not participate in subtyping, only assignability,
/// so the subtyping relation does not even apply to them.
///
/// Type `A` can only be a subtype of type `B` if the set of possible runtime objects
/// that `A` represents is a subset of the set of possible runtime objects that `B` represents.
/// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown,
/// so the question is simply unanswerable for non-fully-static types.
///
/// However, the assignability relation applies to all types, even non-fully-static ones.
fn applies_to<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool {
match self {
TypeRelation::Subtyping => type_1.is_fully_static(db) && type_2.is_fully_static(db),
TypeRelation::Assignability => true,
}
}
/// Determine whether `type_1` and `type_2` are equivalent.
///
/// Depending on whether the context is a subtyping test or an assignability test,
/// this method may call [`Type::is_equivalent_to`] or [`Type::is_assignable_to`].
fn are_equivalent<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool {
match self {
TypeRelation::Subtyping => type_1.is_equivalent_to(db, type_2),
TypeRelation::Assignability => type_1.is_gradual_equivalent_to(db, type_2),
}
}
const fn applies_to_non_fully_static_types(self) -> bool {
matches!(self, TypeRelation::Assignability)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Truthiness {
/// For an object `x`, `bool(x)` will always return `True`
@ -7139,26 +6919,16 @@ impl<'db> BoundMethodType<'db> {
)
}
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
// 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).is_subtype_of(db, other.function(db))
self.function(db)
.has_relation_to(db, other.function(db), relation)
&& other
.self_instance(db)
.is_subtype_of(db, self.self_instance(db))
}
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
// A bound method is a typically assignable to itself. However, we must explicitly verify
// the assignability 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).is_assignable_to(db, other.function(db))
&& other
.self_instance(db)
.is_assignable_to(db, self.self_instance(db))
.has_relation_to(db, self.self_instance(db), relation)
}
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
@ -7276,26 +7046,15 @@ impl<'db> CallableType<'db> {
self.signatures(db).is_fully_static(db)
}
/// Check whether this callable type is a subtype of another callable type.
/// Check whether this callable type has the given relation to another callable type.
///
/// See [`Type::is_subtype_of`] for more details.
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
let self_is_function_like = self.is_function_like(db);
let other_is_function_like = other.is_function_like(db);
(self_is_function_like || !other_is_function_like)
&& self.signatures(db).is_subtype_of(db, other.signatures(db))
}
/// Check whether this callable type is assignable to another callable type.
///
/// See [`Type::is_assignable_to`] for more details.
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_is_function_like = self.is_function_like(db);
let other_is_function_like = other.is_function_like(db);
(self_is_function_like || !other_is_function_like)
&& self
.signatures(db)
.is_assignable_to(db, other.signatures(db))
/// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details.
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
if other.is_function_like(db) && !self.is_function_like(db) {
return false;
}
self.signatures(db)
.has_relation_to(db, other.signatures(db), relation)
}
/// Check whether this callable type is equivalent to another callable type.
@ -7348,50 +7107,17 @@ pub enum MethodWrapperKind<'db> {
}
impl<'db> MethodWrapperKind<'db> {
fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool {
match (self, other) {
(
MethodWrapperKind::FunctionTypeDunderGet(self_function),
MethodWrapperKind::FunctionTypeDunderGet(other_function),
) => self_function.is_subtype_of(db, other_function),
) => self_function.has_relation_to(db, other_function, relation),
(
MethodWrapperKind::FunctionTypeDunderCall(self_function),
MethodWrapperKind::FunctionTypeDunderCall(other_function),
) => self_function.is_subtype_of(db, other_function),
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))
| (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => {
self == other
}
(
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
MethodWrapperKind::FunctionTypeDunderGet(_)
| MethodWrapperKind::FunctionTypeDunderCall(_)
| MethodWrapperKind::PropertyDunderGet(_)
| MethodWrapperKind::PropertyDunderSet(_)
| MethodWrapperKind::StrStartswith(_),
) => false,
}
}
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
match (self, other) {
(
MethodWrapperKind::FunctionTypeDunderGet(self_function),
MethodWrapperKind::FunctionTypeDunderGet(other_function),
) => self_function.is_assignable_to(db, other_function),
(
MethodWrapperKind::FunctionTypeDunderCall(self_function),
MethodWrapperKind::FunctionTypeDunderCall(other_function),
) => self_function.is_assignable_to(db, other_function),
) => self_function.has_relation_to(db, other_function, relation),
(MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_))
| (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_))

View file

@ -12,7 +12,7 @@ use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::{
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeVarInstance,
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance,
};
use crate::{
Db, FxOrderSet, KnownModule, Program,
@ -29,8 +29,8 @@ use crate::{
place_table, semantic_index, use_def_map,
},
types::{
CallArgumentTypes, CallError, CallErrorKind, DynamicType, MetaclassCandidate, TupleType,
UnionBuilder, UnionType, definition_expression_type,
CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, TupleType, UnionBuilder,
UnionType, definition_expression_type,
},
};
use indexmap::IndexSet;
@ -340,23 +340,22 @@ impl<'db> ClassType<'db> {
class_literal.is_final(db)
}
/// Is this class a subclass of `Any` or `Unknown`?
pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool {
self.iter_mro(db).any(|base| {
matches!(
base,
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
)
})
}
/// 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.has_relation_to(db, other, TypeRelation::Subtyping)
}
pub(super) fn has_relation_to(
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
) -> bool {
self.iter_mro(db).any(|base| {
match base {
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
// participate.
ClassBase::Dynamic(_) => false,
ClassBase::Dynamic(_) => {
relation.applies_to_non_fully_static_types() && !other.is_final(db)
}
// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol | ClassBase::Generic => false,
@ -365,9 +364,11 @@ impl<'db> ClassType<'db> {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
(ClassType::Generic(base), ClassType::Generic(other)) => {
base.origin(db) == other.origin(db)
&& base
.specialization(db)
.is_subtype_of(db, other.specialization(db))
&& base.specialization(db).has_relation_to(
db,
other.specialization(db),
relation,
)
}
(ClassType::Generic(_), ClassType::NonGeneric(_))
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
@ -390,30 +391,6 @@ impl<'db> ClassType<'db> {
}
}
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
self.iter_mro(db).any(|base| {
match base {
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) => !other.is_final(db),
ClassBase::Dynamic(_) => false,
// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol | ClassBase::Generic => false,
ClassBase::Class(base) => match (base, other) {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
(ClassType::Generic(base), ClassType::Generic(other)) => {
base.origin(db) == other.origin(db)
&& base
.specialization(db)
.is_assignable_to(db, other.specialization(db))
}
(ClassType::Generic(_), ClassType::NonGeneric(_))
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
},
}
})
}
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
match (self, other) {
(ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other,

View file

@ -279,10 +279,6 @@ impl<'db> ClassBase<'db> {
}
}
}
pub(crate) const fn is_dynamic(self) -> bool {
matches!(self, Self::Dynamic(_))
}
}
impl<'db> From<ClassType<'db>> for ClassBase<'db> {

View file

@ -67,7 +67,9 @@ use crate::semantic_index::semantic_index;
use crate::types::generics::GenericContext;
use crate::types::narrow::ClassInfoConstraintFunction;
use crate::types::signatures::{CallableSignature, Signature};
use crate::types::{BoundMethodType, CallableType, Type, TypeMapping, TypeVarInstance};
use crate::types::{
BoundMethodType, CallableType, Type, TypeMapping, TypeRelation, TypeVarInstance,
};
use crate::{Db, FxOrderSet};
/// A collection of useful spans for annotating functions.
@ -707,6 +709,18 @@ impl<'db> FunctionType<'db> {
Type::BoundMethod(BoundMethodType::new(db, self, self_instance))
}
pub(crate) fn has_relation_to(
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
) -> bool {
match relation {
TypeRelation::Subtyping => self.is_subtype_of(db, other),
TypeRelation::Assignability => self.is_assignable_to(db, other),
}
}
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
// A function type is the subtype of itself, and not of any other function type. However,
// our representation of a function type includes any specialization that should be applied

View file

@ -9,7 +9,7 @@ use crate::types::class_base::ClassBase;
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{
KnownInstanceType, Type, TypeMapping, TypeVarBoundOrConstraints, TypeVarInstance,
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarVariance, UnionType, declaration_type, todo_type,
};
use crate::{Db, FxOrderSet};
@ -358,7 +358,12 @@ impl<'db> Specialization<'db> {
Self::new(db, self.generic_context(db), types)
}
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
pub(crate) fn has_relation_to(
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
) -> bool {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return false;
@ -368,20 +373,31 @@ impl<'db> Specialization<'db> {
.zip(self.types(db))
.zip(other.types(db))
{
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
return false;
if self_type.is_dynamic() || other_type.is_dynamic() {
match relation {
TypeRelation::Assignability => continue,
TypeRelation::Subtyping => return false,
}
}
// Subtyping of each type in the specialization depends on the variance of the
// corresponding typevar:
// 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
// - bivariant: skip, can't make subtyping false
// - invariant: verify that self_type <: other_type AND other_type <: self_type
// - bivariant: skip, can't make subtyping/assignability false
let compatible = match typevar.variance(db) {
TypeVarVariance::Invariant => self_type.is_equivalent_to(db, *other_type),
TypeVarVariance::Covariant => self_type.is_subtype_of(db, *other_type),
TypeVarVariance::Contravariant => other_type.is_subtype_of(db, *self_type),
TypeVarVariance::Invariant => match relation {
TypeRelation::Subtyping => self_type.is_equivalent_to(db, *other_type),
TypeRelation::Assignability => {
self_type.is_assignable_to(db, *other_type)
&& other_type.is_assignable_to(db, *self_type)
}
},
TypeVarVariance::Covariant => self_type.has_relation_to(db, *other_type, relation),
TypeVarVariance::Contravariant => {
other_type.has_relation_to(db, *self_type, relation)
}
TypeVarVariance::Bivariant => true,
};
if !compatible {
@ -426,43 +442,6 @@ impl<'db> Specialization<'db> {
true
}
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return false;
}
for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db))
.zip(other.types(db))
{
if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) {
continue;
}
// 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 assignability false
let compatible = match typevar.variance(db) {
TypeVarVariance::Invariant => {
self_type.is_assignable_to(db, *other_type)
&& other_type.is_assignable_to(db, *self_type)
}
TypeVarVariance::Covariant => self_type.is_assignable_to(db, *other_type),
TypeVarVariance::Contravariant => other_type.is_assignable_to(db, *self_type),
TypeVarVariance::Bivariant => true,
};
if !compatible {
return false;
}
}
true
}
pub(crate) fn is_gradual_equivalent_to(
self,
db: &'db dyn Db,

View file

@ -5,14 +5,16 @@ use std::marker::PhantomData;
use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type};
use crate::place::{Boundness, Place, PlaceAndQualifiers};
use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance};
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
use crate::{Db, FxOrderSet};
pub(super) use synthesized_protocol::SynthesizedProtocolType;
impl<'db> Type<'db> {
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
if class.class_literal(db).0.is_protocol(db) {
if class.is_known(db, KnownClass::Any) {
Self::Dynamic(DynamicType::Any)
} else if class.class_literal(db).0.is_protocol(db) {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
} else {
Self::NominalInstance(NominalInstanceType::from_class(class))
@ -78,19 +80,19 @@ impl<'db> NominalInstanceType<'db> {
Self::from_class(self.class.normalized(db))
}
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
// N.B. The subclass relation is fully static
self.class.is_subclass_of(db, other.class)
pub(super) fn has_relation_to(
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
) -> bool {
self.class.has_relation_to(db, other.class, relation)
}
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_equivalent_to(db, other.class)
}
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_assignable_to(db, other.class)
}
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) {
return true;
@ -254,16 +256,20 @@ impl<'db> ProtocolInstanceType<'db> {
self.inner.interface(db).is_fully_static(db)
}
/// Return `true` if this protocol type is a subtype of the protocol `other`.
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
self.is_fully_static(db) && other.is_fully_static(db) && self.is_assignable_to(db, other)
}
/// Return `true` if this protocol type is assignable to the protocol `other`.
/// Return `true` if this protocol type has the given type relation to the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
other
pub(super) fn has_relation_to(
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
) -> bool {
relation.applies_to(
db,
Type::ProtocolInstance(self),
Type::ProtocolInstance(other),
) && other
.inner
.interface(db)
.is_sub_interface_of(db, self.inner.interface(db))

View file

@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec};
use super::{DynamicType, Type, definition_expression_type};
use crate::semantic_index::definition::Definition;
use crate::types::generics::GenericContext;
use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance, todo_type};
use crate::types::{ClassLiteral, TypeMapping, TypeRelation, TypeVarInstance, todo_type};
use crate::{Db, FxOrderSet};
use ruff_python_ast::{self as ast, name::Name};
@ -98,11 +98,23 @@ impl<'db> CallableSignature<'db> {
.all(|signature| signature.is_fully_static(db))
}
pub(crate) fn has_relation_to(
&self,
db: &'db dyn Db,
other: &Self,
relation: TypeRelation,
) -> bool {
match relation {
TypeRelation::Subtyping => self.is_subtype_of(db, other),
TypeRelation::Assignability => self.is_assignable_to(db, other),
}
}
/// Check whether this callable type is a subtype of another callable type.
///
/// See [`Type::is_subtype_of`] for more details.
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool {
Self::is_assignable_to_impl(
Self::has_relation_to_impl(
&self.overloads,
&other.overloads,
&|self_signature, other_signature| self_signature.is_subtype_of(db, other_signature),
@ -113,7 +125,7 @@ impl<'db> CallableSignature<'db> {
///
/// See [`Type::is_assignable_to`] for more details.
pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Self) -> bool {
Self::is_assignable_to_impl(
Self::has_relation_to_impl(
&self.overloads,
&other.overloads,
&|self_signature, other_signature| self_signature.is_assignable_to(db, other_signature),
@ -124,7 +136,7 @@ impl<'db> CallableSignature<'db> {
/// types.
///
/// The `check_signature` closure is used to check the relation between two [`Signature`]s.
fn is_assignable_to_impl<F>(
fn has_relation_to_impl<F>(
self_signatures: &[Signature<'db>],
other_signatures: &[Signature<'db>],
check_signature: &F,
@ -140,7 +152,7 @@ impl<'db> CallableSignature<'db> {
// `self` is possibly overloaded while `other` is definitely not overloaded.
(_, [_]) => self_signatures.iter().any(|self_signature| {
Self::is_assignable_to_impl(
Self::has_relation_to_impl(
std::slice::from_ref(self_signature),
other_signatures,
check_signature,
@ -149,7 +161,7 @@ impl<'db> CallableSignature<'db> {
// `self` is definitely not overloaded while `other` is possibly overloaded.
([_], _) => other_signatures.iter().all(|other_signature| {
Self::is_assignable_to_impl(
Self::has_relation_to_impl(
self_signatures,
std::slice::from_ref(other_signature),
check_signature,
@ -158,7 +170,7 @@ impl<'db> CallableSignature<'db> {
// `self` is definitely overloaded while `other` is possibly overloaded.
(_, _) => other_signatures.iter().all(|other_signature| {
Self::is_assignable_to_impl(
Self::has_relation_to_impl(
self_signatures,
std::slice::from_ref(other_signature),
check_signature,

View file

@ -1,6 +1,7 @@
use crate::place::PlaceAndQualifiers;
use crate::types::{
ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeVarInstance,
ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeRelation,
TypeVarInstance,
};
use crate::{Db, FxOrderSet};
@ -30,10 +31,14 @@ impl<'db> SubclassOfType<'db> {
SubclassOfInner::Class(class) => {
if class.is_final(db) {
Type::from(class)
} else if class.is_object(db) {
KnownClass::Type.to_instance(db)
} else {
Type::SubclassOf(Self { subclass_of })
match class.known(db) {
Some(KnownClass::Object) => KnownClass::Type.to_instance(db),
Some(KnownClass::Any) => Type::SubclassOf(Self {
subclass_of: SubclassOfInner::Dynamic(DynamicType::Any),
}),
_ => Type::SubclassOf(Self { subclass_of }),
}
}
}
}
@ -103,21 +108,23 @@ impl<'db> SubclassOfType<'db> {
Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy)
}
/// Return `true` if `self` is a subtype of `other`.
///
/// This can only return `true` if `self.subclass_of` is a [`SubclassOfInner::Class`] variant;
/// only fully static types participate in subtyping.
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool {
/// Return `true` if `self` has a certain relation to `other`.
pub(crate) fn has_relation_to(
self,
db: &'db dyn Db,
other: SubclassOfType<'db>,
relation: TypeRelation,
) -> bool {
match (self.subclass_of, other.subclass_of) {
// Non-fully-static types do not participate in subtyping
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false,
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => {
relation.applies_to_non_fully_static_types()
}
// 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)) => {
// N.B. The subclass relation is fully static
self_class.is_subclass_of(db, other_class)
self_class.has_relation_to(db, other_class, relation)
}
}
}