"""Helpers for dealing with nonlocal control such as 'break' and 'return'. Model how these behave differently in different contexts. """ from abc import abstractmethod from typing import Optional, Union from typing_extensions import TYPE_CHECKING from mypyc.ir.ops import ( Branch, BasicBlock, Unreachable, Value, Goto, Integer, Assign, Register, Return, NO_TRACEBACK_LINE_NO ) from mypyc.primitives.exc_ops import set_stop_iteration_value, restore_exc_info_op from mypyc.irbuild.targets import AssignmentTarget if TYPE_CHECKING: from mypyc.irbuild.builder import IRBuilder class NonlocalControl: """ABC representing a stack frame of constructs that modify nonlocal control flow. The nonlocal control flow constructs are break, continue, and return, and their behavior is modified by a number of other constructs. The most obvious is loop, which override where break and continue jump to, but also `except` (which needs to clear exc_info when left) and (eventually) finally blocks (which need to ensure that the finally block is always executed when leaving the try/except blocks). """ @abstractmethod def gen_break(self, builder: 'IRBuilder', line: int) -> None: pass @abstractmethod def gen_continue(self, builder: 'IRBuilder', line: int) -> None: pass @abstractmethod def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None: pass class BaseNonlocalControl(NonlocalControl): """Default nonlocal control outside any statements that affect it.""" def gen_break(self, builder: 'IRBuilder', line: int) -> None: assert False, "break outside of loop" def gen_continue(self, builder: 'IRBuilder', line: int) -> None: assert False, "continue outside of loop" def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None: builder.add(Return(value)) class LoopNonlocalControl(NonlocalControl): """Nonlocal control within a loop.""" def __init__(self, outer: NonlocalControl, continue_block: BasicBlock, break_block: BasicBlock) -> None: self.outer = outer self.continue_block = continue_block self.break_block = break_block def gen_break(self, builder: 'IRBuilder', line: int) -> None: builder.add(Goto(self.break_block)) def gen_continue(self, builder: 'IRBuilder', line: int) -> None: builder.add(Goto(self.continue_block)) def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None: self.outer.gen_return(builder, value, line) class GeneratorNonlocalControl(BaseNonlocalControl): """Default nonlocal control in a generator function outside statements.""" def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None: # Assign an invalid next label number so that the next time # __next__ is called, we jump to the case in which # StopIteration is raised. builder.assign(builder.fn_info.generator_class.next_label_target, Integer(-1), line) # Raise a StopIteration containing a field for the value that # should be returned. Before doing so, create a new block # without an error handler set so that the implicitly thrown # StopIteration isn't caught by except blocks inside of the # generator function. builder.builder.push_error_handler(None) builder.goto_and_activate(BasicBlock()) # Skip creating a traceback frame when we raise here, because # we don't care about the traceback frame and it is kind of # expensive since raising StopIteration is an extremely common # case. Also we call a special internal function to set # StopIteration instead of using RaiseStandardError because # the obvious thing doesn't work if the value is a tuple # (???). builder.call_c(set_stop_iteration_value, [value], NO_TRACEBACK_LINE_NO) builder.add(Unreachable()) builder.builder.pop_error_handler() class CleanupNonlocalControl(NonlocalControl): """Abstract nonlocal control that runs some cleanup code. """ def __init__(self, outer: NonlocalControl) -> None: self.outer = outer @abstractmethod def gen_cleanup(self, builder: 'IRBuilder', line: int) -> None: ... def gen_break(self, builder: 'IRBuilder', line: int) -> None: self.gen_cleanup(builder, line) self.outer.gen_break(builder, line) def gen_continue(self, builder: 'IRBuilder', line: int) -> None: self.gen_cleanup(builder, line) self.outer.gen_continue(builder, line) def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None: self.gen_cleanup(builder, line) self.outer.gen_return(builder, value, line) class TryFinallyNonlocalControl(NonlocalControl): """Nonlocal control within try/finally.""" def __init__(self, target: BasicBlock) -> None: self.target = target self.ret_reg: Optional[Register] = None def gen_break(self, builder: 'IRBuilder', line: int) -> None: builder.error("break inside try/finally block is unimplemented", line) def gen_continue(self, builder: 'IRBuilder', line: int) -> None: builder.error("continue inside try/finally block is unimplemented", line) def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None: if self.ret_reg is None: self.ret_reg = Register(builder.ret_types[-1]) builder.add(Assign(self.ret_reg, value)) builder.add(Goto(self.target)) class ExceptNonlocalControl(CleanupNonlocalControl): """Nonlocal control for except blocks. Just makes sure that sys.exc_info always gets restored when we leave. This is super annoying. """ def __init__(self, outer: NonlocalControl, saved: Union[Value, AssignmentTarget]) -> None: super().__init__(outer) self.saved = saved def gen_cleanup(self, builder: 'IRBuilder', line: int) -> None: builder.call_c(restore_exc_info_op, [builder.read(self.saved)], line) class FinallyNonlocalControl(CleanupNonlocalControl): """Nonlocal control for finally blocks. Just makes sure that sys.exc_info always gets restored when we leave and the return register is decrefed if it isn't null. """ def __init__(self, outer: NonlocalControl, ret_reg: Optional[Value], saved: Value) -> None: super().__init__(outer) self.ret_reg = ret_reg self.saved = saved def gen_cleanup(self, builder: 'IRBuilder', line: int) -> None: # Restore the old exc_info target, cleanup = BasicBlock(), BasicBlock() builder.add(Branch(self.saved, target, cleanup, Branch.IS_ERROR)) builder.activate_block(cleanup) builder.call_c(restore_exc_info_op, [self.saved], line) builder.goto_and_activate(target)