Skip to content

Commit 02de8f9

Browse files
committed
Replace the binder's breaking_out by an 'unreachable' property of Frame
Also add some comments to the binder.
1 parent a702c40 commit 02de8f9

File tree

3 files changed

+112
-67
lines changed

3 files changed

+112
-67
lines changed

mypy/binder.py

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@
1010

1111

1212
class Frame(Dict[Any, Type]):
13-
pass
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.
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+
"""
23+
24+
def __init__(self) -> None:
25+
self.unreachable = False
1426

1527

1628
class Key(AnyType):
@@ -39,13 +51,19 @@ class A:
3951
"""
4052

4153
def __init__(self) -> None:
42-
# The set of frames currently used. These map
54+
# The stack of frames currently used. These map
4355
# expr.literal_hash -- literals like 'foo.bar' --
44-
# to types.
56+
# to types. The last element of this list is the
57+
# top-most, current frame. Each earlier element
58+
# records the state as of when that frame was last
59+
# on top of the stack.
4560
self.frames = [Frame()]
4661

4762
# For frames higher in the stack, we record the set of
48-
# Frames that can escape there
63+
# Frames that can escape there, either by falling off
64+
# the end of the frame or by a loop control construct
65+
# or raised exception. The last element of self.frames
66+
# has no corresponding element in this list.
4967
self.options_on_return = [] # type: List[List[Frame]]
5068

5169
# Maps expr.literal_hash] to get_declaration(expr)
@@ -55,16 +73,8 @@ def __init__(self) -> None:
5573
# Whenever a new key (e.g. x.a.b) is added, we update this
5674
self.dependencies = {} # type: Dict[Key, Set[Key]]
5775

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-
6476
# Whether the last pop changed the newly top frame on exit
6577
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
6878

6979
self.try_frames = set() # type: Set[int]
7080
self.loop_frames = [] # type: List[int]
@@ -105,9 +115,15 @@ def push(self, expr: Node, typ: Type) -> None:
105115
self._add_dependencies(key)
106116
self._push(key, typ)
107117

118+
def unreachable(self) -> bool:
119+
self.frames[-1].unreachable = True
120+
108121
def get(self, expr: Node) -> Type:
109122
return self._get(expr.literal_hash)
110123

