Skip to content

Commit 717d024

Browse files
authored
[ty] Generalize union-type subtyping fast path (#22495)
1 parent b80d8ff commit 717d024

3 files changed

Lines changed: 49 additions & 22 deletions

File tree

crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,23 @@ def _(x: list[str]):
907907
reveal_type(accepts_callable(GenericClass)(x, x))
908908
```
909909

910+
### `Callable`s that return union types
911+
912+
```py
913+
from typing import Callable
914+
915+
class Box[T]:
916+
def get(self) -> T:
917+
raise NotImplementedError
918+
919+
def my_iter[T](f: Callable[[], T | None]) -> Box[T]:
920+
return Box()
921+
922+
def get_int() -> int | None: ...
923+
924+
reveal_type(my_iter(get_int)) # revealed: Box[int]
925+
```
926+
910927
### Don't include identical lower/upper bounds in type mapping multiple times
911928

912929
This is was a performance regression reported in

crates/ty_python_semantic/src/types.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,7 +1840,7 @@ impl<'db> Type<'db> {
18401840
///
18411841
/// This method may have false negatives, but it should not have false positives. It should be
18421842
/// a cheap shallow check, not an exhaustive recursive check.
1843-
fn subtyping_is_always_reflexive(self) -> bool {
1843+
const fn subtyping_is_always_reflexive(self) -> bool {
18441844
match self {
18451845
Type::Never
18461846
| Type::FunctionLiteral(..)
@@ -1861,6 +1861,9 @@ impl<'db> Type<'db> {
18611861
| Type::AlwaysFalsy
18621862
| Type::AlwaysTruthy
18631863
| Type::PropertyInstance(_)
1864+
// `T` is always a subtype of itself,
1865+
// and `T` is always a subtype of `T | None`
1866+
| Type::TypeVar(_)
18641867
// might inherit `Any`, but subtyping is still reflexive
18651868
| Type::ClassLiteral(_)
18661869
=> true,
@@ -1872,7 +1875,6 @@ impl<'db> Type<'db> {
18721875
| Type::Union(_)
18731876
| Type::Intersection(_)
18741877
| Type::Callable(_)
1875-
| Type::TypeVar(_)
18761878
| Type::BoundSuper(_)
18771879
| Type::TypeIs(_)
18781880
| Type::TypeGuard(_)

crates/ty_python_semantic/src/types/relation.rs

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,17 @@ impl TypeRelation<'_> {
195195
pub(crate) const fn is_subtyping(self) -> bool {
196196
matches!(self, TypeRelation::Subtyping)
197197
}
198+
199+
pub(crate) const fn can_safely_assume_reflexivity(self, ty: Type) -> bool {
200+
match self {
201+
TypeRelation::Assignability
202+
| TypeRelation::ConstraintSetAssignability
203+
| TypeRelation::Redundancy => true,
204+
TypeRelation::Subtyping | TypeRelation::SubtypingAssuming(_) => {
205+
ty.subtyping_is_always_reflexive()
206+
}
207+
}
208+
}
198209
}
199210

200211
#[salsa::tracked]
@@ -329,7 +340,7 @@ impl<'db> Type<'db> {
329340
//
330341
// Note that we could do a full equivalence check here, but that would be both expensive
331342
// and unnecessary. This early return is only an optimisation.
332-
if (!relation.is_subtyping() || self.subtyping_is_always_reflexive()) && self == target {
343+
if relation.can_safely_assume_reflexivity(self) && self == target {
333344
return ConstraintSet::from(true);
334345
}
335346

@@ -460,44 +471,41 @@ impl<'db> Type<'db> {
460471
},
461472
}),
462473

463-
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
474+
// In general, a TypeVar `T` is not redundant with a type `S` unless one of the two conditions is satisfied:
464475
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
465476
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
466477
// 2. `T` is a constrained TypeVar and all of `T`'s constraints are subtypes of `S`.
467478
//
468479
// However, there is one exception to this general rule: for any given typevar `T`,
469480
// `T` will always be a subtype of any union containing `T`.
470-
(Type::TypeVar(bound_typevar), Type::Union(union))
471-
if !bound_typevar.is_inferable(db, inferable)
481+
(_, Type::Union(union))
482+
if relation.can_safely_assume_reflexivity(self)
472483
&& union.elements(db).contains(&self) =>
473484
{
474485
ConstraintSet::from(true)
475486
}
476487

477488
// A similar rule applies in reverse to intersection types.
478-
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
479-
if !bound_typevar.is_inferable(db, inferable)
489+
(Type::Intersection(intersection), _)
490+
if relation.can_safely_assume_reflexivity(target)
480491
&& intersection.positive(db).contains(&target) =>
481492
{
482493
ConstraintSet::from(true)
483494
}
484-
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
485-
if !bound_typevar.is_inferable(db, inferable)
486-
&& intersection.negative(db).contains(&target) =>
495+
(Type::Intersection(intersection), _)
496+
if relation.is_assignability()
497+
&& intersection.positive(db).iter().any(Type::is_dynamic) =>
487498
{
488-
ConstraintSet::from(false)
499+
// If the intersection contains `Any`/`Unknown`/`@Todo`, it is assignable to any type.
500+
// `Any` could materialize to `Never`, `Never & T & ~S` simplifies to `Never` for any
501+
// `T` and any `S`, and `Never` is a subtype of all types.
502+
ConstraintSet::from(true)
489503
}
490-
491-
// Two identical typevars must always solve to the same type, so they are always
492-
// subtypes of each other and assignable to each other.
493-
//
494-
// Note that this is not handled by the early return at the beginning of this method,
495-
// since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive.
496-
(Type::TypeVar(lhs_bound_typevar), Type::TypeVar(rhs_bound_typevar))
497-
if !lhs_bound_typevar.is_inferable(db, inferable)
498-
&& lhs_bound_typevar.is_same_typevar_as(db, rhs_bound_typevar) =>
504+
(Type::Intersection(intersection), _)
505+
if relation.can_safely_assume_reflexivity(target)
506+
&& intersection.negative(db).contains(&target) =>
499507
{
500-
ConstraintSet::from(true)
508+
ConstraintSet::from(false)
501509
}
502510

503511
// `type[T]` is a subtype of the class object `A` if every instance of `T` is a subtype of an instance

0 commit comments

Comments
 (0)