[ty] eliminate is_fully_static (#18799)

## Summary

Having a recursive type method to check whether a type is fully static
is inefficient, unnecessary, and makes us overly strict about subtyping
relations.

It's inefficient because we end up re-walking the same types many times
to check for fully-static-ness.

It's unnecessary because we can check relations involving the dynamic
type appropriately, depending whether the relation is subtyping or
assignability.

We use the subtyping relation to simplify unions and intersections. We
can usefully consider that `S <: T` for gradual types also, as long as
it remains true that `S | T` is equivalent to `T` and `S & T` is
equivalent to `S`.

One conservative definition (implemented here) that satisfies this
requirement is that we consider `S <: T` if, for every possible pair of
materializations `S'` and `T'`, `S' <: T'`. Or put differently the top
materialization of `S` (`S+` -- the union of all possible
materializations of `S`) is a subtype of the bottom materialization of
`T` (`T-` -- the intersection of all possible materializations of `T`).
In the most basic cases we can usefully say that `Any <: object` and
that `Never <: Any`, and we can handle more complex cases inductively
from there.

This definition of subtyping for gradual subtypes is not reflexive
(`Any` is not a subtype of `Any`).

As a corollary, we also remove `is_gradual_equivalent_to` --
`is_equivalent_to` now has the meaning that `is_gradual_equivalent_to`
used to have. If necessary, we could restore an
`is_fully_static_equivalent_to` or similar (which would not do an
`is_fully_static` pre-check of the types, but would instead pass a
relation-kind enum down through a recursive equivalence check, similar
to `has_relation_to`), but so far this doesn't appear to be necessary.

Credit to @JelleZijlstra for the observation that `is_fully_static` is
unnecessary and overly restrictive on subtyping.

There is another possible definition of gradual subtyping: instead of
requiring that `S+ <: T-`, we could instead require that `S+ <: T+` and
`S- <: T-`. In other words, instead of requiring all materializations of
`S` to be a subtype of every materialization of `T`, we just require
that every materialization of `S` be a subtype of _some_ materialization
of `T`, and that every materialization of `T` be a supertype of some
materialization of `S`. This definition also preserves the core
invariant that `S <: T` implies that `S | T = T` and `S & T = S`, and it
restores reflexivity: under this definition, `Any` is a subtype of
`Any`, and for any equivalent types `S` and `T`, `S <: T` and `T <: S`.
But unfortunately, this definition breaks transitivity of subtyping,
because nominal subclasses in Python use assignability ("consistent
subtyping") to define acceptable overrides. This means that we may have
a class `A` with `def method(self) -> Any` and a subtype `B(A)` with
`def method(self) -> int`, since `int` is assignable to `Any`. This
means that if we have a protocol `P` with `def method(self) -> Any`, we
would have `B <: A` (from nominal subtyping) and `A <: P` (`Any` is a
subtype of `Any`), but not `B <: P` (`int` is not a subtype of `Any`).
Breaking transitivity of subtyping is not tenable, so we don't use this
definition of subtyping.

## Test Plan

Existing tests (modified in some cases to account for updated
semantics.)

Stable property tests pass at a million iterations:
`QUICKCHECK_TESTS=1000000 cargo test -p ty_python_semantic -- --ignored
types::property_tests::stable`

### Changes to property test type generation

Since we no longer have a method of categorizing built types as
fully-static or not-fully-static, I had to add a previously-discussed
feature to the property tests so that some tests can build types that
are known by construction to be fully static, because there are still
properties that only apply to fully-static types (for example,
reflexiveness of subtyping.)

## Changes to handling of `*args, **kwargs` signatures

This PR "discovered" that, once we allow non-fully-static types to
participate in subtyping under the above definitions, `(*args: Any,
**kwargs: Any) -> Any` is now a subtype of `() -> object`. This is true,
if we take a literal interpretation of the former signature: all
materializations of the parameters `*args: Any, **kwargs: Any` can
accept zero arguments, making the former signature a subtype of the
latter. But the spec actually says that `*args: Any, **kwargs: Any`
should be interpreted as equivalent to `...`, and that makes a
difference here: `(...) -> Any` is not a subtype of `() -> object`,
because (unlike a literal reading of `(*args: Any, **kwargs: Any)`),
`...` can materialize to _any_ signature, including a signature with
required positional arguments.

This matters for this PR because it makes the "any two types are both
assignable to their union" property test fail if we don't implement the
equivalence to `...`. Because `FunctionType.__call__` has the signature
`(*args: Any, **kwargs: Any) -> Any`, and if we take that at face value
it's a subtype of `() -> object`, making `FunctionType` a subtype of `()
-> object)` -- but then a function with a required argument is also a
subtype of `FunctionType`, but not a subtype of `() -> object`. So I
went ahead and implemented the equivalence to `...` in this PR.

## Ecosystem analysis

* Most of the ecosystem report are cases of improved union/intersection
simplification. For example, we can now simplify a union like `bool |
(bool & Unknown) | Unknown` to simply `bool | Unknown`, because we can
now observe that every possible materialization of `bool & Unknown` is
still a subtype of `bool` (whereas before we would set aside `bool &
Unknown` as a not-fully-static type.) This is clearly an improvement.
* The `possibly-unresolved-reference` errors in sockeye, pymongo,
ignite, scrapy and others are true positives for conditional imports
that were formerly silenced by bogus conflicting-declarations (which we
currently don't issue a diagnostic for), because we considered two
different declarations of `Unknown` to be conflicting (we used
`is_equivalent_to` not `is_gradual_equivalent_to`). In this PR that
distinction disappears and all equivalence is gradual, so a declaration
of `Unknown` no longer conflicts with a declaration of `Unknown`, which
then results in us surfacing the possibly-unbound error.
* We will now issue "redundant cast" for casting from a typevar with a
gradual bound to the same typevar (the hydra-zen diagnostic). This seems
like an improvement.
* The new diagnostics in bandersnatch are interesting. For some reason
primer in CI seems to be checking bandersnatch on Python 3.10 (not yet
sure why; this doesn't happen when I run it locally). But bandersnatch
uses `enum.StrEnum`, which doesn't exist on 3.10. That makes the `class
SimpleDigest(StrEnum)` a class that inherits from `Unknown` (and
bypasses our current TODO handling for accessing attributes on enum
classes, since we don't recognize it as an enum class at all). This PR
improves our understanding of assignability to classes that inherit from
`Any` / `Unknown`, and we now recognize that a string literal is not
assignable to a class inheriting `Any` or `Unknown`.
This commit is contained in:
Carl Meyer 2025-06-24 18:02:05 -07:00 committed by GitHub
parent eee5a5a3d6
commit 62975b3ab2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 957 additions and 1633 deletions

View file

@ -558,7 +558,6 @@ pub enum Type<'db> {
BoundSuper(BoundSuperType<'db>),
/// A subtype of `bool` that allows narrowing in both positive and negative cases.
TypeIs(TypeIsType<'db>),
// TODO protocols, overloads, generics
}
#[salsa::tracked]
@ -1165,11 +1164,88 @@ impl<'db> Type<'db> {
}
}
/// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for
/// any `T` of this type.
///
/// This is true for fully static types, but also for some types that may not be fully static.
/// For example, a `ClassLiteral` may inherit `Any`, but its subtyping is still reflexive.
///
/// This method may have false negatives, but it should not have false positives. It should be
/// a cheap shallow check, not an exhaustive recursive check.
fn subtyping_is_always_reflexive(self) -> bool {
match self {
Type::Never
| Type::FunctionLiteral(..)
| Type::BoundMethod(_)
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(..)
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::PropertyInstance(_)
// might inherit `Any`, but subtyping is still reflexive
| Type::ClassLiteral(_) => true,
Type::Dynamic(_)
| Type::NominalInstance(_)
| Type::ProtocolInstance(_)
| Type::GenericAlias(_)
| Type::SubclassOf(_)
| Type::Union(_)
| Type::Intersection(_)
| Type::Callable(_)
| Type::Tuple(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_) => false,
}
}
/// Return true if this type is a [subtype of] type `target`.
///
/// This method returns `false` if either `self` or `other` is not fully static.
/// For fully static types, this means that the set of objects represented by `self` is a
/// subset of the objects represented by `target`.
///
/// For gradual types, it means that the union of all possible sets of values represented by
/// `self` (the "top materialization" of `self`) is a subtype of the intersection of all
/// possible sets of values represented by `target` (the "bottom materialization" of
/// `target`). In other words, for all possible pairs of materializations `self'` and
/// `target'`, `self'` is always a subtype of `target'`.
///
/// Note that this latter expansion of the subtyping relation to non-fully-static types is not
/// described in the typing spec, but the primary use of the subtyping relation is for
/// simplifying unions and intersections, and this expansion to gradual types is sound and
/// allows us to better simplify many unions and intersections. This definition does mean the
/// subtyping relation is not reflexive for non-fully-static types (e.g. `Any` is not a subtype
/// of `Any`).
///
/// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
///
/// There would be an even more general definition of subtyping for gradual types, allowing a
/// type `S` to be a subtype of a type `T` if the top materialization of `S` (`S+`) is a
/// subtype of `T+`, and the bottom materialization of `S` (`S-`) is a subtype of `T-`. This
/// definition is attractive in that it would restore reflexivity of subtyping for all types,
/// and would mean that gradual equivalence of `S` and `T` could be defined simply as `S <: T
/// && T <: S`. It would also be sound, in that simplifying unions or intersections according
/// to this definition of subtyping would still result in an equivalent type.
///
/// Unfortunately using this definition would break transitivity of subtyping when both nominal
/// and structural types are involved, because Liskov enforcement for nominal types is based on
/// assignability, so we can have class `A` with method `def meth(self) -> Any` and a subclass
/// `B(A)` with method `def meth(self) -> int`. In this case, `A` would be a subtype of a
/// protocol `P` with method `def meth(self) -> Any`, but `B` would not be a subtype of `P`,
/// and yet `B` is (by nominal subtyping) a subtype of `A`, so we would have `B <: A` and `A <:
/// P`, but not `B <: P`. Losing transitivity of subtyping is not tenable (it makes union and
/// intersection simplification dependent on the order in which elements are added), so we do
/// not use this more general definition of subtyping.
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.has_relation_to(db, target, TypeRelation::Subtyping)
}
@ -1182,23 +1258,26 @@ impl<'db> Type<'db> {
}
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) {
// Subtyping implies assignability, so if subtyping is reflexive and the two types are
// equivalent, it is both a subtype and assignable. Assignability is always reflexive.
if (relation.is_assignability() || self.subtyping_is_always_reflexive())
&& self.is_equivalent_to(db, target)
{
return true;
}
match (self, target) {
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => true,
// `Never` is the bottom type, the empty set.
// It is a subtype of all other fully static types.
(Type::Never, _) => true,
// Everything is a subtype of `object`.
(_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true,
// `Never` is the bottom type, the empty set.
// It is a subtype of all other types.
(Type::Never, _) => true,
// Dynamic is only a subtype of `object` and only a supertype of `Never`; both were
// handled above. It's always assignable, though.
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => relation.is_assignability(),
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
@ -1219,6 +1298,14 @@ impl<'db> Type<'db> {
false
}
// Two identical typevars must always solve to the same type, so they are always
// subtypes of each other and assignable to each other.
(Type::TypeVar(lhs_typevar), Type::TypeVar(rhs_typevar))
if lhs_typevar == rhs_typevar =>
{
true
}
// A fully static typevar is a subtype of its upper bound, and to something similar to
// the union of its constraints. An unbound, unconstrained, fully static typevar has an
// implicit upper bound of `object` (which is handled above).
@ -1250,7 +1337,7 @@ impl<'db> Type<'db> {
// `Never` is the bottom type, the empty set.
// Other than one unlikely edge case (TypeVars bound to `Never`),
// no other fully static type is a subtype of `Never`.
// no other type is a subtype of or assignable to `Never`.
(_, Type::Never) => false,
(Type::Union(union), _) => union
@ -1295,7 +1382,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, _) => {
relation.are_equivalent(db, target, Type::object(db))
target.is_equivalent_to(db, Type::object(db))
}
// These clauses handle type variants that include function literals. A function
@ -1410,6 +1497,15 @@ impl<'db> Type<'db> {
false
}
// `TypeIs` is invariant.
(Type::TypeIs(left), Type::TypeIs(right)) => {
left.return_type(db)
.has_relation_to(db, right.return_type(db), relation)
&& right
.return_type(db)
.has_relation_to(db, left.return_type(db), relation)
}
// `TypeIs[T]` is a subtype of `bool`.
(Type::TypeIs(_), _) => KnownClass::Bool
.to_instance(db)
@ -1425,11 +1521,7 @@ impl<'db> Type<'db> {
true
}
(Type::Callable(_), _) => {
// TODO: Implement subtyping between callable types and other types like
// function literals, bound methods, class literals, `type[]`, etc.)
false
}
(Type::Callable(_), _) => false,
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
self_tuple.has_relation_to(db, target_tuple, relation)
@ -1449,7 +1541,7 @@ impl<'db> Type<'db> {
}
(Type::Tuple(_), _) => false,
(Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target),
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target),
(Type::BoundSuper(_), _) => KnownClass::Super
.to_instance(db)
.has_relation_to(db, target, relation),
@ -1459,15 +1551,17 @@ impl<'db> Type<'db> {
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
.subclass_of()
.into_class()
.is_none_or(|subclass_of_class| {
.map(|subclass_of_class| {
ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation)
}),
})
.unwrap_or(relation.is_assignability()),
(Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
.subclass_of()
.into_class()
.is_none_or(|subclass_of_class| {
.map(|subclass_of_class| {
ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation)
}),
})
.unwrap_or(relation.is_assignability()),
// This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`?
(Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => {
@ -1494,25 +1588,20 @@ impl<'db> Type<'db> {
.metaclass_instance_type(db)
.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) =>
// `type[Any]` is a subtype of `type[object]`, and is assignable to any `type[...]`
(Type::SubclassOf(subclass_of_ty), other) if subclass_of_ty.is_dynamic() => {
KnownClass::Type
.to_instance(db)
.has_relation_to(db, other, relation)
|| (relation.is_assignability()
&& other.has_relation_to(db, KnownClass::Type.to_instance(db), relation))
}
// Any `type[...]` type is assignable to `type[Any]`
(other, Type::SubclassOf(subclass_of_ty))
if subclass_of_ty.is_dynamic() && relation.is_assignability() =>
{
true
other.has_relation_to(db, KnownClass::Type.to_instance(db), relation)
}
// `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses
@ -1561,43 +1650,7 @@ impl<'db> Type<'db> {
/// Return true if this type is [equivalent to] type `other`.
///
/// This method returns `false` if either `self` or `other` is not fully static.
///
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
// TODO equivalent but not identical types: TypedDicts, Protocols, type aliases, etc.
match (self, other) {
(Type::Union(left), Type::Union(right)) => left.is_equivalent_to(db, right),
(Type::Intersection(left), Type::Intersection(right)) => {
left.is_equivalent_to(db, right)
}
(Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
self_function.is_equivalent_to(db, target_function)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_equivalent_to(db, target_method)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_equivalent_to(db, target_method)
}
(Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right),
(Type::NominalInstance(left), Type::NominalInstance(right)) => {
left.is_equivalent_to(db, right)
}
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.is_equivalent_to(db, right)
}
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
n.class.is_object(db) && protocol.normalized(db) == nominal
}
_ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
}
}
/// Returns true if this type and `other` are gradual equivalent.
/// Two equivalent types represent the same sets of values.
///
/// > Two gradual types `A` and `B` are equivalent
/// > (that is, the same gradual type, not merely consistent with one another)
@ -1606,10 +1659,8 @@ impl<'db> Type<'db> {
/// >
/// > &mdash; [Summary of type relations]
///
/// This powers the `assert_type()` directive.
///
/// [Summary of type relations]: https://typing.python.org/en/latest/spec/concepts.html#summary-of-type-relations
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
if self == other {
return true;
}
@ -1626,32 +1677,30 @@ impl<'db> Type<'db> {
}
(Type::NominalInstance(first), Type::NominalInstance(second)) => {
first.is_gradual_equivalent_to(db, second)
first.is_equivalent_to(db, second)
}
(Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second),
(Type::Tuple(first), Type::Tuple(second)) => first.is_equivalent_to(db, second),
(Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second),
(Type::Union(first), Type::Union(second)) => first.is_equivalent_to(db, second),
(Type::Intersection(first), Type::Intersection(second)) => {
first.is_gradual_equivalent_to(db, second)
first.is_equivalent_to(db, second)
}
(Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => {
self_function.is_gradual_equivalent_to(db, target_function)
self_function.is_equivalent_to(db, target_function)
}
(Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => {
self_method.is_gradual_equivalent_to(db, target_method)
self_method.is_equivalent_to(db, target_method)
}
(Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => {
self_method.is_gradual_equivalent_to(db, target_method)
}
(Type::Callable(first), Type::Callable(second)) => {
first.is_gradual_equivalent_to(db, second)
self_method.is_equivalent_to(db, target_method)
}
(Type::Callable(first), Type::Callable(second)) => first.is_equivalent_to(db, second),
(Type::ProtocolInstance(first), Type::ProtocolInstance(second)) => {
first.is_gradual_equivalent_to(db, second)
first.is_equivalent_to(db, second)
}
(Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n))
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
@ -2125,75 +2174,6 @@ impl<'db> Type<'db> {
}
}
/// Returns true if the type does not contain any gradual forms (as a sub-part).
pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool {
match self {
Type::Dynamic(_) => false,
Type::Never
| Type::FunctionLiteral(..)
| Type::BoundMethod(_)
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(..)
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::PropertyInstance(_) => true,
Type::ProtocolInstance(protocol) => protocol.is_fully_static(db),
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => true,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.is_fully_static(db),
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_fully_static(db)),
},
Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(),
Type::BoundSuper(bound_super) => {
!matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_))
&& !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_))
}
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::NominalInstance(_) => {
// TODO: Ideally, we would iterate over the MRO of the class, check if all
// bases are fully static, and only return `true` if that is the case.
//
// This does not work yet, because we currently infer `Unknown` for some
// generic base classes that we don't understand yet. For example, `str`
// is defined as `class str(Sequence[str])` in typeshed and we currently
// compute its MRO as `(str, Unknown, object)`. This would make us think
// that `str` is a gradual type, which causes all sorts of downstream
// issues because it does not participate in equivalence/subtyping etc.
//
// Another problem is that we run into problems if we eagerly query the
// MRO of class literals here. I have not fully investigated this, but
// iterating over the MRO alone, without even acting on it, causes us to
// infer `Unknown` for many classes.
true
}
Type::Union(union) => union.is_fully_static(db),
Type::Intersection(intersection) => intersection.is_fully_static(db),
// TODO: Once we support them, make sure that we return `false` for other types
// containing gradual forms such as `tuple[Any, ...]`.
// Conversely, make sure to return `true` for homogeneous tuples such as
// `tuple[int, ...]`, once we add support for them.
Type::Tuple(tuple) => tuple.is_fully_static(db),
Type::Callable(callable) => callable.is_fully_static(db),
Type::TypeIs(type_is) => type_is.return_type(db).is_fully_static(db),
}
}
/// Return true if there is just a single inhabitant for this type.
///
/// Note: This function aims to have no false positives, but might return `false`
@ -3744,8 +3724,7 @@ impl<'db> Type<'db> {
KnownFunction::IsEquivalentTo
| KnownFunction::IsSubtypeOf
| KnownFunction::IsAssignableTo
| KnownFunction::IsDisjointFrom
| KnownFunction::IsGradualEquivalentTo,
| KnownFunction::IsDisjointFrom,
) => Binding::single(
self,
Signature::new(
@ -3762,20 +3741,20 @@ impl<'db> Type<'db> {
)
.into(),
Some(
KnownFunction::IsFullyStatic
| KnownFunction::IsSingleton
| KnownFunction::IsSingleValued,
) => Binding::single(
self,
Signature::new(
Parameters::new([Parameter::positional_only(Some(Name::new_static("a")))
Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => {
Binding::single(
self,
Signature::new(
Parameters::new([Parameter::positional_only(Some(Name::new_static(
"a",
)))
.type_form()
.with_annotated_type(Type::any())]),
Some(KnownClass::Bool.to_instance(db)),
),
)
.into(),
Some(KnownClass::Bool.to_instance(db)),
),
)
.into()
}
Some(KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization) => {
Binding::single(
@ -6981,34 +6960,7 @@ pub(crate) enum TypeRelation {
}
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 {
pub(crate) const fn is_assignability(self) -> bool {
matches!(self, TypeRelation::Assignability)
}
}
@ -7147,14 +7099,6 @@ impl<'db> BoundMethodType<'db> {
.self_instance(db)
.is_equivalent_to(db, self.self_instance(db))
}
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.function(db)
.is_gradual_equivalent_to(db, other.function(db))
&& other
.self_instance(db)
.is_gradual_equivalent_to(db, self.self_instance(db))
}
}
/// This type represents the set of all callable objects with a certain, possibly overloaded,
@ -7257,13 +7201,6 @@ impl<'db> CallableType<'db> {
self.signatures(db).find_legacy_typevars(db, typevars);
}
/// Check whether this callable type is fully static.
///
/// See [`Type::is_fully_static`] for more details.
fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.signatures(db).is_fully_static(db)
}
/// Check whether this callable type has the given relation to another callable type.
///
/// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details.
@ -7285,16 +7222,6 @@ impl<'db> CallableType<'db> {
.is_equivalent_to(db, other.signatures(db))
}
/// Check whether this callable type is gradual equivalent to another callable type.
///
/// See [`Type::is_gradual_equivalent_to`] for more details.
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.is_function_like(db) == other.is_function_like(db)
&& self
.signatures(db)
.is_gradual_equivalent_to(db, other.signatures(db))
}
/// See [`Type::replace_self_reference`].
fn replace_self_reference(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Self {
CallableType::new(
@ -7391,39 +7318,6 @@ impl<'db> MethodWrapperKind<'db> {
}
}
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
match (self, other) {
(
MethodWrapperKind::FunctionTypeDunderGet(self_function),
MethodWrapperKind::FunctionTypeDunderGet(other_function),
) => self_function.is_gradual_equivalent_to(db, other_function),
(
MethodWrapperKind::FunctionTypeDunderCall(self_function),
MethodWrapperKind::FunctionTypeDunderCall(other_function),
) => self_function.is_gradual_equivalent_to(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 normalized(self, db: &'db dyn Db) -> Self {
match self {
MethodWrapperKind::FunctionTypeDunderGet(function) => {
@ -7781,10 +7675,6 @@ impl<'db> UnionType<'db> {
}
}
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.elements(db).iter().all(|ty| ty.is_fully_static(db))
}
/// Create a new union type with the elements normalized.
///
/// See [`Type::normalized`] for more details.
@ -7799,15 +7689,8 @@ impl<'db> UnionType<'db> {
UnionType::new(db, new_elements.into_boxed_slice())
}
/// Return `true` if `self` represents the exact same set of possible runtime objects as `other`
/// Return `true` if `self` represents the exact same sets of possible runtime objects as `other`
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
/// Inlined version of [`UnionType::is_fully_static`] to avoid having to lookup
/// `self.elements` multiple times in the Salsa db in this single method.
#[inline]
fn all_fully_static(db: &dyn Db, elements: &[Type]) -> bool {
elements.iter().all(|ty| ty.is_fully_static(db))
}
let self_elements = self.elements(db);
let other_elements = other.elements(db);
@ -7815,14 +7698,6 @@ impl<'db> UnionType<'db> {
return false;
}
if !all_fully_static(db, self_elements) {
return false;
}
if !all_fully_static(db, other_elements) {
return false;
}
if self == other {
return true;
}
@ -7835,39 +7710,6 @@ impl<'db> UnionType<'db> {
sorted_self == other.normalized(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_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
if self == other {
return true;
}
// TODO: `T | Unknown` should be gradually equivalent to `T | Unknown | Any`,
// since they have exactly the same set of possible static materializations
// (they represent the same set of possible sets of possible runtime objects)
if self.elements(db).len() != other.elements(db).len() {
return false;
}
let sorted_self = self.normalized(db);
if sorted_self == other {
return true;
}
let sorted_other = other.normalized(db);
if sorted_self == sorted_other {
return true;
}
sorted_self
.elements(db)
.iter()
.zip(sorted_other.elements(db))
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
}
#[salsa::interned(debug)]
@ -7910,52 +7752,24 @@ impl<'db> IntersectionType<'db> {
)
}
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.positive(db).iter().all(|ty| ty.is_fully_static(db))
&& self.negative(db).iter().all(|ty| ty.is_fully_static(db))
}
/// Return `true` if `self` represents exactly the same set of possible runtime objects as `other`
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
/// Inlined version of [`IntersectionType::is_fully_static`] to avoid having to lookup
/// `positive` and `negative` multiple times in the Salsa db in this single method.
#[inline]
fn all_fully_static(db: &dyn Db, elements: &FxOrderSet<Type>) -> bool {
elements.iter().all(|ty| ty.is_fully_static(db))
}
let self_positive = self.positive(db);
if !all_fully_static(db, self_positive) {
return false;
}
let other_positive = other.positive(db);
if self_positive.len() != other_positive.len() {
return false;
}
if !all_fully_static(db, other_positive) {
return false;
}
let self_negative = self.negative(db);
if !all_fully_static(db, self_negative) {
return false;
}
let other_negative = other.negative(db);
if self_negative.len() != other_negative.len() {
return false;
}
if !all_fully_static(db, other_negative) {
return false;
}
if self == other {
return true;
}
@ -7969,43 +7783,6 @@ impl<'db> IntersectionType<'db> {
sorted_self == other.normalized(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_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
if self == other {
return true;
}
if self.positive(db).len() != other.positive(db).len()
|| self.negative(db).len() != other.negative(db).len()
{
return false;
}
let sorted_self = self.normalized(db);
if sorted_self == other {
return true;
}
let sorted_other = other.normalized(db);
if sorted_self == sorted_other {
return true;
}
sorted_self
.positive(db)
.iter()
.zip(sorted_other.positive(db))
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
&& sorted_self
.negative(db)
.iter()
.zip(sorted_other.negative(db))
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
pub(crate) fn map_with_boundness(
self,
db: &'db dyn Db,

View file

@ -78,6 +78,7 @@ impl<'db> Type<'db> {
}
}
#[derive(Debug)]
enum UnionElement<'db> {
IntLiterals(FxOrderSet<i64>),
StringLiterals(FxOrderSet<StringLiteralType<'db>>),
@ -87,27 +88,26 @@ enum UnionElement<'db> {
impl<'db> UnionElement<'db> {
/// Try reducing this `UnionElement` given the presence in the same union of `other_type`.
///
/// If this `UnionElement` is a group of literals, filter the literals present if needed and
/// return `ReduceResult::KeepIf` with a boolean value indicating whether the remaining group
/// of literals should be kept in the union
///
/// If this `UnionElement` is some other type, return `ReduceResult::Type` so `UnionBuilder`
/// can perform more complex checks on it.
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
match self {
UnionElement::IntLiterals(literals) => {
if other_type.splits_literals(db, LiteralKind::Int) {
let mut collapse = false;
let mut ignore = false;
let negated = other_type.negate(db);
literals.retain(|literal| {
let ty = Type::IntLiteral(*literal);
if negated.is_subtype_of(db, ty) {
collapse = true;
}
if other_type.is_subtype_of(db, ty) {
ignore = true;
}
!ty.is_subtype_of(db, other_type)
});
if collapse {
if ignore {
ReduceResult::Ignore
} else if collapse {
ReduceResult::CollapseToObject
} else {
ReduceResult::KeepIf(!literals.is_empty())
@ -121,15 +121,21 @@ impl<'db> UnionElement<'db> {
UnionElement::StringLiterals(literals) => {
if other_type.splits_literals(db, LiteralKind::String) {
let mut collapse = false;
let mut ignore = false;
let negated = other_type.negate(db);
literals.retain(|literal| {
let ty = Type::StringLiteral(*literal);
if negated.is_subtype_of(db, ty) {
collapse = true;
}
if other_type.is_subtype_of(db, ty) {
ignore = true;
}
!ty.is_subtype_of(db, other_type)
});
if collapse {
if ignore {
ReduceResult::Ignore
} else if collapse {
ReduceResult::CollapseToObject
} else {
ReduceResult::KeepIf(!literals.is_empty())
@ -143,15 +149,21 @@ impl<'db> UnionElement<'db> {
UnionElement::BytesLiterals(literals) => {
if other_type.splits_literals(db, LiteralKind::Bytes) {
let mut collapse = false;
let mut ignore = false;
let negated = other_type.negate(db);
literals.retain(|literal| {
let ty = Type::BytesLiteral(*literal);
if negated.is_subtype_of(db, ty) {
collapse = true;
}
if other_type.is_subtype_of(db, ty) {
ignore = true;
}
!ty.is_subtype_of(db, other_type)
});
if collapse {
if ignore {
ReduceResult::Ignore
} else if collapse {
ReduceResult::CollapseToObject
} else {
ReduceResult::KeepIf(!literals.is_empty())
@ -173,6 +185,8 @@ enum ReduceResult<'db> {
KeepIf(bool),
/// Collapse this entire union to `object`.
CollapseToObject,
/// The new element is a subtype of an existing part of the `UnionElement`, ignore it.
Ignore,
/// The given `Type` can stand-in for the entire `UnionElement` for further union
/// simplification checks.
Type(Type<'db>),
@ -229,9 +243,10 @@ impl<'db> UnionBuilder<'db> {
// means we shouldn't add it. Otherwise, add a new `UnionElement::StringLiterals`
// containing it.
Type::StringLiteral(literal) => {
let mut found = false;
let mut found = None;
let mut to_remove = None;
let ty_negated = ty.negate(self.db);
for element in &mut self.elements {
for (index, element) in self.elements.iter_mut().enumerate() {
match element {
UnionElement::StringLiterals(literals) => {
if literals.len() >= MAX_UNION_LITERALS {
@ -239,14 +254,16 @@ impl<'db> UnionBuilder<'db> {
self.add_in_place(replace_with);
return;
}
literals.insert(literal);
found = true;
break;
found = Some(literals);
continue;
}
UnionElement::Type(existing) => {
if ty.is_subtype_of(self.db, *existing) {
return;
}
if existing.is_subtype_of(self.db, ty) {
to_remove = Some(index);
}
if ty_negated.is_subtype_of(self.db, *existing) {
// The type that includes both this new element, and its negation
// (or a supertype of its negation), must be simply `object`.
@ -257,18 +274,24 @@ impl<'db> UnionBuilder<'db> {
_ => {}
}
}
if !found {
if let Some(found) = found {
found.insert(literal);
} else {
self.elements
.push(UnionElement::StringLiterals(FxOrderSet::from_iter([
literal,
])));
}
if let Some(index) = to_remove {
self.elements.swap_remove(index);
}
}
// Same for bytes literals as for string literals, above.
Type::BytesLiteral(literal) => {
let mut found = false;
let mut found = None;
let mut to_remove = None;
let ty_negated = ty.negate(self.db);
for element in &mut self.elements {
for (index, element) in self.elements.iter_mut().enumerate() {
match element {
UnionElement::BytesLiterals(literals) => {
if literals.len() >= MAX_UNION_LITERALS {
@ -276,14 +299,16 @@ impl<'db> UnionBuilder<'db> {
self.add_in_place(replace_with);
return;
}
literals.insert(literal);
found = true;
break;
found = Some(literals);
continue;
}
UnionElement::Type(existing) => {
if ty.is_subtype_of(self.db, *existing) {
return;
}
if existing.is_subtype_of(self.db, ty) {
to_remove = Some(index);
}
if ty_negated.is_subtype_of(self.db, *existing) {
// The type that includes both this new element, and its negation
// (or a supertype of its negation), must be simply `object`.
@ -294,18 +319,24 @@ impl<'db> UnionBuilder<'db> {
_ => {}
}
}
if !found {
if let Some(found) = found {
found.insert(literal);
} else {
self.elements
.push(UnionElement::BytesLiterals(FxOrderSet::from_iter([
literal,
])));
}
if let Some(index) = to_remove {
self.elements.swap_remove(index);
}
}
// And same for int literals as well.
Type::IntLiteral(literal) => {
let mut found = false;
let mut found = None;
let mut to_remove = None;
let ty_negated = ty.negate(self.db);
for element in &mut self.elements {
for (index, element) in self.elements.iter_mut().enumerate() {
match element {
UnionElement::IntLiterals(literals) => {
if literals.len() >= MAX_UNION_LITERALS {
@ -313,14 +344,16 @@ impl<'db> UnionBuilder<'db> {
self.add_in_place(replace_with);
return;
}
literals.insert(literal);
found = true;
break;
found = Some(literals);
continue;
}
UnionElement::Type(existing) => {
if ty.is_subtype_of(self.db, *existing) {
return;
}
if existing.is_subtype_of(self.db, ty) {
to_remove = Some(index);
}
if ty_negated.is_subtype_of(self.db, *existing) {
// The type that includes both this new element, and its negation
// (or a supertype of its negation), must be simply `object`.
@ -331,10 +364,15 @@ impl<'db> UnionBuilder<'db> {
_ => {}
}
}
if !found {
if let Some(found) = found {
found.insert(literal);
} else {
self.elements
.push(UnionElement::IntLiterals(FxOrderSet::from_iter([literal])));
}
if let Some(index) = to_remove {
self.elements.swap_remove(index);
}
}
// Adding `object` to a union results in `object`.
ty if ty.is_object(self.db) => {
@ -347,7 +385,6 @@ impl<'db> UnionBuilder<'db> {
None
};
let mut to_add = ty;
let mut to_remove = SmallVec::<[usize; 2]>::new();
let ty_negated = ty.negate(self.db);
@ -364,20 +401,17 @@ impl<'db> UnionBuilder<'db> {
self.collapse_to_object();
return;
}
ReduceResult::Ignore => {
return;
}
};
if Some(element_type) == bool_pair {
to_add = KnownClass::Bool.to_instance(self.db);
to_remove.push(index);
// The type we are adding is a BooleanLiteral, which doesn't have any
// subtypes. And we just found that the union already contained our
// mirror-image BooleanLiteral, so it can't also contain bool or any
// supertype of bool. Therefore, we are done.
break;
self.add_in_place(KnownClass::Bool.to_instance(self.db));
return;
}
if ty.is_gradual_equivalent_to(self.db, element_type)
if ty.is_equivalent_to(self.db, element_type)
|| ty.is_subtype_of(self.db, element_type)
|| element_type.is_object(self.db)
{
return;
} else if element_type.is_subtype_of(self.db, ty) {
@ -397,13 +431,13 @@ impl<'db> UnionBuilder<'db> {
}
}
if let Some((&first, rest)) = to_remove.split_first() {
self.elements[first] = UnionElement::Type(to_add);
self.elements[first] = UnionElement::Type(ty);
// We iterate in descending order to keep remaining indices valid after `swap_remove`.
for &index in rest.iter().rev() {
self.elements.swap_remove(index);
}
} else {
self.elements.push(UnionElement::Type(to_add));
self.elements.push(UnionElement::Type(ty));
}
}
}
@ -681,7 +715,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
for (index, existing_positive) in self.positive.iter().enumerate() {
// S & T = S if S <: T
if existing_positive.is_subtype_of(db, new_positive)
|| existing_positive.is_gradual_equivalent_to(db, new_positive)
|| existing_positive.is_equivalent_to(db, new_positive)
{
return;
}
@ -778,7 +812,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
for (index, existing_negative) in self.negative.iter().enumerate() {
// ~S & ~T = ~T if S <: T
if existing_negative.is_subtype_of(db, new_negative)
|| existing_negative.is_gradual_equivalent_to(db, new_negative)
|| existing_negative.is_equivalent_to(db, new_negative)
{
to_remove.push(index);
}

View file

@ -599,21 +599,6 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownFunction::IsGradualEquivalentTo) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, *ty_b),
));
}
}
Some(KnownFunction::IsFullyStatic) => {
if let [Some(ty)] = overload.parameter_types() {
overload
.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
}
}
Some(KnownFunction::IsSingleton) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
@ -801,15 +786,13 @@ impl<'db> Bindings<'db> {
overload.set_return_type(
match instance_ty.static_member(db, attr_name.value(db)) {
Place::Type(ty, Boundness::Bound) => {
if instance_ty.is_fully_static(db) {
ty
} else {
if ty.is_dynamic() {
// Here, we attempt to model the fact that an attribute lookup on
// a non-fully static type could fail. This is an approximation,
// as there are gradual types like `tuple[Any]`, on which a lookup
// of (e.g. of the `index` method) would always succeed.
// a dynamic type could fail
union_with_default(ty)
} else {
ty
}
}
Place::Type(ty, Boundness::PossiblyUnbound) => {
@ -1396,7 +1379,7 @@ impl<'db> CallableBinding<'db> {
.annotated_type()
.unwrap_or(Type::unknown());
if let Some(first_parameter_type) = first_parameter_type {
if !first_parameter_type.is_gradual_equivalent_to(db, current_parameter_type) {
if !first_parameter_type.is_equivalent_to(db, current_parameter_type) {
participating_parameter_index = Some(parameter_index);
break;
}

View file

@ -379,9 +379,10 @@ impl<'db> ClassType<'db> {
) -> bool {
self.iter_mro(db).any(|base| {
match base {
ClassBase::Dynamic(_) => {
relation.applies_to_non_fully_static_types() && !other.is_final(db)
}
ClassBase::Dynamic(_) => match relation {
TypeRelation::Subtyping => other.is_object(db),
TypeRelation::Assignability => !other.is_final(db),
},
// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol | ClassBase::Generic => false,
@ -417,20 +418,6 @@ impl<'db> ClassType<'db> {
}
}
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,
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false,
(ClassType::Generic(this), ClassType::Generic(other)) => {
this.origin(db) == other.origin(db)
&& this
.specialization(db)
.is_gradual_equivalent_to(db, other.specialization(db))
}
}
}
/// 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> {
let (class_literal, specialization) = self.class_literal(db);
@ -1427,14 +1414,15 @@ impl<'db> ClassLiteral<'db> {
continue;
}
// The descriptor handling below is guarded by this fully-static check, because dynamic
// types like `Any` are valid (data) descriptors: since they have all possible attributes,
// they also have a (callable) `__set__` method. The problem is that we can't determine
// the type of the value parameter this way. Instead, we want to use the dynamic type
// itself in this case, so we skip the special descriptor handling.
if attr_ty.is_fully_static(db) {
let dunder_set = attr_ty.class_member(db, "__set__".into());
if let Some(dunder_set) = dunder_set.place.ignore_possibly_unbound() {
let dunder_set = attr_ty.class_member(db, "__set__".into());
if let Place::Type(dunder_set, Boundness::Bound) = dunder_set.place {
// The descriptor handling below is guarded by this not-dynamic check, because
// dynamic types like `Any` are valid (data) descriptors: since they have all
// possible attributes, they also have a (callable) `__set__` method. The
// problem is that we can't determine the type of the value parameter this way.
// Instead, we want to use the dynamic type itself in this case, so we skip the
// special descriptor handling.
if !dunder_set.is_dynamic() {
// This type of this attribute is a data descriptor. Instead of overwriting the
// descriptor attribute, data-classes will (implicitly) call the `__set__` method
// of the descriptor. This means that the synthesized `__init__` parameter for

View file

@ -148,12 +148,12 @@ declare_lint! {
/// ## Examples
/// ```python
/// from typing import reveal_type
/// from ty_extensions import is_fully_static
/// from ty_extensions import is_singleton
///
/// if flag:
/// f = repr # Expects a value
/// else:
/// f = is_fully_static # Expects a type form
/// f = is_singleton # Expects a type form
///
/// f(int) # error
/// ```

View file

@ -736,9 +736,6 @@ impl<'db> FunctionType<'db> {
}
let self_signature = self.signature(db);
let other_signature = other.signature(db);
if !self_signature.is_fully_static(db) || !other_signature.is_fully_static(db) {
return false;
}
self_signature.is_subtype_of(db, other_signature)
}
@ -760,19 +757,9 @@ impl<'db> FunctionType<'db> {
}
let self_signature = self.signature(db);
let other_signature = other.signature(db);
if !self_signature.is_fully_static(db) || !other_signature.is_fully_static(db) {
return false;
}
self_signature.is_equivalent_to(db, other_signature)
}
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.literal(db) == other.literal(db)
&& self
.signature(db)
.is_gradual_equivalent_to(db, other.signature(db))
}
pub(crate) fn find_legacy_typevars(
self,
db: &'db dyn Db,
@ -878,10 +865,6 @@ pub enum KnownFunction {
IsAssignableTo,
/// `ty_extensions.is_disjoint_from`
IsDisjointFrom,
/// `ty_extensions.is_gradual_equivalent_to`
IsGradualEquivalentTo,
/// `ty_extensions.is_fully_static`
IsFullyStatic,
/// `ty_extensions.is_singleton`
IsSingleton,
/// `ty_extensions.is_single_valued`
@ -948,8 +931,6 @@ impl KnownFunction {
Self::IsAssignableTo
| Self::IsDisjointFrom
| Self::IsEquivalentTo
| Self::IsGradualEquivalentTo
| Self::IsFullyStatic
| Self::IsSingleValued
| Self::IsSingleton
| Self::IsSubtypeOf
@ -1009,12 +990,10 @@ pub(crate) mod tests {
| KnownFunction::GenericContext
| KnownFunction::DunderAllNames
| KnownFunction::StaticAssert
| KnownFunction::IsFullyStatic
| KnownFunction::IsDisjointFrom
| KnownFunction::IsSingleValued
| KnownFunction::IsAssignableTo
| KnownFunction::IsEquivalentTo
| KnownFunction::IsGradualEquivalentTo
| KnownFunction::TopMaterialization
| KnownFunction::BottomMaterialization
| KnownFunction::AllMembers => KnownModule::TyExtensions,

View file

@ -463,10 +463,6 @@ 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;
}
// Equivalence of each type in the specialization depends on the variance of the
// corresponding typevar:
// - covariant: verify that self_type == other_type
@ -487,42 +483,6 @@ impl<'db> Specialization<'db> {
true
}
pub(crate) fn is_gradual_equivalent_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))
{
// 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 typevar.variance(db) {
TypeVarVariance::Invariant
| TypeVarVariance::Covariant
| TypeVarVariance::Contravariant => {
self_type.is_gradual_equivalent_to(db, *other_type)
}
TypeVarVariance::Bivariant => true,
};
if !compatible {
return false;
}
}
true
}
pub(crate) fn find_legacy_typevars(
self,
db: &'db dyn Db,

View file

@ -5448,8 +5448,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let [Some(actual_ty), Some(asserted_ty)] =
overload.parameter_types()
{
if !actual_ty
.is_gradual_equivalent_to(self.db(), *asserted_ty)
if !actual_ty.is_equivalent_to(self.db(), *asserted_ty)
{
if let Some(builder) = self.context.report_lint(
&TYPE_ASSERTION_FAILURE,
@ -5586,14 +5585,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let db = self.db();
let contains_unknown_or_todo = |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
if source_type.is_equivalent_to(db, *casted_type)
|| (source_type.normalized(db)
== casted_type.normalized(db)
&& !casted_type.any_over_type(db, &|ty| {
contains_unknown_or_todo(ty)
})
&& !source_type.any_over_type(db, &|ty| {
contains_unknown_or_todo(ty)
}))
&& !casted_type.any_over_type(db, &|ty| {
contains_unknown_or_todo(ty)
})
&& !source_type.any_over_type(db, &|ty| {
contains_unknown_or_todo(ty)
})
{
if let Some(builder) = self
.context

View file

@ -108,10 +108,6 @@ impl<'db> NominalInstanceType<'db> {
!self.class.could_coexist_in_mro_with(db, other.class)
}
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_gradual_equivalent_to(db, other.class)
}
pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool {
self.class.known(db).is_some_and(KnownClass::is_singleton)
}
@ -240,11 +236,6 @@ impl<'db> ProtocolInstanceType<'db> {
self.inner.interface(db).any_over_type(db, type_fn)
}
/// Return `true` if this protocol type is fully static.
pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.inner.interface(db).is_fully_static(db)
}
/// 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
@ -252,13 +243,9 @@ impl<'db> ProtocolInstanceType<'db> {
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
_relation: TypeRelation,
) -> bool {
relation.applies_to(
db,
Type::ProtocolInstance(self),
Type::ProtocolInstance(other),
) && other
other
.inner
.interface(db)
.is_sub_interface_of(db, self.inner.interface(db))
@ -268,15 +255,6 @@ impl<'db> ProtocolInstanceType<'db> {
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.is_fully_static(db)
&& other.is_fully_static(db)
&& self.normalized(db) == other.normalized(db)
}
/// Return `true` if this protocol type is gradually equivalent to the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.normalized(db) == other.normalized(db)
}

View file

@ -50,9 +50,19 @@ macro_rules! type_property_test {
$property
}
};
($test_name:ident, $db:ident, forall fully_static_types $($types:ident),+ . $property:expr) => {
#[quickcheck_macros::quickcheck]
#[ignore]
fn $test_name($($types: crate::types::property_tests::type_generation::FullyStaticTy),+) -> bool {
let $db = &crate::types::property_tests::setup::get_cached_db();
$(let $types = $types.into_type($db);)+
$property
}
};
// A property test with a logical implication.
($name:ident, $db:ident, forall types $($types:ident),+ . $premise:expr => $conclusion:expr) => {
type_property_test!($name, $db, forall types $($types),+ . !($premise) || ($conclusion));
($name:ident, $db:ident, forall $typekind:ident $($types:ident),+ . $premise:expr => $conclusion:expr) => {
type_property_test!($name, $db, forall $typekind $($types),+ . !($premise) || ($conclusion));
};
}
@ -63,11 +73,10 @@ mod stable {
// Reflexivity: `T` is equivalent to itself.
type_property_test!(
equivalent_to_is_reflexive, db,
forall types t. t.is_fully_static(db) => t.is_equivalent_to(db, t)
forall types t. t.is_equivalent_to(db, t)
);
// Symmetry: If `S` is equivalent to `T`, then `T` must be equivalent to `S`.
// Note that this (trivially) holds true for gradual types as well.
type_property_test!(
equivalent_to_is_symmetric, db,
forall types s, t. s.is_equivalent_to(db, t) => t.is_equivalent_to(db, s)
@ -79,18 +88,6 @@ mod stable {
forall types s, t, u. s.is_equivalent_to(db, t) && t.is_equivalent_to(db, u) => s.is_equivalent_to(db, u)
);
// Symmetry: If `S` is gradual equivalent to `T`, `T` is gradual equivalent to `S`.
type_property_test!(
gradual_equivalent_to_is_symmetric, db,
forall types s, t. s.is_gradual_equivalent_to(db, t) => t.is_gradual_equivalent_to(db, s)
);
// A fully static type `T` is a subtype of itself.
type_property_test!(
subtype_of_is_reflexive, db,
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, t)
);
// `S <: T` and `T <: U` implies that `S <: U`.
type_property_test!(
subtype_of_is_transitive, db,
@ -133,28 +130,16 @@ mod stable {
forall types t. t.is_singleton(db) => t.is_single_valued(db)
);
// If `T` contains a gradual form, it should not participate in equivalence
type_property_test!(
non_fully_static_types_do_not_participate_in_equivalence, db,
forall types s, t. !s.is_fully_static(db) => !s.is_equivalent_to(db, t) && !t.is_equivalent_to(db, s)
);
// If `T` contains a gradual form, it should not participate in subtyping
type_property_test!(
non_fully_static_types_do_not_participate_in_subtyping, db,
forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s)
);
// All types should be assignable to `object`
type_property_test!(
all_types_assignable_to_object, db,
forall types t. t.is_assignable_to(db, Type::object(db))
);
// And for fully static types, they should also be subtypes of `object`
// And all types should be subtypes of `object`
type_property_test!(
all_fully_static_types_subtype_of_object, db,
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, Type::object(db))
all_types_subtype_of_object, db,
forall types t. t.is_subtype_of(db, Type::object(db))
);
// Never should be assignable to every type
@ -163,54 +148,63 @@ mod stable {
forall types t. Type::Never.is_assignable_to(db, t)
);
// And it should be a subtype of all fully static types
// And it should be a subtype of all types
type_property_test!(
never_subtype_of_every_fully_static_type, db,
forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t)
never_subtype_of_every_type, db,
forall types t. Type::Never.is_subtype_of(db, t)
);
// Similar to `Never`, a fully-static "bottom" callable type should be a subtype of all
// fully-static callable types
// Similar to `Never`, a "bottom" callable type should be a subtype of all callable types
type_property_test!(
bottom_callable_is_subtype_of_all_fully_static_callable, db,
forall types t. t.is_callable_type() && t.is_fully_static(db)
bottom_callable_is_subtype_of_all_callable, db,
forall types t. t.is_callable_type()
=> CallableType::bottom(db).is_subtype_of(db, t)
);
// For any two fully static types, each type in the pair must be a subtype of their union.
type_property_test!(
all_fully_static_type_pairs_are_subtype_of_their_union, db,
forall types s, t.
s.is_fully_static(db) && t.is_fully_static(db)
=> s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t]))
);
// A fully static type does not have any materializations.
// Thus, two equivalent (fully static) types are also gradual equivalent.
type_property_test!(
two_equivalent_types_are_also_gradual_equivalent, db,
forall types s, t. s.is_equivalent_to(db, t) => s.is_gradual_equivalent_to(db, t)
);
// Two gradual equivalent fully static types are also equivalent.
type_property_test!(
two_gradual_equivalent_fully_static_types_are_also_equivalent, db,
forall types s, t.
s.is_fully_static(db) && s.is_gradual_equivalent_to(db, t) => s.is_equivalent_to(db, t)
);
// `T` can be assigned to itself.
type_property_test!(
assignable_to_is_reflexive, db,
forall types t. t.is_assignable_to(db, t)
);
// For *any* pair of types, whether fully static or not,
// each of the pair should be assignable to the union of the two.
// For *any* pair of types, each of the pair should be assignable to the union of the two.
type_property_test!(
all_type_pairs_are_assignable_to_their_union, db,
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`.
type_property_test!(
only_never_is_subtype_of_any, db,
forall types s. !s.is_equivalent_to(db, Type::Never) => !s.is_subtype_of(db, Type::any())
);
// Only `object` is a supertype of `Any`.
type_property_test!(
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)
);
// Equivalence is commutative.
type_property_test!(
equivalent_to_is_commutative, db,
forall types s, t. s.is_equivalent_to(db, t) == t.is_equivalent_to(db, s)
);
// A fully static type `T` is a subtype of itself. (This is not true for non-fully-static
// types; `Any` is not a subtype of `Any`, only `Never` is.)
type_property_test!(
subtype_of_is_reflexive_for_fully_static_types, db,
forall fully_static_types t. t.is_subtype_of(db, t)
);
// For any two fully static types, each type in the pair must be a subtype of their union.
// (This is clearly not true for non-fully-static types, since their subtyping is not
// reflexive.)
type_property_test!(
all_fully_static_type_pairs_are_subtype_of_their_union, db,
forall fully_static_types s, t. s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t]))
);
}
/// This module contains property tests that currently lead to many false positives.
@ -231,21 +225,21 @@ mod flaky {
forall types t. t.negate(db).negate(db).is_equivalent_to(db, t)
);
// ~T should be disjoint from T
// For any fully static type `T`, `T` should be disjoint from `~T`.
// https://github.com/astral-sh/ty/issues/216
type_property_test!(
negation_is_disjoint, db,
forall types t. t.is_fully_static(db) => t.negate(db).is_disjoint_from(db, t)
negation_of_fully_static_types_is_disjoint, db,
forall fully_static_types t. t.negate(db).is_disjoint_from(db, t)
);
// For two fully static types, their intersection must be a subtype of each type in the pair.
// For two types, their intersection must be a subtype of each type in the pair.
type_property_test!(
all_fully_static_type_pairs_are_supertypes_of_their_intersection, db,
all_type_pairs_are_supertypes_of_their_intersection, db,
forall types s, t.
s.is_fully_static(db) && t.is_fully_static(db)
=> intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t)
intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t)
);
// And for non-fully-static types, the intersection of a pair of types
// And the intersection of a pair of types
// should be assignable to both types of the pair.
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
type_property_test!(
@ -258,8 +252,7 @@ mod flaky {
type_property_test!(
intersection_equivalence_not_order_dependent, db,
forall types s, t, u.
s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db)
=> [s, t, u]
[s, t, u]
.into_iter()
.permutations(3)
.map(|trio_of_types| intersection(db, trio_of_types))
@ -272,8 +265,7 @@ mod flaky {
type_property_test!(
union_equivalence_not_order_dependent, db,
forall types s, t, u.
s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db)
=> [s, t, u]
[s, t, u]
.into_iter()
.permutations(3)
.map(|trio_of_types| union(db, trio_of_types))

View file

@ -207,16 +207,31 @@ impl Ty {
}
}
fn arbitrary_core_type(g: &mut Gen) -> Ty {
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct FullyStaticTy(Ty);
impl FullyStaticTy {
pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> {
self.0.into_type(db)
}
}
fn arbitrary_core_type(g: &mut Gen, fully_static: bool) -> Ty {
// We could select a random integer here, but this would make it much less
// likely to explore interesting edge cases:
let int_lit = Ty::IntLiteral(*g.choose(&[-2, -1, 0, 1, 2]).unwrap());
let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g));
g.choose(&[
Ty::Never,
Ty::Unknown,
Ty::None,
// Update this if new non-fully-static types are added below.
let fully_static_index = 3;
let types = &[
Ty::Any,
Ty::Unknown,
Ty::SubclassOfAny,
// Add fully static types below, dynamic types above.
// Update `fully_static_index` above if adding new dynamic types!
Ty::Never,
Ty::None,
int_lit,
bool_lit,
Ty::StringLiteral(""),
@ -241,7 +256,6 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
Ty::BuiltinInstance("type"),
Ty::AbcInstance("ABC"),
Ty::AbcInstance("ABCMeta"),
Ty::SubclassOfAny,
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("str"),
Ty::SubclassOfBuiltinClass("type"),
@ -261,9 +275,13 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
class: "int",
method: "bit_length",
},
])
.unwrap()
.clone()
];
let types = if fully_static {
&types[fully_static_index..]
} else {
types
};
g.choose(types).unwrap().clone()
}
/// Constructs an arbitrary type.
@ -271,53 +289,54 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
/// The `size` parameter controls the depth of the type tree. For example,
/// a simple type like `int` has a size of 0, `Union[int, str]` has a size
/// of 1, `tuple[int, Union[str, bytes]]` has a size of 2, etc.
fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
///
/// The `fully_static` parameter, if `true`, limits generation to fully static types.
fn arbitrary_type(g: &mut Gen, size: u32, fully_static: bool) -> Ty {
if size == 0 {
arbitrary_core_type(g)
arbitrary_core_type(g, fully_static)
} else {
match u32::arbitrary(g) % 6 {
0 => arbitrary_core_type(g),
0 => arbitrary_core_type(g, fully_static),
1 => Ty::Union(
(0..*g.choose(&[2, 3]).unwrap())
.map(|_| arbitrary_type(g, size - 1))
.map(|_| arbitrary_type(g, size - 1, fully_static))
.collect(),
),
2 => Ty::FixedLengthTuple(
(0..*g.choose(&[0, 1, 2]).unwrap())
.map(|_| arbitrary_type(g, size - 1))
.map(|_| arbitrary_type(g, size - 1, fully_static))
.collect(),
),
3 => Ty::VariableLengthTuple(
(0..*g.choose(&[0, 1, 2]).unwrap())
.map(|_| arbitrary_type(g, size - 1))
.map(|_| arbitrary_type(g, size - 1, fully_static))
.collect(),
Box::new(arbitrary_type(g, size - 1)),
Box::new(arbitrary_type(g, size - 1, fully_static)),
(0..*g.choose(&[0, 1, 2]).unwrap())
.map(|_| arbitrary_type(g, size - 1))
.map(|_| arbitrary_type(g, size - 1, fully_static))
.collect(),
),
4 => Ty::Intersection {
pos: (0..*g.choose(&[0, 1, 2]).unwrap())
.map(|_| arbitrary_type(g, size - 1))
.map(|_| arbitrary_type(g, size - 1, fully_static))
.collect(),
neg: (0..*g.choose(&[0, 1, 2]).unwrap())
.map(|_| arbitrary_type(g, size - 1))
.map(|_| arbitrary_type(g, size - 1, fully_static))
.collect(),
},
5 => Ty::Callable {
params: match u32::arbitrary(g) % 2 {
0 => CallableParams::GradualForm,
1 => CallableParams::List(arbitrary_parameter_list(g, size)),
_ => unreachable!(),
0 if !fully_static => CallableParams::GradualForm,
_ => CallableParams::List(arbitrary_parameter_list(g, size, fully_static)),
},
returns: arbitrary_optional_type(g, size - 1).map(Box::new),
returns: arbitrary_annotation(g, size - 1, fully_static).map(Box::new),
},
_ => unreachable!(),
}
}
}
fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
fn arbitrary_parameter_list(g: &mut Gen, size: u32, fully_static: bool) -> Vec<Param> {
let mut params: Vec<Param> = vec![];
let mut used_names = HashSet::new();
@ -369,11 +388,11 @@ fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
params.push(Param {
kind: next_kind,
name,
annotated_ty: arbitrary_optional_type(g, size),
annotated_ty: arbitrary_annotation(g, size, fully_static),
default_ty: if matches!(next_kind, ParamKind::Variadic | ParamKind::KeywordVariadic) {
None
} else {
arbitrary_optional_type(g, size)
arbitrary_optional_type(g, size, fully_static)
},
});
}
@ -381,10 +400,19 @@ fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
params
}
fn arbitrary_optional_type(g: &mut Gen, size: u32) -> Option<Ty> {
/// An arbitrary optional type, always `Some` if fully static.
fn arbitrary_annotation(g: &mut Gen, size: u32, fully_static: bool) -> Option<Ty> {
if fully_static {
Some(arbitrary_type(g, size, true))
} else {
arbitrary_optional_type(g, size, false)
}
}
fn arbitrary_optional_type(g: &mut Gen, size: u32, fully_static: bool) -> Option<Ty> {
match u32::arbitrary(g) % 2 {
0 => None,
1 => Some(arbitrary_type(g, size)),
1 => Some(arbitrary_type(g, size, fully_static)),
_ => unreachable!(),
}
}
@ -404,7 +432,7 @@ fn arbitrary_optional_name(g: &mut Gen) -> Option<Name> {
impl Arbitrary for Ty {
fn arbitrary(g: &mut Gen) -> Ty {
const MAX_SIZE: u32 = 2;
arbitrary_type(g, MAX_SIZE)
arbitrary_type(g, MAX_SIZE, false)
}
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
@ -491,6 +519,17 @@ impl Arbitrary for Ty {
}
}
impl Arbitrary for FullyStaticTy {
fn arbitrary(g: &mut Gen) -> FullyStaticTy {
const MAX_SIZE: u32 = 2;
FullyStaticTy(arbitrary_type(g, MAX_SIZE, true))
}
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
Box::new(self.0.shrink().map(FullyStaticTy))
}
}
pub(crate) fn intersection<'db>(
db: &'db TestDb,
tys: impl IntoIterator<Item = Type<'db>>,

View file

@ -135,11 +135,6 @@ impl<'db> ProtocolInterface<'db> {
}
}
/// Return `true` if all members of this protocol are fully static.
pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.members(db).all(|member| member.ty.is_fully_static(db))
}
/// Return `true` if if all members on `self` are also members of `other`.
///
/// TODO: this method should consider the types of the members as well as their names.

View file

@ -97,15 +97,6 @@ impl<'db> CallableSignature<'db> {
}
}
/// Check whether this callable type is fully static.
///
/// See [`Type::is_fully_static`] for more details.
pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool {
self.overloads
.iter()
.all(|signature| signature.is_fully_static(db))
}
pub(crate) fn has_relation_to(
&self,
db: &'db dyn Db,
@ -123,9 +114,10 @@ impl<'db> CallableSignature<'db> {
/// See [`Type::is_subtype_of`] for more details.
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool {
Self::has_relation_to_impl(
db,
&self.overloads,
&other.overloads,
&|self_signature, other_signature| self_signature.is_subtype_of(db, other_signature),
TypeRelation::Subtyping,
)
}
@ -134,54 +126,54 @@ 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::has_relation_to_impl(
db,
&self.overloads,
&other.overloads,
&|self_signature, other_signature| self_signature.is_assignable_to(db, other_signature),
TypeRelation::Assignability,
)
}
/// Implementation for the various relation checks between two, possible overloaded, callable
/// Implementation of subtyping and assignability between two, possible overloaded, callable
/// types.
///
/// The `check_signature` closure is used to check the relation between two [`Signature`]s.
fn has_relation_to_impl<F>(
fn has_relation_to_impl(
db: &'db dyn Db,
self_signatures: &[Signature<'db>],
other_signatures: &[Signature<'db>],
check_signature: &F,
) -> bool
where
F: Fn(&Signature<'db>, &Signature<'db>) -> bool,
{
relation: TypeRelation,
) -> bool {
match (self_signatures, other_signatures) {
([self_signature], [other_signature]) => {
// Base case: both callable types contain a single signature.
check_signature(self_signature, other_signature)
self_signature.has_relation_to(db, other_signature, relation)
}
// `self` is possibly overloaded while `other` is definitely not overloaded.
(_, [_]) => self_signatures.iter().any(|self_signature| {
Self::has_relation_to_impl(
db,
std::slice::from_ref(self_signature),
other_signatures,
check_signature,
relation,
)
}),
// `self` is definitely not overloaded while `other` is possibly overloaded.
([_], _) => other_signatures.iter().all(|other_signature| {
Self::has_relation_to_impl(
db,
self_signatures,
std::slice::from_ref(other_signature),
check_signature,
relation,
)
}),
// `self` is definitely overloaded while `other` is possibly overloaded.
(_, _) => other_signatures.iter().all(|other_signature| {
Self::has_relation_to_impl(
db,
self_signatures,
std::slice::from_ref(other_signature),
check_signature,
relation,
)
}),
}
@ -197,14 +189,7 @@ impl<'db> CallableSignature<'db> {
// equivalence check instead of delegating it to the subtype check.
self_signature.is_equivalent_to(db, other_signature)
}
(self_signatures, other_signatures) => {
if !self_signatures
.iter()
.chain(other_signatures.iter())
.all(|signature| signature.is_fully_static(db))
{
return false;
}
(_, _) => {
if self == other {
return true;
}
@ -213,21 +198,6 @@ impl<'db> CallableSignature<'db> {
}
}
/// Check whether this callable type is gradual equivalent to another callable type.
///
/// See [`Type::is_gradual_equivalent_to`] for more details.
pub(crate) fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
match (self.overloads.as_slice(), other.overloads.as_slice()) {
([self_signature], [other_signature]) => {
self_signature.is_gradual_equivalent_to(db, other_signature)
}
_ => {
// TODO: overloads
false
}
}
}
pub(crate) fn replace_self_reference(&self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Self {
Self {
overloads: self
@ -435,61 +405,19 @@ impl<'db> Signature<'db> {
}
}
/// Returns `true` if this is a fully static signature.
///
/// A signature is fully static if all of its parameters and return type are fully static and
/// if it does not use gradual form (`...`) for its parameters.
pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool {
if self.parameters.is_gradual() {
return false;
}
if self.parameters.iter().any(|parameter| {
parameter
.annotated_type()
.is_none_or(|annotated_type| !annotated_type.is_fully_static(db))
}) {
return false;
}
self.return_ty
.is_some_and(|return_type| return_type.is_fully_static(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_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
self.is_equivalent_to_impl(other, |self_type, other_type| {
pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
let check_types = |self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| {
self_type
.unwrap_or(Type::unknown())
.is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
})
}
.is_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
};
/// Return `true` if `self` represents the exact same set of possible runtime objects as `other`.
pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
self.is_equivalent_to_impl(other, |self_type, other_type| {
match (self_type, other_type) {
(Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type),
// We need the catch-all case here because it's not guaranteed that this is a fully
// static type.
_ => false,
}
})
}
/// Implementation for the [`is_equivalent_to`] and [`is_gradual_equivalent_to`] for signature.
///
/// [`is_equivalent_to`]: Self::is_equivalent_to
/// [`is_gradual_equivalent_to`]: Self::is_gradual_equivalent_to
fn is_equivalent_to_impl<F>(&self, other: &Signature<'db>, check_types: F) -> bool
where
F: Fn(Option<Type<'db>>, Option<Type<'db>>) -> bool,
{
// N.B. We don't need to explicitly check for the use of gradual form (`...`) in the
// parameters because it is internally represented by adding `*Any` and `**Any` to the
// parameter list.
if self.parameters.is_gradual() != other.parameters.is_gradual() {
return false;
}
if self.parameters.len() != other.parameters.len() {
return false;
@ -554,38 +482,13 @@ impl<'db> Signature<'db> {
true
}
/// Return `true` if a callable with signature `self` is assignable to a callable with
/// signature `other`.
pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
self.is_assignable_to_impl(other, |type1, type2| {
// In the context of a callable type, the `None` variant represents an `Unknown` type.
type1
.unwrap_or(Type::unknown())
.is_assignable_to(db, type2.unwrap_or(Type::unknown()))
})
}
/// Return `true` if a callable with signature `self` is a subtype of a callable with signature
/// `other`.
///
/// # Panics
///
/// Panics if `self` or `other` is not a fully static signature.
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool {
self.is_assignable_to_impl(other, |type1, type2| {
// SAFETY: Subtype relation is only checked for fully static types.
type1.unwrap().is_subtype_of(db, type2.unwrap())
})
}
/// Implementation for the [`is_assignable_to`] and [`is_subtype_of`] for signature.
///
/// [`is_assignable_to`]: Self::is_assignable_to
/// [`is_subtype_of`]: Self::is_subtype_of
fn is_assignable_to_impl<F>(&self, other: &Signature<'db>, check_types: F) -> bool
where
F: Fn(Option<Type<'db>>, Option<Type<'db>>) -> bool,
{
/// Implementation of subtyping and assignability for signature.
fn has_relation_to(
&self,
db: &'db dyn Db,
other: &Signature<'db>,
relation: TypeRelation,
) -> bool {
/// A helper struct to zip two slices of parameters together that provides control over the
/// two iterators individually. It also keeps track of the current parameter in each
/// iterator.
@ -647,17 +550,40 @@ impl<'db> Signature<'db> {
}
}
let check_types = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| {
type1.unwrap_or(Type::unknown()).has_relation_to(
db,
type2.unwrap_or(Type::unknown()),
relation,
)
};
// Return types are covariant.
if !check_types(self.return_ty, other.return_ty) {
return false;
}
if self.parameters.is_gradual() || other.parameters.is_gradual() {
// If either of the parameter lists contains a gradual form (`...`), then it is
// assignable / subtype to and from any other callable type.
// A gradual parameter list is a supertype of the "bottom" parameter list (*args: object,
// **kwargs: object).
if other.parameters.is_gradual()
&& self
.parameters
.variadic()
.is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db)))
&& self
.parameters
.keyword_variadic()
.is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db)))
{
return true;
}
// If either of the parameter lists is gradual (`...`), then it is assignable to and from
// any other parameter list, but not a subtype or supertype of any other parameter list.
if self.parameters.is_gradual() || other.parameters.is_gradual() {
return relation.is_assignability();
}
let mut parameters = ParametersZip {
current_self: None,
current_other: None,
@ -979,23 +905,36 @@ pub(crate) struct Parameters<'db> {
/// Whether this parameter list represents a gradual form using `...` as the only parameter.
///
/// If this is `true`, the `value` will still contain the variadic and keyword-variadic
/// parameters. This flag is used to distinguish between an explicit `...` in the callable type
/// as in `Callable[..., int]` and the variadic arguments in `lambda` expression as in
/// `lambda *args, **kwargs: None`.
/// parameters.
///
/// Per [the typing specification], any signature with a variadic and a keyword-variadic
/// argument, both annotated (explicitly or implicitly) as `Any` or `Unknown`, is considered
/// equivalent to `...`.
///
/// The display implementation utilizes this flag to use `...` instead of displaying the
/// individual variadic and keyword-variadic parameters.
///
/// Note: This flag is also used to indicate invalid forms of `Callable` annotations.
/// Note: This flag can also result from invalid forms of `Callable` annotations.
///
/// TODO: the spec also allows signatures like `Concatenate[int, ...]`, which have some number
/// of required positional parameters followed by a gradual form. Our representation will need
/// some adjustments to represent that.
///
/// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable
is_gradual: bool,
}
impl<'db> Parameters<'db> {
pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self {
Self {
value: parameters.into_iter().collect(),
is_gradual: false,
}
let value: Vec<Parameter<'db>> = parameters.into_iter().collect();
let is_gradual = value.len() == 2
&& value
.iter()
.any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()))
&& value.iter().any(|p| {
p.is_keyword_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic())
});
Self { value, is_gradual }
}
/// Create an empty parameter list.

View file

@ -73,10 +73,6 @@ impl<'db> SubclassOfType<'db> {
subclass_of.is_dynamic()
}
pub(crate) const fn is_fully_static(self) -> bool {
!self.is_dynamic()
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> {
match self.subclass_of {
SubclassOfInner::Dynamic(_) => match variance {
@ -146,9 +142,13 @@ impl<'db> SubclassOfType<'db> {
relation: TypeRelation,
) -> bool {
match (self.subclass_of, other.subclass_of) {
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => {
relation.applies_to_non_fully_static_types()
(SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => {
relation.is_assignability()
}
(SubclassOfInner::Dynamic(_), SubclassOfInner::Class(other_class)) => {
other_class.is_object(db) || relation.is_assignability()
}
(SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => relation.is_assignability(),
// For example, `type[bool]` describes all possible runtime subclasses of the class `bool`,
// and `type[int]` describes all possible runtime subclasses of the class `int`.

View file

@ -137,18 +137,10 @@ impl<'db> TupleType<'db> {
self.tuple(db).is_equivalent_to(db, other.tuple(db))
}
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.tuple(db).is_gradual_equivalent_to(db, other.tuple(db))
}
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
self.tuple(db).is_disjoint_from(db, other.tuple(db))
}
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.tuple(db).is_fully_static(db)
}
pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool {
self.tuple(db).is_single_valued(db)
}
@ -292,17 +284,6 @@ impl<'db> FixedLengthTupleSpec<'db> {
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.0.len() == other.0.len()
&& (self.0.iter())
.zip(&other.0)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
self.0.iter().all(|ty| ty.is_fully_static(db))
}
fn is_single_valued(&self, db: &'db dyn Db) -> bool {
self.0.iter().all(|ty| ty.is_single_valued(db))
}
@ -667,32 +648,6 @@ impl<'db> VariableLengthTupleSpec<'db> {
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false,
})
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.variable.is_gradual_equivalent_to(db, other.variable)
&& (self.prenormalized_prefix_elements(db, None))
.zip_longest(other.prenormalized_prefix_elements(db, None))
.all(|pair| match pair {
EitherOrBoth::Both(self_ty, other_ty) => {
self_ty.is_gradual_equivalent_to(db, other_ty)
}
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false,
})
&& (self.prenormalized_suffix_elements(db, None))
.zip_longest(other.prenormalized_suffix_elements(db, None))
.all(|pair| match pair {
EitherOrBoth::Both(self_ty, other_ty) => {
self_ty.is_gradual_equivalent_to(db, other_ty)
}
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false,
})
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
self.variable.is_fully_static(db)
&& self.prefix_elements().all(|ty| ty.is_fully_static(db))
&& self.suffix_elements().all(|ty| ty.is_fully_static(db))
}
}
impl<'db> PyIndex<'db> for &VariableLengthTupleSpec<'db> {
@ -873,19 +828,6 @@ impl<'db> TupleSpec<'db> {
}
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> bool {
match (self, other) {
(TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => {
self_tuple.is_gradual_equivalent_to(db, other_tuple)
}
(TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => {
self_tuple.is_gradual_equivalent_to(db, other_tuple)
}
(TupleSpec::Fixed(_), TupleSpec::Variable(_))
| (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false,
}
}
fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool {
// Two tuples with an incompatible number of required elements must always be disjoint.
let (self_min, self_max) = self.size_hint();
@ -950,13 +892,6 @@ impl<'db> TupleSpec<'db> {
false
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
match self {
TupleSpec::Fixed(tuple) => tuple.is_fully_static(db),
TupleSpec::Variable(tuple) => tuple.is_fully_static(db),
}
}
fn is_single_valued(&self, db: &'db dyn Db) -> bool {
match self {
TupleSpec::Fixed(tuple) => tuple.is_single_valued(db),