Skip to content

Commit a3ac923

Browse files
authored
pythongh-87092: expose the compiler's codegen to python for unit tests (pythonGH-99111)
1 parent 06d4e02 commit a3ac923

11 files changed

+321
-97
lines changed

Include/internal/pycore_compile.h

+7
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ extern int _PyAST_Optimize(
4040
_PyASTOptimizeState *state);
4141

4242
/* Access compiler internals for unit testing */
43+
44+
PyAPI_FUNC(PyObject*) _PyCompile_CodeGen(
45+
PyObject *ast,
46+
PyObject *filename,
47+
PyCompilerFlags *flags,
48+
int optimize);
49+
4350
PyAPI_FUNC(PyObject*) _PyCompile_OptimizeCfg(
4451
PyObject *instructions,
4552
PyObject *consts);

Include/internal/pycore_global_objects_fini_generated.h

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

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ struct _Py_global_strings {
267267
STRUCT_FOR_ID(arguments)
268268
STRUCT_FOR_ID(argv)
269269
STRUCT_FOR_ID(as_integer_ratio)
270+
STRUCT_FOR_ID(ast)
270271
STRUCT_FOR_ID(attribute)
271272
STRUCT_FOR_ID(authorizer_callback)
272273
STRUCT_FOR_ID(autocommit)

Include/internal/pycore_runtime_init_generated.h

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

Include/internal/pycore_unicodeobject_generated.h

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

Lib/test/support/bytecode_helper.py

+52-41
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import unittest
44
import dis
55
import io
6-
from _testinternalcapi import optimize_cfg
6+
from _testinternalcapi import compiler_codegen, optimize_cfg
77

88
_UNSPECIFIED = object()
99

@@ -44,8 +44,7 @@ def assertNotInBytecode(self, x, opname, argval=_UNSPECIFIED):
4444
msg = msg % (opname, argval, disassembly)
4545
self.fail(msg)
4646

47-
48-
class CfgOptimizationTestCase(unittest.TestCase):
47+
class CompilationStepTestCase(unittest.TestCase):
4948

5049
HAS_ARG = set(dis.hasarg)
5150
HAS_TARGET = set(dis.hasjrel + dis.hasjabs + dis.hasexc)
@@ -58,24 +57,35 @@ def Label(self):
5857
self.last_label += 1
5958
return self.last_label
6059

61-
def complete_insts_info(self, insts):
62-
# fill in omitted fields in location, and oparg 0 for ops with no arg.
63-
instructions = []
64-
for item in insts:
65-
if isinstance(item, int):
66-
instructions.append(item)
67-
else:
68-
assert isinstance(item, tuple)
69-
inst = list(reversed(item))
70-
opcode = dis.opmap[inst.pop()]
71-
oparg = inst.pop() if opcode in self.HAS_ARG_OR_TARGET else 0
72-
loc = inst + [-1] * (4 - len(inst))
73-
instructions.append((opcode, oparg, *loc))
74-
return instructions
60+
def assertInstructionsMatch(self, actual_, expected_):
61+
# get two lists where each entry is a label or
62+
# an instruction tuple. Compare them, while mapping
63+
# each actual label to a corresponding expected label
64+
# based on their locations.
65+
66+
self.assertIsInstance(actual_, list)
67+
self.assertIsInstance(expected_, list)
68+
69+
actual = self.normalize_insts(actual_)
70+
expected = self.normalize_insts(expected_)
71+
self.assertEqual(len(actual), len(expected))
72+
73+
# compare instructions
74+
for act, exp in zip(actual, expected):
75+
if isinstance(act, int):
76+
self.assertEqual(exp, act)
77+
continue
78+
self.assertIsInstance(exp, tuple)
79+
self.assertIsInstance(act, tuple)
80+
# crop comparison to the provided expected values
81+
if len(act) > len(exp):
82+
act = act[:len(exp)]
83+
self.assertEqual(exp, act)
7584

7685
def normalize_insts(self, insts):
7786
""" Map labels to instruction index.
7887
Remove labels which are not used as jump targets.
88+
Map opcodes to opnames.
7989
"""
8090
labels_map = {}
8191
targets = set()
@@ -107,31 +117,32 @@ def normalize_insts(self, insts):
107117
res.append((opcode, arg, *loc))
108118
return res
109119

110-
def get_optimized(self, insts, consts):
111-
insts = self.complete_insts_info(insts)
112-
insts = optimize_cfg(insts, consts)
113-
return insts, consts
114120

115-
def compareInstructions(self, actual_, expected_):
116-
# get two lists where each entry is a label or
117-
# an instruction tuple. Compare them, while mapping
118-
# each actual label to a corresponding expected label
119-
# based on their locations.
121+
class CodegenTestCase(CompilationStepTestCase):
120122

121-
self.assertIsInstance(actual_, list)
122-
self.assertIsInstance(expected_, list)
123+
def generate_code(self, ast):
124+
insts = compiler_codegen(ast, "my_file.py", 0)
125+
return insts
123126

124-
actual = self.normalize_insts(actual_)
125-
expected = self.normalize_insts(expected_)
126-
self.assertEqual(len(actual), len(expected))
127127

128-
# compare instructions
129-
for act, exp in zip(actual, expected):
130-
if isinstance(act, int):
131-
self.assertEqual(exp, act)
132-
continue
133-
self.assertIsInstance(exp, tuple)
134-
self.assertIsInstance(act, tuple)
135-
# pad exp with -1's (if location info is incomplete)
136-
exp += (-1,) * (len(act) - len(exp))
137-
self.assertEqual(exp, act)
128+
class CfgOptimizationTestCase(CompilationStepTestCase):
129+
130+
def complete_insts_info(self, insts):
131+
# fill in omitted fields in location, and oparg 0 for ops with no arg.
132+
instructions = []
133+
for item in insts:
134+
if isinstance(item, int):
135+
instructions.append(item)
136+
else:
137+
assert isinstance(item, tuple)
138+
inst = list(reversed(item))
139+
opcode = dis.opmap[inst.pop()]
140+
oparg = inst.pop() if opcode in self.HAS_ARG_OR_TARGET else 0
141+
loc = inst + [-1] * (4 - len(inst))
142+
instructions.append((opcode, oparg, *loc))
143+
return instructions
144+
145+
def get_optimized(self, insts, consts):
146+
insts = self.complete_insts_info(insts)
147+
insts = optimize_cfg(insts, consts)
148+
return insts, consts

Lib/test/test_compiler_codegen.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
from test.support.bytecode_helper import CodegenTestCase
3+
4+
# Tests for the code-generation stage of the compiler.
5+
# Examine the un-optimized code generated from the AST.
6+
7+
class IsolatedCodeGenTests(CodegenTestCase):
8+
9+
def codegen_test(self, snippet, expected_insts):
10+
import ast
11+
a = ast.parse(snippet, "my_file.py", "exec");
12+
insts = self.generate_code(a)
13+
self.assertInstructionsMatch(insts, expected_insts)
14+
15+
def test_if_expression(self):
16+
snippet = "42 if True else 24"
17+
false_lbl = self.Label()
18+
expected = [
19+
('RESUME', 0, 0),
20+
('LOAD_CONST', 0, 1),
21+
('POP_JUMP_IF_FALSE', false_lbl := self.Label(), 1),
22+
('LOAD_CONST', 1, 1),
23+
('JUMP', exit_lbl := self.Label()),
24+
false_lbl,
25+
('LOAD_CONST', 2, 1),
26+
exit_lbl,
27+
('POP_TOP', None),
28+
]
29+
self.codegen_test(snippet, expected)
30+
31+
def test_for_loop(self):
32+
snippet = "for x in l:\n\tprint(x)"
33+
false_lbl = self.Label()
34+
expected = [
35+
('RESUME', 0, 0),
36+
('LOAD_NAME', 0, 1),
37+
('GET_ITER', None, 1),
38+
loop_lbl := self.Label(),
39+
('FOR_ITER', exit_lbl := self.Label(), 1),
40+
('STORE_NAME', None, 1),
41+
('PUSH_NULL', None, 2),
42+
('LOAD_NAME', None, 2),
43+
('LOAD_NAME', None, 2),
44+
('CALL', None, 2),
45+
('POP_TOP', None),
46+
('JUMP', loop_lbl),
47+
exit_lbl,
48+
('END_FOR', None),
49+
]
50+
self.codegen_test(snippet, expected)

Lib/test/test_peepholer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,7 @@ def cfg_optimization_test(self, insts, expected_insts,
984984
if expected_consts is None:
985985
expected_consts = consts
986986
opt_insts, opt_consts = self.get_optimized(insts, consts)
987-
self.compareInstructions(opt_insts, expected_insts)
987+
self.assertInstructionsMatch(opt_insts, expected_insts)
988988
self.assertEqual(opt_consts, expected_consts)
989989

990990
def test_conditional_jump_forward_non_const_condition(self):

Modules/_testinternalcapi.c

+22-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +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()
17+
#include "pycore_compile.h" // _PyCompile_CodeGen, _PyCompile_OptimizeCfg
1818
#include "pycore_fileutils.h" // _Py_normpath
1919
#include "pycore_frame.h" // _PyInterpreterFrame
2020
#include "pycore_gc.h" // PyGC_Head
@@ -529,6 +529,26 @@ set_eval_frame_record(PyObject *self, PyObject *list)
529529
Py_RETURN_NONE;
530530
}
531531

532+
/*[clinic input]
533+
534+
_testinternalcapi.compiler_codegen -> object
535+
536+
ast: object
537+
filename: object
538+
optimize: int
539+
540+
Apply compiler code generation to an AST.
541+
[clinic start generated code]*/
542+
543+
static PyObject *
544+
_testinternalcapi_compiler_codegen_impl(PyObject *module, PyObject *ast,
545+
PyObject *filename, int optimize)
546+
/*[clinic end generated code: output=fbbbbfb34700c804 input=e9fbe6562f7f75e4]*/
547+
{
548+
PyCompilerFlags *flags = NULL;
549+
return _PyCompile_CodeGen(ast, filename, flags, optimize);
550+
}
551+
532552

533553
/*[clinic input]
534554
@@ -612,6 +632,7 @@ static PyMethodDef TestMethods[] = {
612632
{"DecodeLocaleEx", decode_locale_ex, METH_VARARGS},
613633
{"set_eval_frame_default", set_eval_frame_default, METH_NOARGS, NULL},
614634
{"set_eval_frame_record", set_eval_frame_record, METH_O, NULL},
635+
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF
615636
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
616637
{"get_interp_settings", get_interp_settings, METH_VARARGS, NULL},
617638
{NULL, NULL} /* sentinel */

Modules/clinic/_testinternalcapi.c.h

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

0 commit comments

Comments
 (0)