pyrefly icon indicating copy to clipboard operation
pyrefly copied to clipboard

Function invocation pins type of arguments too early

Open grievejia opened this issue 9 months ago • 1 comments

Minimal repro:

def foo[T](a: T, b: T) -> T:
    ...

def test(x: int | None, y: int | None) -> int | None:
    if x is None:
        return foo(x, y)
    return None

Expected: No error Actual: Argument int | None is not assignable to parameter b with type None in function foo [bad-argument-type]

grievejia avatar Apr 16 '25 21:04 grievejia

Sandbox URL: https://pyrefly.org/sandbox/?code=CYUwZgBGD20NoBUC6AKAhgLgggNBARlggJQQC0AfNhgFAT0QB0zNNokALiAM4coAeWAJYA7DhAA+EAHLQRIPAE9hYyTLkhSlCKPFTZ82gx2R+O7usN1jDAE4gOAV1siosAUuLX69py8sgQA

lolpack avatar May 01 '25 02:05 lolpack

i don't understand the initial example - if x is None coerces the type of x to None and then foo(x, y) could indeed fail typechecking if y is int, so pyrefly is doing the right thing there.

martindemello avatar Jun 29 '25 22:06 martindemello

@martindemello If the typevar T for foo gets instantiated into int | None then type checking won't fail. i.e.

def foo(a: int | None, b: int | None) -> int | None:
    ...

def test(x: int | None, y: int | None) -> int | None:
    if x is None:
        return foo(x, y)  # This should type check
    return None

We don't really want to assume the call foo(x, y) forces T to be None just because its first argument x is None -- the point here is that if we wait longer and get to see what the type of y is, we could have guessed better type for T.

grievejia avatar Jul 07 '25 18:07 grievejia

If I understand, the following is a even simpler example where the eager pinning of generics can go awry:

def foo[T](a: T, b: T) -> T: ...
foo(0, 1.)   # Argument `float` is not assignable to parameter `b` with type `int` in function `foo`

samwgoldman avatar Jul 07 '25 19:07 samwgoldman

This issue has someone assigned, but has not had recent activity for more than 2 weeks.

If you are still working on this issue, please add a comment so everyone knows. Otherwise, please unassign yourself and allow someone else to take over.

Thank you for your contributions!

github-actions[bot] avatar Aug 05 '25 00:08 github-actions[bot]

Still working on this :)

samwgoldman avatar Aug 11 '25 17:08 samwgoldman

This issue has someone assigned, but has not had recent activity for more than 2 weeks.

If you are still working on this issue, please add a comment so everyone knows. Otherwise, please unassign yourself and allow someone else to take over.

Thank you for your contributions!

github-actions[bot] avatar Aug 26 '25 00:08 github-actions[bot]

This issue has someone assigned, but has not had recent activity for more than 2 weeks.

If you are still working on this issue, please add a comment so everyone knows. Otherwise, please unassign yourself and allow someone else to take over.

Thank you for your contributions!

github-actions[bot] avatar Oct 16 '25 00:10 github-actions[bot]

Now, no errors.

asukaminato0721 avatar Oct 30 '25 08:10 asukaminato0721

Huh, thanks for checking! I'm not sure why the original example doesn't error anymore (maybe it was fixed by one of the recent improvements that @stroxler made?), but it's easy to modify the example to error again:

def foo[T](a: T, b: T) -> T:
    ...

def test(x: int | None, y: int | None) -> int | None:
    if x is None:
        z = foo(x, y) # error: Argument `int | None` is not assignable to parameter `b` with type `None` in function `foo`
        return z
    return None

sandbox

rchen152 avatar Oct 30 '25 16:10 rchen152

Another example, reported from Reddit

x: float = 0.0
y: int = 0
max(0, 0.0)
max(y, 0.0)

sandbox

yangdanny97 avatar Nov 19 '25 18:11 yangdanny97