import dis

import sys

from cStringIO import StringIO

import unittest



def disassemble(func):

    f = StringIO()

    tmp = sys.stdout

    sys.stdout = f

    dis.dis(func)

    sys.stdout = tmp

    result = f.getvalue()

    f.close()

    return result



def dis_single(line):

    return disassemble(compile(line, '', 'single'))



class TestTranforms(unittest.TestCase):



    def test_unot(self):

        # UNARY_NOT JUMP_IF_FALSE POP_TOP  -->  JUMP_IF_TRUE POP_TOP'

        def unot(x):

            if not x == 2:

                del x

        asm = disassemble(unot)

        for elem in ('UNARY_NOT', 'JUMP_IF_FALSE'):

            self.assert_(elem not in asm)

        for elem in ('JUMP_IF_TRUE', 'POP_TOP'):

            self.assert_(elem in asm)



    def test_elim_inversion_of_is_or_in(self):

        for line, elem in (

            ('not a is b', '(is not)',),

            ('not a in b', '(not in)',),

            ('not a is not b', '(is)',),

            ('not a not in b', '(in)',),

            ):

            asm = dis_single(line)

            self.assert_(elem in asm)



    def test_none_as_constant(self):

        # LOAD_GLOBAL None  -->  LOAD_CONST None

        def f(x):

            None

            return x

        asm = disassemble(f)

        for elem in ('LOAD_GLOBAL',):

            self.assert_(elem not in asm)

        for elem in ('LOAD_CONST', '(None)'):

            self.assert_(elem in asm)

        def f():

            'Adding a docstring made this test fail in Py2.5.0'

            return None

        self.assert_('LOAD_CONST' in disassemble(f))

        self.assert_('LOAD_GLOBAL' not in disassemble(f))



    def test_while_one(self):

        # Skip over:  LOAD_CONST trueconst  JUMP_IF_FALSE xx  POP_TOP

        def f():

            while 1:

                pass

            return list

        asm = disassemble(f)

        for elem in ('LOAD_CONST', 'JUMP_IF_FALSE'):

            self.assert_(elem not in asm)

        for elem in ('JUMP_ABSOLUTE',):

            self.assert_(elem in asm)



    def test_pack_unpack(self):

        for line, elem in (

            ('a, = a,', 'LOAD_CONST',),

            ('a, b = a, b', 'ROT_TWO',),

            ('a, b, c = a, b, c', 'ROT_THREE',),

            ):

            asm = dis_single(line)

            self.assert_(elem in asm)

            self.assert_('BUILD_TUPLE' not in asm)

            self.assert_('UNPACK_TUPLE' not in asm)



    def test_folding_of_tuples_of_constants(self):

        for line, elem in (

            ('a = 1,2,3', '((1, 2, 3))'),

            ('("a","b","c")', "(('a', 'b', 'c'))"),

            ('a,b,c = 1,2,3', '((1, 2, 3))'),

            ('(None, 1, None)', '((None, 1, None))'),

            ('((1, 2), 3, 4)', '(((1, 2), 3, 4))'),

            ):

            asm = dis_single(line)

            self.assert_(elem in asm)

            self.assert_('BUILD_TUPLE' not in asm)



        # Bug 1053819:  Tuple of constants misidentified when presented with:

        # . . . opcode_with_arg 100   unary_opcode   BUILD_TUPLE 1  . . .

        # The following would segfault upon compilation

        def crater():

            (~[

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

                0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

            ],)



    def test_folding_of_binops_on_constants(self):

        for line, elem in (

            ('a = 2+3+4', '(9)'),                   # chained fold

            ('"@"*4', "('@@@@')"),                  # check string ops

            ('a="abc" + "def"', "('abcdef')"),      # check string ops

            ('a = 3**4', '(81)'),                   # binary power

            ('a = 3*4', '(12)'),                    # binary multiply

            ('a = 13//4', '(3)'),                   # binary floor divide

            ('a = 14%4', '(2)'),                    # binary modulo

            ('a = 2+3', '(5)'),                     # binary add

            ('a = 13-4', '(9)'),                    # binary subtract

            ('a = (12,13)[1]', '(13)'),             # binary subscr

            ('a = 13 << 2', '(52)'),                # binary lshift

            ('a = 13 >> 2', '(3)'),                 # binary rshift

            ('a = 13 & 7', '(5)'),                  # binary and

            ('a = 13 ^ 7', '(10)'),                 # binary xor

            ('a = 13 | 7', '(15)'),                 # binary or

            ):

            asm = dis_single(line)

            self.assert_(elem in asm, asm)

            self.assert_('BINARY_' not in asm)



        # Verify that unfoldables are skipped

        asm = dis_single('a=2+"b"')

        self.assert_('(2)' in asm)

        self.assert_("('b')" in asm)



        # Verify that large sequences do not result from folding

        asm = dis_single('a="x"*1000')

        self.assert_('(1000)' in asm)



    def test_folding_of_unaryops_on_constants(self):

        for line, elem in (

            ('`1`', "('1')"),                       # unary convert

            ('-0.5', '(-0.5)'),                     # unary negative

            ('~-2', '(1)'),                         # unary invert

        ):

            asm = dis_single(line)

            self.assert_(elem in asm, asm)

            self.assert_('UNARY_' not in asm)



        # Verify that unfoldables are skipped

        for line, elem in (

            ('-"abc"', "('abc')"),                  # unary negative

            ('~"abc"', "('abc')"),                  # unary invert

        ):

            asm = dis_single(line)

            self.assert_(elem in asm, asm)

            self.assert_('UNARY_' in asm)



    def test_elim_extra_return(self):

        # RETURN LOAD_CONST None RETURN  -->  RETURN

        def f(x):

            return x

        asm = disassemble(f)

        self.assert_('LOAD_CONST' not in asm)

        self.assert_('(None)' not in asm)

        self.assertEqual(asm.split().count('RETURN_VALUE'), 1)



    def test_elim_jump_to_return(self):

        # JUMP_FORWARD to RETURN -->  RETURN

        def f(cond, true_value, false_value):

            return true_value if cond else false_value

        asm = disassemble(f)

        self.assert_('JUMP_FORWARD' not in asm)

        self.assert_('JUMP_ABSOLUTE' not in asm)

        self.assertEqual(asm.split().count('RETURN_VALUE'), 2)



    def test_elim_jump_after_return1(self):

        # Eliminate dead code: jumps immediately after returns can't be reached

        def f(cond1, cond2):

            if cond1: return 1

            if cond2: return 2

            while 1:

                return 3

            while 1:

                if cond1: return 4

                return 5

            return 6

        asm = disassemble(f)

        self.assert_('JUMP_FORWARD' not in asm)

        self.assert_('JUMP_ABSOLUTE' not in asm)

        self.assertEqual(asm.split().count('RETURN_VALUE'), 6)



    def test_elim_jump_after_return2(self):

        # Eliminate dead code: jumps immediately after returns can't be reached

        def f(cond1, cond2):

            while 1:

                if cond1: return 4

        asm = disassemble(f)

        self.assert_('JUMP_FORWARD' not in asm)

        # There should be one jump for the while loop.

        self.assertEqual(asm.split().count('JUMP_ABSOLUTE'), 1)

        self.assertEqual(asm.split().count('RETURN_VALUE'), 2)





def test_main(verbose=None):

    import sys

    from test import test_support

    test_classes = (TestTranforms,)

    test_support.run_unittest(*test_classes)



    # verify reference counting

    if verbose and hasattr(sys, "gettotalrefcount"):

        import gc

        counts = [None] * 5

        for i in xrange(len(counts)):

            test_support.run_unittest(*test_classes)

            gc.collect()

            counts[i] = sys.gettotalrefcount()

        print counts



if __name__ == "__main__":

    test_main(verbose=True)

