Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic support for try except #4902

Merged
merged 51 commits into from
Dec 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
440f8d7
Add bytecode support for simple try/except for the catch all case.
sklam Nov 13, 2019
8c4d8b6
Fix stack adjustment for setup except/finally
sklam Nov 13, 2019
b39d31f
Make each bytecode statement branch and check for exception raised
sklam Nov 13, 2019
28fb37f
Minimal support for raise and catch
sklam Nov 20, 2019
92e25ed
Check whether we are in a try block at runtime
sklam Nov 20, 2019
9bc4f53
Proper cleanup of try-block
sklam Nov 20, 2019
b15e371
Add tests
sklam Nov 20, 2019
c342569
Support nested try
sklam Nov 20, 2019
419609f
More testing
sklam Nov 20, 2019
0b9147d
Add test with loops
sklam Nov 20, 2019
5eba76b
Add TryRaise and StaticTryRaise IR node to handle statements that
sklam Nov 22, 2019
0ccad8d
STASH
sklam Nov 22, 2019
7c26ec0
Stronger typing for block kinds
sklam Nov 26, 2019
3594865
Working on exception catching
sklam Nov 27, 2019
5f188e6
Support catching exactly Exception classes
sklam Nov 27, 2019
ca5548d
Fix withlifting
sklam Nov 27, 2019
9b2b035
Fix loop break
sklam Nov 27, 2019
c8b5686
Fix test failure due to unsupported try-except
sklam Dec 2, 2019
f2e4a72
Add test for nested try-except-finally
sklam Dec 2, 2019
a118a7c
Remove old try-except test in test_flow_control.py
sklam Dec 2, 2019
bee41ca
Test try except with refcounted types
sklam Dec 3, 2019
a239196
Test try-except in generator and with-objmode
sklam Dec 3, 2019
9e7eb24
Test try-except with parfors
sklam Dec 3, 2019
d7d8489
Add more skips
sklam Dec 6, 2019
a86e756
Cleaning up and docstrings
sklam Dec 6, 2019
359d1b7
Skip test
sklam Dec 6, 2019
a5e355e
More cleanup
sklam Dec 6, 2019
ce77574
Remove local import
sklam Dec 6, 2019
8a8fede
some more tests
stuartarchibald Dec 9, 2019
f92feaa
Detect unsupport MAKE_FUNCTION use
sklam Dec 9, 2019
8ab1ab6
Make PHI propagate unique definitions
sklam Dec 9, 2019
4b1c261
Fix up tests
sklam Dec 9, 2019
e8b7ac6
Fix problem with empty try-block
sklam Dec 9, 2019
ccc1827
Remove the unreachable return that is upsetting py2
sklam Dec 9, 2019
a815de1
Revert "Make PHI propagate unique definitions"
sklam Dec 9, 2019
72182fd
Mark unsupport test to have closure inside try-except
sklam Dec 9, 2019
b0d529e
Skip test the needs scipy.
sklam Dec 9, 2019
316f3b1
Test and fix problem with type refinement
sklam Dec 10, 2019
79fb508
Forbidden storing of exception objects
sklam Dec 10, 2019
2664fcb
Ban reraise in except
sklam Dec 11, 2019
4f80bb5
Report the user variable
sklam Dec 11, 2019
fe4ef1f
Refactor similar code
sklam Dec 11, 2019
6bf1a1c
Apply suggestions from code review
sklam Dec 11, 2019
38d1a7a
Update numba/byteflow.py
sklam Dec 11, 2019
f127335
Test hack to fix old-style implementation's exception propagation
sklam Dec 11, 2019
04f1c08
Remove dead code
sklam Dec 11, 2019
c1bb22f
Ban reraise in py38 properly
sklam Dec 11, 2019
6af3ef1
Fix issue with splitting LOAD_CONST.
sklam Dec 11, 2019
0a71334
Apply review suggestion
sklam Dec 11, 2019
a0b8d2b
Merge branch 'master' into enh/tryexcept
sklam Dec 12, 2019
7f66bbe
Apply suggestions from code review
sklam Dec 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
313 changes: 273 additions & 40 deletions numba/byteflow.py

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions numba/controlflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,12 +661,17 @@ def run(self):
fn = getattr(self, fname, None)
if fn is not None:
fn(inst)
else:
elif inst.is_jump:
# this catches e.g. try... except
if inst.is_jump:
l = Loc(self.bytecode.func_id.filename, inst.lineno)
l = Loc(self.bytecode.func_id.filename, inst.lineno)
if inst.opname in {"SETUP_EXCEPT", "SETUP_FINALLY"}:
msg = "'try' block not supported until python3.7 or later"
else:
msg = "Use of unsupported opcode (%s) found" % inst.opname
raise UnsupportedError(msg, loc=l)
raise UnsupportedError(msg, loc=l)
else:
# Non-jump instructions are ignored
pass # intentionally

