Skip to content

gh-126835: Move constant tuple folding to CFG #130769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 0 additions & 111 deletions Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,22 +153,6 @@ def test_optimization_levels__debug__(self):
self.assertIsInstance(res.body[0].value, ast.Name)
self.assertEqual(res.body[0].value.id, expected)

def test_optimization_levels_const_folding(self):
folded = ('Expr', (1, 0, 1, 6), ('Constant', (1, 0, 1, 6), (1, 2), None))
not_folded = ('Expr', (1, 0, 1, 6),
('Tuple', (1, 0, 1, 6),
[('Constant', (1, 1, 1, 2), 1, None),
('Constant', (1, 4, 1, 5), 2, None)], ('Load',)))

cases = [(-1, not_folded), (0, not_folded), (1, folded), (2, folded)]
for (optval, expected) in cases:
with self.subTest(optval=optval):
tree1 = ast.parse("(1, 2)", optimize=optval)
tree2 = ast.parse(ast.parse("(1, 2)"), optimize=optval)
for tree in [tree1, tree2]:
res = to_tuple(tree.body[0])
self.assertEqual(res, expected)

def test_invalid_position_information(self):
invalid_linenos = [
(10, 1), (-10, -11), (10, -11), (-5, -2), (-5, 1)
Expand Down Expand Up @@ -3193,101 +3177,6 @@ def test_folding_format(self):

self.assert_ast(code, non_optimized_target, optimized_target)


def test_folding_tuple(self):
code = "(1,)"

non_optimized_target = self.wrap_expr(ast.Tuple(elts=[ast.Constant(1)]))
optimized_target = self.wrap_expr(ast.Constant(value=(1,)))

self.assert_ast(code, non_optimized_target, optimized_target)

def test_folding_type_param_in_function_def(self):
code = "def foo[%s = (1, 2)](): pass"

unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)])
unoptimized_type_params = [
("T", "T", ast.TypeVar),
("**P", "P", ast.ParamSpec),
("*Ts", "Ts", ast.TypeVarTuple),
]

for type, name, type_param in unoptimized_type_params:
result_code = code % type
optimized_target = self.wrap_statement(
ast.FunctionDef(
name='foo',
args=ast.arguments(),
body=[ast.Pass()],
type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))]
)
)
non_optimized_target = self.wrap_statement(
ast.FunctionDef(
name='foo',
args=ast.arguments(),
body=[ast.Pass()],
type_params=[type_param(name=name, default_value=unoptimized_tuple)]
)
)
self.assert_ast(result_code, non_optimized_target, optimized_target)

def test_folding_type_param_in_class_def(self):
code = "class foo[%s = (1, 2)]: pass"

unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)])
unoptimized_type_params = [
("T", "T", ast.TypeVar),
("**P", "P", ast.ParamSpec),
("*Ts", "Ts", ast.TypeVarTuple),
]

for type, name, type_param in unoptimized_type_params:
result_code = code % type
optimized_target = self.wrap_statement(
ast.ClassDef(
name='foo',
body=[ast.Pass()],
type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))]
)
)
non_optimized_target = self.wrap_statement(
ast.ClassDef(
name='foo',
body=[ast.Pass()],
type_params=[type_param(name=name, default_value=unoptimized_tuple)]
)
)
self.assert_ast(result_code, non_optimized_target, optimized_target)

def test_folding_type_param_in_type_alias(self):
code = "type foo[%s = (1, 2)] = 1"

unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)])
unoptimized_type_params = [
("T", "T", ast.TypeVar),
("**P", "P", ast.ParamSpec),
("*Ts", "Ts", ast.TypeVarTuple),
]

for type, name, type_param in unoptimized_type_params:
result_code = code % type
optimized_target = self.wrap_statement(
ast.TypeAlias(
name=ast.Name(id='foo', ctx=ast.Store()),
type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))],
value=ast.Constant(value=1),
)
)
non_optimized_target = self.wrap_statement(
ast.TypeAlias(
name=ast.Name(id='foo', ctx=ast.Store()),
type_params=[type_param(name=name, default_value=unoptimized_tuple)],
value=ast.Constant(value=1),
)
)
self.assert_ast(result_code, non_optimized_target, optimized_target)

def test_folding_match_case_allowed_expressions(self):
def get_match_case_values(node):
result = []
Expand Down
12 changes: 6 additions & 6 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ def test_compile_async_generator(self):
self.assertEqual(type(glob['ticker']()), AsyncGeneratorType)

