| """Helpers for dealing with nonlocal control such as 'break' and 'return'. |
| |
| Model how these behave differently in different contexts. |
| """ |
| |
| from __future__ import annotations |
| |
| from abc import abstractmethod |
| from typing import TYPE_CHECKING |
| |
| from mypyc.ir.class_ir import ClassIR |
| from mypyc.ir.ops import ( |
| ERR_NEVER, |
| NO_TRACEBACK_LINE_NO, |
| Assign, |
| BasicBlock, |
| Branch, |
| Call, |
| Goto, |
| Integer, |
| Register, |
| Return, |
| SetMem, |
| Unreachable, |
| Value, |
| ) |
| from mypyc.ir.rtypes import object_rprimitive |
| from mypyc.primitives.exc_ops import restore_exc_info_op, set_stop_iteration_value |
| |
| 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, line)) |
| |
| |
| 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: |
| # Frame cleanup may clear an attribute holding the return value. Copy it |
| # first so later spill transforms can place any required load here. |
| result = Register(value.type) |
| builder.add(Assign(result, value, line)) |
| |
| gen_generator_func_cleanup(builder, 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 |
| # (???). |
| |
| true, false = BasicBlock(), BasicBlock() |
| stop_iter_reg = builder.fn_info.generator_class.stop_iter_value_reg |
| assert stop_iter_reg is not None |
| |
| builder.add(Branch(stop_iter_reg, true, false, Branch.IS_ERROR)) |
| |
| builder.activate_block(true) |
| # The default/slow path is to raise a StopIteration exception with |
| # return value. |
| builder.call_c(set_stop_iteration_value, [result], NO_TRACEBACK_LINE_NO) |
| builder.add(Unreachable()) |
| builder.builder.pop_error_handler() |
| |
| builder.activate_block(false) |
| # The fast path is to store return value via caller-provided pointer |
| # instead of raising an exception. This can only be used when the |
| # caller is a native function. |
| builder.add(SetMem(object_rprimitive, stop_iter_reg, result)) |
| builder.add(Return(Integer(0, object_rprimitive))) |
| |
| |
| def gen_class_clear_on_completion(builder: IRBuilder, obj: Value, cl: ClassIR, line: int) -> None: |
| call = Call(cl.clear_on_completion, [obj], line) |
| call.error_kind = ERR_NEVER |
| builder.add(call) |
| |
| |
| def gen_generator_func_cleanup(builder: IRBuilder, line: int) -> None: |
| """Clear references held by a completed generator or coroutine.""" |
| cls = builder.fn_info.generator_class |
| |
| # 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(cls.next_label_target, Integer(-1), line) |
| |
| gen_class_clear_on_completion(builder, cls.curr_env_reg, builder.fn_info.env_class, line) |
| if builder.fn_info.env_class is not cls.ir: |
| gen_class_clear_on_completion(builder, cls.self_reg, cls.ir, line) |
| |
| |
| 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: Register | None = 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.assign(self.ret_reg, value, line) |
| |
| 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: Value) -> 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)], 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, saved: Value) -> None: |
| super().__init__(outer) |
| 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) |