gh-126835: Refine constant folding tests in test_peepholer.py::TestTranforms (#131826)

This commit is contained in:
Yan Yanchii 2025-03-28 11:10:22 +01:00 committed by GitHub
parent 8bd88e2827
commit 674dbf3b3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -150,7 +150,7 @@ def test_pack_unpack(self):
self.assertNotInBytecode(code, 'UNPACK_SEQUENCE')
self.check_lnotab(code)
def test_folding_of_tuples_of_constants(self):
def test_constant_folding_tuples_of_constants(self):
for line, elem in (
('a = 1,2,3', (1, 2, 3)),
('("a","b","c")', ('a', 'b', 'c')),
@ -191,7 +191,7 @@ def crater():
],)
self.check_lnotab(crater)
def test_folding_of_lists_of_constants(self):
def test_constant_folding_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)),
@ -205,7 +205,7 @@ def test_folding_of_lists_of_constants(self):
self.assertNotInBytecode(code, 'BUILD_LIST')
self.check_lnotab(code)
def test_folding_of_sets_of_constants(self):
def test_constant_folding_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})),
@ -235,56 +235,175 @@ def g(a):
self.assertTrue(g(4))
self.check_lnotab(g)
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
):
with self.subTest(line=line):
code = compile(line, '', 'single')
if isinstance(elem, int):
self.assertInBytecode(code, 'LOAD_SMALL_INT', elem)
def test_constant_folding_small_int(self):
tests = [
('(0, )[0]', 0),
('(1 + 2, )[0]', 3),
('(2 + 2 * 2, )[0]', 6),
('(1, (1 + 2 + 3, ))[1][0]', 6),
('1 + 2', 3),
('2 + 2 * 2 // 2 - 2', 2),
('(255, )[0]', 255),
('(256, )[0]', None),
('(1000, )[0]', None),
('(1 - 2, )[0]', None),
('255 + 0', 255),
('255 + 1', None),
('-1', None),
('--1', 1),
('--255', 255),
('--256', None),
('~1', None),
('~~1', 1),
('~~255', 255),
('~~256', None),
('++255', 255),
('++256', None),
]
for expr, oparg in tests:
with self.subTest(expr=expr, oparg=oparg):
code = compile(expr, '', 'single')
if oparg is not None:
self.assertInBytecode(code, 'LOAD_SMALL_INT', oparg)
else:
self.assertInBytecode(code, 'LOAD_CONST', elem)
for instr in dis.get_instructions(code):
self.assertFalse(instr.opname.startswith('BINARY_'))
self.assertNotInBytecode(code, 'LOAD_SMALL_INT')
self.check_lnotab(code)
# Verify that unfoldables are skipped
code = compile('a=2+"b"', '', 'single')
self.assertInBytecode(code, 'LOAD_SMALL_INT', 2)
self.assertInBytecode(code, 'LOAD_CONST', 'b')
self.check_lnotab(code)
def test_constant_folding_unaryop(self):
intrinsic_positive = 5
tests = [
('-0', 'UNARY_NEGATIVE', None, True, 'LOAD_SMALL_INT', 0),
('-0.0', 'UNARY_NEGATIVE', None, True, 'LOAD_CONST', -0.0),
('-(1.0-1.0)', 'UNARY_NEGATIVE', None, True, 'LOAD_CONST', -0.0),
('-0.5', 'UNARY_NEGATIVE', None, True, 'LOAD_CONST', -0.5),
('---1', 'UNARY_NEGATIVE', None, True, 'LOAD_CONST', -1),
('---""', 'UNARY_NEGATIVE', None, False, None, None),
('~~~1', 'UNARY_INVERT', None, True, 'LOAD_CONST', -2),
('~~~""', 'UNARY_INVERT', None, False, None, None),
('not not True', 'UNARY_NOT', None, True, 'LOAD_CONST', True),
('not not x', 'UNARY_NOT', None, True, 'LOAD_NAME', 'x'), # this should be optimized regardless of constant or not
('+++1', 'CALL_INTRINSIC_1', intrinsic_positive, True, 'LOAD_SMALL_INT', 1),
('---x', 'UNARY_NEGATIVE', None, False, None, None),
('~~~x', 'UNARY_INVERT', None, False, None, None),
('+++x', 'CALL_INTRINSIC_1', intrinsic_positive, False, None, None),
]
for (
expr,
original_opcode,
original_argval,
is_optimized,
optimized_opcode,
optimized_argval,
) in tests:
with self.subTest(expr=expr, is_optimized=is_optimized):
code = compile(expr, "", "single")
if is_optimized:
self.assertNotInBytecode(code, original_opcode, argval=original_argval)
self.assertInBytecode(code, optimized_opcode, argval=optimized_argval)
else:
self.assertInBytecode(code, original_opcode, argval=original_argval)
self.check_lnotab(code)
# Check that -0.0 works after marshaling
def negzero():
return -(1.0-1.0)
for instr in dis.get_instructions(negzero):
self.assertFalse(instr.opname.startswith('UNARY_'))
self.check_lnotab(negzero)
def test_constant_folding_binop(self):
tests = [
('1 + 2', 'NB_ADD', True, 'LOAD_SMALL_INT', 3),
('1 + 2 + 3', 'NB_ADD', True, 'LOAD_SMALL_INT', 6),
('1 + ""', 'NB_ADD', False, None, None),
('1 - 2', 'NB_SUBTRACT', True, 'LOAD_CONST', -1),
('1 - 2 - 3', 'NB_SUBTRACT', True, 'LOAD_CONST', -4),
('1 - ""', 'NB_SUBTRACT', False, None, None),
('2 * 2', 'NB_MULTIPLY', True, 'LOAD_SMALL_INT', 4),
('2 * 2 * 2', 'NB_MULTIPLY', True, 'LOAD_SMALL_INT', 8),
('2 / 2', 'NB_TRUE_DIVIDE', True, 'LOAD_CONST', 1.0),
('2 / 2 / 2', 'NB_TRUE_DIVIDE', True, 'LOAD_CONST', 0.5),
('2 / ""', 'NB_TRUE_DIVIDE', False, None, None),
('2 // 2', 'NB_FLOOR_DIVIDE', True, 'LOAD_SMALL_INT', 1),
('2 // 2 // 2', 'NB_FLOOR_DIVIDE', True, 'LOAD_SMALL_INT', 0),
('2 // ""', 'NB_FLOOR_DIVIDE', False, None, None),
('2 % 2', 'NB_REMAINDER', True, 'LOAD_SMALL_INT', 0),
('2 % 2 % 2', 'NB_REMAINDER', True, 'LOAD_SMALL_INT', 0),
('2 % ()', 'NB_REMAINDER', False, None, None),
('2 ** 2', 'NB_POWER', True, 'LOAD_SMALL_INT', 4),
('2 ** 2 ** 2', 'NB_POWER', True, 'LOAD_SMALL_INT', 16),
('2 ** ""', 'NB_POWER', False, None, None),
('2 << 2', 'NB_LSHIFT', True, 'LOAD_SMALL_INT', 8),
('2 << 2 << 2', 'NB_LSHIFT', True, 'LOAD_SMALL_INT', 32),
('2 << ""', 'NB_LSHIFT', False, None, None),
('2 >> 2', 'NB_RSHIFT', True, 'LOAD_SMALL_INT', 0),
('2 >> 2 >> 2', 'NB_RSHIFT', True, 'LOAD_SMALL_INT', 0),
('2 >> ""', 'NB_RSHIFT', False, None, None),
('2 | 2', 'NB_OR', True, 'LOAD_SMALL_INT', 2),
('2 | 2 | 2', 'NB_OR', True, 'LOAD_SMALL_INT', 2),
('2 | ""', 'NB_OR', False, None, None),
('2 & 2', 'NB_AND', True, 'LOAD_SMALL_INT', 2),
('2 & 2 & 2', 'NB_AND', True, 'LOAD_SMALL_INT', 2),
('2 & ""', 'NB_AND', False, None, None),
('2 ^ 2', 'NB_XOR', True, 'LOAD_SMALL_INT', 0),
('2 ^ 2 ^ 2', 'NB_XOR', True, 'LOAD_SMALL_INT', 2),
('2 ^ ""', 'NB_XOR', False, None, None),
('(1, )[0]', 'NB_SUBSCR', True, 'LOAD_SMALL_INT', 1),
('(1, )[-1]', 'NB_SUBSCR', True, 'LOAD_SMALL_INT', 1),
('(1 + 2, )[0]', 'NB_SUBSCR', True, 'LOAD_SMALL_INT', 3),
('(1, (1, 2))[1][1]', 'NB_SUBSCR', True, 'LOAD_SMALL_INT', 2),
('(1, 2)[2-1]', 'NB_SUBSCR', True, 'LOAD_SMALL_INT', 2),
('(1, (1, 2))[1][2-1]', 'NB_SUBSCR', True, 'LOAD_SMALL_INT', 2),
('(1, (1, 2))[1:6][0][2-1]', 'NB_SUBSCR', True, 'LOAD_SMALL_INT', 2),
('"a"[0]', 'NB_SUBSCR', True, 'LOAD_CONST', 'a'),
('("a" + "b")[1]', 'NB_SUBSCR', True, 'LOAD_CONST', 'b'),
('("a" + "b", )[0][1]', 'NB_SUBSCR', True, 'LOAD_CONST', 'b'),
('("a" * 10)[9]', 'NB_SUBSCR', True, 'LOAD_CONST', 'a'),
('(1, )[1]', 'NB_SUBSCR', False, None, None),
('(1, )[-2]', 'NB_SUBSCR', False, None, None),
('"a"[1]', 'NB_SUBSCR', False, None, None),
('"a"[-2]', 'NB_SUBSCR', False, None, None),
('("a" + "b")[2]', 'NB_SUBSCR', False, None, None),
('("a" + "b", )[0][2]', 'NB_SUBSCR', False, None, None),
('("a" + "b", )[1][0]', 'NB_SUBSCR', False, None, None),
('("a" * 10)[10]', 'NB_SUBSCR', False, None, None),
('(1, (1, 2))[2:6][0][2-1]', 'NB_SUBSCR', False, None, None),
]
for (
expr,
nb_op,
is_optimized,
optimized_opcode,
optimized_argval
) in tests:
with self.subTest(expr=expr, is_optimized=is_optimized):
code = compile(expr, '', 'single')
nb_op_val = get_binop_argval(nb_op)
if is_optimized:
self.assertNotInBytecode(code, 'BINARY_OP', argval=nb_op_val)
self.assertInBytecode(code, optimized_opcode, argval=optimized_argval)
else:
self.assertInBytecode(code, 'BINARY_OP', argval=nb_op_val)
self.check_lnotab(code)
# Verify that large sequences do not result from folding
code = compile('a="x"*10000', '', 'single')
code = compile('"x"*10000', '', 'single')
self.assertInBytecode(code, 'LOAD_CONST', 10000)
self.assertNotIn("x"*10000, code.co_consts)
self.check_lnotab(code)
code = compile('a=1<<1000', '', 'single')
code = compile('1<<1000', '', 'single')
self.assertInBytecode(code, 'LOAD_CONST', 1000)
self.assertNotIn(1<<1000, code.co_consts)
self.check_lnotab(code)
code = compile('a=2**1000', '', 'single')
code = compile('2**1000', '', 'single')
self.assertInBytecode(code, 'LOAD_CONST', 1000)
self.assertNotIn(2**1000, code.co_consts)
self.check_lnotab(code)
def test_binary_subscr_on_unicode(self):
# Test binary subscript on unicode
# valid code get optimized
code = compile('"foo"[0]', '', 'single')
self.assertInBytecode(code, 'LOAD_CONST', 'f')
@ -307,43 +426,83 @@ def test_binary_subscr_on_unicode(self):
self.assertInBytecode(code, 'BINARY_OP')
self.check_lnotab(code)
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
):
with self.subTest(line=line):
code = compile(line, '', 'single')
if isinstance(elem, int):
self.assertInBytecode(code, 'LOAD_SMALL_INT', elem)
else:
self.assertInBytecode(code, 'LOAD_CONST', elem)
for instr in dis.get_instructions(code):
self.assertFalse(instr.opname.startswith('UNARY_'))
self.check_lnotab(code)
# Check that -0.0 works after marshaling
def negzero():
return -(1.0-1.0)
def test_constant_folding_remove_nop_location(self):
sources = [
"""
(-
-
-
1)
""",
for instr in dis.get_instructions(negzero):
self.assertFalse(instr.opname.startswith('UNARY_'))
self.check_lnotab(negzero)
"""
(1
+
2
+
3)
""",
# Verify that unfoldables are skipped
for line, elem, opname in (
('-"abc"', 'abc', 'UNARY_NEGATIVE'),
('~"abc"', 'abc', 'UNARY_INVERT'),
):
with self.subTest(line=line):
code = compile(line, '', 'single')
self.assertInBytecode(code, 'LOAD_CONST', elem)
self.assertInBytecode(code, opname)
self.check_lnotab(code)
"""
(1,
2,
3)[0]
""",
"""
[1,
2,
3]
""",
"""
{1,
2,
3}
""",
"""
1 in [
1,
2,
3
]
""",
"""
1 in {
1,
2,
3
}
""",
"""
for _ in [1,
2,
3]:
pass
""",
"""
for _ in [1,
2,
x]:
pass
""",
"""
for _ in {1,
2,
3}:
pass
"""
]
for source in sources:
code = compile(textwrap.dedent(source), '', 'single')
self.assertNotInBytecode(code, 'NOP')
def test_elim_extra_return(self):
# RETURN LOAD_CONST None RETURN --> RETURN
@ -459,232 +618,6 @@ def g()->1+1:
self.assertNotInBytecode(f, 'BINARY_OP')
self.check_lnotab(f)
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:
with self.subTest(e=e):
code = compile(e, '', 'single')
for instr in dis.get_instructions(code):
self.assertFalse(instr.opname.startswith('UNARY_'))
self.assertFalse(instr.opname.startswith('BINARY_'))
self.assertFalse(instr.opname.startswith('BUILD_'))
self.check_lnotab(code)
def test_constant_folding_small_int(self):
tests = [
('(0, )[0]', 0),
('(1 + 2, )[0]', 3),
('(2 + 2 * 2, )[0]', 6),
('(1, (1 + 2 + 3, ))[1][0]', 6),
('1 + 2', 3),
('2 + 2 * 2 // 2 - 2', 2),
('(255, )[0]', 255),
('(256, )[0]', None),
('(1000, )[0]', None),
('(1 - 2, )[0]', None),
('255 + 0', 255),
('255 + 1', None),
('-1', None),
('--1', 1),
('--255', 255),
('--256', None),
('~1', None),
('~~1', 1),
('~~255', 255),
('~~256', None),
('++255', 255),
('++256', None),
]
for expr, oparg in tests:
with self.subTest(expr=expr, oparg=oparg):
code = compile(expr, '', 'single')
if oparg is not None:
self.assertInBytecode(code, 'LOAD_SMALL_INT', oparg)
else:
self.assertNotInBytecode(code, 'LOAD_SMALL_INT')
self.check_lnotab(code)
def test_folding_unaryop(self):
intrinsic_positive = 5
tests = [
('---1', 'UNARY_NEGATIVE', None, True),
('---""', 'UNARY_NEGATIVE', None, False),
('~~~1', 'UNARY_INVERT', None, True),
('~~~""', 'UNARY_INVERT', None, False),
('not not True', 'UNARY_NOT', None, True),
('not not x', 'UNARY_NOT', None, True), # this should be optimized regardless of constant or not
('+++1', 'CALL_INTRINSIC_1', intrinsic_positive, True),
('---x', 'UNARY_NEGATIVE', None, False),
('~~~x', 'UNARY_INVERT', None, False),
('+++x', 'CALL_INTRINSIC_1', intrinsic_positive, False),
]
for expr, opcode, oparg, optimized in tests:
with self.subTest(expr=expr, optimized=optimized):
code = compile(expr, '', 'single')
if optimized:
self.assertNotInBytecode(code, opcode, argval=oparg)
else:
self.assertInBytecode(code, opcode, argval=oparg)
self.check_lnotab(code)
def test_folding_binop(self):
tests = [
('1 + 2', False, 'NB_ADD'),
('1 + 2 + 3', False, 'NB_ADD'),
('1 + ""', True, 'NB_ADD'),
('1 - 2', False, 'NB_SUBTRACT'),
('1 - 2 - 3', False, 'NB_SUBTRACT'),
('1 - ""', True, 'NB_SUBTRACT'),
('2 * 2', False, 'NB_MULTIPLY'),
('2 * 2 * 2', False, 'NB_MULTIPLY'),
('2 / 2', False, 'NB_TRUE_DIVIDE'),
('2 / 2 / 2', False, 'NB_TRUE_DIVIDE'),
('2 / ""', True, 'NB_TRUE_DIVIDE'),
('2 // 2', False, 'NB_FLOOR_DIVIDE'),
('2 // 2 // 2', False, 'NB_FLOOR_DIVIDE'),
('2 // ""', True, 'NB_FLOOR_DIVIDE'),
('2 % 2', False, 'NB_REMAINDER'),
('2 % 2 % 2', False, 'NB_REMAINDER'),
('2 % ()', True, 'NB_REMAINDER'),
('2 ** 2', False, 'NB_POWER'),
('2 ** 2 ** 2', False, 'NB_POWER'),
('2 ** ""', True, 'NB_POWER'),
('2 << 2', False, 'NB_LSHIFT'),
('2 << 2 << 2', False, 'NB_LSHIFT'),
('2 << ""', True, 'NB_LSHIFT'),
('2 >> 2', False, 'NB_RSHIFT'),
('2 >> 2 >> 2', False, 'NB_RSHIFT'),
('2 >> ""', True, 'NB_RSHIFT'),
('2 | 2', False, 'NB_OR'),
('2 | 2 | 2', False, 'NB_OR'),
('2 | ""', True, 'NB_OR'),
('2 & 2', False, 'NB_AND'),
('2 & 2 & 2', False, 'NB_AND'),
('2 & ""', True, 'NB_AND'),
('2 ^ 2', False, 'NB_XOR'),
('2 ^ 2 ^ 2', False, 'NB_XOR'),
('2 ^ ""', True, 'NB_XOR'),
('(1, )[0]', False, 'NB_SUBSCR'),
('(1, )[-1]', False, 'NB_SUBSCR'),
('(1 + 2, )[0]', False, 'NB_SUBSCR'),
('(1, (1, 2))[1][1]', False, 'NB_SUBSCR'),
('(1, 2)[2-1]', False, 'NB_SUBSCR'),
('(1, (1, 2))[1][2-1]', False, 'NB_SUBSCR'),
('(1, (1, 2))[1:6][0][2-1]', False, 'NB_SUBSCR'),
('"a"[0]', False, 'NB_SUBSCR'),
('("a" + "b")[1]', False, 'NB_SUBSCR'),
('("a" + "b", )[0][1]', False, 'NB_SUBSCR'),
('("a" * 10)[9]', False, 'NB_SUBSCR'),
('(1, )[1]', True, 'NB_SUBSCR'),
('(1, )[-2]', True, 'NB_SUBSCR'),
('"a"[1]', True, 'NB_SUBSCR'),
('"a"[-2]', True, 'NB_SUBSCR'),
('("a" + "b")[2]', True, 'NB_SUBSCR'),
('("a" + "b", )[0][2]', True, 'NB_SUBSCR'),
('("a" + "b", )[1][0]', True, 'NB_SUBSCR'),
('("a" * 10)[10]', True, 'NB_SUBSCR'),
('(1, (1, 2))[2:6][0][2-1]', True, 'NB_SUBSCR'),
]
for expr, has_error, nb_op in tests:
with self.subTest(expr=expr, has_error=has_error):
code = compile(expr, '', 'single')
nb_op_val = get_binop_argval(nb_op)
if not has_error:
self.assertNotInBytecode(code, 'BINARY_OP', argval=nb_op_val)
else:
self.assertInBytecode(code, 'BINARY_OP', argval=nb_op_val)
self.check_lnotab(code)
def test_constant_folding_remove_nop_location(self):
sources = [
"""
(-
-
-
1)
""",
"""
(1
+
2
+
3)
""",
"""
(1,
2,
3)[0]
""",
"""
[1,
2,
3]
""",
"""
{1,
2,
3}
""",
"""
1 in [
1,
2,
3
]
""",
"""
1 in {
1,
2,
3
}
""",
"""
for _ in [1,
2,
3]:
pass
""",
"""
for _ in [1,
2,
x]:
pass
""",
"""
for _ in {1,
2,
3}:
pass
"""
]
for source in sources:
code = compile(textwrap.dedent(source), '', 'single')
self.assertNotInBytecode(code, 'NOP')
def test_in_literal_list(self):
def containtest():
return x in [a, b]