[ty] Homogeneous and mixed tuples (#18600)
Some checks are pending
CI / cargo fuzz build (push) Blocked by required conditions
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 / 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

We already had support for homogeneous tuples (`tuple[int, ...]`). This
PR extends this to also support mixed tuples (`tuple[str, str,
*tuple[int, ...], str str]`).

A mixed tuple consists of a fixed-length (possibly empty) prefix and
suffix, and a variable-length portion in the middle. Every element of
the variable-length portion must be of the same type. A homogeneous
tuple is then just a mixed tuple with an empty prefix and suffix.

The new data representation uses different Rust types for a fixed-length
(aka heterogeneous) tuple. Another option would have been to use the
`VariableLengthTuple` representation for all tuples, and to wrap the
"variable + suffix" portion in an `Option`. I don't think that would
simplify the method implementations much, though, since we would still
have a 2×2 case analysis for most of them.

One wrinkle is that the definition of the `tuple` class in the typeshed
has a single typevar, and canonically represents a homogeneous tuple.
When getting the class of a tuple instance, that means that we have to
summarize our detailed mixed tuple type information into its
"homogeneous supertype". (We were already doing this for heterogeneous
types.)

A similar thing happens when concatenating two mixed tuples: the
variable-length portion and suffix of the LHS, and the prefix and
variable-length portion of the RHS, all get unioned into the
variable-length portion of the result. The LHS prefix and RHS suffix
carry through unchanged.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Douglas Creager 2025-06-20 18:23:54 -04:00 committed by GitHub
parent d9266284df
commit ea812d0813
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 2432 additions and 758 deletions

View file

@ -58,7 +58,7 @@ reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
reveal_type(e) # revealed: tuple[str, ...]
reveal_type(f) # revealed: @Todo(PEP 646)
reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes]
reveal_type(g) # revealed: @Todo(PEP 646)
reveal_type(h) # revealed: tuple[list[int], list[int]]

View file

@ -1722,7 +1722,7 @@ d = True
reveal_type(d.__class__) # revealed: <class 'bool'>
e = (42, 42)
reveal_type(e.__class__) # revealed: <class 'tuple'>
reveal_type(e.__class__) # revealed: <class 'tuple[Literal[42], Literal[42]]'>
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
reveal_type(a.__class__) # revealed: type[int]

View file

@ -17,6 +17,32 @@ def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
```py
def _(x: tuple[int, ...], y: tuple[str, ...]):
reveal_type(x + x) # revealed: tuple[int, ...]
reveal_type(x + y) # revealed: tuple[int | str, ...]
reveal_type(x + (1, 2)) # revealed: tuple[int, ...]
reveal_type((1, 2) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]]
reveal_type(x + (3, 4)) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]]
reveal_type((1, 2) + x + (3, 4)) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]]
reveal_type((1, 2) + y + (3, 4) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]]
```
We get the same results even when we use a legacy type alias, even though this involves first
inferring the `tuple[...]` expression as a value form. (Doing so gives a generic alias of the
`tuple` type, but as a special case, we include the full detailed tuple element specification in
specializations of `tuple`.)
```py
from typing import Literal
OneTwo = tuple[Literal[1], Literal[2]]
ThreeFour = tuple[Literal[3], Literal[4]]
IntTuple = tuple[int, ...]
StrTuple = tuple[str, ...]
def _(one_two: OneTwo, x: IntTuple, y: StrTuple, three_four: ThreeFour):
reveal_type(x + x) # revealed: tuple[int, ...]
reveal_type(x + y) # revealed: tuple[int | str, ...]
reveal_type(one_two + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]]
reveal_type(x + three_four) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]]
reveal_type(one_two + x + three_four) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]]
reveal_type(one_two + y + three_four + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]]
```

View file

@ -130,6 +130,44 @@ reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int
reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str
```
## Inferring tuple parameter types
```toml
[environment]
python-version = "3.12"
```
```py
from typing import TypeVar
T = TypeVar("T")
def takes_mixed_tuple_suffix(x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T:
return x[-2]
# TODO: revealed: Literal[True]
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
def takes_mixed_tuple_prefix(x: tuple[int, T, *tuple[str, ...], bool, int]) -> T:
return x[1]
# TODO: revealed: Literal[b"foo"]
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
def takes_fixed_tuple(x: tuple[T, int]) -> T:
return x[0]
reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True]
def takes_homogeneous_tuple(x: tuple[T, ...]) -> T:
return x[0]
# TODO: revealed: Literal[42]
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown
# TODO: revealed: Literal[42, 43]
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown
```
## Inferring a bound typevar
<!-- snapshot-diagnostics -->

View file

@ -125,6 +125,35 @@ reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int
reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str
```
## Inferring tuple parameter types
```py
def takes_mixed_tuple_suffix[T](x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T:
return x[-2]
# TODO: revealed: Literal[True]
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
def takes_mixed_tuple_prefix[T](x: tuple[int, T, *tuple[str, ...], bool, int]) -> T:
return x[1]
# TODO: revealed: Literal[b"foo"]
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
def takes_fixed_tuple[T](x: tuple[T, int]) -> T:
return x[0]
reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True]
def takes_homogeneous_tuple[T](x: tuple[T, ...]) -> T:
return x[0]
# TODO: revealed: Literal[42]
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown
# TODO: revealed: Literal[42, 43]
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown
```
## Inferring a bound typevar
<!-- snapshot-diagnostics -->

View file

@ -192,10 +192,9 @@ def _(t1: tuple[int | None, int | None], t2: tuple[int, int] | tuple[None, None]
reveal_type(t1[1]) # revealed: int | None
if t2[0] is not None:
reveal_type(t2[0]) # revealed: int
# TODO: should be int
reveal_type(t2[0]) # revealed: Unknown & ~None
# TODO: should be int
reveal_type(t2[1]) # revealed: Unknown
reveal_type(t2[1]) # revealed: int | None
```
### String subscript

View file

@ -215,12 +215,12 @@ def _(a: tuple[str, int] | tuple[int, str], c: C[Any]):
# TODO: Should be `tuple[int, str]`
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
# TODO: Should be `str`
reveal_type(a[1]) # revealed: Unknown
reveal_type(a[1]) # revealed: str | int
if reveal_type(is_int(a[0])): # revealed: TypeIs[int @ a[0]]
# TODO: Should be `tuple[int, str]`
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
reveal_type(a[0]) # revealed: Unknown & int
reveal_type(a[0]) # revealed: int
# TODO: Should be `TypeGuard[str @ c.v]`
if reveal_type(guard_str(c.v)): # revealed: @Todo(`TypeGuard[]` special form)

View file

@ -69,8 +69,64 @@ def _(m: int, n: int):
t[::0] # error: [zero-stepsize-in-slice]
tuple_slice = t[m:n]
# TODO: Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: tuple[Unknown, ...]
reveal_type(tuple_slice) # revealed: tuple[Literal[1, "a", b"b"] | None, ...]
```
## Slices of homogeneous and mixed tuples
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Literal
def homogeneous(t: tuple[str, ...]) -> None:
reveal_type(t[0]) # revealed: str
reveal_type(t[1]) # revealed: str
reveal_type(t[2]) # revealed: str
reveal_type(t[3]) # revealed: str
reveal_type(t[-1]) # revealed: str
reveal_type(t[-2]) # revealed: str
reveal_type(t[-3]) # revealed: str
reveal_type(t[-4]) # revealed: str
def mixed(s: tuple[str, ...]) -> None:
t = (1, 2, 3) + s + (8, 9, 10)
reveal_type(t[0]) # revealed: Literal[1]
reveal_type(t[1]) # revealed: Literal[2]
reveal_type(t[2]) # revealed: Literal[3]
reveal_type(t[3]) # revealed: str | Literal[8]
reveal_type(t[4]) # revealed: str | Literal[8, 9]
reveal_type(t[5]) # revealed: str | Literal[8, 9, 10]
reveal_type(t[-1]) # revealed: Literal[10]
reveal_type(t[-2]) # revealed: Literal[9]
reveal_type(t[-3]) # revealed: Literal[8]
reveal_type(t[-4]) # revealed: Literal[3] | str
reveal_type(t[-5]) # revealed: Literal[2, 3] | str
reveal_type(t[-6]) # revealed: Literal[1, 2, 3] | str
```
## `tuple` as generic alias
For tuple instances, we can track more detailed information about the length and element types of
the tuple. This information carries over to the generic alias that the tuple is an instance of.
```py
def _(a: tuple, b: tuple[int], c: tuple[int, str], d: tuple[int, ...]) -> None:
reveal_type(a) # revealed: tuple[Unknown, ...]
reveal_type(b) # revealed: tuple[int]
reveal_type(c) # revealed: tuple[int, str]
reveal_type(d) # revealed: tuple[int, ...]
reveal_type(tuple) # revealed: <class 'tuple'>
reveal_type(tuple[int]) # revealed: <class 'tuple[int]'>
reveal_type(tuple[int, str]) # revealed: <class 'tuple[int, str]'>
reveal_type(tuple[int, ...]) # revealed: <class 'tuple[int, ...]'>
```
## Inheritance
@ -83,8 +139,13 @@ python-version = "3.9"
```py
class A(tuple[int, str]): ...
# revealed: tuple[<class 'A'>, <class 'tuple[@Todo(Generic tuple specializations), ...]'>, <class 'Sequence[@Todo(Generic tuple specializations)]'>, <class 'Reversible[@Todo(Generic tuple specializations)]'>, <class 'Collection[@Todo(Generic tuple specializations)]'>, <class 'Iterable[@Todo(Generic tuple specializations)]'>, <class 'Container[@Todo(Generic tuple specializations)]'>, typing.Protocol, typing.Generic, <class 'object'>]
# revealed: tuple[<class 'A'>, <class 'tuple[int, str]'>, <class 'Sequence[int | str]'>, <class 'Reversible[int | str]'>, <class 'Collection[int | str]'>, <class 'Iterable[int | str]'>, <class 'Container[int | str]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(A.__mro__)
class C(tuple): ...
# revealed: tuple[<class 'C'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(C.__mro__)
```
## `typing.Tuple`
@ -109,9 +170,19 @@ def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]):
Inheriting from `Tuple` results in a MRO with `builtins.tuple` and `typing.Generic`. `Tuple` itself
is not a class.
```toml
[environment]
python-version = "3.9"
```
```py
from typing import Tuple
class A(Tuple[int, str]): ...
# revealed: tuple[<class 'A'>, <class 'tuple[int, str]'>, <class 'Sequence[int | str]'>, <class 'Reversible[int | str]'>, <class 'Collection[int | str]'>, <class 'Iterable[int | str]'>, <class 'Container[int | str]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(A.__mro__)
class C(Tuple): ...
# revealed: tuple[<class 'C'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]

View file

@ -16,6 +16,28 @@ def _(p: P, q: Q):
assert_type((p, q), tuple[P, Q])
```
## Instantiating tuples
Like all classes, tuples can be instantiated by invoking the `tuple` class. When instantiating a
specialization of `tuple` we (TODO: should) check that the values passed in match the element types
defined in the specialization.
```py
# TODO: revealed: tuple[()]
reveal_type(tuple()) # revealed: tuple[Unknown, ...]
# TODO: revealed: tuple[Literal[1]]
reveal_type(tuple([1])) # revealed: tuple[Unknown, ...]
reveal_type(tuple[int]([1])) # revealed: tuple[int]
# TODO: error for invalid arguments
reveal_type(tuple[int, str]([1])) # revealed: tuple[int, str]
reveal_type(().__class__()) # revealed: tuple[()]
# TODO: error for invalid arguments
reveal_type((1,).__class__()) # revealed: tuple[Literal[1]]
# TODO: error for invalid arguments
reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]]
```
## Subtyping relationships
The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1`
@ -60,10 +82,7 @@ class AnotherEmptyTuple(tuple[()]): ...
static_assert(not is_equivalent_to(AnotherEmptyTuple, tuple[()]))
# TODO: These should not be errors
# error: [static-assert-error]
static_assert(is_subtype_of(AnotherEmptyTuple, tuple[()]))
# error: [static-assert-error]
static_assert(is_assignable_to(AnotherEmptyTuple, tuple[()]))
```
@ -158,8 +177,6 @@ class NotAlwaysTruthyTuple(tuple[int]):
def __bool__(self) -> bool:
return False
# TODO: This assignment should be allowed
# error: [invalid-assignment]
t: tuple[int] = NotAlwaysTruthyTuple((1,))
```

View file

