mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Splat variadic arguments into parameter list (#18996)
This PR updates our call binding logic to handle splatted arguments. Complicating matters is that we have separated call bind analysis into two phases: parameter matching and type checking. Parameter matching looks at the arity of the function signature and call site, and assigns arguments to parameters. Importantly, we don't yet know the type of each argument! This is needed so that we can decide whether to infer the type of each argument as a type form or value form, depending on the requirements of the parameter that the argument was matched to. This is an issue when splatting an argument, since we need to know how many elements the splatted argument contains to know how many positional parameters to match it against. And to know how many elements the splatted argument has, we need to know its type. To get around this, we now make the assumption that splatted arguments can only be used with value-form parameters. (If you end up splatting an argument into a type-form parameter, we will silently pass in its value-form type instead.) That allows us to preemptively infer the (value-form) type of any splatted argument, so that we have its arity available during parameter matching. We defer inference of non-splatted arguments until after parameter matching has finished, as before. We reuse a lot of the new tuple machinery to make this happen — in particular resizing the tuple spec representing the number of arguments passed in with the tuple length representing the number of parameters the splat was matched with. This work also shows that we might need to change how we are performing argument expansion during overload resolution. At the moment, when we expand parameters, we assume that each argument will still be matched to the same parameters as before, and only retry the type-checking phase. With splatted arguments, this is no longer the case, since the inferred arity of each union element might be different than the arity of the union as a whole, which can affect how many parameters the splatted argument is matched to. See the regression test case in `mdtest/call/function.md` for more details.
This commit is contained in:
parent
9d5ecacdc5
commit
7673d46b71
10 changed files with 694 additions and 130 deletions
|
@ -69,6 +69,246 @@ def _(flag: bool):
|
|||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
## Splatted arguments
|
||||
|
||||
### Unknown argument length
|
||||
|
||||
```py
|
||||
def takes_zero() -> None: ...
|
||||
def takes_one(x: int) -> None: ...
|
||||
def takes_two(x: int, y: int) -> None: ...
|
||||
def takes_two_positional_only(x: int, y: int, /) -> None: ...
|
||||
def takes_two_different(x: int, y: str) -> None: ...
|
||||
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
|
||||
def takes_at_least_zero(*args) -> None: ...
|
||||
def takes_at_least_one(x: int, *args) -> None: ...
|
||||
def takes_at_least_two(x: int, y: int, *args) -> None: ...
|
||||
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(args: list[int]) -> None:
|
||||
takes_zero(*args)
|
||||
takes_one(*args)
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, ...]) -> None:
|
||||
takes_zero(*args)
|
||||
takes_one(*args)
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
```
|
||||
|
||||
### Fixed-length tuple argument
|
||||
|
||||
```py
|
||||
def takes_zero() -> None: ...
|
||||
def takes_one(x: int) -> None: ...
|
||||
def takes_two(x: int, y: int) -> None: ...
|
||||
def takes_two_positional_only(x: int, y: int, /) -> None: ...
|
||||
def takes_two_different(x: int, y: str) -> None: ...
|
||||
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
|
||||
def takes_at_least_zero(*args) -> None: ...
|
||||
def takes_at_least_one(x: int, *args) -> None: ...
|
||||
def takes_at_least_two(x: int, y: int, *args) -> None: ...
|
||||
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(args: tuple[int]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args)
|
||||
takes_two(*args) # error: [missing-argument]
|
||||
takes_two_positional_only(*args) # error: [missing-argument]
|
||||
takes_two_different(*args) # error: [missing-argument]
|
||||
takes_two_different_positional_only(*args) # error: [missing-argument]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [missing-argument]
|
||||
takes_at_least_two_positional_only(*args) # error: [missing-argument]
|
||||
|
||||
def _(args: tuple[int, int]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, str]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args) # error: [invalid-argument-type]
|
||||
takes_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_two_different(*args)
|
||||
takes_two_different_positional_only(*args)
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Mixed tuple argument
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
def takes_zero() -> None: ...
|
||||
def takes_one(x: int) -> None: ...
|
||||
def takes_two(x: int, y: int) -> None: ...
|
||||
def takes_two_positional_only(x: int, y: int, /) -> None: ...
|
||||
def takes_two_different(x: int, y: str) -> None: ...
|
||||
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
|
||||
def takes_at_least_zero(*args) -> None: ...
|
||||
def takes_at_least_one(x: int, *args) -> None: ...
|
||||
def takes_at_least_two(x: int, y: int, *args) -> None: ...
|
||||
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(args: tuple[int, *tuple[int, ...]]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args)
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, *tuple[str, ...]]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args)
|
||||
takes_two(*args) # error: [invalid-argument-type]
|
||||
takes_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_two_different(*args)
|
||||
takes_two_different_positional_only(*args)
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
|
||||
def _(args: tuple[int, int, *tuple[int, ...]]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, int, *tuple[str, ...]]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, *tuple[int, ...], int]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, *tuple[str, ...], int]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args) # error: [invalid-argument-type]
|
||||
takes_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_two_different(*args)
|
||||
takes_two_different_positional_only(*args)
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Argument expansion regression
|
||||
|
||||
This is a regression that was highlighted by the ecosystem check, which shows that we might need to
|
||||
rethink how we perform argument expansion during overload resolution. In particular, we might need
|
||||
to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry
|
||||
`check_types`.
|
||||
|
||||
The issue is that argument expansion might produce a splatted value with a different arity than what
|
||||
we originally inferred for the unexpanded value, and that in turn can affect which parameters the
|
||||
splatted value is matched with.
|
||||
|
||||
The first example correctly produces an error. The `tuple[int, str]` union element has a precise
|
||||
arity of two, and so parameter matching chooses the first overload. The second element of the tuple
|
||||
does not match the second parameter type, which yielding an `invalid-argument-type` error.
|
||||
|
||||
The third example should produce the same error. However, because we have a union, we do not see the
|
||||
precise arity of each union element during parameter matching. Instead, we infer an arity of "zero
|
||||
or more" for the union as a whole, and use that less precise arity when matching parameters. We
|
||||
therefore consider the second overload to still be a potential candidate for the `tuple[int, str]`
|
||||
union element. During type checking, we have to force the arity of each union element to match the
|
||||
inferred arity of the union as a whole (turning `tuple[int, str]` into `tuple[int | str, ...]`).
|
||||
That less precise tuple type-checks successfully against the second overload, making us incorrectly
|
||||
think that `tuple[int, str]` is a valid splatted call.
|
||||
|
||||
If we update argument expansion to retry parameter matching with the precise arity of each union
|
||||
element, we will correctly rule out the second overload for `tuple[int, str]`, just like we do when
|
||||
splatting that tuple directly (instead of as part of a union).
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def f(x: int, y: int) -> None: ...
|
||||
@overload
|
||||
def f(x: int, y: str, z: int) -> None: ...
|
||||
def f(*args): ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(t: tuple[int, str]) -> None:
|
||||
f(*t) # error: [invalid-argument-type]
|
||||
|
||||
def _(t: tuple[int, str, int]) -> None:
|
||||
f(*t)
|
||||
|
||||
def _(t: tuple[int, str] | tuple[int, str, int]) -> None:
|
||||
# TODO: error: [invalid-argument-type]
|
||||
f(*t)
|
||||
```
|
||||
|
||||
## Wrong argument type
|
||||
|
||||
### Positional argument, positional-or-keyword parameter
|
||||
|
|
|
@ -5,6 +5,12 @@ with one or more overloads. This document describes the algorithm that it uses f
|
|||
matching, which is the same as the one mentioned in the
|
||||
[spec](https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation).
|
||||
|
||||
Note that all of the examples that involve positional parameters are tested multiple times: once
|
||||
with the parameters matched with individual positional arguments, and once with the parameters
|
||||
matched with a single positional argument that is splatted into the argument list. Overload
|
||||
resolution is performed _after_ splatted arguments have been expanded, and so both approaches (TODO:
|
||||
should) produce the same results.
|
||||
|
||||
## Arity check
|
||||
|
||||
The first step is to perform arity check. The non-overloaded cases are described in the
|
||||
|
@ -26,10 +32,15 @@ from overloaded import f
|
|||
|
||||
# These match a single overload
|
||||
reveal_type(f()) # revealed: None
|
||||
reveal_type(f(*())) # revealed: None
|
||||
|
||||
reveal_type(f(1)) # revealed: int
|
||||
reveal_type(f(*(1,))) # revealed: int
|
||||
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f("a", "b")) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(*("a", "b"))) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Type checking
|
||||
|
@ -59,8 +70,13 @@ which filters out all but the matching overload:
|
|||
from overloaded import f
|
||||
|
||||
reveal_type(f(1)) # revealed: int
|
||||
reveal_type(f(*(1,))) # revealed: int
|
||||
|
||||
reveal_type(f("a")) # revealed: str
|
||||
reveal_type(f(*("a",))) # revealed: str
|
||||
|
||||
reveal_type(f(b"b")) # revealed: bytes
|
||||
reveal_type(f(*(b"b",))) # revealed: bytes
|
||||
```
|
||||
|
||||
### Single match error
|
||||
|
@ -88,9 +104,12 @@ from typing_extensions import reveal_type
|
|||
from overloaded import f
|
||||
|
||||
reveal_type(f()) # revealed: None
|
||||
reveal_type(f(*())) # revealed: None
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`"
|
||||
reveal_type(f("a")) # revealed: Unknown
|
||||
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`"
|
||||
reveal_type(f(*("a",))) # revealed: Unknown
|
||||
```
|
||||
|
||||
More examples of this diagnostic can be found in the
|
||||
|
@ -117,10 +136,15 @@ from overloaded import A, B, f
|
|||
|
||||
# These calls pass the arity check, and type checking matches both overloads:
|
||||
reveal_type(f(A())) # revealed: A
|
||||
reveal_type(f(*(A(),))) # revealed: A
|
||||
|
||||
reveal_type(f(B())) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(B(),))) # revealed: Unknown
|
||||
|
||||
# But, in this case, the arity check filters out the first overload, so we only have one match:
|
||||
reveal_type(f(B(), 1)) # revealed: B
|
||||
reveal_type(f(*(B(), 1))) # revealed: B
|
||||
```
|
||||
|
||||
## Argument type expansion
|
||||
|
@ -155,8 +179,13 @@ from overloaded import A, B, C, f
|
|||
|
||||
def _(ab: A | B, ac: A | C, bc: B | C):
|
||||
reveal_type(f(ab)) # revealed: A | B
|
||||
reveal_type(f(*(ab,))) # revealed: A | B
|
||||
|
||||
reveal_type(f(bc)) # revealed: B | C
|
||||
reveal_type(f(*(bc,))) # revealed: B | C
|
||||
|
||||
reveal_type(f(ac)) # revealed: A | C
|
||||
reveal_type(f(*(ac,))) # revealed: A | C
|
||||
```
|
||||
|
||||
### Expanding first argument
|
||||
|
@ -189,11 +218,15 @@ from overloaded import A, B, C, D, f
|
|||
|
||||
def _(a_b: A | B):
|
||||
reveal_type(f(a_b, C())) # revealed: A | C
|
||||
reveal_type(f(*(a_b, C()))) # revealed: A | C
|
||||
|
||||
reveal_type(f(a_b, D())) # revealed: B | D
|
||||
reveal_type(f(*(a_b, D()))) # revealed: B | D
|
||||
|
||||
# But, if it doesn't, it should expand the second argument and try again:
|
||||
def _(a_b: A | B, c_d: C | D):
|
||||
reveal_type(f(a_b, c_d)) # revealed: A | B | C | D
|
||||
reveal_type(f(*(a_b, c_d))) # revealed: A | B | C | D
|
||||
```
|
||||
|
||||
### Expanding second argument
|
||||
|
@ -226,9 +259,12 @@ def _(a: A, bc: B | C, cd: C | D):
|
|||
# This also tests that partial matching works correctly as the argument type expansion results
|
||||
# in matching the first and second overloads, but not the third one.
|
||||
reveal_type(f(a, bc)) # revealed: B | C
|
||||
reveal_type(f(*(a, bc))) # revealed: B | C
|
||||
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(a, cd)) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(*(a, cd))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generics (legacy)
|
||||
|
@ -254,7 +290,16 @@ from overloaded import A, f
|
|||
|
||||
def _(x: int, y: A | int):
|
||||
reveal_type(f(x)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(x,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(y)) # revealed: A | int
|
||||
# TODO: revealed: A | int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(y,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generics (PEP 695)
|
||||
|
@ -283,7 +328,16 @@ from overloaded import B, f
|
|||
|
||||
def _(x: int, y: B | int):
|
||||
reveal_type(f(x)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(x,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(y)) # revealed: B | int
|
||||
# TODO: revealed: B | int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(y,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Expanding `bool`
|
||||
|
@ -307,8 +361,13 @@ from overloaded import f
|
|||
|
||||
def _(flag: bool):
|
||||
reveal_type(f(True)) # revealed: T
|
||||
reveal_type(f(*(True,))) # revealed: T
|
||||
|
||||
reveal_type(f(False)) # revealed: F
|
||||
reveal_type(f(*(False,))) # revealed: F
|
||||
|
||||
reveal_type(f(flag)) # revealed: T | F
|
||||
reveal_type(f(*(flag,))) # revealed: T | F
|
||||
```
|
||||
|
||||
### Expanding `tuple`
|
||||
|
@ -338,6 +397,7 @@ from overloaded import A, B, f
|
|||
|
||||
def _(x: tuple[A | B, int], y: tuple[int, bool]):
|
||||
reveal_type(f(x, y)) # revealed: A | B | C | D
|
||||
reveal_type(f(*(x, y))) # revealed: A | B | C | D
|
||||
```
|
||||
|
||||
### Expanding `type`
|
||||
|
@ -365,6 +425,7 @@ from overloaded import A, B, f
|
|||
def _(x: type[A | B]):
|
||||
reveal_type(x) # revealed: type[A] | type[B]
|
||||
reveal_type(f(x)) # revealed: A | B
|
||||
reveal_type(f(*(x,))) # revealed: A | B
|
||||
```
|
||||
|
||||
### Expanding enums
|
||||
|
@ -401,10 +462,19 @@ from overloaded import SomeEnum, A, B, C, f
|
|||
|
||||
def _(x: SomeEnum, y: Literal[SomeEnum.A, SomeEnum.C]):
|
||||
reveal_type(f(SomeEnum.A)) # revealed: A
|
||||
reveal_type(f(*(SomeEnum.A,))) # revealed: A
|
||||
|
||||
reveal_type(f(SomeEnum.B)) # revealed: B
|
||||
reveal_type(f(*(SomeEnum.B,))) # revealed: B
|
||||
|
||||
reveal_type(f(SomeEnum.C)) # revealed: C
|
||||
reveal_type(f(*(SomeEnum.C,))) # revealed: C
|
||||
|
||||
reveal_type(f(x)) # revealed: A | B | C
|
||||
reveal_type(f(*(x,))) # revealed: A | B | C
|
||||
|
||||
reveal_type(f(y)) # revealed: A | C
|
||||
reveal_type(f(*(y,))) # revealed: A | C
|
||||
```
|
||||
|
||||
#### Enum with single member
|
||||
|
@ -459,7 +529,7 @@ def _(missing: Literal[Missing.Value], missing_or_present: Literal[Missing.Value
|
|||
|
||||
#### Enum subclass without members
|
||||
|
||||
An `Enum` subclass without members should *not* be expanded:
|
||||
An `Enum` subclass without members should _not_ be expanded:
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
|
@ -493,9 +563,19 @@ from overloaded import MyEnumSubclass, ActualEnum, f
|
|||
|
||||
def _(actual_enum: ActualEnum, my_enum_instance: MyEnumSubclass):
|
||||
reveal_type(f(actual_enum)) # revealed: Both
|
||||
# TODO: revealed: Both
|
||||
reveal_type(f(*(actual_enum,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(ActualEnum.A)) # revealed: OnlyA
|
||||
# TODO: revealed: OnlyA
|
||||
reveal_type(f(*(ActualEnum.A,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(ActualEnum.B)) # revealed: OnlyB
|
||||
# TODO: revealed: OnlyB
|
||||
reveal_type(f(*(ActualEnum.B,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(my_enum_instance)) # revealed: MyEnumSubclass
|
||||
reveal_type(f(*(my_enum_instance,))) # revealed: MyEnumSubclass
|
||||
```
|
||||
|
||||
### No matching overloads
|
||||
|
@ -524,21 +604,22 @@ from overloaded import A, B, C, D, f
|
|||
|
||||
def _(ab: A | B, ac: A | C, cd: C | D):
|
||||
reveal_type(f(ab)) # revealed: A | B
|
||||
reveal_type(f(*(ab,))) # revealed: A | B
|
||||
|
||||
# The `[A | C]` argument list is expanded to `[A], [C]` where the first list matches the first
|
||||
# overload while the second list doesn't match any of the overloads, so we generate an
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(ac)) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(*(ac,))) # revealed: Unknown
|
||||
|
||||
# None of the expanded argument lists (`[C], [D]`) match any of the overloads, so we generate an
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(cd)) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(*(cd,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Filtering overloads with variadic arguments and parameters
|
||||
|
||||
TODO
|
||||
|
||||
## Filtering based on `Any` / `Unknown`
|
||||
|
||||
This is the step 5 of the overload call evaluation algorithm which specifies that:
|
||||
|
@ -573,10 +654,16 @@ from overloaded import f
|
|||
|
||||
# Anything other than `list` should match the last overload
|
||||
reveal_type(f(1)) # revealed: str
|
||||
reveal_type(f(*(1,))) # revealed: str
|
||||
|
||||
def _(list_int: list[int], list_any: list[Any]):
|
||||
reveal_type(f(list_int)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(list_any)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Single list argument (ambiguous)
|
||||
|
@ -604,16 +691,20 @@ from overloaded import f
|
|||
|
||||
# Anything other than `list` should match the last overload
|
||||
reveal_type(f(1)) # revealed: str
|
||||
reveal_type(f(*(1,))) # revealed: str
|
||||
|
||||
def _(list_int: list[int], list_any: list[Any]):
|
||||
# All materializations of `list[int]` are assignable to `list[int]`, so it matches the first
|
||||
# overload.
|
||||
reveal_type(f(list_int)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
|
||||
# All materializations of `list[Any]` are assignable to `list[int]` and `list[Any]`, but the
|
||||
# return type of first and second overloads are not equivalent, so the overload matching
|
||||
# is ambiguous.
|
||||
reveal_type(f(list_any)) # revealed: Unknown
|
||||
reveal_type(f(*(list_any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Single tuple argument
|
||||
|
@ -637,21 +728,33 @@ from typing import Any
|
|||
from overloaded import f
|
||||
|
||||
reveal_type(f("a")) # revealed: str
|
||||
reveal_type(f(*("a",))) # revealed: str
|
||||
|
||||
reveal_type(f((1, "b"))) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*((1, "b"),))) # revealed: Unknown
|
||||
|
||||
reveal_type(f((1, 2))) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*((1, 2),))) # revealed: Unknown
|
||||
|
||||
def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]):
|
||||
# All materializations are assignable to first overload, so second and third overloads are
|
||||
# eliminated
|
||||
reveal_type(f(int_str)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(int_str,))) # revealed: Unknown
|
||||
|
||||
# All materializations are assignable to second overload, so the third overload is eliminated;
|
||||
# the return type of first and second overload is equivalent
|
||||
reveal_type(f(int_any)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(int_any,))) # revealed: Unknown
|
||||
|
||||
# All materializations of `tuple[Any, Any]` are assignable to the parameters of all the
|
||||
# overloads, but the return types aren't equivalent, so the overload matching is ambiguous
|
||||
reveal_type(f(any_any)) # revealed: Unknown
|
||||
reveal_type(f(*(any_any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Multiple arguments
|
||||
|
@ -681,23 +784,32 @@ def _(list_int: list[int], list_any: list[Any], int_str: tuple[int, str], int_an
|
|||
# All materializations of both argument types are assignable to the first overload, so the
|
||||
# second and third overloads are filtered out
|
||||
reveal_type(f(list_int, int_str)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int, int_str))) # revealed: Unknown
|
||||
|
||||
# All materialization of first argument is assignable to first overload and for the second
|
||||
# argument, they're assignable to the second overload, so the third overload is filtered out
|
||||
reveal_type(f(list_int, int_any)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int, int_any))) # revealed: Unknown
|
||||
|
||||
# All materialization of first argument is assignable to second overload and for the second
|
||||
# argument, they're assignable to the first overload, so the third overload is filtered out
|
||||
reveal_type(f(list_any, int_str)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_any, int_str))) # revealed: Unknown
|
||||
|
||||
# All materializations of both arguments are assignable to the second overload, so the third
|
||||
# overload is filtered out
|
||||
reveal_type(f(list_any, int_any)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_any, int_any))) # revealed: Unknown
|
||||
|
||||
# All materializations of first argument is assignable to the second overload and for the second
|
||||
# argument, they're assignable to the third overload, so no overloads are filtered out; the
|
||||
# return types of the remaining overloads are not equivalent, so overload matching is ambiguous
|
||||
reveal_type(f(list_int, any_any)) # revealed: Unknown
|
||||
reveal_type(f(*(list_int, any_any))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `LiteralString` and `str`
|
||||
|
@ -722,11 +834,16 @@ from overloaded import f
|
|||
|
||||
def _(literal: LiteralString, string: str, any: Any):
|
||||
reveal_type(f(literal)) # revealed: LiteralString
|
||||
# TODO: revealed: LiteralString
|
||||
reveal_type(f(*(literal,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(string)) # revealed: str
|
||||
reveal_type(f(*(string,))) # revealed: str
|
||||
|
||||
# `Any` matches both overloads, but the return types are not equivalent.
|
||||
# Pyright and mypy both reveal `str` here, contrary to the spec.
|
||||
reveal_type(f(any)) # revealed: Unknown
|
||||
reveal_type(f(*(any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generics
|
||||
|
@ -756,10 +873,19 @@ from overloaded import f
|
|||
|
||||
def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any):
|
||||
reveal_type(f(list_int)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `str`
|
||||
reveal_type(f(list_str)) # revealed: Unknown
|
||||
# TODO: Should be `str`
|
||||
reveal_type(f(*(list_str,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(list_any)) # revealed: Unknown
|
||||
reveal_type(f(*(list_any,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(any)) # revealed: Unknown
|
||||
reveal_type(f(*(any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generics (multiple arguments)
|
||||
|
@ -784,12 +910,24 @@ from overloaded import f
|
|||
|
||||
def _(integer: int, string: str, any: Any, list_any: list[Any]):
|
||||
reveal_type(f(integer, string)) # revealed: int
|
||||
reveal_type(f(*(integer, string))) # revealed: int
|
||||
|
||||
reveal_type(f(string, integer)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(string, integer))) # revealed: Unknown
|
||||
|
||||
# This matches the second overload and is _not_ the case of ambiguous overload matching.
|
||||
reveal_type(f(string, any)) # revealed: Any
|
||||
# TODO: Any
|
||||
reveal_type(f(*(string, any))) # revealed: tuple[str, Any]
|
||||
|
||||
reveal_type(f(string, list_any)) # revealed: list[Any]
|
||||
# TODO: revealed: list[Any]
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(string, list_any))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generic `self`
|
||||
|
@ -832,7 +970,54 @@ def _(b_int: B[int], b_str: B[str], b_any: B[Any]):
|
|||
|
||||
### Variadic argument
|
||||
|
||||
TODO: A variadic parameter is being assigned to a number of parameters of the same type
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import Any, overload
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
@overload
|
||||
def f1(x: int) -> A: ...
|
||||
@overload
|
||||
def f1(x: Any, y: Any) -> A: ...
|
||||
|
||||
@overload
|
||||
def f2(x: int) -> A: ...
|
||||
@overload
|
||||
def f2(x: Any, y: Any) -> B: ...
|
||||
|
||||
@overload
|
||||
def f3(x: int) -> A: ...
|
||||
@overload
|
||||
def f3(x: Any, y: Any) -> A: ...
|
||||
@overload
|
||||
def f3(x: Any, y: Any, *, z: str) -> B: ...
|
||||
|
||||
@overload
|
||||
def f4(x: int) -> A: ...
|
||||
@overload
|
||||
def f4(x: Any, y: Any) -> B: ...
|
||||
@overload
|
||||
def f4(x: Any, y: Any, *, z: str) -> B: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
from overloaded import f1, f2, f3, f4
|
||||
|
||||
def _(arg: list[Any]):
|
||||
# Matches both overload and the return types are equivalent
|
||||
reveal_type(f1(*arg)) # revealed: A
|
||||
# Matches both overload but the return types aren't equivalent
|
||||
reveal_type(f2(*arg)) # revealed: Unknown
|
||||
# Filters out the final overload and the return types are equivalent
|
||||
reveal_type(f3(*arg)) # revealed: A
|
||||
# Filters out the final overload but the return types aren't equivalent
|
||||
reveal_type(f4(*arg)) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Non-participating fully-static parameter
|
||||
|
||||
|
@ -868,7 +1053,10 @@ from overloaded import f
|
|||
|
||||
def _(any: Any):
|
||||
reveal_type(f(any, flag=True)) # revealed: int
|
||||
reveal_type(f(*(any,), flag=True)) # revealed: int
|
||||
|
||||
reveal_type(f(any, flag=False)) # revealed: str
|
||||
reveal_type(f(*(any,), flag=False)) # revealed: str
|
||||
```
|
||||
|
||||
### Non-participating gradual parameter
|
||||
|
@ -879,21 +1067,32 @@ def _(any: Any):
|
|||
from typing import Any, Literal, overload
|
||||
|
||||
@overload
|
||||
def f(x: tuple[str, Any], *, flag: Literal[True]) -> int: ...
|
||||
def f(x: tuple[str, Any], flag: Literal[True]) -> int: ...
|
||||
@overload
|
||||
def f(x: tuple[str, Any], *, flag: Literal[False] = ...) -> str: ...
|
||||
def f(x: tuple[str, Any], flag: Literal[False] = ...) -> str: ...
|
||||
@overload
|
||||
def f(x: tuple[str, Any], *, flag: bool = ...) -> int | str: ...
|
||||
def f(x: tuple[str, Any], flag: bool = ...) -> int | str: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from overloaded import f
|
||||
|
||||
def _(any: Any):
|
||||
reveal_type(f(any, flag=True)) # revealed: int
|
||||
reveal_type(f(*(any,), flag=True)) # revealed: int
|
||||
|
||||
reveal_type(f(any, flag=False)) # revealed: str
|
||||
reveal_type(f(*(any,), flag=False)) # revealed: str
|
||||
|
||||
def _(args: tuple[Any, Literal[True]]):
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*args)) # revealed: Unknown
|
||||
|
||||
def _(args: tuple[Any, Literal[False]]):
|
||||
# TODO: revealed: str
|
||||
reveal_type(f(*args)) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Argument type expansion
|
||||
|
@ -938,6 +1137,7 @@ from overloaded import A, B, f
|
|||
|
||||
def _(arg: tuple[A | B, Any]):
|
||||
reveal_type(f(arg)) # revealed: A | B
|
||||
reveal_type(f(*(arg,))) # revealed: A | B
|
||||
```
|
||||
|
||||
#### One argument list ambiguous
|
||||
|
@ -972,6 +1172,7 @@ from overloaded import A, B, C, f
|
|||
|
||||
def _(arg: tuple[A | B, Any]):
|
||||
reveal_type(f(arg)) # revealed: A | Unknown
|
||||
reveal_type(f(*(arg,))) # revealed: A | Unknown
|
||||
```
|
||||
|
||||
#### Both argument lists ambiguous
|
||||
|
@ -1005,4 +1206,5 @@ from overloaded import A, B, C, f
|
|||
|
||||
def _(arg: tuple[A | B, Any]):
|
||||
reveal_type(f(arg)) # revealed: Unknown
|
||||
reveal_type(f(*(arg,))) # revealed: Unknown
|
||||
```
|
||||
|
|
|
@ -161,8 +161,7 @@ def _(d: Any):
|
|||
if f(): # error: [missing-argument]
|
||||
...
|
||||
|
||||
# TODO: no error, once we support splatted call args
|
||||
if g(*d): # error: [missing-argument]
|
||||
if g(*d):
|
||||
...
|
||||
|
||||
if f("foo"): # TODO: error: [invalid-type-guard-call]
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::Db;
|
|||
mod arguments;
|
||||
pub(crate) mod bind;
|
||||
pub(super) use arguments::{Argument, CallArguments};
|
||||
pub(super) use bind::{Binding, Bindings, CallableBinding};
|
||||
pub(super) use bind::{Binding, Bindings, CallableBinding, MatchedArgument};
|
||||
|
||||
/// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was
|
||||
/// unsuccessful.
|
||||
|
|
|
@ -6,7 +6,7 @@ use ruff_python_ast as ast;
|
|||
use crate::Db;
|
||||
use crate::types::KnownClass;
|
||||
use crate::types::enums::enum_member_literals;
|
||||
use crate::types::tuple::{TupleSpec, TupleType};
|
||||
use crate::types::tuple::{TupleLength, TupleSpec, TupleType};
|
||||
|
||||
use super::Type;
|
||||
|
||||
|
@ -16,8 +16,8 @@ pub(crate) enum Argument<'a> {
|
|||
Synthetic,
|
||||
/// A positional argument.
|
||||
Positional,
|
||||
/// A starred positional argument (e.g. `*args`).
|
||||
Variadic,
|
||||
/// A starred positional argument (e.g. `*args`) containing the specified number of elements.
|
||||
Variadic(TupleLength),
|
||||
/// A keyword argument (e.g. `a=1`).
|
||||
Keyword(&'a str),
|
||||
/// The double-starred keywords argument (e.g. `**kwargs`).
|
||||
|
@ -37,24 +37,38 @@ impl<'a, 'db> CallArguments<'a, 'db> {
|
|||
Self { arguments, types }
|
||||
}
|
||||
|
||||
/// Create `CallArguments` from AST arguments
|
||||
pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self {
|
||||
/// Create `CallArguments` from AST arguments. We will use the provided callback to obtain the
|
||||
/// type of each splatted argument, so that we can determine its length. All other arguments
|
||||
/// will remain uninitialized as `Unknown`.
|
||||
pub(crate) fn from_arguments(
|
||||
db: &'db dyn Db,
|
||||
arguments: &'a ast::Arguments,
|
||||
mut infer_argument_type: impl FnMut(&ast::Expr, &ast::Expr) -> Type<'db>,
|
||||
) -> Self {
|
||||
arguments
|
||||
.arguments_source_order()
|
||||
.map(|arg_or_keyword| match arg_or_keyword {
|
||||
ast::ArgOrKeyword::Arg(arg) => match arg {
|
||||
ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic,
|
||||
_ => Argument::Positional,
|
||||
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
|
||||
let ty = infer_argument_type(arg, value);
|
||||
let length = match ty {
|
||||
Type::Tuple(tuple) => tuple.tuple(db).len(),
|
||||
// TODO: have `Type::try_iterator` return a tuple spec, and use its
|
||||
// length as this argument's arity
|
||||
_ => TupleLength::unknown(),
|
||||
};
|
||||
(Argument::Variadic(length), Some(ty))
|
||||
}
|
||||
_ => (Argument::Positional, None),
|
||||
},
|
||||
ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => {
|
||||
if let Some(arg) = arg {
|
||||
Argument::Keyword(&arg.id)
|
||||
(Argument::Keyword(&arg.id), None)
|
||||
} else {
|
||||
Argument::Keywords
|
||||
(Argument::Keywords, None)
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(|argument| (argument, None))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a
|
||||
//! union of types, each of which might contain multiple overloads.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
|
@ -25,7 +26,7 @@ use crate::types::function::{
|
|||
};
|
||||
use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError};
|
||||
use crate::types::signatures::{Parameter, ParameterForm, Parameters};
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::tuple::{Tuple, TupleLength, TupleType};
|
||||
use crate::types::{
|
||||
BoundMethodType, ClassLiteral, DataclassParams, KnownClass, KnownInstanceType,
|
||||
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType,
|
||||
|
@ -1388,24 +1389,22 @@ impl<'db> CallableBinding<'db> {
|
|||
let mut first_parameter_type: Option<Type<'db>> = None;
|
||||
let mut participating_parameter_index = None;
|
||||
|
||||
for overload_index in matching_overload_indexes {
|
||||
'overload: for overload_index in matching_overload_indexes {
|
||||
let overload = &self.overloads[*overload_index];
|
||||
let Some(parameter_index) = overload.argument_parameters[argument_index] else {
|
||||
// There is no parameter for this argument in this overload.
|
||||
break;
|
||||
};
|
||||
// TODO: For an unannotated `self` / `cls` parameter, the type should be
|
||||
// `typing.Self` / `type[typing.Self]`
|
||||
let current_parameter_type = overload.signature.parameters()[parameter_index]
|
||||
.annotated_type()
|
||||
.unwrap_or(Type::unknown());
|
||||
if let Some(first_parameter_type) = first_parameter_type {
|
||||
if !first_parameter_type.is_equivalent_to(db, current_parameter_type) {
|
||||
participating_parameter_index = Some(parameter_index);
|
||||
break;
|
||||
for parameter_index in &overload.argument_matches[argument_index].parameters {
|
||||
// TODO: For an unannotated `self` / `cls` parameter, the type should be
|
||||
// `typing.Self` / `type[typing.Self]`
|
||||
let current_parameter_type = overload.signature.parameters()[*parameter_index]
|
||||
.annotated_type()
|
||||
.unwrap_or(Type::unknown());
|
||||
if let Some(first_parameter_type) = first_parameter_type {
|
||||
if !first_parameter_type.is_equivalent_to(db, current_parameter_type) {
|
||||
participating_parameter_index = Some(*parameter_index);
|
||||
break 'overload;
|
||||
}
|
||||
} else {
|
||||
first_parameter_type = Some(current_parameter_type);
|
||||
}
|
||||
} else {
|
||||
first_parameter_type = Some(current_parameter_type);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1433,20 +1432,18 @@ impl<'db> CallableBinding<'db> {
|
|||
let mut current_parameter_types = vec![];
|
||||
for overload_index in &matching_overload_indexes[..=upto] {
|
||||
let overload = &self.overloads[*overload_index];
|
||||
let Some(parameter_index) = overload.argument_parameters[argument_index] else {
|
||||
// There is no parameter for this argument in this overload.
|
||||
continue;
|
||||
};
|
||||
if !participating_parameter_indexes.contains(¶meter_index) {
|
||||
// This parameter doesn't participate in the filtering process.
|
||||
continue;
|
||||
for parameter_index in &overload.argument_matches[argument_index].parameters {
|
||||
if !participating_parameter_indexes.contains(parameter_index) {
|
||||
// This parameter doesn't participate in the filtering process.
|
||||
continue;
|
||||
}
|
||||
// TODO: For an unannotated `self` / `cls` parameter, the type should be
|
||||
// `typing.Self` / `type[typing.Self]`
|
||||
let parameter_type = overload.signature.parameters()[*parameter_index]
|
||||
.annotated_type()
|
||||
.unwrap_or(Type::unknown());
|
||||
current_parameter_types.push(parameter_type);
|
||||
}
|
||||
// TODO: For an unannotated `self` / `cls` parameter, the type should be
|
||||
// `typing.Self` / `type[typing.Self]`
|
||||
let parameter_type = overload.signature.parameters()[parameter_index]
|
||||
.annotated_type()
|
||||
.unwrap_or(Type::unknown());
|
||||
current_parameter_types.push(parameter_type);
|
||||
}
|
||||
if current_parameter_types.is_empty() {
|
||||
continue;
|
||||
|
@ -1761,9 +1758,7 @@ struct ArgumentMatcher<'a, 'db> {
|
|||
conflicting_forms: &'a mut [bool],
|
||||
errors: &'a mut Vec<BindingError<'db>>,
|
||||
|
||||
/// The parameter that each argument is matched with.
|
||||
argument_parameters: Vec<Option<usize>>,
|
||||
/// Whether each parameter has been matched with an argument.
|
||||
argument_matches: Vec<MatchedArgument>,
|
||||
parameter_matched: Vec<bool>,
|
||||
next_positional: usize,
|
||||
first_excess_positional: Option<usize>,
|
||||
|
@ -1783,7 +1778,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
|||
argument_forms,
|
||||
conflicting_forms,
|
||||
errors,
|
||||
argument_parameters: vec![None; arguments.len()],
|
||||
argument_matches: vec![MatchedArgument::default(); arguments.len()],
|
||||
parameter_matched: vec![false; parameters.len()],
|
||||
next_positional: 0,
|
||||
first_excess_positional: None,
|
||||
|
@ -1828,7 +1823,9 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
|||
});
|
||||
}
|
||||
}
|
||||
self.argument_parameters[argument_index] = Some(parameter_index);
|
||||
let matched_argument = &mut self.argument_matches[argument_index];
|
||||
matched_argument.parameters.push(parameter_index);
|
||||
matched_argument.matched = true;
|
||||
self.parameter_matched[parameter_index] = true;
|
||||
}
|
||||
|
||||
|
@ -1882,7 +1879,34 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(self) -> Box<[Option<usize>]> {
|
||||
fn match_variadic(
|
||||
&mut self,
|
||||
argument_index: usize,
|
||||
argument: Argument<'a>,
|
||||
length: TupleLength,
|
||||
) -> Result<(), ()> {
|
||||
// We must be able to match up the fixed-length portion of the argument with positional
|
||||
// parameters, so we pass on any errors that occur.
|
||||
for _ in 0..length.minimum() {
|
||||
self.match_positional(argument_index, argument)?;
|
||||
}
|
||||
|
||||
// If the tuple is variable-length, we assume that it will soak up all remaining positional
|
||||
// parameters.
|
||||
if length.is_variable() {
|
||||
while self
|
||||
.parameters
|
||||
.get_positional(self.next_positional)
|
||||
.is_some()
|
||||
{
|
||||
self.match_positional(argument_index, argument)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(self) -> Box<[MatchedArgument]> {
|
||||
if let Some(first_excess_argument_index) = self.first_excess_positional {
|
||||
self.errors.push(BindingError::TooManyPositionalArguments {
|
||||
first_excess_argument_index: self.get_argument_index(first_excess_argument_index),
|
||||
|
@ -1911,7 +1935,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
|||
});
|
||||
}
|
||||
|
||||
self.argument_parameters.into_boxed_slice()
|
||||
self.argument_matches.into_boxed_slice()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1919,7 +1943,7 @@ struct ArgumentTypeChecker<'a, 'db> {
|
|||
db: &'db dyn Db,
|
||||
signature: &'a Signature<'db>,
|
||||
arguments: &'a CallArguments<'a, 'db>,
|
||||
argument_parameters: &'a [Option<usize>],
|
||||
argument_matches: &'a [MatchedArgument],
|
||||
parameter_tys: &'a mut [Option<Type<'db>>],
|
||||
errors: &'a mut Vec<BindingError<'db>>,
|
||||
|
||||
|
@ -1932,7 +1956,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
|||
db: &'db dyn Db,
|
||||
signature: &'a Signature<'db>,
|
||||
arguments: &'a CallArguments<'a, 'db>,
|
||||
argument_parameters: &'a [Option<usize>],
|
||||
argument_matches: &'a [MatchedArgument],
|
||||
parameter_tys: &'a mut [Option<Type<'db>>],
|
||||
errors: &'a mut Vec<BindingError<'db>>,
|
||||
) -> Self {
|
||||
|
@ -1940,7 +1964,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
|||
db,
|
||||
signature,
|
||||
arguments,
|
||||
argument_parameters,
|
||||
argument_matches,
|
||||
parameter_tys,
|
||||
errors,
|
||||
specialization: None,
|
||||
|
@ -1987,20 +2011,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
|||
for (argument_index, adjusted_argument_index, _, argument_type) in
|
||||
self.enumerate_argument_types()
|
||||
{
|
||||
let Some(parameter_index) = self.argument_parameters[argument_index] else {
|
||||
// There was an error with argument when matching parameters, so don't bother
|
||||
// type-checking it.
|
||||
continue;
|
||||
};
|
||||
let parameter = ¶meters[parameter_index];
|
||||
let Some(expected_type) = parameter.annotated_type() else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = builder.infer(expected_type, argument_type) {
|
||||
self.errors.push(BindingError::SpecializationError {
|
||||
error,
|
||||
argument_index: adjusted_argument_index,
|
||||
});
|
||||
for parameter_index in &self.argument_matches[argument_index].parameters {
|
||||
let parameter = ¶meters[*parameter_index];
|
||||
let Some(expected_type) = parameter.annotated_type() else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = builder.infer(expected_type, argument_type) {
|
||||
self.errors.push(BindingError::SpecializationError {
|
||||
error,
|
||||
argument_index: adjusted_argument_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
self.specialization = self.signature.generic_context.map(|gc| builder.build(gc));
|
||||
|
@ -2016,16 +2037,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
|||
|
||||
fn check_argument_type(
|
||||
&mut self,
|
||||
argument_index: usize,
|
||||
adjusted_argument_index: Option<usize>,
|
||||
argument: Argument<'a>,
|
||||
mut argument_type: Type<'db>,
|
||||
parameter_index: usize,
|
||||
) {
|
||||
let Some(parameter_index) = self.argument_parameters[argument_index] else {
|
||||
// There was an error with argument when matching parameters, so don't bother
|
||||
// type-checking it.
|
||||
return;
|
||||
};
|
||||
let parameters = self.signature.parameters();
|
||||
let parameter = ¶meters[parameter_index];
|
||||
if let Some(mut expected_ty) = parameter.annotated_type() {
|
||||
|
@ -2064,12 +2080,73 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
|||
for (argument_index, adjusted_argument_index, argument, argument_type) in
|
||||
self.enumerate_argument_types()
|
||||
{
|
||||
self.check_argument_type(
|
||||
argument_index,
|
||||
adjusted_argument_index,
|
||||
argument,
|
||||
argument_type,
|
||||
);
|
||||
// If the argument isn't splatted, just check its type directly.
|
||||
let Argument::Variadic(_) = argument else {
|
||||
for parameter_index in &self.argument_matches[argument_index].parameters {
|
||||
self.check_argument_type(
|
||||
adjusted_argument_index,
|
||||
argument,
|
||||
argument_type,
|
||||
*parameter_index,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
};
|
||||
|
||||
// If the argument is splatted, convert its type into a tuple describing the splatted
|
||||
// elements. For tuples, we don't have to do anything! For other types, we treat it as
|
||||
// an iterator, and create a homogeneous tuple of its output type, since we don't know
|
||||
// how many elements the iterator will produce.
|
||||
// TODO: update `Type::try_iterate` to return this tuple type for us.
|
||||
let argument_types = match argument_type {
|
||||
Type::Tuple(tuple) => Cow::Borrowed(tuple.tuple(self.db)),
|
||||
_ => {
|
||||
let element_type = argument_type.iterate(self.db);
|
||||
Cow::Owned(Tuple::homogeneous(element_type))
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: When we perform argument expansion during overload resolution, we might need
|
||||
// to retry both `match_parameters` _and_ `check_types` for each expansion. Currently
|
||||
// we only retry `check_types`. The issue is that argument expansion might produce a
|
||||
// splatted value with a different arity than what we originally inferred for the
|
||||
// unexpanded value, and that in turn can affect which parameters the splatted value is
|
||||
// matched with. As a workaround, make sure that the splatted tuple contains an
|
||||
// arbitrary number of `Unknown`s at the end, so that if the expanded value has a
|
||||
// smaller arity than the unexpanded value, we still have enough values to assign to
|
||||
// the already matched parameters.
|
||||
let argument_types = match argument_types.as_ref() {
|
||||
Tuple::Fixed(_) => {
|
||||
Cow::Owned(argument_types.concat(self.db, &Tuple::homogeneous(Type::unknown())))
|
||||
}
|
||||
Tuple::Variable(_) => argument_types,
|
||||
};
|
||||
|
||||
// Resize the tuple of argument types to line up with the number of parameters this
|
||||
// argument was matched against. If parameter matching succeeded, then we can (TODO:
|
||||
// should be able to, see above) guarantee that all of the required elements of the
|
||||
// splatted tuple will have been matched with a parameter. But if parameter matching
|
||||
// failed, there might be more required elements. That means we can't use
|
||||
// TupleLength::Fixed below, because we would otherwise get a "too many values" error
|
||||
// when parameter matching failed.
|
||||
let desired_size =
|
||||
TupleLength::Variable(self.argument_matches[argument_index].parameters.len(), 0);
|
||||
let argument_types = argument_types
|
||||
.resize(self.db, desired_size)
|
||||
.expect("argument type should be consistent with its arity");
|
||||
|
||||
// Check the types by zipping through the splatted argument types and their matched
|
||||
// parameters.
|
||||
for (argument_type, parameter_index) in (argument_types.all_elements())
|
||||
.zip(&self.argument_matches[argument_index].parameters)
|
||||
{
|
||||
self.check_argument_type(
|
||||
adjusted_argument_index,
|
||||
argument,
|
||||
*argument_type,
|
||||
*parameter_index,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2078,6 +2155,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Information about which parameter(s) an argument was matched against. This is tracked
|
||||
/// separately for each overload.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MatchedArgument {
|
||||
/// The index of the parameter(s) that an argument was matched against. A splatted argument
|
||||
/// might be matched against multiple parameters.
|
||||
pub parameters: SmallVec<[usize; 1]>,
|
||||
|
||||
/// Whether there were errors matching this argument. For a splatted argument, _all_ splatted
|
||||
/// elements must have been successfully matched. (That means that this can be `false` while
|
||||
/// the `parameters` field is non-empty.)
|
||||
pub matched: bool,
|
||||
}
|
||||
|
||||
/// Binding information for one of the overloads of a callable.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Binding<'db> {
|
||||
|
@ -2101,9 +2192,9 @@ pub(crate) struct Binding<'db> {
|
|||
/// is being used to infer a specialization for the class.
|
||||
inherited_specialization: Option<Specialization<'db>>,
|
||||
|
||||
/// The formal parameter that each argument is matched with, in argument source order, or
|
||||
/// `None` if the argument was not matched to any parameter.
|
||||
argument_parameters: Box<[Option<usize>]>,
|
||||
/// Information about which parameter(s) each argument was matched with, in argument source
|
||||
/// order.
|
||||
argument_matches: Box<[MatchedArgument]>,
|
||||
|
||||
/// Bound types for parameters, in parameter source order, or `None` if no argument was matched
|
||||
/// to that parameter.
|
||||
|
@ -2122,7 +2213,7 @@ impl<'db> Binding<'db> {
|
|||
return_ty: Type::unknown(),
|
||||
specialization: None,
|
||||
inherited_specialization: None,
|
||||
argument_parameters: Box::from([]),
|
||||
argument_matches: Box::from([]),
|
||||
parameter_tys: Box::from([]),
|
||||
errors: vec![],
|
||||
}
|
||||
|
@ -2156,7 +2247,10 @@ impl<'db> Binding<'db> {
|
|||
Argument::Keyword(name) => {
|
||||
let _ = matcher.match_keyword(argument_index, argument, name);
|
||||
}
|
||||
Argument::Variadic | Argument::Keywords => {
|
||||
Argument::Variadic(length) => {
|
||||
let _ = matcher.match_variadic(argument_index, argument, length);
|
||||
}
|
||||
Argument::Keywords => {
|
||||
// TODO
|
||||
continue;
|
||||
}
|
||||
|
@ -2164,7 +2258,7 @@ impl<'db> Binding<'db> {
|
|||
}
|
||||
self.return_ty = self.signature.return_ty.unwrap_or(Type::unknown());
|
||||
self.parameter_tys = vec![None; parameters.len()].into_boxed_slice();
|
||||
self.argument_parameters = matcher.finish();
|
||||
self.argument_matches = matcher.finish();
|
||||
}
|
||||
|
||||
fn check_types(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) {
|
||||
|
@ -2172,7 +2266,7 @@ impl<'db> Binding<'db> {
|
|||
db,
|
||||
&self.signature,
|
||||
arguments,
|
||||
&self.argument_parameters,
|
||||
&self.argument_matches,
|
||||
&mut self.parameter_tys,
|
||||
&mut self.errors,
|
||||
);
|
||||
|
@ -2218,9 +2312,9 @@ impl<'db> Binding<'db> {
|
|||
) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + 'a {
|
||||
argument_types
|
||||
.iter()
|
||||
.zip(&self.argument_parameters)
|
||||
.filter(move |(_, argument_parameter)| {
|
||||
argument_parameter.is_some_and(|ap| ap == parameter_index)
|
||||
.zip(&self.argument_matches)
|
||||
.filter(move |(_, argument_matches)| {
|
||||
argument_matches.parameters.contains(¶meter_index)
|
||||
})
|
||||
.map(|((argument, argument_type), _)| {
|
||||
(argument, argument_type.unwrap_or_else(Type::unknown))
|
||||
|
@ -2265,7 +2359,7 @@ impl<'db> Binding<'db> {
|
|||
return_ty: self.return_ty,
|
||||
specialization: self.specialization,
|
||||
inherited_specialization: self.inherited_specialization,
|
||||
argument_parameters: self.argument_parameters.clone(),
|
||||
argument_matches: self.argument_matches.clone(),
|
||||
parameter_tys: self.parameter_tys.clone(),
|
||||
errors: self.errors.clone(),
|
||||
}
|
||||
|
@ -2276,7 +2370,7 @@ impl<'db> Binding<'db> {
|
|||
return_ty,
|
||||
specialization,
|
||||
inherited_specialization,
|
||||
argument_parameters,
|
||||
argument_matches,
|
||||
parameter_tys,
|
||||
errors,
|
||||
} = snapshot;
|
||||
|
@ -2284,15 +2378,15 @@ impl<'db> Binding<'db> {
|
|||
self.return_ty = return_ty;
|
||||
self.specialization = specialization;
|
||||
self.inherited_specialization = inherited_specialization;
|
||||
self.argument_parameters = argument_parameters;
|
||||
self.argument_matches = argument_matches;
|
||||
self.parameter_tys = parameter_tys;
|
||||
self.errors = errors;
|
||||
}
|
||||
|
||||
/// Returns a vector where each index corresponds to an argument position,
|
||||
/// and the value is the parameter index that argument maps to (if any).
|
||||
pub(crate) fn argument_to_parameter_mapping(&self) -> &[Option<usize>] {
|
||||
&self.argument_parameters
|
||||
pub(crate) fn argument_matches(&self) -> &[MatchedArgument] {
|
||||
&self.argument_matches
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2301,7 +2395,7 @@ struct BindingSnapshot<'db> {
|
|||
return_ty: Type<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
inherited_specialization: Option<Specialization<'db>>,
|
||||
argument_parameters: Box<[Option<usize>]>,
|
||||
argument_matches: Box<[MatchedArgument]>,
|
||||
parameter_tys: Box<[Option<Type<'db>>]>,
|
||||
errors: Vec<BindingError<'db>>,
|
||||
}
|
||||
|
@ -2342,8 +2436,8 @@ impl<'db> CallableBindingSnapshot<'db> {
|
|||
snapshot.specialization = binding.specialization;
|
||||
snapshot.inherited_specialization = binding.inherited_specialization;
|
||||
snapshot
|
||||
.argument_parameters
|
||||
.clone_from(&binding.argument_parameters);
|
||||
.argument_matches
|
||||
.clone_from(&binding.argument_matches);
|
||||
snapshot.parameter_tys.clone_from(&binding.parameter_tys);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::semantic_index::place::ScopeId;
|
|||
use crate::semantic_index::{
|
||||
attribute_scopes, global_scope, place_table, semantic_index, use_def_map,
|
||||
};
|
||||
use crate::types::call::CallArguments;
|
||||
use crate::types::call::{CallArguments, MatchedArgument};
|
||||
use crate::types::signatures::Signature;
|
||||
use crate::types::{ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type};
|
||||
use crate::{Db, HasType, NameKind, SemanticModel};
|
||||
|
@ -724,8 +724,8 @@ pub struct CallSignatureDetails<'db> {
|
|||
pub definition: Option<Definition<'db>>,
|
||||
|
||||
/// Mapping from argument indices to parameter indices. This helps
|
||||
/// identify which argument corresponds to which parameter.
|
||||
pub argument_to_parameter_mapping: Vec<Option<usize>>,
|
||||
/// determine which parameter corresponds to which argument position.
|
||||
pub argument_to_parameter_mapping: Vec<MatchedArgument>,
|
||||
}
|
||||
|
||||
/// Extract signature details from a function call expression.
|
||||
|
@ -742,7 +742,10 @@ pub fn call_signature_details<'db>(
|
|||
|
||||
// Use into_callable to handle all the complex type conversions
|
||||
if let Some(callable_type) = func_type.into_callable(db) {
|
||||
let call_arguments = CallArguments::from_arguments(&call_expr.arguments);
|
||||
let call_arguments =
|
||||
CallArguments::from_arguments(db, &call_expr.arguments, |_, splatted_value| {
|
||||
splatted_value.inferred_type(&model)
|
||||
});
|
||||
let bindings = callable_type.bindings(db).match_parameters(&call_arguments);
|
||||
|
||||
// Extract signature details from all callable bindings
|
||||
|
@ -761,7 +764,7 @@ pub fn call_signature_details<'db>(
|
|||
parameter_label_offsets,
|
||||
parameter_names,
|
||||
definition: signature.definition(),
|
||||
argument_to_parameter_mapping: binding.argument_to_parameter_mapping().to_vec(),
|
||||
argument_to_parameter_mapping: binding.argument_matches().to_vec(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
|
|
@ -2203,7 +2203,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
self.infer_type_parameters(type_params);
|
||||
|
||||
if let Some(arguments) = class.arguments.as_deref() {
|
||||
let mut call_arguments = CallArguments::from_arguments(arguments);
|
||||
let mut call_arguments =
|
||||
CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| {
|
||||
let ty = self.infer_expression(splatted_value);
|
||||
self.store_expression_type(argument, ty);
|
||||
ty
|
||||
});
|
||||
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
|
||||
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
|
||||
}
|
||||
|
@ -5165,19 +5170,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
.zip(argument_forms.iter().copied())
|
||||
.zip(ast_arguments.arguments_source_order());
|
||||
for (((_, argument_type), form), arg_or_keyword) in iter {
|
||||
let ty = match arg_or_keyword {
|
||||
ast::ArgOrKeyword::Arg(arg) => match arg {
|
||||
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
|
||||
let ty = self.infer_argument_type(value, form);
|
||||
self.store_expression_type(arg, ty);
|
||||
ty
|
||||
}
|
||||
_ => self.infer_argument_type(arg, form),
|
||||
},
|
||||
ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => {
|
||||
self.infer_argument_type(value, form)
|
||||
}
|
||||
let argument = match arg_or_keyword {
|
||||
// We already inferred the type of splatted arguments.
|
||||
ast::ArgOrKeyword::Arg(ast::Expr::Starred(_)) => continue,
|
||||
ast::ArgOrKeyword::Arg(arg) => arg,
|
||||
ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => value,
|
||||
};
|
||||
let ty = self.infer_argument_type(argument, form);
|
||||
*argument_type = Some(ty);
|
||||
}
|
||||
}
|
||||
|
@ -5874,7 +5873,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
// We don't call `Type::try_call`, because we want to perform type inference on the
|
||||
// arguments after matching them to parameters, but before checking that the argument types
|
||||
// are assignable to any parameter annotations.
|
||||
let mut call_arguments = CallArguments::from_arguments(arguments);
|
||||
let mut call_arguments =
|
||||
CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| {
|
||||
let ty = self.infer_expression(splatted_value);
|
||||
self.store_expression_type(argument, ty);
|
||||
ty
|
||||
});
|
||||
|
||||
let callable_type = self.infer_maybe_standalone_expression(func);
|
||||
|
||||
|
|
|
@ -38,6 +38,14 @@ pub(crate) enum TupleLength {
|
|||
}
|
||||
|
||||
impl TupleLength {
|
||||
pub(crate) const fn unknown() -> TupleLength {
|
||||
TupleLength::Variable(0, 0)
|
||||
}
|
||||
|
||||
pub(crate) fn is_variable(self) -> bool {
|
||||
matches!(self, TupleLength::Variable(_, _))
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue