import dis
import re
import sys
from io 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 POP_JUMP_IF_FALSE  -->  POP_JUMP_IF_TRUE'
        def unot(x):
            if not x == 2:
                del x
        asm = disassemble(unot)
        for elem in ('UNARY_NOT', 'POP_JUMP_IF_FALSE'):
            self.assertNotIn(elem, asm)
        for elem in ('POP_JUMP_IF_TRUE',):
            self.assertIn(elem, 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.assertIn(elem, asm)

    def test_global_as_constant(self):
        # LOAD_GLOBAL None/True/False  -->  LOAD_CONST None/True/False
        def f(x):
            None
            None
            return x
        def g(x):
            True
            return x
        def h(x):
            False
            return x
        for func, name in ((f, 'None'), (g, 'True'), (h, 'False')):
            asm = disassemble(func)
            for elem in ('LOAD_GLOBAL',):
                self.assertNotIn(elem, asm)
            for elem in ('LOAD_CONST', '('+name+')'):
                self.assertIn(elem, asm)
        def f():
            'Adding a docstring made this test fail in Py2.5.0'
            return None
        self.assertIn('LOAD_CONST', disassemble(f))
        self.assertNotIn('LOAD_GLOBAL', disassemble(f))

    def test_while_one(self):
        # Skip over:  LOAD_CONST trueconst  POP_JUMP_IF_FALSE xx
        def f():
            while 1:
                pass
            return list
        asm = disassemble(f)
        for elem in ('LOAD_CONST', 'POP_JUMP_IF_FALSE'):
            self.assertNotIn(elem, asm)
        for elem in ('JUMP_ABSOLUTE',):
            self.assertIn(elem, 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.assertIn(elem, asm)
            self.assertNotIn('BUILD_TUPLE', asm)
            self.assertNotIn('UNPACK_TUPLE', 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.assertIn(elem, asm)
            self.assertNotIn('BUILD_TUPLE', 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_lists_of_constants(self):
        for line, elem in (
            # in/not in constants with BUILD_LIST should be folded to a tuple:
            ('a in [1,2,3]', '(1, 2, 3)'),
            ('a not in ["a","b","c"]', "(('a', 'b', 'c'))"),
            ('a in [None, 1, None]', '((None, 1, None))'),
            ('a not in [(1, 2), 3, 4]', '(((1, 2), 3, 4))'),
            ):
            asm = dis_single(line)
            self.assertIn(elem, asm)
            self.assertNotIn('BUILD_LIST', asm)

    def test_folding_of_sets_of_constants(self):
        for line, elem in (
            # in/not in constants with BUILD_SET should be folded to a frozenset:
            ('a in {1,2,3}', frozenset({1, 2, 3})),
            ('a not in {"a","b","c"}', frozenset({'a', 'c', 'b'})),
            ('a in {None, 1, None}', frozenset({1, None})),
            ('a not in {(1, 2), 3, 4}', frozenset({(1, 2), 3, 4})),
            ('a in {1, 2, 3, 3, 2, 1}', frozenset({1, 2, 3})),
            ):
            asm = dis_single(line)
            self.assertNotIn('BUILD_SET', asm)

            # Verify that the frozenset 'elem' is in the disassembly
            # The ordering of the elements in repr( frozenset ) isn't
            # guaranteed, so we jump through some hoops to ensure that we have
            # the frozenset we expect:
            self.assertIn('frozenset', asm)
            # Extract the frozenset literal from the disassembly:
            m = re.match(r'.*(frozenset\({.*}\)).*', asm, re.DOTALL)
            self.assertTrue(m)
            self.assertEqual(eval(m.group(1)), elem)

        # Ensure that the resulting code actually works:
        def f(a):
            return a in {1, 2, 3}

        def g(a):
            return a not in {1, 2, 3}

        self.assertTrue(f(3))
        self.assertTrue(not f(4))

        self.assertTrue(not g(3))
        self.assertTrue(g(4))


    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.assertIn(elem, asm, asm)
            self.assertNotIn('BINARY_', asm)

        # Verify that unfoldables are skipped
        asm = dis_single('a=2+"b"')
        self.assertIn('(2)', asm)
        self.assertIn("('b')", asm)

        # Verify that large sequences do not result from folding
        asm = dis_single('a="x"*1000')
        self.assertIn('(1000)', asm)

    def test_folding_of_unaryops_on_constants(self):
        for line, elem in (
            ('-0.5', '(-0.5)'),                     # unary negative
            ('~-2', '(1)'),                         # unary invert
            ('+1', '(1)'),                          # unary positive
        ):
            asm = dis_single(line)
            self.assertIn(elem, asm, asm)
            self.assertNotIn('UNARY_', asm)

        # Verify that unfoldables are skipped
        for line, elem in (
            ('-"abc"', "('abc')"),                  # unary negative
            ('~"abc"', "('abc')"),                  # unary invert
        ):
            asm = dis_single(line)
            self.assertIn(elem, asm, asm)
            self.assertIn('UNARY_', asm)

    def test_elim_extra_return(self):
        # RETURN LOAD_CONST None RETURN  -->  RETURN
        def f(x):
            return x
        asm = disassemble(f)
        self.assertNotIn('LOAD_CONST', asm)
        self.assertNotIn('(None)', 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.assertNotIn('JUMP_FORWARD', asm)
        self.assertNotIn('JUMP_ABSOLUTE', 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.assertNotIn('JUMP_FORWARD', asm)
        self.assertNotIn('JUMP_ABSOLUTE', 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.assertNotIn('JUMP_FORWARD', 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_make_function_doesnt_bail(self):
        def f():
            def g()->1+1:
                pass
            return g
        asm = disassemble(f)
        self.assertNotIn('BINARY_ADD', asm)


def test_main(verbose=None):
    import sys
    from test import support
    test_classes = (TestTranforms,)
    support.run_unittest(*test_classes)

    # verify reference counting
    if verbose and hasattr(sys, "gettotalrefcount"):
        import gc
        counts = [None] * 5
        for i in range(len(counts)):
            support.run_unittest(*test_classes)
            gc.collect()
            counts[i] = sys.gettotalrefcount()
        print(counts)

if __name__ == "__main__":
    test_main(verbose=True)
