-
Notifications
You must be signed in to change notification settings - Fork 229
Description
As of astral-sh/ruff#19321 we use "lazy snapshots" to track when a variable in an enclosing scope is never re-bound, which lets us infer a narrower type in nested functions. For example (working as intended):
def foo():
x: int = 1
def bar():
reveal_type(x) # `Literal[1]`
def foo():
x: int = 1
def bar():
reveal_type(x) # `int` (un-narrowed because of reassignment)
x = 2Similarly, we sweep/ignore enclosing snapshots of a variable if any nested function declares that variable nonlocal and binds it (also working as intended):
def foo():
x: int = 1
def bar():
reveal_type(x) # `int` (un-narrowed because of nonlocal assignment)
def baz():
nonlocal x
x = 2However, that sweeping is currently more aggressive than it needs to be. It finds nonlocal+bound symbols of the same name, even in unrelated later (not earlier) scopes:
def foo():
x: int = 1
def bar():
reveal_type(x) # `int` (should be narrowed)
def bing():
x = 2
def baz():
nonlocal x
x = 3Similarly, it finds symbols in nested scopes that potentially could be aliasing but actually aren't because of shadowing:
def foo():
x: int = 1
def bar():
reveal_type(x) # `int` (should be narrowed)
def bing():
x = 2
def baz():
nonlocal x
x = 3I'm currently working on adding a couple maps to symbol tables that will track "reference -> definition in enclosing scope" and "definition -> references in nested scopes", and it might be easier to fix this once that change lands. I'll link it here when I put up a PR. cc @mtshiba