Skip to content

Commit 53ad5cf

Browse files
committed
Decouple checker and binder by adding more options to frame_context
No more poking around by the checker in loop_frames and try_frames.
1 parent 831354b commit 53ad5cf

File tree

2 files changed

+56
-30
lines changed

2 files changed

+56
-30
lines changed

mypy/binder.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ def __init__(self) -> None:
7373
self.last_pop_changed = False
7474

7575
self.try_frames = set() # type: Set[int]
76-
self.loop_frames = [] # type: List[int]
76+
self.break_frames = [] # type: List[int]
77+
self.continue_frames = [] # type: List[int]
7778

7879
def _add_dependencies(self, key: Key, value: Key = None) -> None:
7980
if value is None:
@@ -267,14 +268,18 @@ def allow_jump(self, index: int) -> None:
267268
frame.unreachable = True
268269
self.options_on_return[index].append(frame)
269270

270-
def push_loop_frame(self) -> None:
271-
self.loop_frames.append(len(self.frames) - 1)
271+
def handle_break(self) -> None:
272+
self.allow_jump(self.break_frames[-1])
273+
self.unreachable()
272274

273-
def pop_loop_frame(self) -> None:
274-
self.loop_frames.pop()
275+
def handle_continue(self) -> None:
276+
self.allow_jump(self.continue_frames[-1])
277+
self.unreachable()
275278

276279
@contextmanager
277-
def frame_context(self, *, can_skip: bool, fall_through: int = 1) -> Iterator[Frame]:
280+
def frame_context(self, *, can_skip: bool, fall_through: int = 1,
281+
break_frame: int = 0, continue_frame: int = 0,
282+
try_frame: bool = False) -> Iterator[Frame]:
278283
"""Return a context manager that pushes/pops frames on enter/exit.
279284
280285
If can_skip is True, control flow is allowed to bypass the
@@ -285,14 +290,43 @@ def frame_context(self, *, can_skip: bool, fall_through: int = 1) -> Iterator[Fr
285290
`fall_through` levels higher. Otherwise control flow ends
286291
at the end of the frame.
287292
293+
If break_frame > 0, then 'break' statements within this frame
294+
will jump out to the frame break_frame levels higher than the
295+
frame created by this call to frame_context. Similarly for
296+
continue_frame and 'continue' statements.
297+
298+
If try_frame is true, then execution is allowed to jump at any
299+
point within the newly created frame (or its descendents) to
300+
its parent (i.e., to the frame that was on top before this
301+
call to frame_context).
302+
288303
After the context manager exits, self.last_pop_changed indicates
289304
whether any types changed in the newly-topmost frame as a result
290305
of popping this frame.
291306
"""
292307
assert len(self.frames) > 1
293-
yield self.push_frame()
308+
309+
if break_frame:
310+
self.break_frames.append(len(self.frames) - break_frame)
311+
if continue_frame:
312+
self.continue_frames.append(len(self.frames) - continue_frame)
313+
if try_frame:
314+
self.try_frames.add(len(self.frames) - 1)
315+
316+
new_frame = self.push_frame()
317+
if try_frame:
318+
# An exception may occur immediately
319+
self.allow_jump(-1)
320+
yield new_frame
294321
self.pop_frame(can_skip, fall_through)
295322

