diff --git a/artiq/compiler/ir.py b/artiq/compiler/ir.py index 92bc4fb3a..8bc84e598 100644 --- a/artiq/compiler/ir.py +++ b/artiq/compiler/ir.py @@ -1245,9 +1245,9 @@ class Raise(Terminator): if len(self.operands) > 1: return self.operands[1] -class Reraise(Terminator): +class Resume(Terminator): """ - A reraise instruction. + A resume instruction. """ """ @@ -1261,7 +1261,7 @@ class Reraise(Terminator): super().__init__(operands, builtins.TNone(), name) def opcode(self): - return "reraise" + return "resume" def exception_target(self): if len(self.operands) > 0: diff --git a/artiq/compiler/transforms/artiq_ir_generator.py b/artiq/compiler/transforms/artiq_ir_generator.py index 7b6a1a985..bd69ca79c 100644 --- a/artiq/compiler/transforms/artiq_ir_generator.py +++ b/artiq/compiler/transforms/artiq_ir_generator.py @@ -8,6 +8,7 @@ semantics explicitly. from collections import OrderedDict, defaultdict from functools import reduce +from itertools import chain from pythonparser import algorithm, diagnostic, ast from .. import types, builtins, asttyped, ir, iodelay @@ -61,6 +62,9 @@ class ARTIQIRGenerator(algorithm.Visitor): the basic block to which ``return`` will transfer control :ivar unwind_target: (:class:`ir.BasicBlock` or None) the basic block to which unwinding will transfer control + :ivar catch_clauses: (list of (:class:`ir.BasicBlock`, :class:`types.Type` or None)) + a list of catch clauses that should be appended to inner try block + landingpad :ivar final_branch: (function (target: :class:`ir.BasicBlock`, block: :class:`ir.BasicBlock) or None) the function that appends to ``block`` a jump through the ``finally`` statement @@ -103,10 +107,13 @@ class ARTIQIRGenerator(algorithm.Visitor): self.current_private_env = None self.current_args = None self.current_assign = None + self.current_exception = None self.break_target = None self.continue_target = None self.return_target = None self.unwind_target = None + self.catch_clauses = [] + self.outer_final = None self.final_branch = None self.function_map = dict() self.variable_map = dict() @@ -650,9 +657,9 @@ class ARTIQIRGenerator(algorithm.Visitor): self.append(ir.Raise(exn)) else: if self.unwind_target is not None: - self.append(ir.Reraise(self.unwind_target)) + self.append(ir.Resume(self.unwind_target)) else: - self.append(ir.Reraise()) + self.append(ir.Resume()) def visit_Raise(self, node): if node.exc is not None and types.is_exn_constructor(node.exc.type): @@ -662,6 +669,9 @@ class ARTIQIRGenerator(algorithm.Visitor): def visit_Try(self, node): dispatcher = self.add_block("try.dispatch") + cleanup = self.add_block('handler.cleanup') + landingpad = ir.LandingPad(cleanup) + dispatcher.append(landingpad) if any(node.finalbody): # k for continuation @@ -677,15 +687,6 @@ class ARTIQIRGenerator(algorithm.Visitor): final_targets.append(target) final_paths.append(block) - final_exn_targets = [] - final_exn_paths = [] - # raise has to be treated differently - # we cannot follow indirectbr for local access validation, so we - # have to construct the control flow explicitly - def exception_final_branch(target, block): - final_exn_targets.append(target) - final_exn_paths.append(block) - if self.break_target is not None: break_proxy = self.add_block("try.break") old_break, self.break_target = self.break_target, break_proxy @@ -706,15 +707,52 @@ class ARTIQIRGenerator(algorithm.Visitor): return_action.append(ir.Return(value)) final_branch(return_action, return_proxy) + old_outer_final, self.outer_final = self.outer_final, final_branch + elif self.outer_final is None: + landingpad.has_cleanup = False + + # we should propagate the clauses to nested try catch blocks + # so nested try catch will jump to our clause if the inner one does not + # match + # note that the phi instruction here requires some hack, see + # llvm_ir_generator process_function for details + clauses = [] + found_catch_all = False + for handler_node in node.handlers: + if found_catch_all: + self.warn_unreachable(handler_node) + continue + exn_type = handler_node.name_type.find() + if handler_node.filter is not None and \ + not builtins.is_exception(exn_type, 'Exception'): + handler = self.add_block("handler." + exn_type.name) + phi = ir.Phi(builtins.TException(), 'exn') + handler.append(phi) + clauses.append((handler, exn_type, phi)) + else: + handler = self.add_block("handler.catchall") + phi = ir.Phi(builtins.TException(), 'exn') + handler.append(phi) + clauses.append((handler, None, phi)) + found_catch_all = True + + all_clauses = clauses[:] + for clause in self.catch_clauses: + # if the last clause is accept all, do not add further clauses + if len(all_clauses) == 0 or all_clauses[-1][1] is not None: + all_clauses.append(clause) + body = self.add_block("try.body") self.append(ir.Branch(body)) self.current_block = body + old_unwind, self.unwind_target = self.unwind_target, dispatcher + old_clauses, self.catch_clauses = self.catch_clauses, all_clauses try: - old_unwind, self.unwind_target = self.unwind_target, dispatcher self.visit(node.body) finally: self.unwind_target = old_unwind + self.catch_clauses = old_clauses if not self.current_block.is_terminated(): self.visit(node.orelse) @@ -723,95 +761,152 @@ class ARTIQIRGenerator(algorithm.Visitor): body = self.current_block if any(node.finalbody): + # if we have a final block, we should not append clauses to our + # landingpad or we will skip the finally block. + # when the finally block calls resume, it will unwind to the outer + # try catch block automatically + all_clauses = clauses + # reset targets if self.break_target: self.break_target = old_break if self.continue_target: self.continue_target = old_continue self.return_target = old_return - old_final_branch, self.final_branch = self.final_branch, exception_final_branch + if any(node.finalbody) or self.outer_final is not None: + # create new unwind target for cleanup + final_dispatcher = self.add_block("try.final.dispatch") + final_landingpad = ir.LandingPad(cleanup) + final_dispatcher.append(final_landingpad) - cleanup = self.add_block('handler.cleanup') - landingpad = dispatcher.append(ir.LandingPad(cleanup)) - if not any(node.finalbody): - landingpad.has_cleanup = False + # make sure that exception clauses are unwinded to the finally block + old_unwind, self.unwind_target = self.unwind_target, final_dispatcher + + if any(node.finalbody): + redirect = final_branch + elif self.outer_final is not None: + redirect = self.outer_final + else: + redirect = lambda dest, proxy: proxy.append(ir.Branch(dest)) + + # we need to set break/continue/return to execute end_catch + if self.break_target is not None: + break_proxy = self.add_block("try.break") + break_proxy.append(ir.Builtin("end_catch", [], builtins.TNone())) + old_break, self.break_target = self.break_target, break_proxy + redirect(old_break, break_proxy) + + if self.continue_target is not None: + continue_proxy = self.add_block("try.continue") + continue_proxy.append(ir.Builtin("end_catch", [], + builtins.TNone())) + old_continue, self.continue_target = self.continue_target, continue_proxy + redirect(old_continue, continue_proxy) + + return_proxy = self.add_block("try.return") + return_proxy.append(ir.Builtin("end_catch", [], builtins.TNone())) + old_return, self.return_target = self.return_target, return_proxy + old_return_target = old_return + if old_return_target is None: + old_return_target = self.add_block("try.doreturn") + value = old_return_target.append(ir.GetLocal(self.current_private_env, "$return")) + old_return_target.append(ir.Return(value)) + redirect(old_return_target, return_proxy) handlers = [] - for handler_node in node.handlers: - exn_type = handler_node.name_type.find() - if handler_node.filter is not None and \ - not builtins.is_exception(exn_type, 'Exception'): - handler = self.add_block("handler." + exn_type.name) - landingpad.add_clause(handler, exn_type) - else: - handler = self.add_block("handler.catchall") - landingpad.add_clause(handler, None) + for (handler_node, (handler, exn_type, phi)) in zip(node.handlers, clauses): self.current_block = handler if handler_node.name is not None: - exn = self.append(ir.Builtin("exncast", [landingpad], handler_node.name_type)) + exn = self.append(ir.Builtin("exncast", [phi], handler_node.name_type)) self._set_local(handler_node.name, exn) self.visit(handler_node.body) + # only need to call end_catch if the current block is not terminated + # other possible paths: break/continue/return/raise + # we will call end_catch in the first 3 cases, and we should not + # end_catch in the last case for nested exception + if not self.current_block.is_terminated(): + self.append(ir.Builtin("end_catch", [], builtins.TNone())) post_handler = self.current_block + handlers.append(post_handler) - handlers.append((handler, post_handler)) + # branch to all possible clauses, including those from outer try catch + # block + # if we have a finally block, all_clauses will not include those from + # the outer block + for (handler, clause, phi) in all_clauses: + phi.add_incoming(landingpad, dispatcher) + landingpad.add_clause(handler, clause) + + if self.break_target: + self.break_target = old_break + if self.continue_target: + self.continue_target = old_continue + self.return_target = old_return if any(node.finalbody): # Finalize and continue after try statement. - self.final_branch = old_final_branch - - for (i, (target, block)) in enumerate(zip(final_exn_targets, final_exn_paths)): - finalizer = self.add_block(f"finally{i}") - self.current_block = block - self.terminate(ir.Branch(finalizer)) - self.current_block = finalizer - self.visit(node.finalbody) - self.terminate(ir.Branch(target)) - - finalizer = self.add_block("finally") - self.current_block = finalizer - - self.visit(node.finalbody) - post_finalizer = self.current_block - - # Finalize and reraise. Separate from previous case to expose flow - # to LocalAccessValidator. - finalizer_reraise = self.add_block("finally.reraise") + self.outer_final = old_outer_final + self.unwind_target = old_unwind + # Exception path + finalizer_reraise = self.add_block("finally.resume") self.current_block = finalizer_reraise - self.visit(node.finalbody) - self.terminate(ir.Reraise(self.unwind_target)) - - self.current_block = tail = self.add_block("try.tail") - if any(node.finalbody): - final_targets.append(tail) - - for block in final_paths: - block.append(ir.Branch(finalizer)) - - if not body.is_terminated(): - body.append(ir.SetLocal(final_state, "$cont", tail)) - body.append(ir.Branch(finalizer)) - + self.terminate(ir.Resume(self.unwind_target)) cleanup.append(ir.Branch(finalizer_reraise)) - for handler, post_handler in handlers: - if not post_handler.is_terminated(): - post_handler.append(ir.SetLocal(final_state, "$cont", tail)) - post_handler.append(ir.Branch(finalizer)) + # Normal path + finalizer = self.add_block("finally") + self.current_block = finalizer + self.visit(node.finalbody) + post_finalizer = self.current_block + self.current_block = tail = self.add_block("try.tail") + final_targets.append(tail) + # if final block is not terminated, branch to tail if not post_finalizer.is_terminated(): dest = post_finalizer.append(ir.GetLocal(final_state, "$cont")) post_finalizer.append(ir.IndirectBranch(dest, final_targets)) + # make sure proxies will branch to finalizer + for block in final_paths: + if finalizer in block.predecessors(): + # avoid producing irreducible graphs + # generate a new finalizer + self.current_block = tmp_finalizer = self.add_block("finally.tmp") + self.visit(node.finalbody) + if not self.current_block.is_terminated(): + assert isinstance(block.instructions[-1], ir.SetLocal) + self.current_block.append(ir.Branch(block.instructions[-1].operands[-1])) + block.instructions[-1].erase() + block.append(ir.Branch(tmp_finalizer)) + self.current_block = tail + else: + block.append(ir.Branch(finalizer)) + # if no raise in body/handlers, branch to finalizer + for block in chain([body], handlers): + if not block.is_terminated(): + if finalizer in block.predecessors(): + # similar to the above case + self.current_block = tmp_finalizer = self.add_block("finally.tmp") + self.visit(node.finalbody) + self.terminate(ir.Branch(tail)) + block.append(ir.Branch(tmp_finalizer)) + self.current_block = tail + else: + block.append(ir.SetLocal(final_state, "$cont", tail)) + block.append(ir.Branch(finalizer)) else: + if self.outer_final is not None: + self.unwind_target = old_unwind + self.current_block = tail = self.add_block("try.tail") if not body.is_terminated(): body.append(ir.Branch(tail)) - cleanup.append(ir.Reraise(self.unwind_target)) + cleanup.append(ir.Resume(self.unwind_target)) - for handler, post_handler in handlers: - if not post_handler.is_terminated(): - post_handler.append(ir.Branch(tail)) + for handler in handlers: + if not handler.is_terminated(): + handler.append(ir.Branch(tail)) def _try_finally(self, body_gen, finally_gen, name): dispatcher = self.add_block("{}.dispatch".format(name)) @@ -830,7 +925,7 @@ class ARTIQIRGenerator(algorithm.Visitor): self.current_block = self.add_block("{}.cleanup".format(name)) dispatcher.append(ir.LandingPad(self.current_block)) finally_gen() - self.raise_exn() + self.terminate(ir.Resume(self.unwind_target)) self.current_block = self.post_body diff --git a/artiq/compiler/transforms/llvm_ir_generator.py b/artiq/compiler/transforms/llvm_ir_generator.py index 1a206391e..60a5dc482 100644 --- a/artiq/compiler/transforms/llvm_ir_generator.py +++ b/artiq/compiler/transforms/llvm_ir_generator.py @@ -171,11 +171,17 @@ class LLVMIRGenerator: self.llfunction = None self.llmap = {} self.llobject_map = {} + self.llpred_map = {} self.phis = [] self.debug_info_emitter = DebugInfoEmitter(self.llmodule) self.empty_metadata = self.llmodule.add_metadata([]) self.quote_fail_msg = None + def add_pred(self, pred, block): + if block not in self.llpred_map: + self.llpred_map[block] = set() + self.llpred_map[block].add(pred) + def needs_sret(self, lltyp, may_be_large=True): if isinstance(lltyp, ll.VoidType): return False @@ -367,7 +373,9 @@ class LLVMIRGenerator: llty = ll.FunctionType(lli32, [], var_arg=True) elif name == "__artiq_raise": llty = ll.FunctionType(llvoid, [self.llty_of_type(builtins.TException())]) - elif name == "__artiq_reraise": + elif name == "__artiq_resume": + llty = ll.FunctionType(llvoid, []) + elif name == "__artiq_end_catch": llty = ll.FunctionType(llvoid, []) elif name == "memcmp": llty = ll.FunctionType(lli32, [llptr, llptr, lli32]) @@ -653,6 +661,28 @@ class LLVMIRGenerator: self.llbuilder = ll.IRBuilder() llblock_map = {} + # this is the predecessor map, from basic block to the set of its + # predecessors + # handling for branch and cbranch is here, and the handling of + # indirectbr and landingpad are in their respective process_* + # function + self.llpred_map = llpred_map = {} + branch_fn = self.llbuilder.branch + cbranch_fn = self.llbuilder.cbranch + def override_branch(block): + nonlocal self, branch_fn + self.add_pred(self.llbuilder.basic_block, block) + return branch_fn(block) + + def override_cbranch(pred, bbif, bbelse): + nonlocal self, cbranch_fn + self.add_pred(self.llbuilder.basic_block, bbif) + self.add_pred(self.llbuilder.basic_block, bbelse) + return cbranch_fn(pred, bbif, bbelse) + + self.llbuilder.branch = override_branch + self.llbuilder.cbranch = override_cbranch + if not func.is_generated: lldisubprogram = self.debug_info_emitter.emit_subprogram(func, self.llfunction) self.llfunction.set_metadata('dbg', lldisubprogram) @@ -675,6 +705,10 @@ class LLVMIRGenerator: # Third, translate all instructions. for block in func.basic_blocks: self.llbuilder.position_at_end(self.llmap[block]) + old_block = None + if len(block.instructions) == 1 and \ + isinstance(block.instructions[0], ir.LandingPad): + old_block = self.llbuilder.basic_block for insn in block.instructions: if insn.loc is not None and not func.is_generated: self.llbuilder.debug_metadata = \ @@ -689,12 +723,28 @@ class LLVMIRGenerator: # instruction so that the result spans several LLVM basic # blocks. This only really matters for phis, which are thus # using a different map (the following one). - llblock_map[block] = self.llbuilder.basic_block + if old_block is None: + llblock_map[block] = self.llbuilder.basic_block + else: + llblock_map[block] = old_block # Fourth, add incoming values to phis. for phi, llphi in self.phis: for value, block in phi.incoming(): - llphi.add_incoming(self.map(value), llblock_map[block]) + if isinstance(phi.type, builtins.TException): + # a hack to patch phi from landingpad + # because landingpad is a single bb in artiq IR, but + # generates multiple bb, we need to find out the + # predecessor to figure out the actual bb + landingpad = llblock_map[block] + for pred in llpred_map[llphi.parent]: + if pred in llpred_map and landingpad in llpred_map[pred]: + llphi.add_incoming(self.map(value), pred) + break + else: + llphi.add_incoming(self.map(value), landingpad) + else: + llphi.add_incoming(self.map(value), llblock_map[block]) finally: self.function_flags = None self.llfunction = None @@ -1247,6 +1297,8 @@ class LLVMIRGenerator: return llstore_lo else: return self.llbuilder.call(self.llbuiltin("delay_mu"), [llinterval]) + elif insn.op == "end_catch": + return self.llbuilder.call(self.llbuiltin("__artiq_end_catch"), []) else: assert False @@ -1678,7 +1730,12 @@ class LLVMIRGenerator: def process_IndirectBranch(self, insn): llinsn = self.llbuilder.branch_indirect(self.map(insn.target())) for dest in insn.destinations(): - llinsn.add_destination(self.map(dest)) + dest = self.map(dest) + self.add_pred(self.llbuilder.basic_block, dest) + if dest not in self.llpred_map: + self.llpred_map[dest] = set() + self.llpred_map[dest].add(self.llbuilder.basic_block) + llinsn.add_destination(dest) return llinsn def process_Return(self, insn): @@ -1716,8 +1773,8 @@ class LLVMIRGenerator: llexn = self.map(insn.value()) return self._gen_raise(insn, self.llbuiltin("__artiq_raise"), [llexn]) - def process_Reraise(self, insn): - return self._gen_raise(insn, self.llbuiltin("__artiq_reraise"), []) + def process_Resume(self, insn): + return self._gen_raise(insn, self.llbuiltin("__artiq_resume"), []) def process_LandingPad(self, insn): # Layout on return from landing pad: {%_Unwind_Exception*, %Exception*} @@ -1726,10 +1783,11 @@ class LLVMIRGenerator: cleanup=insn.has_cleanup) llrawexn = self.llbuilder.extract_value(lllandingpad, 1) llexn = self.llbuilder.bitcast(llrawexn, self.llty_of_type(insn.type)) - llexnnameptr = self.llbuilder.gep(llexn, [self.llindex(0), self.llindex(0)], + llexnidptr = self.llbuilder.gep(llexn, [self.llindex(0), self.llindex(0)], inbounds=True) - llexnname = self.llbuilder.load(llexnnameptr) + llexnid = self.llbuilder.load(llexnidptr) + landingpadbb = self.llbuilder.basic_block for target, typ in insn.clauses(): if typ is None: # we use a null pointer here, similar to how cpp does it @@ -1742,42 +1800,40 @@ class LLVMIRGenerator: ll.Constant(lli32, 0).inttoptr(llptr) ) ) - else: - exnname = "{}:{}".format(typ.id, typ.name) - llclauseexnname = self.llconst_of_const( - ir.Constant(exnname, builtins.TStr())) - llclauseexnnameptr = self.llmodule.globals.get("exn.{}".format(exnname)) - if llclauseexnnameptr is None: - llclauseexnnameptr = ll.GlobalVariable(self.llmodule, llclauseexnname.type, - name="exn.{}".format(exnname)) - llclauseexnnameptr.global_constant = True - llclauseexnnameptr.initializer = llclauseexnname - llclauseexnnameptr.linkage = "private" - llclauseexnnameptr.unnamed_addr = True - lllandingpad.add_clause(ll.CatchClause(llclauseexnnameptr)) - - if typ is None: # typ is None means that we match all exceptions, so no need to # compare - self.llbuilder.branch(self.map(target)) + target = self.map(target) + self.add_pred(landingpadbb, target) + self.add_pred(landingpadbb, self.llbuilder.basic_block) + self.llbuilder.branch(target) else: - llexnlen = self.llbuilder.extract_value(llexnname, 1) - llclauseexnlen = self.llbuilder.extract_value(llclauseexnname, 1) - llmatchinglen = self.llbuilder.icmp_unsigned('==', llexnlen, llclauseexnlen) - with self.llbuilder.if_then(llmatchinglen): - llexnptr = self.llbuilder.extract_value(llexnname, 0) - llclauseexnptr = self.llbuilder.extract_value(llclauseexnname, 0) - llcomparedata = self.llbuilder.call(self.llbuiltin("memcmp"), - [llexnptr, llclauseexnptr, llexnlen]) - llmatchingdata = self.llbuilder.icmp_unsigned('==', llcomparedata, - ll.Constant(lli32, 0)) - with self.llbuilder.if_then(llmatchingdata): - self.llbuilder.branch(self.map(target)) + exnname = "{}:{}".format(typ.id, typ.name) + llclauseexnidptr = self.llmodule.globals.get("exn.{}".format(exnname)) + exnid = ll.Constant(lli32, self.embedding_map.store_str(exnname)) + if llclauseexnidptr is None: + llclauseexnidptr = ll.GlobalVariable(self.llmodule, lli32, + name="exn.{}".format(exnname)) + llclauseexnidptr.global_constant = True + llclauseexnidptr.initializer = exnid + llclauseexnidptr.linkage = "private" + llclauseexnidptr.unnamed_addr = True + lllandingpad.add_clause(ll.CatchClause(llclauseexnidptr)) + llmatchingdata = self.llbuilder.icmp_unsigned("==", llexnid, + exnid) + with self.llbuilder.if_then(llmatchingdata): + target = self.map(target) + self.add_pred(landingpadbb, target) + self.add_pred(landingpadbb, self.llbuilder.basic_block) + self.llbuilder.branch(target) + self.add_pred(landingpadbb, self.llbuilder.basic_block) if self.llbuilder.basic_block.terminator is None: if insn.has_cleanup: - self.llbuilder.branch(self.map(insn.cleanup())) + target = self.map(insn.cleanup()) + self.add_pred(landingpadbb, target) + self.add_pred(landingpadbb, self.llbuilder.basic_block) + self.llbuilder.branch(target) else: self.llbuilder.resume(lllandingpad)