# Close all blocks
for cur, nxt in zip(self.blockseq, self.blockseq[1:]):
Expand Down
8 changes: 8 additions & 0 deletions numba/dataflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,3 +917,11 @@ def terminator(self):
@terminator.setter
def terminator(self, inst):
self._term = inst

@property
def active_try_block(self):
"""Try except not supported.

See byteflow.py
"""
return None
179 changes: 168 additions & 11 deletions numba/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
OPERATORS_TO_BUILTINS,
)
from numba.byteflow import Flow, AdaptDFA, AdaptCFA
from numba.unsafe import eh


_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -86,6 +87,9 @@ def __init__(self, func_id):
self.blocks = {}
# { name: [definitions] } of local variables
self.definitions = collections.defaultdict(list)
# A set to keep track of all exception variables.
# To be used in _legalize_exception_vars()
self._exception_vars = set()

def interpret(self, bytecode):
"""
Expand Down Expand Up @@ -127,12 +131,37 @@ def interpret(self, bytecode):
for inst, kws in self._iter_inst():
self._dispatch(inst, kws)

self._legalize_exception_vars()

# Prepare FunctionIR
fir = ir.FunctionIR(self.blocks, self.is_generator, self.func_id,
self.first_loc, self.definitions,
self.arg_count, self.arg_names)
_logger.debug(fir.dump_to_string())
return fir

def _legalize_exception_vars(self):
"""Search for unsupported use of exception variables.
Note, they cannot be stored into user variable.
"""
# Build a set of exception variables
excvars = self._exception_vars.copy()
# Propagate the exception variables to LHS of assignment
for varname, defnvars in self.definitions.items():
for v in defnvars:
if isinstance(v, ir.Var):
k = v.name
if k in excvars:
excvars.add(varname)
# Filter out the user variables.
uservar = list(filter(lambda x: not x.startswith('$'), excvars))
if uservar:
# Complain about the first user-variable storing an exception
first = uservar[0]
loc = self.current_scope.get(first).loc
msg = "Exception object cannot be stored into variable ({})."
raise errors.UnsupportedError(msg.format(first), loc=loc)

def init_first_block(self):
# Define variables receiving the function arguments
for index, name in enumerate(self.arg_names):
Expand All @@ -158,8 +187,24 @@ def _start_new_block(self, offset):
self.insert_block(offset)
# Ensure the last block is terminated
if oldblock is not None and not oldblock.is_terminated:
jmp = ir.Jump(offset, loc=self.loc)
oldblock.append(jmp)
# Handle ending try block.
tryblk = self.dfainfo.active_try_block
# If there's an active try-block and the handler block is live.
if tryblk is not None and tryblk['end'] in self.cfa.graph.nodes():
# We are in a try-block, insert a branch to except-block.
# This logic cannot be in self._end_current_block()
# because we the non-raising next block-offset.
branch = ir.Branch(
cond=self.get('$exception_check'),
truebr=tryblk['end'],
falsebr=offset,
loc=self.loc,
)
oldblock.append(branch)
# Handle normal case
else:
jmp = ir.Jump(offset, loc=self.loc)
oldblock.append(jmp)
# Get DFA block info
self.dfainfo = self.dfa.infos[self.current_block_offset]
self.assigner = Assigner()
Expand All @@ -171,9 +216,74 @@ def _start_new_block(self, offset):
break

def _end_current_block(self):
# Handle try block
if not self.current_block.is_terminated:
tryblk = self.dfainfo.active_try_block
if tryblk is not None:
self._insert_exception_check()
# Handle normal block cleanup
self._remove_unused_temporaries()
self._insert_outgoing_phis()

def _inject_call(self, func, gv_name, res_name=None):
"""A helper function to inject a call to *func* which is a python
function.