@ -303,6 +303,11 @@ static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str]))
## Assignability of heterogeneous tuple types to homogeneous tuple types
```toml
[environment]
python-version = "3.12"
```
While a homogeneous tuple type is not assignable to any heterogeneous tuple types, a heterogeneous
tuple type can be assignable to a homogeneous tuple type, and homogeneous tuple types can be
assignable to `Sequence`:
@ -312,6 +317,11 @@ from typing import Literal, Any, Sequence
from ty_extensions import static_assert, is_assignable_to, Not, AlwaysFalsy
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1, 2], ...]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[Literal[2], ...]]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[*tuple[Literal[1], ...], Literal[2]]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[str, ...], Literal[2]]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[2], *tuple[str, ...]]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[*tuple[str, ...], Literal[1], Literal[2]]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, ...]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int | str, ...]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Any, ...]))
@ -330,6 +340,218 @@ static_assert(is_assignable_to(tuple[()], Sequence[int]))
static_assert(not is_assignable_to(tuple[int, int], tuple[str, ...]))
```
## Assignability of two mixed tuple types
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Literal, Any, Sequence
from ty_extensions import static_assert, is_assignable_to, Not, AlwaysFalsy
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]],
)
)
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...]],
)
)
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], *tuple[int, ...], Literal[10]],
)
)
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], *tuple[int, ...]],
)
)
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[*tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[*tuple[int, ...], Literal[10]],
)
)
static_assert(
is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[*tuple[int, ...]],
)
)
static_assert(
not is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_assignable_to(
tuple[Literal[1], Literal[2], *tuple[int, ...]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_assignable_to(
tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_assignable_to(
tuple[Literal[1], *tuple[int, ...], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_assignable_to(
tuple[Literal[1], *tuple[int, ...]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_assignable_to(
tuple[*tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_assignable_to(
tuple[*tuple[int, ...], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_assignable_to(
tuple[*tuple[int, ...]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
```
## Assignability of the gradual tuple
```toml
[environment]
python-version = "3.12"
```
As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type, which
is assignable to every tuple of any length.
```py
from typing import Any
from ty_extensions import static_assert, is_assignable_to
static_assert(is_assignable_to(tuple[Any, ...], tuple[Any, ...]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[Any]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[int, ...]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[int]))
static_assert(is_assignable_to(tuple[Any, ...], tuple[int, int]))
```
This also applies when `tuple[Any, ...]` is unpacked into a mixed tuple.
```py
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any, ...]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[int, ...]]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, ...]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, int]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[*tuple[Any, ...], int]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any, ...]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[*tuple[int, ...], int]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, ...]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int]))
static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, int]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[Any, ...], int]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any, ...]))
static_assert(not is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[int, ...], int]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, ...]))
static_assert(not is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int]))
static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, int]))
```
The same is not true of fully static tuple types, since an unbounded homogeneous tuple is defined to
be the _union_ of all tuple lengths, not the _gradual choice_ of them.
```py
static_assert(is_assignable_to(tuple[int, ...], tuple[Any, ...]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[Any]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[int, ...], tuple[int, ...]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[int]))
static_assert(not is_assignable_to(tuple[int, ...], tuple[int, int]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, *tuple[Any, ...]]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any, ...]))
static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any]))
static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, *tuple[int, ...]]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, ...]))
static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int]))
static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, int]))
static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[*tuple[Any, ...], int]))
static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any, ...]))
static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any]))
static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[*tuple[int, ...], int]))
static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[int, ...]))
static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[int]))
static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[int, int]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[Any, ...], int]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any, ...]))
static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any]))
static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any, Any]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[int, ...], int]))
static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, ...]))
static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int]))
static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, int]))
```
## Union types
```py
@ -828,8 +1050,8 @@ sets of possible materializations -- if they represent the same sets of possible
sets of sets of possible runtime objects). By this principle `int | Any` is gradually equivalent to
`Unknown | int`, since they have exactly the same sets of posisble materializations. But
`bool | Any` is not equivalent to `int`, since there are many possible materializations of
`bool | Any` that are not assignable to `int`. It is therefore *not* necessary for `X` to be
gradually equivalent to `Y` in order for `Foo[X]` to be assignable to `Foo[Y]`; it is *only*
`bool | Any` that are not assignable to `int`. It is therefore _not_ necessary for `X` to be
gradually equivalent to `Y` in order for `Foo[X]` to be assignable to `Foo[Y]`; it is _only_
necessary for `X` and `Y` to be mutually assignable.
```py
@ -887,4 +1109,6 @@ static_assert(not is_assignable_to(TypeGuard[Unknown], str)) # error: [static-a
static_assert(not is_assignable_to(TypeIs[Any], str))
```
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form
[gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form
[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

View file

@ -48,8 +48,7 @@ static_assert(not is_fully_static(Any | str))
static_assert(not is_fully_static(str | Unknown))
static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]]))
# TODO: should pass
static_assert(not is_fully_static(tuple[Any, ...])) # error: [static-assert-error]
static_assert(not is_fully_static(tuple[Any, ...]))
static_assert(not is_fully_static(tuple[int, Any]))
static_assert(not is_fully_static(type[Any]))

View file

@ -159,6 +159,11 @@ from typing import Literal, Any, Sequence
from ty_extensions import static_assert, is_subtype_of, Not, AlwaysFalsy
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1, 2], ...]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[Literal[2], ...]]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[*tuple[Literal[1], ...], Literal[2]]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[str, ...], Literal[2]]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[2], *tuple[str, ...]]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[*tuple[str, ...], Literal[1], Literal[2]]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[int, ...]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[int | str, ...]))
static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Not[AlwaysFalsy], ...]))
@ -177,6 +182,215 @@ static_assert(not is_subtype_of(tuple[int, ...], Sequence[Any]))
static_assert(not is_subtype_of(tuple[Any, ...], Sequence[int]))
```
## Subtyping of two mixed tuple types
```py
from typing import Literal, Any, Sequence
from ty_extensions import static_assert, is_subtype_of, Not, AlwaysFalsy
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]],
)
)
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...]],
)
)
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], *tuple[int, ...], Literal[10]],
)
)
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], *tuple[int, ...]],
)
)
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[*tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[*tuple[int, ...], Literal[10]],
)
)
static_assert(
is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
tuple[*tuple[int, ...]],
)
)
static_assert(
not is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_subtype_of(
tuple[Literal[1], Literal[2], *tuple[int, ...]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_subtype_of(
tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_subtype_of(
tuple[Literal[1], *tuple[int, ...], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_subtype_of(
tuple[Literal[1], *tuple[int, ...]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_subtype_of(
tuple[*tuple[int, ...], Literal[9], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_subtype_of(
tuple[*tuple[int, ...], Literal[10]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
static_assert(
not is_subtype_of(
tuple[*tuple[int, ...]],
tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]],
)
)
```
## Subtyping of the gradual tuple
```toml
[environment]
python-version = "3.12"
```
As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type.
However, the special-case behavior of assignability does not also apply to subtyping, since gradual
types to not participate in subtyping.
```py
from typing import Any
from ty_extensions import static_assert, is_subtype_of
static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any]))
static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, Any]))
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int]))
static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, int]))
```
Subtyping also does not apply when `tuple[Any, ...]` is unpacked into a mixed tuple.
```py
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any, Any]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[int, ...]]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, int]))
static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[*tuple[Any, ...], int]))
static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any]))
static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any, Any]))
static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[*tuple[int, ...], int]))
static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int]))
static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int, int]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[Any, ...], int]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any, Any]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[int, ...], int]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int]))
static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, int]))
```
Subtyping does apply to unbounded homogeneous tuples of a fully static type. However, such tuples
are defined to be the _union_ of all tuple lengths, not the _gradual choice_ of them, so no
variable-length tuples are a subtyping of _any_ fixed-length tuple.
```py
static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[int, ...], tuple[Any]))
static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, Any]))
static_assert(is_subtype_of(tuple[int, ...], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[int, ...], tuple[int]))
static_assert(not is_subtype_of(tuple[int, ...], tuple[int, int]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, *tuple[Any, ...]]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any, Any]))
static_assert(is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, *tuple[int, ...]]))
static_assert(is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, int]))
static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[*tuple[Any, ...], int]))
static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any]))
static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any, Any]))
static_assert(is_subtype_of(tuple[*tuple[int, ...], int], tuple[*tuple[int, ...], int]))
static_assert(is_subtype_of(tuple[*tuple[int, ...], int], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[int]))
static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[int, int]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[Any, ...], int]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any, ...]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any, Any]))
static_assert(is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[int, ...], int]))
static_assert(is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, ...]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int]))
static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, int]))
```
## Union types
```py
@ -1639,5 +1853,7 @@ static_assert(is_subtype_of(CallableTypeOf[overload_ab], CallableTypeOf[overload
static_assert(is_subtype_of(CallableTypeOf[overload_ba], CallableTypeOf[overload_ab]))
```
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form
[gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form
[special case for float and complex]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence

View file

@ -7,6 +7,7 @@ cpython # too many cycle iterations
hydpy # too many iterations
ibis # too many iterations
jax # too many iterations
mypy # too many iterations (self-recursive type alias)
packaging # too many iterations
pandas # slow (9s)
pandera # stack overflow

View file

@ -60,7 +60,6 @@ mkdocs
mkosi
mongo-python-driver
more-itertools
mypy
mypy-protobuf
mypy_primer
nionutils

View file

@ -51,6 +51,7 @@ use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::signatures::{Parameter, ParameterForm, Parameters};
use crate::types::tuple::{TupleSpec, TupleType};
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
use crate::{Db, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
@ -78,6 +79,7 @@ mod slots;
mod special_form;
mod string_annotation;
mod subclass_of;
mod tuple;
mod type_ordering;
mod unpacker;
@ -543,8 +545,10 @@ pub enum Type<'db> {
LiteralString,
/// A bytes literal
BytesLiteral(BytesLiteralType<'db>),
/// A heterogeneous tuple type, with elements of the given types in source order.
// TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`.
/// An instance of the builtin `tuple` class.
/// TODO: Consider removing this in favor of `NominalInstance`. This is currently stored as a
/// separate variant partly for historical reasons, and partly to allow us to easily
/// distinguish tuples since they occur so often.
Tuple(TupleType<'db>),
/// An instance of a typevar in a generic class or function. When the generic class or function
/// is specialized, we will replace this typevar with its specialization.
@ -720,13 +724,7 @@ impl<'db> Type<'db> {
.map(|ty| ty.materialize(db, variance.flip())),
)
.build(),
Type::Tuple(tuple_type) => TupleType::from_elements(
db,
tuple_type
.elements(db)
.iter()
.map(|ty| ty.materialize(db, variance)),
),
Type::Tuple(tuple_type) => Type::tuple(db, tuple_type.materialize(db, variance)),
Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
Type::TypeIs(type_is) => {
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
@ -770,8 +768,8 @@ impl<'db> Type<'db> {
Self::Tuple(tuple) => TupleType::from_elements(
db,
tuple
.elements(db)
.iter()
.tuple(db)
.all_elements()
.map(|ty| ty.replace_self_reference(db, class)),
),
@ -881,8 +879,8 @@ impl<'db> Type<'db> {
}
Self::Tuple(tuple) => tuple
.elements(db)
.iter()
.tuple(db)
.all_elements()
.any(|ty| ty.any_over_type(db, type_fn)),
Self::Union(union) => union
@ -1076,13 +1074,6 @@ impl<'db> Type<'db> {
.expect("Expected a Type::IntLiteral variant")
}
pub const fn into_tuple(self) -> Option<TupleType<'db>> {
match self {
Type::Tuple(tuple_type) => Some(tuple_type),
_ => None,
}
}
pub const fn is_boolean_literal(&self) -> bool {
matches!(self, Type::BooleanLiteral(..))
}
@ -1141,7 +1132,7 @@ impl<'db> Type<'db> {
match self {
Type::Union(union) => Type::Union(union.normalized(db)),
Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)),
Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)),
Type::Tuple(tuple) => Type::tuple(db, tuple.normalized(db)),
Type::Callable(callable) => Type::Callable(callable.normalized(db)),
Type::ProtocolInstance(protocol) => protocol.normalized(db),
Type::NominalInstance(instance) => Type::NominalInstance(instance.normalized(db)),
@ -1441,27 +1432,23 @@ impl<'db> Type<'db> {
false
}
// A fully static heterogeneous tuple type `A` is a subtype of a fully static heterogeneous tuple type `B`
// iff the two tuple types have the same number of elements and each element-type in `A` is a subtype
// of the element-type at the same index in `B`. (Now say that 5 times fast.)
//
// For example: `tuple[bool, bool]` is a subtype of `tuple[int, int]`,
// but `tuple[bool, bool, bool]` is not a subtype of `tuple[int, int]`
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
let self_elements = self_tuple.elements(db);
let target_elements = target_tuple.elements(db);
self_elements.len() == target_elements.len()
&& self_elements.iter().zip(target_elements).all(
|(self_element, target_element)| {
self_element.has_relation_to(db, *target_element, relation)
},
)
self_tuple.has_relation_to(db, target_tuple, relation)
}
// `tuple[A, B, C]` is a subtype of `tuple[A | B | C, ...]`
(Type::Tuple(tuple), _) => tuple
.homogeneous_supertype(db)
.has_relation_to(db, target, relation),
(Type::Tuple(self_tuple), Type::NominalInstance(target_instance)) => {
self_tuple.to_class_type(db).is_some_and(|self_class| {
self_class.has_relation_to(db, target_instance.class, relation)
})
}
(Type::NominalInstance(self_instance), Type::Tuple(target_tuple)) => {
target_tuple.to_class_type(db).is_some_and(|target_class| {
self_instance
.class
.has_relation_to(db, target_class, relation)
})
}
(Type::Tuple(_), _) => false,
(Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target),
(Type::BoundSuper(_), _) => KnownClass::Super
@ -1961,14 +1948,15 @@ impl<'db> Type<'db> {
!known_instance.is_instance_of(db, instance.class)
}
(
known_instance_ty @ (Type::SpecialForm(_) | Type::KnownInstance(_)),
Type::Tuple(tuple),
)
| (
Type::Tuple(tuple),
known_instance_ty @ (Type::SpecialForm(_) | Type::KnownInstance(_)),
) => known_instance_ty.is_disjoint_from(db, tuple.homogeneous_supertype(db)),
(Type::SpecialForm(special_form), Type::Tuple(tuple))
| (Type::Tuple(tuple), Type::SpecialForm(special_form)) => tuple
.to_class_type(db)
.is_some_and(|tuple_class| !special_form.is_instance_of(db, tuple_class)),
(Type::KnownInstance(known_instance), Type::Tuple(tuple))
| (Type::Tuple(tuple), Type::KnownInstance(known_instance)) => tuple
.to_class_type(db)
.is_some_and(|tuple_class| !known_instance.is_instance_of(db, tuple_class)),
(Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance))
| (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => {
@ -2113,18 +2101,14 @@ impl<'db> Type<'db> {
}
(Type::Tuple(tuple), Type::Tuple(other_tuple)) => {
let self_elements = tuple.elements(db);
let other_elements = other_tuple.elements(db);
self_elements.len() != other_elements.len()
|| self_elements
.iter()
.zip(other_elements)
.any(|(e1, e2)| e1.is_disjoint_from(db, *e2))
tuple.is_disjoint_from(db, other_tuple)
}
(Type::Tuple(tuple), instance @ Type::NominalInstance(_))
| (instance @ Type::NominalInstance(_), Type::Tuple(tuple)) => {
instance.is_disjoint_from(db, tuple.homogeneous_supertype(db))
(Type::Tuple(tuple), Type::NominalInstance(instance))
| (Type::NominalInstance(instance), Type::Tuple(tuple)) => {
tuple.to_class_type(db).is_some_and(|tuple_class| {
instance.is_disjoint_from_nominal_instance_of_class(db, tuple_class)
})
}
(Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => {
@ -2203,10 +2187,7 @@ impl<'db> Type<'db> {
// containing gradual forms such as `tuple[Any, ...]`.
// Conversely, make sure to return `true` for homogeneous tuples such as
// `tuple[int, ...]`, once we add support for them.
Type::Tuple(tuple) => tuple
.elements(db)
.iter()
.all(|elem| elem.is_fully_static(db)),
Type::Tuple(tuple) => tuple.is_fully_static(db),
Type::Callable(callable) => callable.is_fully_static(db),
Type::TypeIs(type_is) => type_is.return_type(db).is_fully_static(db),
}
@ -2379,11 +2360,7 @@ impl<'db> Type<'db> {
false
}
Type::Tuple(tuple) => tuple
.elements(db)
.iter()
.all(|elem| elem.is_single_valued(db)),
Type::Tuple(tuple) => tuple.is_single_valued(db),
Type::NominalInstance(instance) => instance.is_single_valued(db),
Type::BoundSuper(_) => {
@ -2629,7 +2606,10 @@ impl<'db> Type<'db> {
KnownClass::Str.to_instance(db).instance_member(db, name)
}
Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).instance_member(db, name),
Type::Tuple(tuple) => tuple.homogeneous_supertype(db).instance_member(db, name),
Type::Tuple(tuple) => tuple
.to_class_type(db)
.map(|class| class.instance_member(db, name))
.unwrap_or(Place::Unbound.into()),
Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name),
Type::ModuleLiteral(_) => KnownClass::ModuleType
@ -3474,7 +3454,7 @@ impl<'db> Type<'db> {
Type::BooleanLiteral(bool) => Truthiness::from(*bool),
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()),
Type::Tuple(tuple) => Truthiness::from(!tuple.tuple(db).is_empty()),
};
Ok(truthiness)
@ -3505,7 +3485,11 @@ impl<'db> Type<'db> {
let usize_len = match self {
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
Type::StringLiteral(string) => Some(string.python_len(db)),
Type::Tuple(tuple) => Some(tuple.len(db)),
Type::Tuple(tuple) => match tuple.tuple(db) {
TupleSpec::Fixed(tuple) => Some(tuple.len()),
TupleSpec::Variable(_) => None,
},
_ => None,
};
@ -3705,10 +3689,7 @@ impl<'db> Type<'db> {
db,
[
KnownClass::Str.to_instance(db),
KnownClass::Tuple.to_specialized_instance(
db,
[KnownClass::Str.to_instance(db)],
),
TupleType::homogeneous(db, KnownClass::Str.to_instance(db)),
],
)),
Parameter::positional_only(Some(Name::new_static("start")))
@ -4022,10 +4003,10 @@ impl<'db> Type<'db> {
Parameter::positional_only(Some(Name::new_static("name")))
.with_annotated_type(str_instance),
Parameter::positional_only(Some(Name::new_static("bases")))
.with_annotated_type(
KnownClass::Tuple
.to_specialized_instance(db, [type_instance]),
),
.with_annotated_type(TupleType::homogeneous(
db,
type_instance,
)),
Parameter::positional_only(Some(Name::new_static("dict")))
.with_annotated_type(
KnownClass::Dict.to_specialized_instance(
@ -4173,16 +4154,16 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any())
.type_form(),
Parameter::keyword_only(Name::new_static("type_params"))
.with_annotated_type(KnownClass::Tuple.to_specialized_instance(
.with_annotated_type(TupleType::homogeneous(
db,
[UnionType::from_elements(
UnionType::from_elements(
db,
[
KnownClass::TypeVar.to_instance(db),
KnownClass::ParamSpec.to_instance(db),
KnownClass::TypeVarTuple.to_instance(db),
],
)],
),
))
.with_default_type(TupleType::empty(db)),
]),
@ -4476,7 +4457,10 @@ impl<'db> Type<'db> {
/// ```
fn try_iterate(self, db: &'db dyn Db) -> Result<Type<'db>, IterationError<'db>> {
if let Type::Tuple(tuple_type) = self {
return Ok(UnionType::from_elements(db, tuple_type.elements(db)));
return Ok(UnionType::from_elements(
db,
tuple_type.tuple(db).all_elements(),
));
}
if let Type::GenericAlias(alias) = self {
@ -4639,20 +4623,20 @@ impl<'db> Type<'db> {
// have the class's typevars still in the method signature when we attempt to call it. To
// do this, we instead use the _identity_ specialization, which maps each of the class's
// generic typevars to itself.
let (generic_origin, generic_context, self_type) = match self {
Type::ClassLiteral(class) => match class.generic_context(db) {
Some(generic_context) => {
let specialization = generic_context.identity_specialization(db);
(
let (generic_origin, generic_context, self_type) =
match self {
Type::ClassLiteral(class) => match class.generic_context(db) {
Some(generic_context) => (
Some(class),
Some(generic_context),
Type::GenericAlias(GenericAlias::new(db, class, specialization)),
)
}
Type::from(class.apply_specialization(db, |_| {
generic_context.identity_specialization(db)
})),
),
_ => (None, None, self),
},
_ => (None, None, self),
},
_ => (None, None, self),
};
};
// As of now we do not model custom `__call__` on meta-classes, so the code below
// only deals with interplay between `__new__` and `__init__` methods.
@ -4775,11 +4759,7 @@ impl<'db> Type<'db> {
.map(|specialization| {
Type::instance(
db,
ClassType::Generic(GenericAlias::new(
db,
generic_origin,
specialization,
)),
generic_origin.apply_specialization(db, |_| specialization),
)
})
.unwrap_or(instance_ty);
@ -4966,7 +4946,7 @@ impl<'db> Type<'db> {
// We treat `typing.Type` exactly the same as `builtins.type`:
SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)),
SpecialFormType::Tuple => Ok(KnownClass::Tuple.to_instance(db)),
SpecialFormType::Tuple => Ok(TupleType::homogeneous(db, Type::unknown())),
// Legacy `typing` aliases
SpecialFormType::List => Ok(KnownClass::List.to_instance(db)),
@ -5189,7 +5169,10 @@ impl<'db> Type<'db> {
}
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db),
Type::Tuple(tuple) => tuple
.to_class_type(db)
.map(Type::from)
.unwrap_or_else(Type::unknown),
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => KnownClass::Type.to_instance(db),
@ -5343,12 +5326,7 @@ impl<'db> Type<'db> {
}
builder.build()
}
Type::Tuple(tuple) => TupleType::from_elements(
db,
tuple
.iter(db)
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
),
Type::Tuple(tuple) => Type::Tuple(tuple.apply_type_mapping(db, type_mapping)),
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)),
@ -5439,10 +5417,9 @@ impl<'db> Type<'db> {
negative.find_legacy_typevars(db, typevars);
}
}
Type::Tuple(tuple) => {
for element in tuple.iter(db) {
element.find_legacy_typevars(db, typevars);
}
tuple.find_legacy_typevars(db, typevars);
}
Type::GenericAlias(alias) => {
@ -8134,89 +8111,6 @@ impl<'db> BytesLiteralType<'db> {
}
}
/// # Ordering
/// Ordering is based on the tuple's salsa-assigned id and not on its elements.
/// The id may change between runs, or when the tuple was garbage collected and recreated.
#[salsa::interned(debug)]
#[derive(PartialOrd, Ord)]
pub struct TupleType<'db> {
#[returns(deref)]
elements: Box<[Type<'db>]>,
}
impl<'db> TupleType<'db> {
fn homogeneous_supertype(self, db: &'db dyn Db) -> Type<'db> {
KnownClass::Tuple
.to_specialized_instance(db, [UnionType::from_elements(db, self.elements(db))])
}
pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> {
Type::Tuple(TupleType::new(db, Box::<[Type<'db>]>::from([])))
}
pub(crate) fn from_elements<T: Into<Type<'db>>>(
db: &'db dyn Db,
types: impl IntoIterator<Item = T>,
) -> Type<'db> {
let mut elements = vec![];
for ty in types {
let ty = ty.into();
if ty.is_never() {
return Type::Never;
}
elements.push(ty);
}
Type::Tuple(Self::new(db, elements.into_boxed_slice()))
}
/// Return a normalized version of `self`.
///
/// See [`Type::normalized`] for more details.
#[must_use]
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
let elements: Box<[Type<'db>]> = self
.elements(db)
.iter()
.map(|ty| ty.normalized(db))
.collect();
TupleType::new(db, elements)
}
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_elements = self.elements(db);
let other_elements = other.elements(db);
self_elements.len() == other_elements.len()
&& self_elements
.iter()
.zip(other_elements)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
}
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_elements = self.elements(db);
let other_elements = other.elements(db);
self_elements.len() == other_elements.len()
&& self_elements
.iter()
.zip(other_elements)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
pub fn get(&self, db: &'db dyn Db, index: usize) -> Option<Type<'db>> {
self.elements(db).get(index).copied()
}
pub fn len(&self, db: &'db dyn Db) -> usize {
self.elements(db).len()
}
pub fn iter(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> + 'db + '_ {
self.elements(db).iter().copied()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum BoundSuperError<'db> {
InvalidPivotClassType {

View file

@ -4,7 +4,8 @@ use std::ops::{Deref, DerefMut};
use itertools::{Either, Itertools};
use crate::Db;
use crate::types::{KnownClass, TupleType};
use crate::types::KnownClass;
use crate::types::tuple::{TupleSpec, TupleType};
use super::Type;
@ -210,11 +211,15 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
Type::BooleanLiteral(false),
])
}
Type::Tuple(tuple) => {
Type::Tuple(tuple_type) => {
// Note: This should only account for tuples of known length, i.e., `tuple[bool, ...]`
// should not be expanded here.
let tuple = tuple_type.tuple(db);
if !matches!(tuple, TupleSpec::Fixed(_)) {
return None;
}
let expanded = tuple
.iter(db)
.all_elements()
.map(|element| {
if let Some(expanded) = expand_type(db, element) {
Either::Left(expanded.into_iter())
@ -242,7 +247,8 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
#[cfg(test)]
mod tests {
use crate::db::tests::setup_db;
use crate::types::{KnownClass, TupleType, Type, UnionType};
use crate::types::tuple::TupleType;
use crate::types::{KnownClass, Type, UnionType};
use super::expand_type;
@ -308,7 +314,6 @@ mod tests {
TupleType::from_elements(&db, [false_ty, bytes_ty]),
];
let expanded = expand_type(&db, tuple_type2).unwrap();
assert_eq!(expanded.len(), expected_types.len());
assert_eq!(expanded, expected_types);
// Mixed set of elements where some can be expanded while others cannot be.
@ -328,7 +333,16 @@ mod tests {
TupleType::from_elements(&db, [false_ty, int_ty, bytes_ty, str_ty]),
];
let expanded = expand_type(&db, tuple_type3).unwrap();
assert_eq!(expanded.len(), expected_types.len());
assert_eq!(expanded, expected_types);
// Variable-length tuples are not expanded.
let variable_length_tuple = TupleType::mixed(
&db,
[bool_ty],
int_ty,
[UnionType::from_elements(&db, [str_ty, bytes_ty]), str_ty],
);
let expanded = expand_type(&db, variable_length_tuple);
assert!(expanded.is_none());
}
}

View file

@ -27,9 +27,10 @@ use crate::types::function::{
};
use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError};
use crate::types::signatures::{Parameter, ParameterForm};
use crate::types::tuple::TupleType;
use crate::types::{
BoundMethodType, ClassLiteral, DataclassParams, KnownClass, KnownInstanceType,
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TupleType, TypeMapping, UnionType,
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType,
WrapperDescriptorKind, ide_support, todo_type,
};
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};

View file

@ -12,6 +12,7 @@ use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleType;
use crate::types::{
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance,
};
@ -30,8 +31,8 @@ use crate::{
place_table, semantic_index, use_def_map,
},
types::{
CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, TupleType, UnionBuilder,
UnionType, definition_expression_type,
CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType,
definition_expression_type,
},
};
use indexmap::IndexSet;
@ -203,6 +204,8 @@ impl<'db> GenericAlias<'db> {
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
// A tuple's specialization will include all of its element types, so we don't need to also
// look in `self.tuple`.
self.specialization(db).find_legacy_typevars(db, typevars);
}
}
@ -761,58 +764,46 @@ impl<'db> ClassLiteral<'db> {
index.expect_single_definition(body_scope.node(db).expect_class(&module))
}
pub(crate) fn apply_specialization(
self,
db: &'db dyn Db,
f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>,
) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = f(generic_context);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
}
pub(crate) fn apply_optional_specialization(
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
) -> ClassType<'db> {
match (self.generic_context(db), specialization) {
(None, _) => ClassType::NonGeneric(self),
(Some(generic_context), None) => {
let specialization = generic_context.default_specialization(db);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
(Some(_), Some(specialization)) => {
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
self.apply_specialization(db, |generic_context| {
specialization.unwrap_or_else(|| generic_context.default_specialization(db))
})
}
/// Returns the default specialization of this class. For non-generic classes, the class is
/// returned unchanged. For a non-specialized generic class, we return a generic alias that
/// applies the default specialization to the class's typevars.
pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = generic_context.default_specialization(db);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
}
/// Returns a specialization of this class with a `@Todo`-type
pub(crate) fn todo_specialization(self, db: &'db dyn Db, todo: &'static str) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = generic_context.todo_specialization(db, todo);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
self.apply_specialization(db, |generic_context| {
generic_context.default_specialization(db)
})
}
/// Returns the unknown specialization of this class. For non-generic classes, the class is
/// returned unchanged. For a non-specialized generic class, we return a generic alias that
/// maps each of the class's typevars to `Unknown`.
pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = generic_context.unknown_specialization(db);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
self.apply_specialization(db, |generic_context| {
generic_context.unknown_specialization(db)
})
}
/// Return an iterator over the inferred types of this class's *explicit* bases.
@ -2448,22 +2439,20 @@ impl<'db> KnownClass {
.unwrap_or_else(Type::unknown)
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
/// representing all possible instances of the generic class with a specialization.
/// Lookup a generic [`KnownClass`] in typeshed and return a [`Type`]
/// representing a specialization of that class.
///
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
/// number of types, a debug-level log message will be emitted stating this.
pub(crate) fn to_specialized_instance(
pub(crate) fn to_specialized_class_type(
self,
db: &'db dyn Db,
specialization: impl IntoIterator<Item = Type<'db>>,
) -> Type<'db> {
) -> Option<ClassType<'db>> {
let Type::ClassLiteral(class_literal) = self.to_class_literal(db) else {
return Type::unknown();
};
let Some(generic_context) = class_literal.generic_context(db) else {
return Type::instance(db, ClassType::NonGeneric(class_literal));
return None;
};
let generic_context = class_literal.generic_context(db)?;
let types = specialization.into_iter().collect::<Box<[_]>>();
if types.len() != generic_context.len(db) {
@ -2477,21 +2466,32 @@ impl<'db> KnownClass {
self.display(db)
);
}
return Type::instance(db, class_literal.default_specialization(db));
return Some(class_literal.default_specialization(db));
}
let specialization = generic_context.specialize(db, types);
Type::instance(
db,
ClassType::Generic(GenericAlias::new(db, class_literal, specialization)),
)
Some(class_literal.apply_specialization(db, |_| generic_context.specialize(db, types)))
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
/// representing all possible instances of the generic class with a specialization.
///
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
/// number of types, a debug-level log message will be emitted stating this.
pub(crate) fn to_specialized_instance(
self,
db: &'db dyn Db,
specialization: impl IntoIterator<Item = Type<'db>>,
) -> Type<'db> {
self.to_specialized_class_type(db, specialization)
.and_then(|class_type| Type::from(class_type).to_instance(db))
.unwrap_or_else(Type::unknown)
}
/// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// Return an error if the symbol cannot be found in the expected typeshed module,
/// or if the symbol is not a class definition, or if the symbol is possibly unbound.
pub(crate) fn try_to_class_literal(
fn try_to_class_literal_without_logging(
self,
db: &'db dyn Db,
) -> Result<ClassLiteral<'db>, KnownClassLookupError<'db>> {
@ -2511,14 +2511,13 @@ impl<'db> KnownClass {
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
pub(crate) fn try_to_class_literal(self, db: &'db dyn Db) -> Option<ClassLiteral<'db>> {
// a cache of the `KnownClass`es that we have already failed to lookup in typeshed
// (and therefore that we've already logged a warning for)
static MESSAGES: LazyLock<Mutex<FxHashSet<KnownClass>>> = LazyLock::new(Mutex::default);
self.try_to_class_literal(db)
.map(Type::ClassLiteral)
.unwrap_or_else(|lookup_error| {
self.try_to_class_literal_without_logging(db)
.or_else(|lookup_error| {
if MESSAGES.lock().unwrap().insert(self) {
if matches!(
lookup_error,
@ -2535,12 +2534,22 @@ impl<'db> KnownClass {
match lookup_error {
KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => {
class_literal.into()
Ok(class_literal)
}
KnownClassLookupError::ClassNotFound { .. }
| KnownClassLookupError::SymbolNotAClass { .. } => Type::unknown(),
| KnownClassLookupError::SymbolNotAClass { .. } => Err(()),
}
})
.ok()
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
self.try_to_class_literal(db)
.map(Type::ClassLiteral)
.unwrap_or_else(Type::unknown)
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
@ -2557,7 +2566,7 @@ impl<'db> KnownClass {
/// Return `true` if this symbol can be resolved to a class definition `class` in typeshed,
/// *and* `class` is a subclass of `other`.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
self.try_to_class_literal(db)
self.try_to_class_literal_without_logging(db)
.is_ok_and(|class| class.is_subclass_of(db, None, other))
}

View file

@ -68,6 +68,7 @@ impl<'db> ClassBase<'db> {
if literal.is_known(db, KnownClass::Any) {
Some(Self::Dynamic(DynamicType::Any))
} else if literal.is_known(db, KnownClass::NamedTuple) {
// TODO: Figure out the tuple spec for the named tuple
Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db))
} else {
Some(Self::Class(literal.default_specialization(db)))

View file

@ -14,6 +14,7 @@ use crate::types::string_annotation::{
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::tuple::TupleType;
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
use crate::{Db, Module, ModuleName, Program, declare_lint};
use itertools::Itertools;
@ -1606,7 +1607,7 @@ pub(super) fn report_index_out_of_bounds(
kind: &'static str,
node: AnyNodeRef,
tuple_ty: Type,
length: usize,
length: impl std::fmt::Display,
index: i64,
) {
let Some(builder) = context.report_lint(&INDEX_OUT_OF_BOUNDS, node) else {
@ -2120,7 +2121,7 @@ pub(crate) fn report_invalid_or_unsupported_base(
return;
}
let tuple_of_types = KnownClass::Tuple.to_specialized_instance(db, [instance_of_type]);
let tuple_of_types = TupleType::homogeneous(db, instance_of_type);
let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| {
diagnostic.info(

View file

@ -10,6 +10,7 @@ use crate::types::class::{ClassLiteral, ClassType, GenericAlias};
use crate::types::function::{FunctionType, OverloadLiteral};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleSpec;
use crate::types::{
CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, StringLiteralType,
SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType,
@ -190,16 +191,7 @@ impl Display for DisplayRepresentation<'_> {
escape.bytes_repr(TripleQuotes::No).write(f)
}
Type::Tuple(tuple) => {
f.write_str("tuple[")?;
let elements = tuple.elements(self.db);
if elements.is_empty() {
f.write_str("()")?;
} else {
elements.display(self.db).fmt(f)?;
}
f.write_str("]")
}
Type::Tuple(specialization) => specialization.tuple(self.db).display(self.db).fmt(f),
Type::TypeVar(typevar) => f.write_str(typevar.name(self.db)),
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
@ -224,6 +216,67 @@ impl Display for DisplayRepresentation<'_> {
}
}
impl<'db> TupleSpec<'db> {
pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayTuple<'db> {
DisplayTuple { tuple: self, db }
}
}
pub(crate) struct DisplayTuple<'db> {
tuple: &'db TupleSpec<'db>,
db: &'db dyn Db,
}
impl Display for DisplayTuple<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("tuple[")?;
match self.tuple {
TupleSpec::Fixed(tuple) => {
let elements = tuple.elements_slice();
if elements.is_empty() {
f.write_str("()")?;
} else {
elements.display(self.db).fmt(f)?;
}
}
// Decoder key for which snippets of text need to be included depending on whether
// the tuple contains a prefix and/or suffix:
//
// tuple[ yyy, ... ]
// tuple[xxx, *tuple[yyy, ...] ]
// tuple[xxx, *tuple[yyy, ...], zzz]
// tuple[ *tuple[yyy, ...], zzz]
// PPPPPPPPPPPP P
// SSSSSSS SSSSSS
//
// (Anything that appears above only a P is included only if there's a prefix; anything
// above only an S is included only if there's a suffix; anything about both a P and an
// S is included if there is either a prefix or a suffix. The initial `tuple[` and
// trailing `]` are printed elsewhere. The `yyy, ...` is printed no matter what.)
TupleSpec::Variable(tuple) => {
if !tuple.prefix.is_empty() {
tuple.prefix.display(self.db).fmt(f)?;
f.write_str(", ")?;
}
if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() {
f.write_str("*tuple[")?;
}
tuple.variable.display(self.db).fmt(f)?;
f.write_str(", ...")?;
if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() {
f.write_str("]")?;
}
if !tuple.suffix.is_empty() {
f.write_str(", ")?;
tuple.suffix.display(self.db).fmt(f)?;
}
}
}
f.write_str("]")
}
}
impl<'db> OverloadLiteral<'db> {
// Not currently used, but useful for debugging.
#[expect(dead_code)]
@ -307,15 +360,19 @@ pub(crate) struct DisplayGenericAlias<'db> {
impl Display for DisplayGenericAlias<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{origin}{specialization}",
origin = self.origin.name(self.db),
specialization = self.specialization.display_short(
self.db,
TupleSpecialization::from_class(self.db, self.origin)
),
)
if self.origin.is_known(self.db, KnownClass::Tuple) {
self.specialization.tuple(self.db).display(self.db).fmt(f)
} else {
write!(
f,
"{origin}{specialization}",
origin = self.origin.name(self.db),
specialization = self.specialization.display_short(
self.db,
TupleSpecialization::from_class(self.db, self.origin)
),
)
}
}
}

View file

@ -8,9 +8,10 @@ use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarVariance, UnionType, declaration_type, todo_type,
TypeVarVariance, UnionType, declaration_type,
};
use crate::{Db, FxOrderSet};
@ -143,20 +144,6 @@ impl<'db> GenericContext<'db> {
self.specialize_partial(db, &vec![None; self.variables(db).len()])
}
#[allow(unused_variables)] // Only unused in release builds
pub(crate) fn todo_specialization(
self,
db: &'db dyn Db,
todo: &'static str,
) -> Specialization<'db> {
let types = self
.variables(db)
.iter()
.map(|typevar| typevar.default_ty(db).unwrap_or(todo_type!(todo)))
.collect();
self.specialize(db, types)
}
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
let types = self
.variables(db)
@ -185,7 +172,17 @@ impl<'db> GenericContext<'db> {
types: Box<[Type<'db>]>,
) -> Specialization<'db> {
assert!(self.variables(db).len() == types.len());
Specialization::new(db, self, types)
Specialization::new(db, self, types, None)
}
/// Creates a specialization of this generic context for the `tuple` class.
pub(crate) fn specialize_tuple(
self,
db: &'db dyn Db,
tuple: TupleType<'db>,
) -> Specialization<'db> {
let element_type = UnionType::from_elements(db, tuple.tuple(db).all_elements());
Specialization::new(db, self, Box::from([element_type]), Some(tuple))
}
/// Creates a specialization of this generic context. Panics if the length of `types` does not
@ -230,7 +227,7 @@ impl<'db> GenericContext<'db> {
expanded[idx] = default;
}
Specialization::new(db, self, expanded.into_boxed_slice())
Specialization::new(db, self, expanded.into_boxed_slice(), None)
}
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
@ -273,9 +270,24 @@ pub struct Specialization<'db> {
pub(crate) generic_context: GenericContext<'db>,
#[returns(deref)]
pub(crate) types: Box<[Type<'db>]>,
/// For specializations of `tuple`, we also store more detailed information about the tuple's
/// elements, above what the class's (single) typevar can represent.
tuple_inner: Option<TupleType<'db>>,
}
impl<'db> Specialization<'db> {
/// Returns the tuple spec for a specialization of the `tuple` class.
pub(crate) fn tuple(self, db: &'db dyn Db) -> &'db TupleSpec<'db> {
if let Some(tuple) = self.tuple_inner(db).map(|tuple_type| tuple_type.tuple(db)) {
return tuple;
}
if let [element_type] = self.types(db) {
return TupleType::new(db, TupleSpec::homogeneous(*element_type)).tuple(db);
}
TupleType::new(db, TupleSpec::homogeneous(Type::unknown())).tuple(db)
}
/// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this
/// mapping.
pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option<Type<'db>> {
@ -313,7 +325,10 @@ impl<'db> Specialization<'db> {
.iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect();
Specialization::new(db, self.generic_context(db), types)
let tuple_inner = self
.tuple_inner(db)
.map(|tuple| tuple.apply_type_mapping(db, type_mapping));
Specialization::new(db, self.generic_context(db), types, tuple_inner)
}
/// Applies an optional specialization to this specialization.
@ -350,12 +365,14 @@ impl<'db> Specialization<'db> {
_ => UnionType::from_elements(db, [self_type, other_type]),
})
.collect();
Specialization::new(db, self.generic_context(db), types)
// TODO: Combine the tuple specs too
Specialization::new(db, self.generic_context(db), types, None)
}
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
let types: Box<[_]> = self.types(db).iter().map(|ty| ty.normalized(db)).collect();
Self::new(db, self.generic_context(db), types)
let tuple_inner = self.tuple_inner(db).map(|tuple| tuple.normalized(db));
Self::new(db, self.generic_context(db), types, tuple_inner)
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
@ -374,7 +391,11 @@ impl<'db> Specialization<'db> {
vartype.materialize(db, variance)
})
.collect();
Specialization::new(db, self.generic_context(db), types)
let tuple_inner = self.tuple_inner(db).map(|tuple| {
// Tuples are immutable, so tuple element types are always in covariant position.
tuple.materialize(db, variance)
});
Specialization::new(db, self.generic_context(db), types, tuple_inner)
}
pub(crate) fn has_relation_to(
@ -388,6 +409,11 @@ impl<'db> Specialization<'db> {
return false;
}
if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db))
{
return self_tuple.has_relation_to(db, other_tuple, relation);
}
for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db))
.zip(other.types(db))
@ -570,7 +596,8 @@ impl<'db> SpecializationBuilder<'db> {
.unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown()))
})
.collect();
Specialization::new(self.db, generic_context, types)
// TODO Infer the tuple spec for a tuple type
Specialization::new(self.db, generic_context, types, None)
}
fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) {
@ -641,14 +668,19 @@ impl<'db> SpecializationBuilder<'db> {
}
(Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => {
let formal_elements = formal_tuple.elements(self.db);
let actual_elements = actual_tuple.elements(self.db);
if formal_elements.len() == actual_elements.len() {
for (formal_element, actual_element) in
formal_elements.iter().zip(actual_elements)
{
self.infer(*formal_element, *actual_element)?;
let formal_tuple = formal_tuple.tuple(self.db);
let actual_tuple = actual_tuple.tuple(self.db);
match (formal_tuple, actual_tuple) {
(TupleSpec::Fixed(formal_tuple), TupleSpec::Fixed(actual_tuple)) => {
if formal_tuple.len() == actual_tuple.len() {
for (formal_element, actual_element) in formal_tuple.elements().zip(actual_tuple.elements()) {
self.infer(formal_element, actual_element)?;
}
}
}
// TODO: Infer specializations of variable-length tuples
(TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) => {}
}
}

View file

@ -117,11 +117,16 @@ impl AllMembers {
| Type::KnownInstance(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_) => {
if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) {
| Type::TypeIs(_) => match ty.to_meta_type(db) {
Type::ClassLiteral(class_literal) => {
self.extend_with_class_members(db, class_literal);
}
}
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
self.extend_with_class_members(db, class_literal);
}
_ => {}
},
Type::ModuleLiteral(literal) => {
self.extend_with_type(db, KnownClass::ModuleType.to_instance(db));

View file

@ -95,15 +95,16 @@ use crate::types::function::{
use crate::types::generics::GenericContext;
use crate::types::mro::MroErrorKind;
use crate::types::signatures::{CallableSignature, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
BareTypeAliasType, CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, GenericAlias, IntersectionBuilder, IntersectionType, KnownClass,
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, StringLiteralType,
SubclassOfType, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType,
LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness,
Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeIsType, TypeQualifiers,
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder,
UnionType, binding_type, todo_type,
};
use crate::unpack::{Unpack, UnpackPosition};
use crate::util::subscript::{PyIndex, PySlice};
@ -2443,7 +2444,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo_type!("PEP 646")
} else {
let annotated_type = self.file_expression_type(annotation);
KnownClass::Tuple.to_specialized_instance(self.db(), [annotated_type])
TupleType::homogeneous(self.db(), annotated_type)
};
self.add_declaration_with_binding(
@ -2455,7 +2456,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_binding(
parameter.into(),
definition,
KnownClass::Tuple.to_specialized_instance(self.db(), [Type::unknown()]),
TupleType::homogeneous(self.db(), Type::unknown()),
);
}
}
@ -2832,7 +2833,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`.
let symbol_ty = if let Type::Tuple(tuple) = node_ty {
let mut builder = UnionBuilder::new(self.db());
for element in tuple.elements(self.db()).iter().copied() {
for element in tuple.tuple(self.db()).all_elements() {
builder = builder.add(
if element.is_assignable_to(self.db(), type_base_exception) {
element.to_instance(self.db()).expect(
@ -2855,7 +2856,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)
} else if node_ty.is_assignable_to(
self.db(),
KnownClass::Tuple.to_specialized_instance(self.db(), [type_base_exception]),
TupleType::homogeneous(self.db(), type_base_exception),
) {
extract_tuple_specialization(self.db(), node_ty)
.unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db()))
@ -2865,7 +2866,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.db(),
[
type_base_exception,
KnownClass::Tuple.to_specialized_instance(self.db(), [type_base_exception]),
TupleType::homogeneous(self.db(), type_base_exception),
],
),
) {
@ -3698,9 +3699,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::Expr::List(ast::ExprList { elts, .. })
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
let mut assigned_tys = match assigned_ty {
Some(Type::Tuple(tuple)) => {
Either::Left(tuple.elements(self.db()).iter().copied())
}
Some(Type::Tuple(tuple)) => Either::Left(tuple.tuple(self.db()).all_elements()),
Some(_) | None => Either::Right(std::iter::empty()),
};
@ -6940,6 +6939,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitXor) => {
Some(Type::BooleanLiteral(b1 ^ b2))
}
(Type::BooleanLiteral(b1), Type::BooleanLiteral(_) | Type::IntLiteral(_), op) => self
.infer_binary_expression_type(
node,
@ -6956,19 +6956,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::IntLiteral(i64::from(b2)),
op,
),
(Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => {
// Note: this only works on heterogeneous tuples.
let lhs_elements = lhs.elements(self.db());
let rhs_elements = rhs.elements(self.db());
Some(TupleType::from_elements(
(Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => Some(Type::tuple(
self.db(),
TupleType::new(
self.db(),
lhs_elements
.iter()
.copied()
.chain(rhs_elements.iter().copied()),
))
}
lhs.tuple(self.db()).concat(self.db(), rhs.tuple(self.db())),
),
)),
// We've handled all of the special cases that we support for literals, so we need to
// fall back on looking for dunder methods on one of the operand types.
@ -7425,19 +7420,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// tuples.
//
// Ref: https://github.com/astral-sh/ruff/pull/18251#discussion_r2115909311
if tuple.len(self.db()) > 1 << 12 {
let (minimum_length, _) = tuple.tuple(self.db()).size_hint();
if minimum_length > 1 << 12 {
return None;
}
let mut definitely_true = false;
let mut definitely_false = true;
for element in tuple.elements(self.db()) {
for element in tuple.tuple(self.db()).all_elements() {
if element.is_string_literal() {
if literal == *element {
if literal == element {
definitely_true = true;
definitely_false = false;
}
} else if !literal.is_disjoint_from(self.db(), *element) {
} else if !literal.is_disjoint_from(self.db(), element) {
definitely_false = false;
}
}
@ -7697,12 +7693,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)
}
(Type::Tuple(lhs), Type::Tuple(rhs)) => {
// Note: This only works on heterogeneous tuple types.
let lhs_elements = lhs.elements(self.db());
let rhs_elements = rhs.elements(self.db());
let lhs_tuple = lhs.tuple(self.db());
let rhs_tuple = rhs.tuple(self.db());
let mut tuple_rich_comparison =
|op| self.infer_tuple_rich_comparison(lhs_elements, op, rhs_elements, range);
|op| self.infer_tuple_rich_comparison(lhs_tuple, op, rhs_tuple, range);
match op {
ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq),
@ -7712,14 +7707,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt),
ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge),
ast::CmpOp::In | ast::CmpOp::NotIn => {
let mut eq_count = 0usize;
let mut not_eq_count = 0usize;
let mut any_eq = false;
let mut any_ambiguous = false;
for ty in rhs_elements {
for ty in rhs_tuple.all_elements() {
let eq_result = self.infer_binary_type_comparison(
Type::Tuple(lhs),
ast::CmpOp::Eq,
*ty,
ty,
range,
).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
@ -7729,16 +7724,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// for different union variants. Instead, this is just for us to
// evaluate a possibly truthy value to `false` or `true`.
ty => match ty.bool(self.db()) {
Truthiness::AlwaysTrue => eq_count += 1,
Truthiness::AlwaysFalse => not_eq_count += 1,
Truthiness::Ambiguous => (),
Truthiness::AlwaysTrue => any_eq = true,
Truthiness::AlwaysFalse => (),
Truthiness::Ambiguous => any_ambiguous = true,
},
}
}
if eq_count >= 1 {
if any_eq {
Ok(Type::BooleanLiteral(op.is_in()))
} else if not_eq_count == rhs_elements.len() {
} else if !any_ambiguous {
Ok(Type::BooleanLiteral(op.is_not_in()))
} else {
Ok(KnownClass::Bool.to_instance(self.db()))
@ -7914,13 +7909,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
/// see `<https://github.com/python/cpython/blob/9d6366b60d01305fc5e45100e0cd13e358aa397d/Objects/tupleobject.c#L637>`
fn infer_tuple_rich_comparison(
&mut self,
left: &[Type<'db>],
left: &TupleSpec<'db>,
op: RichCompareOperator,
right: &[Type<'db>],
right: &TupleSpec<'db>,
range: TextRange,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
let left_iter = left.iter().copied();
let right_iter = right.iter().copied();
// If either tuple is variable length, we can make no assumptions about the relative
// lengths of the tuples, and therefore neither about how they compare lexicographically.
// TODO: Consider comparing the prefixes of the tuples, since that could give a comparison
// result regardless of how long the variable-length tuple is.
let (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) = (left, right) else {
return Ok(Type::unknown());
};
let left_iter = left.elements();
let right_iter = right.elements();
let mut builder = UnionBuilder::new(self.db());
@ -8052,11 +8055,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// special cases, too.
if let Type::ClassLiteral(class) = value_ty {
if class.is_known(self.db(), KnownClass::Tuple) {
self.infer_expression(slice);
// TODO heterogeneous and homogeneous tuples in value expressions
return Type::from(
class.todo_specialization(self.db(), "Generic tuple specializations"),
);
return self
.infer_tuple_type_expression(slice)
.to_meta_type(self.db());
}
if let Some(generic_context) = class.generic_context(self.db()) {
return self.infer_explicit_class_specialization(
@ -8067,6 +8068,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
}
}
if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty {
return self
.infer_tuple_type_expression(slice)
.to_meta_type(self.db());
}
let slice_ty = self.infer_expression(slice);
let result_ty = self.infer_subscript_expression_types(value, value_ty, slice_ty);
@ -8113,9 +8119,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.matching_overloads()
.next()
.expect("valid bindings should have matching overload");
let specialization =
generic_context.specialize_partial(self.db(), overload.parameter_types());
Type::from(GenericAlias::new(self.db(), generic_class, specialization))
Type::from(generic_class.apply_specialization(self.db(), |_| {
generic_context.specialize_partial(self.db(), overload.parameter_types())
}))
}
fn infer_subscript_expression_types(
@ -8137,18 +8143,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// Ex) Given `("a", "b", "c", "d")[1]`, return `"b"`
(Type::Tuple(tuple_ty), Type::IntLiteral(int), _) if i32::try_from(int).is_ok() => {
let elements = tuple_ty.elements(self.db());
elements
.iter()
.py_index(i32::try_from(int).expect("checked in branch arm"))
.copied()
let tuple = tuple_ty.tuple(self.db());
tuple
.py_index(
self.db(),
i32::try_from(int).expect("checked in branch arm"),
)
.unwrap_or_else(|_| {
report_index_out_of_bounds(
&self.context,
"tuple",
value_node.into(),
value_ty,
elements.len(),
tuple.display_minimum_length(),
int,
);
Type::unknown()
@ -8156,9 +8163,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
// Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)`
(Type::Tuple(tuple_ty), _, Some(SliceLiteral { start, stop, step })) => {
let elements = tuple_ty.elements(self.db());
let TupleSpec::Fixed(tuple) = tuple_ty.tuple(self.db()) else {
return todo_type!("slice into variable-length tuple");
};
if let Ok(new_elements) = elements.py_slice(start, stop, step) {
if let Ok(new_elements) = tuple.py_slice(self.db(), start, stop, step) {
TupleType::from_elements(self.db(), new_elements)
} else {
report_slice_step_size_zero(&self.context, value_node.into());
@ -8170,9 +8179,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if i32::try_from(int).is_ok() =>
{
let literal_value = literal_ty.value(self.db());
literal_value
.chars()
.py_index(i32::try_from(int).expect("checked in branch arm"))
(&mut literal_value.chars())
.py_index(
self.db(),
i32::try_from(int).expect("checked in branch arm"),
)
.map(|ch| Type::string_literal(self.db(), &ch.to_string()))
.unwrap_or_else(|_| {
report_index_out_of_bounds(
@ -8192,7 +8203,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let chars: Vec<_> = literal_value.chars().collect();
if let Ok(new_chars) = chars.py_slice(start, stop, step) {
if let Ok(new_chars) = chars.py_slice(self.db(), start, stop, step) {
let literal: String = new_chars.collect();
Type::string_literal(self.db(), &literal)
} else {
@ -8206,8 +8217,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
{
let literal_value = literal_ty.value(self.db());
literal_value
.iter()
.py_index(i32::try_from(int).expect("checked in branch arm"))
.py_index(
self.db(),
i32::try_from(int).expect("checked in branch arm"),
)
.map(|byte| Type::IntLiteral((*byte).into()))
.unwrap_or_else(|_| {
report_index_out_of_bounds(
@ -8225,7 +8238,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(Type::BytesLiteral(literal_ty), _, Some(SliceLiteral { start, stop, step })) => {
let literal_value = literal_ty.value(self.db());
if let Ok(new_bytes) = literal_value.py_slice(start, stop, step) {
if let Ok(new_bytes) = literal_value.py_slice(self.db(), start, stop, step) {
let new_bytes: Vec<u8> = new_bytes.copied().collect();
Type::bytes_literal(self.db(), &new_bytes)
} else {
@ -8243,14 +8256,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
value_ty,
Type::IntLiteral(i64::from(bool)),
),
(Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars), _) => self
.legacy_generic_class_context(
(Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars), _) => {
let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else {
// TODO: emit a diagnostic
return Type::unknown();
};
self.legacy_generic_class_context(
value_node,
typevars.elements(self.db()),
typevars.elements_slice(),
LegacyGenericBase::Protocol,
)
.map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context)))
.unwrap_or_else(Type::unknown),
.unwrap_or_else(Type::unknown)
}
(Type::SpecialForm(SpecialFormType::Protocol), typevar, _) => self
.legacy_generic_class_context(
value_node,
@ -8263,14 +8281,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// TODO: emit a diagnostic
todo_type!("doubly-specialized typing.Protocol")
}
(Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars), _) => self
.legacy_generic_class_context(
(Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars), _) => {
let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else {
// TODO: emit a diagnostic
return Type::unknown();
};
self.legacy_generic_class_context(
value_node,
typevars.elements(self.db()),
typevars.elements_slice(),
LegacyGenericBase::Generic,
)
.map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context)))
.unwrap_or_else(Type::unknown),
.unwrap_or_else(Type::unknown)
}
(Type::SpecialForm(SpecialFormType::Generic), typevar, _) => self
.legacy_generic_class_context(
value_node,
@ -9167,10 +9190,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
todo_type!("ellipsis literal in type expression")
}
ast::Expr::Starred(starred) => {
self.infer_starred_expression(starred);
todo_type!("PEP 646")
}
ast::Expr::Starred(starred) => self.infer_starred_type_expression(starred),
}
}
fn infer_starred_type_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
let ast::ExprStarred {
range: _,
node_index: _,
value,
ctx: _,
} = starred;
let starred_type = self.infer_type_expression(value);
if let Type::Tuple(_) = starred_type {
starred_type
} else {
todo_type!("PEP 646")
}
}
@ -9225,7 +9261,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
match element {
ast::Expr::Starred(_) => true,
ast::Expr::Starred(_) => !matches!(element_ty, Type::Tuple(_)),
ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => {
let value_ty = if builder.deferred_state.in_string_annotation() {
// Using `.expression_type` does not work in string annotations, because
@ -9246,13 +9282,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
ast::Expr::Tuple(elements) => {
if let [element, ellipsis @ ast::Expr::EllipsisLiteral(_)] = &*elements.elts {
self.infer_expression(ellipsis);
let result = KnownClass::Tuple
.to_specialized_instance(self.db(), [self.infer_type_expression(element)]);
let result =
TupleType::homogeneous(self.db(), self.infer_type_expression(element));
self.store_expression_type(tuple_slice, result);
return result;
}
let mut element_types = Vec::with_capacity(elements.len());
let mut element_types = TupleSpec::with_capacity(elements.len());
// Whether to infer `Todo` for the whole tuple
// (see docstring for `element_could_alter_type_of_whole_tuple`)
@ -9262,13 +9298,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let element_ty = self.infer_type_expression(element);
return_todo |=
element_could_alter_type_of_whole_tuple(element, element_ty, self);
element_types.push(element_ty);
if let ast::Expr::Starred(_) = element {
if let Type::Tuple(inner_tuple) = element_ty {
element_types =
element_types.concat(self.db(), inner_tuple.tuple(self.db()));
} else {
// TODO: emit a diagnostic
}
} else {
element_types.push(element_ty);
}
}
let ty = if return_todo {
todo_type!("PEP 646")
} else {
TupleType::from_elements(self.db(), element_types)
Type::tuple(self.db(), TupleType::new(self.db(), element_types))
};
// Here, we store the type for the inner `int, str` tuple-expression,

View file

@ -5,6 +5,7 @@ use std::marker::PhantomData;
use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
use crate::place::{Boundness, Place, PlaceAndQualifiers};
use crate::types::tuple::TupleType;
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
use crate::{Db, FxOrderSet};
@ -12,12 +13,18 @@ pub(super) use synthesized_protocol::SynthesizedProtocolType;
impl<'db> Type<'db> {
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
if class.is_known(db, KnownClass::Any) {
Self::Dynamic(DynamicType::Any)
} else if class.class_literal(db).0.is_protocol(db) {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
} else {
Self::NominalInstance(NominalInstanceType::from_class(class))
match (class, class.known(db)) {
(_, Some(KnownClass::Any)) => Self::Dynamic(DynamicType::Any),
(ClassType::NonGeneric(_), Some(KnownClass::Tuple)) => {
TupleType::homogeneous(db, Type::unknown())
}
(ClassType::Generic(alias), Some(KnownClass::Tuple)) => {
Self::tuple(db, TupleType::new(db, alias.specialization(db).tuple(db)))
}
_ if class.class_literal(db).0.is_protocol(db) => {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
}
_ => Self::NominalInstance(NominalInstanceType::from_class(class)),
}
}
@ -98,11 +105,24 @@ impl<'db> NominalInstanceType<'db> {
}
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) {
self.is_disjoint_from_nominal_instance_of_class(db, other.class)
}
// Note that this method only exists so that we can check disjointness between nominal
// instances of `tuple` and some other class. Tuples are currently represented by the
// `Type::Tuple` variant, not `Type::NominalInstance`. We have a TODO to try to remove the
// dedicated `Tuple` variant in favor of `NominalInstance`; if we can do that, then we won't
// need this method, and its logic can be subsumed into `is_disjoint_from`.
pub(super) fn is_disjoint_from_nominal_instance_of_class(
self,
db: &'db dyn Db,
other_class: ClassType,
) -> bool {
if self.class.is_final(db) && !self.class.is_subclass_of(db, other_class) {
return true;
}
if other.class.is_final(db) && !other.class.is_subclass_of(db, self.class) {
if other_class.is_final(db) && !other_class.is_subclass_of(db, self.class) {
return true;
}
@ -116,7 +136,7 @@ impl<'db> NominalInstanceType<'db> {
if self_metaclass == type_type {
return false;
}
let other_metaclass = other.class.metaclass_instance_type(db);
let other_metaclass = other_class.metaclass_instance_type(db);
if other_metaclass == type_type {
return false;
}

View file

@ -175,8 +175,8 @@ impl ClassInfoConstraintFunction {
match classinfo {
Type::Tuple(tuple) => {
let mut builder = UnionBuilder::new(db);
for element in tuple.elements(db) {
builder = builder.add(self.generate_constraint(db, *element)?);
for element in tuple.tuple(db).all_elements() {
builder = builder.add(self.generate_constraint(db, element)?);
}
Some(builder.build())
}
@ -540,7 +540,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
match rhs_ty {
Type::Tuple(rhs_tuple) => Some(UnionType::from_elements(
self.db,
rhs_tuple.elements(self.db),
rhs_tuple.tuple(self.db).all_elements(),
)),
Type::StringLiteral(string_literal) => Some(UnionType::from_elements(

View file

@ -1,8 +1,9 @@
use crate::db::tests::TestDb;
use crate::place::{builtins_symbol, known_module_symbol};
use crate::types::tuple::TupleType;
use crate::types::{
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, Parameter, Parameters,
Signature, SpecialFormType, SubclassOfType, TupleType, Type, UnionType,
Signature, SpecialFormType, SubclassOfType, Type, UnionType,
};
use crate::{Db, KnownModule};
use hashbrown::HashSet;

View file

@ -36,7 +36,7 @@ impl SlotsKind {
match slots_ty {
// __slots__ = ("a", "b")
Type::Tuple(tuple) => {
if tuple.elements(db).is_empty() {
if tuple.tuple(db).is_empty() {
Self::Empty
} else {
Self::NotEmpty

View file

@ -0,0 +1,868 @@
//! Types describing fixed- and variable-length tuples.
//!
//! At runtime, a Python tuple is a fixed-length immutable list of values. There is no restriction
//! on the types of the elements of a tuple value. In the type system, we want to model both
//! "heterogeneous" tuples that have elements of a fixed sequence of specific types, and
//! "homogeneous" tuples that have an unknown number of elements of the same single type. And in
//! fact, we want to model tuples that are a combination of the two ("mixed" tuples), with a
//! heterogeneous prefix and/or suffix, and a homogeneous portion of unknown length in between
//! those.
//!
//! The description of which elements can appear in a `tuple` is called a [`TupleSpec`]. Other
//! things besides `tuple` instances can be described by a tuple spec — for instance, the targets
//! of an unpacking assignment. A `tuple` specialization that includes `Never` as one of its
//! fixed-length elements cannot be instantiated. We reduce the entire `tuple` type down to
//! `Never`. The same is not true of tuple specs in general. (That means that it is [`TupleType`]
//! that adds that "collapse `Never`" behavior, whereas [`TupleSpec`] allows you to add any element
//! types, including `Never`.)
use itertools::Either;
use crate::types::class::{ClassType, KnownClass};
use crate::types::{Type, TypeMapping, TypeRelation, TypeVarInstance, TypeVarVariance, UnionType};
use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError};
use crate::{Db, FxOrderSet};
/// # Ordering
/// Ordering is based on the tuple's salsa-assigned id and not on its elements.
/// The id may change between runs, or when the tuple was garbage collected and recreated.
#[salsa::interned(debug)]
#[derive(PartialOrd, Ord)]
pub struct TupleType<'db> {
#[returns(ref)]
pub(crate) tuple: TupleSpec<'db>,
}
impl<'db> Type<'db> {
pub(crate) fn tuple(db: &'db dyn Db, tuple: TupleType<'db>) -> Self {
// If a fixed-length (i.e., mandatory) element of the tuple is `Never`, then it's not
// possible to instantiate the tuple as a whole. (This is not true of the variable-length
// portion of the tuple, since it can contain no elements.)
if tuple.tuple(db).fixed_elements().any(|ty| ty.is_never()) {
return Type::Never;
}
Self::Tuple(tuple)
}
}
impl<'db> TupleType<'db> {
pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> {
Type::tuple(
db,
TupleType::new(db, TupleSpec::from(FixedLengthTupleSpec::empty())),
)
}
pub(crate) fn from_elements(
db: &'db dyn Db,
types: impl IntoIterator<Item = impl Into<Type<'db>>>,
) -> Type<'db> {
Type::tuple(
db,
TupleType::new(
db,
TupleSpec::from(FixedLengthTupleSpec::from_elements(types)),
),
)
}
#[cfg(test)]
pub(crate) fn mixed(
db: &'db dyn Db,
prefix: impl IntoIterator<Item = impl Into<Type<'db>>>,
variable: Type<'db>,
suffix: impl IntoIterator<Item = impl Into<Type<'db>>>,
) -> Type<'db> {
Type::tuple(
db,
TupleType::new(
db,
TupleSpec::from(VariableLengthTupleSpec::mixed(prefix, variable, suffix)),
),
)
}
pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Type<'db> {
Type::tuple(db, TupleType::new(db, TupleSpec::homogeneous(element)))
}
pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option<ClassType<'db>> {
KnownClass::Tuple
.try_to_class_literal(db)
.and_then(|class_literal| match class_literal.generic_context(db) {
None => Some(ClassType::NonGeneric(class_literal)),
Some(generic_context) if generic_context.variables(db).len() != 1 => None,
Some(generic_context) => Some(
class_literal
.apply_specialization(db, |_| generic_context.specialize_tuple(db, self)),
),
})
}
/// Return a normalized version of `self`.
///
/// See [`Type::normalized`] for more details.
#[must_use]
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
TupleType::new(db, self.tuple(db).normalized(db))
}
pub(crate) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
TupleType::new(db, self.tuple(db).materialize(db, variance))
}
pub(crate) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
) -> Self {
TupleType::new(db, self.tuple(db).apply_type_mapping(db, type_mapping))
}
pub(crate) fn find_legacy_typevars(
self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
self.tuple(db).find_legacy_typevars(db, typevars);
}
pub(crate) fn has_relation_to(
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
) -> bool {
self.tuple(db)
.has_relation_to(db, other.tuple(db), relation)
}
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.tuple(db).is_equivalent_to(db, other.tuple(db))
}
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.tuple(db).is_gradual_equivalent_to(db, other.tuple(db))
}
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
self.tuple(db).is_disjoint_from(db, other.tuple(db))
}
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.tuple(db).is_fully_static(db)
}
pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool {
self.tuple(db).is_single_valued(db)
}
}
/// A fixed-length tuple spec.
///
/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a
/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that
/// must contain an element that can't be instantiated, can't be instantiated itself).
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, salsa::Update)]
pub struct FixedLengthTupleSpec<'db>(Vec<Type<'db>>);
impl<'db> FixedLengthTupleSpec<'db> {
pub(crate) fn empty() -> Self {
Self::default()
}
pub(crate) fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
pub(crate) fn from_elements(elements: impl IntoIterator<Item = impl Into<Type<'db>>>) -> Self {
Self(elements.into_iter().map(Into::into).collect())
}
pub(crate) fn elements_slice(&self) -> &[Type<'db>] {
&self.0
}
pub(crate) fn elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
self.0.iter().copied()
}
/// Returns the length of this tuple.
pub(crate) fn len(&self) -> usize {
self.0.len()
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn concat(&self, other: &TupleSpec<'db>) -> TupleSpec<'db> {
match other {
TupleSpec::Fixed(other) => {
let mut elements = Vec::with_capacity(self.0.len() + other.0.len());
elements.extend_from_slice(&self.0);
elements.extend_from_slice(&other.0);
TupleSpec::Fixed(FixedLengthTupleSpec(elements))
}
TupleSpec::Variable(other) => {
let mut prefix = Vec::with_capacity(self.0.len() + other.prefix.len());
prefix.extend_from_slice(&self.0);
prefix.extend_from_slice(&other.prefix);
TupleSpec::Variable(VariableLengthTupleSpec {
prefix,
variable: other.variable,
suffix: other.suffix.clone(),
})
}
}
}
pub(crate) fn push(&mut self, element: Type<'db>) {
self.0.push(element);
}
pub(crate) fn extend_from_slice(&mut self, elements: &[Type<'db>]) {
self.0.extend_from_slice(elements);
}
#[must_use]
fn normalized(&self, db: &'db dyn Db) -> Self {
Self(self.0.iter().map(|ty| ty.normalized(db)).collect())
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self(
self.0
.iter()
.map(|ty| ty.materialize(db, variance))
.collect(),
)
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
Self(
self.0
.iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect(),
)
}
fn find_legacy_typevars(
&self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
for ty in &self.0 {
ty.find_legacy_typevars(db, typevars);
}
}
fn has_relation_to(
&self,
db: &'db dyn Db,
other: &TupleSpec<'db>,
relation: TypeRelation,
) -> bool {
match other {
TupleSpec::Fixed(other) => {
self.0.len() == other.0.len()
&& (self.0.iter())
.zip(&other.0)
.all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation))
}
TupleSpec::Variable(other) => {
// This tuple must have enough elements to match up with the other tuple's prefix
// and suffix, and each of those elements must pairwise satisfy the relation.
let mut self_iter = self.0.iter();
for other_ty in &other.prefix {
let Some(self_ty) = self_iter.next() else {
return false;
};
if !self_ty.has_relation_to(db, *other_ty, relation) {
return false;
}
}
for other_ty in other.suffix.iter().rev() {
let Some(self_ty) = self_iter.next_back() else {
return false;
};
if !self_ty.has_relation_to(db, *other_ty, relation) {
return false;
}
}
// In addition, any remaining elements in this tuple must satisfy the
// variable-length portion of the other tuple.
self_iter.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation))
}
}
}
fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.0.len() == other.0.len()
&& (self.0.iter())
.zip(&other.0)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.0.len() == other.0.len()
&& (self.0.iter())
.zip(&other.0)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool {
self.0.len() != other.0.len()
|| (self.0.iter())
.zip(&other.0)
.any(|(self_ty, other_ty)| self_ty.is_disjoint_from(db, *other_ty))
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
self.0.iter().all(|ty| ty.is_fully_static(db))
}
fn is_single_valued(&self, db: &'db dyn Db) -> bool {
self.0.iter().all(|ty| ty.is_single_valued(db))
}
}
impl<'db> PyIndex<'db> for &FixedLengthTupleSpec<'db> {
type Item = Type<'db>;
fn py_index(self, db: &'db dyn Db, index: i32) -> Result<Self::Item, OutOfBoundsError> {
self.0.as_slice().py_index(db, index).copied()
}
}
impl<'db> PySlice<'db> for FixedLengthTupleSpec<'db> {
type Item = Type<'db>;
fn py_slice(
&'db self,
db: &'db dyn Db,
start: Option<i32>,
stop: Option<i32>,
step: Option<i32>,
) -> Result<impl Iterator<Item = &'db Self::Item>, StepSizeZeroError> {
self.0.py_slice(db, start, stop, step)
}
}
/// A variable-length tuple spec.
///
/// The tuple spec can contain a fixed-length heterogeneous prefix and/or suffix. All of the
/// elements of the variable-length portion must be of the same type.
///
/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a
/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that
/// must contain an element that can't be instantiated, can't be instantiated itself).
#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
pub struct VariableLengthTupleSpec<'db> {
pub(crate) prefix: Vec<Type<'db>>,
pub(crate) variable: Type<'db>,
pub(crate) suffix: Vec<Type<'db>>,
}
impl<'db> VariableLengthTupleSpec<'db> {
/// Creates a new tuple spec containing zero or more elements of a given type, with no prefix
/// or suffix.
fn homogeneous(ty: Type<'db>) -> Self {
Self {
prefix: vec![],
variable: ty,
suffix: vec![],
}
}
#[cfg(test)]
fn mixed(
prefix: impl IntoIterator<Item = impl Into<Type<'db>>>,
variable: Type<'db>,
suffix: impl IntoIterator<Item = impl Into<Type<'db>>>,
) -> Self {
Self {
prefix: prefix.into_iter().map(Into::into).collect(),
variable,
suffix: suffix.into_iter().map(Into::into).collect(),
}
}
fn fixed_elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
(self.prefix.iter().copied()).chain(self.suffix.iter().copied())
}
fn all_elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
(self.prefix.iter().copied())
.chain(std::iter::once(self.variable))
.chain(self.suffix.iter().copied())
}
/// Returns the minimum length of this tuple.
pub(crate) fn minimum_length(&self) -> usize {
self.prefix.len() + self.suffix.len()
}
fn concat(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> TupleSpec<'db> {
match other {
TupleSpec::Fixed(other) => {
let mut suffix = Vec::with_capacity(self.suffix.len() + other.0.len());
suffix.extend_from_slice(&self.suffix);
suffix.extend_from_slice(&other.0);
TupleSpec::Variable(VariableLengthTupleSpec {
prefix: self.prefix.clone(),
variable: self.variable,
suffix,
})
}
TupleSpec::Variable(other) => {
let variable = UnionType::from_elements(
db,
(self.suffix.iter().copied())
.chain([self.variable, other.variable])
.chain(other.prefix.iter().copied()),
);
TupleSpec::Variable(VariableLengthTupleSpec {
prefix: self.prefix.clone(),
variable,
suffix: other.suffix.clone(),
})
}
}
}
fn push(&mut self, element: Type<'db>) {
self.suffix.push(element);
}
#[must_use]
fn normalized(&self, db: &'db dyn Db) -> Self {
Self {
prefix: self.prefix.iter().map(|ty| ty.normalized(db)).collect(),
variable: self.variable.normalized(db),
suffix: self.suffix.iter().map(|ty| ty.normalized(db)).collect(),
}
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self {
prefix: self
.prefix
.iter()
.map(|ty| ty.materialize(db, variance))
.collect(),
variable: self.variable.materialize(db, variance),
suffix: self
.suffix
.iter()
.map(|ty| ty.materialize(db, variance))
.collect(),
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
Self {
prefix: self
.prefix
.iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect(),
variable: self.variable.apply_type_mapping(db, type_mapping),
suffix: self
.suffix
.iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect(),
}
}
fn find_legacy_typevars(
&self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
for ty in &self.prefix {
ty.find_legacy_typevars(db, typevars);
}
self.variable.find_legacy_typevars(db, typevars);
for ty in &self.suffix {
ty.find_legacy_typevars(db, typevars);
}
}
fn has_relation_to(
&self,
db: &'db dyn Db,
other: &TupleSpec<'db>,
relation: TypeRelation,
) -> bool {
match other {
TupleSpec::Fixed(other) => {
// The `...` length specifier of a variable-length tuple type is interpreted
// differently depending on the type of the variable-length elements.
//
// It typically represents the _union_ of all possible lengths. That means that a
// variable-length tuple type is not a subtype of _any_ fixed-length tuple type.
//
// However, as a special case, if the variable-length portion of the tuple is `Any`
// (or any other dynamic type), then the `...` is the _gradual choice_ of all
// possible lengths. This means that `tuple[Any, ...]` can match any tuple of any
// length.
if relation == TypeRelation::Subtyping || !matches!(self.variable, Type::Dynamic(_))
{
return false;
}
// In addition, the other tuple must have enough elements to match up with this
// tuple's prefix and suffix, and each of those elements must pairwise satisfy the
// relation.
let mut other_iter = other.0.iter();
for self_ty in &self.prefix {
let Some(other_ty) = other_iter.next() else {
return false;
};
if !self_ty.has_relation_to(db, *other_ty, relation) {
return false;
}
}
for self_ty in self.suffix.iter().rev() {
let Some(other_ty) = other_iter.next_back() else {
return false;
};
if !self_ty.has_relation_to(db, *other_ty, relation) {
return false;
}
}
true
}
TupleSpec::Variable(other) => {
// The overlapping parts of the prefixes and suffixes must satisfy the relation.
let mut self_prefix = self.prefix.iter();
let mut other_prefix = other.prefix.iter();
let prefixes_match = (&mut self_prefix)
.zip(&mut other_prefix)
.all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation));
if !prefixes_match {
return false;
}
let mut self_suffix = self.suffix.iter().rev();
let mut other_suffix = other.suffix.iter().rev();
let suffixes_match = (&mut self_suffix)
.zip(&mut other_suffix)
.all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation));
if !suffixes_match {
return false;
}
// Any remaining parts of either prefix or suffix must satisfy the relation with
// the other tuple's variable-length portion.
let prefix_matches_variable = self_prefix
.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation));
if !prefix_matches_variable {
return false;
}
let prefix_matches_variable = other_prefix
.all(|other_ty| self.variable.has_relation_to(db, *other_ty, relation));
if !prefix_matches_variable {
return false;
}
let suffix_matches_variable = self_suffix
.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation));
if !suffix_matches_variable {
return false;
}
let suffix_matches_variable = other_suffix
.all(|other_ty| self.variable.has_relation_to(db, *other_ty, relation));
if !suffix_matches_variable {
return false;
}
// And lastly, the variable-length portions must satisfy the relation.
self.variable.has_relation_to(db, other.variable, relation)
}
}
}
fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.prefix.len() == other.prefix.len()
&& self.suffix.len() == other.suffix.len()
&& self.variable.is_equivalent_to(db, other.variable)
&& (self.prefix.iter())
.zip(&other.prefix)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
&& (self.suffix.iter())
.zip(&other.suffix)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.prefix.len() == other.prefix.len()
&& self.suffix.len() == other.suffix.len()
&& self.variable.is_gradual_equivalent_to(db, other.variable)
&& (self.prefix.iter())
.zip(&other.prefix)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
&& (self.suffix.iter())
.zip(&other.suffix)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
self.variable.is_fully_static(db)
&& self.prefix.iter().all(|ty| ty.is_fully_static(db))
&& self.suffix.iter().all(|ty| ty.is_fully_static(db))
}
}
impl<'db> PyIndex<'db> for &VariableLengthTupleSpec<'db> {
type Item = Type<'db>;
fn py_index(self, db: &'db dyn Db, index: i32) -> Result<Self::Item, OutOfBoundsError> {
match Nth::from_index(index) {
Nth::FromStart(index) => {
if let Some(element) = self.prefix.get(index) {
// index is small enough that it lands in the prefix of the tuple.
return Ok(*element);
}
// index is large enough that it lands past the prefix. The tuple can always be
// large enough that it lands in the variable-length portion. It might also be
// small enough to land in the suffix.
let index_past_prefix = index - self.prefix.len() + 1;
Ok(UnionType::from_elements(
db,
std::iter::once(self.variable)
.chain(self.suffix.iter().copied().take(index_past_prefix)),
))
}
Nth::FromEnd(index_from_end) => {
if index_from_end < self.suffix.len() {
// index is small enough that it lands in the suffix of the tuple.
return Ok(self.suffix[self.suffix.len() - index_from_end - 1]);
}
// index is large enough that it lands past the suffix. The tuple can always be
// large enough that it lands in the variable-length portion. It might also be
// small enough to land in the prefix.
let index_past_suffix = index_from_end - self.suffix.len() + 1;
Ok(UnionType::from_elements(
db,
(self.prefix.iter().rev().copied())
.take(index_past_suffix)
.rev()
.chain(std::iter::once(self.variable)),
))
}
}
}
}
/// A tuple spec that might be fixed- or variable-length.
///
/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a
/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that
/// must contain an element that can't be instantiated, can't be instantiated itself).
#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
pub enum TupleSpec<'db> {
Fixed(FixedLengthTupleSpec<'db>),
Variable(VariableLengthTupleSpec<'db>),
}
impl<'db> TupleSpec<'db> {
pub(crate) fn with_capacity(capacity: usize) -> Self {
TupleSpec::Fixed(FixedLengthTupleSpec::with_capacity(capacity))
}
pub(crate) fn homogeneous(element: Type<'db>) -> Self {
TupleSpec::from(VariableLengthTupleSpec::homogeneous(element))
}
/// Returns an iterator of all of the fixed-length element types of this tuple.
pub(crate) fn fixed_elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
match self {
TupleSpec::Fixed(tuple) => Either::Left(tuple.elements()),
TupleSpec::Variable(tuple) => Either::Right(tuple.fixed_elements()),
}
}
/// Returns an iterator of all of the element types of this tuple. Does not deduplicate the
/// elements, and does not distinguish between fixed- and variable-length elements.
pub(crate) fn all_elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
match self {
TupleSpec::Fixed(tuple) => Either::Left(tuple.elements()),
TupleSpec::Variable(tuple) => Either::Right(tuple.all_elements()),
}
}
pub(crate) fn display_minimum_length(&self) -> String {
match self {
TupleSpec::Fixed(tuple) => tuple.len().to_string(),
TupleSpec::Variable(tuple) => format!("at least {}", tuple.minimum_length()),
}
}
/// Returns the minimum and maximum length of this tuple. (The maximum length will be `None`
/// for a tuple with a variable-length portion.)
pub(crate) fn size_hint(&self) -> (usize, Option<usize>) {
match self {
TupleSpec::Fixed(tuple) => {
let len = tuple.len();
(len, Some(len))
}
TupleSpec::Variable(tuple) => (tuple.minimum_length(), None),
}
}
pub(crate) fn is_empty(&self) -> bool {
match self {
TupleSpec::Fixed(tuple) => tuple.is_empty(),
TupleSpec::Variable(_) => false,
}
}
/// Concatenates another tuple to the end of this tuple, returning a new tuple.
pub(crate) fn concat(&self, db: &'db dyn Db, other: &Self) -> Self {
match self {
TupleSpec::Fixed(tuple) => tuple.concat(other),
TupleSpec::Variable(tuple) => tuple.concat(db, other),
}
}
pub(crate) fn push(&mut self, element: Type<'db>) {
match self {
TupleSpec::Fixed(tuple) => tuple.push(element),
TupleSpec::Variable(tuple) => tuple.push(element),
}
}
fn normalized(&self, db: &'db dyn Db) -> Self {
match self {
TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.normalized(db)),
TupleSpec::Variable(tuple) => TupleSpec::Variable(tuple.normalized(db)),
}
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
match self {
TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.materialize(db, variance)),
TupleSpec::Variable(tuple) => TupleSpec::Variable(tuple.materialize(db, variance)),
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
match self {
TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.apply_type_mapping(db, type_mapping)),
TupleSpec::Variable(tuple) => {
TupleSpec::Variable(tuple.apply_type_mapping(db, type_mapping))
}
}
}
fn find_legacy_typevars(
&self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
match self {
TupleSpec::Fixed(tuple) => tuple.find_legacy_typevars(db, typevars),
TupleSpec::Variable(tuple) => tuple.find_legacy_typevars(db, typevars),
}
}
fn has_relation_to(&self, db: &'db dyn Db, other: &Self, relation: TypeRelation) -> bool {
match self {
TupleSpec::Fixed(self_tuple) => self_tuple.has_relation_to(db, other, relation),
TupleSpec::Variable(self_tuple) => self_tuple.has_relation_to(db, other, relation),
}
}
fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
match (self, other) {
(TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => {
self_tuple.is_equivalent_to(db, other_tuple)
}
(TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => {
self_tuple.is_equivalent_to(db, other_tuple)
}
(TupleSpec::Fixed(_), TupleSpec::Variable(_))
| (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false,
}
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> bool {
match (self, other) {
(TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => {
self_tuple.is_gradual_equivalent_to(db, other_tuple)
}
(TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => {
self_tuple.is_gradual_equivalent_to(db, other_tuple)
}
(TupleSpec::Fixed(_), TupleSpec::Variable(_))
| (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false,
}
}
fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool {
match (self, other) {
(TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => {
self_tuple.is_disjoint_from(db, other_tuple)
}
// Two pure homogeneous tuples `tuple[A, ...]` and `tuple[B, ...]` can never be
// disjoint even if A and B are disjoint, because `tuple[()]` would be assignable to
// both.
// TODO: Consider checking for disjointness between the tuples' prefixes and suffixes.
(TupleSpec::Variable(_), TupleSpec::Variable(_)) => false,
// TODO: Consider checking for disjointness between the fixed-length tuple and the
// variable-length tuple's prefix/suffix.
(TupleSpec::Fixed(_), TupleSpec::Variable(_))
| (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false,
}
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
match self {
TupleSpec::Fixed(tuple) => tuple.is_fully_static(db),
TupleSpec::Variable(tuple) => tuple.is_fully_static(db),
}
}
fn is_single_valued(&self, db: &'db dyn Db) -> bool {
match self {
TupleSpec::Fixed(tuple) => tuple.is_single_valued(db),
TupleSpec::Variable(_) => false,
}
}
}
impl<'db> From<FixedLengthTupleSpec<'db>> for TupleSpec<'db> {
fn from(tuple: FixedLengthTupleSpec<'db>) -> Self {
TupleSpec::Fixed(tuple)
}
}
impl<'db> From<VariableLengthTupleSpec<'db>> for TupleSpec<'db> {
fn from(tuple: VariableLengthTupleSpec<'db>) -> Self {
TupleSpec::Variable(tuple)
}
}
impl<'db> PyIndex<'db> for &TupleSpec<'db> {
type Item = Type<'db>;
fn py_index(self, db: &'db dyn Db, index: i32) -> Result<Self::Item, OutOfBoundsError> {
match self {
TupleSpec::Fixed(tuple) => tuple.py_index(db, index),
TupleSpec::Variable(tuple) => tuple.py_index(db, index),
}
}
}

View file

@ -9,12 +9,13 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use crate::Db;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::place::ScopeId;
use crate::types::{Type, TypeCheckDiagnostics, infer_expression_types};
use crate::types::tuple::{FixedLengthTupleSpec, TupleSpec, TupleType};
use crate::types::{Type, TypeCheckDiagnostics, infer_expression_types, todo_type};
use crate::unpack::{UnpackKind, UnpackValue};
use super::context::InferContext;
use super::diagnostic::INVALID_ASSIGNMENT;
use super::{KnownClass, TupleType, UnionType};
use super::{KnownClass, UnionType};
/// Unpacks the value expression type to their respective targets.
pub(crate) struct Unpacker<'db, 'ast> {
@ -152,53 +153,55 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
_ => ty,
};
if let Some(tuple_ty) = ty.into_tuple() {
let tuple_ty_elements =
self.tuple_ty_elements(target, elts, tuple_ty, value_expr);
if let Type::Tuple(tuple_ty) = ty {
let tuple = self.tuple_ty_elements(target, elts, tuple_ty, value_expr);
let length_mismatch =
match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Too many values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(self.context.secondary(value_expr).message(
format_args!("Got {}", tuple_ty_elements.len()),
));
}
true
let length_mismatch = match elts.len().cmp(&tuple.len()) {
Ordering::Less => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Too many values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(
self.context
.secondary(value_expr)
.message(format_args!("Got {}", tuple.len())),
);
}
Ordering::Greater => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Not enough values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(self.context.secondary(value_expr).message(
format_args!("Got {}", tuple_ty_elements.len()),
));
}
true
true
}
Ordering::Greater => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Not enough values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(
self.context
.secondary(value_expr)
.message(format_args!("Got {}", tuple.len())),
);
}
Ordering::Equal => false,
};
true
}
Ordering::Equal => false,
};
for (index, ty) in tuple_ty_elements.iter().enumerate() {
for (index, ty) in tuple.elements().enumerate() {
if let Some(element_types) = target_types.get_mut(index) {
if length_mismatch {
element_types.push(Type::unknown());
} else {
element_types.push(*ty);
element_types.push(ty);
}
}
}
@ -248,24 +251,36 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
targets: &[ast::Expr],
tuple_ty: TupleType<'db>,
value_expr: AnyNodeRef<'_>,
) -> Cow<'_, [Type<'db>]> {
) -> Cow<'_, FixedLengthTupleSpec<'db>> {
let TupleSpec::Fixed(tuple) = tuple_ty.tuple(self.db()) else {
let todo = todo_type!("Unpack variable-length tuple");
return Cow::Owned(FixedLengthTupleSpec::from_elements(targets.iter().map(
|target| {
if target.is_starred_expr() {
KnownClass::List.to_specialized_instance(self.db(), [todo])
} else {
todo
}
},
)));
};
// If there is a starred expression, it will consume all of the types at that location.
let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else {
// Otherwise, the types will be unpacked 1-1 to the targets.
return Cow::Borrowed(tuple_ty.elements(self.db()));
return Cow::Borrowed(tuple);
};
if tuple_ty.len(self.db()) >= targets.len() - 1 {
if tuple.len() >= targets.len() - 1 {
// This branch is only taken when there are enough elements in the tuple type to
// combine for the starred expression. So, the arithmetic and indexing operations are
// safe to perform.
let mut element_types = Vec::with_capacity(targets.len());
let mut element_types = FixedLengthTupleSpec::with_capacity(targets.len());
let tuple_elements = tuple.elements_slice();
// Insert all the elements before the starred expression.
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
&tuple_ty.elements(self.db())[..starred_index],
);
// SAFETY: Safe because of the length check above.
element_types.extend_from_slice(&tuple_elements[..starred_index]);
// The number of target expressions that are remaining after the starred expression.
// For example, in `(a, *b, c, d) = ...`, the index of starred element `b` is 1 and the
@ -276,11 +291,10 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
// expression, in an exclusive manner. For example, in `(a, *b, c) = (1, 2, 3, 4)`, the
// starred expression `b` will consume the elements `Literal[2]` and `Literal[3]` and
// the index value would be 3.
let starred_end_index = tuple_ty.len(self.db()) - remaining;
let starred_end_index = tuple.len() - remaining;
// SAFETY: Safe because of the length check above.
let starred_element_types =
&tuple_ty.elements(self.db())[starred_index..starred_end_index];
let starred_element_types = &tuple_elements[starred_index..starred_end_index];
element_types.push(KnownClass::List.to_specialized_instance(
self.db(),
@ -292,10 +306,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
));
// Insert the types remaining that aren't consumed by the starred expression.
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
&tuple_ty.elements(self.db())[starred_end_index..],
);
// SAFETY: Safe because of the length check above.
element_types.extend_from_slice(&tuple_elements[starred_end_index..]);
Cow::Owned(element_types)
} else {
@ -305,22 +317,19 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
diag.annotate(
self.context
.secondary(value_expr)
.message(format_args!("Got {}", tuple_ty.len(self.db()))),
.message(format_args!("Got {}", tuple.len())),
);
}
Cow::Owned(
targets
.iter()
.map(|target| {
if target.is_starred_expr() {
KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()])
} else {
Type::unknown()
}
})
.collect(),
)
Cow::Owned(FixedLengthTupleSpec::from_elements(targets.iter().map(
|target| {
if target.is_starred_expr() {
KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()])
} else {
Type::unknown()
}
},
)))
}
}

