Skip to content

Commit 89b8357

Browse files
committed
add unit tests for codegen
1 parent d5f14fc commit 89b8357

File tree

8 files changed

+294
-65
lines changed

8 files changed

+294
-65
lines changed

Include/internal/pycore_compile.h

Lines changed: 7 additions & 0 deletions
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_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ struct _Py_global_strings {
265265
STRUCT_FOR_ID(argdefs)
266266
STRUCT_FOR_ID(arguments)
267267
STRUCT_FOR_ID(argv)
268+
STRUCT_FOR_ID(ast)
268269
STRUCT_FOR_ID(attribute)
269270
STRUCT_FOR_ID(authorizer_callback)
270271
STRUCT_FOR_ID(b)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/support/bytecode_helper.py

Lines changed: 52 additions & 39 deletions
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 compareInstructions(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,34 @@ def normalize_insts(self, insts):
107117
res.append((opcode, arg, *loc))
108118
return res
109119

120+
121+
class CodegenTestCase(CompilationStepTestCase):
122+
123+
def generate_code(self, ast):
124+
insts = compiler_codegen(ast, "my_file.py", 0)
125+
return insts
126+
127+
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+
110145
def get_optimized(self, insts, consts):
111146
insts = self.complete_insts_info(insts)
112147
insts = optimize_cfg(insts, consts)
113148
return insts, consts
114149

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.
120-
121-
self.assertIsInstance(actual_, list)
122-
self.assertIsInstance(expected_, list)
123150

124-
actual = self.normalize_insts(actual_)
125-
expected = self.normalize_insts(expected_)
126-
self.assertEqual(len(actual), len(expected))
127-
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)

Lib/test/test_compiler_codegen.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.compareInstructions(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)
51+

Modules/_testinternalcapi.c

Lines changed: 22 additions & 1 deletion
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
@@ -530,6 +530,26 @@ set_eval_frame_record(PyObject *self, PyObject *list)
530530
Py_RETURN_NONE;
531531
}
532532

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

534554
/*[clinic input]
535555
@@ -613,6 +633,7 @@ static PyMethodDef TestMethods[] = {
613633
{"DecodeLocaleEx", decode_locale_ex, METH_VARARGS},
614634
{"set_eval_frame_default", set_eval_frame_default, METH_NOARGS, NULL},
615635
{"set_eval_frame_record", set_eval_frame_record, METH_O, NULL},
636+
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF
616637
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
617638
{"get_interp_settings", get_interp_settings, METH_VARARGS, NULL},
618639
{NULL, NULL} /* sentinel */

Modules/clinic/_testinternalcapi.c.h

Lines changed: 64 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)