Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] How should we handle unreachable code? #15797

Open
dcreager opened this issue Jan 28, 2025 · 4 comments
Open

[red-knot] How should we handle unreachable code? #15797

dcreager opened this issue Jan 28, 2025 · 4 comments
Labels
red-knot Multi-file analysis & type inference

Comments

@dcreager
Copy link
Member

dcreager commented Jan 28, 2025

We can statically determine that code is unreachable, due to terminal statements or statically known branches:

def f():
    x = 1
    return
    reveal_type(x)  # ???

def g():
    x = 1
    if True:
        return
    reveal_type(x)  # ???

We should consider how we want to handle this. I think the uncontroversial part is that we should produce a (possibly optional?) diagnostic about the code being unreachable. But after that, there are several options:

  • We could mark all bindings as not visible once we reach a terminal statement. That would produce an unresolved-reference errors. This seems least helpful, since it seems likely that the user either (a) didn't intend for the code to become unreachable or (b) is inserting e.g. early returns as part of a debugging process. In both cases the user expects the supposedly unreachable code to be well-typed, and would want diagnostics about ways that it's not.

  • We could consider the bindings visible, but update their inferred type to be Never to signify that those symbols wouldn't have any values in the unreachable code. This is arguably the most accurate interpretation, but doesn't provide much value over the "this code is unreachable" diagnostic.

  • We could type-check the unreachable code as if it were reachable. Note that this would only apply to truly unreachable code, but not in something like:

    def h(cond: bool):
        x = 1
        if cond:
            return
        reveal_type(x)  # revealed: Literal[1]

    Here, the return statement isn't always executed, and so the reveal_type is not unreachable, and the obviously correct result is Literal[1].

We should also look into what mypy and pyright do here, to see if there's an obvious community consensus we should follow.

@dcreager dcreager added the red-knot Multi-file analysis & type inference label Jan 28, 2025
@dcreager
Copy link
Member Author

Per @AlexWaygood: #15676 (comment)

Mypy has terrible UX if it sees a reveal_type in a block of code it considers unreachable: it just doesn't emit any diagnostic for the reveal_type call at all. This has been the source of many bug reports at mypy over the years, because the rule that tells you off for having unreachable code in the first place is disabled by default, even if you opt into mypy's --strict flag. We shouldn't do what mypy does!

@sharkdp
Copy link
Contributor

sharkdp commented Jan 29, 2025

since it seems likely that the user either (a) didn't intend for the code to become unreachable or (b) is inserting e.g. early returns as part of a debugging process. In both cases the user expects the supposedly unreachable code to be well-typed, and would want diagnostics about ways that it's not.

I think there is also a valid use case (c) where the user intends the code to be unreachable, but only in certain scenarios. This would be the case for sys.version_info/sys.platform checks that would lead to unreachable code if checked on a version/platform that does not meet the criterion:

if sys.version_info >= (3, 11):
    # [code that makes use of symbols only available in 3.11+]
  • We could type-check the unreachable code as if it were reachable.

I think this could lead to many false-positives for the scenario (c) above, because we would emit unresolved-reference diagnostics for all 3.11+ symbols, if we check that code with a target version <3.11.

As @AlexWaygood pointed out recently: we shouldn't even treat that use case (c) in the same way that we treat (a) and (b). Of course we would/could still gray-out the code inside that conditional if the target version is <3.11, but emitting a unreachable-code diagnostic for this code section would be rather annoying, I believe.

We can statically determine that code is unreachable, due to terminal statements or statically known branches:

There is also a distinct third option, where we call a function that does not return (has return type Never/NoReturn). Like a function that always raises, goes into an infinite loop or that aborts the process.

See also: this section in the Pyright documentation.

@mitsuhiko
Copy link

Personal feedback: my expectation as a user is that the type inference is unchanged because of the presence of a known return. The reason is that for debugging purposes it's very common to introduce a temporary early return that is unconditional.

Now there are subtleties but I think that the right thing to do is to type check it as if it was reachable.

@eltoder
Copy link

eltoder commented Jan 29, 2025

I think in general, if some code is provably unreachable, this warrants a warning, but should not by itself change types of anything. If the code is unreachable because, for example, you eliminated all alternatives in a union, that should be a warning, plus the union type should decay to Never.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
red-knot Multi-file analysis & type inference
Projects
None yet
Development

No branches or pull requests

4 participants