[ty] Generalize union-type subtyping fast path (#22495)

This commit is contained in:
Alex Waygood
2026-01-16 22:09:06 +00:00
committed by GitHub
parent b80d8ff6ff
commit 717d024ea9
3 changed files with 49 additions and 22 deletions

View File

@@ -907,6 +907,23 @@ def _(x: list[str]):
reveal_type(accepts_callable(GenericClass)(x, x))
```
### `Callable`s that return union types
```py
from typing import Callable
class Box[T]:
def get(self) -> T:
raise NotImplementedError
def my_iter[T](f: Callable[[], T | None]) -> Box[T]:
return Box()
def get_int() -> int | None: ...
reveal_type(my_iter(get_int)) # revealed: Box[int]
```
### Don't include identical lower/upper bounds in type mapping multiple times
This is was a performance regression reported in

View File

@@ -1840,7 +1840,7 @@ impl<'db> Type<'db> {
///
/// This method may have false negatives, but it should not have false positives. It should be
/// a cheap shallow check, not an exhaustive recursive check.
fn subtyping_is_always_reflexive(self) -> bool {
const fn subtyping_is_always_reflexive(self) -> bool {
match self {
Type::Never
| Type::FunctionLiteral(..)
@@ -1861,6 +1861,9 @@ impl<'db> Type<'db> {
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::PropertyInstance(_)
// `T` is always a subtype of itself,
// and `T` is always a subtype of `T | None`
| Type::TypeVar(_)
// might inherit `Any`, but subtyping is still reflexive
| Type::ClassLiteral(_)
=> true,
@@ -1872,7 +1875,6 @@ impl<'db> Type<'db> {
| Type::Union(_)
| Type::Intersection(_)
| Type::Callable(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_)
| Type::TypeGuard(_)

View File

@@ -195,6 +195,17 @@ impl TypeRelation<'_> {
pub(crate) const fn is_subtyping(self) -> bool {
matches!(self, TypeRelation::Subtyping)
}
pub(crate) const fn can_safely_assume_reflexivity(self, ty: Type) -> bool {
match self {
TypeRelation::Assignability
| TypeRelation::ConstraintSetAssignability
| TypeRelation::Redundancy => true,
TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => {
ty.subtyping_is_always_reflexive()
}
}
}
}
#[salsa::tracked]
@@ -329,7 +340,7 @@ impl<'db> Type<'db> {
//
// Note that we could do a full equivalence check here, but that would be both expensive
// and unnecessary. This early return is only an optimisation.
if (!relation.is_subtyping() || self.subtyping_is_always_reflexive()) && self == target {
if relation.can_safely_assume_reflexivity(self) && self == target {
return ConstraintSet::from(true);
}
@@ -460,46 +471,43 @@ impl<'db> Type<'db> {
},
}),
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
// In general, a TypeVar `T` is not redundant with a type `S` unless one of the two conditions is satisfied:
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
// 2. `T` is a constrained TypeVar and all of `T`'s constraints are subtypes of `S`.
//
// However, there is one exception to this general rule: for any given typevar `T`,
// `T` will always be a subtype of any union containing `T`.
(Type::TypeVar(bound_typevar), Type::Union(union))
if !bound_typevar.is_inferable(db, inferable)
(_, Type::Union(union))
if relation.can_safely_assume_reflexivity(self)
&& union.elements(db).contains(&self) =>
{
ConstraintSet::from(true)
}
// A similar rule applies in reverse to intersection types.
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
if !bound_typevar.is_inferable(db, inferable)
(Type::Intersection(intersection), _)
if relation.can_safely_assume_reflexivity(target)
&& intersection.positive(db).contains(&target) =>
{
ConstraintSet::from(true)
}
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
if !bound_typevar.is_inferable(db, inferable)
(Type::Intersection(intersection), _)
if relation.is_assignability()
&& intersection.positive(db).iter().any(Type::is_dynamic) =>
{
// If the intersection contains `Any`/`Unknown`/`@Todo`, it is assignable to any type.
// `Any` could materialize to `Never`, `Never & T & ~S` simplifies to `Never` for any
// `T` and any `S`, and `Never` is a subtype of all types.
ConstraintSet::from(true)
}
(Type::Intersection(intersection), _)
if relation.can_safely_assume_reflexivity(target)
&& intersection.negative(db).contains(&target) =>
{
ConstraintSet::from(false)
}
// Two identical typevars must always solve to the same type, so they are always
// subtypes of each other and assignable to each other.
//
// Note that this is not handled by the early return at the beginning of this method,
// since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive.
(Type::TypeVar(lhs_bound_typevar), Type::TypeVar(rhs_bound_typevar))
if !lhs_bound_typevar.is_inferable(db, inferable)
&& lhs_bound_typevar.is_same_typevar_as(db, rhs_bound_typevar) =>
{
ConstraintSet::from(true)
}
// `type[T]` is a subtype of the class object `A` if every instance of `T` is a subtype of an instance
// of `A`, and vice versa.
(Type::SubclassOf(subclass_of), _)