Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6a3d6dd
add length to variadic argument
dcreager Jun 26, 2025
114a491
match variadic args
dcreager Jun 26, 2025
cf39e0d
resize to variable-length
dcreager Jun 27, 2025
c116820
infer correct arity for splatted tuples
dcreager Jun 27, 2025
953af0e
clippy
dcreager Jun 27, 2025
1949dfb
mdlint
dcreager Jun 27, 2025
4f4cf2b
fix those panics
dcreager Jun 27, 2025
0954aab
add comments
dcreager Jun 27, 2025
910bb1d
argument expansion workaround
dcreager Jun 27, 2025
e5bc935
mdlint
dcreager Jun 27, 2025
87f2ff9
refine comment
dcreager Jun 27, 2025
e8c476d
Combine CallArguments and CallArgumentTypes
dcreager Jul 14, 2025
3a57137
fix docs
dcreager Jul 14, 2025
dec9b73
Merge branch 'dcreager/merge-arguments' into dcreager/splat
dcreager Jul 14, 2025
8929733
wrap in option
dcreager Jul 14, 2025
988479d
move around a bit
dcreager Jul 14, 2025
4a13a6f
Merge branch 'dcreager/merge-arguments' into dcreager/splat
dcreager Jul 14, 2025
06f75c4
fix tests
dcreager Jul 14, 2025
40d117b
use FromIterator
dcreager Jul 15, 2025
5fdaed8
remove unused From
dcreager Jul 15, 2025
d389168
debug assert lengths
dcreager Jul 15, 2025
900240b
add asserting constructor
dcreager Jul 15, 2025
3a7c04d
add types iterator
dcreager Jul 15, 2025
9a1175c
Merge branch 'main' into dcreager/merge-arguments
dcreager Jul 15, 2025
0f0cd47
Merge branch 'dcreager/merge-arguments' into dcreager/splat
dcreager Jul 15, 2025
67a5f66
Merge branch 'main' into dcreager/splat
dcreager Jul 15, 2025
8b65f34
use type alias for arg/param map
dcreager Jul 15, 2025
043bcb6
better argument expansion regression test
dcreager Jul 15, 2025
57c9afc
add more arg type tests
dcreager Jul 15, 2025
d16dbbb
MatchedArgument
dcreager Jul 15, 2025
41447e3
add tests from dhruv
dcreager Jul 15, 2025
ec219b4
break out of the right loop
dcreager Jul 15, 2025
3290874
add positional-only tests
dcreager Jul 15, 2025
369ef05
mdlint
dcreager Jul 15, 2025
c9b9b48
Merge branch 'main' into dcreager/splat
dcreager Jul 22, 2025
abd0c9d
add overload equivalent of every test
dcreager Jul 22, 2025
b1b6087
mdlint
dcreager Jul 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/call/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,181 @@ def _(flag: bool):
reveal_type(foo()) # revealed: int
```

## Splatted arguments
Comment thread
dcreager marked this conversation as resolved.

### Unknown argument length

```py
def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> 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 _(args: list[int]) -> None:
takes_zero(*args)
takes_one(*args)
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)

def _(args: tuple[int, ...]) -> None:
takes_zero(*args)
takes_one(*args)
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*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_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 _(args: tuple[int]) -> None:
# error: [too-many-positional-arguments]
takes_zero(*args)
takes_one(*args)
# error: [missing-argument]
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
# error: [missing-argument]
takes_at_least_two(*args)

def _(args: tuple[int, int]) -> None:
# error: [too-many-positional-arguments]
takes_zero(*args)
# error: [too-many-positional-arguments]
takes_one(*args)
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)

def _(args: tuple[int, str]) -> None:
# error: [too-many-positional-arguments]
takes_zero(*args)
# error: [too-many-positional-arguments]
takes_one(*args)
# error: [invalid-argument-type]
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
# error: [invalid-argument-type]
takes_at_least_two(*args)
```

### 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_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 _(args: tuple[int, *tuple[int, ...]]) -> None:
Comment thread
dcreager marked this conversation as resolved.
# error: [too-many-positional-arguments]
takes_zero(*args)
takes_one(*args)
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)

def _(args: tuple[int, *tuple[str, ...]]) -> None:
# error: [too-many-positional-arguments]
takes_zero(*args)
takes_one(*args)
# error: [invalid-argument-type]
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
# error: [invalid-argument-type]
takes_at_least_two(*args)

def _(args: tuple[int, int, *tuple[int, ...]]) -> None:
# error: [too-many-positional-arguments]
takes_zero(*args)
# error: [too-many-positional-arguments]
takes_one(*args)
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)

def _(args: tuple[int, int, *tuple[str, ...]]) -> None:
# error: [too-many-positional-arguments]
takes_zero(*args)
# error: [too-many-positional-arguments]
takes_one(*args)
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)

def _(args: tuple[int, *tuple[int, ...], int]) -> None:
# error: [too-many-positional-arguments]
takes_zero(*args)
# error: [too-many-positional-arguments]
takes_one(*args)
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)

def _(args: tuple[int, *tuple[str, ...], int]) -> None:
# error: [too-many-positional-arguments]
takes_zero(*args)
# error: [too-many-positional-arguments]
takes_one(*args)
# error: [invalid-argument-type]
takes_two(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
# error: [invalid-argument-type]
takes_at_least_two(*args)
```

### 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. In this example, the ternary operator produces a complex union type,
which we expand when trying to call `range`. Our initial guess at its arity is "zero or more", but
there are individual union elements with more precise arities (such as "exactly two"). `range`, via
overloads and parameter defaults, can take in 1, 2, or 3 parameters. Our initial arity guess causes
us to assign the splatted argument to all three parameters. But when we check the `(0, 100)` union
element during argument expansion, we only have two values to provide for those three parameters.

For now, we have a workaround that pads out the splatted value with `Unknown` when we encounter this
case, but a proper fix would retry parameter matching for each expanded union element.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. The spec does say to loop over from step 2 which is the type checking step.

I've opened astral-sh/ty#735 to keep track of it.


```py
def _(batch_ids=(0, 100)) -> None:
arg = batch_ids if isinstance(batch_ids, tuple) else (0, batch_ids)
# revealed: (Unknown & tuple[Unknown, ...]) | (tuple[Literal[0], Literal[100]] & tuple[Unknown, ...]) | tuple[Literal[0], (Unknown & ~tuple[Unknown, ...]) | (tuple[Literal[0], Literal[100]] & ~tuple[Unknown, ...])]
reveal_type(arg)
range(*arg)
```
Comment thread
dcreager marked this conversation as resolved.

## Wrong argument type

### Positional argument, positional-or-keyword parameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 6 additions & 6 deletions crates/ty_python_semantic/src/types/call/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use itertools::{Either, Itertools};

use crate::Db;
use crate::types::KnownClass;
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::tuple::{TupleLength, TupleSpec, TupleType};

use super::Type;

Expand Down Expand Up @@ -37,9 +37,9 @@ impl<'a> CallArguments<'a> {
}
}

impl<'a> FromIterator<Argument<'a>> for CallArguments<'a> {
fn from_iter<T: IntoIterator<Item = Argument<'a>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
impl<'a> From<Vec<Argument<'a>>> for CallArguments<'a> {
fn from(arguments: Vec<Argument<'a>>) -> Self {
Self(arguments)
}
}

Expand All @@ -49,8 +49,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`).
Expand Down
Loading
Loading