Parameters
----------
func : callable
The function object to be called.
gv_name : str
The variable name to be used to store the function object.
res_name : str; optional
The variable name to be used to store the call result.
If ``None``, a name is created automatically.
"""
gv_fn = ir.Global(gv_name, func, loc=self.loc)
self.store(value=gv_fn, name=gv_name, redefine=True)
callres = ir.Expr.call(self.get(gv_name), (), (), loc=self.loc)
res_name = res_name or '$callres_{}'.format(gv_name)
self.store(value=callres, name=res_name, redefine=True)

def _insert_try_block_begin(self):
"""Insert IR-nodes to mark the start of a `try` block.
"""
self._inject_call(eh.mark_try_block, 'mark_try_block')

def _insert_try_block_end(self):
"""Insert IR-nodes to mark the end of a `try` block.
"""
self._inject_call(eh.end_try_block, 'end_try_block')

def _insert_exception_variables(self):
"""Insert IR-nodes to initialize the exception variables.
"""
tryblk = self.dfainfo.active_try_block
# Get exception variables
endblk = tryblk['end']
edgepushed = self.dfainfo.outgoing_edgepushed.get(endblk)
# Note: the last value on the stack is the exception value
# Note: due to the current limitation, all exception variables are None
if edgepushed:
scope = self.current_scope
const_none = ir.Const(value=None, loc=self.loc)
# For each variable going to the handler block.
for var in edgepushed:
if var in self.definitions:
raise AssertionError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this path reached? Is it tested?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's me saying it should be unreachable. But I am not sure.

"exception variable CANNOT be defined by other code",
)
self.store(value=const_none, name=var)
self._exception_vars.add(var)

def _insert_exception_check(self):
"""Called before the end of a block to inject checks if raised.
"""
self._insert_exception_variables()
# Do exception check
self._inject_call(eh.exception_check, 'exception_check',
'$exception_check')

def _remove_unused_temporaries(self):
"""
Remove assignments to unused temporary variables from the
Expand Down Expand Up @@ -580,6 +690,10 @@ def op_STORE_FAST(self, inst, value):
value = self.get(value)
self.store(value=value, name=dstname)

def op_DELETE_FAST(self, inst):
dstname = self.code_locals[inst.arg]
self.current_block.append(ir.Del(dstname, loc=self.loc))

def op_DUP_TOPX(self, inst, orig, duped):
for src, dst in zip(orig, duped):
self.store(value=self.get(src), name=dst)
Expand Down Expand Up @@ -651,6 +765,13 @@ def op_SETUP_WITH(self, inst, contextmanager):
begin=inst.offset, end=exitpt, loc=self.loc,
))

def op_SETUP_EXCEPT(self, inst):
# Removed since python3.8
self._insert_try_block_begin()

def op_SETUP_FINALLY(self, inst):
self._insert_try_block_begin()

def op_WITH_CLEANUP(self, inst):
"no-op"

Expand All @@ -663,10 +784,13 @@ def op_WITH_CLEANUP_FINISH(self, inst):
def op_END_FINALLY(self, inst):
"no-op"

def op_BEGIN_FINALLY(self, inst, state):
"no-op"
none = ir.Const(None, loc=self.loc)
self.store(value=none, name=state)
def op_BEGIN_FINALLY(self, inst, temps):
# The *temps* are the exception variables
const_none = ir.Const(None, loc=self.loc)
for tmp in temps:
# Set to None for now
self.store(const_none, name=tmp)
self._exception_vars.add(tmp)

if PYVERSION < (3, 6):