124+
def is_unreachable(self) -> bool:
125+
return self.frames[-1].unreachable
126+
111127
def cleanse(self, expr: Node) -> None:
112128
"""Remove all references to a Node from the binder."""
113129
self._cleanse_key(expr.literal_hash)
@@ -126,13 +142,17 @@ def update_from_options(self, frames: List[Frame]) -> bool:
126142
options are the same.
127143
"""
128144

145+
frames = [f for f in frames if not f.unreachable]
129146
changed = False
130147
keys = set(key for f in frames for key in f)
131148

132149
for key in keys:
133150
current_value = self._get(key)
134151
resulting_values = [f.get(key, current_value) for f in frames]
135152
if any(x is None for x in resulting_values):
153+
# We didn't know anything about key before
154+
# (current_value must be None), and we still don't
155+
# know anything about key in at least one possible frame.
136156
continue
137157

138158
if isinstance(self.declarations.get(key), AnyType):
@@ -147,21 +167,26 @@ def update_from_options(self, frames: List[Frame]) -> bool:
147167
self._push(key, type)
148168
changed = True
149169

170+
self.frames[-1].unreachable = not frames
171+
150172
return changed
151173

152-
def pop_frame(self, fall_through: int = 0) -> Frame:
174+
def pop_frame(self, can_skip: bool, fall_through: int) -> Frame:
153175
"""Pop a frame and return it.
154176
155177
See frame_context() for documentation of fall_through.
156178
"""
157-
if fall_through and not self.breaking_out:
179+
180+
if fall_through > 0:
158181
self.allow_jump(-fall_through)
159182

160183
result = self.frames.pop()
161184
options = self.options_on_return.pop()
162185

186+
if can_skip:
187+
options.insert(0, self.frames[-1])
188+
163189
self.last_pop_changed = self.update_from_options(options)
164-
self.last_pop_breaking_out = self.breaking_out
165190

166191
return result
167192

@@ -240,6 +265,8 @@ def allow_jump(self, index: int) -> None:
240265
frame = Frame()
241266
for f in self.frames[index + 1:]:
242267
frame.update(f)
268+
if f.unreachable:
269+
frame.unreachable = True
243270
self.options_on_return[index].append(frame)
244271

245272
def push_loop_frame(self) -> None:
@@ -249,16 +276,30 @@ def pop_loop_frame(self) -> None:
249276
self.loop_frames.pop()
250277

251278
@contextmanager
252-
def frame_context(self, fall_through: int = 0) -> Iterator[Frame]:
279+
def frame_context(self, *, can_skip: bool, fall_through: int = 1) -> Iterator[Frame]:
253280
"""Return a context manager that pushes/pops frames on enter/exit.
254281
255-
If fall_through > 0, then it will allow the frame to escape to
256-
its ancestor `fall_through` levels higher.
282+
If can_skip is True, control flow is allowed to bypass the
283+
newly-created frame.
257284
258-
A simple 'with binder.frame_context(): pass' will change the
259-
last_pop_* flags but nothing else.
285+
If fall_through > 0, then it will allow control flow that
286+
falls off the end of the frame to escape to its ancestor
287+
`fall_through` levels higher. Otherwise control flow ends
288+
at the end of the frame.
289+
290+
After the context manager exits, self.last_pop_changed indicates
291+
whether any types changed in the newly-topmost frame as a result
292+
of popping this frame.
293+
"""
294+
assert len(self.frames) > 1
295+
yield self.push_frame()
296+
self.pop_frame(can_skip, fall_through)
297+
298+
@contextmanager
299+
def top_frame_context(self) -> Iterator[Frame]:
300+
"""A variant of frame_context for use at the top level of
301+
a namespace (module, function, or class).
260302
"""
261-
was_breaking_out = self.breaking_out
303+
assert len(self.frames) == 1
262304
yield self.push_frame()
263-
self.pop_frame(fall_through)
264-
self.breaking_out = was_breaking_out
305+
self.pop_frame(True, 0)

mypy/checker.py

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,9 @@ def visit_file(self, file_node: MypyFile, path: str) -> None:
165165
# gross, but no other clear way to tell
166166
self.is_typeshed_stub = self.is_stub and 'typeshed' in os.path.normpath(path).split(os.sep)
167167

168-
for d in file_node.defs:
169-
self.accept(d)
168+
with self.binder.top_frame_context():
169+
for d in file_node.defs:
170+
self.accept(d)
170171

171172
self.leave_partial_types()
172173

@@ -231,12 +232,10 @@ def accept_loop(self, body: Node, else_body: Node = None) -> Type:
231232
Then check the else_body.
232233
"""
233234
# The outer frame accumulates the results of all iterations
234-
with self.binder.frame_context(1) as outer_frame:
235+
with self.binder.frame_context(can_skip=False):
235236
self.binder.push_loop_frame()
236237
while True:
237-
with self.binder.frame_context(1):
238-
# We may skip each iteration
239-
self.binder.options_on_return[-1].append(outer_frame)
238+
with self.binder.frame_context(can_skip=True):
240239
self.accept(body)
241240
if not self.binder.last_pop_changed:
242241
break
@@ -418,7 +417,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: str) -> None:
418417
for item, typ in self.expand_typevars(defn, typ):
419418
old_binder = self.binder
420419
self.binder = ConditionalTypeBinder()
421-
with self.binder.frame_context():
420+
with self.binder.top_frame_context():
422421
defn.expanded.append(item)
423422

424423
# We may be checking a function definition or an anonymous
@@ -505,7 +504,7 @@ def is_implicit_any(t: Type) -> bool:
505504
self.accept(init)
506505

507506
# Type check body in a new scope.
508-
with self.binder.frame_context():
507+
with self.binder.top_frame_context():
509508
self.accept(item.body)
510509

511510
self.return_types.pop()
@@ -815,7 +814,7 @@ def visit_class_def(self, defn: ClassDef) -> Type:
815814
self.enter_partial_types()
816815
old_binder = self.binder
817816
self.binder = ConditionalTypeBinder()
818-
with self.binder.frame_context():
817+
with self.binder.top_frame_context():
819818
self.accept(defn.defs)
820819
self.binder = old_binder
821820
self.check_multiple_inheritance(typ)
@@ -911,9 +910,9 @@ def visit_block(self, b: Block) -> Type:
911910
if b.is_unreachable:
912911
return None
913912
for s in b.body:
914-
self.accept(s)
915-
if self.binder.breaking_out:
913+
if self.binder.is_unreachable():
916914
break
915+
self.accept(s)
917916