def test_compile_ast(self):
args = ("a*(1,2)", "f.py", "exec")
args = ("a*__debug__", "f.py", "exec")
raw = compile(*args, flags = ast.PyCF_ONLY_AST).body[0]
opt1 = compile(*args, flags = ast.PyCF_OPTIMIZED_AST).body[0]
opt2 = compile(ast.parse(args[0]), *args[1:], flags = ast.PyCF_OPTIMIZED_AST).body[0]
Expand All @@ -565,14 +565,14 @@ def test_compile_ast(self):
self.assertIsInstance(tree.value.left, ast.Name)
self.assertEqual(tree.value.left.id, 'a')

raw_right = raw.value.right # expect Tuple((1, 2))
self.assertIsInstance(raw_right, ast.Tuple)
self.assertListEqual([elt.value for elt in raw_right.elts], [1, 2])
raw_right = raw.value.right
self.assertIsInstance(raw_right, ast.Name)
self.assertEqual(raw_right.id, "__debug__")

for opt in [opt1, opt2]:
opt_right = opt.value.right # expect Constant((1,2))
opt_right = opt.value.right
self.assertIsInstance(opt_right, ast.Constant)
self.assertEqual(opt_right.value, (1, 2))
self.assertEqual(opt_right.value, True)

def test_delattr(self):
sys.spam = 1
Expand Down
29 changes: 27 additions & 2 deletions Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -793,9 +793,9 @@ def check_same_constant(const):
f1, f2 = lambda: "not a name", lambda: ("not a name",)
f3 = lambda x: x in {("not a name",)}
self.assertIs(f1.__code__.co_consts[0],
f2.__code__.co_consts[0][0])
f2.__code__.co_consts[1][0])
self.assertIs(next(iter(f3.__code__.co_consts[1])),
f2.__code__.co_consts[0])
f2.__code__.co_consts[1])

# {0} is converted to a constant frozenset({0}) by the peephole
# optimizer
Expand Down Expand Up @@ -1129,6 +1129,31 @@ def foo(x):
self.assertIn('LOAD_ATTR', instructions)
self.assertIn('CALL', instructions)

def test_folding_type_param(self):
get_code_fn_cls = lambda x: x.co_consts[0].co_consts[2]
get_code_type_alias = lambda x: x.co_consts[0].co_consts[3]
snippets = [
("def foo[T = 40 + 5](): pass", get_code_fn_cls),
("def foo[**P = 40 + 5](): pass", get_code_fn_cls),
("def foo[*Ts = 40 + 5](): pass", get_code_fn_cls),
("class foo[T = 40 + 5]: pass", get_code_fn_cls),
("class foo[**P = 40 + 5]: pass", get_code_fn_cls),
("class foo[*Ts = 40 + 5]: pass", get_code_fn_cls),
("type foo[T = 40 + 5] = 1", get_code_type_alias),
("type foo[**P = 40 + 5] = 1", get_code_type_alias),
("type foo[*Ts = 40 + 5] = 1", get_code_type_alias),
]
for snippet, get_code in snippets:
c = compile(snippet, "<dummy>", "exec")
code = get_code(c)
opcodes = list(dis.get_instructions(code))
instructions = [opcode.opname for opcode in opcodes]
args = [opcode.oparg for opcode in opcodes]
self.assertNotIn(40, args)
self.assertNotIn(5, args)
self.assertIn('LOAD_SMALL_INT', instructions)
self.assertIn(45, args)

def test_lineno_procedure_call(self):
def call():
(
Expand Down
8 changes: 6 additions & 2 deletions Lib/test/test_opcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -1666,7 +1666,8 @@ def to_bool_str():
def test_unpack_sequence(self):
def unpack_sequence_two_tuple():
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
a, b = 1, 2
t = 1, 2
a, b = t
self.assertEqual(a, 1)
self.assertEqual(b, 2)

Expand All @@ -1677,8 +1678,11 @@ def unpack_sequence_two_tuple():

def unpack_sequence_tuple():
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
a, = 1,
a, b, c, d = 1, 2, 3, 4
self.assertEqual(a, 1)
self.assertEqual(b, 2)
self.assertEqual(c, 3)
self.assertEqual(d, 4)

unpack_sequence_tuple()
self.assert_specialized(unpack_sequence_tuple, "UNPACK_SEQUENCE_TUPLE")
Expand Down
107 changes: 106 additions & 1 deletion Lib/test/test_peepholer.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ 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)),
('a,b,c,d = 1,2,3,4', (1, 2, 3, 4)),
('(None, 1, None)', (None, 1, None)),
('((1, 2), 3, 4)', ((1, 2), 3, 4)),
):
Expand Down Expand Up @@ -1349,6 +1349,111 @@ def test_fold_tuple_of_constants(self):
]
self.cfg_optimization_test(same, same, consts=[])

