Skip to content

Commit 734d357

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 59f2f9a commit 734d357

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:
@@ -266,14 +267,18 @@ def allow_jump(self, index: int) -> None:
266267
frame.unreachable = True
267268
self.options_on_return[index].append(frame)
268269

269-
def push_loop_frame(self) -> None:
270-
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()
271273

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

275278
@contextmanager
276-
def frame_context(self, *, can_skip: bool, fall_through: int = 1) -> 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]:
277282
"""Return a context manager that pushes/pops frames on enter/exit.
278283
279284
If can_skip is True, control flow is allowed to bypass the
@@ -284,14 +289,43 @@ def frame_context(self, *, can_skip: bool, fall_through: int = 1) -> Iterator[Fr
284289
`fall_through` levels higher. Otherwise control flow ends
285290
at the end of the frame.
286291
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).
301+
287302
After the context manager exits, self.last_pop_changed indicates
288303
whether any types changed in the newly-topmost frame as a result
289304
of popping this frame.
290305
"""
291306
assert len(self.frames) > 1
292-
yield self.push_frame()
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
293320
self.pop_frame(can_skip, fall_through)
294321

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+
295329
@contextmanager
296330
def top_frame_context(self) -> Iterator[Frame]:
297331
"""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
@@ -233,13 +233,12 @@ def accept_loop(self, body: Node, else_body: Node = None, *,
233233
"""
234234
# The outer frame accumulates the results of all iterations
235235
with self.binder.frame_context(can_skip=False):
236-
self.binder.push_loop_frame()
237236
while True:
238-
with self.binder.frame_context(can_skip=True):
237+
with self.binder.frame_context(can_skip=True,
238+
break_frame=2, continue_frame=1):
239239
self.accept(body)
240240
if not self.binder.last_pop_changed:
241241
break
242-
self.binder.pop_loop_frame()
243242
if exit_condition:
244243
_, else_map = self.find_isinstance_check(exit_condition)
245244
self.push_type_map(else_map)
@@ -1639,19 +1638,17 @@ def visit_try_stmt(self, s: TryStmt) -> Type:
16391638
# This one gets all possible states after the try block exited abnormally
16401639
# (by exception, return, break, etc.)
16411640
with self.binder.frame_context(can_skip=False, fall_through=0):
1641+
# Not only might the body of the try statement exit
1642+
# abnormally, but so might an exception handler or else
1643+
# clause. The finally clause runs in *all* cases, so we
1644+
# need an outer try frame to catch all intermediate states
1645+
# in case an exception is raised during an except or else
1646+
# clause. As an optimization, only create the outer try
1647+
# frame when there actually is a finally clause.
1648+
self.visit_try_without_finally(s, try_frame=bool(s.finally_body))
16421649
if s.finally_body:
1643-
# Not only might the body of the try statement exit abnormally,
1644-
# but so might an exception handler or else clause. The finally
1645-
# clause runs in *all* cases, so we need an outer try frame to
1646-
# catch all intermediate states in case an exception is raised
1647-
# during an except or else clause.
1648-
self.binder.try_frames.add(len(self.binder.frames) - 1)
1649-
self.visit_try_without_finally(s)
1650-
self.binder.try_frames.remove(len(self.binder.frames) - 1)
16511650
# First we check finally_body is type safe on all abnormal exit paths
16521651
self.accept(s.finally_body)
1653-
else:
1654-
self.visit_try_without_finally(s)
16551652

16561653
if s.finally_body:
16571654
# Then we try again for the more restricted set of options
@@ -1669,7 +1666,7 @@ def visit_try_stmt(self, s: TryStmt) -> Type:
16691666

16701667
return None
16711668

1672-
def visit_try_without_finally(self, s: TryStmt) -> None:
1669+
def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None:
16731670
"""Type check a try statement, ignoring the finally block.
16741671
16751672
On entry, the top frame should receive all flow that exits the
@@ -1680,15 +1677,12 @@ def visit_try_without_finally(self, s: TryStmt) -> None:
16801677
# This frame will run the else block if the try fell through.
16811678
# In that case, control flow continues to the parent of what
16821679
# was the top frame on entry.
1683-
with self.binder.frame_context(can_skip=False, fall_through=2):
1680+
with self.binder.frame_context(can_skip=False, fall_through=2, try_frame=try_frame):
16841681
# This frame receives exit via exception, and runs exception handlers
16851682
with self.binder.frame_context(can_skip=False, fall_through=2):
16861683
# Finally, the body of the try statement
1687-
with self.binder.frame_context(can_skip=False, fall_through=2):
1688-
self.binder.try_frames.add(len(self.binder.frames) - 2)
1689-
self.binder.allow_jump(-1)
1684+
with self.binder.frame_context(can_skip=False, fall_through=2, try_frame=True):
16901685
self.accept(s.body)
1691-
self.binder.try_frames.remove(len(self.binder.frames) - 2)
16921686
for i in range(len(s.handlers)):
16931687
with self.binder.frame_context(can_skip=True, fall_through=4):
16941688
if s.types[i]:
@@ -2018,13 +2012,11 @@ def visit_member_expr(self, e: MemberExpr) -> Type:
20182012
return self.expr_checker.visit_member_expr(e)
20192013

20202014
def visit_break_stmt(self, s: BreakStmt) -> Type:
2021-
self.binder.allow_jump(self.binder.loop_frames[-1] - 1)
2022-
self.binder.unreachable()
2015+
self.binder.handle_break()
20232016
return None
20242017

20252018
def visit_continue_stmt(self, s: ContinueStmt) -> Type:
2026-
self.binder.allow_jump(self.binder.loop_frames[-1])
2027-
self.binder.unreachable()
2019+
self.binder.handle_continue()
20282020
return None
20292021

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

0 commit comments

Comments
 (0)