918917
def visit_assignment_stmt(self, s: AssignmentStmt) -> Type:
919918
"""Type check an assignment statement.
@@ -1354,7 +1353,10 @@ def visit_expression_stmt(self, s: ExpressionStmt) -> Type:
13541353

13551354
def visit_return_stmt(self, s: ReturnStmt) -> Type:
13561355
"""Type check a return statement."""
1357-
self.binder.breaking_out = True
1356+
self.check_return_stmt(s)
1357+
self.binder.unreachable()
1358+
1359+
def check_return_stmt(self, s: ReturnStmt) -> None:
13581360
if self.is_within_function():
13591361
if self.function_stack[-1].is_generator:
13601362
return_type = self.get_generator_return_type(self.return_types[-1])
@@ -1422,9 +1424,9 @@ def count_nested_types(self, typ: Instance, check_type: str) -> int:
14221424

14231425
def visit_if_stmt(self, s: IfStmt) -> Type:
14241426
"""Type check an if statement."""
1425-
breaking_out = True
14261427
# This frame records the knowledge from previous if/elif clauses not being taken.
1427-
with self.binder.frame_context():
1428+
# Fall-through to the original frame is handled explicitly in each block.
1429+
with self.binder.frame_context(can_skip=False, fall_through=0):
14281430
for e, b in zip(s.expr, s.body):
14291431
t = self.accept(e)
14301432
self.check_not_void(t, e)
@@ -1438,13 +1440,12 @@ def visit_if_stmt(self, s: IfStmt) -> Type:
14381440
pass
14391441
else:
14401442
# Only type check body if the if condition can be true.
1441-
with self.binder.frame_context(2):
1443+
with self.binder.frame_context(can_skip=True, fall_through=2):
14421444
if if_map:
14431445
for var, type in if_map.items():
14441446
self.binder.push(var, type)
14451447

14461448
self.accept(b)
1447-
breaking_out = breaking_out and self.binder.last_pop_breaking_out
14481449

14491450
if else_map:
14501451
for var, type in else_map.items():
@@ -1457,12 +1458,9 @@ def visit_if_stmt(self, s: IfStmt) -> Type:
14571458
# print("Warning: isinstance always true")
14581459
break
14591460
else: # Didn't break => can't prove one of the conditions is always true
1460-
with self.binder.frame_context(2):
1461+
with self.binder.frame_context(can_skip=False, fall_through=2):
14611462
if s.else_body:
14621463
self.accept(s.else_body)
1463-
breaking_out = breaking_out and self.binder.last_pop_breaking_out
1464-
if breaking_out:
1465-
self.binder.breaking_out = True
14661464
return None
14671465

14681466
def visit_while_stmt(self, s: WhileStmt) -> Type:
@@ -1498,11 +1496,11 @@ def visit_assert_stmt(self, s: AssertStmt) -> Type:
14981496

14991497
def visit_raise_stmt(self, s: RaiseStmt) -> Type:
15001498
"""Type check a raise statement."""
1501-
self.binder.breaking_out = True
15021499
if s.expr:
15031500
self.type_check_raise(s.expr, s)
15041501
if s.from_expr:
15051502
self.type_check_raise(s.from_expr, s)
1503+
self.binder.unreachable()
15061504

15071505
def type_check_raise(self, e: Node, s: RaiseStmt) -> None:
15081506
typ = self.accept(e)
@@ -1531,44 +1529,52 @@ def type_check_raise(self, e: Node, s: RaiseStmt) -> None:
15311529
def visit_try_stmt(self, s: TryStmt) -> Type:
15321530
"""Type check a try statement."""
15331531
# Our enclosing frame will get the result if the try/except falls through.
1534-
# This one gets all possible intermediate states
1535-
with self.binder.frame_context():
1532+
# This one gets all possible states after the try block exited abnormally
1533+
# (by exception, return, break, etc.)
1534+
with self.binder.frame_context(can_skip=False, fall_through=0):
15361535
if s.finally_body:
15371536
self.binder.try_frames.add(len(self.binder.frames) - 1)
1538-
breaking_out = self.visit_try_without_finally(s)
1537+
self.visit_try_without_finally(s)
15391538
self.binder.try_frames.remove(len(self.binder.frames) - 1)
15401539
# First we check finally_body is type safe for all intermediate frames
15411540
self.accept(s.finally_body)
1542-
breaking_out = breaking_out or self.binder.breaking_out
15431541
else:
1544-
breaking_out = self.visit_try_without_finally(s)
1545-
1546-
if not breaking_out and s.finally_body:
1547-
# Then we try again for the more restricted set of options that can fall through
1542+
self.visit_try_without_finally(s)
1543+
1544+
if s.finally_body:
1545+
# Then we try again for the more restricted set of options
1546+
# that can fall through. (Why do we need to check the
1547+
# finally clause twice? Depending on whether the finally
1548+
# clause was reached by the try clause falling off the end
1549+
# or exiting abnormally, after completing the finally clause
1550+
# either flow will continue to after the entire try statement
1551+
# or the exception/return/etc. will be processed and control
1552+
# flow will escape. We need to check that the finally clause
1553+
# type checks in both contexts, but only the resulting types
1554+
# from the latter context affect the type state in the code
1555+
# that follows the try statement.)
15481556
self.accept(s.finally_body)
1549-
self.binder.breaking_out = breaking_out
1557+
15501558
return None
15511559

1552-
def visit_try_without_finally(self, s: TryStmt) -> bool:
1560+
def visit_try_without_finally(self, s: TryStmt) -> None:
15531561
"""Type check a try statement, ignoring the finally block.
15541562
1555-
Return whether we are guaranteed to be breaking out.
15561563
Otherwise, it will place the results possible frames of
15571564
that don't break out into self.binder.frames[-2].
15581565
"""
1559-
breaking_out = True
15601566
# This frame records the possible states that exceptions can leave variables in
15611567
# during the try: block
1562-
with self.binder.frame_context():
1563-
with self.binder.frame_context(3):
1568+
with self.binder.frame_context(can_skip=False, fall_through=0):
1569+
with self.binder.frame_context(can_skip=False, fall_through=3):
15641570
self.binder.try_frames.add(len(self.binder.frames) - 2)
1571+
self.binder.allow_jump(-1)
15651572
self.accept(s.body)
15661573
self.binder.try_frames.remove(len(self.binder.frames) - 2)
15671574
if s.else_body:
15681575
self.accept(s.else_body)
1569-
breaking_out = breaking_out and self.binder.last_pop_breaking_out
15701576
for i in range(len(s.handlers)):
1571-
with self.binder.frame_context(3):
1577+
with self.binder.frame_context(can_skip=True, fall_through=3):
15721578
if s.types[i]:
15731579
t = self.visit_except_handler_test(s.types[i])
15741580
if s.vars[i]:
@@ -1592,8 +1598,6 @@ def visit_try_without_finally(self, s: TryStmt) -> bool:
15921598
var = cast(Var, s.vars[i].node)
15931599
var.type = DeletedType(source=source)
15941600
self.binder.cleanse(s.vars[i])
1595-
breaking_out = breaking_out and self.binder.last_pop_breaking_out
1596-
return breaking_out
15971601

15981602
def visit_except_handler_test(self, n: Node) -> Type:
15991603
"""Type check an exception handler test clause."""
@@ -1811,13 +1815,13 @@ def visit_member_expr(self, e: MemberExpr) -> Type:
18111815
return self.expr_checker.visit_member_expr(e)
18121816

18131817
def visit_break_stmt(self, s: BreakStmt) -> Type:
1814-
self.binder.breaking_out = True
18151818
self.binder.allow_jump(self.binder.loop_frames[-1] - 1)
1819+
self.binder.unreachable()
18161820
return None
18171821

18181822
def visit_continue_stmt(self, s: ContinueStmt) -> Type:
1819-
self.binder.breaking_out = True
18201823
self.binder.allow_jump(self.binder.loop_frames[-1])
1824+
self.binder.unreachable()
18211825
return None
18221826

18231827
def visit_int_expr(self, e: IntExpr) -> Type:

0 commit comments

Comments
 (0)