compiler: fixed try codegen and allocate exceptions

Exceptions are now allocated in the runtime when we raise the exception,
and destroyed when we exit the catch block. Nested exception and try
block is now supported, and should behave the same as in CPython.
Exceptions raised in except blocks will now unwind through finally
blocks, matching the behavior in CPython. Reraise will now preserve
backtrace.

Phi block LLVM IR generation is modified to handle landingpads, which
one ARTIQ IR will map to multiple LLVM IR.
This commit is contained in:
pca006132 2022-01-23 21:12:12 +08:00 committed by Sébastien Bourdeauducq
parent 4644e105b1
commit da4ff44377
3 changed files with 261 additions and 110 deletions

View File

@ -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:

View File

@ -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

View File

@ -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)