323+
if break_frame:
324+
self.break_frames.pop()
325+
if continue_frame:
326+
self.continue_frames.pop()
327+
if try_frame:
328+
self.try_frames.remove(len(self.frames) - 1)
329+
296330
@contextmanager
297331
def top_frame_context(self) -> Iterator[Frame]:
298332
"""A variant of frame_context for use at the top level of

mypy/checker.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,12 @@ def accept_loop(self, body: Node, else_body: Node = None, *,
237237
"""
238238
# The outer frame accumulates the results of all iterations
239239
with self.binder.frame_context(can_skip=False):
240-
self.binder.push_loop_frame()
241240
while True:
242-
with self.binder.frame_context(can_skip=True):
241+
with self.binder.frame_context(can_skip=True,
242+
break_frame=2, continue_frame=1):
243243
self.accept(body)
244244
if not self.binder.last_pop_changed:
245245
break
246-
self.binder.pop_loop_frame()
247246
if exit_condition:
248247
_, else_map = self.find_isinstance_check(exit_condition)
249248
self.push_type_map(else_map)
@@ -1544,19 +1543,17 @@ def visit_try_stmt(self, s: TryStmt) -> Type:
15441543
# This one gets all possible states after the try block exited abnormally
15451544
# (by exception, return, break, etc.)
15461545
with self.binder.frame_context(can_skip=False, fall_through=0):
1546+
# Not only might the body of the try statement exit
1547+
# abnormally, but so might an exception handler or else
1548+
# clause. The finally clause runs in *all* cases, so we
1549+
# need an outer try frame to catch all intermediate states
1550+
# in case an exception is raised during an except or else
1551+
# clause. As an optimization, only create the outer try
1552+
# frame when there actually is a finally clause.
1553+
self.visit_try_without_finally(s, try_frame=bool(s.finally_body))
15471554
if s.finally_body:
1548-
# Not only might the body of the try statement exit abnormally,
1549-
# but so might an exception handler or else clause. The finally
1550-
# clause runs in *all* cases, so we need an outer try frame to
1551-
# catch all intermediate states in case an exception is raised
1552-
# during an except or else clause.
1553-
self.binder.try_frames.add(len(self.binder.frames) - 1)
1554-
self.visit_try_without_finally(s)
1555-
self.binder.try_frames.remove(len(self.binder.frames) - 1)
15561555
# First we check finally_body is type safe on all abnormal exit paths
15571556
self.accept(s.finally_body)
1558-
else:
1559-
self.visit_try_without_finally(s)
15601557

15611558
if s.finally_body:
15621559
# Then we try again for the more restricted set of options
@@ -1574,7 +1571,7 @@ def visit_try_stmt(self, s: TryStmt) -> Type:
15741571

15751572
return None
15761573

1577-
def visit_try_without_finally(self, s: TryStmt) -> None:
1574+
def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None:
15781575
"""Type check a try statement, ignoring the finally block.
15791576
15801577
On entry, the top frame should receive all flow that exits the
@@ -1585,15 +1582,12 @@ def visit_try_without_finally(self, s: TryStmt) -> None:
15851582
# This frame will run the else block if the try fell through.
15861583
# In that case, control flow continues to the parent of what
15871584
# was the top frame on entry.
1588-
with self.binder.frame_context(can_skip=False, fall_through=2):
1585+
with self.binder.frame_context(can_skip=False, fall_through=2, try_frame=try_frame):
15891586
# This frame receives exit via exception, and runs exception handlers
15901587
with self.binder.frame_context(can_skip=False, fall_through=2):
15911588
# Finally, the body of the try statement
1592-
with self.binder.frame_context(can_skip=False, fall_through=2):
1593-
self.binder.try_frames.add(len(self.binder.frames) - 2)
1594-
self.binder.allow_jump(-1)
1589+
with self.binder.frame_context(can_skip=False, fall_through=2, try_frame=True):
15951590
self.accept(s.body)
1596-
self.binder.try_frames.remove(len(self.binder.frames) - 2)
15971591
for i in range(len(s.handlers)):
15981592
with self.binder.frame_context(can_skip=True, fall_through=4):
15991593
if s.types[i]:
@@ -1839,13 +1833,11 @@ def visit_member_expr(self, e: MemberExpr) -> Type:
18391833
return self.expr_checker.visit_member_expr(e)
18401834

18411835
def visit_break_stmt(self, s: BreakStmt) -> Type:
1842-
self.binder.allow_jump(self.binder.loop_frames[-1] - 1)
1843-
self.binder.unreachable()
1836+
self.binder.handle_break()
18441837
return None
18451838

18461839
def visit_continue_stmt(self, s: ContinueStmt) -> Type:
1847-
self.binder.allow_jump(self.binder.loop_frames[-1])
1848-
self.binder.unreachable()
1840+
self.binder.handle_continue()
18491841
return None
18501842

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

0 commit comments

Comments
 (0)