Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(..)
Expand All @@ -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,
Expand All @@ -1872,7 +1875,6 @@ impl<'db> Type<'db> {
| Type::Union(_)
| Type::Intersection(_)
| Type::Callable(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_)
| Type::TypeGuard(_)
Expand Down
48 changes: 28 additions & 20 deletions crates/ty_python_semantic/src/types/relation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -460,44 +471,41 @@ 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)
&& intersection.negative(db).contains(&target) =>
(Type::Intersection(intersection), _)
if relation.is_assignability()
&& intersection.positive(db).iter().any(Type::is_dynamic) =>
{
ConstraintSet::from(false)
// 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)
}

// 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) =>
(Type::Intersection(intersection), _)
if relation.can_safely_assume_reflexivity(target)
&& intersection.negative(db).contains(&target) =>
{
ConstraintSet::from(true)
ConstraintSet::from(false)
}

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