[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:
Douglas Creager 2025-07-22 14:33:08 -04:00 committed by GitHub
parent 9d5ecacdc5
commit 7673d46b71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 694 additions and 130 deletions

View file

@ -149,7 +149,7 @@ fn create_signature_details_from_call_signature_details(
details
.argument_to_parameter_mapping
.get(current_arg_index)
.and_then(|&param_index| param_index)
.and_then(|mapping| mapping.parameters.first().copied())
.or({
// If we can't find a mapping for this argument, but we have a current
// argument index, use that as the active parameter if it's within bounds.
@ -242,11 +242,11 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]
// First, try to find a signature where all arguments have valid parameter mappings.
let perfect_match = signature_details.iter().position(|details| {
// Check if all arguments have valid parameter mappings (i.e., are not None).
// Check if all arguments have valid parameter mappings.
details
.argument_to_parameter_mapping
.iter()
.all(Option::is_some)
.all(|mapping| mapping.matched)
});
if let Some(index) = perfect_match {
@ -261,7 +261,7 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]
details
.argument_to_parameter_mapping
.iter()
.filter(|mapping| mapping.is_some())
.filter(|mapping| mapping.matched)
.count()
})?;

View file

@ -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

View file

@ -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
```

View file

@ -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]

View file

@ -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.

View file

@ -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()
}

View file

@ -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(&parameter_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 = &parameters[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 = &parameters[*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 = &parameters[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(&parameter_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);
}

View file

@ -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()

View file

@ -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);

View file

@ -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>) {