Skip to content

nonlocal snapshot sweeping considers unrelated scopes, sweeps too much #927

@oconnor663

Description

@oconnor663

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 = 2

Similarly, 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 = 2

However, 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 = 3

Similarly, 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 = 3

I'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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions