summaryrefslogtreecommitdiffstats
path: root/Lib/test/test_peepholer.py
blob: 1cacdea5692cecebe2afd7cce0d722202f27e3c3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
import dis
import re
import sys
from io import StringIO
import unittest
from math import copysign

def disassemble(func):
    f = StringIO()
    tmp = sys.stdout
    sys.stdout = f
    try:
        dis.dis(func)
    finally:
        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)

        # Long tuples should be folded too.
        asm = dis_single(repr(tuple(range(10000))))
        # One LOAD_CONST for the tuple, one for the None return value
        self.assertEqual(asm.count('LOAD_CONST'), 2)
        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_binary_subscr_on_unicode(self):
        # valid code get optimized
        asm = dis_single('"foo"[0]')
        self.assertIn("('f')", asm)
        self.assertNotIn('BINARY_SUBSCR', asm)
        asm = dis_single('"\u0061\uffff"[1]')
        self.assertIn("('\\uffff')", asm)
        self.assertNotIn('BINARY_SUBSCR', asm)
        asm = dis_single('"\U00012345abcdef"[3]')
        self.assertIn("('c')", asm)
        self.assertNotIn('BINARY_SUBSCR', asm)

        # invalid code doesn't get optimized
        # out of range
        asm = dis_single('"fuu"[10]')
        self.assertIn('BINARY_SUBSCR', asm)

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

        # Check that -0.0 works after marshaling
        def negzero():
            return -(1.0-1.0)

        self.assertNotIn('UNARY_', disassemble(negzero))
        self.assertTrue(copysign(1.0, negzero()) < 0)

        # 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_constant_folding(self):
        # Issue #11244: aggressive constant folding.
        exprs = [
            "3 * -5",
            "-3 * 5",
            "2 * (3 * 4)",
            "(2 * 3) * 4",
            "(-1, 2, 3)",
            "(1, -2, 3)",
            "(1, 2, -3)",
            "(1, 2, -3) * 6",
            "lambda x: x in {(3 * -5) + (-1 - 6), (1, -2, 3) * 2, None}",
        ]
        for e in exprs:
            asm = dis_single(e)
            self.assertNotIn('UNARY_', asm, e)
            self.assertNotIn('BINARY_', asm, e)
            self.assertNotIn('BUILD_', asm, e)

class TestBuglets(unittest.TestCase):

    def test_bug_11510(self):
        # folded constant set optimization was commingled with the tuple
        # unpacking optimization which would fail if the set had duplicate
        # elements so that the set length was unexpected
        def f():
            x, y = {1, 1}
            return x, y
        with self.assertRaises(ValueError):
            f()


def test_main(verbose=None):
    import sys
    from test import support
    test_classes = (TestTranforms, TestBuglets)
    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)