[ty] Introduce TypeRelation::Redundancy (#20602)

## Summary

The union `T | U` can be validly simplified to `U` iff:
1. `T` is a subtype of `U` OR
2. `T` is equivalent to `U` OR
3. `U` is a union and contains a type that is equivalent to `T` OR
4. `T` is an intersection and contains a type that is equivalent to `U`

(In practice, the only situation in which 2, 3 or 4 would be true when
(1) was not true would be if `T` or `U` is a dynamic type.)

Currently we achieve these simplifications in the union builder by doing
something along the lines of `t.is_subtype_of(db, u) ||
t.is_equivalent_to_(db, u) ||
t.into_intersection().is_some_and(|intersection|
intersection.positive(db).contains(&u)) ||
u.into_union().is_some_and(|union| union.elements(db).contains(&t))`.
But this is both slow and misses some cases (it doesn't simplify the
union `Any | (Unknown & ~None)` to `Any`, for example). We can improve
the consistency and performance of our union simplifications by adding a
third type relation that sits in between `TypeRelation::Subtyping` and
`TypeRelation::Assignability`: `TypeRelation::UnionSimplification`.

This change leads to simpler, more user-friendly types due to the more
consistent simplification. It also lead to a pretty huge performance
improvement!

## Test Plan

Existing tests, plus some new ones.
This commit is contained in:
Alex Waygood 2025-10-03 18:35:30 +01:00 committed by GitHub
parent 673167a565
commit c91b457044
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 281 additions and 92 deletions

View file

@ -138,6 +138,11 @@ static_assert(is_equivalent_to(Any, Any | Intersection[Any, str]))
static_assert(is_equivalent_to(Any, Intersection[str, Any] | Any))
static_assert(is_equivalent_to(Any, Any | Intersection[Any, Not[None]]))
static_assert(is_equivalent_to(Any, Intersection[Not[None], Any] | Any))
static_assert(is_equivalent_to(Any, Unknown | Intersection[Unknown, str]))
static_assert(is_equivalent_to(Any, Intersection[str, Unknown] | Unknown))
static_assert(is_equivalent_to(Any, Unknown | Intersection[Unknown, Not[None]]))
static_assert(is_equivalent_to(Any, Intersection[Not[None], Unknown] | Unknown))
```
## Tuples

View file

@ -306,3 +306,74 @@ def _(c: BC, d: BD):
reveal_type(c) # revealed: Literal[b""]
reveal_type(d) # revealed: Literal[b""]
```
## Unions of tuples
A union of a fixed-length tuple and a variable-length tuple must be collapsed to the variable-length
element, never to the fixed-length element (`tuple[()] | tuple[Any, ...]` -> `tuple[Any, ...]`, not
`tuple[()]`).
```py
from typing import Any
def f(
a: tuple[()] | tuple[int, ...],
b: tuple[int, ...] | tuple[()],
c: tuple[int] | tuple[str, ...],
d: tuple[str, ...] | tuple[int],
e: tuple[()] | tuple[Any, ...],
f: tuple[Any, ...] | tuple[()],
g: tuple[Any, ...] | tuple[Any | str, ...],
h: tuple[Any | str, ...] | tuple[Any, ...],
):
reveal_type(a) # revealed: tuple[int, ...]
reveal_type(b) # revealed: tuple[int, ...]
reveal_type(c) # revealed: tuple[int] | tuple[str, ...]
reveal_type(d) # revealed: tuple[str, ...] | tuple[int]
reveal_type(e) # revealed: tuple[Any, ...]
reveal_type(f) # revealed: tuple[Any, ...]
reveal_type(g) # revealed: tuple[Any | str, ...]
reveal_type(h) # revealed: tuple[Any | str, ...]
```
## Unions of other generic containers
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Any
class Bivariant[T]: ...
class Covariant[T]:
def get(self) -> T:
raise NotImplementedError
class Contravariant[T]:
def receive(self, input: T) -> None: ...
class Invariant[T]:
mutable_attribute: T
def _(
a: Bivariant[Any] | Bivariant[Any | str],
b: Bivariant[Any | str] | Bivariant[Any],
c: Covariant[Any] | Covariant[Any | str],
d: Covariant[Any | str] | Covariant[Any],
e: Contravariant[Any | str] | Contravariant[Any],
f: Contravariant[Any] | Contravariant[Any | str],
g: Invariant[Any] | Invariant[Any | str],
h: Invariant[Any | str] | Invariant[Any],
):
reveal_type(a) # revealed: Bivariant[Any]
reveal_type(b) # revealed: Bivariant[Any | str]
reveal_type(c) # revealed: Covariant[Any | str]
reveal_type(d) # revealed: Covariant[Any | str]
reveal_type(e) # revealed: Contravariant[Any]
reveal_type(f) # revealed: Contravariant[Any]
reveal_type(g) # revealed: Invariant[Any] | Invariant[Any | str]
reveal_type(h) # revealed: Invariant[Any | str] | Invariant[Any]
```