[red-knot] Add Type::TypeVar variant (#17102)

This adds a new `Type` variant for holding an instance of a typevar
inside of a generic function or class. We don't handle specializing the
typevars yet, but this should implement most of the typing rules for
inside the generic function/class, where we don't know yet which
specific type the typevar will be specialized to.

This PR does _not_ yet handle the constraint that multiple occurrences
of the typevar must be specialized to the _same_ time. (There is an
existing test case for this in `generics/functions.md` which is still
marked as TODO.)

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Douglas Creager 2025-04-03 14:36:29 -04:00 committed by GitHub
parent 45c43735e0
commit 64e7e1aa64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 990 additions and 122 deletions

View file

@ -162,6 +162,17 @@ impl HasNavigationTargets for Type<'_> {
| Type::PropertyInstance(_)
| Type::Tuple(_) => self.to_meta_type(db.upcast()).navigation_targets(db),
Type::TypeVar(var) => {
let definition = var.definition(db);
let full_range = definition.full_range(db.upcast());
NavigationTargets::single(NavigationTarget {
file: full_range.file(),
focus_range: definition.focus_range(db.upcast()).range(),
full_range: full_range.range(),
})
}
Type::Intersection(intersection) => intersection.navigation_targets(db),
Type::Dynamic(_)

View file

@ -107,8 +107,7 @@ def good_return[T: int](x: T) -> T:
return x
def bad_return[T: int](x: T) -> T:
# TODO: error: int is not assignable to T
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `Literal[1]`"
# error: [invalid-return-type] "Object of type `int` is not assignable to return type `T`"
return x + 1
```

View file

@ -48,4 +48,511 @@ class C[T]:
reveal_type(x) # revealed: T
```
## Fully static typevars
We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This
is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This
is similar to how you can assign an expression whose type is not fully static to a target whose type
is.)
```py
from knot_extensions import is_fully_static, static_assert
from typing import Any
def unbounded_unconstrained[T](t: list[T]) -> None:
static_assert(is_fully_static(T))
def bounded[T: int](t: list[T]) -> None:
static_assert(is_fully_static(T))
def bounded_by_gradual[T: Any](t: list[T]) -> None:
static_assert(not is_fully_static(T))
def constrained[T: (int, str)](t: list[T]) -> None:
static_assert(is_fully_static(T))
def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None:
static_assert(not is_fully_static(T))
```
## Subtyping and assignability
(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static
typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual
typevars.)
We can make no assumption about what type an unbounded, unconstrained, fully static typevar will be
specialized to. Properties are true of the typevar only if they are true for every valid
specialization. Thus, the typevar is a subtype of itself and of `object`, but not of any other type
(including other typevars).
```py
from knot_extensions import is_assignable_to, is_subtype_of, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None:
static_assert(is_assignable_to(T, T))
static_assert(is_assignable_to(T, object))
static_assert(not is_assignable_to(T, Super))
static_assert(is_assignable_to(U, U))
static_assert(is_assignable_to(U, object))
static_assert(not is_assignable_to(U, Super))
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(is_subtype_of(T, T))
static_assert(is_subtype_of(T, object))
static_assert(not is_subtype_of(T, Super))
static_assert(is_subtype_of(U, U))
static_assert(is_subtype_of(U, object))
static_assert(not is_subtype_of(U, Super))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of
its bound. (A typevar with a non-fully-static bound is itself non-fully-static, and therefore does
not participate in subtyping.) A fully static bound is not assignable to, nor a subtype of, the
typevar, since the typevar might be specialized to a smaller type. (This is true even if the bound
is a final class, since the typevar can still be specialized to `Never`.)
```py
from typing import Any
from typing_extensions import final
def bounded[T: Super](t: list[T]) -> None:
static_assert(is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(Super, T))
static_assert(not is_assignable_to(Sub, T))
static_assert(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Sub, T))
def bounded_by_gradual[T: Any](t: list[T]) -> None:
static_assert(is_assignable_to(T, Any))
static_assert(is_assignable_to(Any, T))
static_assert(is_assignable_to(T, Super))
static_assert(not is_assignable_to(Super, T))
static_assert(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(Sub, T))
static_assert(not is_subtype_of(T, Any))
static_assert(not is_subtype_of(Any, T))
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(Sub, T))
@final
class FinalClass: ...
def bounded_final[T: FinalClass](t: list[T]) -> None:
static_assert(is_assignable_to(T, FinalClass))
static_assert(not is_assignable_to(FinalClass, T))
static_assert(is_subtype_of(T, FinalClass))
static_assert(not is_subtype_of(FinalClass, T))
```
Two distinct fully static typevars are not subtypes of each other, even if they have the same
bounds, since there is (still) 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.
```py
def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
A constrained fully static typevar is assignable to the union of its constraints, but not to any of
the constraints individually. None of the constraints are subtypes of the typevar, though the
intersection of all of its constraints is a subtype of the typevar.
```py
from knot_extensions import Intersection
def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
static_assert(not is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Unrelated))
static_assert(is_assignable_to(T, Super | Unrelated))
static_assert(is_assignable_to(T, Base | Unrelated))
static_assert(not is_assignable_to(T, Sub | Unrelated))
static_assert(not is_assignable_to(Super, T))
static_assert(not is_assignable_to(Unrelated, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Unrelated))
static_assert(is_subtype_of(T, Super | Unrelated))
static_assert(is_subtype_of(T, Base | Unrelated))
static_assert(not is_subtype_of(T, Sub | Unrelated))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None:
static_assert(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Unrelated))
static_assert(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Super | Any))
static_assert(is_assignable_to(T, Super | Unrelated))
static_assert(not is_assignable_to(Super, T))
static_assert(is_assignable_to(Base, T))
static_assert(not is_assignable_to(Unrelated, T))
static_assert(is_assignable_to(Any, T))
static_assert(not is_assignable_to(Super | Any, T))
static_assert(is_assignable_to(Base | Any, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(is_assignable_to(Intersection[Base, Any], T))
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Unrelated))
static_assert(not is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Super | Any))
static_assert(not is_subtype_of(T, Super | Unrelated))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Base, T))
static_assert(not is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Any, T))
static_assert(not is_subtype_of(Super | Any, T))
static_assert(not is_subtype_of(Base | Any, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T))
static_assert(not is_subtype_of(Intersection[Base, Any], T))
```
Two distinct fully static typevars are not subtypes of each other, even if they have the same
constraints, and even if any of the constraints are final. There must always be at least two
distinct constraints, meaning that there is (still) no guarantee that they will be specialized to
the same type.
```py
def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
@final
class AnotherFinalClass: ...
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
## Singletons and single-valued types
(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the
claims also apply to _single-valued_ types.)
An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a
non-singleton type.
```py
from knot_extensions import is_singleton, is_single_valued, static_assert
def unbounded_unconstrained[T](t: list[T]) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
```
A bounded typevar is not a singleton, even if its bound is a singleton, since it can still be
specialized to `Never`.
```py
def bounded[T: None](t: list[T]) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
```
A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot
specialize a constrained typevar to a subtype of a constraint.)
```py
from typing_extensions import Literal
def constrained_non_singletons[T: (int, str)](t: list[T]) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None:
static_assert(is_singleton(T))
def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None:
static_assert(is_single_valued(T))
```
## Unions involving typevars
The union of an unbounded unconstrained typevar with any other type cannot be simplified, since
there is no guarantee what type the typevar will be specialized to.
```py
from typing import Any
class Super: ...
class Base(Super): ...
class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T](t: T) -> None:
def _(x: T | Super) -> None:
reveal_type(x) # revealed: T | Super
def _(x: T | Base) -> None:
reveal_type(x) # revealed: T | Base
def _(x: T | Sub) -> None:
reveal_type(x) # revealed: T | Sub
def _(x: T | Unrelated) -> None:
reveal_type(x) # revealed: T | Unrelated
def _(x: T | Any) -> None:
reveal_type(x) # revealed: T | Any
```
The union of a bounded typevar with its bound is that bound. (The typevar is guaranteed to be
specialized to a subtype of the bound.) The union of a bounded typevar with a subtype of its bound
cannot be simplified. (The typevar might be specialized to a different subtype of the bound.)
```py
def bounded[T: Base](t: T) -> None:
def _(x: T | Super) -> None:
reveal_type(x) # revealed: Super
def _(x: T | Base) -> None:
reveal_type(x) # revealed: Base
def _(x: T | Sub) -> None:
reveal_type(x) # revealed: T | Sub
def _(x: T | Unrelated) -> None:
reveal_type(x) # revealed: T | Unrelated
def _(x: T | Any) -> None:
reveal_type(x) # revealed: T | Any
```
The union of a constrained typevar with a type depends on how that type relates to the constraints.
If all of the constraints are a subtype of that type, the union simplifies to that type. Inversely,
if the type is a subtype of every constraint, the union simplifies to the typevar. Otherwise, the
union cannot be simplified.
```py
def constrained[T: (Base, Sub)](t: T) -> None:
def _(x: T | Super) -> None:
reveal_type(x) # revealed: Super
def _(x: T | Base) -> None:
reveal_type(x) # revealed: Base
def _(x: T | Sub) -> None:
reveal_type(x) # revealed: T
def _(x: T | Unrelated) -> None:
reveal_type(x) # revealed: T | Unrelated
def _(x: T | Any) -> None:
reveal_type(x) # revealed: T | Any
```
## Intersections involving typevars
The intersection of an unbounded unconstrained typevar with any other type cannot be simplified,
since there is no guarantee what type the typevar will be specialized to.
```py
from knot_extensions import Intersection
from typing import Any
class Super: ...
class Base(Super): ...
class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T](t: T) -> None:
def _(x: Intersection[T, Super]) -> None:
reveal_type(x) # revealed: T & Super
def _(x: Intersection[T, Base]) -> None:
reveal_type(x) # revealed: T & Base
def _(x: Intersection[T, Sub]) -> None:
reveal_type(x) # revealed: T & Sub
def _(x: Intersection[T, Unrelated]) -> None:
reveal_type(x) # revealed: T & Unrelated
def _(x: Intersection[T, Any]) -> None:
reveal_type(x) # revealed: T & Any
```
The intersection of a bounded typevar with its bound or a supertype of its bound is the typevar
itself. (The typevar might be specialized to a subtype of the bound.) The intersection of a bounded
typevar with a subtype of its bound cannot be simplified. (The typevar might be specialized to a
different subtype of the bound.) The intersection of a bounded typevar with a type that is disjoint
from its bound is `Never`.
```py
def bounded[T: Base](t: T) -> None:
def _(x: Intersection[T, Super]) -> None:
reveal_type(x) # revealed: T
def _(x: Intersection[T, Base]) -> None:
reveal_type(x) # revealed: T
def _(x: Intersection[T, Sub]) -> None:
reveal_type(x) # revealed: T & Sub
def _(x: Intersection[T, None]) -> None:
reveal_type(x) # revealed: Never
def _(x: Intersection[T, Any]) -> None:
reveal_type(x) # revealed: T & Any
```
Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must
be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints,
since that would allow the typevar to take on values from _multiple_ constraints simultaneously. The
`OneOf` connector would not be a “type” according to a strict reading of the typing spec, since it
would not represent a single set of runtime objects; it would instead represent a _set of_ sets of
runtime objects. This is one reason we have not actually added this connector to our data model yet.
Nevertheless, describing constrained typevars this way helps explain how we simplify intersections
involving them.
This means that when intersecting a constrained typevar with a type `T`, constraints that are
supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover,
constraints that are disjoint from `T` are no longer valid specializations of the typevar, since
`Never` is an identity for `OneOf`. After these simplifications, if only one constraint remains, we
can simplify the intersection as a whole to that constraint.
```py
def constrained[T: (Base, Sub, Unrelated)](t: T) -> None:
def _(x: Intersection[T, Base]) -> None:
# With OneOf this would be OneOf[Base, Sub]
reveal_type(x) # revealed: T & Base
def _(x: Intersection[T, Unrelated]) -> None:
reveal_type(x) # revealed: Unrelated
def _(x: Intersection[T, Sub]) -> None:
reveal_type(x) # revealed: Sub
def _(x: Intersection[T, None]) -> None:
reveal_type(x) # revealed: Never
def _(x: Intersection[T, Any]) -> None:
reveal_type(x) # revealed: T & Any
```
We can simplify the intersection similarly when removing a type from a constrained typevar, since
this is modeled internally as an intersection with a negation.
```py
from knot_extensions import Not
def remove_constraint[T: (int, str, bool)](t: T) -> None:
def _(x: Intersection[T, Not[int]]) -> None:
reveal_type(x) # revealed: str
def _(x: Intersection[T, Not[str]]) -> None:
# With OneOf this would be OneOf[int, bool]
reveal_type(x) # revealed: T & ~str
def _(x: Intersection[T, Not[bool]]) -> None:
reveal_type(x) # revealed: T & ~bool
def _(x: Intersection[T, Not[int], Not[str]]) -> None:
reveal_type(x) # revealed: Never
def _(x: Intersection[T, Not[None]]) -> None:
reveal_type(x) # revealed: T
def _(x: Intersection[T, Not[Any]]) -> None:
reveal_type(x) # revealed: T & Any
```
## Narrowing
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:
```py
class P: ...
class Q: ...
class R: ...
def f[T: (P, Q)](t: T) -> None:
if isinstance(t, P):
reveal_type(t) # revealed: P
p: P = t
else:
reveal_type(t) # revealed: Q
q: Q = t
if isinstance(t, Q):
reveal_type(t) # revealed: Q
q: Q = t
else:
reveal_type(t) # revealed: P
p: P = t
def g[T: (P, Q, R)](t: T) -> None:
if isinstance(t, P):
reveal_type(t) # revealed: P
p: P = t
elif isinstance(t, Q):
reveal_type(t) # revealed: Q & ~P
q: Q = t
else:
reveal_type(t) # revealed: R
r: R = t
if isinstance(t, P):
reveal_type(t) # revealed: P
p: P = t
elif isinstance(t, Q):
reveal_type(t) # revealed: Q & ~P
q: Q = t
elif isinstance(t, R):
reveal_type(t) # revealed: R & ~P & ~Q
r: R = t
else:
reveal_type(t) # revealed: Never
```
[pep 695]: https://peps.python.org/pep-0695/

View file

@ -312,7 +312,10 @@ pub enum Type<'db> {
/// A heterogeneous tuple type, with elements of the given types in source order.
// TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`.
Tuple(TupleType<'db>),
// TODO protocols, callable types, overloads, generics, type vars
/// An instance of a typevar in a generic class or function. When the generic class or function
/// is specialized, we will replace this typevar with its specialization.
TypeVar(TypeVarInstance<'db>),
// TODO protocols, overloads, generics
}
#[salsa::tracked]
@ -398,6 +401,15 @@ impl<'db> Type<'db> {
ClassBase::Class(_) => false,
},
Self::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.contains_todo(db),
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.any(|constraint| constraint.contains_todo(db)),
},
Self::Tuple(tuple) => tuple.elements(db).iter().any(|ty| ty.contains_todo(db)),
Self::Union(union) => union.elements(db).iter().any(|ty| ty.contains_todo(db)),
@ -633,6 +645,27 @@ impl<'db> Type<'db> {
| Type::KnownInstance(_)
| Type::IntLiteral(_)
| Type::SubclassOf(_) => self,
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
Type::TypeVar(TypeVarInstance::new(
db,
typevar.name(db).clone(),
typevar.definition(db),
Some(TypeVarBoundOrConstraints::UpperBound(bound.normalized(db))),
typevar.default_ty(db),
))
}
Some(TypeVarBoundOrConstraints::Constraints(union)) => {
Type::TypeVar(TypeVarInstance::new(
db,
typevar.name(db).clone(),
typevar.definition(db),
Some(TypeVarBoundOrConstraints::Constraints(union.normalized(db))),
typevar.default_ty(db),
))
}
None => self,
},
}
}
@ -680,6 +713,30 @@ impl<'db> Type<'db> {
// Everything is a subtype of `object`.
(_, Type::Instance(InstanceType { class })) if 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()
@ -690,6 +747,24 @@ impl<'db> Type<'db> {
.iter()
.any(|&elem_ty| self.is_subtype_of(db, elem_ty)),
(_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) {
// No types are a subtype of a bounded typevar, or of an unbounded unconstrained
// typevar, since there's no guarantee what type the typevar will be specialized
// to. If the typevar is bounded, it might be specialized to a smaller type than
// the bound. (This is true even if the bound is a final class, since the typevar
// can still be specialized to `Never`.)
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(_)) => false,
// If the typevar is constrained, there must be multiple constraints, and the
// typevar might be specialized to any one of them. However, the constraints do not
// have to be disjoint, which means an lhs type might be a subtype of all of the
// constraints.
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| self.is_subtype_of(db, *constraint)),
},
// If both sides are intersections we need to handle the right side first
// (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B,
// but none of A, B, or C is a subtype of (A & B).
@ -874,9 +949,9 @@ impl<'db> Type<'db> {
self.is_subtype_of(db, KnownClass::Property.to_instance(db))
}
// Other than the special cases enumerated above,
// `Instance` types are never subtypes of any other variants
(Type::Instance(_), _) => false,
// Other than the special cases enumerated above, `Instance` types and typevars are
// never subtypes of any other variants
(Type::Instance(_) | Type::TypeVar(_), _) => false,
}
}
@ -887,6 +962,7 @@ impl<'db> Type<'db> {
if self.is_gradual_equivalent_to(db, target) {
return true;
}
match (self, target) {
// Never can be assigned to any type.
(Type::Never, _) => true,
@ -899,6 +975,30 @@ impl<'db> Type<'db> {
// TODO this special case might be removable once the below cases are comprehensive
(_, Type::Instance(InstanceType { class })) if 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)
@ -911,6 +1011,24 @@ impl<'db> Type<'db> {
.iter()
.any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
(_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) {
// No types are assignable to a bounded typevar, or to an unbounded unconstrained
// typevar, since there's no guarantee what type the typevar will be specialized
// to. If the typevar is bounded, it might be specialized to a smaller type than
// the bound. (This is true even if the bound is a final class, since the typevar
// can still be specialized to `Never`.)
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(_)) => false,
// If the typevar is constrained, there must be multiple constraints, and the
// typevar might be specialized to any one of them. However, the constraints do not
// have to be disjoint, which means an lhs type might be assignable to all of the
// constraints.
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| self.is_assignable_to(db, *constraint)),
},
// If both sides are intersections we need to handle the right side first
// (A & B & C) is assignable to (A & B) because the left is assignable to both A and B,
// but none of A, B, or C is assignable to (A & B).
@ -1115,6 +1233,8 @@ impl<'db> Type<'db> {
}
}
(Type::TypeVar(first), Type::TypeVar(second)) => first == second,
(Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second),
(Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second),
@ -1141,6 +1261,33 @@ impl<'db> Type<'db> {
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false,
// A typevar is never disjoint from itself, since all occurrences of the typevar must
// be specialized to the same type. (This is an important difference between typevars
// and `Any`!) Different typevars might be disjoint, depending on their bounds and
// constraints, which are handled below.
(Type::TypeVar(self_typevar), Type::TypeVar(other_typevar))
if self_typevar == other_typevar =>
{
false
}
// An unbounded typevar is never disjoint from any other type, since it might be
// specialized to any type. A bounded typevar is not disjoint from its bound, and is
// only disjoint from other types if its bound is. A constrained typevar is disjoint
// from a type if all of its constraints are.
(Type::TypeVar(typevar), other) | (other, Type::TypeVar(typevar)) => {
match typevar.bound_or_constraints(db) {
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.is_disjoint_from(db, other)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_disjoint_from(db, other)),
}
}
(Type::Union(union), other) | (other, Type::Union(union)) => union
.elements(db)
.iter()
@ -1482,6 +1629,16 @@ impl<'db> Type<'db> {
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::PropertyInstance(_) => true,
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::ClassLiteral(_) | Type::Instance(_) => {
// TODO: Ideally, we would iterate over the MRO of the class, check if all
@ -1533,6 +1690,21 @@ impl<'db> Type<'db> {
// are both of type Literal[345], for example.
false
}
// An unbounded, unconstrained typevar is not a singleton, because it can be
// specialized to a non-singleton type. A bounded typevar is not a singleton, even if
// the bound is a final singleton class, since it can still be specialized to `Never`.
// A constrained typevar is a singleton if all of its constraints are singletons. (Note
// that you cannot specialize a constrained typevar to a subtype of a constraint.)
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(_)) => false,
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_singleton(db)),
},
// We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton.
Type::SubclassOf(..) => false,
Type::BooleanLiteral(_)
@ -1611,6 +1783,21 @@ impl<'db> Type<'db> {
| Type::SliceLiteral(..)
| Type::KnownInstance(..) => true,
// An unbounded, unconstrained typevar is not single-valued, because it can be
// specialized to a multiple-valued type. A bounded typevar is not single-valued, even
// if the bound is a final single-valued class, since it can still be specialized to
// `Never`. A constrained typevar is single-valued if all of its constraints are
// single-valued. (Note that you cannot specialize a constrained typevar to a subtype
// of a constraint.)
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => false,
Some(TypeVarBoundOrConstraints::UpperBound(_)) => false,
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db)
.iter()
.all(|constraint| constraint.is_single_valued(db)),
},
Type::SubclassOf(..) => {
// TODO: Same comment as above for `is_singleton`
false
@ -1758,6 +1945,7 @@ impl<'db> Type<'db> {
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_)
| Type::TypeVar(_)
| Type::Instance(_)
| Type::PropertyInstance(_) => None,
}
@ -1829,6 +2017,17 @@ impl<'db> Type<'db> {
.instance_member(db, name),
Type::Callable(_) => KnownClass::Object.to_instance(db).instance_member(db, name),
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => KnownClass::Object.to_instance(db).instance_member(db, name),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.instance_member(db, name)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.map_with_boundness_and_qualifiers(db, |constraint| {
constraint.instance_member(db, name)
}),
},
Type::IntLiteral(_) => KnownClass::Int.to_instance(db).instance_member(db, name),
Type::BooleanLiteral(_) => KnownClass::Bool.to_instance(db).instance_member(db, name),
Type::StringLiteral(_) | Type::LiteralString => {
@ -2265,6 +2464,7 @@ impl<'db> Type<'db> {
| Type::LiteralString
| Type::SliceLiteral(..)
| Type::Tuple(..)
| Type::TypeVar(..)
| Type::KnownInstance(..)
| Type::PropertyInstance(..)
| Type::FunctionLiteral(..) => {
@ -2384,6 +2584,108 @@ impl<'db> Type<'db> {
db: &'db dyn Db,
allow_short_circuit: bool,
) -> Result<Truthiness, BoolError<'db>> {
let type_to_truthiness = |ty| {
if let Type::BooleanLiteral(bool_val) = ty {
Truthiness::from(bool_val)
} else {
Truthiness::Ambiguous
}
};
let try_dunder_bool = || {
// We only check the `__bool__` method for truth testing, even though at
// runtime there is a fallback to `__len__`, since `__bool__` takes precedence
// and a subclass could add a `__bool__` method.
match self.try_call_dunder(db, "__bool__", CallArgumentTypes::none()) {
Ok(outcome) => {
let return_type = outcome.return_type(db);
if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) {
// The type has a `__bool__` method, but it doesn't return a
// boolean.
return Err(BoolError::IncorrectReturnType {
return_type,
not_boolable_type: *self,
});
}
Ok(type_to_truthiness(return_type))
}
Err(CallDunderError::PossiblyUnbound(outcome)) => {
let return_type = outcome.return_type(db);
if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) {
// The type has a `__bool__` method, but it doesn't return a
// boolean.
return Err(BoolError::IncorrectReturnType {
return_type: outcome.return_type(db),
not_boolable_type: *self,
});
}
// Don't trust possibly unbound `__bool__` method.
Ok(Truthiness::Ambiguous)
}
Err(CallDunderError::MethodNotAvailable) => Ok(Truthiness::Ambiguous),
Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => {
Err(BoolError::IncorrectArguments {
truthiness: type_to_truthiness(bindings.return_type(db)),
not_boolable_type: *self,
})
}
Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => {
Err(BoolError::NotCallable {
not_boolable_type: *self,
})
}
Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => {
Err(BoolError::Other {
not_boolable_type: *self,
})
}
}
};
let try_union = |union: UnionType<'db>| {
let mut truthiness = None;
let mut all_not_callable = true;
let mut has_errors = false;
for element in union.elements(db) {
let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) {
Ok(truthiness) => truthiness,
Err(err) => {
has_errors = true;
all_not_callable &= matches!(err, BoolError::NotCallable { .. });
err.fallback_truthiness()
}
};
truthiness.get_or_insert(element_truthiness);
if Some(element_truthiness) != truthiness {
truthiness = Some(Truthiness::Ambiguous);
if allow_short_circuit {
return Ok(Truthiness::Ambiguous);
}
}
}
if has_errors {
if all_not_callable {
return Err(BoolError::NotCallable {
not_boolable_type: *self,
});
}
return Err(BoolError::Union {
union,
truthiness: truthiness.unwrap_or(Truthiness::Ambiguous),
});
}
Ok(truthiness.unwrap_or(Truthiness::Ambiguous))
};
let truthiness = match self {
Type::Dynamic(_) | Type::Never | Type::Callable(_) | Type::LiteralString => {
Truthiness::Ambiguous
@ -2410,114 +2712,26 @@ impl<'db> Type<'db> {
}
},
instance_ty @ Type::Instance(InstanceType { class }) => match class.known(db) {
Some(known_class) => known_class.bool(),
None => {
// We only check the `__bool__` method for truth testing, even though at
// runtime there is a fallback to `__len__`, since `__bool__` takes precedence
// and a subclass could add a `__bool__` method.
let type_to_truthiness = |ty| {
if let Type::BooleanLiteral(bool_val) = ty {
Truthiness::from(bool_val)
} else {
Truthiness::Ambiguous
}
};
match self.try_call_dunder(db, "__bool__", CallArgumentTypes::none()) {
Ok(outcome) => {
let return_type = outcome.return_type(db);
if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) {
// The type has a `__bool__` method, but it doesn't return a
// boolean.
return Err(BoolError::IncorrectReturnType {
return_type,
not_boolable_type: *instance_ty,
});
}
type_to_truthiness(return_type)
}
Err(CallDunderError::PossiblyUnbound(outcome)) => {
let return_type = outcome.return_type(db);
if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) {
// The type has a `__bool__` method, but it doesn't return a
// boolean.
return Err(BoolError::IncorrectReturnType {
return_type: outcome.return_type(db),
not_boolable_type: *instance_ty,
});
}
// Don't trust possibly unbound `__bool__` method.
Truthiness::Ambiguous
}
Err(CallDunderError::MethodNotAvailable) => Truthiness::Ambiguous,
Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => {
return Err(BoolError::IncorrectArguments {
truthiness: type_to_truthiness(bindings.return_type(db)),
not_boolable_type: *instance_ty,
});
}
Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => {
return Err(BoolError::NotCallable {
not_boolable_type: *instance_ty,
});
}
Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => {
return Err(BoolError::Other {
not_boolable_type: *self,
})
}
}
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => Truthiness::Ambiguous,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.try_bool_impl(db, allow_short_circuit)?
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
try_union(constraints)?
}
},
Type::Instance(InstanceType { class }) => match class.known(db) {
Some(known_class) => known_class.bool(),
None => try_dunder_bool()?,
},
Type::KnownInstance(known_instance) => known_instance.bool(),
Type::PropertyInstance(_) => Truthiness::AlwaysTrue,
Type::Union(union) => {
let mut truthiness = None;
let mut all_not_callable = true;
let mut has_errors = false;
for element in union.elements(db) {
let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) {
Ok(truthiness) => truthiness,
Err(err) => {
has_errors = true;
all_not_callable &= matches!(err, BoolError::NotCallable { .. });
err.fallback_truthiness()
}
};
truthiness.get_or_insert(element_truthiness);
if Some(element_truthiness) != truthiness {
truthiness = Some(Truthiness::Ambiguous);
if allow_short_circuit {
return Ok(Truthiness::Ambiguous);
}
}
}
if has_errors {
if all_not_callable {
return Err(BoolError::NotCallable {
not_boolable_type: *self,
});
}
return Err(BoolError::Union {
union: *union,
truthiness: truthiness.unwrap_or(Truthiness::Ambiguous),
});
}
truthiness.unwrap_or(Truthiness::Ambiguous)
}
Type::Union(union) => try_union(*union)?,
Type::Intersection(_) => {
// TODO
@ -3274,6 +3488,7 @@ impl<'db> Type<'db> {
| Type::StringLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_)
| Type::TypeVar(_)
| Type::LiteralString
| Type::AlwaysTruthy
| Type::AlwaysFalsy => None,
@ -3327,6 +3542,7 @@ impl<'db> Type<'db> {
| Type::ModuleLiteral(_)
| Type::StringLiteral(_)
| Type::Tuple(_)
| Type::TypeVar(_)
| Type::Callable(_)
| Type::BoundMethod(_)
| Type::WrapperDescriptor(_)
@ -3362,8 +3578,7 @@ impl<'db> Type<'db> {
KnownInstanceType::Deque => Ok(KnownClass::Deque.to_instance(db)),
KnownInstanceType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)),
// TODO map this to a new `Type::TypeVar` variant
KnownInstanceType::TypeVar(_) => Ok(*self),
KnownInstanceType::TypeVar(typevar) => Ok(Type::TypeVar(*typevar)),
// TODO: Use an opt-in rule for a bare `Callable`
KnownInstanceType::Callable => Ok(Type::Callable(CallableType::unknown(db))),
@ -3541,6 +3756,17 @@ impl<'db> Type<'db> {
Type::Callable(_) => KnownClass::Type.to_instance(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db),
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => KnownClass::Object.to_class_literal(db),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db),
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
// TODO: If we add a proper `OneOf` connector, we should use that here instead
// of union. (Using a union here doesn't break anything, but it is imprecise.)
constraints.map(db, |constraint| constraint.to_meta_type(db))
}
},
Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db),
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
ClassBase::Dynamic(_) => *self,
@ -3809,10 +4035,10 @@ impl<'db> InvalidTypeExpression<'db> {
/// Data regarding a single type variable.
///
/// This is referenced by `KnownInstanceType::TypeVar` (to represent the singleton type of the
/// runtime `typing.TypeVar` object itself). In the future, it will also be referenced also by a
/// new `Type` variant to represent the type that this typevar represents as an annotation: that
/// is, an unknown set of objects, constrained by the upper-bound/constraints on this type var,
/// defaulting to the default type of this type var when not otherwise bound to a type.
/// runtime `typing.TypeVar` object itself), and by `Type::TypeVar` to represent the type that this
/// typevar represents as an annotation: that is, an unknown set of objects, constrained by the
/// upper-bound/constraints on this type var, defaulting to the default type of this type var when
/// not otherwise bound to a type.
///
/// This must be a tracked struct, not an interned one, because typevar equivalence is by identity,
/// not by value. Two typevars that have the same name, bound/constraints, and default, are still
@ -3856,7 +4082,7 @@ impl<'db> TypeVarInstance<'db> {
#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)]
pub enum TypeVarBoundOrConstraints<'db> {
UpperBound(Type<'db>),
Constraints(TupleType<'db>),
Constraints(UnionType<'db>),
}
/// Error returned if a type is not (or may not be) a context manager.

View file

@ -26,7 +26,7 @@
//! eliminate the supertype from the intersection).
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
use crate::types::{IntersectionType, KnownClass, Type, UnionType};
use crate::types::{IntersectionType, KnownClass, Type, TypeVarBoundOrConstraints, UnionType};
use crate::{Db, FxOrderSet};
use smallvec::SmallVec;
@ -485,7 +485,109 @@ impl<'db> InnerIntersectionBuilder<'db> {
}
}
/// Tries to simplify any constrained typevars in the intersection:
///
/// - If the intersection contains a positive entry for exactly one of the constraints, we can
/// remove the typevar (effectively replacing it with that one positive constraint).
///
/// - If the intersection contains negative entries for all but one of the constraints, we can
/// remove the negative constraints and replace the typevar with the remaining positive
/// constraint.
///
/// - If the intersection contains negative entries for all of the constraints, the overall
/// intersection is `Never`.
fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) {
let mut to_add = SmallVec::<[Type<'db>; 1]>::new();
let mut positive_to_remove = SmallVec::<[usize; 1]>::new();
let mut negative_to_remove = Vec::new();
for (typevar_index, ty) in self.positive.iter().enumerate() {
let Type::TypeVar(typevar) = ty else {
continue;
};
let Some(TypeVarBoundOrConstraints::Constraints(constraints)) =
typevar.bound_or_constraints(db)
else {
continue;
};
// Determine which constraints appear as positive entries in the intersection. Note
// that we shouldn't have duplicate entries in the positive or negative lists, so we
// don't need to worry about finding any particular constraint more than once.
let constraints = constraints.elements(db);
let mut positive_constraint_count = 0;
for positive in &self.positive {
// This linear search should be fine as long as we don't encounter typevars with
// thousands of constraints.
positive_constraint_count += constraints
.iter()
.filter(|c| c.is_subtype_of(db, *positive))
.count();
}
// If precisely one constraint appears as a positive element, we can replace the
// typevar with that positive constraint.
if positive_constraint_count == 1 {
positive_to_remove.push(typevar_index);
continue;
}
// Determine which constraints appear as negative entries in the intersection.
let mut to_remove = Vec::with_capacity(constraints.len());
let mut remaining_constraints: Vec<_> = constraints.iter().copied().map(Some).collect();
for (negative_index, negative) in self.negative.iter().enumerate() {
// This linear search should be fine as long as we don't encounter typevars with
// thousands of constraints.
let matching_constraints = constraints
.iter()
.enumerate()
.filter(|(_, c)| c.is_subtype_of(db, *negative));
for (constraint_index, _) in matching_constraints {
to_remove.push(negative_index);
remaining_constraints[constraint_index] = None;
}
}
let mut iter = remaining_constraints.into_iter().flatten();
let Some(remaining_constraint) = iter.next() else {
// All of the typevar constraints have been removed, so the entire intersection is
// `Never`.
*self = Self::default();
self.positive.insert(Type::Never);
return;
};
let more_than_one_remaining_constraint = iter.next().is_some();
if more_than_one_remaining_constraint {
// This typevar cannot be simplified.
continue;
}
// Only one typevar constraint remains. Remove all of the negative constraints, and
// replace the typevar itself with the remaining positive constraint.
to_add.push(remaining_constraint);
positive_to_remove.push(typevar_index);
negative_to_remove.extend(to_remove);
}
// We don't need to sort the positive list, since we only append to it in increasing order.
for index in positive_to_remove.into_iter().rev() {
self.positive.swap_remove_index(index);
}
negative_to_remove.sort_unstable();
negative_to_remove.dedup();
for index in negative_to_remove.into_iter().rev() {
self.negative.swap_remove_index(index);
}
for remaining_constraint in to_add {
self.add_positive(db, remaining_constraint);
}
}
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
self.simplify_constrained_typevars(db);
match (self.positive.len(), self.negative.len()) {
(0, 0) => Type::object(db),
(1, 0) => self.positive[0],

View file

@ -85,6 +85,7 @@ impl<'db> ClassBase<'db> {
| Type::SliceLiteral(_)
| Type::ModuleLiteral(_)
| Type::SubclassOf(_)
| Type::TypeVar(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy => None,
Type::KnownInstance(known_instance) => match known_instance {

View file

@ -165,6 +165,9 @@ impl Display for DisplayRepresentation<'_> {
}
f.write_str("]")
}
Type::TypeVar(typevar) => {
write!(f, "{}", typevar.name(self.db))
}
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
}

View file

@ -1997,14 +1997,25 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_expression(expr);
None
} else {
let tuple = TupleType::new(
// We don't use UnionType::from_elements or UnionBuilder here, because we don't
// want to simplify the list of constraints like we do with the elements of an
// actual union type.
// TODO: Consider using a new `OneOfType` connective here instead, since that
// more accurately represents the actual semantics of typevar constraints.
let elements = UnionType::new(
self.db(),
elts.iter()
.map(|expr| self.infer_type_expression(expr))
.collect::<Box<_>>(),
.collect::<Box<[_]>>(),
);
let constraints = TypeVarBoundOrConstraints::Constraints(elements);
// But when we construct an actual union type for the constraint expression as
// a whole, we do use UnionType::from_elements to maintain the invariant that
// all union types are simplified.
self.store_expression_type(
expr,
UnionType::from_elements(self.db(), elements.elements(self.db())),
);
let constraints = TypeVarBoundOrConstraints::Constraints(tuple);
self.store_expression_type(expr, Type::Tuple(tuple));
Some(constraints)
}
}
@ -2344,6 +2355,7 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::BoundMethod(_)
| Type::MethodWrapper(_)
| Type::WrapperDescriptor(_)
| Type::TypeVar(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy => match object_ty.class_member(db, attribute.into()) {
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
@ -4462,7 +4474,8 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_),
| Type::Tuple(_)
| Type::TypeVar(_),
) => {
let unary_dunder_method = match op {
ast::UnaryOp::Invert => "__invert__",
@ -4737,7 +4750,8 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_),
| Type::Tuple(_)
| Type::TypeVar(_),
Type::FunctionLiteral(_)
| Type::Callable(..)
| Type::BoundMethod(_)
@ -4757,7 +4771,8 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_),
| Type::Tuple(_)
| Type::TypeVar(_),
op,
) => {
// We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from

View file

@ -121,6 +121,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::Instance(_), _) => Ordering::Less,
(_, Type::Instance(_)) => Ordering::Greater,
(Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right),
(Type::TypeVar(_), _) => Ordering::Less,
(_, Type::TypeVar(_)) => Ordering::Greater,
(Type::AlwaysTruthy, _) => Ordering::Less,
(_, Type::AlwaysTruthy) => Ordering::Greater,