Skip to content

Commit b124e18

Browse files
mtshibasharkdpcarljmMichaReiser
authored
[ty] improve lazy scope place lookup (#19321)
Co-authored-by: David Peter <sharkdp@users.noreply.github.com> Co-authored-by: Carl Meyer <carl@oddbird.net> Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent 57373a7 commit b124e18

File tree

15 files changed

+493
-179
lines changed

15 files changed

+493
-179
lines changed

crates/ty_python_semantic/resources/mdtest/del.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def enclosing():
123123
nonlocal x
124124
def bar():
125125
# allowed, refers to `x` in `enclosing`
126-
reveal_type(x) # revealed: Unknown | Literal[2]
126+
reveal_type(x) # revealed: Literal[2]
127127
bar()
128128
del x # allowed, deletes `x` in `enclosing` (though we don't track that)
129129
```

crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,69 @@ def f(x: str | None):
238238

239239
[reveal_type(x) for _ in range(1)] # revealed: str
240240

241+
# When there is a reassignment, any narrowing constraints on the place are invalidated in lazy scopes.
242+
x = None
243+
```
244+
245+
If a variable defined in a private scope is never reassigned, narrowing remains in effect in the
246+
inner lazy scope.
247+
248+
```py
249+
def f(const: str | None):
250+
if const is not None:
251+
def _():
252+
# The `const is not None` narrowing constraint is still valid since `const` has not been reassigned
253+
reveal_type(const) # revealed: str
254+
255+
class C2:
256+
reveal_type(const) # revealed: str
257+
258+
[reveal_type(const) for _ in range(1)] # revealed: str
259+
```
260+
261+
And even if there is an attribute or subscript assignment to the variable, narrowing of the variable
262+
is still valid in the inner lazy scope.
263+
264+
```py
265+
def f(l: list[str | None] | None):
266+
if l is not None:
267+
def _():
268+
reveal_type(l) # revealed: list[str | None]
269+
l[0] = None
270+
271+
def f(a: A):
272+
if a:
273+
def _():
274+
reveal_type(a) # revealed: A & ~AlwaysFalsy
275+
a.x = None
276+
```
277+
278+
Narrowing is invalidated if a `nonlocal` declaration is made within a lazy scope.
279+
280+
```py
281+
def f(non_local: str | None):
282+
if non_local is not None:
283+
def _():
284+
nonlocal non_local
285+
non_local = None
286+
287+
def _():
288+
reveal_type(non_local) # revealed: str | None
289+
290+
def f(non_local: str | None):
291+
def _():
292+
nonlocal non_local
293+
non_local = None
294+
if non_local is not None:
295+
def _():
296+
reveal_type(non_local) # revealed: str | None
297+
```
298+
299+
The same goes for public variables, attributes, and subscripts, because it is difficult to track all
300+
of their changes.
301+
302+
```py
303+
def f():
241304
if g is not None:
242305
def _():
243306
reveal_type(g) # revealed: str | None
@@ -249,6 +312,7 @@ def f(x: str | None):
249312

250313
if a.x is not None:
251314
def _():
315+
# Lazy nested scope narrowing is not performed on attributes/subscripts because it's difficult to track their changes.
252316
reveal_type(a.x) # revealed: Unknown | str | None
253317

254318
class D:
@@ -282,7 +346,7 @@ l: list[str | Literal[1] | None] = [None]
282346

283347
def f(x: str | Literal[1] | None):
284348
class C:
285-
if x is not None:
349+
if x is not None: # TODO: should be an unresolved-reference error
286350
def _():
287351
if x != 1:
288352
reveal_type(x) # revealed: str | None
@@ -293,6 +357,38 @@ def f(x: str | Literal[1] | None):
293357

294358
[reveal_type(x) for _ in range(1) if x != 1] # revealed: str
295359

360+
x = None
361+
362+
def _():
363+
# error: [unresolved-reference]
364+
if x is not None:
365+
def _():
366+
if x != 1:
367+
reveal_type(x) # revealed: Never
368+
x = None
369+
370+
def f(const: str | Literal[1] | None):
371+
class C:
372+
if const is not None:
373+
def _():
374+
if const != 1:
375+
# TODO: should be `str`
376+
reveal_type(const) # revealed: str | None
377+
378+
class D:
379+
if const != 1:
380+
reveal_type(const) # revealed: str
381+
382+
[reveal_type(const) for _ in range(1) if const != 1] # revealed: str
383+
384+
def _():
385+
if const is not None:
386+
def _():
387+
if const != 1:
388+
reveal_type(const) # revealed: str
389+
390+
def f():
391+
class C:
296392
if g is not None:
297393
def _():
298394
if g != 1:

crates/ty_python_semantic/resources/mdtest/public_types.md

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ def outer() -> None:
2020
x = A()
2121

2222
def inner() -> None:
23-
# TODO: We might ideally be able to eliminate `Unknown` from the union here since `x` resolves to an
24-
# outer scope that is a function scope (as opposed to module global scope), and `x` is never declared
25-
# nonlocal in a nested scope that also assigns to it.
26-
reveal_type(x) # revealed: Unknown | A | B
23+
reveal_type(x) # revealed: A | B
2724
# This call would observe `x` as `A`.
2825
inner()
2926

@@ -40,7 +37,7 @@ def outer(flag: bool) -> None:
4037
x = A()
4138

4239
def inner() -> None:
43-
reveal_type(x) # revealed: Unknown | A | B | C
40+
reveal_type(x) # revealed: A | B | C
4441
inner()
4542

4643
if flag:
@@ -62,7 +59,7 @@ def outer() -> None:
6259
x = A()
6360

6461
def inner() -> None:
65-
reveal_type(x) # revealed: Unknown | A | C
62+
reveal_type(x) # revealed: A | C
6663
inner()
6764

6865
if False:
@@ -76,7 +73,7 @@ def outer(flag: bool) -> None:
7673
x = A()
7774

7875
def inner() -> None:
79-
reveal_type(x) # revealed: Unknown | A | C
76+
reveal_type(x) # revealed: A | C
8077
inner()
8178

8279
if flag:
@@ -96,24 +93,18 @@ def outer(flag: bool) -> None:
9693
x = A()
9794

9895
def inner() -> None:
99-
reveal_type(x) # revealed: Unknown | A
96+
reveal_type(x) # revealed: A
10097
inner()
10198
```
10299

103-
In the future, we may try to be smarter about which bindings must or must not be a visible to a
104-
given nested scope, depending where it is defined. In the above case, this shouldn't change the
105-
behavior -- `x` is defined before `inner` in the same branch, so should be considered
106-
definitely-bound for `inner`. But in other cases we may want to emit `possibly-unresolved-reference`
107-
in future:
108-
109100
```py
110101
def outer(flag: bool) -> None:
111102
if flag:
112103
x = A()
113104

114105
def inner() -> None:
115106
# TODO: Ideally, we would emit a possibly-unresolved-reference error here.
116-
reveal_type(x) # revealed: Unknown | A
107+
reveal_type(x) # revealed: A
117108
inner()
118109
```
119110

@@ -126,7 +117,7 @@ def outer() -> None:
126117
x = A()
127118

128119
def inner() -> None:
129-
reveal_type(x) # revealed: Unknown | A
120+
reveal_type(x) # revealed: A
130121
inner()
131122

132123
return
@@ -136,7 +127,7 @@ def outer(flag: bool) -> None:
136127
x = A()
137128

138129
def inner() -> None:
139-
reveal_type(x) # revealed: Unknown | A | B
130+
reveal_type(x) # revealed: A | B
140131
if flag:
141132
x = B()
142133
inner()
@@ -161,7 +152,7 @@ def f0() -> None:
161152
def f2() -> None:
162153
def f3() -> None:
163154
def f4() -> None:
164-
reveal_type(x) # revealed: Unknown | A | B
155+
reveal_type(x) # revealed: A | B
165156
f4()
166157
f3()
167158
f2()
@@ -172,6 +163,29 @@ def f0() -> None:
172163
f1()
173164
```
174165

166+
## Narrowing
167+
168+
In general, it is not safe to narrow the public type of a symbol using constraints introduced in an
169+
outer scope (because the symbol's value may have changed by the time the lazy scope is actually
170+
evaluated), but they can be applied if there is no reassignment of the symbol.
171+
172+
```py
173+
class A: ...
174+
175+
def outer(x: A | None):
176+
if x is not None:
177+
def inner() -> None:
178+
reveal_type(x) # revealed: A | None
179+
inner()
180+
x = None
181+
182+
def outer(x: A | None):
183+
if x is not None:
184+
def inner() -> None:
185+
reveal_type(x) # revealed: A
186+
inner()
187+
```
188+
175189
## At module level
176190

177191
The behavior is the same if the outer scope is the global scope of a module:
@@ -232,32 +246,16 @@ def _():
232246

233247
## Limitations
234248

235-
### Type narrowing
236-
237-
We currently do not further analyze control flow, so we do not support cases where the inner scope
238-
is only executed in a branch where the type of `x` is narrowed:
239-
240-
```py
241-
class A: ...
242-
243-
def outer(x: A | None):
244-
if x is not None:
245-
def inner() -> None:
246-
# TODO: should ideally be `A`
247-
reveal_type(x) # revealed: A | None
248-
inner()
249-
```
250-
251249
### Shadowing
252250

253-
Similarly, since we do not analyze control flow in the outer scope here, we assume that `inner()`
254-
could be called between the two assignments to `x`:
251+
Since we do not analyze control flow in the outer scope here, we assume that `inner()` could be
252+
called between the two assignments to `x`:
255253

256254
```py
257255
def outer() -> None:
258256
def inner() -> None:
259-
# TODO: this should ideally be `Unknown | Literal[1]`, but no other type checker supports this either
260-
reveal_type(x) # revealed: Unknown | None | Literal[1]
257+
# TODO: this should ideally be `Literal[1]`, but no other type checker supports this either
258+
reveal_type(x) # revealed: None | Literal[1]
261259
x = None
262260

263261
# [additional code here]
@@ -279,8 +277,8 @@ def outer() -> None:
279277
x = 1
280278

281279
def inner() -> None:
282-
# TODO: this should be `Unknown | Literal[1]`. Mypy and pyright support this.
283-
reveal_type(x) # revealed: Unknown | None | Literal[1]
280+
# TODO: this should be `Literal[1]`. Mypy and pyright support this.
281+
reveal_type(x) # revealed: None | Literal[1]
284282
inner()
285283
```
286284

@@ -314,8 +312,8 @@ def outer() -> None:
314312
set_x()
315313

316314
def inner() -> None:
317-
# TODO: this should ideally be `Unknown | None | Literal[1]`. Mypy and pyright support this.
318-
reveal_type(x) # revealed: Unknown | None
315+
# TODO: this should ideally be `None | Literal[1]`. Mypy and pyright support this.
316+
reveal_type(x) # revealed: None
319317
inner()
320318
```
321319

crates/ty_python_semantic/resources/mdtest/scopes/eager.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def _():
299299
x = 1
300300

301301
def f():
302-
# revealed: Unknown | Literal[1, 2]
302+
# revealed: Literal[1, 2]
303303
[reveal_type(x) for a in range(1)]
304304
x = 2
305305
```
@@ -316,7 +316,7 @@ def _():
316316

317317
class A:
318318
def f():
319-
# revealed: Unknown | Literal[1, 2]
319+
# revealed: Literal[1, 2]
320320
reveal_type(x)
321321

322322
x = 2
@@ -333,7 +333,7 @@ def _():
333333

334334
def f():
335335
def g():
336-
# revealed: Unknown | Literal[1, 2]
336+
# revealed: Literal[1, 2]
337337
reveal_type(x)
338338
x = 2
339339
```
@@ -351,7 +351,7 @@ def _():
351351

352352
class A:
353353
def f():
354-
# revealed: Unknown | Literal[1, 2]
354+
# revealed: Literal[1, 2]
355355
[reveal_type(x) for a in range(1)]
356356

357357
x = 2

0 commit comments

Comments
 (0)