"""Transform that inserts error checks after opcodes. When initially building the IR, the code doesn't perform error checks for exceptions. This module is used to insert all required error checks afterwards. Each Op describes how it indicates an error condition (if at all). We need to split basic blocks on each error check since branches can only be placed at the end of a basic block. """ from typing import List, Optional from mypyc.ir.ops import ( Value, BasicBlock, LoadErrorValue, Return, Branch, RegisterOp, ComparisonOp, CallC, Integer, ERR_NEVER, ERR_MAGIC, ERR_FALSE, ERR_ALWAYS, ERR_MAGIC_OVERLAPPING, NO_TRACEBACK_LINE_NO ) from mypyc.ir.func_ir import FuncIR from mypyc.ir.rtypes import bool_rprimitive from mypyc.primitives.registry import CFunctionDescription from mypyc.primitives.exc_ops import err_occurred_op def insert_exception_handling(ir: FuncIR) -> None: # Generate error block if any ops may raise an exception. If an op # fails without its own error handler, we'll branch to this # block. The block just returns an error value. error_label = None for block in ir.blocks: can_raise = any(op.can_raise() for op in block.ops) if can_raise: error_label = add_handler_block(ir) break if error_label: ir.blocks = split_blocks_at_errors(ir.blocks, error_label, ir.traceback_name) def add_handler_block(ir: FuncIR) -> BasicBlock: block = BasicBlock() ir.blocks.append(block) op = LoadErrorValue(ir.ret_type) block.ops.append(op) block.ops.append(Return(op)) return block def split_blocks_at_errors(blocks: List[BasicBlock], default_error_handler: BasicBlock, func_name: Optional[str]) -> List[BasicBlock]: new_blocks: List[BasicBlock] = [] # First split blocks on ops that may raise. for block in blocks: ops = block.ops block.ops = [] cur_block = block new_blocks.append(cur_block) # If the block has an error handler specified, use it. Otherwise # fall back to the default. error_label = block.error_handler or default_error_handler block.error_handler = None for op in ops: target: Value = op cur_block.ops.append(op) if isinstance(op, RegisterOp) and op.error_kind != ERR_NEVER: # Split new_block = BasicBlock() new_blocks.append(new_block) if op.error_kind == ERR_MAGIC: # Op returns an error value on error that depends on result RType. variant = Branch.IS_ERROR negated = False elif op.error_kind == ERR_FALSE: # Op returns a C false value on error. variant = Branch.BOOL negated = True elif op.error_kind == ERR_ALWAYS: variant = Branch.BOOL negated = True # this is a hack to represent the always fail # semantics, using a temporary bool with value false target = Integer(0, bool_rprimitive) elif op.error_kind == ERR_MAGIC_OVERLAPPING: errvalue = Integer(int(target.type.c_undefined), rtype=op.type) comp = ComparisonOp(target, errvalue, ComparisonOp.EQ) cur_block.ops.append(comp) new_block2 = BasicBlock() new_blocks.append(new_block2) branch = Branch(comp, true_label=new_block2, false_label=new_block, op=Branch.BOOL) cur_block.ops.append(branch) cur_block = new_block2 target = primitive_call(err_occurred_op, [], target.line) cur_block.ops.append(target) variant = Branch.IS_ERROR negated = True else: assert False, 'unknown error kind %d' % op.error_kind # Void ops can't generate errors since error is always # indicated by a special value stored in a register. if op.error_kind != ERR_ALWAYS: assert not op.is_void, "void op generating errors?" branch = Branch(target, true_label=error_label, false_label=new_block, op=variant, line=op.line) branch.negated = negated if op.line != NO_TRACEBACK_LINE_NO and func_name is not None: branch.traceback_entry = (func_name, op.line) cur_block.ops.append(branch) cur_block = new_block return new_blocks def primitive_call(desc: CFunctionDescription, args: List[Value], line: int) -> CallC: return CallC( desc.c_function_name, [], desc.return_type, desc.steals, desc.is_borrowed, desc.error_kind, line, )