Skip to content

Commit 1ba1351

Browse files
committed
[ty] Support walrus operator scope leaking from comprehensions
Lazy Avoid clone
1 parent 9259e3d commit 1ba1351

File tree

4 files changed

+235
-26
lines changed

4 files changed

+235
-26
lines changed

crates/ty_python_semantic/resources/mdtest/assignment/walrus.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,153 @@ x = 0
1515
(x := x + 1)
1616
reveal_type(x) # revealed: Literal[1]
1717
```
18+
19+
## Walrus in comprehensions
20+
21+
PEP 572: Named expressions in comprehensions bind the target in the first enclosing scope that is
22+
not a comprehension.
23+
24+
### List comprehension element
25+
26+
```py
27+
class Iterator:
28+
def __next__(self) -> int:
29+
return 42
30+
31+
class Iterable:
32+
def __iter__(self) -> Iterator:
33+
return Iterator()
34+
35+
[(a := b * 2) for b in Iterable()]
36+
reveal_type(a) # revealed: int
37+
```
38+
39+
### Comprehension filter
40+
41+
```py
42+
class Iterator:
43+
def __next__(self) -> int:
44+
return 42
45+
46+
class Iterable:
47+
def __iter__(self) -> Iterator:
48+
return Iterator()
49+
50+
[c for d in Iterable() if (c := d - 10) > 0]
51+
reveal_type(c) # revealed: int
52+
```
53+
54+
### Dict comprehension
55+
56+
```py
57+
class Iterator:
58+
def __next__(self) -> int:
59+
return 42
60+
61+
class Iterable:
62+
def __iter__(self) -> Iterator:
63+
return Iterator()
64+
65+
{(e := f * 2): (g := f * 3) for f in Iterable()}
66+
reveal_type(e) # revealed: int
67+
reveal_type(g) # revealed: int
68+
```
69+
70+
### Generator expression
71+
72+
```py
73+
class Iterator:
74+
def __next__(self) -> int:
75+
return 42
76+
77+
class Iterable:
78+
def __iter__(self) -> Iterator:
79+
return Iterator()
80+
81+
list(((h := i * 2) for i in Iterable()))
82+
reveal_type(h) # revealed: int
83+
```
84+
85+
### Class body comprehension
86+
87+
```py
88+
class C:
89+
[(x := y) for y in range(3)]
90+
reveal_type(x) # revealed: int
91+
```
92+
93+
### First generator `iter`
94+
95+
The `iter` of the first generator is evaluated in the enclosing scope. A walrus there should bind in
96+
the enclosing scope as usual (no comprehension scope is involved).
97+
98+
```py
99+
def returns_list() -> list[int]:
100+
return [1, 2, 3]
101+
102+
[x for x in (y := returns_list())]
103+
reveal_type(y) # revealed: list[int]
104+
```
105+
106+
### Nested comprehension
107+
108+
```py
109+
[[(x := y) for y in range(3)] for _ in range(3)]
110+
reveal_type(x) # revealed: int
111+
```
112+
113+
### Updates lazy snapshots in nested scopes
114+
115+
```py
116+
def returns_str() -> str:
117+
return "foo"
118+
119+
def outer() -> None:
120+
x = returns_str()
121+
122+
def inner() -> None:
123+
reveal_type(x) # revealed: str | int
124+
[(x := y) for y in range(3)]
125+
inner()
126+
```
127+
128+
### Possibly defined in `except` handlers
129+
130+
```py
131+
def could_raise() -> list[int]:
132+
return [1]
133+
134+
try:
135+
[(y := n) for n in could_raise()]
136+
except:
137+
# error: [possibly-unresolved-reference]
138+
reveal_type(y) # revealed: int
139+
```
140+
141+
### Honoring `global` declaration
142+
143+
PEP 572: the walrus honors a `global` declaration in the enclosing scope.
144+
145+
```py
146+
x: int = 0
147+
148+
def f() -> None:
149+
global x
150+
[(x := y) for y in range(3)]
151+
reveal_type(x) # revealed: int
152+
```
153+
154+
### Honoring `nonlocal` declaration
155+
156+
PEP 572: the walrus honors a `nonlocal` declaration in the enclosing scope.
157+
158+
```py
159+
def outer() -> None:
160+
x = "hello"
161+
162+
def inner() -> None:
163+
nonlocal x
164+
[(x := y) for y in range(3)]
165+
reveal_type(x) # revealed: int
166+
inner()
167+
```

crates/ty_python_semantic/resources/mdtest/import/star.md

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -491,22 +491,13 @@ reveal_type(s) # revealed: Unknown
491491
# error: [unresolved-reference]
492492
reveal_type(t) # revealed: Unknown
493493

494-
# TODO: these should all reveal `Unknown | int` and should not emit errors.
495-
# (We don't generally model elsewhere in ty that bindings from walruses
496-
# "leak" from comprehension scopes into outer scopes, but we should.)
497-
# See https://github.com/astral-sh/ruff/issues/16954
498-
# error: [unresolved-reference]
499-
reveal_type(g) # revealed: Unknown
500-
# error: [unresolved-reference]
501-
reveal_type(i) # revealed: Unknown
502-
# error: [unresolved-reference]
503-
reveal_type(k) # revealed: Unknown
504-
# error: [unresolved-reference]
505-
reveal_type(m) # revealed: Unknown
506-
# error: [unresolved-reference]
507-
reveal_type(o) # revealed: Unknown
508-
# error: [unresolved-reference]
509-
reveal_type(q) # revealed: Unknown
494+
# PEP 572: walrus targets in comprehensions leak into the enclosing scope.
495+
reveal_type(g) # revealed: int
496+
reveal_type(i) # revealed: int
497+
reveal_type(k) # revealed: int
498+
reveal_type(m) # revealed: int
499+
reveal_type(o) # revealed: int
500+
reveal_type(q) # revealed: int
510501
```
511502

