Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ python-version = "3.12"
```

```py
from typing import final
from typing import Any, final
from ty_extensions import static_assert, is_disjoint_from

@final
Expand All @@ -106,9 +106,12 @@ class Foo[T]:
class A: ...
class B: ...

static_assert(not is_disjoint_from(A, B))
static_assert(not is_disjoint_from(Foo[A], Foo[B]))
static_assert(not is_disjoint_from(Foo[A], Foo[Any]))
static_assert(not is_disjoint_from(Foo[Any], Foo[B]))

# TODO: `int` and `str` are disjoint bases, so these should be disjoint.
# `Foo[Never]` is a subtype of both `Foo[int]` and `Foo[str]`.
static_assert(not is_disjoint_from(Foo[int], Foo[str]))
```

Expand Down
36 changes: 28 additions & 8 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,31 @@ impl<'db> ClassType<'db> {
.find_map(|base| base.as_disjoint_base(db))
}

/// Return `true` if this class could exist in the MRO of `other`.
pub(super) fn could_exist_in_mro_of(self, db: &'db dyn Db, other: Self) -> bool {
other
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| match (self, class) {
(ClassType::NonGeneric(this_class), ClassType::NonGeneric(other_class)) => {
this_class == other_class
}
(ClassType::Generic(this_alias), ClassType::Generic(other_alias)) => {
this_alias.origin(db) == other_alias.origin(db)
&& !this_alias
.specialization(db)
.is_disjoint_from(
db,
other_alias.specialization(db),
InferableTypeVars::None,
)
.is_always_satisfied(db)
}
(ClassType::NonGeneric(_), ClassType::Generic(_))
| (ClassType::Generic(_), ClassType::NonGeneric(_)) => false,
})
}

/// Return `true` if this class could coexist in an MRO with `other`.
///
/// For two given classes `A` and `B`, it is often possible to say for sure
Expand All @@ -717,16 +742,11 @@ impl<'db> ClassType<'db> {
}

if self.is_final(db) {
return self
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| class.class_literal(db).0 == other.class_literal(db).0);
return other.could_exist_in_mro_of(db, self);
}

if other.is_final(db) {
return other
.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|class| class.class_literal(db).0 == self.class_literal(db).0);
return self.could_exist_in_mro_of(db, other);
}

// Two disjoint bases can only coexist in an MRO if one is a subclass of the other.
Expand Down
93 changes: 82 additions & 11 deletions crates/ty_python_semantic/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind, ScopeId};
use crate::semantic_index::{SemanticIndex, semantic_index};
use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::constraints::ConstraintSet;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::instance::{Protocol, ProtocolInstanceType};
use crate::types::signatures::Parameters;
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
Expand Down Expand Up @@ -1166,18 +1166,20 @@ impl<'db> Specialization<'db> {
let self_materialization_kind = self.materialization_kind(db);
let other_materialization_kind = other.materialization_kind(db);

let mut result = ConstraintSet::from(true);
for ((bound_typevar, self_type), other_type) in (generic_context.variables(db))
.zip(self.types(db))
.zip(other.types(db))
{
let types = itertools::izip!(
generic_context.variables(db),
self.types(db),
other.types(db)
);

types.when_all(db, |(bound_typevar, self_type, other_type)| {
// Subtyping/assignability of each type in the specialization depends on the variance
// of the corresponding typevar:
// - covariant: verify that self_type <: other_type
// - contravariant: verify that other_type <: self_type
// - invariant: verify that self_type <: other_type AND other_type <: self_type
// - bivariant: skip, can't make subtyping/assignability false
let compatible = match bound_typevar.variance(db) {
match bound_typevar.variance(db) {
TypeVarVariance::Invariant => has_relation_in_invariant_position(
db,
self_type,
Expand Down Expand Up @@ -1206,13 +1208,82 @@ impl<'db> Specialization<'db> {
disjointness_visitor,
),
TypeVarVariance::Bivariant => ConstraintSet::from(true),
};
if result.intersect(db, compatible).is_never_satisfied(db) {
return result;
}
})
}

pub(crate) fn is_disjoint_from(
self,
db: &'db dyn Db,
other: Self,
inferable: InferableTypeVars<'_, 'db>,
) -> ConstraintSet<'db> {
self.is_disjoint_from_impl(
db,
other,
inferable,
&IsDisjointVisitor::default(),
&HasRelationToVisitor::default(),
)
}

pub(crate) fn is_disjoint_from_impl(
self,
db: &'db dyn Db,
other: Self,
inferable: InferableTypeVars<'_, 'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
relation_visitor: &HasRelationToVisitor<'db>,
) -> ConstraintSet<'db> {
let generic_context = self.generic_context(db);
if generic_context != other.generic_context(db) {
return ConstraintSet::from(true);
}

result
if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db))
{
return self_tuple.is_disjoint_from_impl(
db,
other_tuple,
inferable,
disjointness_visitor,
relation_visitor,
);
}

let types = itertools::izip!(
generic_context.variables(db),
self.types(db),
other.types(db)
);

types.when_all(
db,
|(bound_typevar, self_type, other_type)| match bound_typevar.variance(db) {
// TODO: This check can lead to false negatives.
//
// For example, `Foo[int]` and `Foo[bool]` are disjoint, even though `bool` is a subtype
// of `int`. However, given two non-inferable type variables `T` and `U`, `Foo[T]` and
// `Foo[U]` should not be considered disjoint, as `T` and `U` could be specialized to the
// same type. We don't currently have a good typing relationship to represent this.
TypeVarVariance::Invariant => self_type.is_disjoint_from_impl(
db,
*other_type,
inferable,
disjointness_visitor,
relation_visitor,
),

// If `Foo[T]` is covariant in `T`, `Foo[Never]` is a subtype of `Foo[A]` and `Foo[B]`
TypeVarVariance::Covariant => ConstraintSet::from(false),

// If `Foo[T]` is contravariant in `T`, `Foo[A | B]` is a subtype of `Foo[A]` and `Foo[B]`
TypeVarVariance::Contravariant => ConstraintSet::from(false),

// If `Foo[T]` is bivariant in `T`, `Foo[A]` and `Foo[B]` are mutual subtypes.
TypeVarVariance::Bivariant => ConstraintSet::from(false),
},
)
}

pub(crate) fn is_equivalent_to_impl(
Expand Down
17 changes: 17 additions & 0 deletions crates/ty_python_semantic/src/types/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,23 @@ impl<'db> TupleType<'db> {
)
}

pub(crate) fn is_disjoint_from_impl(
self,
db: &'db dyn Db,
other: Self,
inferable: InferableTypeVars<'_, 'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
relation_visitor: &HasRelationToVisitor<'db>,
) -> ConstraintSet<'db> {
self.tuple(db).is_disjoint_from_impl(
db,
other.tuple(db),
inferable,
disjointness_visitor,
relation_visitor,
)
}

pub(crate) fn is_equivalent_to_impl(
self,
db: &'db dyn Db,
Expand Down
Loading