Compare commits

...

1 Commits

Author SHA1 Message Date
Carl Meyer
e3876e6d6d [ty] fix assigning a typevar to a union with itself 2025-05-06 16:43:02 -07:00
2 changed files with 81 additions and 50 deletions

View File

@@ -320,6 +320,17 @@ def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, An
static_assert(not is_subtype_of(U, T))
```
A bound or constrained typevar is a subtype of itself in a union:
```py
def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
static_assert(is_assignable_to(T, T | None))
static_assert(is_assignable_to(U, U | None))
static_assert(is_subtype_of(T, T | None))
static_assert(is_subtype_of(U, U | None))
```
## Singletons and single-valued types
(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the

View File

@@ -850,6 +850,13 @@ impl<'db> Type<'db> {
}
}
pub const fn into_typevar(self) -> Option<TypeVarInstance<'db>> {
match self {
Type::TypeVar(typevar) => Some(typevar),
_ => None,
}
}
pub const fn is_boolean_literal(&self) -> bool {
matches!(self, Type::BooleanLiteral(..))
}
@@ -980,7 +987,7 @@ impl<'db> Type<'db> {
return false;
}
match (self, target) {
let result = match (self, target) {
// We should have handled these immediately above.
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => {
unreachable!("Non-fully-static types do not participate in subtyping!")
@@ -995,30 +1002,6 @@ impl<'db> Type<'db> {
// Everything is a subtype of `object`.
(_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true,
// A fully static typevar is always a subtype of itself, and is never a subtype of any
// other typevar, since there is no guarantee that they will be specialized to the same
// type. (This is true even if both typevars are bounded by the same final class, since
// you can specialize the typevars to `Never` in addition to that final class.)
(Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => {
self_typevar == other_typevar
}
// 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).
(Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => {
match typevar.bound_or_constraints(db) {
None => unreachable!(),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.is_subtype_of(db, target)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_subtype_of(db, target)),
}
}
(Type::Union(union), _) => union
.elements(db)
.iter()
@@ -1284,7 +1267,33 @@ impl<'db> Type<'db> {
// Other than the special cases enumerated above, `Instance` types and typevars are
// never subtypes of any other variants
(Type::NominalInstance(_) | Type::TypeVar(_), _) => false,
};
// 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).
//
// This must be handled here, only if the above failed, because a typevar `T: int` needs to
// both a subtype of `int` and a subtype of e.g. `T | None`, so we need to also try
// treating it as its bound, if treating it as itself failed.
if !result {
if let Some(typevar) = self.into_typevar() {
match typevar.bound_or_constraints(db) {
None => {}
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
return bound.is_subtype_of(db, target);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
return constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_subtype_of(db, target));
}
}
}
}
result
}
/// Return true if this type is [assignable to] type `target`.
@@ -1295,7 +1304,7 @@ impl<'db> Type<'db> {
return true;
}
match (self, target) {
let result = match (self, target) {
// Never can be assigned to any type.
(Type::Never, _) => true,
@@ -1307,30 +1316,6 @@ impl<'db> Type<'db> {
// TODO this special case might be removable once the below cases are comprehensive
(_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true,
// A typevar is always assignable to itself, and is never assignable to any other
// typevar, since there is no guarantee that they will be specialized to the same
// type. (This is true even if both typevars are bounded by the same final class, since
// you can specialize the typevars to `Never` in addition to that final class.)
(Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => {
self_typevar == other_typevar
}
// A typevar is assignable to its upper bound, and to something similar to the union of
// its constraints. An unbound, unconstrained typevar has an implicit upper bound of
// `object` (which is handled above).
(Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => {
match typevar.bound_or_constraints(db) {
None => unreachable!(),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.is_assignable_to(db, target)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_assignable_to(db, target)),
}
}
// A union is assignable to a type T iff every element of the union is assignable to T.
(Type::Union(union), ty) => union
.elements(db)
@@ -1510,7 +1495,33 @@ impl<'db> Type<'db> {
// TODO other types containing gradual forms
_ => self.is_subtype_of(db, target),
};
// A typevar is assignable to its upper bound, and to something similar to the union of
// its constraints. An unbound, unconstrained typevar has an implicit upper bound of
// `object` (which is handled above).
//
// This must be handled here, only if the above failed, because a typevar `T: int` needs to
// both assignable to `int` and to e.g. `T | None`, so we need to also try treating it as
// its bound, if treating it as itself failed.
if !result {
if let Some(typevar) = self.into_typevar() {
match typevar.bound_or_constraints(db) {
None => {}
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
return bound.is_assignable_to(db, target);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
return constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_assignable_to(db, target));
}
}
}
}
result
}
/// Return true if this type is [equivalent to] type `other`.
@@ -1528,6 +1539,11 @@ impl<'db> Type<'db> {
}
(Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
(Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right),
// A fully static typevar is always equivalent to itself, and not any other typevar,
// since there is no guarantee that they will be specialized to the same type. (This is
// true even if both typevars are bounded by the same final class, since you can
// specialize the typevars to `Never` in addition to that final class.)
(Type::TypeVar(first), Type::TypeVar(second)) => first == second,
(Type::NominalInstance(left), Type::NominalInstance(right)) => {
left.is_equivalent_to(db, right)
}
@@ -1570,6 +1586,10 @@ impl<'db> Type<'db> {
}
}
// A typevar is always gradually equivalent to itself, and not to any other typevar,
// since there is no guarantee that they will be specialized to the same type. (This is
// true even if both typevars are bounded by the same final class, since you can
// specialize the typevars to `Never` in addition to that final class.)
(Type::TypeVar(first), Type::TypeVar(second)) => first == second,
(Type::NominalInstance(first), Type::NominalInstance(second)) => {