1
- from typing import (Any , Dict , List , Set , Iterator , Union )
1
+ from typing import (Dict , List , Set , Iterator , Union )
2
2
from contextlib import contextmanager
3
3
4
4
from mypy .types import Type , AnyType , PartialType
5
- from mypy .nodes import (Node , Expression , Var , RefExpr , SymbolTableNode )
5
+ from mypy .nodes import (Key , Node , Expression , Var , RefExpr , SymbolTableNode )
6
6
7
7
from mypy .subtypes import is_subtype
8
8
from mypy .join import join_simple
9
9
from mypy .sametypes import is_same_type
10
10
11
11
12
- class Frame (Dict [Any , Type ]):
13
- pass
12
+ class Frame (Dict [Key , Type ]):
13
+ """A Frame represents a specific point in the execution of a program.
14
+ It carries information about the current types of expressions at
15
+ that point, arising either from assignments to those expressions
16
+ or the result of isinstance checks. It also records whether it is
17
+ possible to reach that point at all.
14
18
19
+ This information is not copied into a new Frame when it is pushed
20
+ onto the stack, so a given Frame only has information about types
21
+ that were assigned in that frame.
22
+ """
15
23
16
- class Key ( AnyType ) :
17
- pass
24
+ def __init__ ( self ) -> None :
25
+ self . unreachable = False
18
26
19
27
20
28
class ConditionalTypeBinder :
@@ -39,13 +47,19 @@ class A:
39
47
"""
40
48
41
49
def __init__ (self ) -> None :
42
- # The set of frames currently used. These map
50
+ # The stack of frames currently used. These map
43
51
# expr.literal_hash -- literals like 'foo.bar' --
44
- # to types.
52
+ # to types. The last element of this list is the
53
+ # top-most, current frame. Each earlier element
54
+ # records the state as of when that frame was last
55
+ # on top of the stack.
45
56
self .frames = [Frame ()]
46
57
47
58
# For frames higher in the stack, we record the set of
48
- # Frames that can escape there
59
+ # Frames that can escape there, either by falling off
60
+ # the end of the frame or by a loop control construct
61
+ # or raised exception. The last element of self.frames
62
+ # has no corresponding element in this list.
49
63
self .options_on_return = [] # type: List[List[Frame]]
50
64
51
65
# Maps expr.literal_hash] to get_declaration(expr)
@@ -55,27 +69,20 @@ def __init__(self) -> None:
55
69
# Whenever a new key (e.g. x.a.b) is added, we update this
56
70
self .dependencies = {} # type: Dict[Key, Set[Key]]
57
71
58
- # breaking_out is set to True on return/break/continue/raise
59
- # It is cleared on pop_frame() and placed in last_pop_breaking_out
60
- # Lines of code after breaking_out = True are unreachable and not
61
- # typechecked.
62
- self .breaking_out = False
63
-
64
72
# Whether the last pop changed the newly top frame on exit
65
73
self .last_pop_changed = False
66
- # Whether the last pop was necessarily breaking out, and couldn't fall through
67
- self .last_pop_breaking_out = False
68
74
69
75
self .try_frames = set () # type: Set[int]
70
- self .loop_frames = [] # type: List[int]
76
+ self .break_frames = [] # type: List[int]
77
+ self .continue_frames = [] # type: List[int]
71
78
72
79
def _add_dependencies (self , key : Key , value : Key = None ) -> None :
73
80
if value is None :
74
81
value = key
75
82
else :
76
83
self .dependencies .setdefault (key , set ()).add (value )
77
- if isinstance ( key , tuple ) :
78
- for elt in key :
84
+ for elt in key :
85
+ if isinstance ( elt , Key ) :
79
86
self ._add_dependencies (elt , value )
80
87
81
88
def push_frame (self ) -> Frame :
@@ -105,9 +112,17 @@ def push(self, node: Node, typ: Type) -> None:
105
112
self ._add_dependencies (key )
106
113
self ._push (key , typ )
107
114
115
+ def unreachable (self ) -> None :
116
+ self .frames [- 1 ].unreachable = True
117
+
108
118
def get (self , expr : Union [Expression , Var ]) -> Type :
109
119
return self ._get (expr .literal_hash )
110
120
121
+ def is_unreachable (self ) -> bool :
122
+ # TODO: Copy the value of unreachable into new frames to avoid
123
+ # this traversal on every statement?
124
+ return any (f .unreachable for f in self .frames )
125
+
111
126
def cleanse (self , expr : Expression ) -> None :
112
127
"""Remove all references to a Node from the binder."""
113
128
self ._cleanse_key (expr .literal_hash )
@@ -126,13 +141,17 @@ def update_from_options(self, frames: List[Frame]) -> bool:
126
141
options are the same.
127
142
"""
128
143
144
+ frames = [f for f in frames if not f .unreachable ]
129
145
changed = False
130
146
keys = set (key for f in frames for key in f )
131
147
132
148
for key in keys :
133
149
current_value = self ._get (key )
134
150
resulting_values = [f .get (key , current_value ) for f in frames ]
135
151
if any (x is None for x in resulting_values ):
152
+ # We didn't know anything about key before
153
+ # (current_value must be None), and we still don't
154
+ # know anything about key in at least one possible frame.
136
155
continue
137
156
138
157
if isinstance (self .declarations .get (key ), AnyType ):
@@ -147,27 +166,32 @@ def update_from_options(self, frames: List[Frame]) -> bool:
147
166
self ._push (key , type )
148
167
changed = True
149
168
169
+ self .frames [- 1 ].unreachable = not frames
170
+
150
171
return changed
151
172
152
- def pop_frame (self , fall_through : int = 0 ) -> Frame :
173
+ def pop_frame (self , can_skip : bool , fall_through : int ) -> Frame :
153
174
"""Pop a frame and return it.
154
175
155
176
See frame_context() for documentation of fall_through.
156
177
"""
157
- if fall_through and not self .breaking_out :
178
+
179
+ if fall_through > 0 :
158
180
self .allow_jump (- fall_through )
159
181
160
182
result = self .frames .pop ()
161
183
options = self .options_on_return .pop ()
162
184
185
+ if can_skip :
186
+ options .insert (0 , self .frames [- 1 ])
187
+
163
188
self .last_pop_changed = self .update_from_options (options )
164
- self .last_pop_breaking_out = self .breaking_out
165
189
166
190
return result
167
191
168
- def get_declaration (self , node : Node ) -> Type :
169
- if isinstance (node , ( RefExpr , SymbolTableNode )) and isinstance (node .node , Var ):
170
- type = node .node .type
192
+ def get_declaration (self , expr : Node ) -> Type :
193
+ if isinstance (expr , RefExpr ) and isinstance (expr .node , Var ):
194
+ type = expr .node .type
171
195
if isinstance (type , PartialType ):
172
196
return None
173
197
return type
@@ -239,25 +263,74 @@ def allow_jump(self, index: int) -> None:
239
263
frame = Frame ()
240
264
for f in self .frames [index + 1 :]:
241
265
frame .update (f )
266
+ if f .unreachable :
267
+ frame .unreachable = True
242
268
self .options_on_return [index ].append (frame )
243
269
244
- def push_loop_frame (self ) -> None :
245
- self .loop_frames .append (len (self .frames ) - 1 )
270
+ def handle_break (self ) -> None :
271
+ self .allow_jump (self .break_frames [- 1 ])
272
+ self .unreachable ()
246
273
247
- def pop_loop_frame (self ) -> None :
248
- self .loop_frames .pop ()
274
+ def handle_continue (self ) -> None :
275
+ self .allow_jump (self .continue_frames [- 1 ])
276
+ self .unreachable ()
249
277
250
278
@contextmanager
251
- def frame_context (self , fall_through : int = 0 ) -> Iterator [Frame ]:
279
+ def frame_context (self , * , can_skip : bool , fall_through : int = 1 ,
280
+ break_frame : int = 0 , continue_frame : int = 0 ,
281
+ try_frame : bool = False ) -> Iterator [Frame ]:
252
282
"""Return a context manager that pushes/pops frames on enter/exit.
253
283
254
- If fall_through > 0, then it will allow the frame to escape to
255
- its ancestor `fall_through` levels higher.
284
+ If can_skip is True, control flow is allowed to bypass the
285
+ newly-created frame.
286
+
287
+ If fall_through > 0, then it will allow control flow that
288
+ falls off the end of the frame to escape to its ancestor
289
+ `fall_through` levels higher. Otherwise control flow ends
290
+ at the end of the frame.
291
+
292
+ If break_frame > 0, then 'break' statements within this frame
293
+ will jump out to the frame break_frame levels higher than the
294
+ frame created by this call to frame_context. Similarly for
295
+ continue_frame and 'continue' statements.
296
+
297
+ If try_frame is true, then execution is allowed to jump at any
298
+ point within the newly created frame (or its descendents) to
299
+ its parent (i.e., to the frame that was on top before this
300
+ call to frame_context).
256
301
257
- A simple 'with binder.frame_context(): pass' will change the
258
- last_pop_* flags but nothing else.
302
+ After the context manager exits, self.last_pop_changed indicates
303
+ whether any types changed in the newly-topmost frame as a result
304
+ of popping this frame.
305
+ """
306
+ assert len (self .frames ) > 1
307
+
308
+ if break_frame :
309
+ self .break_frames .append (len (self .frames ) - break_frame )
310
+ if continue_frame :
311
+ self .continue_frames .append (len (self .frames ) - continue_frame )
312
+ if try_frame :
313
+ self .try_frames .add (len (self .frames ) - 1 )
314
+
315
+ new_frame = self .push_frame ()
316
+ if try_frame :
317
+ # An exception may occur immediately
318
+ self .allow_jump (- 1 )
319
+ yield new_frame
320
+ self .pop_frame (can_skip , fall_through )
321
+
322
+ if break_frame :
323
+ self .break_frames .pop ()
324
+ if continue_frame :
325
+ self .continue_frames .pop ()
326
+ if try_frame :
327
+ self .try_frames .remove (len (self .frames ) - 1 )
328
+
329
+ @contextmanager
330
+ def top_frame_context (self ) -> Iterator [Frame ]:
331
+ """A variant of frame_context for use at the top level of
332
+ a namespace (module, function, or class).
259
333
"""
260
- was_breaking_out = self .breaking_out
334
+ assert len ( self .frames ) == 1
261
335
yield self .push_frame ()
262
- self .pop_frame (fall_through )
263
- self .breaking_out = was_breaking_out
336
+ self .pop_frame (True , 0 )
0 commit comments