ruff icon indicating copy to clipboard operation
ruff copied to clipboard

[red-knot] model that class/comprehension bodies run immediately

Open carljm opened this issue 1 year ago • 2 comments

Currently, we have a general strategy for name lookups in enclosing scopes: they use the type as seen from end of the scope where it's defined. For example:

x = 1

def f():
    # This is correct for any call to `f` that would happen after this module has
    # fully executed. It could be wrong if `f` is called during execution of the
    # module body.
    reveal_type(x)  # revealed: Literal["foo"]

x = "foo"

For nested functions, this is a pretty reasonable default behavior. Another option would be to consider all definitions of x (or at least all definitions of x visible from the point where f is defined, forwards). This would result in revealing Literal[1] | Literal["foo"] for x in the inner scope of f, instead. This would be safer, but also more annoying for the more common case where f is not called from the module body. It would also mean we would always consider x possibly-undefined inside f, if f is defined before x, which could be really annoying.

We could try to be smarter by detecting, for example, that the function f is (or might be) called from module level, and only consider earlier definitions if we observe this possibility. But this will always be best-effort, since reliably detecting all possible paths (other functions, even possibly in other modules) through which f could be called from module level isn't feasible.

But there are cases where we can be quite sure that the nested scope will run before the end of the outer scope. For example, class definitions:

x = 1
class C:
    y = x
x = "foo"

# This is currently what we reveal, but it's clearly wrong: should be `Literal[1]`
reveal_type(C.y)  # revealed: Literal["foo"]

The class body always runs immediately, so it should resolve types in the enclosing scope from the class definition's point in control flow, not from end-of-scope.

carljm avatar Oct 25 '24 22:10 carljm

Should we do the same for list/dict/set comprehensions? (Generators expressions obviously do not run immediately, unlike list/dict/set comprehensions.)

AlexWaygood avatar Oct 26 '24 18:10 AlexWaygood

Yes!

carljm avatar Oct 26 '24 18:10 carljm

See https://github.com/astral-sh/ruff/issues/13879 for discussion of generator expressions as it relates to this task.

carljm avatar Jan 09 '25 17:01 carljm

https://github.com/astral-sh/ruff/pull/16079

dcreager avatar Feb 10 '25 22:02 dcreager

@dcreager is this complete now?

MichaReiser avatar Feb 19 '25 20:02 MichaReiser

I think it is.

carljm avatar Feb 19 '25 20:02 carljm

I'm looking at how we're representing the bindings snapshots next, to see if there's any performance we can squeeze out of it. But I'm :+1: to closing this since the functionality is there and working.

dcreager avatar Feb 19 '25 22:02 dcreager