diff --git a/llpython/byte_flow.py b/llpython/byte_flow.py index d6a8267..b37d39d 100644 --- a/llpython/byte_flow.py +++ b/llpython/byte_flow.py @@ -10,6 +10,7 @@ from . import opcode_util from . import byte_control # ______________________________________________________________________ +# Class definition(s) class BytecodeFlowBuilder (BasicBlockVisitor): '''Transforms a CFG into a bytecode "flow tree". @@ -116,10 +117,17 @@ class BytecodeFlowBuilder (BasicBlockVisitor): op_BINARY_XOR = _op def op_BREAK_LOOP (self, i, op, arg): - # XXX Not sure this is correct. - loop_i, _, loop_arg, _ = self.control_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_LIST = _op @@ -131,7 +139,7 @@ class BytecodeFlowBuilder (BasicBlockVisitor): op_CALL_FUNCTION_VAR = _op op_CALL_FUNCTION_VAR_KW = _op op_COMPARE_OP = _op - #op_CONTINUE_LOOP = _not_implemented + op_CONTINUE_LOOP = op_BREAK_LOOP op_DELETE_ATTR = _op op_DELETE_FAST = _op op_DELETE_GLOBAL = _op @@ -146,7 +154,11 @@ class BytecodeFlowBuilder (BasicBlockVisitor): self.stack += self.stack[-arg:] #op_DUP_TOP_TWO = _not_implemented - #op_END_FINALLY = _not_implemented + + # See the note regarding END_FINALLY in the definition of + # opcope_util.OPCODE_MAP. + op_END_FINALLY = _op + op_EXEC_STMT = _op def op_EXTENDED_ARG (self, i, op, arg): @@ -249,7 +261,20 @@ class BytecodeFlowBuilder (BasicBlockVisitor): op_SETUP_FINALLY = _op_SETUP op_SETUP_LOOP = _op_SETUP - #op_SETUP_WITH = _not_implemented + 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 = i, op, self.opnames[op], None, [ctx] + self.stack.append(exit_handler) + ret_val = 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 = _not_implemented diff --git a/llpython/opcode_util.py b/llpython/opcode_util.py index b6ccf11..657c851 100644 --- a/llpython/opcode_util.py +++ b/llpython/opcode_util.py @@ -60,7 +60,7 @@ OPCODE_MAP = { 'CALL_FUNCTION_VAR': OpcodeData(-3, 1, None), 'CALL_FUNCTION_VAR_KW': OpcodeData(-4, 1, None), 'COMPARE_OP': OpcodeData(2, 1, None), - 'CONTINUE_LOOP': NO_OPCODE_DATA, + 'CONTINUE_LOOP': OpcodeData(0, None, 1), 'DELETE_ATTR': OpcodeData(1, None, 1), 'DELETE_DEREF': NO_OPCODE_DATA, 'DELETE_FAST': OpcodeData(0, None, 1), @@ -74,7 +74,15 @@ OPCODE_MAP = { 'DUP_TOP': NO_OPCODE_DATA, 'DUP_TOPX': NO_OPCODE_DATA, 'DUP_TOP_TWO': NO_OPCODE_DATA, - 'END_FINALLY': 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), @@ -109,7 +117,7 @@ OPCODE_MAP = { 'LOAD_DEREF': OpcodeData(0, 1, None), 'LOAD_FAST': OpcodeData(0, 1, None), 'LOAD_GLOBAL': OpcodeData(0, 1, None), - 'LOAD_LOCALS': NO_OPCODE_DATA, + 'LOAD_LOCALS': OpcodeData(0, 1, None), 'LOAD_NAME': OpcodeData(0, 1, None), 'MAKE_CLOSURE': NO_OPCODE_DATA, 'MAKE_FUNCTION': OpcodeData(-2, 1, None), diff --git a/llpython/tests/test_addr_flow.py b/llpython/tests/test_addr_flow.py index 774e853..93db19d 100644 --- a/llpython/tests/test_addr_flow.py +++ b/llpython/tests/test_addr_flow.py @@ -5,42 +5,21 @@ from __future__ import absolute_import import unittest -from llpython import addr_flow, opcode_util +from llpython import addr_flow -from . import test_byte_control as tbc +from . import test_byte_flow # ______________________________________________________________________ # Class (test case) definition(s) -class TestAddrFlow(unittest.TestCase): - def fail_unless_valid_flow(self, flow): - raise NotImplementedError("XXX") - # TODO: Make sure child indices are valid bytecode addresses - # TODO: Make sure opcode has a "reasonable" number of child indices +class TestAddressFlowBuilder(unittest.TestCase, test_byte_flow.FlowTestMixin): + BUILDER_CLS = addr_flow.AddressFlowBuilder - def test_try_finally_0(self): - self.fail_unless_valid_flow( - addr_flow.build_addr_flow(tbc.try_finally_0)) - - def test_try_finally_1(self): - self.fail_unless_valid_flow( - addr_flow.build_addr_flow(tbc.try_finally_1)) - - def test_try_finally_2(self): - self.fail_unless_valid_flow( - addr_flow.build_addr_flow(tbc.try_finally_2)) - - def test_try_finally_3(self): - self.fail_unless_valid_flow( - addr_flow.build_addr_flow(tbc.try_finally_3)) - - def test_try_finally_4(self): - self.fail_unless_valid_flow( - addr_flow.build_addr_flow(tbc.try_finally_4)) - - def test_try_finally_5(self): - self.fail_unless_valid_flow( - addr_flow.build_addr_flow(tbc.try_finally_5)) + 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 diff --git a/llpython/tests/test_byte_flow.py b/llpython/tests/test_byte_flow.py new file mode 100644 index 0000000..471813e --- /dev/null +++ b/llpython/tests/test_byte_flow.py @@ -0,0 +1,98 @@ +#! /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 + + def build_and_test_flow(self, func): + 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