allow reads of "free" variables to refer to a global declaration

Previously this worked if there was also a binding in the same scope as
the `global` declaration (probably almost always the case), but CPython
doesn't require this.

This change surfaced an error in an existing test, where a global
variable was only ever declared and bound using the `global` keyword,
and never mentioned explicitly in the global scope. @AlexWaygood
suggested we probably want to keep that requirement, so I'm adding an a
new test for that on top of fixing the failing test.
This commit is contained in:
Jack O'Connor 2025-07-14 07:58:32 -07:00
parent 3b4667ec32
commit 5f2e855c29
2 changed files with 28 additions and 2 deletions

View file

@ -44,6 +44,24 @@ def f():
reveal_type(x) # revealed: str reveal_type(x) # revealed: str
``` ```
## Reads terminate at the `global` keyword in an enclosing scope, even if there's no binding in that scope
_Unlike_ variables that are explicitly declared `nonlocal` (below), implicitly nonlocal ("free")
reads can come from a variable that's declared `global` in an enclosing scope. It doesn't matter
whether the variable is bound in that scope:
```py
x: int = 1
def f():
x: str = "hello"
def g():
global x
def h():
# allowed: this loads the global `x` variable due to the `global` declaration in the immediate enclosing scope
y: int = x
```
## The `nonlocal` keyword ## The `nonlocal` keyword
Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in
@ -264,8 +282,8 @@ def f1():
@staticmethod @staticmethod
def f3(): def f3():
# This scope declares `x` nonlocal and `y` as global, and it shadows `z` without # This scope declares `x` nonlocal, shadows `y` without a type declaration, and
# giving it a type declaration. # declares `z` global.
nonlocal x nonlocal x
x = 4 x = 4
y = 5 y = 5

View file

@ -6079,6 +6079,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let Some(enclosing_place) = enclosing_place_table.place_by_expr(expr) else { let Some(enclosing_place) = enclosing_place_table.place_by_expr(expr) else {
continue; continue;
}; };
if enclosing_place.is_marked_global() {
// Reads of "free" variables can terminate at an enclosing scope that marks the
// variable `global` but doesn't actually bind it. In that case, stop walking
// scopes and proceed to the global handling below. (But note that it's a
// semantic syntax error for the `nonlocal` keyword to do this. See
// `infer_nonlocal_statement`.)
break;
}
if enclosing_place.is_bound() || enclosing_place.is_declared() { if enclosing_place.is_bound() || enclosing_place.is_declared() {
// We can return early here, because the nearest function-like scope that // We can return early here, because the nearest function-like scope that
// defines a name must be the only source for the nonlocal reference (at // defines a name must be the only source for the nonlocal reference (at