mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Avoid unnecessary argument type expansion (#19999)
## Summary Part of: https://github.com/astral-sh/ty/issues/868 This PR adds a heuristic to avoid argument type expansion if it's going to eventually lead to no matching overload. This is done by checking whether the non-expandable argument types are assignable to the corresponding annotated parameter type. If one of them is not assignable to all of the remaining overloads, then argument type expansion isn't going to help. ## Test Plan Add mdtest that would otherwise take a long time because of the number of arguments that it would need to expand (30).
This commit is contained in:
parent
99111961c0
commit
d43a3d34dd
3 changed files with 277 additions and 1 deletions
|
@ -620,6 +620,217 @@ def _(ab: A | B, ac: A | C, cd: C | D):
|
|||
reveal_type(f(*(cd,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Optimization: Avoid argument type expansion
|
||||
|
||||
Argument type expansion could lead to exponential growth of the number of argument lists that needs
|
||||
to be evaluated, so ty deploys some heuristics to prevent this from happening.
|
||||
|
||||
Heuristic: If an argument type that cannot be expanded and cannot be assighned to any of the
|
||||
remaining overloads before argument type expansion, then even with argument type expansion, it won't
|
||||
lead to a successful evaluation of the call.
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import overload
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
@overload
|
||||
def f() -> None: ...
|
||||
@overload
|
||||
def f(**kwargs: int) -> C: ...
|
||||
@overload
|
||||
def f(x: A, /, **kwargs: int) -> A: ...
|
||||
@overload
|
||||
def f(x: B, /, **kwargs: int) -> B: ...
|
||||
|
||||
class Foo:
|
||||
@overload
|
||||
def f(self) -> None: ...
|
||||
@overload
|
||||
def f(self, **kwargs: int) -> C: ...
|
||||
@overload
|
||||
def f(self, x: A, /, **kwargs: int) -> A: ...
|
||||
@overload
|
||||
def f(self, x: B, /, **kwargs: int) -> B: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from overloaded import A, B, C, Foo, f
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
def _(ab: A | B, a=1):
|
||||
reveal_type(f(a1=a, a2=a, a3=a)) # revealed: C
|
||||
reveal_type(f(A(), a1=a, a2=a, a3=a)) # revealed: A
|
||||
reveal_type(f(B(), a1=a, a2=a, a3=a)) # revealed: B
|
||||
|
||||
# Here, the arity check filters out the first and second overload, type checking fails on the
|
||||
# remaining overloads, so ty moves on to argument type expansion. But, the first argument (`C`)
|
||||
# isn't assignable to any of the remaining overloads (3 and 4), so there's no point in expanding
|
||||
# the other 30 arguments of type `Unknown | Literal[1]` which would result in allocating a
|
||||
# vector containing 2**30 argument lists after expanding all of the arguments.
|
||||
reveal_type(
|
||||
# error: [no-matching-overload]
|
||||
# revealed: Unknown
|
||||
f(
|
||||
C(),
|
||||
a1=a,
|
||||
a2=a,
|
||||
a3=a,
|
||||
a4=a,
|
||||
a5=a,
|
||||
a6=a,
|
||||
a7=a,
|
||||
a8=a,
|
||||
a9=a,
|
||||
a10=a,
|
||||
a11=a,
|
||||
a12=a,
|
||||
a13=a,
|
||||
a14=a,
|
||||
a15=a,
|
||||
a16=a,
|
||||
a17=a,
|
||||
a18=a,
|
||||
a19=a,
|
||||
a20=a,
|
||||
a21=a,
|
||||
a22=a,
|
||||
a23=a,
|
||||
a24=a,
|
||||
a25=a,
|
||||
a26=a,
|
||||
a27=a,
|
||||
a28=a,
|
||||
a29=a,
|
||||
a30=a,
|
||||
)
|
||||
)
|
||||
|
||||
# Here, the heuristics won't come into play because all arguments can be expanded but expanding
|
||||
# the first argument resutls in a successful evaluation of the call, so there's no exponential
|
||||
# growth of the number of argument lists.
|
||||
reveal_type(
|
||||
# revealed: A | B
|
||||
f(
|
||||
ab,
|
||||
a1=a,
|
||||
a2=a,
|
||||
a3=a,
|
||||
a4=a,
|
||||
a5=a,
|
||||
a6=a,
|
||||
a7=a,
|
||||
a8=a,
|
||||
a9=a,
|
||||
a10=a,
|
||||
a11=a,
|
||||
a12=a,
|
||||
a13=a,
|
||||
a14=a,
|
||||
a15=a,
|
||||
a16=a,
|
||||
a17=a,
|
||||
a18=a,
|
||||
a19=a,
|
||||
a20=a,
|
||||
a21=a,
|
||||
a22=a,
|
||||
a23=a,
|
||||
a24=a,
|
||||
a25=a,
|
||||
a26=a,
|
||||
a27=a,
|
||||
a28=a,
|
||||
a29=a,
|
||||
a30=a,
|
||||
)
|
||||
)
|
||||
|
||||
def _(foo: Foo, ab: A | B, a=1):
|
||||
reveal_type(foo.f(a1=a, a2=a, a3=a)) # revealed: C
|
||||
reveal_type(foo.f(A(), a1=a, a2=a, a3=a)) # revealed: A
|
||||
reveal_type(foo.f(B(), a1=a, a2=a, a3=a)) # revealed: B
|
||||
|
||||
reveal_type(
|
||||
# error: [no-matching-overload]
|
||||
# revealed: Unknown
|
||||
foo.f(
|
||||
C(),
|
||||
a1=a,
|
||||
a2=a,
|
||||
a3=a,
|
||||
a4=a,
|
||||
a5=a,
|
||||
a6=a,
|
||||
a7=a,
|
||||
a8=a,
|
||||
a9=a,
|
||||
a10=a,
|
||||
a11=a,
|
||||
a12=a,
|
||||
a13=a,
|
||||
a14=a,
|
||||
a15=a,
|
||||
a16=a,
|
||||
a17=a,
|
||||
a18=a,
|
||||
a19=a,
|
||||
a20=a,
|
||||
a21=a,
|
||||
a22=a,
|
||||
a23=a,
|
||||
a24=a,
|
||||
a25=a,
|
||||
a26=a,
|
||||
a27=a,
|
||||
a28=a,
|
||||
a29=a,
|
||||
a30=a,
|
||||
)
|
||||
)
|
||||
|
||||
reveal_type(
|
||||
# revealed: A | B
|
||||
foo.f(
|
||||
ab,
|
||||
a1=a,
|
||||
a2=a,
|
||||
a3=a,
|
||||
a4=a,
|
||||
a5=a,
|
||||
a6=a,
|
||||
a7=a,
|
||||
a8=a,
|
||||
a9=a,
|
||||
a10=a,
|
||||
a11=a,
|
||||
a12=a,
|
||||
a13=a,
|
||||
a14=a,
|
||||
a15=a,
|
||||
a16=a,
|
||||
a17=a,
|
||||
a18=a,
|
||||
a19=a,
|
||||
a20=a,
|
||||
a21=a,
|
||||
a22=a,
|
||||
a23=a,
|
||||
a24=a,
|
||||
a25=a,
|
||||
a26=a,
|
||||
a27=a,
|
||||
a28=a,
|
||||
a29=a,
|
||||
a30=a,
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Filtering based on `Any` / `Unknown`
|
||||
|
||||
This is the step 5 of the overload call evaluation algorithm which specifies that:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue