Skip to content
Merged
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 @@ -327,6 +327,17 @@ def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
static_assert(is_subtype_of(U, U | None))
```

A bound or constrained typevar in a union with a dynamic type is assignable to the typevar:

```py
def union_with_dynamic[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
static_assert(is_assignable_to(T | Any, T))
static_assert(is_assignable_to(U | Any, U))

static_assert(not is_subtype_of(T | Any, T))
static_assert(not is_subtype_of(U | Any, U))
```

And an intersection of a typevar with another type is always a subtype of the TypeVar:

```py
Expand Down
669 changes: 410 additions & 259 deletions crates/ty_python_semantic/src/types.rs

Large diffs are not rendered by default.

92 changes: 67 additions & 25 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::semantic_index::{
BindingWithConstraints, DeclarationWithConstraint, SemanticIndex, attribute_declarations,
attribute_scopes,
};
use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
use crate::types::enums::enum_metadata;
Expand All @@ -28,10 +29,10 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{
ApplyTypeMappingVisitor, BareTypeAliasType, Binding, BoundSuperError, BoundSuperType,
CallableType, DataclassParams, DeprecatedInstance, HasRelationToVisitor, KnownInstanceType,
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, VarianceInferable,
declaration_type, infer_definition_types, todo_type,
CallableType, DataclassParams, DeprecatedInstance, HasRelationToVisitor, IsEquivalentVisitor,
KnownInstanceType, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType,
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
VarianceInferable, declaration_type, infer_definition_types, todo_type,
};
use crate::{
Db, FxIndexMap, FxOrderSet, Program,
Expand All @@ -49,7 +50,7 @@ use crate::{
},
types::{
CallArguments, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType,
cyclic::PairVisitor, definition_expression_type,
definition_expression_type,
},
};
use indexmap::IndexSet;
Expand Down Expand Up @@ -536,64 +537,88 @@ impl<'db> ClassType<'db> {

/// Return `true` if `other` is present in this class's MRO.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
self.has_relation_to_impl(db, other, TypeRelation::Subtyping, &PairVisitor::new(true))
self.when_subclass_of(db, other)
}

pub(super) fn has_relation_to_impl(
pub(super) fn when_subclass_of<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: ClassType<'db>,
) -> C {
self.has_relation_to_impl(
db,
other,
TypeRelation::Subtyping,
&HasRelationToVisitor::new(C::always_satisfiable(db)),
)
}

pub(super) fn has_relation_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
visitor: &HasRelationToVisitor<'db>,
) -> bool {
self.iter_mro(db).any(|base| {
visitor: &HasRelationToVisitor<'db, C>,
) -> C {
self.iter_mro(db).when_any(db, |base| {
match base {
ClassBase::Dynamic(_) => match relation {
TypeRelation::Subtyping => other.is_object(db),
TypeRelation::Assignability => !other.is_final(db),
TypeRelation::Subtyping => C::from_bool(db, other.is_object(db)),
TypeRelation::Assignability => C::from_bool(db, !other.is_final(db)),
},

// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol | ClassBase::Generic => false,
ClassBase::Protocol | ClassBase::Generic => C::unsatisfiable(db),

ClassBase::Class(base) => match (base, other) {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => {
C::from_bool(db, base == other)
}
(ClassType::Generic(base), ClassType::Generic(other)) => {
base.origin(db) == other.origin(db)
&& base.specialization(db).has_relation_to_impl(
C::from_bool(db, base.origin(db) == other.origin(db)).and(db, || {
base.specialization(db).has_relation_to_impl(
db,
other.specialization(db),
relation,
visitor,
)
})
}
(ClassType::Generic(_), ClassType::NonGeneric(_))
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => C::unsatisfiable(db),
},

ClassBase::TypedDict => {
// TODO: Implement subclassing and assignability for TypedDicts.
true
C::always_satisfiable(db)
}
}
})
}

pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
pub(super) fn is_equivalent_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: ClassType<'db>,
visitor: &IsEquivalentVisitor<'db, C>,
) -> C {
if self == other {
return true;
return C::always_satisfiable(db);
}

match (self, other) {
// A non-generic class is never equivalent to a generic class.
// Two non-generic classes are only equivalent if they are equal (handled above).
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false,
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => C::unsatisfiable(db),

(ClassType::Generic(this), ClassType::Generic(other)) => {
this.origin(db) == other.origin(db)
&& this
.specialization(db)
.is_equivalent_to(db, other.specialization(db))
C::from_bool(db, this.origin(db) == other.origin(db)).and(db, || {
this.specialization(db).is_equivalent_to_impl(
db,
other.specialization(db),
visitor,
)
})
}
}
}
Expand Down Expand Up @@ -1613,6 +1638,15 @@ impl<'db> ClassLiteral<'db> {
.contains(&ClassBase::Class(other))
}

pub(super) fn when_subclass_of<C: Constraints<'db>>(
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
other: ClassType<'db>,
) -> C {
C::from_bool(db, self.is_subclass_of(db, specialization, other))
}

/// Return `true` if this class constitutes a typed dict specification (inherits from
/// `typing.TypedDict`, either directly or indirectly).
#[salsa::tracked(
Expand Down Expand Up @@ -4186,6 +4220,14 @@ impl KnownClass {
.is_ok_and(|class| class.is_subclass_of(db, None, other))
}

pub(super) fn when_subclass_of<'db, C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: ClassType<'db>,
) -> C {
C::from_bool(db, self.is_subclass_of(db, other))
}