def test_fold_constant_intrinsic_list_to_tuple(self):
INTRINSIC_LIST_TO_TUPLE = 6

# long tuple
consts = 1000
before = (
[('BUILD_LIST', 0, 0)] +
[('LOAD_CONST', 0, 0), ('LIST_APPEND', 1, 0)] * consts +
[('CALL_INTRINSIC_1', INTRINSIC_LIST_TO_TUPLE, 0), ('RETURN_VALUE', None, 0)]
)
after = [
('LOAD_CONST', 1, 0),
('RETURN_VALUE', None, 0)
]
result_const = tuple(["test"] * consts)
self.cfg_optimization_test(before, after, consts=["test"], expected_consts=["test", result_const])

# empty list
before = [
('BUILD_LIST', 0, 0),
('CALL_INTRINSIC_1', INTRINSIC_LIST_TO_TUPLE, 0),
('RETURN_VALUE', None, 0)
]
after = [
('LOAD_CONST', 0, 0),
('RETURN_VALUE', None, 0)
]
self.cfg_optimization_test(before, after, consts=[], expected_consts=[()])

# multiple BUILD_LIST 0: ([], 1, [], 2)
same = [
('BUILD_LIST', 0, 0),
('BUILD_LIST', 0, 0),
('LIST_APPEND', 1, 0),
('LOAD_SMALL_INT', 1, 0),
('LIST_APPEND', 1, 0),
('BUILD_LIST', 0, 0),
('LIST_APPEND', 1, 0),
('LOAD_SMALL_INT', 2, 0),
('LIST_APPEND', 1, 0),
('CALL_INTRINSIC_1', INTRINSIC_LIST_TO_TUPLE, 0),
('RETURN_VALUE', None, 0)
]
self.cfg_optimization_test(same, same, consts=[])

# nested folding: (1, 1+1, 3)
before = [
('BUILD_LIST', 0, 0),
('LOAD_SMALL_INT', 1, 0),
('LIST_APPEND', 1, 0),
('LOAD_SMALL_INT', 1, 0),
('LOAD_SMALL_INT', 1, 0),
('BINARY_OP', get_binop_argval('NB_ADD'), 0),
('LIST_APPEND', 1, 0),
('LOAD_SMALL_INT', 3, 0),
('LIST_APPEND', 1, 0),
('CALL_INTRINSIC_1', INTRINSIC_LIST_TO_TUPLE, 0),
('RETURN_VALUE', None, 0)
]
after = [
('LOAD_CONST', 0, 0),
('RETURN_VALUE', None, 0)
]
self.cfg_optimization_test(before, after, consts=[], expected_consts=[(1, 2, 3)])

# NOP's in between: (1, 2, 3)
before = [
('BUILD_LIST', 0, 0),
('NOP', None, 0),
('LOAD_SMALL_INT', 1, 0),
('NOP', None, 0),
('NOP', None, 0),
('LIST_APPEND', 1, 0),
('NOP', None, 0),
('LOAD_SMALL_INT', 2, 0),
('NOP', None, 0),
('NOP', None, 0),
('LIST_APPEND', 1, 0),
('NOP', None, 0),
('LOAD_SMALL_INT', 3, 0),
('NOP', None, 0),
('LIST_APPEND', 1, 0),
('NOP', None, 0),
('CALL_INTRINSIC_1', INTRINSIC_LIST_TO_TUPLE, 0),
('RETURN_VALUE', None, 0)
]
after = [
('LOAD_CONST', 0, 0),
('RETURN_VALUE', None, 0)
]
self.cfg_optimization_test(before, after, consts=[], expected_consts=[(1, 2, 3)])

# no sequence start
same = [
('LOAD_SMALL_INT', 1, 0),
('LIST_APPEND', 1, 0),
('LOAD_SMALL_INT', 2, 0),
('LIST_APPEND', 1, 0),
('LOAD_SMALL_INT', 3, 0),
('LIST_APPEND', 1, 0),
('CALL_INTRINSIC_1', INTRINSIC_LIST_TO_TUPLE, 0),
('RETURN_VALUE', None, 0)
]
self.cfg_optimization_test(same, same, consts=[])

def test_optimize_if_const_list(self):
before = [
('NOP', None, 0),
Expand Down
16 changes: 8 additions & 8 deletions Programs/test_frozenmain.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading