Skip to content

Commit 80b7148

Browse files
authored
gh-87092: Expose assembler to unit tests (#103988)
1 parent a474e04 commit 80b7148

11 files changed

+329
-48
lines changed

Include/internal/pycore_compile.h

+4
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ PyAPI_FUNC(PyObject*) _PyCompile_OptimizeCfg(
103103
PyObject *instructions,
104104
PyObject *consts);
105105

106+
PyAPI_FUNC(PyCodeObject*)
107+
_PyCompile_Assemble(_PyCompile_CodeUnitMetadata *umd, PyObject *filename,
108+
PyObject *instructions);
109+
106110
#ifdef __cplusplus
107111
}
108112
#endif

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
@@ -517,6 +517,7 @@ struct _Py_global_strings {
517517
STRUCT_FOR_ID(memlimit)
518518
STRUCT_FOR_ID(message)
519519
STRUCT_FOR_ID(metaclass)
520+
STRUCT_FOR_ID(metadata)
520521
STRUCT_FOR_ID(method)
521522
STRUCT_FOR_ID(mod)
522523
STRUCT_FOR_ID(mode)

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

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

Lib/test/support/bytecode_helper.py

+19-13
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 compiler_codegen, optimize_cfg
6+
from _testinternalcapi import compiler_codegen, optimize_cfg, assemble_code_object
77

88
_UNSPECIFIED = object()
99

@@ -108,6 +108,18 @@ def normalize_insts(self, insts):
108108
res.append((opcode, arg, *loc))
109109
return res
110110

111+
def complete_insts_info(self, insts):
112+
# fill in omitted fields in location, and oparg 0 for ops with no arg.
113+
res = []
114+
for item in insts:
115+
assert isinstance(item, tuple)
116+
inst = list(item)
117+
opcode = dis.opmap[inst[0]]
118+
oparg = inst[1]
119+
loc = inst[2:] + [-1] * (6 - len(inst))
120+
res.append((opcode, oparg, *loc))
121+
return res
122+
111123

112124
class CodegenTestCase(CompilationStepTestCase):
113125

@@ -118,20 +130,14 @@ def generate_code(self, ast):
118130

119131
class CfgOptimizationTestCase(CompilationStepTestCase):
120132

121-
def complete_insts_info(self, insts):
122-
# fill in omitted fields in location, and oparg 0 for ops with no arg.
123-
res = []
124-
for item in insts:
125-
assert isinstance(item, tuple)
126-
inst = list(reversed(item))
127-
opcode = dis.opmap[inst.pop()]
128-
oparg = inst.pop()
129-
loc = inst + [-1] * (4 - len(inst))
130-
res.append((opcode, oparg, *loc))
131-
return res
132-
133133
def get_optimized(self, insts, consts):
134134
insts = self.normalize_insts(insts)
135135
insts = self.complete_insts_info(insts)
136136
insts = optimize_cfg(insts, consts)
137137
return insts, consts
138+
139+
class AssemblerTestCase(CompilationStepTestCase):
140+
141+
def get_code_object(self, filename, insts, metadata):
142+
co = assemble_code_object(filename, insts, metadata)
143+
return co

Lib/test/test_compiler_assemble.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
2+
import ast
3+
import types
4+
5+
from test.support.bytecode_helper import AssemblerTestCase
6+
7+
8+
# Tests for the code-object creation stage of the compiler.
9+
10+
class IsolatedAssembleTests(AssemblerTestCase):
11+
12+
def complete_metadata(self, metadata, filename="myfile.py"):
13+
if metadata is None:
14+
metadata = {}
15+
for key in ['name', 'qualname']:
16+
metadata.setdefault(key, key)
17+
for key in ['consts']:
18+
metadata.setdefault(key, [])
19+
for key in ['names', 'varnames', 'cellvars', 'freevars']:
20+
metadata.setdefault(key, {})
21+
for key in ['argcount', 'posonlyargcount', 'kwonlyargcount']:
22+
metadata.setdefault(key, 0)
23+
metadata.setdefault('firstlineno', 1)
24+
metadata.setdefault('filename', filename)
25+
return metadata
26+
27+
def assemble_test(self, insts, metadata, expected):
28+
metadata = self.complete_metadata(metadata)
29+
insts = self.complete_insts_info(insts)
30+
31+
co = self.get_code_object(metadata['filename'], insts, metadata)
32+
self.assertIsInstance(co, types.CodeType)
33+
34+
expected_metadata = {}
35+
for key, value in metadata.items():
36+
if isinstance(value, list):
37+
expected_metadata[key] = tuple(value)
38+
elif isinstance(value, dict):
39+
expected_metadata[key] = tuple(value.keys())
40+
else:
41+
expected_metadata[key] = value
42+
43+
for key, value in expected_metadata.items():
44+
self.assertEqual(getattr(co, "co_" + key), value)
45+
46+
f = types.FunctionType(co, {})
47+
for args, res in expected.items():
48+
self.assertEqual(f(*args), res)
49+
50+
def test_simple_expr(self):
51+
metadata = {
52+
'filename' : 'avg.py',
53+
'name' : 'avg',
54+
'qualname' : 'stats.avg',
55+
'consts' : [2],
56+
'argcount' : 2,
57+
'varnames' : {'x' : 0, 'y' : 1},
58+
}
59+
60+
# code for "return (x+y)/2"
61+
insts = [
62+
('RESUME', 0),
63+
('LOAD_FAST', 0, 1), # 'x'
64+
('LOAD_FAST', 1, 1), # 'y'
65+
('BINARY_OP', 0, 1), # '+'
66+
('LOAD_CONST', 0, 1), # 2
67+
('BINARY_OP', 11, 1), # '/'
68+
('RETURN_VALUE', 1),
69+
]
70+
expected = {(3, 4) : 3.5, (-100, 200) : 50, (10, 18) : 14}
71+
self.assemble_test(insts, metadata, expected)

Modules/_testinternalcapi.c

+64-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_CodeGen, _PyCompile_OptimizeCfg
17+
#include "pycore_compile.h" // _PyCompile_CodeGen, _PyCompile_OptimizeCfg, _PyCompile_Assemble
1818
#include "pycore_fileutils.h" // _Py_normpath
1919
#include "pycore_frame.h" // _PyInterpreterFrame
2020
#include "pycore_gc.h" // PyGC_Head
@@ -625,6 +625,68 @@ _testinternalcapi_optimize_cfg_impl(PyObject *module, PyObject *instructions,
625625
return _PyCompile_OptimizeCfg(instructions, consts);
626626
}
627627

628+
static int
629+
get_nonnegative_int_from_dict(PyObject *dict, const char *key) {
630+
PyObject *obj = PyDict_GetItemString(dict, key);
631+
if (obj == NULL) {
632+
return -1;
633+
}
634+
return PyLong_AsLong(obj);
635+
}
636+
637+
/*[clinic input]
638+
639+
_testinternalcapi.assemble_code_object -> object
640+
641+
filename: object
642+
instructions: object
643+
metadata: object
644+
645+
Create a code object for the given instructions.
646+
[clinic start generated code]*/
647+
648+
static PyObject *
649+
_testinternalcapi_assemble_code_object_impl(PyObject *module,
650+
PyObject *filename,
651+
PyObject *instructions,
652+
PyObject *metadata)
653+
/*[clinic end generated code: output=38003dc16a930f48 input=e713ad77f08fb3a8]*/
654+
655+
{
656+
assert(PyDict_Check(metadata));
657+
_PyCompile_CodeUnitMetadata umd;
658+
659+
umd.u_name = PyDict_GetItemString(metadata, "name");
660+
umd.u_qualname = PyDict_GetItemString(metadata, "qualname");
661+
662+
assert(PyUnicode_Check(umd.u_name));
663+
assert(PyUnicode_Check(umd.u_qualname));
664+
665+
umd.u_consts = PyDict_GetItemString(metadata, "consts");
666+
umd.u_names = PyDict_GetItemString(metadata, "names");
667+
umd.u_varnames = PyDict_GetItemString(metadata, "varnames");
668+
umd.u_cellvars = PyDict_GetItemString(metadata, "cellvars");
669+
umd.u_freevars = PyDict_GetItemString(metadata, "freevars");
670+
671+
assert(PyList_Check(umd.u_consts));
672+
assert(PyDict_Check(umd.u_names));
673+
assert(PyDict_Check(umd.u_varnames));
674+
assert(PyDict_Check(umd.u_cellvars));
675+
assert(PyDict_Check(umd.u_freevars));
676+
677+
umd.u_argcount = get_nonnegative_int_from_dict(metadata, "argcount");
678+
umd.u_posonlyargcount = get_nonnegative_int_from_dict(metadata, "posonlyargcount");
679+
umd.u_kwonlyargcount = get_nonnegative_int_from_dict(metadata, "kwonlyargcount");
680+
umd.u_firstlineno = get_nonnegative_int_from_dict(metadata, "firstlineno");
681+
682+
assert(umd.u_argcount >= 0);
683+
assert(umd.u_posonlyargcount >= 0);
684+
assert(umd.u_kwonlyargcount >= 0);
685+
assert(umd.u_firstlineno >= 0);
686+
687+
return (PyObject*)_PyCompile_Assemble(&umd, filename, instructions);
688+
}
689+
628690

629691
static PyObject *
630692
get_interp_settings(PyObject *self, PyObject *args)
@@ -705,6 +767,7 @@ static PyMethodDef module_functions[] = {
705767
{"set_eval_frame_record", set_eval_frame_record, METH_O, NULL},
706768
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF
707769
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
770+
_TESTINTERNALCAPI_ASSEMBLE_CODE_OBJECT_METHODDEF
708771
{"get_interp_settings", get_interp_settings, METH_VARARGS, NULL},
709772
{"clear_extension", clear_extension, METH_VARARGS, NULL},
710773
{NULL, NULL} /* sentinel */

Modules/clinic/_testinternalcapi.c.h

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

0 commit comments

Comments
 (0)