/// Return the module in which we should look up the definition for this class
fn canonical_module(self, db: &dyn Db) -> KnownModule {
match self {
Expand Down
5 changes: 2 additions & 3 deletions crates/ty_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use crate::types::generics::Specialization;
use crate::types::tuple::TupleType;
use crate::types::{
ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType,
MroError, MroIterator, NormalizedVisitor, SpecialFormType, Type, TypeMapping, TypeTransformer,
todo_type,
MroError, MroIterator, NormalizedVisitor, SpecialFormType, Type, TypeMapping, todo_type,
};

/// Enumeration of the possible kinds of types we allow in class bases.
Expand Down Expand Up @@ -292,7 +291,7 @@ impl<'db> ClassBase<'db> {
self.apply_type_mapping_impl(
db,
&TypeMapping::Specialization(specialization),
&TypeTransformer::default(),
&ApplyTypeMappingVisitor::default(),
)
} else {
self
Expand Down
184 changes: 184 additions & 0 deletions crates/ty_python_semantic/src/types/constraints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//! Constraints under which type properties hold
//!
//! For "concrete" types (which contain no type variables), type properties like assignability have
//! simple answers: one type is either assignable to another type, or it isn't. (The _rules_ for
//! comparing two particular concrete types can be rather complex, but the _answer_ is a simple
//! "yes" or "no".)
//!
//! These properties are more complex when type variables are involved, because there are (usually)
//! many different concrete types that a typevar can be specialized to, and the type property might
//! hold for some specializations, but not for others. That means that for types that include
//! typevars, "Is this type assignable to another?" no longer makes sense as a question. The better
//! question is: "Under what constraints is this type assignable to another?".
//!
//! This module provides the machinery for representing the "under what constraints" part of that
//! question. An individual constraint restricts the specialization of a single typevar to be within a
//! particular lower and upper bound. You can then build up more complex constraint sets using
//! union, intersection, and negation operations (just like types themselves).
//!
//! NOTE: This module is currently in a transitional state: we've added a trait that our constraint
//! set implementations will conform to, and updated all of our type property implementations to
//! work on any impl of that trait. But the only impl we have right now is `bool`, which means that
//! we are still not tracking the full detail as promised in the description above. (`bool` is a
//! perfectly fine impl, but it can generate false positives when you have to break down a
//! particular assignability check into subchecks: each subcheck might say "yes", but technically
//! under conflicting constraints, which a single `bool` can't track.) Soon we will add a proper
//! constraint set implementation, and the `bool` impl of the trait (and possibly the trait itself)
//! will go away.

use crate::Db;

/// Encodes the constraints under which a type property (e.g. assignability) holds.
pub(crate) trait Constraints<'db>: Clone + Sized {
/// Returns a constraint set that never holds
fn unsatisfiable(db: &'db dyn Db) -> Self;

/// Returns a constraint set that always holds
fn always_satisfiable(db: &'db dyn Db) -> Self;

/// Returns whether this constraint set never holds
fn is_never_satisfied(&self, db: &'db dyn Db) -> bool;

/// Returns whether this constraint set always holds
fn is_always_satisfied(&self, db: &'db dyn Db) -> bool;

/// Updates this constraint set to hold the union of itself and another constraint set.
fn union(&mut self, db: &'db dyn Db, other: Self) -> &Self;

/// Updates this constraint set to hold the intersection of itself and another constraint set.
fn intersect(&mut self, db: &'db dyn Db, other: Self) -> &Self;

/// Returns the negation of this constraint set.
fn negate(self, db: &'db dyn Db) -> Self;

/// Returns a constraint set representing a boolean condition.
fn from_bool(db: &'db dyn Db, b: bool) -> Self {
if b {
Self::always_satisfiable(db)
} else {
Self::unsatisfiable(db)
}
}

/// Returns the intersection of this constraint set and another. The other constraint set is
/// provided as a thunk, to implement short-circuiting: the thunk is not forced if the
/// constraint set is already saturated.
fn and(mut self, db: &'db dyn Db, other: impl FnOnce() -> Self) -> Self {
if !self.is_never_satisfied(db) {
self.intersect(db, other());
}
self
}

/// Returns the union of this constraint set and another. The other constraint set is provided
/// as a thunk, to implement short-circuiting: the thunk is not forced if the constraint set is
/// already saturated.
fn or(mut self, db: &'db dyn Db, other: impl FnOnce() -> Self) -> Self {
if !self.is_always_satisfied(db) {
self.union(db, other());
}
self
}
}

impl<'db> Constraints<'db> for bool {
fn unsatisfiable(_db: &'db dyn Db) -> Self {
false
}

fn always_satisfiable(_db: &'db dyn Db) -> Self {
true
}

fn is_never_satisfied(&self, _db: &'db dyn Db) -> bool {
!*self
}

fn is_always_satisfied(&self, _db: &'db dyn Db) -> bool {
*self
}

fn union(&mut self, _db: &'db dyn Db, other: Self) -> &Self {
*self = *self || other;
self
}

fn intersect(&mut self, _db: &'db dyn Db, other: Self) -> &Self {
*self = *self && other;
self
}

fn negate(self, _db: &'db dyn Db) -> Self {
!self
}
}

/// An extension trait for building constraint sets from [`Option`] values.
pub(crate) trait OptionConstraintsExtension<T> {
/// Returns [`always_satisfiable`][Constraints::always_satisfiable] if the option is `None`;
/// otherwise applies a function to determine under what constraints the value inside of it
/// holds.
fn when_none_or<'db, C: Constraints<'db>>(self, db: &'db dyn Db, f: impl FnOnce(T) -> C) -> C;

/// Returns [`unsatisfiable`][Constraints::unsatisfiable] if the option is `None`; otherwise
/// applies a function to determine under what constraints the value inside of it holds.
fn when_some_and<'db, C: Constraints<'db>>(self, db: &'db dyn Db, f: impl FnOnce(T) -> C) -> C;
}

impl<T> OptionConstraintsExtension<T> for Option<T> {
fn when_none_or<'db, C: Constraints<'db>>(self, db: &'db dyn Db, f: impl FnOnce(T) -> C) -> C {
match self {
Some(value) => f(value),
None => C::always_satisfiable(db),
}
}

fn when_some_and<'db, C: Constraints<'db>>(self, db: &'db dyn Db, f: impl FnOnce(T) -> C) -> C {
match self {
Some(value) => f(value),
None => C::unsatisfiable(db),
}
}
}

/// An extension trait for building constraint sets from an [`Iterator`].
pub(crate) trait IteratorConstraintsExtension<T> {
/// Returns the constraints under which any element of the iterator holds.
///
/// This method short-circuits; if we encounter any element that
/// [`is_always_satisfied`][Constraints::is_always_satisfied] true, then the overall result
/// must be as well, and we stop consuming elements from the iterator.
fn when_any<'db, C: Constraints<'db>>(self, db: &'db dyn Db, f: impl FnMut(T) -> C) -> C;

/// Returns the constraints under which every element of the iterator holds.
///
/// This method short-circuits; if we encounter any element that
/// [`is_never_satisfied`][Constraints::is_never_satisfied] true, then the overall result must
/// be as well, and we stop consuming elements from the iterator.
fn when_all<'db, C: Constraints<'db>>(self, db: &'db dyn Db, f: impl FnMut(T) -> C) -> C;
}

impl<I, T> IteratorConstraintsExtension<T> for I
where
I: Iterator<Item = T>,
{
fn when_any<'db, C: Constraints<'db>>(self, db: &'db dyn Db, mut f: impl FnMut(T) -> C) -> C {
let mut result = C::unsatisfiable(db);
for child in self {
if result.union(db, f(child)).is_always_satisfied(db) {
return result;
}
}
result
}

fn when_all<'db, C: Constraints<'db>>(self, db: &'db dyn Db, mut f: impl FnMut(T) -> C) -> C {
let mut result = C::always_satisfiable(db);
for child in self {
if result.intersect(db, f(child)).is_never_satisfied(db) {
return result;
}
}
result
}
}
Loading
Loading