Expand Down Expand Up @@ -961,8 +1085,11 @@ def op_JUMP_FORWARD(self, inst):
jmp = ir.Jump(inst.get_jump_target(), loc=self.loc)
self.current_block.append(jmp)

def op_POP_BLOCK(self, inst):
self.syntax_blocks.pop()
def op_POP_BLOCK(self, inst, kind=None):
if kind is None:
self.syntax_blocks.pop()
elif kind == 'try':
self._insert_try_block_end()

def op_RETURN_VALUE(self, inst, retval, castval):
self.store(ir.Expr.cast(self.get(retval), loc=self.loc), castval)
Expand All @@ -979,6 +1106,18 @@ def op_COMPARE_OP(self, inst, lhs, rhs, res):
tmp = self.get(res)
out = ir.Expr.unary('not', value=tmp, loc=self.loc)
self.store(out, res)
elif op == 'exception match':
gv_fn = ir.Global(
"exception_match", eh.exception_match, loc=self.loc,
)
exc_match_name = '$exc_match'
self.store(value=gv_fn, name=exc_match_name, redefine=True)
lhs = self.get(lhs)
rhs = self.get(rhs)
exc = ir.Expr.call(
self.get(exc_match_name), args=(lhs, rhs), kws=(), loc=self.loc,
)
self.store(exc, res)
else:
self._binop(op, lhs, rhs, res)

Expand Down Expand Up @@ -1022,8 +1161,17 @@ def op_JUMP_IF_TRUE_OR_POP(self, inst, pred):
def op_RAISE_VARARGS(self, inst, exc):
if exc is not None:
exc = self.get(exc)
stmt = ir.Raise(exception=exc, loc=self.loc)
self.current_block.append(stmt)
tryblk = self.dfainfo.active_try_block
if tryblk is not None:
# In a try block
stmt = ir.TryRaise(exception=exc, loc=self.loc)
self.current_block.append(stmt)
self._insert_try_block_end()
self.current_block.append(ir.Jump(tryblk['end'], loc=self.loc))
else:
# Not in a try block
stmt = ir.Raise(exception=exc, loc=self.loc)
self.current_block.append(stmt)

def op_YIELD_VALUE(self, inst, value, res):
# initialize index to None. it's being set later in post-processing
Expand All @@ -1041,7 +1189,16 @@ def op_MAKE_FUNCTION(self, inst, name, code, closure, annotations, kwdefaults, d
defaults = tuple([self.get(name) for name in defaults])
else:
defaults = self.get(defaults)
fcode = self.definitions[code][0].value

assume_code_const = self.definitions[code][0]
if not isinstance(assume_code_const, ir.Const):
msg = (
"Unsupported use of closure. "
"Probably caused by complex control-flow constructs; "
"e.g. try-except"
)
raise errors.UnsupportedError(msg, loc=self.loc)
fcode = assume_code_const.value
if name:
name = self.get(name)
if closure:
Expand Down
37 changes: 37 additions & 0 deletions numba/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,43 @@ def get_targets(self):
return []


class TryRaise(Stmt):
"""A raise statement inside a try-block
Similar to ``Raise`` but does not terminate.
"""
def __init__(self, exception, loc):
assert exception is None or isinstance(exception, Var)
assert isinstance(loc, Loc)
self.exception = exception
self.loc = loc

def __str__(self):
return "try_raise %s" % self.exception


class StaticTryRaise(Stmt):
"""A raise statement inside a try-block.
Similar to ``StaticRaise`` but does not terminate.
"""

def __init__(self, exc_class, exc_args, loc):
assert exc_class is None or isinstance(exc_class, type)
assert isinstance(loc, Loc)
assert exc_args is None or isinstance(exc_args, tuple)
self.exc_class = exc_class
self.exc_args = exc_args
self.loc = loc

def __str__(self):
if self.exc_class is None:
return "static_try_raise"
elif self.exc_args is None:
return "static_try_raise %s" % (self.exc_class,)
else:
return "static_try_raise %s(%s)" % (self.exc_class,
", ".join(map(repr, self.exc_args)))


class Return(Terminator):
"""
Return to caller.
Expand Down