Skip to content

Commit 3c4ec82

Browse files
authored
[red-knot] support non-local name lookups (#13177)
Add support for non-local name lookups. There's one TODO around annotated assignments without a RHS; these need a fair amount of attention, which they'll get in an upcoming PR about declared vs inferred types. Fixes #11663
1 parent 29c36a5 commit 3c4ec82

File tree

3 files changed

+161
-41
lines changed

3 files changed

+161
-41
lines changed

crates/red_knot_python_semantic/src/semantic_index/symbol.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ impl FileScopeId {
149149
FileScopeId::from_u32(0)
150150
}
151151

152+
pub fn is_global(self) -> bool {
153+
self == FileScopeId::global()
154+
}
155+
152156
pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> {
153157
let index = semantic_index(db, file);
154158
index.scope_ids_by_scope[self]

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 156 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ use crate::semantic_index::ast_ids::{HasScopedAstId, HasScopedUseId, ScopedExpre
4444
use crate::semantic_index::definition::{Definition, DefinitionKind, DefinitionNodeKey};
4545
use crate::semantic_index::expression::Expression;
4646
use crate::semantic_index::semantic_index;
47-
use crate::semantic_index::symbol::{FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId};
47+
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
4848
use crate::semantic_index::SemanticIndex;
4949
use crate::types::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
5050
use crate::types::{
5151
builtins_symbol_ty_by_name, definitions_ty, global_symbol_ty_by_name, symbol_ty,
52-
BytesLiteralType, ClassType, FunctionType, StringLiteralType, Type, UnionBuilder,
52+
symbol_ty_by_name, BytesLiteralType, ClassType, FunctionType, StringLiteralType, Type,
53+
UnionBuilder,
5354
};
5455
use crate::Db;
5556

@@ -301,7 +302,7 @@ impl<'db> TypeInferenceBuilder<'db> {
301302
self.file.is_stub(self.db.upcast())
302303
}
303304

304-
/// Are we currently inferred deferred types?
305+
/// Are we currently inferring deferred types?
305306
fn is_deferred(&self) -> bool {
306307
matches!(self.region, InferenceRegion::Deferred(_))
307308
}
@@ -1823,6 +1824,61 @@ impl<'db> TypeInferenceBuilder<'db> {
18231824
Type::Unknown
18241825
}
18251826

1827+
/// Look up a name reference that isn't bound in the local scope.
1828+
fn lookup_name(&self, name: &ast::name::Name) -> Type<'db> {
1829+
let file_scope_id = self.scope.file_scope_id(self.db);
1830+
let symbols = self.index.symbol_table(file_scope_id);
1831+
let symbol = symbols
1832+
.symbol_by_name(name)
1833+
.expect("Expected the symbol table to create a symbol for every Name node");
1834+
1835+
// In function-like scopes, any local variable (symbol that is defined in this
1836+
// scope) can only have a definition in this scope, or be undefined; it never references
1837+
// another scope. (At runtime, it would use the `LOAD_FAST` opcode.)
1838+
if !symbol.is_defined() || !self.scope.is_function_like(self.db) {
1839+
// Walk up parent scopes looking for a possible enclosing scope that may have a
1840+
// definition of this name visible to us (would be `LOAD_DEREF` at runtime.)
1841+
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id) {
1842+
// Class scopes are not visible to nested scopes, and we need to handle global
1843+
// scope differently (because an unbound name there falls back to builtins), so
1844+
// check only function-like scopes.
1845+
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(self.db, self.file);
1846+
if !enclosing_scope_id.is_function_like(self.db) {
1847+
continue;
1848+
}
1849+
let enclosing_symbol_table = self.index.symbol_table(enclosing_scope_file_id);
1850+
let Some(enclosing_symbol) = enclosing_symbol_table.symbol_by_name(name) else {
1851+
continue;
1852+
};
1853+
// TODO a "definition" that is just an annotated-assignment with no RHS should not
1854+
// count as "is_defined" here.
1855+
if enclosing_symbol.is_defined() {
1856+
// We can return early here, because the nearest function-like scope that
1857+
// defines a name must be the only source for the nonlocal reference (at
1858+
// runtime, it is the scope that creates the cell for our closure.) If the name
1859+
// isn't bound in that scope, we should get an unbound name, not continue
1860+
// falling back to other scopes / globals / builtins.
1861+
return symbol_ty_by_name(self.db, enclosing_scope_id, name);
1862+
}
1863+
}
1864+
// No nonlocal binding, check module globals. Avoid infinite recursion if `self.scope`
1865+
// already is module globals.
1866+
let ty = if file_scope_id.is_global() {
1867+
Type::Unbound
1868+
} else {
1869+
global_symbol_ty_by_name(self.db, self.file, name)
1870+
};
1871+
// Fallback to builtins (without infinite recursion if we're already in builtins.)
1872+
if ty.may_be_unbound(self.db) && Some(self.scope) != builtins_scope(self.db) {
1873+
ty.replace_unbound_with(self.db, builtins_symbol_ty_by_name(self.db, name))
1874+
} else {
1875+
ty
1876+
}
1877+
} else {
1878+
Type::Unbound
1879+
}
1880+
}
1881+
18261882
fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> {
18271883
let ast::ExprName { range: _, id, ctx } = name;
18281884
let file_scope_id = self.scope.file_scope_id(self.db);
@@ -1843,30 +1899,7 @@ impl<'db> TypeInferenceBuilder<'db> {
18431899
let may_be_unbound = use_def.use_may_be_unbound(use_id);
18441900

18451901
let unbound_ty = if may_be_unbound {
1846-
let symbols = self.index.symbol_table(file_scope_id);
1847-
// SAFETY: the symbol table always creates a symbol for every Name node.
1848-
let symbol = symbols.symbol_by_name(id).unwrap();
1849-
if !symbol.is_defined() || !self.scope.is_function_like(self.db) {
1850-
// implicit global
1851-
let unbound_ty = if file_scope_id == FileScopeId::global() {
1852-
Type::Unbound
1853-
} else {
1854-
global_symbol_ty_by_name(self.db, self.file, id)
1855-
};
1856-
// fallback to builtins
1857-
if unbound_ty.may_be_unbound(self.db)
1858-
&& Some(self.scope) != builtins_scope(self.db)
1859-
{
1860-
Some(unbound_ty.replace_unbound_with(
1861-
self.db,
1862-
builtins_symbol_ty_by_name(self.db, id),
1863-
))
1864-
} else {
1865-
Some(unbound_ty)
1866-
}
1867-
} else {
1868-
Some(Type::Unbound)
1869-
}
1902+
Some(self.lookup_name(id))
18701903
} else {
18711904
None
18721905
};
@@ -2385,6 +2418,31 @@ mod tests {
23852418
assert_eq!(ty.display(db).to_string(), expected);
23862419
}
23872420

2421+
fn assert_scope_ty(
2422+
db: &TestDb,
2423+
file_name: &str,
2424+
scopes: &[&str],
2425+
symbol_name: &str,
2426+
expected: &str,
2427+
) {
2428+
let file = system_path_to_file(db, file_name).expect("Expected file to exist.");
2429+
let index = semantic_index(db, file);
2430+
let mut file_scope_id = FileScopeId::global();
2431+
let mut scope = file_scope_id.to_scope_id(db, file);
2432+
for expected_scope_name in scopes {
2433+
file_scope_id = index
2434+
.child_scopes(file_scope_id)
2435+
.next()
2436+
.unwrap_or_else(|| panic!("scope of {expected_scope_name}"))
2437+
.0;
2438+
scope = file_scope_id.to_scope_id(db, file);
2439+
assert_eq!(scope.name(db), *expected_scope_name);
2440+
}
2441+
2442+
let ty = symbol_ty_by_name(db, scope, symbol_name);
2443+
assert_eq!(ty.display(db).to_string(), expected);
2444+
}
2445+
23882446
#[test]
23892447
fn follow_import_to_class() -> anyhow::Result<()> {
23902448
let mut db = setup_db();
@@ -3601,15 +3659,6 @@ mod tests {
36013659
Ok(())
36023660
}
36033661

3604-
fn first_public_def<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> {
3605-
let scope = global_scope(db, file);
3606-
use_def_map(db, scope)
3607-
.public_definitions(symbol_table(db, scope).symbol_id_by_name(name).unwrap())
3608-
.next()
3609-
.unwrap()
3610-
.definition
3611-
}
3612-
36133662
#[test]
36143663
fn big_int() -> anyhow::Result<()> {
36153664
let mut db = setup_db();
@@ -3694,6 +3743,77 @@ mod tests {
36943743
Ok(())
36953744
}
36963745

3746+
#[test]
3747+
fn nonlocal_name_reference() -> anyhow::Result<()> {
3748+
let mut db = setup_db();
3749+
3750+
db.write_dedented(
3751+
"/src/a.py",
3752+
"
3753+
def f():
3754+
x = 1
3755+
def g():
3756+
y = x
3757+
",
3758+
)?;
3759+
3760+
assert_scope_ty(&db, "/src/a.py", &["f", "g"], "y", "Literal[1]");
3761+
3762+
Ok(())
3763+
}
3764+
3765+
#[test]
3766+
fn nonlocal_name_reference_multi_level() -> anyhow::Result<()> {
3767+
let mut db = setup_db();
3768+
3769+
db.write_dedented(
3770+
"/src/a.py",
3771+
"
3772+
def f():
3773+
x = 1
3774+
def g():
3775+
def h():
3776+
y = x
3777+
",
3778+
)?;
3779+
3780+
assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]");
3781+
3782+
Ok(())
3783+
}
3784+
3785+
#[test]
3786+
fn nonlocal_name_reference_skips_class_scope() -> anyhow::Result<()> {
3787+
let mut db = setup_db();
3788+
3789+
db.write_dedented(
3790+
"/src/a.py",
3791+
"
3792+
def f():
3793+
x = 1
3794+
class C:
3795+
x = 2
3796+
def g():
3797+
y = x
3798+
",
3799+
)?;
3800+
3801+
assert_scope_ty(&db, "/src/a.py", &["f", "C", "g"], "y", "Literal[1]");
3802+
3803+
Ok(())
3804+
}
3805+
3806+
// Incremental inference tests
3807+
3808+
fn first_public_def<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> {
3809+
let scope = global_scope(db, file);
3810+
use_def_map(db, scope)
3811+
.public_definitions(symbol_table(db, scope).symbol_id_by_name(name).unwrap())
3812+
.next()
3813+
.unwrap()
3814+
.definition
3815+
}
3816+
36973817
#[test]
36983818
fn dependency_public_symbol_type_change() -> anyhow::Result<()> {
36993819
let mut db = setup_db();

crates/ruff_benchmark/benches/red_knot.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ struct Case {
2121

2222
const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/8e8a4baf652f6e1cee7acde9d78c4b6154539748/Lib/tomllib";
2323

24-
// The "unresolved import" is because we don't understand `*` imports yet.
24+
// The failed import from 'collections.abc' is due to lack of support for 'import *'.
2525
static EXPECTED_DIAGNOSTICS: &[&str] = &[
2626
"/src/tomllib/_parser.py:7:29: Module 'collections.abc' has no member 'Iterable'",
27-
"/src/tomllib/_parser.py:686:23: Object of type 'Unbound' is not callable",
2827
"Line 69 is too long (89 characters)",
2928
"Use double quotes for strings",
3029
"Use double quotes for strings",
@@ -33,10 +32,7 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
3332
"Use double quotes for strings",
3433
"Use double quotes for strings",
3534
"Use double quotes for strings",
36-
"/src/tomllib/_parser.py:330:32: Name 'header' used when not defined.",
37-
"/src/tomllib/_parser.py:330:41: Name 'key' used when not defined.",
3835
"/src/tomllib/_parser.py:628:75: Name 'e' used when not defined.",
39-
"/src/tomllib/_parser.py:686:23: Name 'parse_float' used when not defined.",
4036
];
4137

4238
fn get_test_file(name: &str) -> TestFile {

0 commit comments

Comments
 (0)