Skip to content

Commit 7673d46

Browse files
authored
[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.
1 parent 9d5ecac commit 7673d46

File tree

10 files changed

+694
-130
lines changed

10 files changed

+694
-130
lines changed

crates/ty_ide/src/signature_help.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ fn create_signature_details_from_call_signature_details(
149149
details
150150
.argument_to_parameter_mapping
151151
.get(current_arg_index)
152-
.and_then(|&param_index| param_index)
152+
.and_then(|mapping| mapping.parameters.first().copied())
153153
.or({
154154
// If we can't find a mapping for this argument, but we have a current
155155
// 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]
242242

243243
// First, try to find a signature where all arguments have valid parameter mappings.
244244
let perfect_match = signature_details.iter().position(|details| {
245-
// Check if all arguments have valid parameter mappings (i.e., are not None).
245+
// Check if all arguments have valid parameter mappings.
246246
details
247247
.argument_to_parameter_mapping
248248
.iter()
249-
.all(Option::is_some)
249+
.all(|mapping| mapping.matched)
250250
});
251251

252252
if let Some(index) = perfect_match {
@@ -261,7 +261,7 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]
261261
details
262262
.argument_to_parameter_mapping
263263
.iter()
264-
.filter(|mapping| mapping.is_some())
264+
.filter(|mapping| mapping.matched)
265265
.count()
266266
})?;
267267

crates/ty_python_semantic/resources/mdtest/call/function.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,246 @@ def _(flag: bool):
6969
reveal_type(foo()) # revealed: int
7070
```
7171

72+
## Splatted arguments
73+
74+
### Unknown argument length
75+
76+
```py
77+
def takes_zero() -> None: ...
78+
def takes_one(x: int) -> None: ...
79+
def takes_two(x: int, y: int) -> None: ...
80+
def takes_two_positional_only(x: int, y: int, /) -> None: ...
81+
def takes_two_different(x: int, y: str) -> None: ...
82+
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
83+
def takes_at_least_zero(*args) -> None: ...
84+
def takes_at_least_one(x: int, *args) -> None: ...
85+
def takes_at_least_two(x: int, y: int, *args) -> None: ...
86+
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
87+
88+
# Test all of the above with a number of different splatted argument types
89+
90+
def _(args: list[int]) -> None:
91+
takes_zero(*args)
92+
takes_one(*args)
93+
takes_two(*args)
94+
takes_two_positional_only(*args)
95+
takes_two_different(*args) # error: [invalid-argument-type]
96+
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
97+
takes_at_least_zero(*args)
98+
takes_at_least_one(*args)
99+
takes_at_least_two(*args)
100+
takes_at_least_two_positional_only(*args)
101+
102+
def _(args: tuple[int, ...]) -> None:
103+
takes_zero(*args)
104+
takes_one(*args)
105+
takes_two(*args)
106+
takes_two_positional_only(*args)
107+
takes_two_different(*args) # error: [invalid-argument-type]
108+
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
109+
takes_at_least_zero(*args)
110+
takes_at_least_one(*args)
111+
takes_at_least_two(*args)
112+
takes_at_least_two_positional_only(*args)
113+
```
114+
115+
### Fixed-length tuple argument
116+
117+
```py
118+
def takes_zero() -> None: ...
119+
def takes_one(x: int) -> None: ...
120+
def takes_two(x: int, y: int) -> None: ...
121+
def takes_two_positional_only(x: int, y: int, /) -> None: ...
122+
def takes_two_different(x: int, y: str) -> None: ...
123+
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
124+
def takes_at_least_zero(*args) -> None: ...
125+
def takes_at_least_one(x: int, *args) -> None: ...
126+
def takes_at_least_two(x: int, y: int, *args) -> None: ...
127+
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
128+
129+
# Test all of the above with a number of different splatted argument types
130+
131+
def _(args: tuple[int]) -> None:
132+
takes_zero(*args) # error: [too-many-positional-arguments]
133+
takes_one(*args)
134+
takes_two(*args) # error: [missing-argument]
135+
takes_two_positional_only(*args) # error: [missing-argument]
136+
takes_two_different(*args) # error: [missing-argument]
137+
takes_two_different_positional_only(*args) # error: [missing-argument]
138+
takes_at_least_zero(*args)
139+
takes_at_least_one(*args)
140+
takes_at_least_two(*args) # error: [missing-argument]
141+
takes_at_least_two_positional_only(*args) # error: [missing-argument]
142+
143+
def _(args: tuple[int, int]) -> None:
144+
takes_zero(*args) # error: [too-many-positional-arguments]
145+
takes_one(*args) # error: [too-many-positional-arguments]
146+
takes_two(*args)
147+
takes_two_positional_only(*args)
148+
takes_two_different(*args) # error: [invalid-argument-type]
149+
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
150+
takes_at_least_zero(*args)
151+
takes_at_least_one(*args)
152+
takes_at_least_two(*args)
153+
takes_at_least_two_positional_only(*args)
154+
155+
def _(args: tuple[int, str]) -> None:
156+
takes_zero(*args) # error: [too-many-positional-arguments]
157+
takes_one(*args) # error: [too-many-positional-arguments]
158+
takes_two(*args) # error: [invalid-argument-type]
159+
takes_two_positional_only(*args) # error: [invalid-argument-type]
160+
takes_two_different(*args)
161+
takes_two_different_positional_only(*args)
162+
takes_at_least_zero(*args)
163+
takes_at_least_one(*args)
164+
takes_at_least_two(*args) # error: [invalid-argument-type]
165+
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
166+
```
167+
168+
### Mixed tuple argument
169+
170+
```toml
171+
[environment]
172+
python-version = "3.11"
173+
```
174+
175+
```py
176+
def takes_zero() -> None: ...
177+
def takes_one(x: int) -> None: ...
178+
def takes_two(x: int, y: int) -> None: ...
179+
def takes_two_positional_only(x: int, y: int, /) -> None: ...
180+
def takes_two_different(x: int, y: str) -> None: ...
181+
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
182+
def takes_at_least_zero(*args) -> None: ...
183+
def takes_at_least_one(x: int, *args) -> None: ...
184+
def takes_at_least_two(x: int, y: int, *args) -> None: ...
185+
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
186+
187+
# Test all of the above with a number of different splatted argument types
188+
189+
def _(args: tuple[int, *tuple[int, ...]]) -> None:
190+
takes_zero(*args) # error: [too-many-positional-arguments]
191+
takes_one(*args)
192+
takes_two(*args)
193+
takes_two_positional_only(*args)
194+
takes_two_different(*args) # error: [invalid-argument-type]
195+
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
196+
takes_at_least_zero(*args)
197+
takes_at_least_one(*args)
198+
takes_at_least_two(*args)
199+
takes_at_least_two_positional_only(*args)
200+
201+
def _(args: tuple[int, *tuple[str, ...]]) -> None:
202+
takes_zero(*args) # error: [too-many-positional-arguments]
203+
takes_one(*args)
204+
takes_two(*args) # error: [invalid-argument-type]
205+
takes_two_positional_only(*args) # error: [invalid-argument-type]
206+
takes_two_different(*args)
207+
takes_two_different_positional_only(*args)
208+
takes_at_least_zero(*args)
209+
takes_at_least_one(*args)
210+
takes_at_least_two(*args) # error: [invalid-argument-type]
211+
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
212+
213+
def _(args: tuple[int, int, *tuple[int, ...]]) -> None:
214+
takes_zero(*args) # error: [too-many-positional-arguments]
215+
takes_one(*args) # error: [too-many-positional-arguments]
216+
takes_two(*args)
217+
takes_two_positional_only(*args)
218+
takes_two_different(*args) # error: [invalid-argument-type]
219+
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
220+
takes_at_least_zero(*args)
221+
takes_at_least_one(*args)
222+
takes_at_least_two(*args)
223+
takes_at_least_two_positional_only(*args)
224+
225+
def _(args: tuple[int, int, *tuple[str, ...]]) -> None:
226+
takes_zero(*args) # error: [too-many-positional-arguments]
227+
takes_one(*args) # error: [too-many-positional-arguments]
228+
takes_two(*args)
229+
takes_two_positional_only(*args)
230+
takes_two_different(*args) # error: [invalid-argument-type]
231+
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
232+
takes_at_least_zero(*args)
233+
takes_at_least_one(*args)
234+
takes_at_least_two(*args)
235+
takes_at_least_two_positional_only(*args)
236+
237+
def _(args: tuple[int, *tuple[int, ...], int]) -> None:
238+
takes_zero(*args) # error: [too-many-positional-arguments]
239+
takes_one(*args) # error: [too-many-positional-arguments]
240+
takes_two(*args)
241+
takes_two_positional_only(*args)
242+
takes_two_different(*args) # error: [invalid-argument-type]
243+
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
244+
takes_at_least_zero(*args)
245+
takes_at_least_one(*args)
246+
takes_at_least_two(*args)
247+
takes_at_least_two_positional_only(*args)
248+
249+
def _(args: tuple[int, *tuple[str, ...], int]) -> None:
250+
takes_zero(*args) # error: [too-many-positional-arguments]
251+
takes_one(*args) # error: [too-many-positional-arguments]
252+
takes_two(*args) # error: [invalid-argument-type]
253+
takes_two_positional_only(*args) # error: [invalid-argument-type]
254+
takes_two_different(*args)
255+
takes_two_different_positional_only(*args)
256+
takes_at_least_zero(*args)
257+
takes_at_least_one(*args)
258+
takes_at_least_two(*args) # error: [invalid-argument-type]
259+
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
260+
```
261+
262+
### Argument expansion regression
263+
264+
This is a regression that was highlighted by the ecosystem check, which shows that we might need to
265+
rethink how we perform argument expansion during overload resolution. In particular, we might need
266+
to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry
267+
`check_types`.
268+
269+
The issue is that argument expansion might produce a splatted value with a different arity than what
270+
we originally inferred for the unexpanded value, and that in turn can affect which parameters the
271+
splatted value is matched with.
272+
273+
The first example correctly produces an error. The `tuple[int, str]` union element has a precise
274+
arity of two, and so parameter matching chooses the first overload. The second element of the tuple
275+
does not match the second parameter type, which yielding an `invalid-argument-type` error.
276+
277+
The third example should produce the same error. However, because we have a union, we do not see the
278+
precise arity of each union element during parameter matching. Instead, we infer an arity of "zero
279+
or more" for the union as a whole, and use that less precise arity when matching parameters. We
280+
therefore consider the second overload to still be a potential candidate for the `tuple[int, str]`
281+
union element. During type checking, we have to force the arity of each union element to match the
282+
inferred arity of the union as a whole (turning `tuple[int, str]` into `tuple[int | str, ...]`).
283+
That less precise tuple type-checks successfully against the second overload, making us incorrectly
284+
think that `tuple[int, str]` is a valid splatted call.
285+
286+
If we update argument expansion to retry parameter matching with the precise arity of each union
287+
element, we will correctly rule out the second overload for `tuple[int, str]`, just like we do when
288+
splatting that tuple directly (instead of as part of a union).
289+
290+
```py
291+
from typing import overload
292+
293+
@overload
294+
def f(x: int, y: int) -> None: ...
295+
@overload
296+
def f(x: int, y: str, z: int) -> None: ...
297+
def f(*args): ...
298+
299+
# Test all of the above with a number of different splatted argument types
300+
301+
def _(t: tuple[int, str]) -> None:
302+
f(*t) # error: [invalid-argument-type]
303+
304+
def _(t: tuple[int, str, int]) -> None:
305+
f(*t)
306+
307+
def _(t: tuple[int, str] | tuple[int, str, int]) -> None:
308+
# TODO: error: [invalid-argument-type]
309+
f(*t)
310+
```
311+
72312
## Wrong argument type
73313

74314
### Positional argument, positional-or-keyword parameter

0 commit comments

Comments
 (0)