import builtins import collections import dis import operator import logging from numba.core import errors, dataflow, controlflow, ir, config from numba.core.errors import NotDefinedError, error_extras from numba.core.utils import (PYVERSION, BINOPS_TO_OPERATORS, INPLACE_BINOPS_TO_OPERATORS,) from numba.core.byteflow import Flow, AdaptDFA, AdaptCFA from numba.core.unsafe import eh from numba.cpython.unsafe.tuple import unpack_single_tuple class _UNKNOWN_VALUE(object): """Represents an unknown value, this is for ease of debugging purposes only. """ def __init__(self, varname): self._varname = varname def __repr__(self): return "_UNKNOWN_VALUE({})".format(self._varname) _logger = logging.getLogger(__name__) class Assigner(object): """ This object keeps track of potential assignment simplifications inside a code block. For example `$O.1 = x` followed by `y = $0.1` can be simplified into `y = x`, but it's not possible anymore if we have `x = z` in-between those two instructions. NOTE: this is not only an optimization, but is actually necessary due to certain limitations of Numba - such as only accepting the returning of an array passed as function argument. """ def __init__(self): # { destination variable name -> source Var object } self.dest_to_src = {} # Basically a reverse mapping of dest_to_src: # { source variable name -> all destination names in dest_to_src } self.src_invalidate = collections.defaultdict(list) self.unused_dests = set() def assign(self, srcvar, destvar): """ Assign *srcvar* to *destvar*. Return either *srcvar* or a possible simplified assignment source (earlier assigned to *srcvar*). """ srcname = srcvar.name destname = destvar.name if destname in self.src_invalidate: # destvar will change, invalidate all previously known # simplifications for d in self.src_invalidate.pop(destname): self.dest_to_src.pop(d) if srcname in self.dest_to_src: srcvar = self.dest_to_src[srcname] if destvar.is_temp: self.dest_to_src[destname] = srcvar self.src_invalidate[srcname].append(destname) self.unused_dests.add(destname) return srcvar def get_assignment_source(self, destname): """ Get a possible assignment source (a ir.Var instance) to replace *destname*, otherwise None. """ if destname in self.dest_to_src: return self.dest_to_src[destname] self.unused_dests.discard(destname) return None def peep_hole_list_to_tuple(func_ir): """ This peephole rewrites a bytecode sequence new to Python 3.9 that looks like e.g.: def foo(a): return (*a,) 41 0 BUILD_LIST 0 2 LOAD_FAST 0 (a) 4 LIST_EXTEND 1 6 LIST_TO_TUPLE 8 RETURN_VAL essentially, the unpacking of tuples is written as a list which is appended to/extended and then "magicked" into a tuple by the new LIST_TO_TUPLE opcode. This peephole repeatedly analyses the bytecode in a block looking for a window between a `LIST_TO_TUPLE` and `BUILD_LIST` and... 1. Turns the BUILD_LIST into a BUILD_TUPLE 2. Sets an accumulator's initial value as the target of the BUILD_TUPLE 3. Searches for 'extend' on the original list and turns these into binary additions on the accumulator. 4. Searches for 'append' on the original list and turns these into a `BUILD_TUPLE` which is then appended via binary addition to the accumulator. 5. Assigns the accumulator to the variable that exits the peephole and the rest of the block/code refers to as the result of the unpack operation. 6. Patches up """ _DEBUG = False # For all blocks for offset, blk in func_ir.blocks.items(): # keep doing the peephole rewrite until nothing is left that matches while True: # first try and find a matching region # i.e. BUILD_LIST......LIST_TO_TUPLE def find_postive_region(): found = False for idx in reversed(range(len(blk.body))): stmt = blk.body[idx] if isinstance(stmt, ir.Assign): value = stmt.value if (isinstance(value, ir.Expr) and value.op == 'list_to_tuple'): target_list = value.info[0] found = True bt = (idx, stmt) if found: if isinstance(stmt, ir.Assign): if stmt.target.name == target_list: region = (bt, (idx, stmt)) return region region = find_postive_region() # if there's a peep hole region then do something with it if region is not None: peep_hole = blk.body[region[1][0] : region[0][0]] if _DEBUG: print("\nWINDOW:") for x in peep_hole: print(x) print("") appends = [] extends = [] init = region[1][1] const_list = init.target.name # Walk through the peep_hole and find things that are being # "extend"ed and "append"ed to the BUILD_LIST for x in peep_hole: if isinstance(x, ir.Assign): if isinstance(x.value, ir.Expr): expr = x.value if (expr.op == 'getattr' and expr.value.name == const_list): # it's not strictly necessary to split out # extends and appends, but it helps with # debugging to do so! if expr.attr == 'extend': extends.append(x.target.name) elif expr.attr == 'append': appends.append(x.target.name) else: assert 0 # go back through the peep hole build new IR based on it. new_hole = [] def append_and_fix(x): """ Adds to the new_hole and fixes up definitions""" new_hole.append(x) if x.target.name in func_ir._definitions: # if there's already a definition, drop it, should only # be 1 as the way cpython emits the sequence for # `list_to_tuple` should ensure this. assert len(func_ir._definitions[x.target.name]) == 1 func_ir._definitions[x.target.name].clear() func_ir._definitions[x.target.name].append(x.value) the_build_list = init.target # Do the transform on the peep hole if _DEBUG: print("\nBLOCK:") blk.dump() # This section basically accumulates list appends and extends # as binop(+) on tuples, it drops all the getattr() for extend # and append as they are now dead and replaced with binop(+). # It also switches out the build_list for a build_tuple and then # ensures everything is wired up and defined ok. t2l_agn = region[0][1] acc = the_build_list for x in peep_hole: if isinstance(x, ir.Assign): if isinstance(x.value, ir.Expr): expr = x.value if expr.op == 'getattr': if (x.target.name in extends or x.target.name in appends): # drop definition, it's being wholesale # replaced. func_ir._definitions.pop(x.target.name) continue else: # a getattr on something we're not # interested in new_hole.append(x) elif expr.op == 'call': fname = expr.func.name if fname in extends or fname in appends: arg = expr.args[0] if isinstance(arg, ir.Var): tmp_name = "%s_var_%s" % (fname, arg.name) if fname in appends: bt = ir.Expr.build_tuple([arg,], expr.loc) else: bt = arg var = ir.Var(arg.scope, tmp_name, expr.loc) asgn = ir.Assign(bt, var, expr.loc) append_and_fix(asgn) arg = var # this needs to be a binary add new = ir.Expr.binop(fn=operator.add, lhs=acc, rhs=arg, loc=x.loc) asgn = ir.Assign(new, x.target, expr.loc) append_and_fix(asgn) acc = asgn.target else: # there could be a call in the unpack, like # *(a, x.append(y)) new_hole.append(x) elif (expr.op == 'build_list' and x.target.name == const_list): new = ir.Expr.build_tuple(expr.items, expr.loc) asgn = ir.Assign(new, x.target, expr.loc) # Not a temporary any more append_and_fix(asgn) else: new_hole.append(x) else: new_hole.append(x) else: # stick everything else in as-is new_hole.append(x) # Finally write the result back into the original build list as # everything refers to it. new_hole.append(ir.Assign(acc, t2l_agn.target, the_build_list.loc)) if _DEBUG: print("\nNEW HOLE:") for x in new_hole: print(x) # and then update the block body with the modified region cpy = blk.body[:] head = cpy[:region[1][0]] tail = blk.body[region[0][0] + 1:] tmp = head + new_hole + tail blk.body.clear() blk.body.extend(tmp) if _DEBUG: print("\nDUMP post hole:") blk.dump() else: # else escape break return func_ir def peep_hole_delete_with_exit(func_ir): """ This rewrite removes variables used to store the `__exit__` function loaded by SETUP_WITH. """ dead_vars = set() for blk in func_ir.blocks.values(): for stmt in blk.body: # Any statement that uses a variable with the '$setup_with_exitfn' # prefix is considered dead. used = set(stmt.list_vars()) for v in used: if v.name.startswith('$setup_with_exitfn'): dead_vars.add(v) # Any assignment that uses any of the dead variable is considered # dead. if used & dead_vars: if isinstance(stmt, ir.Assign): dead_vars.add(stmt.target) new_body = [] for stmt in blk.body: # Skip any statements that uses anyone of the dead variable. if not (set(stmt.list_vars()) & dead_vars): new_body.append(stmt) blk.body.clear() blk.body.extend(new_body) return func_ir class Interpreter(object): """A bytecode interpreter that builds up the IR. """ def __init__(self, func_id): self.func_id = func_id self.arg_count = func_id.arg_count self.arg_names = func_id.arg_names self.loc = self.first_loc = ir.Loc.from_function_id(func_id) self.is_generator = func_id.is_generator # { inst offset : ir.Block } 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): """ Generate IR for this bytecode. """ self.bytecode = bytecode self.scopes = [] global_scope = ir.Scope(parent=None, loc=self.loc) self.scopes.append(global_scope) if PYVERSION < (3, 7): # Control flow analysis self.cfa = controlflow.ControlFlowAnalysis(bytecode) self.cfa.run() if config.DUMP_CFG: self.cfa.dump() # Data flow analysis self.dfa = dataflow.DataFlowAnalysis(self.cfa) self.dfa.run() else: flow = Flow(bytecode) flow.run() self.dfa = AdaptDFA(flow) self.cfa = AdaptCFA(flow) if config.DUMP_CFG: self.cfa.dump() # Temp states during interpretation self.current_block = None self.current_block_offset = None self.syntax_blocks = [] self.dfainfo = None self.scopes.append(ir.Scope(parent=self.current_scope, loc=self.loc)) # Interpret loop for inst, kws in self._iter_inst(): self._dispatch(inst, kws) self._legalize_exception_vars() # Prepare FunctionIR func_ir = ir.FunctionIR(self.blocks, self.is_generator, self.func_id, self.first_loc, self.definitions, self.arg_count, self.arg_names) _logger.debug(func_ir.dump_to_string()) # post process the IR to rewrite opcodes/byte sequences that are too # involved to risk handling as part of direct interpretation peepholes = [] if PYVERSION in [(3, 9), (3, 10)]: peepholes.append(peep_hole_list_to_tuple) peepholes.append(peep_hole_delete_with_exit) post_processed_ir = self.post_process(peepholes, func_ir) return post_processed_ir def post_process(self, peepholes, func_ir): for peep in peepholes: func_ir = peep(func_ir) return func_ir 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): val = ir.Arg(index=index, name=name, loc=self.loc) self.store(val, name) def _iter_inst(self): for blkct, block in enumerate(self.cfa.iterliveblocks()): firstinst = self.bytecode[block.offset] self.loc = self.loc.with_lineno(firstinst.lineno) self._start_new_block(block.offset) if blkct == 0: # Is first block self.init_first_block() for offset, kws in self.dfainfo.insts: inst = self.bytecode[offset] self.loc = self.loc.with_lineno(inst.lineno) yield inst, kws self._end_current_block() def _start_new_block(self, offset): oldblock = self.current_block self.insert_block(offset) # Ensure the last block is terminated if oldblock is not None and not oldblock.is_terminated: # 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() # Check out-of-scope syntactic-block while self.syntax_blocks: if offset >= self.syntax_blocks[-1].exit: self.syntax_blocks.pop() else: 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: 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( "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 current block. """ new_body = [] replaced_var = {} for inst in self.current_block.body: # the same temporary is assigned to multiple variables in cases # like a = b[i] = 1, so need to handle replaced temporaries in # later setitem/setattr nodes if (isinstance(inst, (ir.SetItem, ir.SetAttr)) and inst.value.name in replaced_var): inst.value = replaced_var[inst.value.name] elif isinstance(inst, ir.Assign): if (inst.target.is_temp and inst.target.name in self.assigner.unused_dests): continue # the same temporary is assigned to multiple variables in cases # like a = b = 1, so need to handle replaced temporaries in # later assignments if (isinstance(inst.value, ir.Var) and inst.value.name in replaced_var): inst.value = replaced_var[inst.value.name] new_body.append(inst) continue # chained unpack cases may reuse temporary # e.g. a = (b, c) = (x, y) if (isinstance(inst.value, ir.Expr) and inst.value.op == "exhaust_iter" and inst.value.value.name in replaced_var): inst.value.value = replaced_var[inst.value.value.name] new_body.append(inst) continue # eliminate temporary variables that are assigned to user # variables right after creation. E.g.: # $1 = f(); a = $1 -> a = f() # the temporary variable is not reused elsewhere since CPython # bytecode is stack-based and this pattern corresponds to a pop if (isinstance(inst.value, ir.Var) and inst.value.is_temp and new_body and isinstance(new_body[-1], ir.Assign)): prev_assign = new_body[-1] # _var_used_in_binop check makes sure we don't create a new # inplace binop operation which can fail # (see TestFunctionType.test_in_iter_func_call) if (prev_assign.target.name == inst.value.name and not self._var_used_in_binop( inst.target.name, prev_assign.value)): replaced_var[inst.value.name] = inst.target prev_assign.target = inst.target # replace temp var definition in target with proper defs self.definitions[inst.target.name].remove(inst.value) self.definitions[inst.target.name].extend( self.definitions.pop(inst.value.name) ) continue new_body.append(inst) self.current_block.body = new_body def _var_used_in_binop(self, varname, expr): """return True if 'expr' is a binary expression and 'varname' is used in it as an argument """ return (isinstance(expr, ir.Expr) and expr.op in ("binop", "inplace_binop") and (varname == expr.lhs.name or varname == expr.rhs.name)) def _insert_outgoing_phis(self): """ Add assignments to forward requested outgoing values to subsequent blocks. """ for phiname, varname in self.dfainfo.outgoing_phis.items(): target = self.current_scope.get_or_define(phiname, loc=self.loc) stmt = ir.Assign(value=self.get(varname), target=target, loc=self.loc) self.definitions[target.name].append(stmt.value) if not self.current_block.is_terminated: self.current_block.append(stmt) else: self.current_block.insert_before_terminator(stmt) def get_global_value(self, name): """ Get a global value from the func_global (first) or as a builtins (second). If both failed, return a ir.UNDEFINED. """ try: return self.func_id.func.__globals__[name] except KeyError: return getattr(builtins, name, ir.UNDEFINED) def get_closure_value(self, index): """ Get a value from the cell contained in this function's closure. If not set, return a ir.UNDEFINED. """ cell = self.func_id.func.__closure__[index] try: return cell.cell_contents except ValueError: return ir.UNDEFINED @property def current_scope(self): return self.scopes[-1] @property def code_consts(self): return self.bytecode.co_consts @property def code_locals(self): return self.bytecode.co_varnames @property def code_names(self): return self.bytecode.co_names @property def code_cellvars(self): return self.bytecode.co_cellvars @property def code_freevars(self): return self.bytecode.co_freevars def _dispatch(self, inst, kws): assert self.current_block is not None fname = "op_%s" % inst.opname.replace('+', '_') try: fn = getattr(self, fname) except AttributeError: raise NotImplementedError(inst) else: try: return fn(inst, **kws) except errors.NotDefinedError as e: if e.loc is None: loc = self.loc else: loc = e.loc err = errors.NotDefinedError(e.name, loc=loc) if not config.FULL_TRACEBACKS: raise err from None else: raise err # --- Scope operations --- def store(self, value, name, redefine=False): """ Store *value* (a Expr or Var instance) into the variable named *name* (a str object). Returns the target variable. """ if redefine or self.current_block_offset in self.cfa.backbone: rename = not (name in self.code_cellvars) target = self.current_scope.redefine(name, loc=self.loc, rename=rename) else: target = self.current_scope.get_or_define(name, loc=self.loc) if isinstance(value, ir.Var): value = self.assigner.assign(value, target) stmt = ir.Assign(value=value, target=target, loc=self.loc) self.current_block.append(stmt) self.definitions[target.name].append(value) return target def get(self, name): """ Get the variable (a Var instance) with the given *name*. """ # Implicit argument for comprehension starts with '.' # See Parameter class in inspect.py (from Python source) if name[0] == '.' and name[1:].isdigit(): name = 'implicit{}'.format(name[1:]) # Try to simplify the variable lookup by returning an earlier # variable assigned to *name*. var = self.assigner.get_assignment_source(name) if var is None: var = self.current_scope.get(name) return var # --- Block operations --- def insert_block(self, offset, scope=None, loc=None): scope = scope or self.current_scope loc = loc or self.loc blk = ir.Block(scope=scope, loc=loc) self.blocks[offset] = blk self.current_block = blk self.current_block_offset = offset return blk # --- Bytecode handlers --- def op_NOP(self, inst): pass def op_PRINT_ITEM(self, inst, item, printvar, res): item = self.get(item) printgv = ir.Global("print", print, loc=self.loc) self.store(value=printgv, name=printvar) call = ir.Expr.call(self.get(printvar), (item,), (), loc=self.loc) self.store(value=call, name=res) def op_PRINT_NEWLINE(self, inst, printvar, res): printgv = ir.Global("print", print, loc=self.loc) self.store(value=printgv, name=printvar) call = ir.Expr.call(self.get(printvar), (), (), loc=self.loc) self.store(value=call, name=res) def op_UNPACK_SEQUENCE(self, inst, iterable, stores, tupleobj): count = len(stores) # Exhaust the iterable into a tuple-like object tup = ir.Expr.exhaust_iter(value=self.get(iterable), loc=self.loc, count=count) self.store(name=tupleobj, value=tup) # then index the tuple-like object to extract the values for i, st in enumerate(stores): expr = ir.Expr.static_getitem(self.get(tupleobj), index=i, index_var=None, loc=self.loc) self.store(expr, st) def op_FORMAT_VALUE(self, inst, value, res, strvar): """ FORMAT_VALUE(flags): flags argument specifies format spec which is not supported yet. Currently, str() is simply called on the value. https://docs.python.org/3/library/dis.html#opcode-FORMAT_VALUE """ value = self.get(value) strgv = ir.Global("str", str, loc=self.loc) self.store(value=strgv, name=strvar) call = ir.Expr.call(self.get(strvar), (value,), (), loc=self.loc) self.store(value=call, name=res) def op_BUILD_STRING(self, inst, strings, tmps): """ BUILD_STRING(count): Concatenates count strings. Required for supporting f-strings. https://docs.python.org/3/library/dis.html#opcode-BUILD_STRING """ count = inst.arg # corner case: f"" if count == 0: const = ir.Const("", loc=self.loc) self.store(const, tmps[-1]) return prev = self.get(strings[0]) for other, tmp in zip(strings[1:], tmps): other = self.get(other) expr = ir.Expr.binop( operator.add, lhs=prev, rhs=other, loc=self.loc ) self.store(expr, tmp) prev = self.get(tmp) def op_BUILD_SLICE(self, inst, start, stop, step, res, slicevar): start = self.get(start) stop = self.get(stop) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) if step is None: sliceinst = ir.Expr.call(self.get(slicevar), (start, stop), (), loc=self.loc) else: step = self.get(step) sliceinst = ir.Expr.call(self.get(slicevar), (start, stop, step), (), loc=self.loc) self.store(value=sliceinst, name=res) def op_SLICE_0(self, inst, base, res, slicevar, indexvar, nonevar): base = self.get(base) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) index = ir.Expr.call(self.get(slicevar), (none, none), (), loc=self.loc) self.store(value=index, name=indexvar) expr = ir.Expr.getitem(base, self.get(indexvar), loc=self.loc) self.store(value=expr, name=res) def op_SLICE_1(self, inst, base, start, nonevar, res, slicevar, indexvar): base = self.get(base) start = self.get(start) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (start, none), (), loc=self.loc) self.store(value=index, name=indexvar) expr = ir.Expr.getitem(base, self.get(indexvar), loc=self.loc) self.store(value=expr, name=res) def op_SLICE_2(self, inst, base, nonevar, stop, res, slicevar, indexvar): base = self.get(base) stop = self.get(stop) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (none, stop,), (), loc=self.loc) self.store(value=index, name=indexvar) expr = ir.Expr.getitem(base, self.get(indexvar), loc=self.loc) self.store(value=expr, name=res) def op_SLICE_3(self, inst, base, start, stop, res, slicevar, indexvar): base = self.get(base) start = self.get(start) stop = self.get(stop) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (start, stop), (), loc=self.loc) self.store(value=index, name=indexvar) expr = ir.Expr.getitem(base, self.get(indexvar), loc=self.loc) self.store(value=expr, name=res) def op_STORE_SLICE_0(self, inst, base, value, slicevar, indexvar, nonevar): base = self.get(base) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) index = ir.Expr.call(self.get(slicevar), (none, none), (), loc=self.loc) self.store(value=index, name=indexvar) stmt = ir.SetItem(base, self.get(indexvar), self.get(value), loc=self.loc) self.current_block.append(stmt) def op_STORE_SLICE_1(self, inst, base, start, nonevar, value, slicevar, indexvar): base = self.get(base) start = self.get(start) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (start, none), (), loc=self.loc) self.store(value=index, name=indexvar) stmt = ir.SetItem(base, self.get(indexvar), self.get(value), loc=self.loc) self.current_block.append(stmt) def op_STORE_SLICE_2(self, inst, base, nonevar, stop, value, slicevar, indexvar): base = self.get(base) stop = self.get(stop) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (none, stop,), (), loc=self.loc) self.store(value=index, name=indexvar) stmt = ir.SetItem(base, self.get(indexvar), self.get(value), loc=self.loc) self.current_block.append(stmt) def op_STORE_SLICE_3(self, inst, base, start, stop, value, slicevar, indexvar): base = self.get(base) start = self.get(start) stop = self.get(stop) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (start, stop), (), loc=self.loc) self.store(value=index, name=indexvar) stmt = ir.SetItem(base, self.get(indexvar), self.get(value), loc=self.loc) self.current_block.append(stmt) def op_DELETE_SLICE_0(self, inst, base, slicevar, indexvar, nonevar): base = self.get(base) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) index = ir.Expr.call(self.get(slicevar), (none, none), (), loc=self.loc) self.store(value=index, name=indexvar) stmt = ir.DelItem(base, self.get(indexvar), loc=self.loc) self.current_block.append(stmt) def op_DELETE_SLICE_1(self, inst, base, start, nonevar, slicevar, indexvar): base = self.get(base) start = self.get(start) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (start, none), (), loc=self.loc) self.store(value=index, name=indexvar) stmt = ir.DelItem(base, self.get(indexvar), loc=self.loc) self.current_block.append(stmt) def op_DELETE_SLICE_2(self, inst, base, nonevar, stop, slicevar, indexvar): base = self.get(base) stop = self.get(stop) nonegv = ir.Const(None, loc=self.loc) self.store(value=nonegv, name=nonevar) none = self.get(nonevar) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (none, stop,), (), loc=self.loc) self.store(value=index, name=indexvar) stmt = ir.DelItem(base, self.get(indexvar), loc=self.loc) self.current_block.append(stmt) def op_DELETE_SLICE_3(self, inst, base, start, stop, slicevar, indexvar): base = self.get(base) start = self.get(start) stop = self.get(stop) slicegv = ir.Global("slice", slice, loc=self.loc) self.store(value=slicegv, name=slicevar) index = ir.Expr.call(self.get(slicevar), (start, stop), (), loc=self.loc) self.store(value=index, name=indexvar) stmt = ir.DelItem(base, self.get(indexvar), loc=self.loc) self.current_block.append(stmt) def op_LOAD_FAST(self, inst, res): srcname = self.code_locals[inst.arg] self.store(value=self.get(srcname), name=res) def op_STORE_FAST(self, inst, value): dstname = self.code_locals[inst.arg] 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) op_DUP_TOP = op_DUP_TOPX op_DUP_TOP_TWO = op_DUP_TOPX def op_STORE_ATTR(self, inst, target, value): attr = self.code_names[inst.arg] sa = ir.SetAttr(target=self.get(target), value=self.get(value), attr=attr, loc=self.loc) self.current_block.append(sa) def op_DELETE_ATTR(self, inst, target): attr = self.code_names[inst.arg] sa = ir.DelAttr(target=self.get(target), attr=attr, loc=self.loc) self.current_block.append(sa) def op_LOAD_ATTR(self, inst, item, res): item = self.get(item) attr = self.code_names[inst.arg] getattr = ir.Expr.getattr(item, attr, loc=self.loc) self.store(getattr, res) def op_LOAD_CONST(self, inst, res): value = self.code_consts[inst.arg] if isinstance(value, tuple): st = [] for x in value: nm = '$const_%s' % str(x) val_const = ir.Const(x, loc=self.loc) target = self.store(val_const, name=nm, redefine=True) st.append(target) const = ir.Expr.build_tuple(st, loc=self.loc) elif isinstance(value, frozenset): st = [] for x in value: nm = '$const_%s' % str(x) val_const = ir.Const(x, loc=self.loc) target = self.store(val_const, name=nm, redefine=True) st.append(target) const = ir.Expr.build_set(st, loc=self.loc) else: const = ir.Const(value, loc=self.loc) self.store(const, res) def op_LOAD_GLOBAL(self, inst, res): name = self.code_names[inst.arg] value = self.get_global_value(name) gl = ir.Global(name, value, loc=self.loc) self.store(gl, res) def op_LOAD_DEREF(self, inst, res): n_cellvars = len(self.code_cellvars) if inst.arg < n_cellvars: name = self.code_cellvars[inst.arg] gl = self.get(name) else: idx = inst.arg - n_cellvars name = self.code_freevars[idx] value = self.get_closure_value(idx) gl = ir.FreeVar(idx, name, value, loc=self.loc) self.store(gl, res) def op_STORE_DEREF(self, inst, value): n_cellvars = len(self.code_cellvars) if inst.arg < n_cellvars: dstname = self.code_cellvars[inst.arg] else: dstname = self.code_freevars[inst.arg - n_cellvars] value = self.get(value) self.store(value=value, name=dstname) def op_SETUP_LOOP(self, inst): assert self.blocks[inst.offset] is self.current_block loop = ir.Loop(inst.offset, exit=(inst.next + inst.arg)) self.syntax_blocks.append(loop) def op_SETUP_WITH(self, inst, contextmanager, exitfn=None): assert self.blocks[inst.offset] is self.current_block # Handle with exitpt = inst.next + inst.arg wth = ir.With(inst.offset, exit=exitpt) self.syntax_blocks.append(wth) ctxmgr = self.get(contextmanager) self.current_block.append(ir.EnterWith(contextmanager=ctxmgr, begin=inst.offset, end=exitpt, loc=self.loc,)) # Store exit fn exit_fn_obj = ir.Const(None, loc=self.loc) self.store(value=exit_fn_obj, name=exitfn) 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" def op_WITH_CLEANUP_START(self, inst): "no-op" def op_WITH_CLEANUP_FINISH(self, inst): "no-op" def op_END_FINALLY(self, inst): "no-op" 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): def op_CALL_FUNCTION(self, inst, func, args, kws, res, vararg): func = self.get(func) args = [self.get(x) for x in args] if vararg is not None: vararg = self.get(vararg) # Process keywords keyvalues = [] removethese = [] for k, v in kws: k, v = self.get(k), self.get(v) for inst in self.current_block.body: if isinstance(inst, ir.Assign) and inst.target is k: removethese.append(inst) keyvalues.append((inst.value.value, v)) # Remove keyword constant statements for inst in removethese: self.current_block.remove(inst) expr = ir.Expr.call(func, args, keyvalues, loc=self.loc, vararg=vararg) self.store(expr, res) op_CALL_FUNCTION_VAR = op_CALL_FUNCTION else: def op_CALL_FUNCTION(self, inst, func, args, res): func = self.get(func) args = [self.get(x) for x in args] expr = ir.Expr.call(func, args, (), loc=self.loc) self.store(expr, res) def op_CALL_FUNCTION_KW(self, inst, func, args, names, res): func = self.get(func) args = [self.get(x) for x in args] # Find names const names = self.get(names) for inst in self.current_block.body: if isinstance(inst, ir.Assign) and inst.target is names: self.current_block.remove(inst) # scan up the block looking for the values, remove them # and find their name strings named_items = [] for x in inst.value.items: for y in self.current_block.body[::-1]: if x == y.target: self.current_block.remove(y) named_items.append(y.value.value) break keys = named_items break nkeys = len(keys) posvals = args[:-nkeys] kwvals = args[-nkeys:] keyvalues = list(zip(keys, kwvals)) expr = ir.Expr.call(func, posvals, keyvalues, loc=self.loc) self.store(expr, res) def op_CALL_FUNCTION_EX(self, inst, func, vararg, res): func = self.get(func) vararg = self.get(vararg) expr = ir.Expr.call(func, [], [], loc=self.loc, vararg=vararg) self.store(expr, res) def _build_tuple_unpack(self, inst, tuples, temps, is_assign): first = self.get(tuples[0]) if is_assign: # it's assign-like, defer handling to an intrinsic that will have # type information. # Can deal with tuples only, i.e. y = (*x,). where x = gv_name = "unpack_single_tuple" gv_fn = ir.Global(gv_name, unpack_single_tuple, loc=self.loc,) self.store(value=gv_fn, name=gv_name, redefine=True) exc = ir.Expr.call(self.get(gv_name), args=(first,), kws=(), loc=self.loc,) self.store(exc, temps[0]) else: for other, tmp in zip(map(self.get, tuples[1:]), temps): out = ir.Expr.binop(fn=operator.add, lhs=first, rhs=other, loc=self.loc) self.store(out, tmp) first = self.get(tmp) def op_BUILD_TUPLE_UNPACK_WITH_CALL(self, inst, tuples, temps, is_assign): # just unpack the input tuple, call inst will be handled afterwards self._build_tuple_unpack(inst, tuples, temps, is_assign) def op_BUILD_TUPLE_UNPACK(self, inst, tuples, temps, is_assign): self._build_tuple_unpack(inst, tuples, temps, is_assign) def op_LIST_TO_TUPLE(self, inst, const_list, res): expr = ir.Expr.dummy('list_to_tuple', (const_list,), loc=self.loc) self.store(expr, res) def op_BUILD_CONST_KEY_MAP(self, inst, keys, keytmps, values, res): # Unpack the constant key-tuple and reused build_map which takes # a sequence of (key, value) pair. keyvar = self.get(keys) # TODO: refactor this pattern. occurred several times. for inst in self.current_block.body: if isinstance(inst, ir.Assign) and inst.target is keyvar: self.current_block.remove(inst) # scan up the block looking for the values, remove them # and find their name strings named_items = [] for x in inst.value.items: for y in self.current_block.body[::-1]: if x == y.target: self.current_block.remove(y) named_items.append(y.value.value) break keytup = named_items break assert len(keytup) == len(values) keyconsts = [ir.Const(value=x, loc=self.loc) for x in keytup] for kval, tmp in zip(keyconsts, keytmps): self.store(kval, tmp) items = list(zip(map(self.get, keytmps), map(self.get, values))) # sort out literal values literal_items = [] for v in values: defns = self.definitions[v] if len(defns) != 1: break defn = defns[0] if not isinstance(defn, ir.Const): break literal_items.append(defn.value) def resolve_const(v): defns = self.definitions[v] if len(defns) != 1: return _UNKNOWN_VALUE(self.get(v).name) defn = defns[0] if not isinstance(defn, ir.Const): return _UNKNOWN_VALUE(self.get(v).name) return defn.value if len(literal_items) != len(values): literal_dict = {x: resolve_const(y) for x, y in zip(keytup, values)} else: literal_dict = {x:y for x, y in zip(keytup, literal_items)} # to deal with things like {'a': 1, 'a': 'cat', 'b': 2, 'a': 2j} # store the index of the actual used value for a given key, this is # used when lowering to pull the right value out into the tuple repr # of a mixed value type dictionary. value_indexes = {} for i, k in enumerate(keytup): value_indexes[k] = i expr = ir.Expr.build_map(items=items, size=2, literal_value=literal_dict, value_indexes=value_indexes, loc=self.loc) self.store(expr, res) def op_GET_ITER(self, inst, value, res): expr = ir.Expr.getiter(value=self.get(value), loc=self.loc) self.store(expr, res) def op_FOR_ITER(self, inst, iterator, pair, indval, pred): """ Assign new block other this instruction. """ assert inst.offset in self.blocks, "FOR_ITER must be block head" # Emit code val = self.get(iterator) pairval = ir.Expr.iternext(value=val, loc=self.loc) self.store(pairval, pair) iternext = ir.Expr.pair_first(value=self.get(pair), loc=self.loc) self.store(iternext, indval) isvalid = ir.Expr.pair_second(value=self.get(pair), loc=self.loc) self.store(isvalid, pred) # Conditional jump br = ir.Branch(cond=self.get(pred), truebr=inst.next, falsebr=inst.get_jump_target(), loc=self.loc) self.current_block.append(br) def op_BINARY_SUBSCR(self, inst, target, index, res): index = self.get(index) target = self.get(target) expr = ir.Expr.getitem(target, index=index, loc=self.loc) self.store(expr, res) def op_STORE_SUBSCR(self, inst, target, index, value): index = self.get(index) target = self.get(target) value = self.get(value) stmt = ir.SetItem(target=target, index=index, value=value, loc=self.loc) self.current_block.append(stmt) def op_DELETE_SUBSCR(self, inst, target, index): index = self.get(index) target = self.get(target) stmt = ir.DelItem(target=target, index=index, loc=self.loc) self.current_block.append(stmt) def op_BUILD_TUPLE(self, inst, items, res): expr = ir.Expr.build_tuple(items=[self.get(x) for x in items], loc=self.loc) self.store(expr, res) def op_BUILD_LIST(self, inst, items, res): expr = ir.Expr.build_list(items=[self.get(x) for x in items], loc=self.loc) self.store(expr, res) def op_BUILD_SET(self, inst, items, res): expr = ir.Expr.build_set(items=[self.get(x) for x in items], loc=self.loc) self.store(expr, res) def op_SET_UPDATE(self, inst, target, value, updatevar, res): target = self.get(target) value = self.get(value) updateattr = ir.Expr.getattr(target, 'update', loc=self.loc) self.store(value=updateattr, name=updatevar) updateinst = ir.Expr.call(self.get(updatevar), (value,), (), loc=self.loc) self.store(value=updateinst, name=res) def op_BUILD_MAP(self, inst, items, size, res): got_items = [(self.get(k), self.get(v)) for k, v in items] # sort out literal values, this is a bit contrived but is to handle # situations like `{1: 10, 1: 10}` where the size of the literal dict # is smaller than the definition def get_literals(target): literal_items = [] values = [self.get(v.name) for v in target] for v in values: defns = self.definitions[v.name] if len(defns) != 1: break defn = defns[0] if not isinstance(defn, ir.Const): break literal_items.append(defn.value) return literal_items literal_keys = get_literals(x[0] for x in got_items) literal_values = get_literals(x[1] for x in got_items) has_literal_keys = len(literal_keys) == len(got_items) has_literal_values = len(literal_values) == len(got_items) value_indexes = {} if not has_literal_keys and not has_literal_values: literal_dict = None elif has_literal_keys and not has_literal_values: literal_dict = {x: _UNKNOWN_VALUE(y[1]) for x, y in zip(literal_keys, got_items)} for i, k in enumerate(literal_keys): value_indexes[k] = i else: literal_dict = {x: y for x, y in zip(literal_keys, literal_values)} for i, k in enumerate(literal_keys): value_indexes[k] = i expr = ir.Expr.build_map(items=got_items, size=size, literal_value=literal_dict, value_indexes=value_indexes, loc=self.loc) self.store(expr, res) def op_STORE_MAP(self, inst, dct, key, value): stmt = ir.StoreMap(dct=self.get(dct), key=self.get(key), value=self.get(value), loc=self.loc) self.current_block.append(stmt) def op_UNARY_NEGATIVE(self, inst, value, res): value = self.get(value) expr = ir.Expr.unary('-', value=value, loc=self.loc) return self.store(expr, res) def op_UNARY_POSITIVE(self, inst, value, res): value = self.get(value) expr = ir.Expr.unary('+', value=value, loc=self.loc) return self.store(expr, res) def op_UNARY_INVERT(self, inst, value, res): value = self.get(value) expr = ir.Expr.unary('~', value=value, loc=self.loc) return self.store(expr, res) def op_UNARY_NOT(self, inst, value, res): value = self.get(value) expr = ir.Expr.unary('not', value=value, loc=self.loc) return self.store(expr, res) def _binop(self, op, lhs, rhs, res): op = BINOPS_TO_OPERATORS[op] lhs = self.get(lhs) rhs = self.get(rhs) expr = ir.Expr.binop(op, lhs=lhs, rhs=rhs, loc=self.loc) self.store(expr, res) def _inplace_binop(self, op, lhs, rhs, res): immuop = BINOPS_TO_OPERATORS[op] op = INPLACE_BINOPS_TO_OPERATORS[op + '='] lhs = self.get(lhs) rhs = self.get(rhs) expr = ir.Expr.inplace_binop(op, immuop, lhs=lhs, rhs=rhs, loc=self.loc) self.store(expr, res) def op_BINARY_ADD(self, inst, lhs, rhs, res): self._binop('+', lhs, rhs, res) def op_BINARY_SUBTRACT(self, inst, lhs, rhs, res): self._binop('-', lhs, rhs, res) def op_BINARY_MULTIPLY(self, inst, lhs, rhs, res): self._binop('*', lhs, rhs, res) def op_BINARY_DIVIDE(self, inst, lhs, rhs, res): self._binop('/?', lhs, rhs, res) def op_BINARY_TRUE_DIVIDE(self, inst, lhs, rhs, res): self._binop('/', lhs, rhs, res) def op_BINARY_FLOOR_DIVIDE(self, inst, lhs, rhs, res): self._binop('//', lhs, rhs, res) def op_BINARY_MODULO(self, inst, lhs, rhs, res): self._binop('%', lhs, rhs, res) def op_BINARY_POWER(self, inst, lhs, rhs, res): self._binop('**', lhs, rhs, res) def op_BINARY_MATRIX_MULTIPLY(self, inst, lhs, rhs, res): self._binop('@', lhs, rhs, res) def op_BINARY_LSHIFT(self, inst, lhs, rhs, res): self._binop('<<', lhs, rhs, res) def op_BINARY_RSHIFT(self, inst, lhs, rhs, res): self._binop('>>', lhs, rhs, res) def op_BINARY_AND(self, inst, lhs, rhs, res): self._binop('&', lhs, rhs, res) def op_BINARY_OR(self, inst, lhs, rhs, res): self._binop('|', lhs, rhs, res) def op_BINARY_XOR(self, inst, lhs, rhs, res): self._binop('^', lhs, rhs, res) def op_INPLACE_ADD(self, inst, lhs, rhs, res): self._inplace_binop('+', lhs, rhs, res) def op_INPLACE_SUBTRACT(self, inst, lhs, rhs, res): self._inplace_binop('-', lhs, rhs, res) def op_INPLACE_MULTIPLY(self, inst, lhs, rhs, res): self._inplace_binop('*', lhs, rhs, res) def op_INPLACE_DIVIDE(self, inst, lhs, rhs, res): self._inplace_binop('/?', lhs, rhs, res) def op_INPLACE_TRUE_DIVIDE(self, inst, lhs, rhs, res): self._inplace_binop('/', lhs, rhs, res) def op_INPLACE_FLOOR_DIVIDE(self, inst, lhs, rhs, res): self._inplace_binop('//', lhs, rhs, res) def op_INPLACE_MODULO(self, inst, lhs, rhs, res): self._inplace_binop('%', lhs, rhs, res) def op_INPLACE_POWER(self, inst, lhs, rhs, res): self._inplace_binop('**', lhs, rhs, res) def op_INPLACE_MATRIX_MULTIPLY(self, inst, lhs, rhs, res): self._inplace_binop('@', lhs, rhs, res) def op_INPLACE_LSHIFT(self, inst, lhs, rhs, res): self._inplace_binop('<<', lhs, rhs, res) def op_INPLACE_RSHIFT(self, inst, lhs, rhs, res): self._inplace_binop('>>', lhs, rhs, res) def op_INPLACE_AND(self, inst, lhs, rhs, res): self._inplace_binop('&', lhs, rhs, res) def op_INPLACE_OR(self, inst, lhs, rhs, res): self._inplace_binop('|', lhs, rhs, res) def op_INPLACE_XOR(self, inst, lhs, rhs, res): self._inplace_binop('^', lhs, rhs, res) def op_JUMP_ABSOLUTE(self, inst): jmp = ir.Jump(inst.get_jump_target(), loc=self.loc) self.current_block.append(jmp) 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, kind=None): if kind is None: self.syntax_blocks.pop() elif kind == 'with': d = ir.PopBlock(loc=self.loc) self.current_block.append(d) 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) ret = ir.Return(self.get(castval), loc=self.loc) self.current_block.append(ret) def op_COMPARE_OP(self, inst, lhs, rhs, res): op = dis.cmp_op[inst.arg] if op == 'in' or op == 'not in': lhs, rhs = rhs, lhs if op == 'not in': self._binop('in', 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) def op_IS_OP(self, inst, lhs, rhs, res): # invert if op case is 1 op = 'is not' if inst.arg == 1 else 'is' self._binop(op, lhs, rhs, res) def op_CONTAINS_OP(self, inst, lhs, rhs, res): lhs, rhs = rhs, lhs self._binop('in', lhs, rhs, res) # invert if op case is 1 if inst.arg == 1: tmp = self.get(res) out = ir.Expr.unary('not', value=tmp, loc=self.loc) self.store(out, res) def op_BREAK_LOOP(self, inst, end=None): if end is None: loop = self.syntax_blocks[-1] assert isinstance(loop, ir.Loop) end = loop.exit jmp = ir.Jump(target=end, loc=self.loc) self.current_block.append(jmp) def _op_JUMP_IF(self, inst, pred, iftrue): brs = { True: inst.get_jump_target(), False: inst.next, } truebr = brs[iftrue] falsebr = brs[not iftrue] name = "bool%s" % (inst.offset) gv_fn = ir.Global("bool", bool, loc=self.loc) self.store(value=gv_fn, name=name) callres = ir.Expr.call(self.get(name), (self.get(pred),), (), loc=self.loc) pname = "$%spred" % (inst.offset) predicate = self.store(value=callres, name=pname) bra = ir.Branch(cond=predicate, truebr=truebr, falsebr=falsebr, loc=self.loc) self.current_block.append(bra) def op_JUMP_IF_FALSE(self, inst, pred): self._op_JUMP_IF(inst, pred=pred, iftrue=False) def op_JUMP_IF_TRUE(self, inst, pred): self._op_JUMP_IF(inst, pred=pred, iftrue=True) def op_POP_JUMP_IF_FALSE(self, inst, pred): self._op_JUMP_IF(inst, pred=pred, iftrue=False) def op_POP_JUMP_IF_TRUE(self, inst, pred): self._op_JUMP_IF(inst, pred=pred, iftrue=True) def op_JUMP_IF_FALSE_OR_POP(self, inst, pred): self._op_JUMP_IF(inst, pred=pred, iftrue=False) def op_JUMP_IF_TRUE_OR_POP(self, inst, pred): self._op_JUMP_IF(inst, pred=pred, iftrue=True) def op_JUMP_IF_NOT_EXC_MATCH(self, inst, pred, tos, tos1): truebr = inst.next falsebr = inst.get_jump_target() 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(tos1) rhs = self.get(tos) exc = ir.Expr.call( self.get(exc_match_name), args=(lhs, rhs), kws=(), loc=self.loc, ) predicate = self.store(exc, pred) bra = ir.Branch(cond=predicate, truebr=truebr, falsebr=falsebr, loc=self.loc) self.current_block.append(bra) def op_RERAISE(self, inst, exc): # Numba can't handle this case and it's caught else where, this is a # runtime guard in case this is reached by unknown means. msg = (f"Unreachable condition reached (op code RERAISE executed)" f"{error_extras['reportable']}") stmt = ir.StaticRaise(AssertionError, (msg,), self.loc) self.current_block.append(stmt) def op_RAISE_VARARGS(self, inst, exc): if exc is not None: exc = self.get(exc) 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 index = None inst = ir.Yield(value=self.get(value), index=index, loc=self.loc) return self.store(inst, res) def op_MAKE_FUNCTION(self, inst, name, code, closure, annotations, kwdefaults, defaults, res): # annotations are ignored by numba but useful for static analysis # re. https://github.com/numba/numba/issues/7269 if kwdefaults is not None: msg = "op_MAKE_FUNCTION with kwdefaults is not implemented" raise NotImplementedError(msg) if defaults: if isinstance(defaults, tuple): defaults = tuple([self.get(name) for name in defaults]) else: defaults = self.get(defaults) 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: closure = self.get(closure) expr = ir.Expr.make_function(name, fcode, closure, defaults, self.loc) self.store(expr, res) def op_MAKE_CLOSURE(self, inst, name, code, closure, annotations, kwdefaults, defaults, res): self.op_MAKE_FUNCTION(inst, name, code, closure, annotations, kwdefaults, defaults, res) def op_LOAD_CLOSURE(self, inst, res): n_cellvars = len(self.code_cellvars) if inst.arg < n_cellvars: name = self.code_cellvars[inst.arg] try: gl = self.get(name) except NotDefinedError: msg = "Unsupported use of op_LOAD_CLOSURE encountered" raise NotImplementedError(msg) else: idx = inst.arg - n_cellvars name = self.code_freevars[idx] value = self.get_closure_value(idx) gl = ir.FreeVar(idx, name, value, loc=self.loc) self.store(gl, res) def op_LIST_APPEND(self, inst, target, value, appendvar, res): target = self.get(target) value = self.get(value) appendattr = ir.Expr.getattr(target, 'append', loc=self.loc) self.store(value=appendattr, name=appendvar) appendinst = ir.Expr.call(self.get(appendvar), (value,), (), loc=self.loc) self.store(value=appendinst, name=res) def op_LIST_EXTEND(self, inst, target, value, extendvar, res): target = self.get(target) value = self.get(value) # If the statements between the current instruction and the target # are N * consts followed by build_tuple AND the target has no items, # it's a situation where a list is being statically initialised, rewrite # the build_tuple as a build_list, drop the extend, and wire up the # target as the result from the build_tuple that's been rewritten. # See if this is the first statement in a block, if so its probably from # control flow in a tuple unpack like: # `(*(1, (2,) if predicate else (3,)))` # this cannot be handled as present so raise msg = ("An unsupported bytecode sequence has been encountered: " "op_LIST_EXTEND at the start of a block.\n\nThis could be " "due to the use of a branch in a tuple unpacking statement.") if not self.current_block.body: raise errors.UnsupportedError(msg) # is last emitted statement a build_tuple? stmt = self.current_block.body[-1] ok = isinstance(stmt.value, ir.Expr) and stmt.value.op == "build_tuple" # check statements from self.current_block.body[-1] through to target, # make sure they are consts build_empty_list = None if ok: for stmt in reversed(self.current_block.body[:-1]): if not isinstance(stmt, ir.Assign): ok = False break # if its not a const, it needs to be the `build_list` for the # target, else it's something else we don't know about so just # bail if isinstance(stmt.value, ir.Const): continue # it's not a const, check for target elif isinstance(stmt.value, ir.Expr) and stmt.target == target: build_empty_list = stmt # it's only ok to do this if the target has no initializer # already ok = not stmt.value.items break else: ok = False break if ok and build_empty_list is None: raise errors.UnsupportedError(msg) if ok: stmts = self.current_block.body build_tuple_asgn = self.current_block.body[-1] # move build list to last issued statement stmts.append(stmts.pop(stmts.index(build_empty_list))) # fix the build list build_tuple = build_tuple_asgn.value build_list = build_empty_list.value build_list.items = build_tuple.items else: # it's just a list extend with no static init, let it be extendattr = ir.Expr.getattr(target, 'extend', loc=self.loc) self.store(value=extendattr, name=extendvar) extendinst = ir.Expr.call(self.get(extendvar), (value,), (), loc=self.loc) self.store(value=extendinst, name=res) def op_MAP_ADD(self, inst, target, key, value, setitemvar, res): target = self.get(target) key = self.get(key) value = self.get(value) setitemattr = ir.Expr.getattr(target, '__setitem__', loc=self.loc) self.store(value=setitemattr, name=setitemvar) appendinst = ir.Expr.call(self.get(setitemvar), (key, value,), (), loc=self.loc) self.store(value=appendinst, name=res) def op_LOAD_ASSERTION_ERROR(self, inst, res): gv_fn = ir.Global("AssertionError", AssertionError, loc=self.loc) self.store(value=gv_fn, name=res) # NOTE: The LOAD_METHOD opcode is implemented as a LOAD_ATTR for ease, # however this means a new object (the bound-method instance) could be # created. Conversely, using a pure LOAD_METHOD no intermediary is present # and it is essentially like a pointer grab and forward to CALL_METHOD. The # net outcome is that the implementation in Numba produces the same result, # but in object mode it may be that it runs more slowly than it would if # run in CPython. def op_LOAD_METHOD(self, *args, **kws): self.op_LOAD_ATTR(*args, **kws) def op_CALL_METHOD(self, *args, **kws): self.op_CALL_FUNCTION(*args, **kws)