[ty] Implement constraint implication for compound types (#21366)
Some checks are pending
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This PR updates the constraint implication type relationship to work on
compound types as well. (A compound type is a non-atomic type, like
`list[T]`.)

The goal of constraint implication is to check whether the requirements
of a constraint imply that a particular subtyping relationship holds.
Before, we were only checking atomic typevars. That would let us verify
that the constraint set `T ≤ bool` implies that `T` is always a subtype
of `int`. (In this case, the lhs of the subtyping check, `T`, is an
atomic typevar.)

But we weren't recursing into compound types, to look for nested
occurrences of typevars. That means that we weren't able to see that `T
≤ bool` implies that `Covariant[T]` is always a subtype of
`Covariant[int]`.

Doing this recursion means that we have to carry the constraint set
along with us as we recurse into types as part of `has_relation_to`, by
adding constraint implication as a new `TypeRelation` variant. (Before
it was just a method on `ConstraintSet`.)

---------

Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
This commit is contained in:
Douglas Creager 2025-11-14 18:43:00 -05:00 committed by GitHub
parent d63b4b0383
commit 698231a47a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 321 additions and 111 deletions

View file

@ -12,8 +12,7 @@ a particular constraint set hold_.
## Concrete types ## Concrete types
For concrete types, constraint implication is exactly the same as subtyping. (A concrete type is any For concrete types, constraint implication is exactly the same as subtyping. (A concrete type is any
fully static type that is not a typevar. It can _contain_ a typevar, though — `list[T]` is fully static type that does not contain a typevar.)
considered concrete.)
```py ```py
from ty_extensions import ConstraintSet, is_subtype_of, static_assert from ty_extensions import ConstraintSet, is_subtype_of, static_assert
@ -191,4 +190,163 @@ def mutually_constrained[T, U]():
static_assert(not given_int.implies_subtype_of(T, str)) static_assert(not given_int.implies_subtype_of(T, str))
``` ```
## Compound types
All of the relationships in the above section also apply when a typevar appears in a compound type.
```py
from typing import Never
from ty_extensions import ConstraintSet, static_assert
class Covariant[T]:
def get(self) -> T:
raise ValueError
def given_constraints[T]():
static_assert(not ConstraintSet.always().implies_subtype_of(Covariant[T], Covariant[int]))
static_assert(not ConstraintSet.always().implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(not ConstraintSet.always().implies_subtype_of(Covariant[T], Covariant[str]))
# These are vacuously true; false implies anything
static_assert(ConstraintSet.never().implies_subtype_of(Covariant[T], Covariant[int]))
static_assert(ConstraintSet.never().implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(ConstraintSet.never().implies_subtype_of(Covariant[T], Covariant[str]))
# For a covariant typevar, (T ≤ int) implies that (Covariant[T] ≤ Covariant[int]).
given_int = ConstraintSet.range(Never, T, int)
static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str]))
given_bool = ConstraintSet.range(Never, T, bool)
static_assert(given_bool.implies_subtype_of(Covariant[T], Covariant[int]))
static_assert(given_bool.implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(not given_bool.implies_subtype_of(Covariant[T], Covariant[str]))
given_bool_int = ConstraintSet.range(bool, T, int)
static_assert(not given_bool_int.implies_subtype_of(Covariant[int], Covariant[T]))
static_assert(given_bool_int.implies_subtype_of(Covariant[bool], Covariant[T]))
static_assert(not given_bool_int.implies_subtype_of(Covariant[str], Covariant[T]))
def mutually_constrained[T, U]():
# If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Covariant[T] ≤ Covariant[int]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str]))
# If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Covariant[T] ≤ Covariant[int]).
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str]))
```
Many of the relationships are reversed for typevars that appear in contravariant types.
```py
class Contravariant[T]:
def set(self, value: T):
pass
def given_constraints[T]():
static_assert(not ConstraintSet.always().implies_subtype_of(Contravariant[int], Contravariant[T]))
static_assert(not ConstraintSet.always().implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(not ConstraintSet.always().implies_subtype_of(Contravariant[str], Contravariant[T]))
# These are vacuously true; false implies anything
static_assert(ConstraintSet.never().implies_subtype_of(Contravariant[int], Contravariant[T]))
static_assert(ConstraintSet.never().implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(ConstraintSet.never().implies_subtype_of(Contravariant[str], Contravariant[T]))
# For a contravariant typevar, (T ≤ int) implies that (Contravariant[int] ≤ Contravariant[T]).
# (The order of the comparison is reversed because of contravariance.)
given_int = ConstraintSet.range(Never, T, int)
static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T]))
given_bool = ConstraintSet.range(Never, T, int)
static_assert(given_bool.implies_subtype_of(Contravariant[int], Contravariant[T]))
static_assert(not given_bool.implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(not given_bool.implies_subtype_of(Contravariant[str], Contravariant[T]))
def mutually_constrained[T, U]():
# If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Contravariant[int] ≤ Contravariant[T]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T]))
# If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Contravariant[int] ≤ Contravariant[T]).
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T]))
```
For invariant typevars, subtyping of the typevar does not imply subtyping of the compound type in
either direction. But an equality constraint on the typevar does.
```py
class Invariant[T]:
def get(self) -> T:
raise ValueError
def set(self, value: T):
pass
def given_constraints[T]():
static_assert(not ConstraintSet.always().implies_subtype_of(Invariant[T], Invariant[int]))
static_assert(not ConstraintSet.always().implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(not ConstraintSet.always().implies_subtype_of(Invariant[T], Invariant[str]))
# These are vacuously true; false implies anything
static_assert(ConstraintSet.never().implies_subtype_of(Invariant[T], Invariant[int]))
static_assert(ConstraintSet.never().implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(ConstraintSet.never().implies_subtype_of(Invariant[T], Invariant[str]))
# For an invariant typevar, (T ≤ int) does not imply that (Invariant[T] ≤ Invariant[int]).
given_int = ConstraintSet.range(Never, T, int)
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str]))
# It also does not imply the contravariant ordering (Invariant[int] ≤ Invariant[T]).
static_assert(not given_int.implies_subtype_of(Invariant[int], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[bool], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[str], Invariant[T]))
# But (T = int) does imply both.
given_int = ConstraintSet.range(int, T, int)
static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int]))
static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[bool], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(not given_int.implies_subtype_of(Invariant[str], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str]))
def mutually_constrained[T, U]():
# If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well. But because T is invariant, that
# does _not_ imply that (Invariant[T] ≤ Invariant[int]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str]))
# If (T = U ∧ U = int), then (T = int) must be true as well. That is an equality constraint, so
# even though T is invariant, it does imply that (Invariant[T] ≤ Invariant[int]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(int, U, int)
static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int]))
static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(not given_int.implies_subtype_of(Invariant[bool], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str]))
static_assert(not given_int.implies_subtype_of(Invariant[str], Invariant[T]))
```
[subtyping]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence [subtyping]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence

View file

@ -200,7 +200,7 @@ pub(crate) type ApplyTypeMappingVisitor<'db> = TypeTransformer<'db, TypeMapping<
/// A [`PairVisitor`] that is used in `has_relation_to` methods. /// A [`PairVisitor`] that is used in `has_relation_to` methods.
pub(crate) type HasRelationToVisitor<'db> = pub(crate) type HasRelationToVisitor<'db> =
CycleDetector<TypeRelation, (Type<'db>, Type<'db>, TypeRelation), ConstraintSet<'db>>; CycleDetector<TypeRelation<'db>, (Type<'db>, Type<'db>, TypeRelation<'db>), ConstraintSet<'db>>;
impl Default for HasRelationToVisitor<'_> { impl Default for HasRelationToVisitor<'_> {
fn default() -> Self { fn default() -> Self {
@ -1623,6 +1623,25 @@ impl<'db> Type<'db> {
self.has_relation_to(db, target, inferable, TypeRelation::Subtyping) self.has_relation_to(db, target, inferable, TypeRelation::Subtyping)
} }
/// Return the constraints under which this type is a subtype of type `target`, assuming that
/// all of the restrictions in `constraints` hold.
///
/// See [`TypeRelation::SubtypingAssuming`] for more details.
fn when_subtype_of_assuming(
self,
db: &'db dyn Db,
target: Type<'db>,
assuming: ConstraintSet<'db>,
inferable: InferableTypeVars<'_, 'db>,
) -> ConstraintSet<'db> {
self.has_relation_to(
db,
target,
inferable,
TypeRelation::SubtypingAssuming(assuming),
)
}
/// Return true if this type is assignable to type `target`. /// Return true if this type is assignable to type `target`.
/// ///
/// See [`TypeRelation::Assignability`] for more details. /// See [`TypeRelation::Assignability`] for more details.
@ -1654,7 +1673,7 @@ impl<'db> Type<'db> {
db: &'db dyn Db, db: &'db dyn Db,
target: Type<'db>, target: Type<'db>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
self.has_relation_to_impl( self.has_relation_to_impl(
db, db,
@ -1671,7 +1690,7 @@ impl<'db> Type<'db> {
db: &'db dyn Db, db: &'db dyn Db,
target: Type<'db>, target: Type<'db>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -1684,6 +1703,14 @@ impl<'db> Type<'db> {
return ConstraintSet::from(true); return ConstraintSet::from(true);
} }
// Handle constraint implication first. If either `self` or `target` is a typevar, check
// the constraint set to see if the corresponding constraint is satisfied.
if let TypeRelation::SubtypingAssuming(constraints) = relation
&& (self.is_type_var() || target.is_type_var())
{
return constraints.implies_subtype_of(db, self, target);
}
match (self, target) { match (self, target) {
// Everything is a subtype of `object`. // Everything is a subtype of `object`.
(_, Type::NominalInstance(instance)) if instance.is_object() => { (_, Type::NominalInstance(instance)) if instance.is_object() => {
@ -1763,7 +1790,7 @@ impl<'db> Type<'db> {
"DynamicType::Divergent should have been handled in an earlier branch" "DynamicType::Divergent should have been handled in an earlier branch"
); );
ConstraintSet::from(match relation { ConstraintSet::from(match relation {
TypeRelation::Subtyping => false, TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false,
TypeRelation::Assignability => true, TypeRelation::Assignability => true,
TypeRelation::Redundancy => match target { TypeRelation::Redundancy => match target {
Type::Dynamic(_) => true, Type::Dynamic(_) => true,
@ -1773,7 +1800,7 @@ impl<'db> Type<'db> {
}) })
} }
(_, Type::Dynamic(_)) => ConstraintSet::from(match relation { (_, Type::Dynamic(_)) => ConstraintSet::from(match relation {
TypeRelation::Subtyping => false, TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => false,
TypeRelation::Assignability => true, TypeRelation::Assignability => true,
TypeRelation::Redundancy => match self { TypeRelation::Redundancy => match self {
Type::Dynamic(_) => true, Type::Dynamic(_) => true,
@ -1970,12 +1997,16 @@ impl<'db> Type<'db> {
// to non-transitivity (highly undesirable); and pragmatically, a full implementation // to non-transitivity (highly undesirable); and pragmatically, a full implementation
// of redundancy may not generally lead to simpler types in many situations. // of redundancy may not generally lead to simpler types in many situations.
let self_ty = match relation { let self_ty = match relation {
TypeRelation::Subtyping | TypeRelation::Redundancy => self, TypeRelation::Subtyping
| TypeRelation::Redundancy
| TypeRelation::SubtypingAssuming(_) => self,
TypeRelation::Assignability => self.bottom_materialization(db), TypeRelation::Assignability => self.bottom_materialization(db),
}; };
intersection.negative(db).iter().when_all(db, |&neg_ty| { intersection.negative(db).iter().when_all(db, |&neg_ty| {
let neg_ty = match relation { let neg_ty = match relation {
TypeRelation::Subtyping | TypeRelation::Redundancy => neg_ty, TypeRelation::Subtyping
| TypeRelation::Redundancy
| TypeRelation::SubtypingAssuming(_) => neg_ty,
TypeRelation::Assignability => neg_ty.bottom_materialization(db), TypeRelation::Assignability => neg_ty.bottom_materialization(db),
}; };
self_ty.is_disjoint_from_impl( self_ty.is_disjoint_from_impl(
@ -10322,7 +10353,7 @@ impl<'db> ConstructorCallError<'db> {
/// A non-exhaustive enumeration of relations that can exist between types. /// A non-exhaustive enumeration of relations that can exist between types.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub(crate) enum TypeRelation { pub(crate) enum TypeRelation<'db> {
/// The "subtyping" relation. /// The "subtyping" relation.
/// ///
/// A [fully static] type `B` is a subtype of a fully static type `A` if and only if /// A [fully static] type `B` is a subtype of a fully static type `A` if and only if
@ -10446,9 +10477,46 @@ pub(crate) enum TypeRelation {
/// [fully static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type /// [fully static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type
/// [materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize /// [materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
Redundancy, Redundancy,
/// The "constraint implication" relationship, aka "implies subtype of".
///
/// This relationship tests whether one type is a [subtype][Self::Subtyping] of another,
/// assuming that the constraints in a particular constraint set hold.
///
/// For concrete types (types that do not contain typevars), this relationship is the same as
/// [subtyping][Self::Subtyping]. (Constraint sets place restrictions on typevars, so if you
/// are not comparing typevars, the constraint set can have no effect on whether subtyping
/// holds.)
///
/// If you're comparing a typevar, we have to consider what restrictions the constraint set
/// places on that typevar to determine if subtyping holds. For instance, if you want to check
/// whether `T ≤ int`, then the answer will depend on what constraint set you are considering:
///
/// ```text
/// implies_subtype_of(T ≤ bool, T, int) ⇒ true
/// implies_subtype_of(T ≤ int, T, int) ⇒ true
/// implies_subtype_of(T ≤ str, T, int) ⇒ false
/// ```
///
/// In the first two cases, the constraint set ensures that `T` will always specialize to a
/// type that is a subtype of `int`. In the final case, the constraint set requires `T` to
/// specialize to a subtype of `str`, and there is no such type that is also a subtype of
/// `int`.
///
/// There are two constraint sets that deserve special consideration.
///
/// - The "always true" constraint set does not place any restrictions on any typevar. In this
/// case, `implies_subtype_of` will return the same result as `when_subtype_of`, even if
/// you're comparing against a typevar.
///
/// - The "always false" constraint set represents an impossible situation. In this case, every
/// subtype check will be vacuously true, even if you're comparing two concrete types that
/// are not actually subtypes of each other. (That is, `implies_subtype_of(false, int, str)`
/// will return true!)
SubtypingAssuming(ConstraintSet<'db>),
} }
impl TypeRelation { impl TypeRelation<'_> {
pub(crate) const fn is_assignability(self) -> bool { pub(crate) const fn is_assignability(self) -> bool {
matches!(self, TypeRelation::Assignability) matches!(self, TypeRelation::Assignability)
} }
@ -10615,7 +10683,7 @@ impl<'db> BoundMethodType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -10782,7 +10850,7 @@ impl<'db> CallableType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -10891,7 +10959,7 @@ impl<'db> KnownBoundMethodType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {

View file

@ -1216,10 +1216,10 @@ impl<'db> Bindings<'db> {
continue; continue;
}; };
let result = tracked.constraints(db).when_subtype_of_given( let result = ty_a.when_subtype_of_assuming(
db, db,
*ty_a,
*ty_b, *ty_b,
tracked.constraints(db),
InferableTypeVars::None, InferableTypeVars::None,
); );
let tracked = TrackedConstraintSet::new(db, result); let tracked = TrackedConstraintSet::new(db, result);