512503
### An annotation without a value is a definition in a stub but not a `.py` file

crates/ty_python_semantic/src/semantic_index/builder.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,28 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
285285
false
286286
}
287287

288+
/// Returns the enclosing non-comprehension scope for walrus operator targets,
289+
/// per [PEP 572]. Named expressions in comprehensions bind in the first
290+
/// enclosing scope that is *not* a comprehension.
291+
///
292+
/// Returns `None` if the current scope is not a comprehension.
293+
///
294+
/// [PEP 572]: https://peps.python.org/pep-0572/#scope-of-the-target
295+
fn enclosing_scope_for_walrus(&self) -> Option<(FileScopeId, usize)> {
296+
if self.scopes[self.current_scope()].kind() != ScopeKind::Comprehension {
297+
return None;
298+
}
299+
self.scope_stack
300+
.iter()
301+
.enumerate()
302+
.rev()
303+
.skip(1)
304+
.find_map(|(index, info)| {
305+
(self.scopes[info.file_scope_id].kind() != ScopeKind::Comprehension)
306+
.then_some((info.file_scope_id, index))
307+
})
308+
}
309+
288310
/// Push a new loop, returning the outer loop, if any.
289311
fn push_loop(&mut self) -> Option<Loop> {
290312
self.current_scope_info_mut()
@@ -801,10 +823,28 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
801823
);
802824
}
803825
Some(CurrentAssignment::Named(named)) => {
804-
// TODO(dhruvmanila): If the current scope is a comprehension, then the
805-
// named expression is implicitly nonlocal. This is yet to be
806-
// implemented.
807-
self.add_definition(place_id, named);
826+
if let Some((enclosing_scope, scope_index)) = self.enclosing_scope_for_walrus() {
827+
// PEP 572: walrus in comprehension binds in enclosing scope.
828+
let target_name = named
829+
.target
830+
.as_name_expr()
831+
.expect("target should be a Name expression")
832+
.id
833+
.clone();
834+
let (symbol_id, added) =
835+
self.place_tables[enclosing_scope].add_symbol(Symbol::new(target_name));
836+
if added {
837+
self.use_def_maps[enclosing_scope].add_place(symbol_id.into());
838+
}
839+
self.push_additional_definition_in_scope(
840+
enclosing_scope,
841+
scope_index,
842+
symbol_id.into(),
843+
named,
844+
);
845+
} else {
846+
self.add_definition(place_id, named);
847+
}
808848
}
809849
Some(CurrentAssignment::Comprehension {
810850
unpack,
@@ -3167,11 +3207,16 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
31673207
}
31683208
}
31693209
ast::Expr::Named(node) => {
3170-
// TODO walrus in comprehensions is implicitly nonlocal
31713210
self.visit_expr(&node.value);
31723211

31733212
// See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements
31743213
if node.target.is_name_expr() {
3214+
// PEP 572: walrus in comprehension binds in the enclosing scope.
3215+
// Make the value a standalone expression so inference can evaluate
3216+
// it in the comprehension scope where the iteration variables are visible.
3217+
if self.enclosing_scope_for_walrus().is_some() {
3218+
self.add_standalone_expression(&node.value);
3219+
}
31753220
self.push_assignment(node.into());
31763221
self.visit_expr(&node.target);
31773222
self.pop_assignment();

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6209,10 +6209,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
62096209
fn infer_named_expression(&mut self, named: &ast::ExprNamed) -> Type<'db> {
62106210
// See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements
62116211
if named.target.is_name_expr() {
6212-
let definition = self.index.expect_single_definition(named);
6213-
let result = infer_definition_types(self.db(), definition);
6214-
self.extend_definition(result);
6215-
result.binding_type(definition)
6212+
let db = self.db();
6213+
6214+
if self.scope().node(db).scope_kind() == ScopeKind::Comprehension {
6215+
// PEP 572: walrus in comprehension binds in the enclosing scope.
6216+
// Infer the value via its standalone expression in this scope;
6217+
// the definition lives in the enclosing scope and will be
6218+
// inferred when that scope needs it.
6219+
let expression = self.index.expression(named.value.as_ref());
6220+
let result = infer_expression_types(db, expression, TypeContext::default());
6221+
self.extend_expression(result);
6222+
result.expression_type(named.value.as_ref())
6223+
} else {
6224+
let definition = self.index.expect_single_definition(named);
6225+
let result = infer_definition_types(db, definition);
6226+
self.extend_definition(result);
6227+
result.binding_type(definition)
6228+
}
62166229
} else {
62176230
// For syntactically invalid targets, we still need to run type inference:
62186231
self.infer_expression(&named.target, TypeContext::default());
@@ -6235,7 +6248,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
62356248

62366249
let add = self.add_binding(named.target.as_ref().into(), definition);
62376250

6238-
let ty = self.infer_expression(value, add.type_context());
6251+
// PEP 572: walrus in a comprehension binds in the enclosing scope, but
6252+
// the value references comprehension-scoped variables. The builder
6253+
// registers the value as a standalone expression in the comprehension
6254+
// scope so we can infer it there. We must not `extend` the result
6255+
// because expression IDs are only meaningful within their own scope.
6256+
let ty = if let Some(expression) = self.index.try_expression(value.as_ref()) {
6257+
let result = infer_expression_types(self.db(), expression, add.type_context());
6258+
result.expression_type(value.as_ref())
6259+
} else {
6260+
self.infer_expression(value, add.type_context())
6261+
};
62396262
self.store_expression_type(target, ty);
62406263
add.insert(self, ty)
62416264
}

0 commit comments

Comments
 (0)