From 14ac8af8f4b6473541427beabb9c2ce4337eca68 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 22 Feb 2020 20:18:10 +0000 Subject: [PATCH] [mypyc] Refactor methods into top-level functions in mypyc.genstatement (#8432) Also experiment with formatting long import statements with one name per line. Work on mypyc/mypyc#714. --- mypyc/genfunc.py | 6 +- mypyc/genopsvisitor.py | 48 +- mypyc/genstatement.py | 1009 +++++++++++++++++++++------------------- 3 files changed, 559 insertions(+), 504 deletions(-) diff --git a/mypyc/genfunc.py b/mypyc/genfunc.py index 3ed76382a0aa..58cca370e364 100644 --- a/mypyc/genfunc.py +++ b/mypyc/genfunc.py @@ -29,7 +29,7 @@ from mypyc.sametype import is_same_method_signature from mypyc.genopsutil import concrete_arg_kind, is_constant, add_self_to_env from mypyc.genopscontext import FuncInfo, GeneratorClass, ImplicitClass -from mypyc.genstatement import BuildStatementIR +from mypyc.genstatement import transform_try_except from mypyc.genops import IRBuilder @@ -852,8 +852,8 @@ def else_body() -> None: self.builder.nonlocal_control[-1].gen_break(self.builder, o.line) self.builder.push_loop_stack(loop_block, done_block) - BuildStatementIR(self.builder).visit_try_except( - try_body, [(None, None, except_body)], else_body, o.line + transform_try_except( + self.builder, try_body, [(None, None, except_body)], else_body, o.line ) self.builder.pop_loop_stack() diff --git a/mypyc/genopsvisitor.py b/mypyc/genopsvisitor.py index 275c7c40ebb0..4735b55350ea 100644 --- a/mypyc/genopsvisitor.py +++ b/mypyc/genopsvisitor.py @@ -23,7 +23,23 @@ from mypyc.genops import IRVisitor, IRBuilder, UnsupportedException from mypyc.genclass import BuildClassIR from mypyc.genfunc import BuildFuncIR -from mypyc.genstatement import BuildStatementIR +from mypyc.genstatement import ( + transform_block, + transform_expression_stmt, + transform_return_stmt, + transform_assignment_stmt, + transform_operator_assignment_stmt, + transform_if_stmt, + transform_while_stmt, + transform_for_stmt, + transform_break_stmt, + transform_continue_stmt, + transform_raise_stmt, + transform_try_stmt, + transform_with_stmt, + transform_assert_stmt, + transform_del_stmt, +) from mypyc.genexpr import BuildExpressionIR @@ -69,54 +85,54 @@ def visit_decorator(self, dec: Decorator) -> None: BuildFuncIR(self.builder).visit_decorator(dec) def visit_block(self, block: Block) -> None: - BuildStatementIR(self.builder).visit_block(block) + transform_block(self.builder, block) # Statements def visit_expression_stmt(self, stmt: ExpressionStmt) -> None: - BuildStatementIR(self.builder).visit_expression_stmt(stmt) + transform_expression_stmt(self.builder, stmt) def visit_return_stmt(self, stmt: ReturnStmt) -> None: - BuildStatementIR(self.builder).visit_return_stmt(stmt) + transform_return_stmt(self.builder, stmt) def visit_assignment_stmt(self, stmt: AssignmentStmt) -> None: - BuildStatementIR(self.builder).visit_assignment_stmt(stmt) + transform_assignment_stmt(self.builder, stmt) def visit_operator_assignment_stmt(self, stmt: OperatorAssignmentStmt) -> None: - BuildStatementIR(self.builder).visit_operator_assignment_stmt(stmt) + transform_operator_assignment_stmt(self.builder, stmt) def visit_if_stmt(self, stmt: IfStmt) -> None: - BuildStatementIR(self.builder).visit_if_stmt(stmt) + transform_if_stmt(self.builder, stmt) def visit_while_stmt(self, stmt: WhileStmt) -> None: - BuildStatementIR(self.builder).visit_while_stmt(stmt) + transform_while_stmt(self.builder, stmt) def visit_for_stmt(self, stmt: ForStmt) -> None: - BuildStatementIR(self.builder).visit_for_stmt(stmt) + transform_for_stmt(self.builder, stmt) def visit_break_stmt(self, stmt: BreakStmt) -> None: - BuildStatementIR(self.builder).visit_break_stmt(stmt) + transform_break_stmt(self.builder, stmt) def visit_continue_stmt(self, stmt: ContinueStmt) -> None: - BuildStatementIR(self.builder).visit_continue_stmt(stmt) + transform_continue_stmt(self.builder, stmt) def visit_raise_stmt(self, stmt: RaiseStmt) -> None: - BuildStatementIR(self.builder).visit_raise_stmt(stmt) + transform_raise_stmt(self.builder, stmt) def visit_try_stmt(self, stmt: TryStmt) -> None: - BuildStatementIR(self.builder).visit_try_stmt(stmt) + transform_try_stmt(self.builder, stmt) def visit_with_stmt(self, stmt: WithStmt) -> None: - BuildStatementIR(self.builder).visit_with_stmt(stmt) + transform_with_stmt(self.builder, stmt) def visit_pass_stmt(self, stmt: PassStmt) -> None: pass def visit_assert_stmt(self, stmt: AssertStmt) -> None: - BuildStatementIR(self.builder).visit_assert_stmt(stmt) + transform_assert_stmt(self.builder, stmt) def visit_del_stmt(self, stmt: DelStmt) -> None: - BuildStatementIR(self.builder).visit_del_stmt(stmt) + transform_del_stmt(self.builder, stmt) def visit_global_decl(self, stmt: GlobalDecl) -> None: # Pure declaration -- no runtime effect diff --git a/mypyc/genstatement.py b/mypyc/genstatement.py index c74d67d9a991..ced764fbbe5d 100644 --- a/mypyc/genstatement.py +++ b/mypyc/genstatement.py @@ -24,499 +24,538 @@ GenFunc = Callable[[], None] -class BuildStatementIR: - def __init__(self, builder: IRBuilder) -> None: - self.builder = builder - - def visit_block(self, block: Block) -> None: - if not block.is_unreachable: - for stmt in block.body: - self.builder.accept(stmt) - # Raise a RuntimeError if we hit a non-empty unreachable block. - # Don't complain about empty unreachable blocks, since mypy inserts - # those after `if MYPY`. - elif block.body: - self.builder.add(RaiseStandardError(RaiseStandardError.RUNTIME_ERROR, - 'Reached allegedly unreachable code!', - block.line)) - self.builder.add(Unreachable()) - - def visit_expression_stmt(self, stmt: ExpressionStmt) -> None: - if isinstance(stmt.expr, StrExpr): - # Docstring. Ignore - return - # ExpressionStmts do not need to be coerced like other Expressions. - stmt.expr.accept(self.builder.visitor) - - def visit_return_stmt(self, stmt: ReturnStmt) -> None: - if stmt.expr: - retval = self.builder.accept(stmt.expr) - else: - retval = self.builder.builder.none() - retval = self.builder.coerce(retval, self.builder.ret_types[-1], stmt.line) - self.builder.nonlocal_control[-1].gen_return(self.builder, retval, stmt.line) - - def visit_assignment_stmt(self, stmt: AssignmentStmt) -> None: - assert len(stmt.lvalues) >= 1 - self.builder.disallow_class_assignments(stmt.lvalues, stmt.line) - lvalue = stmt.lvalues[0] - if stmt.type and isinstance(stmt.rvalue, TempNode): - # This is actually a variable annotation without initializer. Don't generate - # an assignment but we need to call get_assignment_target since it adds a - # name binding as a side effect. - self.builder.get_assignment_target(lvalue, stmt.line) - return - - line = stmt.rvalue.line - rvalue_reg = self.builder.accept(stmt.rvalue) - if self.builder.non_function_scope() and stmt.is_final_def: - self.builder.init_final_static(lvalue, rvalue_reg) - for lvalue in stmt.lvalues: - target = self.builder.get_assignment_target(lvalue) - self.builder.assign(target, rvalue_reg, line) - - def visit_operator_assignment_stmt(self, stmt: OperatorAssignmentStmt) -> None: - """Operator assignment statement such as x += 1""" - self.builder.disallow_class_assignments([stmt.lvalue], stmt.line) - target = self.builder.get_assignment_target(stmt.lvalue) - target_value = self.builder.read(target, stmt.line) - rreg = self.builder.accept(stmt.rvalue) - # the Python parser strips the '=' from operator assignment statements, so re-add it - op = stmt.op + '=' - res = self.builder.binary_op(target_value, rreg, op, stmt.line) - # usually operator assignments are done in-place - # but when target doesn't support that we need to manually assign - self.builder.assign(target, res, res.line) - - def visit_if_stmt(self, stmt: IfStmt) -> None: - if_body, next = BasicBlock(), BasicBlock() - else_body = BasicBlock() if stmt.else_body else next - - # If statements are normalized - assert len(stmt.expr) == 1 - - self.builder.process_conditional(stmt.expr[0], if_body, else_body) - self.builder.activate_block(if_body) - self.builder.accept(stmt.body[0]) - self.builder.goto(next) - if stmt.else_body: - self.builder.activate_block(else_body) - self.builder.accept(stmt.else_body) - self.builder.goto(next) - self.builder.activate_block(next) - - def visit_while_stmt(self, s: WhileStmt) -> None: - body, next, top, else_block = BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock() - normal_loop_exit = else_block if s.else_body is not None else next - - self.builder.push_loop_stack(top, next) - - # Split block so that we get a handle to the top of the loop. - self.builder.goto_and_activate(top) - self.builder.process_conditional(s.expr, body, normal_loop_exit) - - self.builder.activate_block(body) - self.builder.accept(s.body) - # Add branch to the top at the end of the body. - self.builder.goto(top) - - self.builder.pop_loop_stack() - - if s.else_body is not None: - self.builder.activate_block(else_block) - self.builder.accept(s.else_body) - self.builder.goto(next) - - self.builder.activate_block(next) - - def visit_for_stmt(self, s: ForStmt) -> None: - def body() -> None: - self.builder.accept(s.body) - - def else_block() -> None: - assert s.else_body is not None - self.builder.accept(s.else_body) - - self.builder.for_loop_helper(s.index, s.expr, body, - else_block if s.else_body else None, s.line) - - def visit_break_stmt(self, node: BreakStmt) -> None: - self.builder.nonlocal_control[-1].gen_break(self.builder, node.line) - - def visit_continue_stmt(self, node: ContinueStmt) -> None: - self.builder.nonlocal_control[-1].gen_continue(self.builder, node.line) - - def visit_raise_stmt(self, s: RaiseStmt) -> None: - if s.expr is None: - self.builder.primitive_op(reraise_exception_op, [], NO_TRACEBACK_LINE_NO) - self.builder.add(Unreachable()) - return - - exc = self.builder.accept(s.expr) - self.builder.primitive_op(raise_exception_op, [exc], s.line) - self.builder.add(Unreachable()) - - def visit_try_except(self, +def transform_block(builder: IRBuilder, block: Block) -> None: + if not block.is_unreachable: + for stmt in block.body: + builder.accept(stmt) + # Raise a RuntimeError if we hit a non-empty unreachable block. + # Don't complain about empty unreachable blocks, since mypy inserts + # those after `if MYPY`. + elif block.body: + builder.add(RaiseStandardError(RaiseStandardError.RUNTIME_ERROR, + 'Reached allegedly unreachable code!', + block.line)) + builder.add(Unreachable()) + + +def transform_expression_stmt(builder: IRBuilder, stmt: ExpressionStmt) -> None: + if isinstance(stmt.expr, StrExpr): + # Docstring. Ignore + return + # ExpressionStmts do not need to be coerced like other Expressions. + stmt.expr.accept(builder.visitor) + + +def transform_return_stmt(builder: IRBuilder, stmt: ReturnStmt) -> None: + if stmt.expr: + retval = builder.accept(stmt.expr) + else: + retval = builder.builder.none() + retval = builder.coerce(retval, builder.ret_types[-1], stmt.line) + builder.nonlocal_control[-1].gen_return(builder, retval, stmt.line) + + +def transform_assignment_stmt(builder: IRBuilder, stmt: AssignmentStmt) -> None: + assert len(stmt.lvalues) >= 1 + builder.disallow_class_assignments(stmt.lvalues, stmt.line) + lvalue = stmt.lvalues[0] + if stmt.type and isinstance(stmt.rvalue, TempNode): + # This is actually a variable annotation without initializer. Don't generate + # an assignment but we need to call get_assignment_target since it adds a + # name binding as a side effect. + builder.get_assignment_target(lvalue, stmt.line) + return + + line = stmt.rvalue.line + rvalue_reg = builder.accept(stmt.rvalue) + if builder.non_function_scope() and stmt.is_final_def: + builder.init_final_static(lvalue, rvalue_reg) + for lvalue in stmt.lvalues: + target = builder.get_assignment_target(lvalue) + builder.assign(target, rvalue_reg, line) + + +def transform_operator_assignment_stmt(builder: IRBuilder, stmt: OperatorAssignmentStmt) -> None: + """Operator assignment statement such as x += 1""" + builder.disallow_class_assignments([stmt.lvalue], stmt.line) + target = builder.get_assignment_target(stmt.lvalue) + target_value = builder.read(target, stmt.line) + rreg = builder.accept(stmt.rvalue) + # the Python parser strips the '=' from operator assignment statements, so re-add it + op = stmt.op + '=' + res = builder.binary_op(target_value, rreg, op, stmt.line) + # usually operator assignments are done in-place + # but when target doesn't support that we need to manually assign + builder.assign(target, res, res.line) + + +def transform_if_stmt(builder: IRBuilder, stmt: IfStmt) -> None: + if_body, next = BasicBlock(), BasicBlock() + else_body = BasicBlock() if stmt.else_body else next + + # If statements are normalized + assert len(stmt.expr) == 1 + + builder.process_conditional(stmt.expr[0], if_body, else_body) + builder.activate_block(if_body) + builder.accept(stmt.body[0]) + builder.goto(next) + if stmt.else_body: + builder.activate_block(else_body) + builder.accept(stmt.else_body) + builder.goto(next) + builder.activate_block(next) + + +def transform_while_stmt(builder: IRBuilder, s: WhileStmt) -> None: + body, next, top, else_block = BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock() + normal_loop_exit = else_block if s.else_body is not None else next + + builder.push_loop_stack(top, next) + + # Split block so that we get a handle to the top of the loop. + builder.goto_and_activate(top) + builder.process_conditional(s.expr, body, normal_loop_exit) + + builder.activate_block(body) + builder.accept(s.body) + # Add branch to the top at the end of the body. + builder.goto(top) + + builder.pop_loop_stack() + + if s.else_body is not None: + builder.activate_block(else_block) + builder.accept(s.else_body) + builder.goto(next) + + builder.activate_block(next) + + +def transform_for_stmt(builder: IRBuilder, s: ForStmt) -> None: + def body() -> None: + builder.accept(s.body) + + def else_block() -> None: + assert s.else_body is not None + builder.accept(s.else_body) + + builder.for_loop_helper(s.index, s.expr, body, + else_block if s.else_body else None, s.line) + + +def transform_break_stmt(builder: IRBuilder, node: BreakStmt) -> None: + builder.nonlocal_control[-1].gen_break(builder, node.line) + + +def transform_continue_stmt(builder: IRBuilder, node: ContinueStmt) -> None: + builder.nonlocal_control[-1].gen_continue(builder, node.line) + + +def transform_raise_stmt(builder: IRBuilder, s: RaiseStmt) -> None: + if s.expr is None: + builder.primitive_op(reraise_exception_op, [], NO_TRACEBACK_LINE_NO) + builder.add(Unreachable()) + return + + exc = builder.accept(s.expr) + builder.primitive_op(raise_exception_op, [exc], s.line) + builder.add(Unreachable()) + + +def transform_try_except(builder: IRBuilder, body: GenFunc, handlers: Sequence[ Tuple[Optional[Expression], Optional[Expression], GenFunc]], else_body: Optional[GenFunc], line: int) -> None: - """Generalized try/except/else handling that takes functions to gen the bodies. - - The point of this is to also be able to support with.""" - assert handlers, "try needs except" - - except_entry, exit_block, cleanup_block = BasicBlock(), BasicBlock(), BasicBlock() - double_except_block = BasicBlock() - # If there is an else block, jump there after the try, otherwise just leave - else_block = BasicBlock() if else_body else exit_block - - # Compile the try block with an error handler - self.builder.builder.push_error_handler(except_entry) - self.builder.goto_and_activate(BasicBlock()) - body() - self.builder.goto(else_block) - self.builder.builder.pop_error_handler() - - # The error handler catches the error and then checks it - # against the except clauses. We compile the error handler - # itself with an error handler so that it can properly restore - # the *old* exc_info if an exception occurs. - # The exception chaining will be done automatically when the - # exception is raised, based on the exception in exc_info. - self.builder.builder.push_error_handler(double_except_block) - self.builder.activate_block(except_entry) - old_exc = self.builder.maybe_spill(self.builder.primitive_op(error_catch_op, [], line)) - # Compile the except blocks with the nonlocal control flow overridden to clear exc_info - self.builder.nonlocal_control.append( - ExceptNonlocalControl(self.builder.nonlocal_control[-1], old_exc)) - - # Process the bodies - for type, var, handler_body in handlers: - next_block = None - if type: - next_block, body_block = BasicBlock(), BasicBlock() - matches = self.builder.primitive_op( - exc_matches_op, [self.builder.accept(type)], type.line - ) - self.builder.add(Branch(matches, body_block, next_block, Branch.BOOL_EXPR)) - self.builder.activate_block(body_block) - if var: - target = self.builder.get_assignment_target(var) - self.builder.assign( - target, - self.builder.primitive_op(get_exc_value_op, [], var.line), - var.line - ) - handler_body() - self.builder.goto(cleanup_block) - if next_block: - self.builder.activate_block(next_block) - - # Reraise the exception if needed - if next_block: - self.builder.primitive_op(reraise_exception_op, [], NO_TRACEBACK_LINE_NO) - self.builder.add(Unreachable()) - - self.builder.nonlocal_control.pop() - self.builder.builder.pop_error_handler() - - # Cleanup for if we leave except through normal control flow: - # restore the saved exc_info information and continue propagating - # the exception if it exists. - self.builder.activate_block(cleanup_block) - self.builder.primitive_op(restore_exc_info_op, [self.builder.read(old_exc)], line) - self.builder.goto(exit_block) - - # Cleanup for if we leave except through a raised exception: - # restore the saved exc_info information and continue propagating - # the exception. - self.builder.activate_block(double_except_block) - self.builder.primitive_op(restore_exc_info_op, [self.builder.read(old_exc)], line) - self.builder.primitive_op(keep_propagating_op, [], NO_TRACEBACK_LINE_NO) - self.builder.add(Unreachable()) - - # If present, compile the else body in the obvious way - if else_body: - self.builder.activate_block(else_block) - else_body() - self.builder.goto(exit_block) - - self.builder.activate_block(exit_block) - - def visit_try_except_stmt(self, t: TryStmt) -> None: - def body() -> None: - self.builder.accept(t.body) - - # Work around scoping woes - def make_handler(body: Block) -> GenFunc: - return lambda: self.builder.accept(body) - - handlers = [(type, var, make_handler(body)) for type, var, body in - zip(t.types, t.vars, t.handlers)] - else_body = (lambda: self.builder.accept(t.else_body)) if t.else_body else None - self.visit_try_except(body, handlers, else_body, t.line) - - def try_finally_try(self, err_handler: BasicBlock, return_entry: BasicBlock, - main_entry: BasicBlock, try_body: GenFunc) -> Optional[Register]: - # Compile the try block with an error handler - control = TryFinallyNonlocalControl(return_entry) - self.builder.builder.push_error_handler(err_handler) - - self.builder.nonlocal_control.append(control) - self.builder.goto_and_activate(BasicBlock()) - try_body() - self.builder.goto(main_entry) - self.builder.nonlocal_control.pop() - self.builder.builder.pop_error_handler() - - return control.ret_reg - - def try_finally_entry_blocks(self, - err_handler: BasicBlock, return_entry: BasicBlock, - main_entry: BasicBlock, finally_block: BasicBlock, - ret_reg: Optional[Register]) -> Value: - old_exc = self.builder.alloc_temp(exc_rtuple) - - # Entry block for non-exceptional flow - self.builder.activate_block(main_entry) - if ret_reg: - self.builder.add( - Assign( - ret_reg, - self.builder.add(LoadErrorValue(self.builder.ret_types[-1])) - ) - ) - self.builder.goto(return_entry) - - self.builder.activate_block(return_entry) - self.builder.add(Assign(old_exc, self.builder.add(LoadErrorValue(exc_rtuple)))) - self.builder.goto(finally_block) - - # Entry block for errors - self.builder.activate_block(err_handler) - if ret_reg: - self.builder.add( - Assign( - ret_reg, - self.builder.add(LoadErrorValue(self.builder.ret_types[-1])) - ) + """Generalized try/except/else handling that takes functions to gen the bodies. + + The point of this is to also be able to support with.""" + assert handlers, "try needs except" + + except_entry, exit_block, cleanup_block = BasicBlock(), BasicBlock(), BasicBlock() + double_except_block = BasicBlock() + # If there is an else block, jump there after the try, otherwise just leave + else_block = BasicBlock() if else_body else exit_block + + # Compile the try block with an error handler + builder.builder.push_error_handler(except_entry) + builder.goto_and_activate(BasicBlock()) + body() + builder.goto(else_block) + builder.builder.pop_error_handler() + + # The error handler catches the error and then checks it + # against the except clauses. We compile the error handler + # itself with an error handler so that it can properly restore + # the *old* exc_info if an exception occurs. + # The exception chaining will be done automatically when the + # exception is raised, based on the exception in exc_info. + builder.builder.push_error_handler(double_except_block) + builder.activate_block(except_entry) + old_exc = builder.maybe_spill(builder.primitive_op(error_catch_op, [], line)) + # Compile the except blocks with the nonlocal control flow overridden to clear exc_info + builder.nonlocal_control.append( + ExceptNonlocalControl(builder.nonlocal_control[-1], old_exc)) + + # Process the bodies + for type, var, handler_body in handlers: + next_block = None + if type: + next_block, body_block = BasicBlock(), BasicBlock() + matches = builder.primitive_op( + exc_matches_op, [builder.accept(type)], type.line ) - self.builder.add(Assign(old_exc, self.builder.primitive_op(error_catch_op, [], -1))) - self.builder.goto(finally_block) - - return old_exc - - def try_finally_body( - self, finally_block: BasicBlock, finally_body: GenFunc, - ret_reg: Optional[Value], old_exc: Value) -> Tuple[BasicBlock, - 'FinallyNonlocalControl']: - cleanup_block = BasicBlock() - # Compile the finally block with the nonlocal control flow overridden to restore exc_info - self.builder.builder.push_error_handler(cleanup_block) - finally_control = FinallyNonlocalControl( - self.builder.nonlocal_control[-1], ret_reg, old_exc) - self.builder.nonlocal_control.append(finally_control) - self.builder.activate_block(finally_block) - finally_body() - self.builder.nonlocal_control.pop() - - return cleanup_block, finally_control - - def try_finally_resolve_control(self, cleanup_block: BasicBlock, - finally_control: FinallyNonlocalControl, - old_exc: Value, ret_reg: Optional[Value]) -> BasicBlock: - """Resolve the control flow out of a finally block. - - This means returning if there was a return, propagating - exceptions, break/continue (soon), or just continuing on. - """ - reraise, rest = BasicBlock(), BasicBlock() - self.builder.add(Branch(old_exc, rest, reraise, Branch.IS_ERROR)) - - # Reraise the exception if there was one - self.builder.activate_block(reraise) - self.builder.primitive_op(reraise_exception_op, [], NO_TRACEBACK_LINE_NO) - self.builder.add(Unreachable()) - self.builder.builder.pop_error_handler() - - # If there was a return, keep returning - if ret_reg: - self.builder.activate_block(rest) - return_block, rest = BasicBlock(), BasicBlock() - self.builder.add(Branch(ret_reg, rest, return_block, Branch.IS_ERROR)) - - self.builder.activate_block(return_block) - self.builder.nonlocal_control[-1].gen_return(self.builder, ret_reg, -1) - - # TODO: handle break/continue - self.builder.activate_block(rest) - out_block = BasicBlock() - self.builder.goto(out_block) - - # If there was an exception, restore again - self.builder.activate_block(cleanup_block) - finally_control.gen_cleanup(self.builder, -1) - self.builder.primitive_op(keep_propagating_op, [], NO_TRACEBACK_LINE_NO) - self.builder.add(Unreachable()) - - return out_block - - def visit_try_finally_stmt(self, try_body: GenFunc, finally_body: GenFunc) -> None: - """Generalized try/finally handling that takes functions to gen the bodies. - - The point of this is to also be able to support with.""" - # Finally is a big pain, because there are so many ways that - # exits can occur. We emit 10+ basic blocks for every finally! - - err_handler, main_entry, return_entry, finally_block = ( - BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock()) - - # Compile the body of the try - ret_reg = self.try_finally_try( - err_handler, return_entry, main_entry, try_body) - - # Set up the entry blocks for the finally statement - old_exc = self.try_finally_entry_blocks( - err_handler, return_entry, main_entry, finally_block, ret_reg) - - # Compile the body of the finally - cleanup_block, finally_control = self.try_finally_body( - finally_block, finally_body, ret_reg, old_exc) - - # Resolve the control flow out of the finally block - out_block = self.try_finally_resolve_control( - cleanup_block, finally_control, old_exc, ret_reg) - - self.builder.activate_block(out_block) - - def visit_try_stmt(self, t: TryStmt) -> None: - # Our compilation strategy for try/except/else/finally is to - # treat try/except/else and try/finally as separate language - # constructs that we compile separately. When we have a - # try/except/else/finally, we treat the try/except/else as the - # body of a try/finally block. - if t.finally_body: - def visit_try_body() -> None: - if t.handlers: - self.visit_try_except_stmt(t) - else: - self.builder.accept(t.body) - body = t.finally_body - - self.visit_try_finally_stmt(visit_try_body, lambda: self.builder.accept(body)) - else: - self.visit_try_except_stmt(t) - - def get_sys_exc_info(self) -> List[Value]: - exc_info = self.builder.primitive_op(get_exc_info_op, [], -1) - return [self.builder.add(TupleGet(exc_info, i, -1)) for i in range(3)] - - def visit_with(self, expr: Expression, target: Optional[Lvalue], - body: GenFunc, line: int) -> None: - - # This is basically a straight transcription of the Python code in PEP 343. - # I don't actually understand why a bunch of it is the way it is. - # We could probably optimize the case where the manager is compiled by us, - # but that is not our common case at all, so. - mgr_v = self.builder.accept(expr) - typ = self.builder.primitive_op(type_op, [mgr_v], line) - exit_ = self.builder.maybe_spill(self.builder.py_get_attr(typ, '__exit__', line)) - value = self.builder.py_call( - self.builder.py_get_attr(typ, '__enter__', line), [mgr_v], line - ) - mgr = self.builder.maybe_spill(mgr_v) - exc = self.builder.maybe_spill_assignable(self.builder.primitive_op(true_op, [], -1)) - - def try_body() -> None: - if target: - self.builder.assign(self.builder.get_assignment_target(target), value, line) - body() - - def except_body() -> None: - self.builder.assign(exc, self.builder.primitive_op(false_op, [], -1), line) - out_block, reraise_block = BasicBlock(), BasicBlock() - self.builder.add_bool_branch( - self.builder.py_call(self.builder.read(exit_), - [self.builder.read(mgr)] + self.get_sys_exc_info(), line), - out_block, - reraise_block + builder.add(Branch(matches, body_block, next_block, Branch.BOOL_EXPR)) + builder.activate_block(body_block) + if var: + target = builder.get_assignment_target(var) + builder.assign( + target, + builder.primitive_op(get_exc_value_op, [], var.line), + var.line ) - self.builder.activate_block(reraise_block) - self.builder.primitive_op(reraise_exception_op, [], NO_TRACEBACK_LINE_NO) - self.builder.add(Unreachable()) - self.builder.activate_block(out_block) - - def finally_body() -> None: - out_block, exit_block = BasicBlock(), BasicBlock() - self.builder.add( - Branch(self.builder.read(exc), exit_block, out_block, Branch.BOOL_EXPR) + handler_body() + builder.goto(cleanup_block) + if next_block: + builder.activate_block(next_block) + + # Reraise the exception if needed + if next_block: + builder.primitive_op(reraise_exception_op, [], NO_TRACEBACK_LINE_NO) + builder.add(Unreachable()) + + builder.nonlocal_control.pop() + builder.builder.pop_error_handler() + + # Cleanup for if we leave except through normal control flow: + # restore the saved exc_info information and continue propagating + # the exception if it exists. + builder.activate_block(cleanup_block) + builder.primitive_op(restore_exc_info_op, [builder.read(old_exc)], line) + builder.goto(exit_block) + + # Cleanup for if we leave except through a raised exception: + # restore the saved exc_info information and continue propagating + # the exception. + builder.activate_block(double_except_block) + builder.primitive_op(restore_exc_info_op, [builder.read(old_exc)], line) + builder.primitive_op(keep_propagating_op, [], NO_TRACEBACK_LINE_NO) + builder.add(Unreachable()) + + # If present, compile the else body in the obvious way + if else_body: + builder.activate_block(else_block) + else_body() + builder.goto(exit_block) + + builder.activate_block(exit_block) + + +def transform_try_except_stmt(builder: IRBuilder, t: TryStmt) -> None: + def body() -> None: + builder.accept(t.body) + + # Work around scoping woes + def make_handler(body: Block) -> GenFunc: + return lambda: builder.accept(body) + + handlers = [(type, var, make_handler(body)) + for type, var, body in zip(t.types, t.vars, t.handlers)] + else_body = (lambda: builder.accept(t.else_body)) if t.else_body else None + transform_try_except(builder, body, handlers, else_body, t.line) + + +def try_finally_try(builder: IRBuilder, + err_handler: BasicBlock, + return_entry: BasicBlock, + main_entry: BasicBlock, + try_body: GenFunc) -> Optional[Register]: + # Compile the try block with an error handler + control = TryFinallyNonlocalControl(return_entry) + builder.builder.push_error_handler(err_handler) + + builder.nonlocal_control.append(control) + builder.goto_and_activate(BasicBlock()) + try_body() + builder.goto(main_entry) + builder.nonlocal_control.pop() + builder.builder.pop_error_handler() + + return control.ret_reg + + +def try_finally_entry_blocks(builder: IRBuilder, + err_handler: BasicBlock, + return_entry: BasicBlock, + main_entry: BasicBlock, + finally_block: BasicBlock, + ret_reg: Optional[Register]) -> Value: + old_exc = builder.alloc_temp(exc_rtuple) + + # Entry block for non-exceptional flow + builder.activate_block(main_entry) + if ret_reg: + builder.add( + Assign( + ret_reg, + builder.add(LoadErrorValue(builder.ret_types[-1])) ) - self.builder.activate_block(exit_block) - none = self.builder.none_object() - self.builder.py_call( - self.builder.read(exit_), [self.builder.read(mgr), none, none, none], line + ) + builder.goto(return_entry) + + builder.activate_block(return_entry) + builder.add(Assign(old_exc, builder.add(LoadErrorValue(exc_rtuple)))) + builder.goto(finally_block) + + # Entry block for errors + builder.activate_block(err_handler) + if ret_reg: + builder.add( + Assign( + ret_reg, + builder.add(LoadErrorValue(builder.ret_types[-1])) ) - self.builder.goto_and_activate(out_block) - - self.visit_try_finally_stmt( - lambda: self.visit_try_except(try_body, [(None, None, except_body)], None, line), - finally_body) - - def visit_with_stmt(self, o: WithStmt) -> None: - # Generate separate logic for each expr in it, left to right - def generate(i: int) -> None: - if i >= len(o.expr): - self.builder.accept(o.body) + ) + builder.add(Assign(old_exc, builder.primitive_op(error_catch_op, [], -1))) + builder.goto(finally_block) + + return old_exc + + +def try_finally_body( + builder: IRBuilder, + finally_block: BasicBlock, + finally_body: GenFunc, + ret_reg: Optional[Value], + old_exc: Value) -> Tuple[BasicBlock, FinallyNonlocalControl]: + cleanup_block = BasicBlock() + # Compile the finally block with the nonlocal control flow overridden to restore exc_info + builder.builder.push_error_handler(cleanup_block) + finally_control = FinallyNonlocalControl( + builder.nonlocal_control[-1], ret_reg, old_exc) + builder.nonlocal_control.append(finally_control) + builder.activate_block(finally_block) + finally_body() + builder.nonlocal_control.pop() + + return cleanup_block, finally_control + + +def try_finally_resolve_control(builder: IRBuilder, + cleanup_block: BasicBlock, + finally_control: FinallyNonlocalControl, + old_exc: Value, + ret_reg: Optional[Value]) -> BasicBlock: + """Resolve the control flow out of a finally block. + + This means returning if there was a return, propagating + exceptions, break/continue (soon), or just continuing on. + """ + reraise, rest = BasicBlock(), BasicBlock() + builder.add(Branch(old_exc, rest, reraise, Branch.IS_ERROR)) + + # Reraise the exception if there was one + builder.activate_block(reraise) + builder.primitive_op(reraise_exception_op, [], NO_TRACEBACK_LINE_NO) + builder.add(Unreachable()) + builder.builder.pop_error_handler() + + # If there was a return, keep returning + if ret_reg: + builder.activate_block(rest) + return_block, rest = BasicBlock(), BasicBlock() + builder.add(Branch(ret_reg, rest, return_block, Branch.IS_ERROR)) + + builder.activate_block(return_block) + builder.nonlocal_control[-1].gen_return(builder, ret_reg, -1) + + # TODO: handle break/continue + builder.activate_block(rest) + out_block = BasicBlock() + builder.goto(out_block) + + # If there was an exception, restore again + builder.activate_block(cleanup_block) + finally_control.gen_cleanup(builder, -1) + builder.primitive_op(keep_propagating_op, [], NO_TRACEBACK_LINE_NO) + builder.add(Unreachable()) + + return out_block + + +def transform_try_finally_stmt(builder: IRBuilder, + try_body: GenFunc, + finally_body: GenFunc) -> None: + """Generalized try/finally handling that takes functions to gen the bodies. + + The point of this is to also be able to support with.""" + # Finally is a big pain, because there are so many ways that + # exits can occur. We emit 10+ basic blocks for every finally! + + err_handler, main_entry, return_entry, finally_block = ( + BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock()) + + # Compile the body of the try + ret_reg = try_finally_try( + builder, err_handler, return_entry, main_entry, try_body) + + # Set up the entry blocks for the finally statement + old_exc = try_finally_entry_blocks( + builder, err_handler, return_entry, main_entry, finally_block, ret_reg) + + # Compile the body of the finally + cleanup_block, finally_control = try_finally_body( + builder, finally_block, finally_body, ret_reg, old_exc) + + # Resolve the control flow out of the finally block + out_block = try_finally_resolve_control( + builder, cleanup_block, finally_control, old_exc, ret_reg) + + builder.activate_block(out_block) + + +def transform_try_stmt(builder: IRBuilder, t: TryStmt) -> None: + # Our compilation strategy for try/except/else/finally is to + # treat try/except/else and try/finally as separate language + # constructs that we compile separately. When we have a + # try/except/else/finally, we treat the try/except/else as the + # body of a try/finally block. + if t.finally_body: + def transform_try_body() -> None: + if t.handlers: + transform_try_except_stmt(builder, t) else: - self.visit_with(o.expr[i], o.target[i], lambda: generate(i + 1), o.line) - - generate(0) - - def visit_assert_stmt(self, a: AssertStmt) -> None: - if self.builder.options.strip_asserts: - return - cond = self.builder.accept(a.expr) - ok_block, error_block = BasicBlock(), BasicBlock() - self.builder.add_bool_branch(cond, ok_block, error_block) - self.builder.activate_block(error_block) - if a.msg is None: - # Special case (for simpler generated code) - self.builder.add(RaiseStandardError(RaiseStandardError.ASSERTION_ERROR, None, a.line)) - elif isinstance(a.msg, StrExpr): - # Another special case - self.builder.add(RaiseStandardError(RaiseStandardError.ASSERTION_ERROR, a.msg.value, - a.line)) + builder.accept(t.body) + body = t.finally_body + + transform_try_finally_stmt(builder, transform_try_body, lambda: builder.accept(body)) + else: + transform_try_except_stmt(builder, t) + + +def get_sys_exc_info(builder: IRBuilder) -> List[Value]: + exc_info = builder.primitive_op(get_exc_info_op, [], -1) + return [builder.add(TupleGet(exc_info, i, -1)) for i in range(3)] + + +def transform_with(builder: IRBuilder, + expr: Expression, + target: Optional[Lvalue], + body: GenFunc, + line: int) -> None: + # This is basically a straight transcription of the Python code in PEP 343. + # I don't actually understand why a bunch of it is the way it is. + # We could probably optimize the case where the manager is compiled by us, + # but that is not our common case at all, so. + mgr_v = builder.accept(expr) + typ = builder.primitive_op(type_op, [mgr_v], line) + exit_ = builder.maybe_spill(builder.py_get_attr(typ, '__exit__', line)) + value = builder.py_call( + builder.py_get_attr(typ, '__enter__', line), [mgr_v], line + ) + mgr = builder.maybe_spill(mgr_v) + exc = builder.maybe_spill_assignable(builder.primitive_op(true_op, [], -1)) + + def try_body() -> None: + if target: + builder.assign(builder.get_assignment_target(target), value, line) + body() + + def except_body() -> None: + builder.assign(exc, builder.primitive_op(false_op, [], -1), line) + out_block, reraise_block = BasicBlock(), BasicBlock() + builder.add_bool_branch( + builder.py_call(builder.read(exit_), + [builder.read(mgr)] + get_sys_exc_info(builder), line), + out_block, + reraise_block + ) + builder.activate_block(reraise_block) + builder.primitive_op(reraise_exception_op, [], NO_TRACEBACK_LINE_NO) + builder.add(Unreachable()) + builder.activate_block(out_block) + + def finally_body() -> None: + out_block, exit_block = BasicBlock(), BasicBlock() + builder.add( + Branch(builder.read(exc), exit_block, out_block, Branch.BOOL_EXPR) + ) + builder.activate_block(exit_block) + none = builder.none_object() + builder.py_call( + builder.read(exit_), [builder.read(mgr), none, none, none], line + ) + builder.goto_and_activate(out_block) + + transform_try_finally_stmt( + builder, + lambda: transform_try_except(builder, + try_body, + [(None, None, except_body)], + None, + line), + finally_body + ) + + +def transform_with_stmt(builder: IRBuilder, o: WithStmt) -> None: + # Generate separate logic for each expr in it, left to right + def generate(i: int) -> None: + if i >= len(o.expr): + builder.accept(o.body) else: - # The general case -- explicitly construct an exception instance - message = self.builder.accept(a.msg) - exc_type = self.builder.load_module_attr_by_fullname('builtins.AssertionError', a.line) - exc = self.builder.py_call(exc_type, [message], a.line) - self.builder.primitive_op(raise_exception_op, [exc], a.line) - self.builder.add(Unreachable()) - self.builder.activate_block(ok_block) - - def visit_del_stmt(self, o: DelStmt) -> None: - self.visit_del_item(self.builder.get_assignment_target(o.expr), o.line) - - def visit_del_item(self, target: AssignmentTarget, line: int) -> None: - if isinstance(target, AssignmentTargetIndex): - self.builder.gen_method_call( - target.base, - '__delitem__', - [target.index], - result_type=None, - line=line - ) - elif isinstance(target, AssignmentTargetAttr): - key = self.builder.load_static_unicode(target.attr) - self.builder.add(PrimitiveOp([target.obj, key], py_delattr_op, line)) - elif isinstance(target, AssignmentTargetRegister): - # Delete a local by assigning an error value to it, which will - # prompt the insertion of uninit checks. - self.builder.add(Assign(target.register, - self.builder.add(LoadErrorValue(target.type, undefines=True)))) - elif isinstance(target, AssignmentTargetTuple): - for subtarget in target.items: - self.visit_del_item(subtarget, line) + transform_with(builder, o.expr[i], o.target[i], lambda: generate(i + 1), o.line) + + generate(0) + + +def transform_assert_stmt(builder: IRBuilder, a: AssertStmt) -> None: + if builder.options.strip_asserts: + return + cond = builder.accept(a.expr) + ok_block, error_block = BasicBlock(), BasicBlock() + builder.add_bool_branch(cond, ok_block, error_block) + builder.activate_block(error_block) + if a.msg is None: + # Special case (for simpler generated code) + builder.add(RaiseStandardError(RaiseStandardError.ASSERTION_ERROR, None, a.line)) + elif isinstance(a.msg, StrExpr): + # Another special case + builder.add(RaiseStandardError(RaiseStandardError.ASSERTION_ERROR, a.msg.value, + a.line)) + else: + # The general case -- explicitly construct an exception instance + message = builder.accept(a.msg) + exc_type = builder.load_module_attr_by_fullname('builtins.AssertionError', a.line) + exc = builder.py_call(exc_type, [message], a.line) + builder.primitive_op(raise_exception_op, [exc], a.line) + builder.add(Unreachable()) + builder.activate_block(ok_block) + + +def transform_del_stmt(builder: IRBuilder, o: DelStmt) -> None: + transform_del_item(builder, builder.get_assignment_target(o.expr), o.line) + + +def transform_del_item(builder: IRBuilder, target: AssignmentTarget, line: int) -> None: + if isinstance(target, AssignmentTargetIndex): + builder.gen_method_call( + target.base, + '__delitem__', + [target.index], + result_type=None, + line=line + ) + elif isinstance(target, AssignmentTargetAttr): + key = builder.load_static_unicode(target.attr) + builder.add(PrimitiveOp([target.obj, key], py_delattr_op, line)) + elif isinstance(target, AssignmentTargetRegister): + # Delete a local by assigning an error value to it, which will + # prompt the insertion of uninit checks. + builder.add(Assign(target.register, + builder.add(LoadErrorValue(target.type, undefines=True)))) + elif isinstance(target, AssignmentTargetTuple): + for subtarget in target.items: + transform_del_item(builder, subtarget, line)