View file

@ -541,14 +541,16 @@ impl<'db> ClassType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
self.iter_mro(db).when_any(db, |base| { self.iter_mro(db).when_any(db, |base| {
match base { match base {
ClassBase::Dynamic(_) => match relation { ClassBase::Dynamic(_) => match relation {
TypeRelation::Subtyping | TypeRelation::Redundancy => { TypeRelation::Subtyping
| TypeRelation::Redundancy
| TypeRelation::SubtypingAssuming(_) => {
ConstraintSet::from(other.is_object(db)) ConstraintSet::from(other.is_object(db))
} }
TypeRelation::Assignability => ConstraintSet::from(!other.is_final(db)), TypeRelation::Assignability => ConstraintSet::from(!other.is_final(db)),

View file

@ -185,11 +185,12 @@ impl<'db> ConstraintSet<'db> {
typevar: BoundTypeVarInstance<'db>, typevar: BoundTypeVarInstance<'db>,
lower: Type<'db>, lower: Type<'db>,
upper: Type<'db>, upper: Type<'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
) -> Self { ) -> Self {
let (lower, upper) = match relation { let (lower, upper) = match relation {
// TODO: Is this the correct constraint for redundancy? TypeRelation::Subtyping
TypeRelation::Subtyping | TypeRelation::Redundancy => ( | TypeRelation::Redundancy
| TypeRelation::SubtypingAssuming(_) => (
lower.top_materialization(db), lower.top_materialization(db),
upper.bottom_materialization(db), upper.bottom_materialization(db),
), ),
@ -215,47 +216,16 @@ impl<'db> ConstraintSet<'db> {
} }
/// Returns the constraints under which `lhs` is a subtype of `rhs`, assuming that the /// Returns the constraints under which `lhs` is a subtype of `rhs`, assuming that the
/// constraints in this constraint set hold. /// constraints in this constraint set hold. Panics if neither of the types being compared are
/// /// a typevar. (That case is handled by `Type::has_relation_to`.)
/// For concrete types (types that are not typevars), this returns the same result as pub(crate) fn implies_subtype_of(
/// [`when_subtype_of`][Type::when_subtype_of]. (Constraint sets place restrictions on
/// typevars, so if you are not comparing typevars, the constraint set can have no effect on
/// whether subtyping holds.)
///
/// If you're comparing a typevar, we have to consider what restrictions the constraint set
/// places on that typevar to determine if subtyping holds. For instance, if you want to check
/// whether `T ≤ int`, then answer will depend on what constraint set you are considering:
///
/// ```text
/// when_subtype_of_given(T ≤ bool, T, int) ⇒ true
/// when_subtype_of_given(T ≤ int, T, int) ⇒ true
/// when_subtype_of_given(T ≤ str, T, int) ⇒ false
/// ```
///
/// In the first two cases, the constraint set ensures that `T` will always specialize to a
/// type that is a subtype of `int`. In the final case, the constraint set requires `T` to
/// specialize to a subtype of `str`, and there is no such type that is also a subtype of
/// `int`.
///
/// There are two constraint sets that deserve special consideration.
///
/// - The "always true" constraint set does not place any restrictions on any typevar. In this
/// case, `when_subtype_of_given` will return the same result as `when_subtype_of`, even if
/// you're comparing against a typevar.
///
/// - The "always false" constraint set represents an impossible situation. In this case, every
/// subtype check will be vacuously true, even if you're comparing two concrete types that
/// are not actually subtypes of each other. (That is,
/// `when_subtype_of_given(false, int, str)` will return true!)
pub(crate) fn when_subtype_of_given(
self, self,
db: &'db dyn Db, db: &'db dyn Db,
lhs: Type<'db>, lhs: Type<'db>,
rhs: Type<'db>, rhs: Type<'db>,
inferable: InferableTypeVars<'_, 'db>,
) -> Self { ) -> Self {
Self { Self {
node: self.node.when_subtype_of_given(db, lhs, rhs, inferable), node: self.node.implies_subtype_of(db, lhs, rhs),
} }
} }
@ -829,13 +799,7 @@ impl<'db> Node<'db> {
simplified.and(db, domain) simplified.and(db, domain)
} }
fn when_subtype_of_given( fn implies_subtype_of(self, db: &'db dyn Db, lhs: Type<'db>, rhs: Type<'db>) -> Self {
self,
db: &'db dyn Db,
lhs: Type<'db>,
rhs: Type<'db>,
inferable: InferableTypeVars<'_, 'db>,
) -> Self {
// When checking subtyping involving a typevar, we can turn the subtyping check into a // When checking subtyping involving a typevar, we can turn the subtyping check into a
// constraint (i.e, "is `T` a subtype of `int` becomes the constraint `T ≤ int`), and then // constraint (i.e, "is `T` a subtype of `int` becomes the constraint `T ≤ int`), and then
// check when the BDD implies that constraint. // check when the BDD implies that constraint.
@ -846,8 +810,7 @@ impl<'db> Node<'db> {
(_, Type::TypeVar(bound_typevar)) => { (_, Type::TypeVar(bound_typevar)) => {
ConstrainedTypeVar::new_node(db, bound_typevar, lhs, Type::object()) ConstrainedTypeVar::new_node(db, bound_typevar, lhs, Type::object())
} }
// If neither type is a typevar, then we fall back on a normal subtyping check. _ => panic!("at least one type should be a typevar"),
_ => return lhs.when_subtype_of(db, rhs, inferable).node,
}; };
self.satisfies(db, constraint) self.satisfies(db, constraint)
@ -1464,10 +1427,12 @@ impl<'db> InteriorNode<'db> {
_ => continue, _ => continue,
}; };
let new_node = Node::new_constraint( let new_constraint =
db, ConstrainedTypeVar::new(db, constrained_typevar, new_lower, new_upper);
ConstrainedTypeVar::new(db, constrained_typevar, new_lower, new_upper), if seen_constraints.contains(&new_constraint) {
); continue;
}
let new_node = Node::new_constraint(db, new_constraint);
let positive_left_node = let positive_left_node =
Node::new_satisfied_constraint(db, left_constraint.when_true()); Node::new_satisfied_constraint(db, left_constraint.when_true());
let positive_right_node = let positive_right_node =
@ -1481,7 +1446,18 @@ impl<'db> InteriorNode<'db> {
continue; continue;
} }
// From here on out we know that both constraints constrain the same typevar. // From here on out we know that both constraints constrain the same typevar. The
// clause above will propagate all that we know about the current typevar relative to
// other typevars, producing constraints on this typevar that have concrete lower/upper
// bounds. That means we can skip the simplifications below if any bound is another
// typevar.
if left_constraint.lower(db).is_type_var()
|| left_constraint.upper(db).is_type_var()
|| right_constraint.lower(db).is_type_var()
|| right_constraint.upper(db).is_type_var()
{
continue;
}
// Containment: The range of one constraint might completely contain the range of the // Containment: The range of one constraint might completely contain the range of the
// other. If so, there are several potential simplifications. // other. If so, there are several potential simplifications.

View file

@ -971,7 +971,7 @@ impl<'db> FunctionType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -979,8 +979,10 @@ impl<'db> FunctionType<'db> {
// our representation of a function type includes any specialization that should be applied // our representation of a function type includes any specialization that should be applied
// to the signature. Different specializations of the same function type are only subtypes // to the signature. Different specializations of the same function type are only subtypes
// of each other if they result in subtype signatures. // of each other if they result in subtype signatures.
if matches!(relation, TypeRelation::Subtyping | TypeRelation::Redundancy) if matches!(
&& self.normalized(db) == other.normalized(db) relation,
TypeRelation::Subtyping | TypeRelation::Redundancy | TypeRelation::SubtypingAssuming(_)
) && self.normalized(db) == other.normalized(db)
{ {
return ConstraintSet::from(true); return ConstraintSet::from(true);
} }

View file

@ -732,7 +732,7 @@ fn has_relation_in_invariant_position<'db>(
base_type: &Type<'db>, base_type: &Type<'db>,
base_materialization: Option<MaterializationKind>, base_materialization: Option<MaterializationKind>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -781,30 +781,34 @@ fn has_relation_in_invariant_position<'db>(
) )
}), }),
// For gradual types, A <: B (subtyping) is defined as Top[A] <: Bottom[B] // For gradual types, A <: B (subtyping) is defined as Top[A] <: Bottom[B]
(None, Some(base_mat), TypeRelation::Subtyping | TypeRelation::Redundancy) => { (
is_subtype_in_invariant_position( None,
db, Some(base_mat),
derived_type, TypeRelation::Subtyping | TypeRelation::Redundancy | TypeRelation::SubtypingAssuming(_),
MaterializationKind::Top, ) => is_subtype_in_invariant_position(
base_type, db,
base_mat, derived_type,
inferable, MaterializationKind::Top,
relation_visitor, base_type,
disjointness_visitor, base_mat,
) inferable,
} relation_visitor,
(Some(derived_mat), None, TypeRelation::Subtyping | TypeRelation::Redundancy) => { disjointness_visitor,
is_subtype_in_invariant_position( ),
db, (
derived_type, Some(derived_mat),
derived_mat, None,
base_type, TypeRelation::Subtyping | TypeRelation::Redundancy | TypeRelation::SubtypingAssuming(_),
MaterializationKind::Bottom, ) => is_subtype_in_invariant_position(
inferable, db,
relation_visitor, derived_type,
disjointness_visitor, derived_mat,
) base_type,
} MaterializationKind::Bottom,
inferable,
relation_visitor,
disjointness_visitor,
),
// And A <~ B (assignability) is Bottom[A] <: Top[B] // And A <~ B (assignability) is Bottom[A] <: Top[B]
(None, Some(base_mat), TypeRelation::Assignability) => is_subtype_in_invariant_position( (None, Some(base_mat), TypeRelation::Assignability) => is_subtype_in_invariant_position(
db, db,
@ -1072,7 +1076,7 @@ impl<'db> Specialization<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {

View file

@ -127,7 +127,7 @@ impl<'db> Type<'db> {
db: &'db dyn Db, db: &'db dyn Db,
protocol: ProtocolInstanceType<'db>, protocol: ProtocolInstanceType<'db>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -370,7 +370,7 @@ impl<'db> NominalInstanceType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {

View file

@ -234,7 +234,7 @@ impl<'db> ProtocolInterface<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -634,7 +634,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Type<'db>, other: Type<'db>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {

View file

@ -234,7 +234,7 @@ impl<'db> CallableSignature<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: &Self, other: &Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -256,7 +256,7 @@ impl<'db> CallableSignature<'db> {
self_signatures: &[Signature<'db>], self_signatures: &[Signature<'db>],
other_signatures: &[Signature<'db>], other_signatures: &[Signature<'db>],
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -732,7 +732,7 @@ impl<'db> Signature<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: &Signature<'db>, other: &Signature<'db>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {

View file

@ -137,7 +137,7 @@ impl<'db> SubclassOfType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: SubclassOfType<'db>, other: SubclassOfType<'db>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {

View file

@ -260,7 +260,7 @@ impl<'db> TupleType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
other: Self, other: Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -442,7 +442,7 @@ impl<'db> FixedLengthTuple<Type<'db>> {
db: &'db dyn Db, db: &'db dyn Db,
other: &Tuple<Type<'db>>, other: &Tuple<Type<'db>>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -799,7 +799,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
db: &'db dyn Db, db: &'db dyn Db,
other: &Tuple<Type<'db>>, other: &Tuple<Type<'db>>,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {
@ -1191,7 +1191,7 @@ impl<'db> Tuple<Type<'db>> {
db: &'db dyn Db, db: &'db dyn Db,
other: &Self, other: &Self,
inferable: InferableTypeVars<'_, 'db>, inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation, relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>, relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> { ) -> ConstraintSet<'db> {