[mypyc] Support ERR_ALWAYS (#9073) Related to mypyc/mypyc#734, with a focus on exceptions related ops. This PR adds a new error kind: ERR_ALWAYS, which indicates the op always fails. It adds temporary false value to ensure such behavior in the exception handling transform and makes the raise op void.
diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index ef88b8c..6d6b46b 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py
@@ -130,15 +130,7 @@ self.emit_line('if ({}) {{'.format(cond)) - if op.traceback_entry is not None: - globals_static = self.emitter.static_name('globals', self.module_name) - self.emit_line('CPy_AddTraceback("%s", "%s", %d, %s);' % ( - self.source_path.replace("\\", "\\\\"), - op.traceback_entry[0], - op.traceback_entry[1], - globals_static)) - if DEBUG_ERRORS: - self.emit_line('assert(PyErr_Occurred() != NULL && "failure w/o err!");') + self.emit_traceback(op) self.emit_lines( 'goto %s;' % self.label(op.true), @@ -422,7 +414,10 @@ self.emitter.emit_line('{} = 0;'.format(self.reg(op))) def visit_call_c(self, op: CallC) -> None: - dest = self.get_dest_assign(op) + if op.is_void: + dest = '' + else: + dest = self.get_dest_assign(op) args = ', '.join(self.reg(arg) for arg in op.args) self.emitter.emit_line("{}{}({});".format(dest, op.function_name, args)) @@ -472,3 +467,14 @@ def emit_declaration(self, line: str) -> None: self.declarations.emit_line(line) + + def emit_traceback(self, op: Branch) -> None: + if op.traceback_entry is not None: + globals_static = self.emitter.static_name('globals', self.module_name) + self.emit_line('CPy_AddTraceback("%s", "%s", %d, %s);' % ( + self.source_path.replace("\\", "\\\\"), + op.traceback_entry[0], + op.traceback_entry[1], + globals_static)) + if DEBUG_ERRORS: + self.emit_line('assert(PyErr_Occurred() != NULL && "failure w/o err!");')
diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 2eb53b4..0344d49 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py
@@ -295,6 +295,8 @@ ERR_FALSE = 2 # type: Final # Generates negative integer on exception ERR_NEG_INT = 3 # type: Final +# Always fails +ERR_ALWAYS = 4 # type: Final # Hack: using this line number for an op will suppress it in tracebacks NO_TRACEBACK_LINE_NO = -10000 @@ -1167,7 +1169,10 @@ def to_str(self, env: Environment) -> str: args_str = ', '.join(env.format('%r', arg) for arg in self.args) - return env.format('%r = %s(%s)', self, self.function_name, args_str) + if self.is_void: + return env.format('%s(%s)', self.function_name, args_str) + else: + return env.format('%r = %s(%s)', self, self.function_name, args_str) def sources(self) -> List[Value]: return self.args
diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index a3c65a9..1f66993 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py
@@ -243,7 +243,7 @@ return exc = builder.accept(s.expr) - builder.primitive_op(raise_exception_op, [exc], s.line) + builder.call_c(raise_exception_op, [exc], s.line) builder.add(Unreachable()) @@ -614,7 +614,7 @@ 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.call_c(raise_exception_op, [exc], a.line) builder.add(Unreachable()) builder.activate_block(ok_block)
diff --git a/mypyc/primitives/exc_ops.py b/mypyc/primitives/exc_ops.py index ea79203..a42f8d3 100644 --- a/mypyc/primitives/exc_ops.py +++ b/mypyc/primitives/exc_ops.py
@@ -1,21 +1,18 @@ """Exception-related primitive ops.""" -from mypyc.ir.ops import ERR_NEVER, ERR_FALSE +from mypyc.ir.ops import ERR_NEVER, ERR_FALSE, ERR_ALWAYS from mypyc.ir.rtypes import bool_rprimitive, object_rprimitive, void_rtype, exc_rtuple from mypyc.primitives.registry import ( - simple_emit, call_emit, call_void_emit, call_and_fail_emit, custom_op, + simple_emit, call_emit, call_void_emit, call_and_fail_emit, custom_op, c_custom_op ) # If the argument is a class, raise an instance of the class. Otherwise, assume # that the argument is an exception object, and raise it. -# -# TODO: Making this raise conditionally is kind of hokey. -raise_exception_op = custom_op( +raise_exception_op = c_custom_op( arg_types=[object_rprimitive], - result_type=bool_rprimitive, - error_kind=ERR_FALSE, - format_str='raise_exception({args[0]}); {dest} = 0', - emit=call_and_fail_emit('CPy_Raise')) + return_type=void_rtype, + c_function_name='CPy_Raise', + error_kind=ERR_ALWAYS) # Raise StopIteration exception with the specified value (which can be NULL). set_stop_iteration_value = custom_op(
diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index 272a28e..b47af32 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test
@@ -1322,24 +1322,22 @@ r0 :: object r1 :: str r2, r3 :: object - r4 :: bool L0: r0 = builtins :: module r1 = unicode_1 :: static ('Exception') r2 = getattr r0, r1 r3 = py_call(r2) - raise_exception(r3); r4 = 0 + CPy_Raise(r3) unreachable def bar(): r0 :: object r1 :: str r2 :: object - r3 :: bool L0: r0 = builtins :: module r1 = unicode_1 :: static ('Exception') r2 = getattr r0, r1 - raise_exception(r2); r3 = 0 + CPy_Raise(r2) unreachable [case testModuleTopLevel_toplevel]
diff --git a/mypyc/test-data/irbuild-statements.test b/mypyc/test-data/irbuild-statements.test index 3587c8e..d973221 100644 --- a/mypyc/test-data/irbuild-statements.test +++ b/mypyc/test-data/irbuild-statements.test
@@ -614,8 +614,7 @@ r4 :: object r5 :: str r6, r7 :: object - r8 :: bool - r9 :: None + r8 :: None L0: r0 = builtins.None :: object r1 = x is not r0 @@ -629,11 +628,11 @@ r5 = unicode_3 :: static ('AssertionError') r6 = getattr r4, r5 r7 = py_call(r6, s) - raise_exception(r7); r8 = 0 + CPy_Raise(r7) unreachable L3: - r9 = None - return r9 + r8 = None + return r8 [case testDelList] def delList() -> None:
diff --git a/mypyc/test-data/irbuild-try.test b/mypyc/test-data/irbuild-try.test index 5df1420..f5cee48 100644 --- a/mypyc/test-data/irbuild-try.test +++ b/mypyc/test-data/irbuild-try.test
@@ -277,14 +277,13 @@ r1 :: object r2 :: str r3, r4 :: object - r5 :: bool - r6, r7, r8 :: tuple[object, object, object] - r9 :: str - r10 :: object - r11 :: str - r12, r13 :: object - r14, r15 :: bool - r16 :: None + r5, r6, r7 :: tuple[object, object, object] + r8 :: str + r9 :: object + r10 :: str + r11, r12 :: object + r13, r14 :: bool + r15 :: None L0: L1: if b goto L2 else goto L3 :: bool @@ -294,39 +293,39 @@ r2 = unicode_2 :: static ('Exception') r3 = getattr r1, r2 r4 = py_call(r3, r0) - raise_exception(r4); r5 = 0 + CPy_Raise(r4) unreachable L3: L4: L5: - r7 = <error> :: tuple[object, object, object] - r6 = r7 + r6 = <error> :: tuple[object, object, object] + r5 = r6 goto L7 L6: (handler for L1, L2, L3) - r8 = error_catch - r6 = r8 + r7 = error_catch + r5 = r7 L7: - r9 = unicode_3 :: static ('finally') - r10 = builtins :: module - r11 = unicode_4 :: static ('print') - r12 = getattr r10, r11 - r13 = py_call(r12, r9) - if is_error(r6) goto L9 else goto L8 + r8 = unicode_3 :: static ('finally') + r9 = builtins :: module + r10 = unicode_4 :: static ('print') + r11 = getattr r9, r10 + r12 = py_call(r11, r8) + if is_error(r5) goto L9 else goto L8 L8: - reraise_exc; r14 = 0 + reraise_exc; r13 = 0 unreachable L9: goto L13 L10: (handler for L7, L8) - if is_error(r6) goto L12 else goto L11 + if is_error(r5) goto L12 else goto L11 L11: - restore_exc_info r6 + restore_exc_info r5 L12: - r15 = keep_propagating + r14 = keep_propagating unreachable L13: - r16 = None - return r16 + r15 = None + return r15 [case testWith] from typing import Any
diff --git a/mypyc/transform/exceptions.py b/mypyc/transform/exceptions.py index d1f82e5..755ba60 100644 --- a/mypyc/transform/exceptions.py +++ b/mypyc/transform/exceptions.py
@@ -12,10 +12,11 @@ from typing import List, Optional from mypyc.ir.ops import ( - BasicBlock, LoadErrorValue, Return, Branch, RegisterOp, ERR_NEVER, ERR_MAGIC, - ERR_FALSE, ERR_NEG_INT, NO_TRACEBACK_LINE_NO, + BasicBlock, LoadErrorValue, Return, Branch, RegisterOp, LoadInt, ERR_NEVER, ERR_MAGIC, + ERR_FALSE, ERR_NEG_INT, ERR_ALWAYS, NO_TRACEBACK_LINE_NO, Environment ) from mypyc.ir.func_ir import FuncIR +from mypyc.ir.rtypes import bool_rprimitive def insert_exception_handling(ir: FuncIR) -> None: @@ -29,7 +30,7 @@ error_label = add_handler_block(ir) break if error_label: - ir.blocks = split_blocks_at_errors(ir.blocks, error_label, ir.traceback_name) + ir.blocks = split_blocks_at_errors(ir.blocks, error_label, ir.traceback_name, ir.env) def add_handler_block(ir: FuncIR) -> BasicBlock: @@ -44,7 +45,8 @@ def split_blocks_at_errors(blocks: List[BasicBlock], default_error_handler: BasicBlock, - func_name: Optional[str]) -> List[BasicBlock]: + func_name: Optional[str], + env: Environment) -> List[BasicBlock]: new_blocks = [] # type: List[BasicBlock] # First split blocks on ops that may raise. @@ -60,6 +62,7 @@ block.error_handler = None for op in ops: + target = op cur_block.ops.append(op) if isinstance(op, RegisterOp) and op.error_kind != ERR_NEVER: # Split @@ -77,14 +80,24 @@ elif op.error_kind == ERR_NEG_INT: variant = Branch.NEG_INT_EXPR negated = False + elif op.error_kind == ERR_ALWAYS: + variant = Branch.BOOL_EXPR + negated = True + # this is a hack to represent the always fail + # semantics, using a temporary bool with value false + tmp = LoadInt(0, rtype=bool_rprimitive) + cur_block.ops.append(tmp) + env.add_op(tmp) + target = tmp 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. - assert not op.is_void, "void op generating errors?" + if op.error_kind != ERR_ALWAYS: + assert not op.is_void, "void op generating errors?" - branch = Branch(op, + branch = Branch(target, true_label=error_label, false_label=new_block, op=variant,