diff --git a/llpython/addr_flow.py b/llpython/addr_flow.py new file mode 100644 index 0000000..d9a774b --- /dev/null +++ b/llpython/addr_flow.py @@ -0,0 +1,88 @@ +#! /usr/bin/env python +# ______________________________________________________________________ +# Module imports + +from __future__ import absolute_import + +from .byte_flow import Instr, BytecodeFlowBuilder, demo_flow_builder +from .opcode_util import build_basic_blocks, itercodeobjs + +# ______________________________________________________________________ +# Class definition(s) + +class AddressFlowBuilder(BytecodeFlowBuilder): + ''' + Builds on top of the BytecodeFlowBuilder with two important differences: + + * Child nodes are represented by bytecode indices. + + * All operations (other than purely stack manipulation operations) + are retained in the block list (as opposed to being nested). + + The resulting data structure describes a directed acyclic graph + (DAG) in a similar fashion to BytecodeFlowBuilder: + + * `flow_dag` ``:=`` ``{`` `blocks` ``*`` ``}`` + * `blocks` ``:=`` `block_index` ``:`` ``[`` `bytecode_tuple` ``*`` ``]`` + * `bytecode_tuple` ``:=`` ``(`` `opcode_index` ``,`` `opcode` ``,`` + `opname` ``,`` `arg` ``,`` ``[`` `opcode_index` ``*`` ``]`` ``)`` + ''' + def _visit_op(self, i, op, arg, opname, pops, pushes, appends): + assert pops is not None, ('%s not well defined in opcode_util.' + 'OPCODE_MAP' % opname) + if pops: + if pops < 0: + pops = arg - pops - 1 + assert pops <= len(self.stack), ("Stack underflow at instruction " + "%d (%s)!" % (i, opname)) + stk_args = [stk_arg[0] for stk_arg in self.stack[-pops:]] + del self.stack[-pops:] + else: + stk_args = [] + ret_val = Instr(i, op, opname, arg, stk_args) + if pushes: + self.stack.append(ret_val) + self.block.append(ret_val) + return ret_val + + def op_IMPORT_FROM (self, i, op, arg): + # References top of stack without popping, so we can't use the + # generic machinery. + opname = self.opmap[op][0] + ret_val = Instr(i, op, opname, arg, [self.stack[-1][0]]) + self.stack.append(ret_val) + self.block.append(ret_val) + return ret_val + + def op_JUMP_IF_FALSE (self, i, op, arg): + ret_val = Instr(i, op, self.opnames[op], arg, [self.stack[-1][0]]) + self.block.append(ret_val) + return ret_val + + op_JUMP_IF_TRUE = op_JUMP_IF_FALSE + + def op_LIST_APPEND (self, i, op, arg): + '''This method is used for both LIST_APPEND, and SET_ADD + opcodes.''' + elem = self.stack.pop() + container = self.stack[-arg] + ret_val = Instr(i, op, self.opnames[op], arg, [container[0], elem[0]]) + self.block.append(ret_val) + return ret_val + + op_SET_ADD = op_LIST_APPEND + +# ______________________________________________________________________ +# Main (self-test) routine + +def main(*args): + return demo_flow_builder(AddressFlowBuilder, *args) + +# ______________________________________________________________________ + +if __name__ == "__main__": + import sys + main(*sys.argv[1:]) + +# ______________________________________________________________________ +# End of addr_flow.py diff --git a/llpython/af_to_api.py b/llpython/af_to_api.py new file mode 100644 index 0000000..d3027f3 --- /dev/null +++ b/llpython/af_to_api.py @@ -0,0 +1,437 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# ______________________________________________________________________ +# Module imports + +from __future__ import print_function, division, absolute_import + +import inspect +import opcode + +import llvm.core as lc + +from . import byte_control +from . import addr_flow +from . import opcode_util +from .bytetype import lvoid, li1, lc_int, lc_long, l_pyobj_p +from .bytecode_visitor import GenericFlowVisitor +from .nobitey import get_string_constant + +# ______________________________________________________________________ +# Class definitions + +class AddressFlowToLLVMPyAPICalls(GenericFlowVisitor): + ''' + Code generator for translating from a Python code object and its + address flow (output by addr_flow.AddressFlowBuilder) into an LLVM + function. The resulting LLVM function calls into a user-provided + (or undefined) API function for each interpreter byte code. + + Target API function names are based on a programmable name + mangling scheme: + + OPCODE_NAME + + The and strings are determined by the prefix() + and postfix() methods. These methods may either be overloaded, or + specialized at construction time using _prefix and _postfix + arguments. The OPCODE_NAME is the opcode name as determined by + the map in opcode.opname. + ''' + def __init__(self, _prefix=None, _postfix=None, **kwds): + if _prefix is not None: + if inspect.isfunction(_prefix): + __prefix = _prefix + else: + def __prefix(opname, *opargs): + return _prefix + self.prefix = __prefix + if _postfix is not None: + if inspect.isfunction(_postfix): + __postfix = _postfix + else: + def __postfix(opname, *opargs): + return _postfix + self.postfix = __postfix + + def prefix(self, opname, *opargs): + return '' + + def postfix(self, opname, *opargs): + return '' + + def get_op_function(self, opname, *opargs, **kwds): + print(opname, opargs, kwds) + target_fn_ty = lc.Type.function(kwds.get('return_type', self.obj_type), + [arg.type for arg in opargs]) + target_fn_name = ''.join((self.prefix(opname, *opargs), opname, + self.postfix(opname, *opargs))) + return self.llvm_module.get_or_insert_function(target_fn_ty, + target_fn_name) + + def call_op_function(self, index, opname, *opargs, **kwds): + target_fn = self.get_op_function(opname, *opargs, **kwds) + if target_fn.type.pointee.return_type != lvoid: + name = kwds.get('name', 'op_%d' % index) + result = self.builder.call(target_fn, opargs, name) + else: + result = self.builder.call(target_fn, opargs) + return result + + def translate_cfg(self, code_obj, cfg, llvm_module=None, + monotype=l_pyobj_p, **kwds): + ''' + Generate LLVM code for the given code object and it's control + flow graph. + + If no LLVM module is given as an argument, translate_cfg() + creates a new module. + + Returns the resulting LLVM module. + ''' + assert inspect.iscode(code_obj) + self.obj_type = monotype + self.null = lc.Constant.null(self.obj_type) + self.code_obj = code_obj + self.cfg = cfg + self.target_function_name = kwds.get( + 'target_function_name', 'co_%s_%x' % (code_obj.co_name, + id(code_obj))) + if llvm_module is None: + llvm_module = lc.Module.new('lmod_' + self.target_function_name) + self.llvm_module = llvm_module + self.visit(cfg.blocks) + del self.llvm_module + del self.cfg + del self.code_obj + return llvm_module + + def enter_flow_object(self, flow): + ''' + Set up any state for dealing a new dictionary of basic blocks. + ''' + super(AddressFlowToLLVMPyAPICalls, self).enter_flow_object(flow) + self.nargs = opcode_util.get_nargs(self.code_obj) + lltype = lc.Type.function( + self.obj_type, tuple(self.obj_type for _ in range(self.nargs))) + self.llvm_function = self.llvm_module.add_function( + lltype, self.target_function_name) + self.llvm_blocks = {} + for block in self.block_list: + if 0 in self.cfg.blocks_reaching[block]: + bb = self.llvm_function.append_basic_block( + 'BLOCK_%d' % (block,)) + self.llvm_blocks[block] = bb + self.symtab = {} + self.values = {} + + def exit_flow_object(self, flow): + ''' + Clean up any state created while visiting the given dictionary + of basic blocks. + ''' + super(AddressFlowToLLVMPyAPICalls, self).exit_flow_object(flow) + del self.symtab + del self.llvm_blocks + del self.llvm_function + + def generate_co_init(self): + ''' + Initialize the code object's local variables on the stack. + ''' + for name in self.code_obj.co_varnames: + ptr = self.builder.alloca(self.obj_type, name + '_p') + self.symtab[name] = ptr + self.builder.store(self.null, ptr) + for arg_index, arg in zip(range(self.nargs), self.llvm_function.args): + if arg_index == 0: + arg.name = '_globals_%x' % id(self.code_obj) + self.globals = arg + else: + local_index = arg_index - 1 + name = self.code_obj.co_varnames[local_index] + arg.name = name + self.call_op_function( + None, 'STORE_FAST', arg, self.symtab[name], + return_type=lvoid) + + def generate_co_deinit(self, index): + for value in self.symtab.values(): + self.call_op_function(index, 'DELETE_FAST', + lc.Constant.int(li1, 0), value, + return_type=lvoid) + + def enter_block(self, block): + ''' + Set up state for generating code in a new basic block. If + this is the first basic block, initialize the local variables + using generate_co_init(). + ''' + ret_val = False + if block in self.llvm_blocks: + self.llvm_block = self.llvm_blocks[block] + self.builder = lc.Builder.new(self.llvm_block) + if block == 0: + self.generate_co_init() + ret_val = True + return ret_val + + def exit_block(self, block): + ''' + Tear down any state created for code generation in the current + basic block. If the basic block isn't already terminated by a + control flow statement, assume it branches to the next basic + block. + ''' + # XXX Isn't this really a bug in GenericFlowVisitor.visit()? + if block in self.llvm_blocks: + bb_instrs = self.llvm_block.instructions + if ((len(bb_instrs) == 0) or + (not bb_instrs[-1].is_terminator)): + out_blocks = list(self.cfg.blocks_out[block]) + assert len(out_blocks) == 1, [str(i) for i in bb_instrs] + self.builder.branch(self.llvm_blocks[out_blocks[0]]) + del self.builder + del self.llvm_block + + def _op(self, i, op, arg, *args, **kwds): + args = [self.values[stkarg] for stkarg in args] + if arg is not None: + args.insert(0, lc.Constant.int(lc_int, arg)) + # XXX Modify the visitor! Should be passing Instr named + # tuples here, and not looking up the operation name. + result = self.call_op_function(i, self.opnames[op], *args, **kwds) + self.values[i] = result + return [result] + + def _not_implemented(self, i, op, arg, *args, **kwds): + raise NotImplementedError(self.opnames[op]) + + op_BINARY_ADD = _op + op_BINARY_AND = _op + op_BINARY_DIVIDE = _op + op_BINARY_FLOOR_DIVIDE = _op + op_BINARY_LSHIFT = _op + op_BINARY_MODULO = _op + op_BINARY_MULTIPLY = _op + op_BINARY_OR = _op + op_BINARY_POWER = _op + op_BINARY_RSHIFT = _op + op_BINARY_SUBSCR = _op + op_BINARY_SUBTRACT = _op + op_BINARY_TRUE_DIVIDE = _op + op_BINARY_XOR = _op + op_BREAK_LOOP = _op + op_BUILD_CLASS = _op + op_BUILD_LIST = _op + op_BUILD_MAP = _op + op_BUILD_SET = _op + op_BUILD_SLICE = _op + op_BUILD_TUPLE = _op + op_CALL_FUNCTION = _op + op_CALL_FUNCTION_KW = _op + op_CALL_FUNCTION_VAR = _op + op_CALL_FUNCTION_VAR_KW = _op + op_COMPARE_OP = _op + op_CONTINUE_LOOP = _op + op_DELETE_ATTR = _op + op_DELETE_DEREF = _op + + def op_DELETE_FAST(self, i, op, arg, *args, **kwds): + varname = self.code_obj.co_varnames[arg] + result = self.call_op_function(i, 'DELETE_FAST', + lc.Constant.int(li1, 1), + self.symtab[varname], return_type=lvoid) + return [result] + + def op_DELETE_GLOBAL(self, i, op, arg, *args, **kwds): + varname = get_string_constant(self.llvm_module, + self.code_obj.co_names[arg]) + result = self.call_op_function(i, 'DELETE_GLOBAL', self.globals, + varname, return_type=lvoid) + self.values[i] = result + return [result] + + op_DELETE_NAME = _op + op_DELETE_SLICE = _op + op_DELETE_SUBSCR = _op + op_DUP_TOP = _not_implemented + op_DUP_TOPX = _not_implemented + op_DUP_TOP_TWO = _not_implemented + op_END_FINALLY = _op + op_EXEC_STMT = _op + op_EXTENDED_ARG = _op + op_FOR_ITER = _op + op_GET_ITER = _op + op_IMPORT_FROM = _op + op_IMPORT_NAME = _op + op_IMPORT_STAR = _op + op_INPLACE_ADD = _op + op_INPLACE_AND = _op + op_INPLACE_DIVIDE = _op + op_INPLACE_FLOOR_DIVIDE = _op + op_INPLACE_LSHIFT = _op + op_INPLACE_MODULO = _op + op_INPLACE_MULTIPLY = _op + op_INPLACE_OR = _op + op_INPLACE_POWER = _op + op_INPLACE_RSHIFT = _op + op_INPLACE_SUBTRACT = _op + op_INPLACE_TRUE_DIVIDE = _op + op_INPLACE_XOR = _op + + def op_JUMP_ABSOLUTE(self, i, op, arg, *args, **kwds): + return [self.builder.branch(self.llvm_blocks[arg])] + + def op_JUMP_FORWARD(self, i, op, arg, *args, **kwds): + return [self.builder.branch(self.llvm_blocks[i + arg + 3])] + + op_JUMP_IF_FALSE = _not_implemented + op_JUMP_IF_FALSE_OR_POP = _not_implemented + op_JUMP_IF_TRUE = _not_implemented + op_JUMP_IF_TRUE_OR_POP = _not_implemented + op_LIST_APPEND = _op + op_LOAD_ATTR = _op + op_LOAD_BUILD_CLASS = _op + op_LOAD_CLOSURE = _op + + def op_LOAD_CONST(self, i, op, arg, *args, **kwds): + py_val = self.code_obj.co_consts[arg] + if isinstance(py_val, int): + # XXX Add bounds check on integer values; use big int + # (from string?) constructor if necessary. + result = self.call_op_function(i, 'LOAD_CONST_INT', + lc.Constant.int(lc_long, py_val)) + elif isinstance(py_val, float): + result = self.call_op_function(i, 'LOAD_CONST_FLOAT', + lc.Constant.double(py_val)) + elif py_val is None: + result = self.call_op_function(i, 'LOAD_CONST_NONE') + else: + raise NotImplementedError('Constant conversion for %r' % (py_val,)) + self.values[i] = result + return [result] + + op_LOAD_DEREF = _op + + def op_LOAD_FAST(self, i, op, arg, *args, **kwds): + varname = self.code_obj.co_varnames[arg] + args = self.symtab[varname], + result = self.call_op_function(i, 'LOAD_FAST', *args) + self.values[i] = result + return [result] + + def op_LOAD_GLOBAL(self, i, op, arg, *args, **kwds): + varname = get_string_constant(self.llvm_module, + self.code_obj.co_names[arg]) + result = self.call_op_function(i, 'LOAD_GLOBAL', self.globals, varname) + self.values[i] = result + return [result] + + op_LOAD_LOCALS = _op + op_LOAD_NAME = _op + op_MAKE_CLOSURE = _op + op_MAKE_FUNCTION = _op + op_MAP_ADD = _op + op_NOP = _op + op_POP_BLOCK = _op + op_POP_EXCEPT = _op + + def _op_cbranch(self, i, op, arg, *args, **kwds): + if op in opcode.hasjabs: + branch_taken = self.llvm_blocks[arg] + else: + branch_taken = self.llvm_blocks[i + arg + 3] + branch_not_taken = self.llvm_blocks[i + 3] + _kwds = kwds.copy() + _kwds.update(return_type=li1) + test = self._op(i, op, None, *args, **_kwds)[0] + return [test, self.builder.cbranch(test, branch_taken, + branch_not_taken)] + + op_POP_JUMP_IF_FALSE = _op_cbranch + op_POP_JUMP_IF_TRUE = _op_cbranch + + def op_POP_TOP(self, i, op, arg, *args): + return [self.call_op_function(i, 'POP_TOP', self.values[args[0]], + return_type=lvoid)] + + op_PRINT_EXPR = _op + op_PRINT_ITEM = _op + op_PRINT_ITEM_TO = _op + op_PRINT_NEWLINE = _op + op_PRINT_NEWLINE_TO = _op + op_RAISE_VARARGS = _op + + def op_RETURN_VALUE(self, i, op, arg, *args): + self.generate_co_deinit(i) + return [self.builder.ret(self.values[args[0]])] + + op_ROT_FOUR = _op + op_ROT_THREE = _op + op_ROT_TWO = _op + op_SETUP_EXCEPT = _op + op_SETUP_FINALLY = _op + op_SETUP_LOOP = _op_cbranch + op_SETUP_WITH = _op + op_SET_ADD = _op + op_SLICE = _op + op_STOP_CODE = _op + op_STORE_ATTR = _op + op_STORE_DEREF = _op + + def op_STORE_FAST(self, i, op, arg, *args): + src_index, = args + src = self.values[src_index] + varname = self.code_obj.co_varnames[arg] + dest = self.symtab[varname] + result = self.call_op_function(i, 'STORE_FAST', src, dest, + return_type=lvoid) + return [result] + + def op_STORE_GLOBAL(self, i, op, arg, *args): + varname = get_string_constant(self.llvm_module, + self.code_obj.co_names[arg]) + result = self.call_op_function( + i, 'STORE_GLOBAL', self.values[args[0]], self.globals, varname, + return_type=lvoid) + return [result] + + op_STORE_LOCALS = _not_implemented + op_STORE_MAP = _op + op_STORE_NAME = _op + op_STORE_SLICE = _op + op_STORE_SUBSCR = _op + op_UNARY_CONVERT = _op + op_UNARY_INVERT = _op + op_UNARY_NEGATIVE = _op + op_UNARY_NOT = _op + op_UNARY_POSITIVE = _op + op_UNPACK_EX = _op + op_UNPACK_SEQUENCE = _op + op_WITH_CLEANUP = _op + op_YIELD_VALUE = _op + +# ______________________________________________________________________ +# Function definition(s) + +def demo_translator(*args, **kwds): + def _visit(obj): + if inspect.isfunction(obj): + obj = opcode_util.get_code_object(obj) + print('\n; %s\n; %r' % ('_' * 70, obj)) + cfg = byte_control.ControlFlowBuilder.build_cfg_from_co(obj) + cfg.blocks = addr_flow.AddressFlowBuilder().visit_cfg(cfg) + print(AddressFlowToLLVMPyAPICalls(**kwds).translate_cfg(obj, cfg, + **kwds)) + return opcode_util.visit_code_args(_visit, *args) + +# ______________________________________________________________________ +# Main (self-test) routine + +if __name__ == '__main__': + import sys + demo_translator(*sys.argv[1:], _prefix = '_') + +# ______________________________________________________________________ +# End of af_to_api.py diff --git a/llpython/byte_control.py b/llpython/byte_control.py index 8ca407e..e84b78e 100644 --- a/llpython/byte_control.py +++ b/llpython/byte_control.py @@ -1,13 +1,45 @@ # ______________________________________________________________________ -from __future__ import absolute_import -import opcode -from . import opcode_util -import pprint +# Module imports +from __future__ import absolute_import + +import opcode +import pprint +import inspect + +from . import opcode_util from .bytecode_visitor import BasicBlockVisitor, BenignBytecodeVisitorMixin from .control_flow import ControlFlowGraph # ______________________________________________________________________ +# Module data + +# The following opcodes branch based on the control (a.k.a. frame) +# stack: +RETURN_VALUE, CONTINUE_LOOP, BREAK_LOOP, END_FINALLY, RAISE_VARARGS = ( + opcode.opmap[opname] for opname in ( + 'RETURN_VALUE', 'CONTINUE_LOOP', 'BREAK_LOOP', 'END_FINALLY', + 'RAISE_VARARGS')) + +# The following opcodes push a new frame on the control stack: +SETUP_EXCEPT, SETUP_FINALLY, SETUP_LOOP, SETUP_WITH = ( + opcode.opmap.get(opname, None) for opname in ( + 'SETUP_EXCEPT', 'SETUP_FINALLY', 'SETUP_LOOP', 'SETUP_WITH')) + +WHY_NOT = 1 +WHY_EXCEPTION = WHY_NOT << 1 +WHY_RERAISE = WHY_EXCEPTION << 1 # We don't worry about this code + # during CFA, since its primary use + # is to log traceback information; + # WHY_RERAISE's bytecode control flow + # is the same as WHY_EXCEPTION. +WHY_RETURN = WHY_RERAISE << 1 +WHY_BREAK = WHY_RETURN << 1 +WHY_CONTINUE = WHY_BREAK << 1 +WHY_YIELD = WHY_CONTINUE << 1 + +# ______________________________________________________________________ +# Class definition(s) class ControlFlowBuilder (BenignBytecodeVisitorMixin, BasicBlockVisitor): '''Visitor responsible for traversing a bytecode basic block map and @@ -17,10 +49,10 @@ class ControlFlowBuilder (BenignBytecodeVisitorMixin, BasicBlockVisitor): which is used by later transformers for dataflow analysis. ''' def visit (self, flow, nargs = 0, *args, **kws): - '''Given a bytecode flow, and an optional number of arguments, - return a :py:class:`llpython.control_flow.ControlFlowGraph` - instance describing the full control flow of the bytecode - flow.''' + '''Given a map of bytecode basic blocks, and an optional + number of arguments, return a + :py:class:`llpython.control_flow.ControlFlowGraph` instance + describing the full control flow of the bytecode flow.''' self.nargs = nargs ret_val = super(ControlFlowBuilder, self).visit(flow, *args, **kws) del self.nargs @@ -32,17 +64,26 @@ class ControlFlowBuilder (BenignBytecodeVisitorMixin, BasicBlockVisitor): self.block_list = list(blocks.keys()) self.block_list.sort() self.cfg = ControlFlowGraph() - self.loop_stack = [] + self.control_stack = [] + self.continue_targets = {} # Map from SETUP_LOOP addresses to + # start of loop addresses, based on + # observed CONTINUE_LOOP opcodes. + self.break_targets = set() # Set of SETUP_LOOP address that + # have at least one observed + # BREAK_LOOP opcode corresponding + # to them. for block in self.block_list: self.cfg.add_block(block, blocks[block]) def exit_blocks (self, blocks): super(ControlFlowBuilder, self).exit_blocks(blocks) assert self.blocks == blocks + self.cfg.unlink_unreachables() self.cfg.compute_dataflow() self.cfg.update_for_ssa() ret_val = self.cfg - del self.loop_stack + del self.continue_targets + del self.control_stack del self.cfg del self.block_list del self.blocks @@ -59,23 +100,91 @@ class ControlFlowBuilder (BenignBytecodeVisitorMixin, BasicBlockVisitor): def _get_next_block (self, block): return self.block_list[self.block_list.index(block) + 1] + def _generate_handler_edge (self, block, i, op, arg, why): + """Given a reason (corresponding to the why code in + Python/ceval.c), interrupt control flow, possibly using the + control flow (a.k.a. frame) stack to calculate the next + target. + + Returns True if an edge was added to the CFG, False otherwise. + Based on the opcode the return result may mean different + things (for example: if why == WHY_RETURN, then a False return + result means the function returned, and no edge was + generated).""" + ret_val = False + if len(self.control_stack) > 0: + handlers = set((SETUP_FINALLY, SETUP_WITH)) + if why == WHY_EXCEPTION: + handlers.add(SETUP_EXCEPT) + reversed_stack = reversed(self.control_stack) + target = None + for handler_i, handler_op, handler_arg in reversed_stack: + if handler_op in handlers: + target = handler_i + handler_arg + 3 + elif handler_op == SETUP_LOOP: + if why == WHY_CONTINUE: + # Only generate a WHY_CONTINUE edge if a continue + # statement has been observed for this loop. + if handler_i not in self.continue_targets: + break + elif op == CONTINUE_LOOP: + target = arg + assert target == self.continue_targets[handler_i] + else: + target = self.continue_targets[handler_i] + elif why == WHY_BREAK: + # Only generate a WHY_BREAK edge if a break + # statement has been observed for this loop. + if handler_i not in self.break_targets: + break + else: + target = handler_i + handler_arg + 3 + if target is not None: + self.cfg.add_edge(block, target) + ret_val = True + break + return ret_val + def exit_block (self, block): assert block == self.block del self.block i, op, arg = self.blocks[block][-1] - opname = opcode.opname[op] - if op in opcode.hasjabs: + goto_next = False + if op == RETURN_VALUE: + self._generate_handler_edge(block, i, op, arg, WHY_RETURN) + elif op == CONTINUE_LOOP: + branched = self._generate_handler_edge(block, i, op, arg, + WHY_CONTINUE) + assert branched, ("Attempted to continue outside of loop %r" % + (self.blocks[block][-1],)) + elif op == BREAK_LOOP: + branched = self._generate_handler_edge(block, i, op, arg, + WHY_BREAK) + assert branched, ("Attempted to break outside of loop %r" % + (self.blocks[block][-1],)) + elif op == RAISE_VARARGS: + self._generate_handler_edge(block, i, op, arg, WHY_EXCEPTION) + elif op == END_FINALLY: + # The following does a lot of redundant traversal of the + # simulated frame stack, but it works, and keeps a lot of + # special case logic out of _generate_handler_edge(). + self._generate_handler_edge(block, i, op, arg, WHY_EXCEPTION) + self._generate_handler_edge(block, i, op, arg, WHY_RETURN) + self._generate_handler_edge(block, i, op, arg, WHY_BREAK) + self._generate_handler_edge(block, i, op, arg, WHY_CONTINUE) + goto_next = True # why == WHY_NOT + elif op in opcode.hasjabs: self.cfg.add_edge(block, arg) elif op in opcode.hasjrel: self.cfg.add_edge(block, i + arg + 3) - elif opname == 'BREAK_LOOP': - loop_i, _, loop_arg = self.loop_stack[-1] - self.cfg.add_edge(block, loop_i + loop_arg + 3) - elif opname != 'RETURN_VALUE': - self.cfg.add_edge(block, self._get_next_block(block)) - if op in opcode_util.hascbranch: + else: + goto_next = True + if op in opcode_util.hascbranch or goto_next: self.cfg.add_edge(block, self._get_next_block(block)) + # ____________________________________________________________ + # LOAD/STORE_FAST + def op_LOAD_FAST (self, i, op, arg, *args, **kws): self.cfg.blocks_reads[self.block].add(arg) return super(ControlFlowBuilder, self).op_LOAD_FAST(i, op, arg, *args, @@ -86,34 +195,97 @@ class ControlFlowBuilder (BenignBytecodeVisitorMixin, BasicBlockVisitor): return super(ControlFlowBuilder, self).op_STORE_FAST(i, op, arg, *args, **kws) + # ____________________________________________________________ + # *_LOOP: Special loop control flow. + + def _get_current_loop (self): + for handler in reversed(self.control_stack): + if handler[1] == SETUP_LOOP: + return handler + return None, None, None + + def op_BREAK_LOOP (self, i, op, arg, *args, **kws): + handler_i, _, _ = self._get_current_loop() + assert handler_i is not None + self.break_targets.add(handler_i) + + def op_CONTINUE_LOOP (self, i, op, arg, *args, **kws): + """ + CONTINUE_LOOP has to be handled differently than BREAK_LOOP, + since its argument specifies where the start of the loop is + (in the case of for-loops, FOR_ITER defines the true start of + the loop, instead of SETUP_LOOP.) + """ + handler_i, _, _ = self._get_current_loop() + assert handler_i is not None + if handler_i in self.continue_targets: + assert arg == self.continue_targets[handler_i] + else: + self.continue_targets[handler_i] = arg + + # ____________________________________________________________ + # POP_BLOCK + + def op_POP_BLOCK (self, i, op, arg, *args, **kws): + self.control_stack.pop() + return super(ControlFlowBuilder, self).op_POP_BLOCK(i, op, arg, *args, + **kws) + # ____________________________________________________________ + # SETUP_* + + def op_SETUP_EXCEPT (self, i, op, arg, *args, **kws): + self.control_stack.append((i, op, arg)) + return super(ControlFlowBuilder, self).op_SETUP_EXCEPT(i, op, arg, + *args, **kws) + + def op_SETUP_FINALLY (self, i, op, arg, *args, **kws): + self.control_stack.append((i, op, arg)) + return super(ControlFlowBuilder, self).op_SETUP_FINALLY(i, op, arg, + *args, **kws) + def op_SETUP_LOOP (self, i, op, arg, *args, **kws): - self.loop_stack.append((i, op, arg)) + self.control_stack.append((i, op, arg)) return super(ControlFlowBuilder, self).op_SETUP_LOOP(i, op, arg, *args, **kws) - def op_POP_BLOCK (self, i, op, arg, *args, **kws): - self.loop_stack.pop() - return super(ControlFlowBuilder, self).op_POP_BLOCK(i, op, arg, *args, - **kws) + def op_SETUP_WITH (self, i, op, arg, *args, **kws): + self.control_stack.append((i, op, arg)) + return super(ControlFlowBuilder, self).op_SETUP_WITH(i, op, arg, *args, + **kws) + + # ____________________________________________________________ + # Class convenience methods + + @classmethod + def build_cfg_from_co(cls, co_obj): + return cls().visit(opcode_util.build_basic_blocks(co_obj), + co_obj.co_argcount) + + @classmethod + def build_cfg(cls, func): + co_obj = opcode_util.get_code_object(func) + return cls.build_cfg_from_co(co_obj) # ______________________________________________________________________ def build_cfg (func): '''Given a Python function, create a bytecode flow, visit the flow object, and return a control flow graph.''' - co_obj = opcode_util.get_code_object(func) - return ControlFlowBuilder().visit(opcode_util.build_basic_blocks(co_obj), - co_obj.co_argcount) + return ControlFlowBuilder.build_cfg(func) # ______________________________________________________________________ # Main (self-test) routine def main (*args, **kws): - from tests import llfuncs - if not args: - args = ('doslice',) - for arg in args: - build_cfg(getattr(llfuncs, arg)).pprint() + def _visit(obj): + print("_" * 70) + print(obj) + if inspect.isfunction(obj): + cfg = build_cfg(obj) + else: + cfg = ControlFlowBuilder.build_cfg_from_co(obj) + cfg.pprint() + return opcode_util.visit_code_args(_visit, *args, **kws) # ______________________________________________________________________ diff --git a/llpython/byte_flow.py b/llpython/byte_flow.py index 197c839..0038e4d 100644 --- a/llpython/byte_flow.py +++ b/llpython/byte_flow.py @@ -1,10 +1,21 @@ # ______________________________________________________________________ + from __future__ import absolute_import import dis import opcode +import pprint +import inspect +from collections import namedtuple from .bytecode_visitor import BasicBlockVisitor from . import opcode_util +from . import byte_control + +# ______________________________________________________________________ +# Class definition(s) + +Instr = namedtuple('Instr', ('address', 'opcode', 'opname', 'oparg', + 'stackargs')) # ______________________________________________________________________ @@ -41,7 +52,7 @@ class BytecodeFlowBuilder (BasicBlockVisitor): del self.stack[-pops:] else: stk_args = [] - ret_val = (i, op, opname, arg, stk_args) + ret_val = Instr(i, op, opname, arg, stk_args) if pushes: self.stack.append(ret_val) if appends: @@ -63,13 +74,13 @@ class BytecodeFlowBuilder (BasicBlockVisitor): labels.sort() self.blocks = dict((index, []) for index in labels) - self.loop_stack = [] + self.control_stack = [] self.stacks = {} def exit_blocks (self, blocks): ret_val = self.blocks del self.stacks - del self.loop_stack + del self.control_stack del self.blocks return ret_val @@ -113,11 +124,19 @@ class BytecodeFlowBuilder (BasicBlockVisitor): op_BINARY_XOR = _op def op_BREAK_LOOP (self, i, op, arg): - loop_i, _, loop_arg = self.loop_stack[-1] - assert arg is None - return self._op(i, op, loop_i + loop_arg + 3) + if self.opnames[op] == 'BREAK_LOOP': + # Break target was already computed in control flow analysis; + # reuse that, replacing the opcode argument. + blocks_out = tuple(self.cfg.blocks_out[self.block_no]) + assert len(blocks_out) == 1 + assert arg is None + arg = blocks_out[0] + # else: Continue target is already in the argument. Note that + # the argument might not be the same as CFG destination block, + # since we might have a finally block to visit first. + return self._op(i, op, arg) - #op_BUILD_CLASS = _op + op_BUILD_CLASS = _op op_BUILD_LIST = _op op_BUILD_MAP = _op op_BUILD_SLICE = _op @@ -127,7 +146,7 @@ class BytecodeFlowBuilder (BasicBlockVisitor): op_CALL_FUNCTION_VAR = _op op_CALL_FUNCTION_VAR_KW = _op op_COMPARE_OP = _op - #op_CONTINUE_LOOP = _op + op_CONTINUE_LOOP = op_BREAK_LOOP op_DELETE_ATTR = _op op_DELETE_FAST = _op op_DELETE_GLOBAL = _op @@ -141,12 +160,29 @@ class BytecodeFlowBuilder (BasicBlockVisitor): def op_DUP_TOPX (self, i, op, arg): self.stack += self.stack[-arg:] - #op_END_FINALLY = _op + #op_DUP_TOP_TWO = _not_implemented + + # See the note regarding END_FINALLY in the definition of + # opcope_util.OPCODE_MAP. + op_END_FINALLY = _op + op_EXEC_STMT = _op - #op_EXTENDED_ARG = _op + + def op_EXTENDED_ARG (self, i, op, arg): + raise ValueError("Unexpected EXTENDED_ARG opcode at index %d (should " + "be removed by itercode)." % i) + op_FOR_ITER = _op op_GET_ITER = _op - op_IMPORT_FROM = _op + + def op_IMPORT_FROM (self, i, op, arg): + # References top of stack without popping, so we can't use the + # generic machinery. + opname = self.opmap[op][0] + ret_val = Instr(i, op, opname, arg, [self.stack[-1]]) + self.stack.append(ret_val) + return ret_val + op_IMPORT_NAME = _op op_IMPORT_STAR = _op op_INPLACE_ADD = _op @@ -166,15 +202,26 @@ class BytecodeFlowBuilder (BasicBlockVisitor): op_JUMP_FORWARD = _op def op_JUMP_IF_FALSE (self, i, op, arg): - opname, _, _, _ = self.opmap[op] - ret_val = (i, op, opname, arg, [self.stack[-1]]) + ret_val = Instr(i, op, self.opnames[op], arg, [self.stack[-1]]) self.block.append(ret_val) return ret_val + #op_JUMP_IF_FALSE_OR_POP = _not_implemented op_JUMP_IF_TRUE = op_JUMP_IF_FALSE - op_LIST_APPEND = _op + #op_JUMP_IF_TRUE_OR_POP = op_JUMP_IF_FALSE_OR_POP + + def op_LIST_APPEND (self, i, op, arg): + '''This method is used for both LIST_APPEND, and SET_ADD + opcodes.''' + elem = self.stack.pop() + container = self.stack[-arg] + ret_val = Instr(i, op, self.opnames[op], arg, [container, elem]) + self.block.append(ret_val) + return ret_val + op_LOAD_ATTR = _op - op_LOAD_CLOSURE = _op + #op_LOAD_BUILD_CLASS = _not_implemented + #op_LOAD_CLOSURE = _not_implemented op_LOAD_CONST = _op op_LOAD_DEREF = _op op_LOAD_FAST = _op @@ -186,8 +233,9 @@ class BytecodeFlowBuilder (BasicBlockVisitor): op_NOP = _op def op_POP_BLOCK (self, i, op, arg): - self.loop_stack.pop() - return self._op(i, op, arg) + _, _, _, target_stack_size = self.control_stack.pop() + pops = len(self.stack) - target_stack_size + return self._visit_op(i, op, arg, self.opnames[op], pops, 0, 1) op_POP_JUMP_IF_FALSE = _op op_POP_JUMP_IF_TRUE = _op @@ -210,19 +258,38 @@ class BytecodeFlowBuilder (BasicBlockVisitor): def op_ROT_TWO (self, i, op, arg): self.stack[-2:] = (self.stack[-1], self.stack[-2]) - #op_SETUP_EXCEPT = _op - #op_SETUP_FINALLY = _op + def _op_SETUP (self, i, op, arg): + self.control_stack.append((i, op, arg, len(self.stack))) + ret_val = Instr(i, op, self.opnames[op], arg, []) + self.block.append(ret_val) + return ret_val - def op_SETUP_LOOP (self, i, op, arg): - self.loop_stack.append((i, op, arg)) - self.block.append((i, op, self.opnames[op], arg, [])) + op_SETUP_EXCEPT = _op_SETUP + op_SETUP_FINALLY = _op_SETUP + op_SETUP_LOOP = _op_SETUP + def op_SETUP_WITH (self, i, op, arg): + assert arg is not None + # Care has to be taken here. SETUP_WITH pushes two things on + # the value stack (the exit ), and once on the handler frame. + ctx = self.stack.pop() + # We signal that the value is an exit handler by setting arg to None + exit_handler = Instr(i, op, self.opnames[op], None, [ctx]) + self.stack.append(exit_handler) + ret_val = Instr(i, op, self.opnames[op], arg, [ctx]) + self.control_stack.append((i, op, arg, len(self.stack))) + self.stack.append(ret_val) + self.block.append(ret_val) + return ret_val + + op_SET_ADD = op_LIST_APPEND op_SLICE = _op - #op_STOP_CODE = _op + #op_STOP_CODE = _not_implemented op_STORE_ATTR = _op op_STORE_DEREF = _op op_STORE_FAST = _op op_STORE_GLOBAL = _op + #op_STORE_LOCALS = _not_implemented op_STORE_MAP = _op op_STORE_NAME = _op op_STORE_SLICE = _op @@ -232,29 +299,68 @@ class BytecodeFlowBuilder (BasicBlockVisitor): op_UNARY_NEGATIVE = _op op_UNARY_NOT = _op op_UNARY_POSITIVE = _op - op_UNPACK_SEQUENCE = _op - #op_WITH_CLEANUP = _op + #op_UNPACK_EX = _not_implemented + + def op_UNPACK_SEQUENCE (self, i, op, arg): + seq = self.stack.pop() + opname = self.opnames[op] + while arg > 0: + arg -= 1 + ret_val = Instr(i, op, opname, arg, [seq]) + self.stack.append(ret_val) + return ret_val + + #op_WITH_CLEANUP = _not_implemented op_YIELD_VALUE = _op + @classmethod + def build_flow(cls, func): + '''Given a Python function, return a flow representation of that + function.''' + cfg = byte_control.build_cfg(func) + return cls().visit_cfg(cfg) + + @classmethod + def build_flow_from_co(cls, code_obj): + '''Given a Python code object, return a flow representation of + that code object.''' + cfg = byte_control.ControlFlowBuilder.build_cfg_from_co(code_obj) + return cls().visit_cfg(cfg) + + @classmethod + def build_flows_from_co(cls, root_code_obj): + '''Given a Python code object, return a map from that code + object and any nested code objects to flow representations of + those code objects.''' + return dict((co, cls.build_flow_from_co(co)) + for co in opcode_util.itercodeobjs(root_code_obj)) + +# ______________________________________________________________________ +# Function definition(s) + +def build_flow(func): + '''Kept for backwards compatibility in downstream modules. Use + BytecodeFlowBuilder.build_flow() instead.''' + return BytecodeFlowBuilder.build_flow(func) + # ______________________________________________________________________ -def build_flow (func): - '''Given a Python function, return a bytecode flow tree for that - function.''' - import byte_control - cfg = byte_control.build_cfg(func) - return BytecodeFlowBuilder().visit_cfg(cfg) +def demo_flow_builder(builder_cls, *args): + def _visit(obj): + print("_" * 70) + print(obj) + if inspect.isfunction(obj): + flow = builder_cls.build_flow(obj) + else: + flow = builder_cls.build_flow_from_co(obj) + pprint.pprint(flow) + return opcode_util.visit_code_args(_visit, *args) # ______________________________________________________________________ # Main (self-test) routine -def main (*args): - import pprint - from tests import llfuncs - if not args: - args = ('doslice',) - for arg in args: - pprint.pprint(build_flow(getattr(llfuncs, arg))) +def main(*args): + return demo_flow_builder(BytecodeFlowBuilder, *args) # ______________________________________________________________________ diff --git a/llpython/byte_translator.py b/llpython/byte_translator.py index a6e5864..c631702 100644 --- a/llpython/byte_translator.py +++ b/llpython/byte_translator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # ______________________________________________________________________ '''Defines a bytecode based LLVM translator for llpython code. ''' @@ -8,6 +9,7 @@ import opcode import types import logging +from llvm import LLVMException import llvm.core as lc from . import opcode_util @@ -38,6 +40,13 @@ _compare_mapping_sint = {'>':lc.ICMP_SGT, '<=':lc.ICMP_SLE, '!=':lc.ICMP_NE} +_compare_mapping_ptr = {'>':lc.ICMP_UGT, + '<':lc.ICMP_ULT, + '==':lc.ICMP_EQ, + '>=':lc.ICMP_UGE, + '<=':lc.ICMP_ULE, + '!=':lc.ICMP_NE} + # XXX Stolen from numba.llvm_types: class LLVMCaster (object): @@ -118,6 +127,37 @@ class LLVMCaster (object): raise NotImplementedError(lkind1, lkind2) return ret_val +# ______________________________________________________________________ + +def _convert_const(py_val): + '''Convert a constant Python value into a comparable LLVM + constant. Preserves Python values and data structures such as + lists, tuples, and None. + ''' + if isinstance(py_val, list): + ret_val = [_convert_const(child) for child in py_val] + elif isinstance(py_val, tuple): + ret_val = tuple(_convert_const(child) for child in py_val) + elif isinstance(py_val, int): + ret_val = lc.Constant.int(bytetype.lc_int, py_val) + elif isinstance(py_val, float): + ret_val = lc.Constant.double(py_val) + elif py_val == None: + ret_val = py_val + else: + raise NotImplementedError('Constant conversion for %r' % (py_val,)) + return ret_val + +# ______________________________________________________________________ + +def get_or_insert_global_variable(llvm_module, variable_ty, variable_name): + try: + ret_val = llvm_module.get_global_variable_named(variable_name) + # XXX Check LLVM value is of correct type?! + except LLVMException: + ret_val = llvm_module.add_global_variable(variable_ty, variable_name) + return ret_val + # ______________________________________________________________________ # Class definitions @@ -196,6 +236,10 @@ class LLVMTranslator (BytecodeFlowVisitor): if self.llvm_function is None: self.llvm_function = self.llvm_module.add_function( self.llvm_type, self.target_function_name) + if self.llvm_function.args and not self.llvm_function.args[0].name: + for index in range(len(self.llvm_function.args)): + argname = self.code_obj.co_varnames[index] + self.llvm_function.args[index].name = argname self.llvm_blocks = {} self.llvm_definitions = {} self.pending_phis = {} @@ -371,11 +415,12 @@ class LLVMTranslator (BytecodeFlowVisitor): raise NotImplementedError("LLVMTranslator.op_BUILD_SLICE") def op_BUILD_TUPLE (self, i, op, arg, *args, **kws): - return args + return [args] def op_CALL_FUNCTION (self, i, op, arg, *args, **kws): fn = args[0] args = args[1:] + argcount = len(args) fn_name = getattr(fn, '__name__', None) if isinstance(fn, (types.FunctionType, types.MethodType)): ret_val = [fn(self.builder, *args)] @@ -415,6 +460,9 @@ class LLVMTranslator (BytecodeFlowVisitor): elif arg1.type.kind in (lc.TYPE_FLOAT, lc.TYPE_DOUBLE): ret_val = [self.builder.fcmp(_compare_mapping_float[cmp_kind], arg1, arg2)] + elif isinstance(arg1.type, lc.PointerType): + ret_val = [self.builder.icmp(_compare_mapping_ptr[cmp_kind], + arg1, arg2)] else: raise NotImplementedError('Comparison of type %r' % (arg1.type,)) return ret_val @@ -474,17 +522,7 @@ class LLVMTranslator (BytecodeFlowVisitor): raise NotImplementedError("LLVMTranslator.op_LOAD_ATTR") def op_LOAD_CONST (self, i, op, arg, *args, **kws): - py_val = self.code_obj.co_consts[arg] - if isinstance(py_val, int): - ret_val = [lc.Constant.int(bytetype.lc_int, py_val)] - elif isinstance(py_val, float): - ret_val = [lc.Constant.double(py_val)] - elif py_val == None: - ret_val = [None] - else: - raise NotImplementedError('Constant converstion for %r' % - (py_val,)) - return ret_val + return [_convert_const(self.code_obj.co_consts[arg])] def op_LOAD_DEREF (self, i, op, arg, *args, **kws): name = self.code_obj.co_freevars[arg] @@ -540,10 +578,23 @@ class LLVMTranslator (BytecodeFlowVisitor): return [self.builder.store(store_val, dest_addr)] def op_UNARY_CONVERT (self, i, op, arg, *args, **kws): - raise NotImplementedError("LLVMTranslator.op_UNARY_CONVERT") + var_ty = args[0] + if isinstance(var_ty, lc.Type): + var_name = var_ty.__name__ + ret_val = [get_or_insert_global_variable(self.llvm_module, var_ty, + var_name)] + else: + raise NotImplementedError("LLVMTranslator.op_UNARY_CONVERT: %r" % + (var_ty,)) + return ret_val def op_UNARY_INVERT (self, i, op, arg, *args, **kws): - raise NotImplementedError("LLVMTranslator.op_UNARY_INVERT") + arg1, = args + if isinstance(arg1.type, lc.IntegerType): + ret_val = [self.builder.xor(arg1, lc.Constant.int(arg1.type, -1))] + else: + raise NotImplementedError('Invert for type %r' % (arg1.type,)) + return ret_val def op_UNARY_NEGATIVE (self, i, op, arg, *args, **kws): raise NotImplementedError("LLVMTranslator.op_UNARY_NEGATIVE") @@ -590,7 +641,7 @@ def llpython_into (llvm_function, **kws): # Main (self-test) routine def main (*args): - from tests import llfuncs, llfunctys + from .tests import llfuncs, llfunctys if not args: args = ('doslice',) elif 'all' in args: diff --git a/llpython/bytecode_visitor.py b/llpython/bytecode_visitor.py index 3fa3f48..61fafe2 100644 --- a/llpython/bytecode_visitor.py +++ b/llpython/bytecode_visitor.py @@ -170,8 +170,8 @@ class BasicBlockVisitor (BytecodeVisitor): block_indices.sort() for block_index in block_indices: self.enter_block(block_index) - for i, op, arg in blocks[block_index]: - self.visit_op(i, op, arg) + for op_tuple in blocks[block_index]: + self.visit_op(*op_tuple) self.exit_block(block_index) return self.exit_blocks(blocks) @@ -189,7 +189,7 @@ class BasicBlockVisitor (BytecodeVisitor): # ______________________________________________________________________ -class BytecodeFlowVisitor (BytecodeVisitor): +class GenericFlowVisitor (BytecodeVisitor): def visit (self, flow): self.block_list = list(flow.keys()) self.block_list.sort() @@ -208,15 +208,6 @@ class BytecodeFlowVisitor (BytecodeVisitor): del self.block_list return self.exit_flow_object(flow) - def visit_op (self, i, op, arg, *args, **kws): - new_args = [] - for child_i, child_op, _, child_arg, child_args in args: - new_args.extend(self.visit_op(child_i, child_op, child_arg, - *child_args)) - ret_val = super(BytecodeFlowVisitor, self).visit_op(i, op, arg, - *new_args) - return ret_val - def enter_flow_object (self, flow): self.new_flow = {} @@ -233,6 +224,18 @@ class BytecodeFlowVisitor (BytecodeVisitor): # ______________________________________________________________________ +class BytecodeFlowVisitor (GenericFlowVisitor): + def visit_op (self, i, op, arg, *args, **kws): + new_args = [] + for child_i, child_op, _, child_arg, child_args in args: + new_args.extend(self.visit_op(child_i, child_op, child_arg, + *child_args)) + ret_val = super(BytecodeFlowVisitor, self).visit_op(i, op, arg, + *new_args) + return ret_val + +# ______________________________________________________________________ + class BenignBytecodeVisitorMixin (object): def _do_nothing (self, i, op, arg, *args, **kws): return [(i, op, self.opnames[op], arg, args)] diff --git a/llpython/bytetype.py b/llpython/bytetype.py index 484d09e..fabcb64 100644 --- a/llpython/bytetype.py +++ b/llpython/bytetype.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # ______________________________________________________________________ import ctypes @@ -22,9 +23,10 @@ li8_ptr = lc.Type.pointer(li8) lc_int = lc.Type.int(ctypes.sizeof(ctypes.c_int) * 8) lc_long = lc.Type.int(ctypes.sizeof(ctypes.c_long) * 8) -l_pyobject_head = [lc_size_t, lc.Type.pointer(li32)] +l_pyobject_head = [lc_size_t, li8_ptr] l_pyobject_head_struct = lc.Type.struct(l_pyobject_head) l_pyobj_p = l_pyobject_head_struct_p = lc.Type.pointer(l_pyobject_head_struct) +l_pyobj_pp = lc.Type.pointer(l_pyobj_p) l_pyfunc = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) strlen = lc.Type.function(lc_size_t, (li8_ptr,)) @@ -33,10 +35,38 @@ strndup = lc.Type.function(li8_ptr, (li8_ptr, lc_size_t)) malloc = lc.Type.function(li8_ptr, (lc_size_t,)) free = lc.Type.function(lvoid, (li8_ptr,)) -Py_BuildValue = lc.Type.function(l_pyobj_p, [li8_ptr], True) PyArg_ParseTuple = lc.Type.function(lc_int, [l_pyobj_p, li8_ptr], True) +PyBool_FromLong = lc.Type.function(l_pyobj_p, [lc_long]) +PyErr_GivenExceptionMatches = lc.Type.function(lc_int, (l_pyobj_p, l_pyobj_p)) PyEval_SaveThread = lc.Type.function(li8_ptr, []) -PyEval_RestoreThread = lc.Type.function(lc.Type.void(), [li8_ptr]) +PyEval_RestoreThread = lc.Type.function(lvoid, [li8_ptr]) +PyInt_AsLong = lc.Type.function(lc_long, [l_pyobj_p]) +PyInt_FromLong = lc.Type.function(l_pyobj_p, [lc_long]) +PyNumber_Add = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_Divide = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_Multiply = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_Remainder = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_Subtract = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_TrueDivide = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_InPlaceAdd = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_InPlaceDivide = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_InPlaceMultiply = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_InPlaceRemainder = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_InPlaceSubtract = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +PyNumber_InPlaceTrueDivide = lc.Type.function(l_pyobj_p, (l_pyobj_p, + l_pyobj_p)) +PyObject_IsTrue = lc.Type.function(lc_int, [l_pyobj_p]) +PyObject_RichCompare = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p, + lc_int)) +PySequence_Contains = lc.Type.function(lc_int, (l_pyobj_p, l_pyobj_p)) +PyString_Check = lc.Type.function(lc_int, [l_pyobj_p]) +PyString_CheckExact = lc.Type.function(lc_int, [l_pyobj_p]) +PyString_Format = lc.Type.function(l_pyobj_p, (l_pyobj_p, l_pyobj_p)) +Py_BuildValue = lc.Type.function(l_pyobj_p, [li8_ptr], True) +Py_DecRef = lc.Type.function(lvoid, [l_pyobj_p]) +Py_IncRef = lc.Type.function(lvoid, [l_pyobj_p]) + +PyInt_Type = li8 # ______________________________________________________________________ # End of bytetype.py diff --git a/llpython/control_flow.py b/llpython/control_flow.py index 8026aba..def320e 100644 --- a/llpython/control_flow.py +++ b/llpython/control_flow.py @@ -30,12 +30,12 @@ class ControlFlowGraph (object): def unlink_unreachables (self): changed = True - next_blocks = self.blocks.keys() + next_blocks = set(self.blocks.keys()) next_blocks.remove(0) while changed: changed = False blocks = next_blocks - next_blocks = blocks[:] + next_blocks = blocks.copy() for block in blocks: if len(self.blocks_in[block]) == 0: blocks_out = self.blocks_out[block] @@ -100,15 +100,25 @@ class ControlFlowGraph (object): if hasattr(self, 'reaching_definitions'): del self.reaching_definitions - def idom (self, block): - '''Compute the immediate dominator (idom) of the given block - key. Returns None if the block has no in edges. + def get_a_dom (self, block): + '''Find an immediate predecessor of the given block such that + the predecessor is either the only entry point, or the + precessor is not in its own dominance set (a non-loop + predecessor). Returns None if the given block has no + predecessor. - Note that in the case where there are multiple immediate - dominators (a join after a non-loop branch), this returns one - of the predecessors, but is not guaranteed to reliably select - one over the others (depends on the ordering of the set type - iterator).''' + Note that in the case where there are multiple dominators (a + join after a non-loop branch), this returns one of the + predecessors, but is not guaranteed to reliably select one + over the others (depends on the ordering of the set type + iterator). + + Note: Previously, this method's documentation erroneously + identified the return value as being the immediate dominator + of the input block. Instead, it attempts to find a "nearby" + dominator. Normally, the immediate dominator of a join is the + least upperbound of the closed immediate dominance + relationship over its two entrants.''' preds = self.blocks_in[block] npreds = len(preds) if npreds == 0: @@ -142,7 +152,7 @@ class ControlFlowGraph (object): ret_val = {} for pred in preds: ret_val[pred] = self.block_writes_to_writer_map(pred) - crnt = self.idom(pred) + crnt = self.get_a_dom(pred) while crnt != None: crnt_writer_map = self.block_writes_to_writer_map(crnt) # This order of update favors the first definitions @@ -150,7 +160,7 @@ class ControlFlowGraph (object): # visits blocks in reverse execution order. crnt_writer_map.update(ret_val[pred]) ret_val[pred] = crnt_writer_map - crnt = self.idom(crnt) + crnt = self.get_a_dom(crnt) if not has_memoized: self.reaching_definitions = {} self.reaching_definitions[block] = ret_val diff --git a/llpython/opcode_util.py b/llpython/opcode_util.py index 7a1972b..5737fc0 100644 --- a/llpython/opcode_util.py +++ b/llpython/opcode_util.py @@ -1,147 +1,173 @@ # ______________________________________________________________________ +from collections import namedtuple import dis import opcode +import inspect # ______________________________________________________________________ # Module data -hasjump = opcode.hasjrel + opcode.hasjabs -hascbranch = [op for op in hasjump - if 'IF' in opcode.opname[op] - or opcode.opname[op] in ('FOR_ITER', 'SETUP_LOOP')] +# Note that opcode.hasjrel and opcode.hasjabs applies only to opcodes +# that calculate a jump point based on the argument. This ignores +# jumps that use the frame stack to calculate their targets, and +# exceptions. + +NON_ARG_JUMP_NAMES = ('BREAK_LOOP', 'RETURN_VALUE', 'END_FINALLY', + 'RAISE_VARARGS') +NON_ARG_JUMPS = [opcode.opmap[opname] + for opname in NON_ARG_JUMP_NAMES + if opname in opcode.opmap] +HAS_CBRANCH_NAMES = 'FOR_ITER', +hasjump = opcode.hasjrel + opcode.hasjabs + NON_ARG_JUMPS +hascbranch = [op for op, opname in ((op, opcode.opname[op]) + for op in hasjump) + if 'IF' in opname + or 'SETUP' in opname + or opname in HAS_CBRANCH_NAMES] + +OpcodeData = namedtuple('OpcodeData', ('pops', 'pushes', 'is_stmt')) +NO_OPCODE_DATA = OpcodeData(None, None, None) # Since the actual opcode value may change, manage opcode abstraction # data by opcode name. OPCODE_MAP = { - 'BINARY_ADD': (2, 1, None), - 'BINARY_AND': (2, 1, None), - 'BINARY_DIVIDE': (2, 1, None), - 'BINARY_FLOOR_DIVIDE': (2, 1, None), - 'BINARY_LSHIFT': (2, 1, None), - 'BINARY_MODULO': (2, 1, None), - 'BINARY_MULTIPLY': (2, 1, None), - 'BINARY_OR': (2, 1, None), - 'BINARY_POWER': (2, 1, None), - 'BINARY_RSHIFT': (2, 1, None), - 'BINARY_SUBSCR': (2, 1, None), - 'BINARY_SUBTRACT': (2, 1, None), - 'BINARY_TRUE_DIVIDE': (2, 1, None), - 'BINARY_XOR': (2, 1, None), - 'BREAK_LOOP': (0, None, 1), - 'BUILD_CLASS': (None, None, None), - 'BUILD_LIST': (-1, 1, None), - 'BUILD_MAP': (None, None, None), - 'BUILD_SET': (None, None, None), - 'BUILD_SLICE': (None, None, None), - 'BUILD_TUPLE': (-1, 1, None), - 'CALL_FUNCTION': (-2, 1, None), - 'CALL_FUNCTION_KW': (-3, 1, None), - 'CALL_FUNCTION_VAR': (-3, 1, None), - 'CALL_FUNCTION_VAR_KW': (-4, 1, None), - 'COMPARE_OP': (2, 1, None), - 'CONTINUE_LOOP': (None, None, None), - 'DELETE_ATTR': (1, None, 1), - 'DELETE_DEREF': (None, None, None), - 'DELETE_FAST': (0, None, 1), - 'DELETE_GLOBAL': (0, None, 1), - 'DELETE_NAME': (0, None, 1), - 'DELETE_SLICE+0': (1, None, 1), - 'DELETE_SLICE+1': (2, None, 1), - 'DELETE_SLICE+2': (2, None, 1), - 'DELETE_SLICE+3': (3, None, 1), - 'DELETE_SUBSCR': (2, None, 1), - 'DUP_TOP': (None, None, None), - 'DUP_TOPX': (None, None, None), - 'DUP_TOP_TWO': (None, None, None), - 'END_FINALLY': (None, None, None), - 'EXEC_STMT': (None, None, None), - 'EXTENDED_ARG': (None, None, None), - 'FOR_ITER': (1, 1, 1), - 'GET_ITER': (1, 1, None), - 'IMPORT_FROM': (None, None, None), - 'IMPORT_NAME': (None, None, None), - 'IMPORT_STAR': (1, None, 1), - 'INPLACE_ADD': (2, 1, None), - 'INPLACE_AND': (2, 1, None), - 'INPLACE_DIVIDE': (2, 1, None), - 'INPLACE_FLOOR_DIVIDE': (2, 1, None), - 'INPLACE_LSHIFT': (2, 1, None), - 'INPLACE_MODULO': (2, 1, None), - 'INPLACE_MULTIPLY': (2, 1, None), - 'INPLACE_OR': (2, 1, None), - 'INPLACE_POWER': (2, 1, None), - 'INPLACE_RSHIFT': (2, 1, None), - 'INPLACE_SUBTRACT': (2, 1, None), - 'INPLACE_TRUE_DIVIDE': (2, 1, None), - 'INPLACE_XOR': (2, 1, None), - 'JUMP_ABSOLUTE': (0, None, 1), - 'JUMP_FORWARD': (0, None, 1), - 'JUMP_IF_FALSE': (1, 1, 1), - 'JUMP_IF_FALSE_OR_POP': (None, None, None), - 'JUMP_IF_TRUE': (1, 1, 1), - 'JUMP_IF_TRUE_OR_POP': (None, None, None), - 'LIST_APPEND': (2, 0, 1), - 'LOAD_ATTR': (1, 1, None), - 'LOAD_BUILD_CLASS': (None, None, None), - 'LOAD_CLOSURE': (None, None, None), - 'LOAD_CONST': (0, 1, None), - 'LOAD_DEREF': (0, 1, None), - 'LOAD_FAST': (0, 1, None), - 'LOAD_GLOBAL': (0, 1, None), - 'LOAD_LOCALS': (None, None, None), - 'LOAD_NAME': (0, 1, None), - 'MAKE_CLOSURE': (None, None, None), - 'MAKE_FUNCTION': (-2, 1, None), - 'MAP_ADD': (None, None, None), - 'NOP': (0, None, None), - 'POP_BLOCK': (0, None, 1), - 'POP_EXCEPT': (None, None, None), - 'POP_JUMP_IF_FALSE': (1, None, 1), - 'POP_JUMP_IF_TRUE': (1, None, 1), - 'POP_TOP': (1, None, 1), - 'PRINT_EXPR': (1, None, 1), - 'PRINT_ITEM': (1, None, 1), - 'PRINT_ITEM_TO': (2, None, 1), - 'PRINT_NEWLINE': (0, None, 1), - 'PRINT_NEWLINE_TO': (1, None, 1), - 'RAISE_VARARGS': (None, None, None), - 'RETURN_VALUE': (1, None, 1), - 'ROT_FOUR': (None, None, None), - 'ROT_THREE': (None, None, None), - 'ROT_TWO': (None, None, None), - 'SETUP_EXCEPT': (None, None, None), - 'SETUP_FINALLY': (None, None, None), - 'SETUP_LOOP': (None, None, None), - 'SETUP_WITH': (None, None, None), - 'SET_ADD': (None, None, None), - 'SLICE+0': (1, 1, None), - 'SLICE+1': (2, 1, None), - 'SLICE+2': (2, 1, None), - 'SLICE+3': (3, 1, None), - 'STOP_CODE': (None, None, None), - 'STORE_ATTR': (2, None, 1), - 'STORE_DEREF': (1, 0, 1), - 'STORE_FAST': (1, None, 1), - 'STORE_GLOBAL': (1, None, 1), - 'STORE_LOCALS': (None, None, None), - 'STORE_MAP': (1, None, 1), - 'STORE_NAME': (1, None, 1), - 'STORE_SLICE+0': (1, None, 1), - 'STORE_SLICE+1': (2, None, 1), - 'STORE_SLICE+2': (2, None, 1), - 'STORE_SLICE+3': (3, None, 1), - 'STORE_SUBSCR': (3, None, 1), - 'UNARY_CONVERT': (1, 1, None), - 'UNARY_INVERT': (1, 1, None), - 'UNARY_NEGATIVE': (1, 1, None), - 'UNARY_NOT': (1, 1, None), - 'UNARY_POSITIVE': (1, 1, None), - 'UNPACK_EX': (None, None, None), - 'UNPACK_SEQUENCE': (None, None, None), - 'WITH_CLEANUP': (None, None, None), - 'YIELD_VALUE': (1, None, 1), + 'BINARY_ADD': OpcodeData(2, 1, None), + 'BINARY_AND': OpcodeData(2, 1, None), + 'BINARY_DIVIDE': OpcodeData(2, 1, None), + 'BINARY_FLOOR_DIVIDE': OpcodeData(2, 1, None), + 'BINARY_LSHIFT': OpcodeData(2, 1, None), + 'BINARY_MODULO': OpcodeData(2, 1, None), + 'BINARY_MULTIPLY': OpcodeData(2, 1, None), + 'BINARY_OR': OpcodeData(2, 1, None), + 'BINARY_POWER': OpcodeData(2, 1, None), + 'BINARY_RSHIFT': OpcodeData(2, 1, None), + 'BINARY_SUBSCR': OpcodeData(2, 1, None), + 'BINARY_SUBTRACT': OpcodeData(2, 1, None), + 'BINARY_TRUE_DIVIDE': OpcodeData(2, 1, None), + 'BINARY_XOR': OpcodeData(2, 1, None), + 'BREAK_LOOP': OpcodeData(0, None, 1), + 'BUILD_CLASS': OpcodeData(3, 1, None), + 'BUILD_LIST': OpcodeData(-1, 1, None), + 'BUILD_MAP': OpcodeData(-1, 1, None), + 'BUILD_SET': OpcodeData(-1, 1, None), + 'BUILD_SLICE': OpcodeData(-1, 1, None), # oparg should only be 2 or 3 + 'BUILD_TUPLE': OpcodeData(-1, 1, None), + 'CALL_FUNCTION': OpcodeData(-2, 1, None), + 'CALL_FUNCTION_KW': OpcodeData(-3, 1, None), + 'CALL_FUNCTION_VAR': OpcodeData(-3, 1, None), + 'CALL_FUNCTION_VAR_KW': OpcodeData(-4, 1, None), + 'COMPARE_OP': OpcodeData(2, 1, None), + 'CONTINUE_LOOP': OpcodeData(0, None, 1), + 'DELETE_ATTR': OpcodeData(1, None, 1), + 'DELETE_DEREF': NO_OPCODE_DATA, + 'DELETE_FAST': OpcodeData(0, None, 1), + 'DELETE_GLOBAL': OpcodeData(0, None, 1), + 'DELETE_NAME': OpcodeData(0, None, 1), + 'DELETE_SLICE+0': OpcodeData(1, None, 1), + 'DELETE_SLICE+1': OpcodeData(2, None, 1), + 'DELETE_SLICE+2': OpcodeData(2, None, 1), + 'DELETE_SLICE+3': OpcodeData(3, None, 1), + 'DELETE_SUBSCR': OpcodeData(2, None, 1), + 'DUP_TOP': NO_OPCODE_DATA, + 'DUP_TOPX': NO_OPCODE_DATA, + 'DUP_TOP_TWO': NO_OPCODE_DATA, + + # The data for END_FINALLY is a total fabrication; END_FINALLY may + # pop 1 or 3 values off the value stack, based on the type of the + # top of the value stack. If, however, a value stack simulator + # ignores the part of the CPython evaluator loop that pushes the + # why code on the value stack for WHY_RETURN and WHY_CONTINUE (as + # this table does), this should work out fine. + 'END_FINALLY': OpcodeData(0, 0, 1), + + 'EXEC_STMT': OpcodeData(3, 0, 1), + 'EXTENDED_ARG': NO_OPCODE_DATA, + 'FOR_ITER': OpcodeData(1, 1, 1), + 'GET_ITER': OpcodeData(1, 1, None), + 'IMPORT_FROM': NO_OPCODE_DATA, + 'IMPORT_NAME': OpcodeData(2, 1, None), + 'IMPORT_STAR': OpcodeData(1, None, 1), + 'INPLACE_ADD': OpcodeData(2, 1, None), + 'INPLACE_AND': OpcodeData(2, 1, None), + 'INPLACE_DIVIDE': OpcodeData(2, 1, None), + 'INPLACE_FLOOR_DIVIDE': OpcodeData(2, 1, None), + 'INPLACE_LSHIFT': OpcodeData(2, 1, None), + 'INPLACE_MODULO': OpcodeData(2, 1, None), + 'INPLACE_MULTIPLY': OpcodeData(2, 1, None), + 'INPLACE_OR': OpcodeData(2, 1, None), + 'INPLACE_POWER': OpcodeData(2, 1, None), + 'INPLACE_RSHIFT': OpcodeData(2, 1, None), + 'INPLACE_SUBTRACT': OpcodeData(2, 1, None), + 'INPLACE_TRUE_DIVIDE': OpcodeData(2, 1, None), + 'INPLACE_XOR': OpcodeData(2, 1, None), + 'JUMP_ABSOLUTE': OpcodeData(0, None, 1), + 'JUMP_FORWARD': OpcodeData(0, None, 1), + 'JUMP_IF_FALSE': OpcodeData(1, 1, 1), + 'JUMP_IF_FALSE_OR_POP': NO_OPCODE_DATA, + 'JUMP_IF_TRUE': OpcodeData(1, 1, 1), + 'JUMP_IF_TRUE_OR_POP': NO_OPCODE_DATA, + 'LIST_APPEND': NO_OPCODE_DATA, + 'LOAD_ATTR': OpcodeData(1, 1, None), + 'LOAD_BUILD_CLASS': NO_OPCODE_DATA, + 'LOAD_CLOSURE': NO_OPCODE_DATA, + 'LOAD_CONST': OpcodeData(0, 1, None), + 'LOAD_DEREF': OpcodeData(0, 1, None), + 'LOAD_FAST': OpcodeData(0, 1, None), + 'LOAD_GLOBAL': OpcodeData(0, 1, None), + 'LOAD_LOCALS': OpcodeData(0, 1, None), + 'LOAD_NAME': OpcodeData(0, 1, None), + 'MAKE_CLOSURE': NO_OPCODE_DATA, + 'MAKE_FUNCTION': OpcodeData(-2, 1, None), + 'MAP_ADD': NO_OPCODE_DATA, + 'NOP': OpcodeData(0, None, None), + 'POP_BLOCK': OpcodeData(0, None, 1), + 'POP_EXCEPT': NO_OPCODE_DATA, + 'POP_JUMP_IF_FALSE': OpcodeData(1, None, 1), + 'POP_JUMP_IF_TRUE': OpcodeData(1, None, 1), + 'POP_TOP': OpcodeData(1, None, 1), + 'PRINT_EXPR': OpcodeData(1, None, 1), + 'PRINT_ITEM': OpcodeData(1, None, 1), + 'PRINT_ITEM_TO': OpcodeData(2, None, 1), + 'PRINT_NEWLINE': OpcodeData(0, None, 1), + 'PRINT_NEWLINE_TO': OpcodeData(1, None, 1), + 'RAISE_VARARGS': OpcodeData(-1, None, 1), + 'RETURN_VALUE': OpcodeData(1, None, 1), + 'ROT_FOUR': NO_OPCODE_DATA, + 'ROT_THREE': NO_OPCODE_DATA, + 'ROT_TWO': NO_OPCODE_DATA, + 'SETUP_EXCEPT': NO_OPCODE_DATA, + 'SETUP_FINALLY': NO_OPCODE_DATA, + 'SETUP_LOOP': NO_OPCODE_DATA, + 'SETUP_WITH': NO_OPCODE_DATA, + 'SET_ADD': NO_OPCODE_DATA, + 'SLICE+0': OpcodeData(1, 1, None), + 'SLICE+1': OpcodeData(2, 1, None), + 'SLICE+2': OpcodeData(2, 1, None), + 'SLICE+3': OpcodeData(3, 1, None), + 'STOP_CODE': NO_OPCODE_DATA, + 'STORE_ATTR': OpcodeData(2, None, 1), + 'STORE_DEREF': OpcodeData(1, 0, 1), + 'STORE_FAST': OpcodeData(1, None, 1), + 'STORE_GLOBAL': OpcodeData(1, None, 1), + 'STORE_LOCALS': NO_OPCODE_DATA, + 'STORE_MAP': OpcodeData(1, None, 1), + 'STORE_NAME': OpcodeData(1, None, 1), + 'STORE_SLICE+0': OpcodeData(1, None, 1), + 'STORE_SLICE+1': OpcodeData(2, None, 1), + 'STORE_SLICE+2': OpcodeData(2, None, 1), + 'STORE_SLICE+3': OpcodeData(3, None, 1), + 'STORE_SUBSCR': OpcodeData(3, None, 1), + 'UNARY_CONVERT': OpcodeData(1, 1, None), + 'UNARY_INVERT': OpcodeData(1, 1, None), + 'UNARY_NEGATIVE': OpcodeData(1, 1, None), + 'UNARY_NOT': OpcodeData(1, 1, None), + 'UNARY_POSITIVE': OpcodeData(1, 1, None), + 'UNPACK_EX': NO_OPCODE_DATA, + 'UNPACK_SEQUENCE': NO_OPCODE_DATA, + 'WITH_CLEANUP': NO_OPCODE_DATA, + 'YIELD_VALUE': OpcodeData(1, None, 1), } # ______________________________________________________________________ @@ -167,6 +193,7 @@ def itercode(code, start = 0): i = i + 2 if op == opcode.EXTENDED_ARG: extended_arg = oparg * 65536 + continue delta = yield num, op, oparg if delta is not None: @@ -176,6 +203,50 @@ def itercode(code, start = 0): # ______________________________________________________________________ +def itercodeobjs(codeobj): + "Iterator that traverses code objects via the co_consts member." + yield codeobj + for const in codeobj.co_consts: + if inspect.iscode(const): + for childobj in itercodeobjs(const): + yield childobj + +# ______________________________________________________________________ + +def visit_code_args(visitor, *args, **kws): + """Utility function for testing or demonstrating various code + analysis passes in llpython. + + Takes a visitor function and a sequence of command line arguments. + The visitor function should be able to handle either function + objects or code objects.""" + def _visit_code_objs(root_obj): + for code_obj in itercodeobjs(root_obj): + visitor(code_obj) + try: + from .tests import llfuncs + except ImportError: + llfuncs = object() + if not args: + if 'default_args' in kws: + args = kws['default_args'] + else: + args = ('pymod',) + for arg in args: + if inspect.iscode(arg): + _visit_code_objs(arg) + elif inspect.isfunction(arg): + _visit_code_objs(get_code_object(arg)) + elif arg.endswith('.py'): + with open(arg) as in_file: + in_source = in_file.read() + in_codeobj = compile(in_source, arg, 'exec') + _visit_code_objs(in_codeobj) + else: + visitor(getattr(llfuncs, arg)) + +# ______________________________________________________________________ + def extendlabels(code, labels = None): """Extend the set of jump target labels to account for the passthrough targets of conditional branches. @@ -194,15 +265,8 @@ def extendlabels(code, labels = None): i += 1 if op >= dis.HAVE_ARGUMENT: i += 2 - label = -1 - if op in hasjump: - label = i - if label >= 0: - if label not in labels: - labels.append(label) - elif op == opcode.opmap['BREAK_LOOP']: - if i not in labels: - labels.append(i) + if op in hasjump and i < n and i not in labels: + labels.append(i) return labels # ______________________________________________________________________ @@ -221,5 +285,12 @@ def build_basic_blocks (co_obj): labels + [len(co_code)])) return blocks +# ______________________________________________________________________ + +def get_nargs(co_obj): + flags = co_obj.co_flags + return (1 + co_obj.co_argcount + (1 if flags & 4 else 0) + + (1 if flags & 8 else 0)) + # ______________________________________________________________________ # End of opcode_util.py diff --git a/llpython/tests/test_addr_flow.py b/llpython/tests/test_addr_flow.py new file mode 100644 index 0000000..93db19d --- /dev/null +++ b/llpython/tests/test_addr_flow.py @@ -0,0 +1,31 @@ +#! /usr/bin/env python +# ______________________________________________________________________ + +from __future__ import absolute_import + +import unittest + +from llpython import addr_flow + +from . import test_byte_flow + +# ______________________________________________________________________ +# Class (test case) definition(s) + +class TestAddressFlowBuilder(unittest.TestCase, test_byte_flow.FlowTestMixin): + BUILDER_CLS = addr_flow.AddressFlowBuilder + + def fail_unless_valid_instruction(self, instr): + super(TestAddressFlowBuilder, self).fail_unless_valid_instruction( + instr) + for arg_addr in instr[-1]: + self.fail_unless_valid_address(arg_addr) + +# ______________________________________________________________________ +# Main (unit test) routine + +if __name__ == "__main__": + unittest.main() + +# ______________________________________________________________________ +# End of test_addr_flow.py diff --git a/llpython/tests/test_all.py b/llpython/tests/test_all.py new file mode 100644 index 0000000..82aa9c8 --- /dev/null +++ b/llpython/tests/test_all.py @@ -0,0 +1,16 @@ +#! /usr/bin/env python +# ______________________________________________________________________ + +import unittest + +from .test_byte_control import TestByteControl +from .test_byte_flow import TestBytecodeFlowBuilder +from .test_addr_flow import TestAddressFlowBuilder + +# ______________________________________________________________________ + +if __name__ == "__main__": + unittest.main() + +# ______________________________________________________________________ +# End of llpython/tests/test_all.py diff --git a/llpython/tests/test_byte_control.py b/llpython/tests/test_byte_control.py new file mode 100644 index 0000000..3ee3e32 --- /dev/null +++ b/llpython/tests/test_byte_control.py @@ -0,0 +1,279 @@ +#! /usr/bin/env python +# ______________________________________________________________________ + +from __future__ import absolute_import + +import sys +import unittest + +from llpython import byte_control + +# ______________________________________________________________________ +# Global data + +# Technically we could also compute if we need special logic for the +# old bytecode compiler by scanning for JUMP_IF_TRUE and JUMP_IF_FALSE +# opcodes. These opcodes require additional POP_TOP's be inserted. +OLD_BYTECODE_COMPILER = sys.version_info < (2, 7) + +got_done = 0 + +# ______________________________________________________________________ +# Utility function definitions + +def do_something(): + global got_done + got_done += 1 + print("Something good got done.") + return got_done + +# ____________________________________________________________ + +def do_something_else(): + raise Exception("Something bad got done, and I don't like it") + +# ______________________________________________________________________ +# Test function definitions + +def try_finally_0(m, n): # why == WHY_RETURN + try: + return n - m + finally: + do_something() + return do_something_else() + +# ____________________________________________________________ + +def try_finally_1(m, n): # why == WHY_BREAK + i = -1 + for i in range(m, n): + try: + if i == 101: + break + finally: + do_something() + return i + +# ____________________________________________________________ + +def try_finally_2(m, n): # why == WHY_CONTINUE + i = m + while i < n: + try: + if i == 101: + i += 200 + continue + finally: + do_something() + i += 1 + return i + +# ____________________________________________________________ + +def try_finally_3(m, n): # why == WHY_EXCEPTION (or WHY_RETURN) + d = {} + try: + return d[n] - d[m] + finally: + do_something() + return do_something_else() + +# ____________________________________________________________ + +def try_finally_4(m, n): # why == WHY_NOT + try: + rv = n - m + finally: + do_something() + return rv + +# ____________________________________________________________ + +def try_finally_5(m, n): + for i in range(m, n): + try: + if i == 99: + break + elif i == 121: + continue + elif i == 86: + return + elif i < -102: + raise ValueError(i) + else: + try: + if i < 0: + raise ValueError(i) + finally: + do_something() + finally: + do_something() + return do_something() + +# ______________________________________________________________________ +# Class (test case) definition(s) + +class TestByteControl(unittest.TestCase): + def fail_unless_cfg_match(self, test_cfg, block_count, edges): + assert len(test_cfg.blocks) == block_count + block_keys = list(test_cfg.blocks.keys()) + block_keys.sort() + expected_blocks_in = dict((block_key, set()) + for block_key in block_keys) + expected_blocks_out = dict((block_key, set()) + for block_key in block_keys) + for from_block_ofs, to_block_ofs in edges: + from_block = block_keys[from_block_ofs] + to_block = block_keys[to_block_ofs] + expected_blocks_in[to_block].add(from_block) + expected_blocks_out[from_block].add(to_block) + for block_key in block_keys: + expected_in = expected_blocks_in[block_key] + test_in = test_cfg.blocks_in[block_key] + self.assertEqual( + expected_in, test_in, '%r != %r for blocks_in[%d]' % ( + test_in, expected_in, block_key)) + expected_out = expected_blocks_out[block_key] + test_out = test_cfg.blocks_out[block_key] + self.assertEqual( + expected_out, test_out, '%r != %r for set blocks_out[%d]' % ( + test_out, expected_out, block_key)) + + def test_raise(self): + cfg = byte_control.build_cfg(do_something_else) + self.fail_unless_cfg_match(cfg, 2, ()) + + def test_try_finally_0(self): + """ + Expected CFG (Python 2.7+): + digraph CFG_try_finally_0 { + BLOCK_0 -> BLOCK_3; // 0 -> 1 + BLOCK_0 -> BLOCK_15; // 0 -> 3 + BLOCK_3 -> BLOCK_15; // 1 -> 3 + // DEAD: BLOCK_11 -> BLOCK_15; // 2 -> 3 + BLOCK_15 -> BLOCK_23; // 3 -> 4, why == WHY_NOT + BLOCK_23; // 4 + } + (Possibly terminal blocks: 15, 23.) + """ + cfg = byte_control.build_cfg(try_finally_0) + self.fail_unless_cfg_match(cfg, 5, ((0, 1), (0, 3), (1, 3), + (3, 4))) + + def test_try_finally_1(self): + """ + Expected CFG (Python 2.7+): + digraph CFG_try_finally_1 { + BLOCK_0 -> BLOCK_9; // 0 -> 1 + BLOCK_0 -> BLOCK_63; // 0 -> 11 + BLOCK_9 -> BLOCK_22; // 1 -> 2 + BLOCK_22 -> BLOCK_25; // 2 -> 3 + BLOCK_22 -> BLOCK_62; // 2 -> 10 + BLOCK_25 -> BLOCK_31; // 3 -> 4 + BLOCK_25 -> BLOCK_51; // 3 -> 8 + BLOCK_31 -> BLOCK_43; // 4 -> 5 + BLOCK_31 -> BLOCK_47; // 4 -> 7 + BLOCK_43 -> BLOCK_51; // 5 -> 8 + // DEAD: BLOCK_44 -> BLOCK_47; // 6 -> 7 + BLOCK_47 -> BLOCK_51; // 7 -> 8 + BLOCK_51 -> BLOCK_59; // 8 -> 9, why == WHY_NOT + BLOCK_51 -> BLOCK_63; // 8 -> 11, why == WHY_BREAK, WHY_RETURN, ... + BLOCK_59 -> BLOCK_22; // 9 -> 2 + BLOCK_62 -> BLOCK_63; // 10 -> 11 + BLOCK_63; // 11 + } + (Possibly terminal blocks: 51, 63.) + """ + cfg = byte_control.build_cfg(try_finally_1) + if not OLD_BYTECODE_COMPILER: + self.fail_unless_cfg_match( + cfg, 12, ((0, 1), (0, 11), (1, 2), (2, 3), (2, 10), (3, 4), + (3, 8), (4, 5), (4, 7), (5, 8), (7, 8), + (8, 9), (8, 11), (9, 2), (10, 11))) + else: + self.fail_unless_cfg_match( + cfg, 13, ((0, 1), (0, 12), (1, 2), (2, 3), (2, 11), (3, 4), + (3, 9), (4, 5), (4, 7), (5, 9), (7, 8), + (8, 9), (9, 10), (9, 12), (10, 2), (11, 12))) + + def test_try_finally_2(self): + """ + Expected CFG (Python 2.7+): + digraph CFG_try_finally_2 { + BLOCK_0 -> BLOCK_9; // 0 -> 1 + BLOCK_0 -> BLOCK_78; // 0 -> 10 + BLOCK_9 -> BLOCK_21; // 1 -> 2 + BLOCK_9 -> BLOCK_77; // 1 -> 9 + BLOCK_21 -> BLOCK_24; // 2 -> 3 + BLOCK_21 -> BLOCK_56; // 2 -> 7 + BLOCK_24 -> BLOCK_36; // 3 -> 4 + BLOCK_24 -> BLOCK_52; // 3 -> 6 + BLOCK_36 -> BLOCK_56; // 4 -> 7 + // DEAD: BLOCK_49 -> BLOCK_52; // 5 -> 6 + BLOCK_52 -> BLOCK_56; // 6 -> 7 + BLOCK_56 -> BLOCK_9; // 7 -> 1, why == WHY_CONTINUE + BLOCK_56 -> BLOCK_64; // 7 -> 8, why == WHY_NOT + BLOCK_64 -> BLOCK_9; // 8 -> 1 + BLOCK_77 -> BLOCK_78; // 9 -> 10 + BLOCK_78; // 10 + } + (Possibly terminal blocks: 56, 78.) + """ + cfg = byte_control.build_cfg(try_finally_2) + if not OLD_BYTECODE_COMPILER: + self.fail_unless_cfg_match( + cfg, 11, ((0, 1), (0, 10), (1, 2), (1, 9), (2, 3), (2, 7), + (3, 4), (3, 6), (4, 7), (6, 7), (7, 1), + (7, 8), (8, 1), (9, 10))) + + def test_try_finally_3(self): + """ + Expected (Python 2.7+): + digraph CFG_foo3 { + BLOCK_0 -> BLOCK_9; // 0 -> 1 + BLOCK_0 -> BLOCK_29; // 0 -> 3 + BLOCK_9 -> BLOCK_29; // 1 -> 3 + // DEAD: BLOCK_25 -> BLOCK_29; // 2 -> 3 + BLOCK_29 -> BLOCK_37; // 3 -> 4, why == WHY_NOT + BLOCK_37; // 4 + } + (Possibly terminal blocks: 29, 37.) + """ + cfg = byte_control.build_cfg(try_finally_3) + self.fail_unless_cfg_match(cfg, 5, ((0, 1), (0, 3), (1, 3), + (3, 4))) + + def test_try_finally_4(self): + """ + Expected: + digraph CFG_foo4 { + BLOCK_0 -> BLOCK_3; // 0 -> 1 + BLOCK_0 -> BLOCK_17; // 0 -> 2 + BLOCK_3 -> BLOCK_17; // 1 -> 2 + BLOCK_17 -> BLOCK_25; // 2 -> 3, why == WHY_NOT + BLOCK_25; // 3 + } + (Possibly terminal blocks: 17, 25.) + """ + cfg = byte_control.build_cfg(try_finally_4) + self.fail_unless_cfg_match(cfg, 4, ((0, 1), (0, 2), (1, 2), (2, 3))) + + def test_try_finally_5(self): + cfg = byte_control.build_cfg(try_finally_5) + if not OLD_BYTECODE_COMPILER: + self.fail_unless_cfg_match( + cfg, 26, ((0, 1), (0, 25), (1, 2), (2, 24), (2, 3), (3, 4), + (3, 22), (4, 5), (4, 7), (5, 22), (7, 8), + (7, 10), (8, 22), (10, 11), (10, 12), + (11, 22), (12, 13), (12, 15), (13, 22), + (15, 16), (15, 20), (16, 17), (16, 19), (17, 20), + (19, 20), (20, 21), (20, 22), (21, 22), + (22, 25), (22, 2), (22, 23), (23, 2), (24, 25))) + +# ______________________________________________________________________ + +if __name__ == "__main__": + unittest.main() + +# ______________________________________________________________________ +# End of test_byte_control.py diff --git a/llpython/tests/test_byte_flow.py b/llpython/tests/test_byte_flow.py new file mode 100644 index 0000000..8c4decb --- /dev/null +++ b/llpython/tests/test_byte_flow.py @@ -0,0 +1,101 @@ +#! /usr/bin/env python +# ______________________________________________________________________ + +from __future__ import absolute_import + +import unittest + +from llpython import byte_flow +from llpython import opcode_util + +from . import test_byte_control as tbc +from . import llfuncs + +# ______________________________________________________________________ +# Class (test case) definition(s) + +class FlowTestMixin(object): + + def fail_unless_valid_address(self, address): + self.failUnless(address >= 0) + self.failUnless(address < self.max_addr) + self.failUnless(address in self.valid_addrs) + + def fail_unless_valid_instruction(self, instr): + address = instr[0] + self.visited.add(address) + self.fail_unless_valid_address(instr[0]) + + def fail_unless_valid_flow(self, flow, func): + self.failUnless(len(flow) > 0) + func_code = opcode_util.get_code_object(func).co_code + self.valid_addrs = set(addr for addr, _, _ in + opcode_util.itercode(func_code)) + self.visited = set() + self.max_addr = len(func_code) + for block_index, block_instrs in flow.items(): + self.failUnless(block_index < self.max_addr) + for instr in block_instrs: + self.fail_unless_valid_instruction(instr) + del self.max_addr + # Make sure that all instructions identified by itercode were + # checked at least once; they should be represented in the + # resulting flow, even if their basic block is unreachable. + self.failUnless(self.valid_addrs == self.visited, + 'Failed to visit following addresses: %r' % + (self.valid_addrs - self.visited)) + del self.visited + del self.valid_addrs + return flow + + def build_and_test_flow(self, func): + return self.fail_unless_valid_flow( + self.BUILDER_CLS.build_flow(func), func) + + def test_doslice(self): + self.build_and_test_flow(llfuncs.doslice) + + def test_ipow(self): + self.build_and_test_flow(llfuncs.ipow) + + def test_pymod(self): + self.build_and_test_flow(llfuncs.pymod) + + def test_try_finally_0(self): + self.build_and_test_flow(tbc.try_finally_0) + + def test_try_finally_1(self): + self.build_and_test_flow(tbc.try_finally_1) + + def test_try_finally_2(self): + self.build_and_test_flow(tbc.try_finally_2) + + def test_try_finally_3(self): + self.build_and_test_flow(tbc.try_finally_3) + + def test_try_finally_4(self): + self.build_and_test_flow(tbc.try_finally_4) + + def test_try_finally_5(self): + self.build_and_test_flow(tbc.try_finally_5) + +# ______________________________________________________________________ + +class TestBytecodeFlowBuilder(unittest.TestCase, FlowTestMixin): + + BUILDER_CLS = byte_flow.BytecodeFlowBuilder + + def fail_unless_valid_instruction(self, instr): + super(TestBytecodeFlowBuilder, self).fail_unless_valid_instruction( + instr) + for child_instr in instr[-1]: + self.fail_unless_valid_instruction(child_instr) + +# ______________________________________________________________________ +# Main (unit test) routine + +if __name__ == "__main__": + unittest.main() + +# ______________________________________________________________________ +# End of test_byte_flow.py diff --git a/llpython/type_flow.py b/llpython/type_flow.py new file mode 100644 index 0000000..c87f3c4 --- /dev/null +++ b/llpython/type_flow.py @@ -0,0 +1,284 @@ +#! /usr/bin/env python +# ______________________________________________________________________ +# Module imports + +from .bytecode_visitor import BasicBlockVisitor, BenignBytecodeVisitorMixin + +DEBUG_SIMPLIFY = False + +if DEBUG_SIMPLIFY: + from pprint import pprint as pp + +# ______________________________________________________________________ +# Class definition(s) + +class TypeFlowBuilder(BenignBytecodeVisitorMixin, BasicBlockVisitor): + def __init__(self, co_obj, *args, **kws): + super(TypeFlowBuilder, self).__init__(*args, **kws) + self.co_obj = co_obj + self.locals = {} + self.globals = {} + self.refs = {} + self.type_flow = {} + self.requirements = {} + + def get_type_eqns(self): + return self.type_flow, self.requirements, self.locals, self.globals + + def simplify(self): + """ + This method isn't working as intended. It should simplify + strongly connected components s.t. instead of outputing + several types like the following: + + {0: set(['in0']), + 3: set(['in1']), + ... + 10: set([0, 3, 34, 37, 62, 65, 'in0', 'in1']), + ... + 34: set([0, 3, 34, 37, 62, 65, 'in0', 'in1']), + 37: set(['in1']), + ... + 62: set([0, 3, 34, 37, 62, 65, 'in0', 'in1']), + 65: set(['in1']), + ... + 75: set([0, 3, 34, 37, 62, 65, 'in0', 'in1']), + ...} + + It outputs the following: + + {0: set(['in0']), + 3: set(['in1']), + ... + 10: set(['in0', 'in1']), + ... + 34: set(['in0', 'in1']), + 37: set(['in1']), + ... + 62: set(['in0', 'in1']), + 65: set(['in1']), + ... + 75: set(['in0', 'in1']), + ...} + """ + if not DEBUG_SIMPLIFY: + raise NotImplementedError("See docstring.") + type_flow = self.type_flow + changed = True + while changed: + changed = False + next_flow = type_flow.copy() + for index, types in type_flow.items(): + if isinstance(types, set): + next_types = set.union( + *(type_flow.get(child_index, + set([child_index])) + for child_index in types)) + else: + next_types = set([types]) + if next_types != types: + next_flow[index] = next_types + changed = True + pp(next_flow) + print() + type_flow = next_flow + return type_flow + + def _op(self, i, op, opname, arg, args, *extras, **kws): + self.type_flow[i] = set(args) + + op_BINARY_ADD = _op + op_BINARY_AND = _op + op_BINARY_DIVIDE = _op + op_BINARY_FLOOR_DIVIDE = _op + op_BINARY_LSHIFT = _op + op_BINARY_MODULO = _op + op_BINARY_MULTIPLY = _op + op_BINARY_OR = _op + op_BINARY_POWER = _op + op_BINARY_RSHIFT = _op + op_BINARY_SUBSCR = _op + op_BINARY_SUBTRACT = _op + op_BINARY_TRUE_DIVIDE = _op + op_BINARY_XOR = _op + + #op_BUILD_CLASS = _do_nothing + #op_BUILD_LIST = _do_nothing + #op_BUILD_MAP = _do_nothing + #op_BUILD_SET = _do_nothing + #op_BUILD_SLICE = _do_nothing + #op_BUILD_TUPLE = _do_nothing + + #op_CALL_FUNCTION = _do_nothing + #op_CALL_FUNCTION_KW = _do_nothing + #op_CALL_FUNCTION_VAR = _do_nothing + #op_CALL_FUNCTION_VAR_KW = _do_nothing + + def op_COMPARE_OP(self, i, op, opname, arg, args, *extras, **kws): + self.requirements[i] = set(args) + self.type_flow[i] = bool + + #op_CONTINUE_LOOP = _do_nothing + #op_DELETE_ATTR = _do_nothing + #op_DELETE_DEREF = _do_nothing + #op_DELETE_FAST = _do_nothing + #op_DELETE_GLOBAL = _do_nothing + #op_DELETE_NAME = _do_nothing + #op_DELETE_SLICE = _do_nothing + #op_DELETE_SUBSCR = _do_nothing + #op_END_FINALLY = _do_nothing + #op_EXEC_STMT = _do_nothing + #op_EXTENDED_ARG = _do_nothing + #op_FOR_ITER = _do_nothing + #op_GET_ITER = _do_nothing + #op_IMPORT_FROM = _do_nothing + #op_IMPORT_NAME = _do_nothing + #op_IMPORT_STAR = _do_nothing + + op_INPLACE_ADD = _op + op_INPLACE_AND = _op + op_INPLACE_DIVIDE = _op + op_INPLACE_FLOOR_DIVIDE = _op + op_INPLACE_LSHIFT = _op + op_INPLACE_MODULO = _op + op_INPLACE_MULTIPLY = _op + op_INPLACE_OR = _op + op_INPLACE_POWER = _op + op_INPLACE_RSHIFT = _op + op_INPLACE_SUBTRACT = _op + op_INPLACE_TRUE_DIVIDE = _op + op_INPLACE_XOR = _op + + #op_JUMP_ABSOLUTE = _do_nothing + #op_JUMP_FORWARD = _do_nothing + #op_JUMP_IF_FALSE = _do_nothing + #op_JUMP_IF_FALSE_OR_POP = _do_nothing + #op_JUMP_IF_TRUE = _do_nothing + #op_JUMP_IF_TRUE_OR_POP = _do_nothing + #op_LIST_APPEND = _do_nothing + #op_LOAD_ATTR = _do_nothing + #op_LOAD_BUILD_CLASS = _do_nothing + #op_LOAD_CLOSURE = _do_nothing + + def op_LOAD_CONST(self, i, op, opname, arg, args, *extras, **kws): + self.type_flow[i] = type(self.co_obj.co_consts[arg]) + + def op_LOAD_DEREF(self, i, op, opname, arg, args, *extras, **kws): + if arg not in self.refs: + result = set(('inref%d' % arg,)) + self.refs[arg] = result + else: + result = self.refs[arg] + self.type_flow[i] = result + + def op_LOAD_FAST(self, i, op, opname, arg, args, *extras, **kws): + if arg not in self.locals: + if arg < self.co_obj.co_argcount: + result = set(('in%d' % arg,)) + else: + result = set() + self.locals[arg] = result + else: + result = self.locals[arg] + self.type_flow[i] = result + + def op_LOAD_GLOBAL(self, i, op, opname, arg, args, *extras, **kws): + if arg not in self.globals: + result = set(('in%d' % arg,)) + self.globals[arg] = result + else: + result = self.globals[arg] + self.type_flow[i] = result + + #op_LOAD_LOCALS = _do_nothing + #op_LOAD_NAME = _do_nothing + #op_MAKE_CLOSURE = _do_nothing + #op_MAKE_FUNCTION = _do_nothing + #op_MAP_ADD = _do_nothing + #op_NOP = _do_nothing + #op_POP_BLOCK = _do_nothing + #op_POP_EXCEPT = _do_nothing + #op_POP_JUMP_IF_FALSE = _do_nothing + #op_POP_JUMP_IF_TRUE = _do_nothing + #op_POP_TOP = _do_nothing + #op_PRINT_EXPR = _do_nothing + #op_PRINT_ITEM = _do_nothing + #op_PRINT_ITEM_TO = _do_nothing + #op_PRINT_NEWLINE = _do_nothing + #op_PRINT_NEWLINE_TO = _do_nothing + #op_RAISE_VARARGS = _do_nothing + + op_RETURN_VALUE = _op + + #op_SETUP_EXCEPT = _do_nothing + #op_SETUP_FINALLY = _do_nothing + #op_SETUP_LOOP = _do_nothing + #op_SETUP_WITH = _do_nothing + #op_SET_ADD = _do_nothing + + #op_SLICE = _do_nothing + + #op_STOP_CODE = _do_nothing + #op_STORE_ATTR = _do_nothing + #op_STORE_DEREF = _do_nothing + + def op_STORE_FAST(self, i, op, opname, arg, args, *extras, **kws): + if arg not in self.locals: + if arg < self.co_obj.co_argcount: + result = set(('in%d' % arg,)) + else: + result = set() + self.locals[arg] = result + else: + result = self.locals[arg] + assert len(args) == 1 + result.add(args[0]) + + #op_STORE_GLOBAL = _do_nothing + #op_STORE_LOCALS = _do_nothing + #op_STORE_MAP = _do_nothing + #op_STORE_NAME = _do_nothing + #op_STORE_SLICE = _do_nothing + #op_STORE_SUBSCR = _do_nothing + + op_UNARY_CONVERT = _op + op_UNARY_INVERT = _op + op_UNARY_NEGATIVE = _op + op_UNARY_NOT = _op + op_UNARY_POSITIVE = _op + + #op_UNPACK_EX = _do_nothing + #op_UNPACK_SEQUENCE = _do_nothing + #op_WITH_CLEANUP = _do_nothing + #op_YIELD_VALUE = _do_nothing + +# ______________________________________________________________________ +# Function definition(s) + +def build_type_flow(func): + from .opcode_util import get_code_object + from .addr_flow import AddressFlowBuilder + blocks = AddressFlowBuilder.build_flow(func) + ty_builder = TypeFlowBuilder(get_code_object(func)) + ty_builder.visit(blocks) + return ty_builder.get_type_eqns() + +# ______________________________________________________________________ +# Main (self-test) routine + +def main(*args): + import pprint + from .tests import llfuncs + if not args: + args = ('pymod',) + for arg in args: + pprint.pprint(build_type_flow(getattr(llfuncs, arg))) + +# ______________________________________________________________________ + +if __name__ == "__main__": + import sys + main(*sys.argv[1:]) + +# ______________________________________________________________________ +# End of type_flow.py