Skip to content

Commit 7d44721

Browse files
committed
pythongh-93678: added _testinternalcapi.optimize_cfg() function and test utils for compiler optimization unit tests
1 parent 41757bf commit 7d44721

8 files changed

+506
-49
lines changed

Include/internal/pycore_compile.h

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ extern int _PyAST_Optimize(
3838
struct _arena *arena,
3939
_PyASTOptimizeState *state);
4040

41+
/* Access compiler internals for unit testing */
42+
PyAPI_FUNC(PyObject*) _PyCompile_OptimizeCfg(
43+
PyObject *instructions,
44+
PyObject *consts);
45+
4146
#ifdef __cplusplus
4247
}
4348
#endif

Include/internal/pycore_global_strings.h

+2
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ struct _Py_global_strings {
298298
STRUCT_FOR_ID(code)
299299
STRUCT_FOR_ID(command)
300300
STRUCT_FOR_ID(comment_factory)
301+
STRUCT_FOR_ID(consts)
301302
STRUCT_FOR_ID(context)
302303
STRUCT_FOR_ID(cookie)
303304
STRUCT_FOR_ID(copy)
@@ -407,6 +408,7 @@ struct _Py_global_strings {
407408
STRUCT_FOR_ID(input)
408409
STRUCT_FOR_ID(insert_comments)
409410
STRUCT_FOR_ID(insert_pis)
411+
STRUCT_FOR_ID(instructions)
410412
STRUCT_FOR_ID(intern)
411413
STRUCT_FOR_ID(intersection)
412414
STRUCT_FOR_ID(isatty)

Include/internal/pycore_runtime_init_generated.h

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/support/bytecode_helper.py

+94
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import unittest
44
import dis
55
import io
6+
from _testinternalcapi import optimize_cfg
67

78
_UNSPECIFIED = object()
89

@@ -40,3 +41,96 @@ def assertNotInBytecode(self, x, opname, argval=_UNSPECIFIED):
4041
msg = '(%s,%r) occurs in bytecode:\n%s'
4142
msg = msg % (opname, argval, disassembly)
4243
self.fail(msg)
44+
45+
46+
class CfgOptimizationTestCase(unittest.TestCase):
47+
48+
HAS_ARG = set(dis.hasarg)
49+
HAS_TARGET = set(dis.hasjrel + dis.hasjabs + dis.hasexc)
50+
HAS_ARG_OR_TARGET = HAS_ARG.union(HAS_TARGET)
51+
52+
def setUp(self):
53+
self.last_label = 0
54+
55+
def Label(self):
56+
self.last_label += 1
57+
return self.last_label
58+
59+
def complete_insts_info(self, insts):
60+
# fill in omitted fields in location, and oparg 0 for ops with no arg.
61+
instructions = []
62+
for item in insts:
63+
if isinstance(item, int):
64+
instructions.append(item)
65+
else:
66+
assert isinstance(item, tuple)
67+
inst = list(reversed(item))
68+
opcode = dis.opmap[inst.pop()]
69+
oparg = inst.pop() if opcode in self.HAS_ARG_OR_TARGET else 0
70+
loc = inst + [-1] * (4 - len(inst))
71+
instructions.append((opcode, oparg, *loc))
72+
return instructions
73+
74+
def normalize_insts(self, insts):
75+
""" Map labels to instruction index.
76+
Remove labels which are not used as jump targets.
77+
"""
78+
labels_map = {}
79+
targets = set()
80+
idx = 1
81+
for item in insts:
82+
assert isinstance(item, (int, tuple))
83+
if isinstance(item, tuple):
84+
opcode, oparg, *_ = item
85+
if dis.opmap.get(opcode, opcode) in self.HAS_TARGET:
86+
targets.add(oparg)
87+
idx += 1
88+
elif isinstance(item, int):
89+
assert item not in labels_map, "label reused"
90+
labels_map[item] = idx
91+
92+
res = []
93+
for item in insts:
94+
if isinstance(item, int) and item in targets:
95+
if not res or labels_map[item] != res[-1]:
96+
res.append(labels_map[item])
97+
elif isinstance(item, tuple):
98+
opcode, oparg, *loc = item
99+
opcode = dis.opmap.get(opcode, opcode)
100+
if opcode in self.HAS_TARGET:
101+
arg = labels_map[oparg]
102+
else:
103+
arg = oparg if opcode in self.HAS_TARGET else None
104+
opcode = dis.opname[opcode]
105+
res.append((opcode, arg, *loc))
106+
return res
107+
108+
def get_optimized(self, insts, consts):
109+
insts = self.complete_insts_info(insts)
110+
insts = optimize_cfg(insts, consts)
111+
return insts, consts
112+
113+
def compareInstructions(self, actual_, expected_):
114+
# get two lists where each entry is a label or
115+
# an instruction tuple. Compare them, while mapping
116+
# each actual label to a corresponding expected label
117+
# based on their locations.
118+
119+
self.assertIsInstance(actual_, list)
120+
self.assertIsInstance(expected_, list)
121+
122+
actual = self.normalize_insts(actual_)
123+
expected = self.normalize_insts(expected_)
124+
self.assertEqual(len(actual), len(expected))
125+
126+
# compare instructions
127+
for act, exp in zip(actual, expected):
128+
if isinstance(act, int):
129+
self.assertEqual(exp, act)
130+
continue
131+
self.assertIsInstance(exp, tuple)
132+
self.assertIsInstance(act, tuple)
133+
# pad exp with -1's (if location info is incomplete)
134+
exp += (-1,) * (len(act) - len(exp))
135+
self.assertEqual(exp, act)
136+

Lib/test/test_peepholer.py

+77-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import textwrap
55
import unittest
66

7-
from test.support.bytecode_helper import BytecodeTestCase
7+
from test.support.bytecode_helper import BytecodeTestCase, CfgOptimizationTestCase
88

99

1010
def compile_pattern_with_fast_locals(pattern):
@@ -864,5 +864,81 @@ def trace(frame, event, arg):
864864
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
865865

866866

867+
class DirectiCfgOptimizerTests(CfgOptimizationTestCase):
868+
869+
def cfg_optimization_test(self, insts, expected_insts,
870+
consts=None, expected_consts=None):
871+
if expected_consts is None:
872+
expected_consts = consts
873+
opt_insts, opt_consts = self.get_optimized(insts, consts)
874+
self.compareInstructions(opt_insts, expected_insts)
875+
self.assertEqual(opt_consts, expected_consts)
876+
877+
def test_conditional_jump_forward_non_const_condition(self):
878+
insts = [
879+
('LOAD_NAME', 1, 11),
880+
('POP_JUMP_IF_TRUE', lbl := self.Label(), 12),
881+
('LOAD_CONST', 2, 13),
882+
lbl,
883+
('LOAD_CONST', 3, 14),
884+
]
885+
expected = [
886+
('LOAD_NAME', '1', 11),
887+
('POP_JUMP_IF_TRUE', lbl := self.Label(), 12),
888+
('LOAD_CONST', '2', 13),
889+
lbl,
890+
('LOAD_CONST', '3', 14)
891+
]
892+
self.cfg_optimization_test(insts, expected, consts=list(range(5)))
893+
894+
def test_conditional_jump_forward_const_condition(self):
895+
# The unreachable branch of the jump is removed
896+
897+
insts = [
898+
('LOAD_CONST', 3, 11),
899+
('POP_JUMP_IF_TRUE', lbl := self.Label(), 12),
900+
('LOAD_CONST', 2, 13),
901+
lbl,
902+
('LOAD_CONST', 3, 14),
903+
]
904+
expected = [
905+
('NOP', None, 11),
906+
('JUMP', lbl := self.Label(), 12),
907+
lbl,
908+
('LOAD_CONST', '3', 14)
909+
]
910+
self.cfg_optimization_test(insts, expected, consts=list(range(5)))
911+
912+
def test_conditional_jump_backward_non_const_condition(self):
913+
insts = [
914+
lbl1 := self.Label(),
915+
('LOAD_NAME', 1, 11),
916+
('POP_JUMP_IF_TRUE', lbl1, 12),
917+
('LOAD_CONST', 2, 13),
918+
]
919+
expected = [
920+
lbl := self.Label(),
921+
('LOAD_NAME', '1', 11),
922+
('POP_JUMP_IF_TRUE', lbl, 12),
923+
('LOAD_CONST', '2', 13)
924+
]
925+
self.cfg_optimization_test(insts, expected, consts=list(range(5)))
926+
927+
def test_conditional_jump_backward_const_condition(self):
928+
# The unreachable branch of the jump is removed
929+
insts = [
930+
lbl1 := self.Label(),
931+
('LOAD_CONST', 1, 11),
932+
('POP_JUMP_IF_TRUE', lbl1, 12),
933+
('LOAD_CONST', 2, 13),
934+
]
935+
expected = [
936+
lbl := self.Label(),
937+
('NOP', None, 11),
938+
('JUMP', lbl, 12)
939+
]
940+
self.cfg_optimization_test(insts, expected, consts=list(range(5)))
941+
942+
867943
if __name__ == "__main__":
868944
unittest.main()

Modules/_testinternalcapi.c

+26
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "Python.h"
1515
#include "pycore_atomic_funcs.h" // _Py_atomic_int_get()
1616
#include "pycore_bitutils.h" // _Py_bswap32()
17+
#include "pycore_compile.h" // _PyCompile_OptimizeCfg()
1718
#include "pycore_fileutils.h" // _Py_normpath
1819
#include "pycore_frame.h" // _PyInterpreterFrame
1920
#include "pycore_gc.h" // PyGC_Head
@@ -25,7 +26,12 @@
2526
#include "pycore_pystate.h" // _PyThreadState_GET()
2627
#include "osdefs.h" // MAXPATHLEN
2728

29+
#include "clinic/_testinternalcapi.c.h"
2830

31+
/*[clinic input]
32+
module _testinternalcapi
33+
[clinic start generated code]*/
34+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=7bb583d8c9eb9a78]*/
2935
static PyObject *
3036
get_configs(PyObject *self, PyObject *Py_UNUSED(args))
3137
{
@@ -525,6 +531,25 @@ set_eval_frame_record(PyObject *self, PyObject *list)
525531
}
526532

527533

534+
/*[clinic input]
535+
536+
_testinternalcapi.optimize_cfg -> object
537+
538+
instructions: object
539+
consts: object
540+
541+
Apply compiler optimizations to an instruction list.
542+
[clinic start generated code]*/
543+
544+
static PyObject *
545+
_testinternalcapi_optimize_cfg_impl(PyObject *module, PyObject *instructions,
546+
PyObject *consts)
547+
/*[clinic end generated code: output=5412aeafca683c8b input=7e8a3de86ebdd0f9]*/
548+
{
549+
return _PyCompile_OptimizeCfg(instructions, consts);
550+
}
551+
552+
528553
static PyMethodDef TestMethods[] = {
529554
{"get_configs", get_configs, METH_NOARGS},
530555
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -543,6 +568,7 @@ static PyMethodDef TestMethods[] = {
543568
{"DecodeLocaleEx", decode_locale_ex, METH_VARARGS},
544569
{"set_eval_frame_default", set_eval_frame_default, METH_NOARGS, NULL},
545570
{"set_eval_frame_record", set_eval_frame_record, METH_O, NULL},
571+
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
546572
{NULL, NULL} /* sentinel */
547573
};
548574

0 commit comments

Comments
 (0)