Compare commits
3 Commits
jack/loop-
...
jack/nonlo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f22497bdd | ||
|
|
05cf7c3458 | ||
|
|
664a9a28dc |
@@ -670,7 +670,12 @@ impl SemanticSyntaxContext for Checker<'_> {
|
|||||||
| SemanticSyntaxErrorKind::InvalidStarExpression
|
| SemanticSyntaxErrorKind::InvalidStarExpression
|
||||||
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
|
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
|
||||||
| SemanticSyntaxErrorKind::DuplicateParameter(_)
|
| SemanticSyntaxErrorKind::DuplicateParameter(_)
|
||||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel
|
||||||
|
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
|
||||||
|
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
|
||||||
|
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
|
||||||
|
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_)
|
||||||
|
| SemanticSyntaxErrorKind::InvalidNonlocal(_) => {
|
||||||
self.semantic_errors.borrow_mut().push(error);
|
self.semantic_errors.borrow_mut().push(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -952,6 +952,9 @@ impl Display for SemanticSyntaxError {
|
|||||||
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => {
|
SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start: _ } => {
|
||||||
write!(f, "name `{name}` is used prior to global declaration")
|
write!(f, "name `{name}` is used prior to global declaration")
|
||||||
}
|
}
|
||||||
|
SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { name, start: _ } => {
|
||||||
|
write!(f, "name `{name}` is used prior to nonlocal declaration")
|
||||||
|
}
|
||||||
SemanticSyntaxErrorKind::InvalidStarExpression => {
|
SemanticSyntaxErrorKind::InvalidStarExpression => {
|
||||||
f.write_str("Starred expression cannot be used here")
|
f.write_str("Starred expression cannot be used here")
|
||||||
}
|
}
|
||||||
@@ -977,6 +980,18 @@ impl Display for SemanticSyntaxError {
|
|||||||
SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
||||||
write!(f, "nonlocal declaration not allowed at module level")
|
write!(f, "nonlocal declaration not allowed at module level")
|
||||||
}
|
}
|
||||||
|
SemanticSyntaxErrorKind::NonlocalAndGlobal(name) => {
|
||||||
|
write!(f, "name `{name}` is nonlocal and global")
|
||||||
|
}
|
||||||
|
SemanticSyntaxErrorKind::AnnotatedGlobal(name) => {
|
||||||
|
write!(f, "annotated name `{name}` can't be global")
|
||||||
|
}
|
||||||
|
SemanticSyntaxErrorKind::AnnotatedNonlocal(name) => {
|
||||||
|
write!(f, "annotated name `{name}` can't be nonlocal")
|
||||||
|
}
|
||||||
|
SemanticSyntaxErrorKind::InvalidNonlocal(name) => {
|
||||||
|
write!(f, "no binding for nonlocal `{name}` found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1207,6 +1222,24 @@ pub enum SemanticSyntaxErrorKind {
|
|||||||
/// [#111123]: https://github.com/python/cpython/issues/111123
|
/// [#111123]: https://github.com/python/cpython/issues/111123
|
||||||
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
|
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
|
||||||
|
|
||||||
|
/// Represents the use of a `nonlocal` variable before its `nonlocal` declaration.
|
||||||
|
///
|
||||||
|
/// ## Examples
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// def f():
|
||||||
|
/// counter = 0
|
||||||
|
/// def increment():
|
||||||
|
/// print(f"Adding 1 to {counter}")
|
||||||
|
/// nonlocal counter # SyntaxError: name 'counter' is used prior to nonlocal declaration
|
||||||
|
/// counter += 1
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Known Issues
|
||||||
|
///
|
||||||
|
/// See [`LoadBeforeGlobalDeclaration`][Self::LoadBeforeGlobalDeclaration].
|
||||||
|
LoadBeforeNonlocalDeclaration { name: String, start: TextSize },
|
||||||
|
|
||||||
/// Represents the use of a starred expression in an invalid location, such as a `return` or
|
/// Represents the use of a starred expression in an invalid location, such as a `return` or
|
||||||
/// `yield` statement.
|
/// `yield` statement.
|
||||||
///
|
///
|
||||||
@@ -1307,6 +1340,41 @@ pub enum SemanticSyntaxErrorKind {
|
|||||||
|
|
||||||
/// Represents a nonlocal declaration at module level
|
/// Represents a nonlocal declaration at module level
|
||||||
NonlocalDeclarationAtModuleLevel,
|
NonlocalDeclarationAtModuleLevel,
|
||||||
|
|
||||||
|
/// Represents the same variable declared as both nonlocal and global
|
||||||
|
NonlocalAndGlobal(String),
|
||||||
|
|
||||||
|
/// Represents a type annotation on a variable that's been declared global
|
||||||
|
AnnotatedGlobal(String),
|
||||||
|
|
||||||
|
/// Represents a type annotation on a variable that's been declared nonlocal
|
||||||
|
AnnotatedNonlocal(String),
|
||||||
|
|
||||||
|
/// Represents a nonlocal declaration with no definition in an enclosing scope
|
||||||
|
///
|
||||||
|
/// ## Examples
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// def f():
|
||||||
|
/// nonlocal x # error
|
||||||
|
///
|
||||||
|
/// # Global variables don't count.
|
||||||
|
/// x = 1
|
||||||
|
/// def f():
|
||||||
|
/// nonlocal x # error
|
||||||
|
///
|
||||||
|
/// def f():
|
||||||
|
/// x = 1
|
||||||
|
/// def g():
|
||||||
|
/// nonlocal x # allowed
|
||||||
|
///
|
||||||
|
/// # The definition can come later.
|
||||||
|
/// def f():
|
||||||
|
/// def g():
|
||||||
|
/// nonlocal x # allowed
|
||||||
|
/// x = 1
|
||||||
|
/// ```
|
||||||
|
InvalidNonlocal(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||||
|
|||||||
@@ -1304,7 +1304,7 @@ scope of the name that was declared `global`, can add a symbol to the global nam
|
|||||||
def f():
|
def f():
|
||||||
global g, h
|
global g, h
|
||||||
|
|
||||||
g: bool = True
|
g = True
|
||||||
|
|
||||||
f()
|
f()
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def f():
|
|||||||
x = 1
|
x = 1
|
||||||
def g() -> None:
|
def g() -> None:
|
||||||
nonlocal x
|
nonlocal x
|
||||||
global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global"
|
global x # error: [invalid-syntax] "name `x` is nonlocal and global"
|
||||||
x = None
|
x = None
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -209,5 +209,18 @@ x: int = 1
|
|||||||
|
|
||||||
def f():
|
def f():
|
||||||
global x
|
global x
|
||||||
x: str = "foo" # TODO: error: [invalid-syntax] "annotated name 'x' can't be global"
|
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be global"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global declarations affect the inferred type of the binding
|
||||||
|
|
||||||
|
Even if the `global` declaration isn't used in an assignment, we conservatively assume it could be:
|
||||||
|
|
||||||
|
```py
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def f():
|
||||||
|
global x
|
||||||
|
|
||||||
|
# TODO: reveal_type(x) # revealed: Unknown | Literal["1"]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -43,3 +43,321 @@ def f():
|
|||||||
def h():
|
def h():
|
||||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## The `nonlocal` keyword
|
||||||
|
|
||||||
|
Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in
|
||||||
|
enclosing scopes. This example isn't a type error, because the inner `x` shadows the outer one:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x: int = 1
|
||||||
|
def g():
|
||||||
|
x = "hello" # allowed
|
||||||
|
```
|
||||||
|
|
||||||
|
With `nonlocal` it is a type error, because `x` refers to the same place in both scopes:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x: int = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
x = "hello" # error: [invalid-assignment] "Object of type `Literal["hello"]` is not assignable to `int`"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local variable bindings "look ahead" to any assignment in the current scope
|
||||||
|
|
||||||
|
The binding `x = 2` in `g` causes the earlier read of `x` to refer to `g`'s not-yet-initialized
|
||||||
|
binding, rather than to `x = 1` in `f`'s scope:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
if x == 1: # error: [unresolved-reference] "Name `x` used when not defined"
|
||||||
|
x = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
The `nonlocal` keyword makes this example legal (and makes the assignment `x = 2` affect the outer
|
||||||
|
scope):
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
if x == 1:
|
||||||
|
x = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
For the same reason, using the `+=` operator in an inner scope is an error without `nonlocal`
|
||||||
|
(unless you shadow the outer variable first):
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
x += 1 # error: [unresolved-reference] "Name `x` used when not defined"
|
||||||
|
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
x = 1
|
||||||
|
x += 1 # allowed, but doesn't affect the outer scope
|
||||||
|
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
x += 1 # allowed, and affects the outer scope
|
||||||
|
```
|
||||||
|
|
||||||
|
## `nonlocal` declarations must match an outer binding
|
||||||
|
|
||||||
|
`nonlocal x` isn't allowed when there's no binding for `x` in an enclosing scope:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
def g():
|
||||||
|
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||||
|
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x, y # error: [invalid-syntax] "no binding for nonlocal `y` found"
|
||||||
|
```
|
||||||
|
|
||||||
|
A global `x` doesn't work. The target must be in a function-like scope:
|
||||||
|
|
||||||
|
```py
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def f():
|
||||||
|
def g():
|
||||||
|
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||||
|
|
||||||
|
def f():
|
||||||
|
global x
|
||||||
|
def g():
|
||||||
|
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||||
|
```
|
||||||
|
|
||||||
|
A class-scoped `x` also doesn't work:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
x = 1
|
||||||
|
@staticmethod
|
||||||
|
def f():
|
||||||
|
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||||
|
```
|
||||||
|
|
||||||
|
However, class-scoped bindings don't break the `nonlocal` chain the way `global` declarations do:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x: int = 1
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
x: str = "hello"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def g():
|
||||||
|
# Skips the class scope and reaches the outer function scope.
|
||||||
|
nonlocal x
|
||||||
|
x = 2 # allowed
|
||||||
|
x = "goodbye" # error: [invalid-assignment]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `nonlocal` uses the closest binding
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
x = 2
|
||||||
|
def h():
|
||||||
|
nonlocal x
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[2]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `nonlocal` "chaining"
|
||||||
|
|
||||||
|
Multiple `nonlocal` statements can "chain" through nested scopes:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
def h():
|
||||||
|
nonlocal x
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
And the `nonlocal` chain can skip over a scope that doesn't bind the variable:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f1():
|
||||||
|
x = 1
|
||||||
|
def f2():
|
||||||
|
nonlocal x
|
||||||
|
def f3():
|
||||||
|
# No binding; this scope gets skipped.
|
||||||
|
def f4():
|
||||||
|
nonlocal x
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
But a `global` statement breaks the chain:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
global x
|
||||||
|
def h():
|
||||||
|
nonlocal x # error: [invalid-syntax] "no binding for nonlocal `x` found"
|
||||||
|
```
|
||||||
|
|
||||||
|
## `nonlocal` bindings respect declared types from the defining scope, even without a binding
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x: int
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
x = "string" # error: [invalid-assignment] "Object of type `Literal["string"]` is not assignable to `int`"
|
||||||
|
```
|
||||||
|
|
||||||
|
## A complicated mixture of `nonlocal` chaining, empty scopes, class scopes, and the `global` keyword
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f1():
|
||||||
|
# The original bindings of `x`, `y`, and `z` with type declarations.
|
||||||
|
x: int = 1
|
||||||
|
y: int = 2
|
||||||
|
z: int = 3
|
||||||
|
|
||||||
|
def f2():
|
||||||
|
# This scope doesn't touch `x`, `y`, or `z` at all.
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
# This class scope is totally ignored.
|
||||||
|
x: str = "a"
|
||||||
|
y: str = "b"
|
||||||
|
z: str = "c"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def f3():
|
||||||
|
# This scope declares `x` nonlocal and `y` as global, and it shadows `z` without
|
||||||
|
# giving it a type declaration.
|
||||||
|
nonlocal x
|
||||||
|
x = 4
|
||||||
|
y = 5
|
||||||
|
global z
|
||||||
|
z = 6
|
||||||
|
|
||||||
|
def f4():
|
||||||
|
# This scope sees `x` from `f1` and `y` from `f3`. It *can't* declare `z`
|
||||||
|
# nonlocal, because of the global statement above, but it *can* load `z` as a
|
||||||
|
# "free" variable, in which case it sees the global value.
|
||||||
|
nonlocal x, y, z # error: [invalid-syntax] "no binding for nonlocal `z` found"
|
||||||
|
x = "string" # error: [invalid-assignment]
|
||||||
|
y = "string" # allowed, because `f3`'s `y` is untyped
|
||||||
|
reveal_type(z) # revealed: Unknown | Literal[6]
|
||||||
|
```
|
||||||
|
|
||||||
|
## TODO: `nonlocal` affects the inferred type in the outer scope
|
||||||
|
|
||||||
|
Without `nonlocal`, `g` can't write to `x`, and the inferred type of `x` in `f`'s scope isn't
|
||||||
|
affected by `g`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
But with `nonlocal`, `g` could write to `x`, and that affects its inferred type in `f`. That's true
|
||||||
|
regardless of whether `g` actually writes to `x`. With a write:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||||
|
x += 1
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[2]
|
||||||
|
# TODO: should be `Unknown | Literal[1]`
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
Without a write:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||||
|
# TODO: should be `Unknown | Literal[1]`
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Annotating a `nonlocal` binding is a syntax error
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x: int = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be nonlocal"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use before `nonlocal`
|
||||||
|
|
||||||
|
Using a name prior to its `nonlocal` declaration in the same scope is a syntax error:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
x = 2
|
||||||
|
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is true even if there are multiple `nonlocal` declarations of the same variable, as long as any
|
||||||
|
of them come after the usage:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
x = 2
|
||||||
|
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
|
||||||
|
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
def g():
|
||||||
|
nonlocal x
|
||||||
|
nonlocal x
|
||||||
|
x = 2 # allowed
|
||||||
|
```
|
||||||
|
|
||||||
|
## `nonlocal` before outer initialization
|
||||||
|
|
||||||
|
`nonlocal x` works even if `x` isn't bound in the enclosing scope until afterwards:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
def g():
|
||||||
|
# This is allowed, because of the subsequent definition of `x`.
|
||||||
|
nonlocal x
|
||||||
|
x = 1
|
||||||
|
```
|
||||||
|
|||||||
@@ -147,8 +147,7 @@ def nonlocal_use():
|
|||||||
X: Final[int] = 1
|
X: Final[int] = 1
|
||||||
def inner():
|
def inner():
|
||||||
nonlocal X
|
nonlocal X
|
||||||
# TODO: this should be an error
|
X = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `X` is not allowed: Reassignment of `Final` symbol"
|
||||||
X = 2
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`main.py`:
|
`main.py`:
|
||||||
|
|||||||
@@ -217,9 +217,6 @@ pub(crate) struct SemanticIndex<'db> {
|
|||||||
/// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`].
|
/// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`].
|
||||||
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
|
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
|
||||||
|
|
||||||
/// Map from the file-local [`FileScopeId`] to the set of explicit-global symbols it contains.
|
|
||||||
globals_by_scope: FxHashMap<FileScopeId, FxHashSet<ScopedPlaceId>>,
|
|
||||||
|
|
||||||
/// Use-def map for each scope in this file.
|
/// Use-def map for each scope in this file.
|
||||||
use_def_maps: IndexVec<FileScopeId, ArcUseDefMap<'db>>,
|
use_def_maps: IndexVec<FileScopeId, ArcUseDefMap<'db>>,
|
||||||
|
|
||||||
@@ -308,9 +305,19 @@ impl<'db> SemanticIndex<'db> {
|
|||||||
symbol: ScopedPlaceId,
|
symbol: ScopedPlaceId,
|
||||||
scope: FileScopeId,
|
scope: FileScopeId,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.globals_by_scope
|
self.place_table(scope)
|
||||||
.get(&scope)
|
.place_expr(symbol)
|
||||||
.is_some_and(|globals| globals.contains(&symbol))
|
.is_marked_global()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn symbol_is_nonlocal_in_scope(
|
||||||
|
&self,
|
||||||
|
symbol: ScopedPlaceId,
|
||||||
|
scope: FileScopeId,
|
||||||
|
) -> bool {
|
||||||
|
self.place_table(scope)
|
||||||
|
.place_expr(symbol)
|
||||||
|
.is_marked_nonlocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the id of the parent scope.
|
/// Returns the id of the parent scope.
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
|
|||||||
current_match_case: Option<CurrentMatchCase<'ast>>,
|
current_match_case: Option<CurrentMatchCase<'ast>>,
|
||||||
/// The name of the first function parameter of the innermost function that we're currently visiting.
|
/// The name of the first function parameter of the innermost function that we're currently visiting.
|
||||||
current_first_parameter_name: Option<&'ast str>,
|
current_first_parameter_name: Option<&'ast str>,
|
||||||
|
/// Functions defined in the current scope. We walk their bodies at the end of the scope.
|
||||||
|
deferred_function_bodies: Vec<&'ast ast::StmtFunctionDef>,
|
||||||
|
|
||||||
/// Per-scope contexts regarding nested `try`/`except` statements
|
/// Per-scope contexts regarding nested `try`/`except` statements
|
||||||
try_node_context_stack_manager: TryNodeContextStackManager,
|
try_node_context_stack_manager: TryNodeContextStackManager,
|
||||||
@@ -103,7 +105,6 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
|
|||||||
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
|
use_def_maps: IndexVec<FileScopeId, UseDefMapBuilder<'db>>,
|
||||||
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
|
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
|
||||||
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
||||||
globals_by_scope: FxHashMap<FileScopeId, FxHashSet<ScopedPlaceId>>,
|
|
||||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
|
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
|
||||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||||
imported_modules: FxHashSet<ModuleName>,
|
imported_modules: FxHashSet<ModuleName>,
|
||||||
@@ -127,6 +128,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
current_assignments: vec![],
|
current_assignments: vec![],
|
||||||
current_match_case: None,
|
current_match_case: None,
|
||||||
current_first_parameter_name: None,
|
current_first_parameter_name: None,
|
||||||
|
deferred_function_bodies: Vec::new(),
|
||||||
try_node_context_stack_manager: TryNodeContextStackManager::default(),
|
try_node_context_stack_manager: TryNodeContextStackManager::default(),
|
||||||
|
|
||||||
has_future_annotations: false,
|
has_future_annotations: false,
|
||||||
@@ -141,7 +143,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
scopes_by_node: FxHashMap::default(),
|
scopes_by_node: FxHashMap::default(),
|
||||||
definitions_by_node: FxHashMap::default(),
|
definitions_by_node: FxHashMap::default(),
|
||||||
expressions_by_node: FxHashMap::default(),
|
expressions_by_node: FxHashMap::default(),
|
||||||
globals_by_scope: FxHashMap::default(),
|
|
||||||
|
|
||||||
imported_modules: FxHashSet::default(),
|
imported_modules: FxHashSet::default(),
|
||||||
generator_functions: FxHashSet::default(),
|
generator_functions: FxHashSet::default(),
|
||||||
@@ -349,7 +350,12 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
popped_scope_id
|
popped_scope_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_place_table(&mut self) -> &mut PlaceTableBuilder {
|
fn current_place_table(&self) -> &PlaceTableBuilder {
|
||||||
|
let scope_id = self.current_scope();
|
||||||
|
&self.place_tables[scope_id]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_place_table_mut(&mut self) -> &mut PlaceTableBuilder {
|
||||||
let scope_id = self.current_scope();
|
let scope_id = self.current_scope();
|
||||||
&mut self.place_tables[scope_id]
|
&mut self.place_tables[scope_id]
|
||||||
}
|
}
|
||||||
@@ -389,7 +395,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
/// Add a symbol to the place table and the use-def map.
|
/// Add a symbol to the place table and the use-def map.
|
||||||
/// Return the [`ScopedPlaceId`] that uniquely identifies the symbol in both.
|
/// Return the [`ScopedPlaceId`] that uniquely identifies the symbol in both.
|
||||||
fn add_symbol(&mut self, name: Name) -> ScopedPlaceId {
|
fn add_symbol(&mut self, name: Name) -> ScopedPlaceId {
|
||||||
let (place_id, added) = self.current_place_table().add_symbol(name);
|
let (place_id, added) = self.current_place_table_mut().add_symbol(name);
|
||||||
if added {
|
if added {
|
||||||
self.current_use_def_map_mut().add_place(place_id);
|
self.current_use_def_map_mut().add_place(place_id);
|
||||||
}
|
}
|
||||||
@@ -399,7 +405,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
/// Add a place to the place table and the use-def map.
|
/// Add a place to the place table and the use-def map.
|
||||||
/// Return the [`ScopedPlaceId`] that uniquely identifies the place in both.
|
/// Return the [`ScopedPlaceId`] that uniquely identifies the place in both.
|
||||||
fn add_place(&mut self, place_expr: PlaceExprWithFlags) -> ScopedPlaceId {
|
fn add_place(&mut self, place_expr: PlaceExprWithFlags) -> ScopedPlaceId {
|
||||||
let (place_id, added) = self.current_place_table().add_place(place_expr);
|
let (place_id, added) = self.current_place_table_mut().add_place(place_expr);
|
||||||
if added {
|
if added {
|
||||||
self.current_use_def_map_mut().add_place(place_id);
|
self.current_use_def_map_mut().add_place(place_id);
|
||||||
}
|
}
|
||||||
@@ -407,15 +413,15 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mark_place_bound(&mut self, id: ScopedPlaceId) {
|
fn mark_place_bound(&mut self, id: ScopedPlaceId) {
|
||||||
self.current_place_table().mark_place_bound(id);
|
self.current_place_table_mut().mark_place_bound(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_place_declared(&mut self, id: ScopedPlaceId) {
|
fn mark_place_declared(&mut self, id: ScopedPlaceId) {
|
||||||
self.current_place_table().mark_place_declared(id);
|
self.current_place_table_mut().mark_place_declared(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_place_used(&mut self, id: ScopedPlaceId) {
|
fn mark_place_used(&mut self, id: ScopedPlaceId) {
|
||||||
self.current_place_table().mark_place_used(id);
|
self.current_place_table_mut().mark_place_used(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_entry_for_definition_key(&mut self, key: DefinitionNodeKey) -> &mut Definitions<'db> {
|
fn add_entry_for_definition_key(&mut self, key: DefinitionNodeKey) -> &mut Definitions<'db> {
|
||||||
@@ -1004,8 +1010,83 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visit_function_body(&mut self, function_def: &'ast ast::StmtFunctionDef) {
|
||||||
|
let ast::StmtFunctionDef {
|
||||||
|
parameters,
|
||||||
|
type_params,
|
||||||
|
returns,
|
||||||
|
body,
|
||||||
|
..
|
||||||
|
} = function_def;
|
||||||
|
self.with_type_params(
|
||||||
|
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
||||||
|
type_params.as_deref(),
|
||||||
|
|builder| {
|
||||||
|
builder.visit_parameters(parameters);
|
||||||
|
if let Some(returns) = returns {
|
||||||
|
builder.visit_annotation(returns);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.push_scope(NodeWithScopeRef::Function(function_def));
|
||||||
|
|
||||||
|
builder.declare_parameters(parameters);
|
||||||
|
|
||||||
|
let mut first_parameter_name = parameters
|
||||||
|
.iter_non_variadic_params()
|
||||||
|
.next()
|
||||||
|
.map(|first_param| first_param.parameter.name.id().as_str());
|
||||||
|
std::mem::swap(
|
||||||
|
&mut builder.current_first_parameter_name,
|
||||||
|
&mut first_parameter_name,
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.visit_scoped_body(body);
|
||||||
|
|
||||||
|
builder.current_first_parameter_name = first_parameter_name;
|
||||||
|
builder.pop_scope()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk the body of a scope, either the global scope or a function scope.
|
||||||
|
///
|
||||||
|
/// When we encounter a (top-level or nested) function definition, we add the function's name
|
||||||
|
/// to the current scope, but we defer walking its body until the end. (See the `FunctionDef`
|
||||||
|
/// branch of `visit_stmt`.) This deferred approach is necessary to be able to check `nonlocal`
|
||||||
|
/// statements as we encounter them, for example:
|
||||||
|
///
|
||||||
|
/// ```py
|
||||||
|
/// def f():
|
||||||
|
/// def g():
|
||||||
|
/// nonlocal x # allowed
|
||||||
|
/// nonlocal y # SyntaxError: no binding for nonlocal 'y' found
|
||||||
|
/// x = 1
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// See the comments in the `Nonlocal` branch of `visit_stmt`, which relies on this binding
|
||||||
|
/// information being present.
|
||||||
|
fn visit_scoped_body(&mut self, body: &'ast [ast::Stmt]) {
|
||||||
|
debug_assert!(
|
||||||
|
self.deferred_function_bodies.is_empty(),
|
||||||
|
"every function starts with a clean scope",
|
||||||
|
);
|
||||||
|
|
||||||
|
// If this scope contains function definitions, they'll be added to
|
||||||
|
// `self.deferred_function_bodies` as we walk each statement.
|
||||||
|
self.visit_body(body);
|
||||||
|
|
||||||
|
// Now that we've walked all the statements in this scope, walk any deferred function
|
||||||
|
// bodies. This is recursive, so we need to clear out the contents of
|
||||||
|
// `self.deferred_function_bodies` and give each function a fresh list (or else we'll fail
|
||||||
|
// the `debug_assert!` above).
|
||||||
|
let taken_deferred_function_bodies = std::mem::take(&mut self.deferred_function_bodies);
|
||||||
|
for function_def in taken_deferred_function_bodies {
|
||||||
|
self.visit_function_body(function_def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn build(mut self) -> SemanticIndex<'db> {
|
pub(super) fn build(mut self) -> SemanticIndex<'db> {
|
||||||
self.visit_body(self.module.suite());
|
self.visit_scoped_body(self.module.suite());
|
||||||
|
|
||||||
// Pop the root scope
|
// Pop the root scope
|
||||||
self.pop_scope();
|
self.pop_scope();
|
||||||
@@ -1042,7 +1123,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
self.scopes_by_node.shrink_to_fit();
|
self.scopes_by_node.shrink_to_fit();
|
||||||
self.generator_functions.shrink_to_fit();
|
self.generator_functions.shrink_to_fit();
|
||||||
self.eager_snapshots.shrink_to_fit();
|
self.eager_snapshots.shrink_to_fit();
|
||||||
self.globals_by_scope.shrink_to_fit();
|
|
||||||
|
|
||||||
SemanticIndex {
|
SemanticIndex {
|
||||||
place_tables,
|
place_tables,
|
||||||
@@ -1050,7 +1130,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||||||
definitions_by_node: self.definitions_by_node,
|
definitions_by_node: self.definitions_by_node,
|
||||||
expressions_by_node: self.expressions_by_node,
|
expressions_by_node: self.expressions_by_node,
|
||||||
scope_ids_by_scope: self.scope_ids_by_scope,
|
scope_ids_by_scope: self.scope_ids_by_scope,
|
||||||
globals_by_scope: self.globals_by_scope,
|
|
||||||
ast_ids,
|
ast_ids,
|
||||||
scopes_by_expression: self.scopes_by_expression,
|
scopes_by_expression: self.scopes_by_expression,
|
||||||
scopes_by_node: self.scopes_by_node,
|
scopes_by_node: self.scopes_by_node,
|
||||||
@@ -1084,46 +1163,19 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||||||
let ast::StmtFunctionDef {
|
let ast::StmtFunctionDef {
|
||||||
decorator_list,
|
decorator_list,
|
||||||
parameters,
|
parameters,
|
||||||
type_params,
|
|
||||||
name,
|
name,
|
||||||
returns,
|
..
|
||||||
body,
|
|
||||||
is_async: _,
|
|
||||||
range: _,
|
|
||||||
node_index: _,
|
|
||||||
} = function_def;
|
} = function_def;
|
||||||
|
|
||||||
|
// Like Ruff, we don't walk the body of the function here. Instead, we defer it to
|
||||||
|
// the end of the current scope. See `visit_scoped_body`. See also the comments in
|
||||||
|
// the `Nonlocal` branch below about why this deferred visit order is necessary.
|
||||||
|
self.deferred_function_bodies.push(function_def);
|
||||||
|
|
||||||
for decorator in decorator_list {
|
for decorator in decorator_list {
|
||||||
self.visit_decorator(decorator);
|
self.visit_decorator(decorator);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.with_type_params(
|
|
||||||
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
|
||||||
type_params.as_deref(),
|
|
||||||
|builder| {
|
|
||||||
builder.visit_parameters(parameters);
|
|
||||||
if let Some(returns) = returns {
|
|
||||||
builder.visit_annotation(returns);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.push_scope(NodeWithScopeRef::Function(function_def));
|
|
||||||
|
|
||||||
builder.declare_parameters(parameters);
|
|
||||||
|
|
||||||
let mut first_parameter_name = parameters
|
|
||||||
.iter_non_variadic_params()
|
|
||||||
.next()
|
|
||||||
.map(|first_param| first_param.parameter.name.id().as_str());
|
|
||||||
std::mem::swap(
|
|
||||||
&mut builder.current_first_parameter_name,
|
|
||||||
&mut first_parameter_name,
|
|
||||||
);
|
|
||||||
|
|
||||||
builder.visit_body(body);
|
|
||||||
|
|
||||||
builder.current_first_parameter_name = first_parameter_name;
|
|
||||||
builder.pop_scope()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// The default value of the parameters needs to be evaluated in the
|
// The default value of the parameters needs to be evaluated in the
|
||||||
// enclosing scope.
|
// enclosing scope.
|
||||||
for default in parameters
|
for default in parameters
|
||||||
@@ -1418,6 +1470,29 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||||||
self.visit_expr(value);
|
self.visit_expr(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let ast::Expr::Name(name) = &*node.target {
|
||||||
|
let symbol_id = self.add_symbol(name.id.clone());
|
||||||
|
let symbol = self.current_place_table().place_expr(symbol_id);
|
||||||
|
// Check whether the variable has been declared global.
|
||||||
|
if symbol.is_marked_global() {
|
||||||
|
self.report_semantic_error(SemanticSyntaxError {
|
||||||
|
kind: SemanticSyntaxErrorKind::AnnotatedGlobal(name.id.as_str().into()),
|
||||||
|
range: name.range,
|
||||||
|
python_version: self.python_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check whether the variable has been declared nonlocal.
|
||||||
|
if symbol.is_marked_nonlocal() {
|
||||||
|
self.report_semantic_error(SemanticSyntaxError {
|
||||||
|
kind: SemanticSyntaxErrorKind::AnnotatedNonlocal(
|
||||||
|
name.id.as_str().into(),
|
||||||
|
),
|
||||||
|
range: name.range,
|
||||||
|
python_version: self.python_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// See https://docs.python.org/3/library/ast.html#ast.AnnAssign
|
// See https://docs.python.org/3/library/ast.html#ast.AnnAssign
|
||||||
if matches!(
|
if matches!(
|
||||||
*node.target,
|
*node.target,
|
||||||
@@ -1858,8 +1933,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||||||
}) => {
|
}) => {
|
||||||
for name in names {
|
for name in names {
|
||||||
let symbol_id = self.add_symbol(name.id.clone());
|
let symbol_id = self.add_symbol(name.id.clone());
|
||||||
let symbol_table = self.current_place_table();
|
let symbol = self.current_place_table().place_expr(symbol_id);
|
||||||
let symbol = symbol_table.place_expr(symbol_id);
|
// Check whether the variable has already been accessed in this scope.
|
||||||
if symbol.is_bound() || symbol.is_declared() || symbol.is_used() {
|
if symbol.is_bound() || symbol.is_declared() || symbol.is_used() {
|
||||||
self.report_semantic_error(SemanticSyntaxError {
|
self.report_semantic_error(SemanticSyntaxError {
|
||||||
kind: SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration {
|
kind: SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration {
|
||||||
@@ -1870,11 +1945,112 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||||||
python_version: self.python_version,
|
python_version: self.python_version,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let scope_id = self.current_scope();
|
// Check whether the variable has also been declared nonlocal.
|
||||||
self.globals_by_scope
|
if symbol.is_marked_nonlocal() {
|
||||||
.entry(scope_id)
|
self.report_semantic_error(SemanticSyntaxError {
|
||||||
.or_default()
|
kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()),
|
||||||
.insert(symbol_id);
|
range: name.range,
|
||||||
|
python_version: self.python_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.current_place_table_mut().mark_place_global(symbol_id);
|
||||||
|
}
|
||||||
|
walk_stmt(self, stmt);
|
||||||
|
}
|
||||||
|
ast::Stmt::Nonlocal(ast::StmtNonlocal {
|
||||||
|
range: _,
|
||||||
|
node_index: _,
|
||||||
|
names,
|
||||||
|
}) => {
|
||||||
|
for name in names {
|
||||||
|
let local_scoped_place_id = self.add_symbol(name.id.clone());
|
||||||
|
let local_place = self.current_place_table().place_expr(local_scoped_place_id);
|
||||||
|
// Check whether the variable has already been accessed in this scope.
|
||||||
|
if local_place.is_bound() || local_place.is_declared() || local_place.is_used()
|
||||||
|
{
|
||||||
|
self.report_semantic_error(SemanticSyntaxError {
|
||||||
|
kind: SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration {
|
||||||
|
name: name.to_string(),
|
||||||
|
start: name.range.start(),
|
||||||
|
},
|
||||||
|
range: name.range,
|
||||||
|
python_version: self.python_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check whether the variable has also been declared global.
|
||||||
|
if local_place.is_marked_global() {
|
||||||
|
self.report_semantic_error(SemanticSyntaxError {
|
||||||
|
kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()),
|
||||||
|
range: name.range,
|
||||||
|
python_version: self.python_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// The name is required to exist in an enclosing scope, but that definition
|
||||||
|
// might come later. For example, this is example legal:
|
||||||
|
//
|
||||||
|
// ```py
|
||||||
|
// def f():
|
||||||
|
// def g():
|
||||||
|
// nonlocal x
|
||||||
|
// x = 1
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// To handle cases like this, we have to walk `x = 1` before we walk `nonlocal
|
||||||
|
// x`. In other words, walking function bodies must be "deferred" to the end of
|
||||||
|
// the scope where they're defined. See the `FunctionDef` branch above.
|
||||||
|
let name_expr = PlaceExpr::name(name.id.clone());
|
||||||
|
let mut found_matching_definition = false;
|
||||||
|
for enclosing_scope_info in self.scope_stack.iter().rev().skip(1) {
|
||||||
|
let enclosing_scope = &self.scopes[enclosing_scope_info.file_scope_id];
|
||||||
|
if !enclosing_scope.kind().is_function_like() {
|
||||||
|
// Skip over class scopes and the global scope.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let enclosing_place_table =
|
||||||
|
&self.place_tables[enclosing_scope_info.file_scope_id];
|
||||||
|
let Some(enclosing_scoped_place_id) =
|
||||||
|
enclosing_place_table.place_id_by_expr(&name_expr)
|
||||||
|
else {
|
||||||
|
// This name isn't defined in this scope. Keep going.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let enclosing_place =
|
||||||
|
enclosing_place_table.place_expr(enclosing_scoped_place_id);
|
||||||
|
// We've found a definition for this name in an enclosing function-like
|
||||||
|
// scope. Either this definition is the valid place this name refers to, or
|
||||||
|
// else we'll emit a syntax error. Either way, we won't walk any more
|
||||||
|
// enclosing scopes. Note that there are differences here compared to
|
||||||
|
// `infer_place_load`: A regular load (e.g. `print(x)`) is allowed to refer
|
||||||
|
// to a global variable (e.g. `x = 1` in the global scope), and similarly
|
||||||
|
// it's allowed to refer to a variable in an enclosing function that's
|
||||||
|
// declared `global` (e.g. `global x`). However, the `nonlocal` keyword
|
||||||
|
// can't refer to global variables (that's a `SyntaxError`), and it also
|
||||||
|
// can't refer to variables in enclosing functions that are declared
|
||||||
|
// `global` (also a `SyntaxError`).
|
||||||
|
if enclosing_place.is_marked_global() {
|
||||||
|
// A "chain" of `nonlocal` statements is "broken" by a `global`
|
||||||
|
// statement. Stop looping and report that this `nonlocal` statement is
|
||||||
|
// invalid.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// We found a definition, and we've checked that that place isn't declared
|
||||||
|
// `global` in its scope, but it's ok if it's `nonlocal`. If a chain of
|
||||||
|
// `nonlocal` statements fails to lead to a valid binding, the outermost
|
||||||
|
// one will be an error; we don't need to report an error for each one.
|
||||||
|
found_matching_definition = true;
|
||||||
|
self.current_place_table_mut()
|
||||||
|
.mark_place_nonlocal(local_scoped_place_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !found_matching_definition {
|
||||||
|
// There's no matching definition in an enclosing scope. This `nonlocal`
|
||||||
|
// statement is invalid.
|
||||||
|
self.report_semantic_error(SemanticSyntaxError {
|
||||||
|
kind: SemanticSyntaxErrorKind::InvalidNonlocal(name.to_string()),
|
||||||
|
range: name.range,
|
||||||
|
python_version: self.python_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
walk_stmt(self, stmt);
|
walk_stmt(self, stmt);
|
||||||
}
|
}
|
||||||
@@ -1888,7 +2064,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
|||||||
for target in targets {
|
for target in targets {
|
||||||
if let Ok(target) = PlaceExpr::try_from(target) {
|
if let Ok(target) = PlaceExpr::try_from(target) {
|
||||||
let place_id = self.add_place(PlaceExprWithFlags::new(target));
|
let place_id = self.add_place(PlaceExprWithFlags::new(target));
|
||||||
self.current_place_table().mark_place_used(place_id);
|
self.current_place_table_mut().mark_place_used(place_id);
|
||||||
self.delete_binding(place_id);
|
self.delete_binding(place_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,6 +330,16 @@ impl PlaceExprWithFlags {
|
|||||||
self.flags.contains(PlaceFlags::IS_DECLARED)
|
self.flags.contains(PlaceFlags::IS_DECLARED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is the place `global` its containing scope?
|
||||||
|
pub fn is_marked_global(&self) -> bool {
|
||||||
|
self.flags.contains(PlaceFlags::MARKED_GLOBAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the place `nonlocal` its containing scope?
|
||||||
|
pub fn is_marked_nonlocal(&self) -> bool {
|
||||||
|
self.flags.contains(PlaceFlags::MARKED_NONLOCAL)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn as_name(&self) -> Option<&Name> {
|
pub(crate) fn as_name(&self) -> Option<&Name> {
|
||||||
self.expr.as_name()
|
self.expr.as_name()
|
||||||
}
|
}
|
||||||
@@ -397,9 +407,7 @@ bitflags! {
|
|||||||
const IS_USED = 1 << 0;
|
const IS_USED = 1 << 0;
|
||||||
const IS_BOUND = 1 << 1;
|
const IS_BOUND = 1 << 1;
|
||||||
const IS_DECLARED = 1 << 2;
|
const IS_DECLARED = 1 << 2;
|
||||||
/// TODO: This flag is not yet set by anything
|
|
||||||
const MARKED_GLOBAL = 1 << 3;
|
const MARKED_GLOBAL = 1 << 3;
|
||||||
/// TODO: This flag is not yet set by anything
|
|
||||||
const MARKED_NONLOCAL = 1 << 4;
|
const MARKED_NONLOCAL = 1 << 4;
|
||||||
const IS_INSTANCE_ATTRIBUTE = 1 << 5;
|
const IS_INSTANCE_ATTRIBUTE = 1 << 5;
|
||||||
}
|
}
|
||||||
@@ -663,7 +671,7 @@ impl PlaceTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the place named `name`.
|
/// Returns the place named `name`.
|
||||||
#[allow(unused)] // used in tests
|
#[cfg(test)]
|
||||||
pub(crate) fn place_by_name(&self, name: &str) -> Option<&PlaceExprWithFlags> {
|
pub(crate) fn place_by_name(&self, name: &str) -> Option<&PlaceExprWithFlags> {
|
||||||
let id = self.place_id_by_name(name)?;
|
let id = self.place_id_by_name(name)?;
|
||||||
Some(self.place_expr(id))
|
Some(self.place_expr(id))
|
||||||
@@ -814,6 +822,14 @@ impl PlaceTableBuilder {
|
|||||||
self.table.places[id].insert_flags(PlaceFlags::IS_USED);
|
self.table.places[id].insert_flags(PlaceFlags::IS_USED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn mark_place_global(&mut self, id: ScopedPlaceId) {
|
||||||
|
self.table.places[id].insert_flags(PlaceFlags::MARKED_GLOBAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mark_place_nonlocal(&mut self, id: ScopedPlaceId) {
|
||||||
|
self.table.places[id].insert_flags(PlaceFlags::MARKED_NONLOCAL);
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn places(&self) -> impl Iterator<Item = &PlaceExprWithFlags> {
|
pub(super) fn places(&self) -> impl Iterator<Item = &PlaceExprWithFlags> {
|
||||||
self.table.places()
|
self.table.places()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1564,6 +1564,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||||||
let mut bound_ty = ty;
|
let mut bound_ty = ty;
|
||||||
|
|
||||||
let global_use_def_map = self.index.use_def_map(FileScopeId::global());
|
let global_use_def_map = self.index.use_def_map(FileScopeId::global());
|
||||||
|
let nonlocal_use_def_map;
|
||||||
let place_id = binding.place(self.db());
|
let place_id = binding.place(self.db());
|
||||||
let place = place_table.place_expr(place_id);
|
let place = place_table.place_expr(place_id);
|
||||||
let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, place_id);
|
let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, place_id);
|
||||||
@@ -1574,9 +1575,58 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||||||
.place_id_by_expr(&place.expr)
|
.place_id_by_expr(&place.expr)
|
||||||
{
|
{
|
||||||
Some(id) => (global_use_def_map.end_of_scope_declarations(id), false),
|
Some(id) => (global_use_def_map.end_of_scope_declarations(id), false),
|
||||||
// This case is a syntax error (load before global declaration) but ignore that here
|
// This variable shows up in `global` declarations but doesn't have an explicit
|
||||||
|
// binding in the global scope.
|
||||||
None => (use_def.declarations_at_binding(binding), true),
|
None => (use_def.declarations_at_binding(binding), true),
|
||||||
}
|
}
|
||||||
|
} else if self
|
||||||
|
.index
|
||||||
|
.symbol_is_nonlocal_in_scope(place_id, file_scope_id)
|
||||||
|
{
|
||||||
|
// If we run out of ancestor scopes without finding a definition, we'll fall back to
|
||||||
|
// the local scope. This will also be a syntax error in `infer_nonlocal_statement` (no
|
||||||
|
// binding for `nonlocal` found), but ignore that here.
|
||||||
|
let mut declarations = use_def.declarations_at_binding(binding);
|
||||||
|
let mut is_local = true;
|
||||||
|
// Walk up parent scopes looking for the enclosing scope that has definition of this
|
||||||
|
// name. `ancestor_scopes` includes the current scope, so skip that one.
|
||||||
|
for (enclosing_scope_file_id, enclosing_scope) in
|
||||||
|
self.index.ancestor_scopes(file_scope_id).skip(1)
|
||||||
|
{
|
||||||
|
// Ignore class scopes and the global scope.
|
||||||
|
if !enclosing_scope.kind().is_function_like() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let enclosing_place_table = self.index.place_table(enclosing_scope_file_id);
|
||||||
|
let Some(enclosing_place_id) = enclosing_place_table.place_id_by_expr(&place.expr)
|
||||||
|
else {
|
||||||
|
// This ancestor scope doesn't have a binding. Keep going.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if self
|
||||||
|
.index
|
||||||
|
.symbol_is_nonlocal_in_scope(enclosing_place_id, enclosing_scope_file_id)
|
||||||
|
{
|
||||||
|
// The variable is `nonlocal` in this ancestor scope. Keep going.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.index
|
||||||
|
.symbol_is_global_in_scope(enclosing_place_id, enclosing_scope_file_id)
|
||||||
|
{
|
||||||
|
// The variable is `global` in this ancestor scope. This breaks the `nonlocal`
|
||||||
|
// chain, and it's a syntax error in `infer_nonlocal_statement`. Ignore that
|
||||||
|
// here and just bail out of this loop.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// We found the closest definition. Note that (unlike in `infer_place_load`) this
|
||||||
|
// does *not* need to be a binding. It could be just `x: int`.
|
||||||
|
nonlocal_use_def_map = self.index.use_def_map(enclosing_scope_file_id);
|
||||||
|
declarations = nonlocal_use_def_map.end_of_scope_declarations(enclosing_place_id);
|
||||||
|
is_local = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
(declarations, is_local)
|
||||||
} else {
|
} else {
|
||||||
(use_def.declarations_at_binding(binding), true)
|
(use_def.declarations_at_binding(binding), true)
|
||||||
};
|
};
|
||||||
@@ -5800,14 +5850,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||||||
|
|
||||||
let current_file = self.file();
|
let current_file = self.file();
|
||||||
|
|
||||||
|
let mut is_nonlocal_binding = false;
|
||||||
if let Some(name) = expr.as_name() {
|
if let Some(name) = expr.as_name() {
|
||||||
let skip_non_global_scopes = place_table
|
if let Some(symbol_id) = place_table.place_id_by_name(name) {
|
||||||
.place_id_by_name(name)
|
if self.skip_non_global_scopes(file_scope_id, symbol_id) {
|
||||||
.is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id));
|
|
||||||
|
|
||||||
if skip_non_global_scopes {
|
|
||||||
return global_symbol(self.db(), self.file(), name);
|
return global_symbol(self.db(), self.file(), name);
|
||||||
}
|
}
|
||||||
|
is_nonlocal_binding = self
|
||||||
|
.index
|
||||||
|
.symbol_is_nonlocal_in_scope(symbol_id, file_scope_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a function-like scope and there is one or more binding in this scope (but
|
// If it's a function-like scope and there is one or more binding in this scope (but
|
||||||
@@ -5819,7 +5871,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||||||
// a local variable or not in function-like scopes. If a variable has any bindings in a
|
// a local variable or not in function-like scopes. If a variable has any bindings in a
|
||||||
// function-like scope, it is considered a local variable; it never references another
|
// function-like scope, it is considered a local variable; it never references another
|
||||||
// scope. (At runtime, it would use the `LOAD_FAST` opcode.)
|
// scope. (At runtime, it would use the `LOAD_FAST` opcode.)
|
||||||
if has_bindings_in_this_scope && scope.is_function_like(db) {
|
if has_bindings_in_this_scope && scope.is_function_like(db) && !is_nonlocal_binding {
|
||||||
return Place::Unbound.into();
|
return Place::Unbound.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user