View file

@ -4,13 +4,15 @@
use itertools::Either;
use crate::Db;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct OutOfBoundsError;
pub(crate) trait PyIndex {
type Item;
pub(crate) trait PyIndex<'db> {
type Item: 'db;
fn py_index(&mut self, index: i32) -> Result<Self::Item, OutOfBoundsError>;
fn py_index(self, db: &'db dyn Db, index: i32) -> Result<Self::Item, OutOfBoundsError>;
}
fn from_nonnegative_i32(index: i32) -> usize {
@ -39,13 +41,13 @@ enum Position {
AfterEnd,
}
enum Nth {
pub(crate) enum Nth {
FromStart(usize),
FromEnd(usize),
}
impl Nth {
fn from_index(index: i32) -> Self {
pub(crate) fn from_index(index: i32) -> Self {
if index >= 0 {
Nth::FromStart(from_nonnegative_i32(index))
} else {
@ -75,13 +77,26 @@ impl Nth {
}
}
impl<I, T> PyIndex for T
impl<'db, T> PyIndex<'db> for &'db [T] {
type Item = &'db T;
fn py_index(self, _db: &'db dyn Db, index: i32) -> Result<&'db T, OutOfBoundsError> {
match Nth::from_index(index) {
Nth::FromStart(nth) => self.get(nth).ok_or(OutOfBoundsError),
Nth::FromEnd(nth_rev) => (self.len().checked_sub(nth_rev + 1))
.map(|idx| &self[idx])
.ok_or(OutOfBoundsError),
}
}
}
impl<'db, I: 'db, T> PyIndex<'db> for &mut T
where
T: DoubleEndedIterator<Item = I>,
{
type Item = I;
fn py_index(&mut self, index: i32) -> Result<I, OutOfBoundsError> {
fn py_index(self, _db: &'db dyn Db, index: i32) -> Result<I, OutOfBoundsError> {
match Nth::from_index(index) {
Nth::FromStart(nth) => self.nth(nth).ok_or(OutOfBoundsError),
Nth::FromEnd(nth_rev) => self.nth_back(nth_rev).ok_or(OutOfBoundsError),
@ -92,32 +107,28 @@ where
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct StepSizeZeroError;
pub(crate) trait PySlice {
type Item;
pub(crate) trait PySlice<'db> {
type Item: 'db;
fn py_slice(
&self,
&'db self,
db: &'db dyn Db,
start: Option<i32>,
stop: Option<i32>,
step: Option<i32>,
) -> Result<
Either<impl Iterator<Item = &Self::Item>, impl Iterator<Item = &Self::Item>>,
StepSizeZeroError,
>;
) -> Result<impl Iterator<Item = &'db Self::Item>, StepSizeZeroError>;
}
impl<T> PySlice for [T] {
impl<'db, T: 'db> PySlice<'db> for [T] {
type Item = T;
fn py_slice(
&self,
&'db self,
_db: &'db dyn Db,
start: Option<i32>,
stop: Option<i32>,
step_int: Option<i32>,
) -> Result<
Either<impl Iterator<Item = &Self::Item>, impl Iterator<Item = &Self::Item>>,
StepSizeZeroError,
> {
) -> Result<impl Iterator<Item = &'db Self::Item>, StepSizeZeroError> {
let step_int = step_int.unwrap_or(1);
if step_int == 0 {
return Err(StepSizeZeroError);
@ -194,6 +205,8 @@ impl<T> PySlice for [T] {
#[cfg(test)]
#[expect(clippy::redundant_clone)]
mod tests {
use crate::Db;
use crate::db::tests::setup_db;
use crate::util::subscript::{OutOfBoundsError, StepSizeZeroError};
use super::{PyIndex, PySlice};
@ -201,302 +214,387 @@ mod tests {
#[test]
fn py_index_empty() {
let db = setup_db();
let iter = std::iter::empty::<char>();
assert_eq!(iter.clone().py_index(0), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(-1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(i32::MIN), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(i32::MAX), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, 0), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, -1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, i32::MIN), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, i32::MAX), Err(OutOfBoundsError));
}
#[test]
fn py_index_single_element() {
let db = setup_db();
let iter = ['a'].into_iter();
assert_eq!(iter.clone().py_index(0), Ok('a'));
assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(-1), Ok('a'));
assert_eq!(iter.clone().py_index(-2), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, 0), Ok('a'));
assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, -1), Ok('a'));
assert_eq!(iter.clone().py_index(&db, -2), Err(OutOfBoundsError));
}
#[test]
fn py_index_more_elements() {
let db = setup_db();
let iter = ['a', 'b', 'c', 'd', 'e'].into_iter();
assert_eq!(iter.clone().py_index(0), Ok('a'));
assert_eq!(iter.clone().py_index(1), Ok('b'));
assert_eq!(iter.clone().py_index(4), Ok('e'));
assert_eq!(iter.clone().py_index(5), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, 0), Ok('a'));
assert_eq!(iter.clone().py_index(&db, 1), Ok('b'));
assert_eq!(iter.clone().py_index(&db, 4), Ok('e'));
assert_eq!(iter.clone().py_index(&db, 5), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(-1), Ok('e'));
assert_eq!(iter.clone().py_index(-2), Ok('d'));
assert_eq!(iter.clone().py_index(-5), Ok('a'));
assert_eq!(iter.clone().py_index(-6), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, -1), Ok('e'));
assert_eq!(iter.clone().py_index(&db, -2), Ok('d'));
assert_eq!(iter.clone().py_index(&db, -5), Ok('a'));
assert_eq!(iter.clone().py_index(&db, -6), Err(OutOfBoundsError));
}
#[test]
fn py_index_uses_full_index_range() {
let db = setup_db();
let iter = 0..=u32::MAX;
// u32::MAX - |i32::MIN| + 1 = 2^32 - 1 - 2^31 + 1 = 2^31
assert_eq!(iter.clone().py_index(i32::MIN), Ok(2u32.pow(31)));
assert_eq!(iter.clone().py_index(-2), Ok(u32::MAX - 2 + 1));
assert_eq!(iter.clone().py_index(-1), Ok(u32::MAX - 1 + 1));
assert_eq!(iter.clone().py_index(&db, i32::MIN), Ok(2u32.pow(31)));
assert_eq!(iter.clone().py_index(&db, -2), Ok(u32::MAX - 2 + 1));
assert_eq!(iter.clone().py_index(&db, -1), Ok(u32::MAX - 1 + 1));
assert_eq!(iter.clone().py_index(0), Ok(0));
assert_eq!(iter.clone().py_index(1), Ok(1));
assert_eq!(iter.clone().py_index(i32::MAX), Ok(i32::MAX as u32));
assert_eq!(iter.clone().py_index(&db, 0), Ok(0));
assert_eq!(iter.clone().py_index(&db, 1), Ok(1));
assert_eq!(iter.clone().py_index(&db, i32::MAX), Ok(i32::MAX as u32));
}
#[track_caller]
fn assert_eq_slice<const N: usize, const M: usize>(
db: &dyn Db,
input: &[char; N],
start: Option<i32>,
stop: Option<i32>,
step: Option<i32>,
expected: &[char; M],
) {
assert_equal(input.py_slice(start, stop, step).unwrap(), expected.iter());
assert_equal(
input.py_slice(db, start, stop, step).unwrap(),
expected.iter(),
);
}
#[test]
fn py_slice_empty_input() {
let db = setup_db();
let input = [];
assert_eq_slice(&input, None, None, None, &[]);
assert_eq_slice(&input, Some(0), None, None, &[]);
assert_eq_slice(&input, None, Some(0), None, &[]);
assert_eq_slice(&input, Some(0), Some(0), None, &[]);
assert_eq_slice(&input, Some(-5), Some(-5), None, &[]);
assert_eq_slice(&input, None, None, Some(-1), &[]);
assert_eq_slice(&input, None, None, Some(2), &[]);
assert_eq_slice(&db, &input, None, None, None, &[]);
assert_eq_slice(&db, &input, Some(0), None, None, &[]);
assert_eq_slice(&db, &input, None, Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(-5), Some(-5), None, &[]);
assert_eq_slice(&db, &input, None, None, Some(-1), &[]);
assert_eq_slice(&db, &input, None, None, Some(2), &[]);
}
#[test]
fn py_slice_single_element_input() {
let db = setup_db();
let input = ['a'];
assert_eq_slice(&input, None, None, None, &['a']);
assert_eq_slice(&db, &input, None, None, None, &['a']);
assert_eq_slice(&input, Some(0), None, None, &['a']);
assert_eq_slice(&input, None, Some(0), None, &[]);
assert_eq_slice(&input, Some(0), Some(0), None, &[]);
assert_eq_slice(&input, Some(0), Some(1), None, &['a']);
assert_eq_slice(&input, Some(0), Some(2), None, &['a']);
assert_eq_slice(&db, &input, Some(0), None, None, &['a']);
assert_eq_slice(&db, &input, None, Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(0), Some(2), None, &['a']);
assert_eq_slice(&input, Some(-1), None, None, &['a']);
assert_eq_slice(&input, Some(-1), Some(-1), None, &[]);
assert_eq_slice(&input, Some(-1), Some(0), None, &[]);
assert_eq_slice(&input, Some(-1), Some(1), None, &['a']);
assert_eq_slice(&input, Some(-1), Some(2), None, &['a']);
assert_eq_slice(&input, None, Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(-1), None, None, &['a']);
assert_eq_slice(&db, &input, Some(-1), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(-1), Some(2), None, &['a']);
assert_eq_slice(&db, &input, None, Some(-1), None, &[]);
assert_eq_slice(&input, Some(-2), None, None, &['a']);
assert_eq_slice(&input, Some(-2), Some(-1), None, &[]);
assert_eq_slice(&input, Some(-2), Some(0), None, &[]);
assert_eq_slice(&input, Some(-2), Some(1), None, &['a']);
assert_eq_slice(&input, Some(-2), Some(2), None, &['a']);
assert_eq_slice(&db, &input, Some(-2), None, None, &['a']);
assert_eq_slice(&db, &input, Some(-2), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(-2), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(-2), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(-2), Some(2), None, &['a']);
}
#[test]
fn py_slice_nonnegative_indices() {
let db = setup_db();
let input = ['a', 'b', 'c', 'd', 'e'];
assert_eq_slice(&input, None, Some(0), None, &[]);
assert_eq_slice(&input, None, Some(1), None, &['a']);
assert_eq_slice(&input, None, Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, None, None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, None, Some(0), None, &[]);
assert_eq_slice(&db, &input, None, Some(1), None, &['a']);
assert_eq_slice(&db, &input, None, Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, None, None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(0), Some(0), None, &[]);
assert_eq_slice(&input, Some(0), Some(1), None, &['a']);
assert_eq_slice(&input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(0), Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(0), Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(
&db,
&input,
Some(0),
Some(5),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(
&db,
&input,
Some(0),
Some(6),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(1), Some(0), None, &[]);
assert_eq_slice(&input, Some(1), Some(1), None, &[]);
assert_eq_slice(&input, Some(1), Some(2), None, &['b']);
assert_eq_slice(&input, Some(1), Some(4), None, &['b', 'c', 'd']);
assert_eq_slice(&input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(1), None, None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(1), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(1), Some(1), None, &[]);
assert_eq_slice(&db, &input, Some(1), Some(2), None, &['b']);
assert_eq_slice(&db, &input, Some(1), Some(4), None, &['b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(1), None, None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(4), Some(0), None, &[]);
assert_eq_slice(&input, Some(4), Some(4), None, &[]);
assert_eq_slice(&input, Some(4), Some(5), None, &['e']);
assert_eq_slice(&input, Some(4), Some(6), None, &['e']);
assert_eq_slice(&input, Some(4), None, None, &['e']);
assert_eq_slice(&db, &input, Some(4), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(4), Some(4), None, &[]);
assert_eq_slice(&db, &input, Some(4), Some(5), None, &['e']);
assert_eq_slice(&db, &input, Some(4), Some(6), None, &['e']);
assert_eq_slice(&db, &input, Some(4), None, None, &['e']);
assert_eq_slice(&input, Some(5), Some(0), None, &[]);
assert_eq_slice(&input, Some(5), Some(5), None, &[]);
assert_eq_slice(&input, Some(5), Some(6), None, &[]);
assert_eq_slice(&input, Some(5), None, None, &[]);
assert_eq_slice(&db, &input, Some(5), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(5), Some(5), None, &[]);
assert_eq_slice(&db, &input, Some(5), Some(6), None, &[]);
assert_eq_slice(&db, &input, Some(5), None, None, &[]);
assert_eq_slice(&input, Some(6), Some(0), None, &[]);
assert_eq_slice(&input, Some(6), Some(6), None, &[]);
assert_eq_slice(&input, Some(6), None, None, &[]);
assert_eq_slice(&db, &input, Some(6), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(6), Some(6), None, &[]);
assert_eq_slice(&db, &input, Some(6), None, None, &[]);
}
#[test]
fn py_slice_negative_indices() {
let db = setup_db();
let input = ['a', 'b', 'c', 'd', 'e'];
assert_eq_slice(&input, Some(-6), None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(-6), Some(-4), None, &['a']);
assert_eq_slice(&input, Some(-6), Some(-5), None, &[]);
assert_eq_slice(&input, Some(-6), Some(-6), None, &[]);
assert_eq_slice(&input, Some(-6), Some(-10), None, &[]);
assert_eq_slice(
&db,
&input,
Some(-6),
None,
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-6), Some(-4), None, &['a']);
assert_eq_slice(&db, &input, Some(-6), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(-6), Some(-6), None, &[]);
assert_eq_slice(&db, &input, Some(-6), Some(-10), None, &[]);
assert_eq_slice(&input, Some(-5), None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(-5), Some(-4), None, &['a']);
assert_eq_slice(&input, Some(-5), Some(-5), None, &[]);
assert_eq_slice(&input, Some(-5), Some(-6), None, &[]);
assert_eq_slice(&input, Some(-5), Some(-10), None, &[]);
assert_eq_slice(
&db,
&input,
Some(-5),
None,
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-5), Some(-4), None, &['a']);
assert_eq_slice(&db, &input, Some(-5), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(-5), Some(-6), None, &[]);
assert_eq_slice(&db, &input, Some(-5), Some(-10), None, &[]);
assert_eq_slice(&input, Some(-4), None, None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-4), Some(-1), None, &['b', 'c', 'd']);
assert_eq_slice(&input, Some(-4), Some(-3), None, &['b']);
assert_eq_slice(&input, Some(-4), Some(-4), None, &[]);
assert_eq_slice(&input, Some(-4), Some(-10), None, &[]);
assert_eq_slice(&db, &input, Some(-4), None, None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(-4), Some(-1), None, &['b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-4), Some(-3), None, &['b']);
assert_eq_slice(&db, &input, Some(-4), Some(-4), None, &[]);
assert_eq_slice(&db, &input, Some(-4), Some(-10), None, &[]);
assert_eq_slice(&input, Some(-1), None, None, &['e']);
assert_eq_slice(&input, Some(-1), Some(-1), None, &[]);
assert_eq_slice(&input, Some(-1), Some(-10), None, &[]);
assert_eq_slice(&db, &input, Some(-1), None, None, &['e']);
assert_eq_slice(&db, &input, Some(-1), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(-10), None, &[]);
assert_eq_slice(&input, None, Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, None, Some(-4), None, &['a']);
assert_eq_slice(&input, None, Some(-5), None, &[]);
assert_eq_slice(&input, None, Some(-6), None, &[]);
assert_eq_slice(&db, &input, None, Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, None, Some(-4), None, &['a']);
assert_eq_slice(&db, &input, None, Some(-5), None, &[]);
assert_eq_slice(&db, &input, None, Some(-6), None, &[]);
}
#[test]
fn py_slice_mixed_positive_negative_indices() {
let db = setup_db();
let input = ['a', 'b', 'c', 'd', 'e'];
assert_eq_slice(&input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(1), Some(-1), None, &['b', 'c', 'd']);
assert_eq_slice(&input, Some(3), Some(-1), None, &['d']);
assert_eq_slice(&input, Some(4), Some(-1), None, &[]);
assert_eq_slice(&input, Some(5), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(1), Some(-1), None, &['b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(3), Some(-1), None, &['d']);
assert_eq_slice(&db, &input, Some(4), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(5), Some(-1), None, &[]);
assert_eq_slice(&input, Some(0), Some(-4), None, &['a']);
assert_eq_slice(&input, Some(1), Some(-4), None, &[]);
assert_eq_slice(&input, Some(3), Some(-4), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(-4), None, &['a']);
assert_eq_slice(&db, &input, Some(1), Some(-4), None, &[]);
assert_eq_slice(&db, &input, Some(3), Some(-4), None, &[]);
assert_eq_slice(&input, Some(0), Some(-5), None, &[]);
assert_eq_slice(&input, Some(1), Some(-5), None, &[]);
assert_eq_slice(&input, Some(3), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(1), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(3), Some(-5), None, &[]);
assert_eq_slice(&input, Some(0), Some(-6), None, &[]);
assert_eq_slice(&input, Some(1), Some(-6), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(-6), None, &[]);
assert_eq_slice(&db, &input, Some(1), Some(-6), None, &[]);
assert_eq_slice(&input, Some(-6), Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-6), Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(-6), Some(1), None, &['a']);
assert_eq_slice(&input, Some(-6), Some(0), None, &[]);
assert_eq_slice(
&db,
&input,
Some(-6),
Some(6),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(
&db,
&input,
Some(-6),
Some(5),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-6), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(-6), Some(0), None, &[]);
assert_eq_slice(&input, Some(-5), Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-5), Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(-5), Some(1), None, &['a']);
assert_eq_slice(&input, Some(-5), Some(0), None, &[]);
assert_eq_slice(
&db,
&input,
Some(-5),
Some(6),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(
&db,
&input,
Some(-5),
Some(5),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-5), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(-5), Some(0), None, &[]);
assert_eq_slice(&input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-4), Some(4), None, &['b', 'c', 'd']);
assert_eq_slice(&input, Some(-4), Some(2), None, &['b']);
assert_eq_slice(&input, Some(-4), Some(1), None, &[]);
assert_eq_slice(&input, Some(-4), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(-4), Some(4), None, &['b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-4), Some(2), None, &['b']);
assert_eq_slice(&db, &input, Some(-4), Some(1), None, &[]);
assert_eq_slice(&db, &input, Some(-4), Some(0), None, &[]);
assert_eq_slice(&input, Some(-1), Some(6), None, &['e']);
assert_eq_slice(&input, Some(-1), Some(5), None, &['e']);
assert_eq_slice(&input, Some(-1), Some(4), None, &[]);
assert_eq_slice(&input, Some(-1), Some(1), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(6), None, &['e']);
assert_eq_slice(&db, &input, Some(-1), Some(5), None, &['e']);
assert_eq_slice(&db, &input, Some(-1), Some(4), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(1), None, &[]);
}
#[test]
fn py_slice_step_forward() {
let db = setup_db();
// indices: 0 1 2 3 4 5 6
let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
// Step size zero is invalid:
assert!(matches!(
input.py_slice(None, None, Some(0)),
input.py_slice(&db, None, None, Some(0)),
Err(StepSizeZeroError)
));
assert!(matches!(
input.py_slice(Some(0), Some(5), Some(0)),
input.py_slice(&db, Some(0), Some(5), Some(0)),
Err(StepSizeZeroError)
));
assert!(matches!(
input.py_slice(Some(0), Some(0), Some(0)),
input.py_slice(&db, Some(0), Some(0), Some(0)),
Err(StepSizeZeroError)
));
assert_eq_slice(&input, Some(0), Some(8), Some(2), &['a', 'c', 'e', 'g']);
assert_eq_slice(&input, Some(0), Some(7), Some(2), &['a', 'c', 'e', 'g']);
assert_eq_slice(&input, Some(0), Some(6), Some(2), &['a', 'c', 'e']);
assert_eq_slice(&input, Some(0), Some(5), Some(2), &['a', 'c', 'e']);
assert_eq_slice(&input, Some(0), Some(4), Some(2), &['a', 'c']);
assert_eq_slice(&input, Some(0), Some(3), Some(2), &['a', 'c']);
assert_eq_slice(&input, Some(0), Some(2), Some(2), &['a']);
assert_eq_slice(&input, Some(0), Some(1), Some(2), &['a']);
assert_eq_slice(&input, Some(0), Some(0), Some(2), &[]);
assert_eq_slice(&input, Some(1), Some(5), Some(2), &['b', 'd']);
assert_eq_slice(
&db,
&input,
Some(0),
Some(8),
Some(2),
&['a', 'c', 'e', 'g'],
);
assert_eq_slice(
&db,
&input,
Some(0),
Some(7),
Some(2),
&['a', 'c', 'e', 'g'],
);
assert_eq_slice(&db, &input, Some(0), Some(6), Some(2), &['a', 'c', 'e']);
assert_eq_slice(&db, &input, Some(0), Some(5), Some(2), &['a', 'c', 'e']);
assert_eq_slice(&db, &input, Some(0), Some(4), Some(2), &['a', 'c']);
assert_eq_slice(&db, &input, Some(0), Some(3), Some(2), &['a', 'c']);
assert_eq_slice(&db, &input, Some(0), Some(2), Some(2), &['a']);
assert_eq_slice(&db, &input, Some(0), Some(1), Some(2), &['a']);
assert_eq_slice(&db, &input, Some(0), Some(0), Some(2), &[]);
assert_eq_slice(&db, &input, Some(1), Some(5), Some(2), &['b', 'd']);
assert_eq_slice(&input, Some(0), Some(7), Some(3), &['a', 'd', 'g']);
assert_eq_slice(&input, Some(0), Some(6), Some(3), &['a', 'd']);
assert_eq_slice(&db, &input, Some(0), Some(7), Some(3), &['a', 'd', 'g']);
assert_eq_slice(&db, &input, Some(0), Some(6), Some(3), &['a', 'd']);
assert_eq_slice(&input, Some(0), None, Some(10), &['a']);
assert_eq_slice(&db, &input, Some(0), None, Some(10), &['a']);
}
#[test]
fn py_slice_step_backward() {
let db = setup_db();
// indices: 0 1 2 3 4 5 6
let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
assert_eq_slice(&input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']);
assert_eq_slice(&input, Some(4), Some(0), Some(-2), &['e', 'c']);
assert_eq_slice(&input, Some(3), Some(0), Some(-2), &['d', 'b']);
assert_eq_slice(&input, Some(2), Some(0), Some(-2), &['c']);
assert_eq_slice(&input, Some(1), Some(0), Some(-2), &['b']);
assert_eq_slice(&input, Some(0), Some(0), Some(-2), &[]);
assert_eq_slice(&db, &input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&db, &input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&db, &input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']);
assert_eq_slice(&db, &input, Some(4), Some(0), Some(-2), &['e', 'c']);
assert_eq_slice(&db, &input, Some(3), Some(0), Some(-2), &['d', 'b']);
assert_eq_slice(&db, &input, Some(2), Some(0), Some(-2), &['c']);
assert_eq_slice(&db, &input, Some(1), Some(0), Some(-2), &['b']);
assert_eq_slice(&db, &input, Some(0), Some(0), Some(-2), &[]);
assert_eq_slice(&input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']);
assert_eq_slice(&input, None, None, Some(-2), &['g', 'e', 'c', 'a']);
assert_eq_slice(&input, None, Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&db, &input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']);
assert_eq_slice(&db, &input, None, None, Some(-2), &['g', 'e', 'c', 'a']);
assert_eq_slice(&db, &input, None, Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&input, Some(5), Some(1), Some(-2), &['f', 'd']);
assert_eq_slice(&input, Some(5), Some(2), Some(-2), &['f', 'd']);
assert_eq_slice(&input, Some(5), Some(3), Some(-2), &['f']);
assert_eq_slice(&input, Some(5), Some(4), Some(-2), &['f']);
assert_eq_slice(&input, Some(5), Some(5), Some(-2), &[]);
assert_eq_slice(&db, &input, Some(5), Some(1), Some(-2), &['f', 'd']);
assert_eq_slice(&db, &input, Some(5), Some(2), Some(-2), &['f', 'd']);
assert_eq_slice(&db, &input, Some(5), Some(3), Some(-2), &['f']);
assert_eq_slice(&db, &input, Some(5), Some(4), Some(-2), &['f']);
assert_eq_slice(&db, &input, Some(5), Some(5), Some(-2), &[]);
assert_eq_slice(&input, Some(6), None, Some(-3), &['g', 'd', 'a']);
assert_eq_slice(&input, Some(6), Some(0), Some(-3), &['g', 'd']);
assert_eq_slice(&db, &input, Some(6), None, Some(-3), &['g', 'd', 'a']);
assert_eq_slice(&db, &input, Some(6), Some(0), Some(-3), &['g', 'd']);
assert_eq_slice(&input, Some(7), None, Some(-10), &['g']);
assert_eq_slice(&db, &input, Some(7), None, Some(-10), &['g']);
assert_eq_slice(&input, Some(-6), Some(-9), Some(-1), &['b', 'a']);
assert_eq_slice(&input, Some(-6), Some(-8), Some(-1), &['b', 'a']);
assert_eq_slice(&input, Some(-6), Some(-7), Some(-1), &['b']);
assert_eq_slice(&input, Some(-6), Some(-6), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-6), Some(-9), Some(-1), &['b', 'a']);
assert_eq_slice(&db, &input, Some(-6), Some(-8), Some(-1), &['b', 'a']);
assert_eq_slice(&db, &input, Some(-6), Some(-7), Some(-1), &['b']);
assert_eq_slice(&db, &input, Some(-6), Some(-6), Some(-1), &[]);
assert_eq_slice(&input, Some(-7), Some(-9), Some(-1), &['a']);
assert_eq_slice(&db, &input, Some(-7), Some(-9), Some(-1), &['a']);
assert_eq_slice(&input, Some(-8), Some(-9), Some(-1), &[]);
assert_eq_slice(&input, Some(-9), Some(-9), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-8), Some(-9), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-9), Some(-9), Some(-1), &[]);
assert_eq_slice(&input, Some(-6), Some(-2), Some(-1), &[]);
assert_eq_slice(&input, Some(-9), Some(-6), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-6), Some(-2), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-9), Some(-6), Some(-1), &[]);
}
}