mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[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
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:
parent
af259faed5
commit
18eaa659c1
10 changed files with 696 additions and 191 deletions
|
@ -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]))
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue