[ty] Introduce a representation for the top/bottom materialization of an invariant generic (#20076)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

Part of #994. This adds a new field to the Specialization struct to
record when we're dealing with the top or bottom materialization of an
invariant generic. It also implements subtyping and assignability for
these objects.

Next planned steps after this is done are to implement other operations
on top/bottom materializations; probably attribute access is an
important one.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Jelle Zijlstra 2025-08-27 17:53:57 -07:00 committed by GitHub
parent af259faed5
commit 18eaa659c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 696 additions and 191 deletions

View file

@ -47,7 +47,7 @@ The invariant position is replaced with an unresolved type variable.
```py
def _(top_list: Top[list[Any]]):
reveal_type(top_list) # revealed: list[T_all]
reveal_type(top_list) # revealed: Top[list[Any]]
```
### Bottom materialization
@ -75,7 +75,7 @@ type variable.
```py
def _(bottom_list: Bottom[list[Any]]):
reveal_type(bottom_list) # revealed: list[T_all]
reveal_type(bottom_list) # revealed: Bottom[list[Any]]
```
## Fully static types
@ -230,14 +230,14 @@ def _(
top_aiu: Top[LTAnyIntUnknown],
bottom_aiu: Bottom[LTAnyIntUnknown],
):
reveal_type(top_ai) # revealed: list[tuple[T_all, int]]
reveal_type(bottom_ai) # revealed: list[tuple[T_all, int]]
reveal_type(top_ai) # revealed: Top[list[tuple[Any, int]]]
reveal_type(bottom_ai) # revealed: Bottom[list[tuple[Any, int]]]
reveal_type(top_su) # revealed: list[tuple[str, T_all]]
reveal_type(bottom_su) # revealed: list[tuple[str, T_all]]
reveal_type(top_su) # revealed: Top[list[tuple[str, Unknown]]]
reveal_type(bottom_su) # revealed: Bottom[list[tuple[str, Unknown]]]
reveal_type(top_aiu) # revealed: list[tuple[T_all, int, T_all]]
reveal_type(bottom_aiu) # revealed: list[tuple[T_all, int, T_all]]
reveal_type(top_aiu) # revealed: Top[list[tuple[Any, int, Unknown]]]
reveal_type(bottom_aiu) # revealed: Bottom[list[tuple[Any, int, Unknown]]]
```
## Union
@ -286,14 +286,14 @@ def _(
top_aiu: Top[list[Any | int | Unknown]],
bottom_aiu: Bottom[list[Any | int | Unknown]],
):
reveal_type(top_ai) # revealed: list[T_all | int]
reveal_type(bottom_ai) # revealed: list[T_all | int]
reveal_type(top_ai) # revealed: Top[list[Any | int]]
reveal_type(bottom_ai) # revealed: Bottom[list[Any | int]]
reveal_type(top_su) # revealed: list[str | T_all]
reveal_type(bottom_su) # revealed: list[str | T_all]
reveal_type(top_su) # revealed: Top[list[str | Unknown]]
reveal_type(bottom_su) # revealed: Bottom[list[str | Unknown]]
reveal_type(top_aiu) # revealed: list[T_all | int]
reveal_type(bottom_aiu) # revealed: list[T_all | int]
reveal_type(top_aiu) # revealed: Top[list[Any | int]]
reveal_type(bottom_aiu) # revealed: Bottom[list[Any | int]]
```
## Intersection
@ -320,8 +320,10 @@ def _(
top: Top[Intersection[list[Any], list[int]]],
bottom: Bottom[Intersection[list[Any], list[int]]],
):
reveal_type(top) # revealed: list[T_all] & list[int]
reveal_type(bottom) # revealed: list[T_all] & list[int]
# Top[list[Any] & list[int]] = Top[list[Any]] & list[int] = list[int]
reveal_type(top) # revealed: list[int]
# Bottom[list[Any] & list[int]] = Bottom[list[Any]] & list[int] = Bottom[list[Any]]
reveal_type(bottom) # revealed: Bottom[list[Any]]
```
## Negation (via `Not`)
@ -366,8 +368,8 @@ static_assert(is_equivalent_to(Bottom[type[int | Any]], type[int]))
# Here, `T` has an upper bound of `type`
def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]):
reveal_type(top) # revealed: list[T_all]
reveal_type(bottom) # revealed: list[T_all]
reveal_type(top) # revealed: Top[list[type[Any]]]
reveal_type(bottom) # revealed: Bottom[list[type[Any]]]
```
## Type variables
@ -427,8 +429,8 @@ class GenericContravariant(Generic[T_contra]):
pass
def _(top: Top[GenericInvariant[Any]], bottom: Bottom[GenericInvariant[Any]]):
reveal_type(top) # revealed: GenericInvariant[T_all]
reveal_type(bottom) # revealed: GenericInvariant[T_all]
reveal_type(top) # revealed: Top[GenericInvariant[Any]]
reveal_type(bottom) # revealed: Bottom[GenericInvariant[Any]]
static_assert(is_equivalent_to(Top[GenericCovariant[Any]], GenericCovariant[object]))
static_assert(is_equivalent_to(Bottom[GenericCovariant[Any]], GenericCovariant[Never]))
@ -448,8 +450,8 @@ type CovariantCallable = Callable[[GenericCovariant[Any]], None]
type ContravariantCallable = Callable[[GenericContravariant[Any]], None]
def invariant(top: Top[InvariantCallable], bottom: Bottom[InvariantCallable]) -> None:
reveal_type(top) # revealed: (GenericInvariant[T_all], /) -> None
reveal_type(bottom) # revealed: (GenericInvariant[T_all], /) -> None
reveal_type(top) # revealed: (Bottom[GenericInvariant[Any]], /) -> None
reveal_type(bottom) # revealed: (Top[GenericInvariant[Any]], /) -> None
def covariant(top: Top[CovariantCallable], bottom: Bottom[CovariantCallable]) -> None:
reveal_type(top) # revealed: (GenericCovariant[Never], /) -> None
@ -492,3 +494,207 @@ def _(
bottom_1: Bottom[1], # error: [invalid-type-form]
): ...
```
## Nested use
`Top[T]` and `Bottom[T]` are always fully static types. Therefore, they have only one
materialization (themselves) and applying `Top` or `Bottom` again does nothing.
```py
from typing import Any
from ty_extensions import Top, Bottom, static_assert, is_equivalent_to
static_assert(is_equivalent_to(Top[Top[list[Any]]], Top[list[Any]]))
static_assert(is_equivalent_to(Bottom[Top[list[Any]]], Top[list[Any]]))
static_assert(is_equivalent_to(Bottom[Bottom[list[Any]]], Bottom[list[Any]]))
static_assert(is_equivalent_to(Top[Bottom[list[Any]]], Bottom[list[Any]]))
```
## Subtyping
Any `list[T]` is a subtype of `Top[list[Any]]`, but with more restrictive gradual types, not all
other specializations are subtypes.
```py
from typing import Any, Literal
from ty_extensions import is_subtype_of, static_assert, Top, Intersection, Bottom
# None and Top
static_assert(is_subtype_of(list[int], Top[list[Any]]))
static_assert(not is_subtype_of(Top[list[Any]], list[int]))
static_assert(is_subtype_of(list[bool], Top[list[Intersection[int, Any]]]))
static_assert(is_subtype_of(list[int], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(list[int | str], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(list[object], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(list[str], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(list[str | bool], Top[list[Intersection[int, Any]]]))
# Top and Top
static_assert(is_subtype_of(Top[list[int | Any]], Top[list[Any]]))
static_assert(not is_subtype_of(Top[list[Any]], Top[list[int | Any]]))
static_assert(is_subtype_of(Top[list[Intersection[int, Any]]], Top[list[Any]]))
static_assert(not is_subtype_of(Top[list[Any]], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(Top[list[Intersection[int, Any]]], Top[list[int | Any]]))
static_assert(not is_subtype_of(Top[list[int | Any]], Top[list[Intersection[int, Any]]]))
static_assert(not is_subtype_of(Top[list[str | Any]], Top[list[int | Any]]))
static_assert(is_subtype_of(Top[list[str | int | Any]], Top[list[int | Any]]))
static_assert(not is_subtype_of(Top[list[int | Any]], Top[list[str | int | Any]]))
# Bottom and Top
static_assert(is_subtype_of(Bottom[list[Any]], Top[list[Any]]))
static_assert(is_subtype_of(Bottom[list[Any]], Top[list[int | Any]]))
static_assert(is_subtype_of(Bottom[list[int | Any]], Top[list[Any]]))
static_assert(is_subtype_of(Bottom[list[int | Any]], Top[list[int | str]]))
static_assert(is_subtype_of(Bottom[list[Intersection[int, Any]]], Top[list[Intersection[str, Any]]]))
static_assert(not is_subtype_of(Bottom[list[Intersection[int, bool | Any]]], Bottom[list[Intersection[str, Literal["x"] | Any]]]))
# None and None
static_assert(not is_subtype_of(list[int], list[Any]))
static_assert(not is_subtype_of(list[Any], list[int]))
static_assert(is_subtype_of(list[int], list[int]))
static_assert(not is_subtype_of(list[int], list[object]))
static_assert(not is_subtype_of(list[object], list[int]))
# Top and None
static_assert(not is_subtype_of(Top[list[Any]], list[Any]))
static_assert(not is_subtype_of(Top[list[Any]], list[int]))
static_assert(is_subtype_of(Top[list[int]], list[int]))
# Bottom and None
static_assert(is_subtype_of(Bottom[list[Any]], list[object]))
static_assert(is_subtype_of(Bottom[list[int | Any]], list[str | int]))
static_assert(not is_subtype_of(Bottom[list[str | Any]], list[Intersection[int, bool | Any]]))
# None and Bottom
static_assert(not is_subtype_of(list[int], Bottom[list[Any]]))
static_assert(not is_subtype_of(list[int], Bottom[list[int | Any]]))
static_assert(is_subtype_of(list[int], Bottom[list[int]]))
# Top and Bottom
static_assert(not is_subtype_of(Top[list[Any]], Bottom[list[Any]]))
static_assert(not is_subtype_of(Top[list[int | Any]], Bottom[list[int | Any]]))
static_assert(is_subtype_of(Top[list[int]], Bottom[list[int]]))
# Bottom and Bottom
static_assert(is_subtype_of(Bottom[list[Any]], Bottom[list[int | str | Any]]))
static_assert(is_subtype_of(Bottom[list[int | Any]], Bottom[list[int | str | Any]]))
static_assert(is_subtype_of(Bottom[list[bool | Any]], Bottom[list[int | Any]]))
static_assert(not is_subtype_of(Bottom[list[int | Any]], Bottom[list[bool | Any]]))
static_assert(not is_subtype_of(Bottom[list[int | Any]], Bottom[list[Any]]))
```
## Assignability
### General
Assignability is the same as subtyping for top and bottom materializations, because those are fully
static types, but some gradual types are assignable even if they are not subtypes.
```py
from typing import Any, Literal
from ty_extensions import is_assignable_to, static_assert, Top, Intersection, Bottom
# None and Top
static_assert(is_assignable_to(list[Any], Top[list[Any]]))
static_assert(is_assignable_to(list[int], Top[list[Any]]))
static_assert(not is_assignable_to(Top[list[Any]], list[int]))
static_assert(is_assignable_to(list[bool], Top[list[Intersection[int, Any]]]))
static_assert(is_assignable_to(list[int], Top[list[Intersection[int, Any]]]))
static_assert(is_assignable_to(list[Any], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(list[int | str], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(list[object], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(list[str], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(list[str | bool], Top[list[Intersection[int, Any]]]))
# Top and Top
static_assert(is_assignable_to(Top[list[int | Any]], Top[list[Any]]))
static_assert(not is_assignable_to(Top[list[Any]], Top[list[int | Any]]))
static_assert(is_assignable_to(Top[list[Intersection[int, Any]]], Top[list[Any]]))
static_assert(not is_assignable_to(Top[list[Any]], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(Top[list[Intersection[int, Any]]], Top[list[int | Any]]))
static_assert(not is_assignable_to(Top[list[int | Any]], Top[list[Intersection[int, Any]]]))
static_assert(not is_assignable_to(Top[list[str | Any]], Top[list[int | Any]]))
static_assert(is_assignable_to(Top[list[str | int | Any]], Top[list[int | Any]]))
static_assert(not is_assignable_to(Top[list[int | Any]], Top[list[str | int | Any]]))
# Bottom and Top
static_assert(is_assignable_to(Bottom[list[Any]], Top[list[Any]]))
static_assert(is_assignable_to(Bottom[list[Any]], Top[list[int | Any]]))
static_assert(is_assignable_to(Bottom[list[int | Any]], Top[list[Any]]))
static_assert(is_assignable_to(Bottom[list[Intersection[int, Any]]], Top[list[Intersection[str, Any]]]))
static_assert(
not is_assignable_to(Bottom[list[Intersection[int, bool | Any]]], Bottom[list[Intersection[str, Literal["x"] | Any]]])
)
# None and None
static_assert(is_assignable_to(list[int], list[Any]))
static_assert(is_assignable_to(list[Any], list[int]))
static_assert(is_assignable_to(list[int], list[int]))
static_assert(not is_assignable_to(list[int], list[object]))
static_assert(not is_assignable_to(list[object], list[int]))
# Top and None
static_assert(is_assignable_to(Top[list[Any]], list[Any]))
static_assert(not is_assignable_to(Top[list[Any]], list[int]))
static_assert(is_assignable_to(Top[list[int]], list[int]))
# Bottom and None
static_assert(is_assignable_to(Bottom[list[Any]], list[object]))
static_assert(is_assignable_to(Bottom[list[int | Any]], Top[list[str | int]]))
static_assert(not is_assignable_to(Bottom[list[str | Any]], list[Intersection[int, bool | Any]]))
# None and Bottom
static_assert(is_assignable_to(list[Any], Bottom[list[Any]]))
static_assert(not is_assignable_to(list[int], Bottom[list[Any]]))
static_assert(not is_assignable_to(list[int], Bottom[list[int | Any]]))
static_assert(is_assignable_to(list[int], Bottom[list[int]]))
# Top and Bottom
static_assert(not is_assignable_to(Top[list[Any]], Bottom[list[Any]]))
static_assert(not is_assignable_to(Top[list[int | Any]], Bottom[list[int | Any]]))
static_assert(is_assignable_to(Top[list[int]], Bottom[list[int]]))
# Bottom and Bottom
static_assert(is_assignable_to(Bottom[list[Any]], Bottom[list[int | str | Any]]))
static_assert(is_assignable_to(Bottom[list[int | Any]], Bottom[list[int | str | Any]]))
static_assert(is_assignable_to(Bottom[list[bool | Any]], Bottom[list[int | Any]]))
static_assert(not is_assignable_to(Bottom[list[int | Any]], Bottom[list[bool | Any]]))
static_assert(not is_assignable_to(Bottom[list[int | Any]], Bottom[list[Any]]))
```
### Subclasses with different variance
We need to take special care when an invariant class inherits from a covariant or contravariant one.
This comes up frequently in practice because `list` (invariant) inherits from `Sequence` and a
number of other covariant ABCs, but we'll use a synthetic example.
```py
from typing import Generic, TypeVar, Any
from ty_extensions import static_assert, is_assignable_to, is_equivalent_to, Top
class A:
pass
class B(A):
pass
T_co = TypeVar("T_co", covariant=True)
T = TypeVar("T")
class CovariantBase(Generic[T_co]):
def get(self) -> T_co:
raise NotImplementedError
class InvariantChild(CovariantBase[T]):
def push(self, obj: T) -> None: ...
static_assert(is_assignable_to(InvariantChild[A], CovariantBase[A]))
static_assert(is_assignable_to(InvariantChild[B], CovariantBase[A]))
static_assert(not is_assignable_to(InvariantChild[A], CovariantBase[B]))
static_assert(not is_assignable_to(InvariantChild[B], InvariantChild[A]))
static_assert(is_equivalent_to(Top[CovariantBase[Any]], CovariantBase[object]))
static_assert(is_assignable_to(InvariantChild[Any], CovariantBase[A]))
static_assert(not is_assignable_to(Top[InvariantChild[Any]], CovariantBase[A]))
```