Skip to content

Commit c179c0e

Browse files
authored
gh-117680: make _PyInstructionSequence a PyObject and use it in tests (#117629)
1 parent ae8dfd2 commit c179c0e

17 files changed

+838
-242
lines changed

Include/internal/pycore_global_objects_fini_generated.h

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

Include/internal/pycore_global_strings.h

+4
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ struct _Py_global_strings {
353353
STRUCT_FOR_ID(co_stacksize)
354354
STRUCT_FOR_ID(co_varnames)
355355
STRUCT_FOR_ID(code)
356+
STRUCT_FOR_ID(col_offset)
356357
STRUCT_FOR_ID(command)
357358
STRUCT_FOR_ID(comment_factory)
358359
STRUCT_FOR_ID(compile_mode)
@@ -402,6 +403,7 @@ struct _Py_global_strings {
402403
STRUCT_FOR_ID(encode)
403404
STRUCT_FOR_ID(encoding)
404405
STRUCT_FOR_ID(end)
406+
STRUCT_FOR_ID(end_col_offset)
405407
STRUCT_FOR_ID(end_lineno)
406408
STRUCT_FOR_ID(end_offset)
407409
STRUCT_FOR_ID(endpos)
@@ -522,6 +524,7 @@ struct _Py_global_strings {
522524
STRUCT_FOR_ID(kw1)
523525
STRUCT_FOR_ID(kw2)
524526
STRUCT_FOR_ID(kwdefaults)
527+
STRUCT_FOR_ID(label)
525528
STRUCT_FOR_ID(lambda)
526529
STRUCT_FOR_ID(last)
527530
STRUCT_FOR_ID(last_exc)
@@ -585,6 +588,7 @@ struct _Py_global_strings {
585588
STRUCT_FOR_ID(namespaces)
586589
STRUCT_FOR_ID(narg)
587590
STRUCT_FOR_ID(ndigits)
591+
STRUCT_FOR_ID(nested)
588592
STRUCT_FOR_ID(new_file_name)
589593
STRUCT_FOR_ID(new_limit)
590594
STRUCT_FOR_ID(newline)

Include/internal/pycore_instruction_sequence.h

+14-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
# error "this header requires Py_BUILD_CORE define"
66
#endif
77

8+
#include "pycore_symtable.h"
9+
810
#ifdef __cplusplus
911
extern "C" {
1012
#endif
1113

14+
1215
typedef struct {
1316
int h_label;
1417
int h_startdepth;
@@ -26,23 +29,30 @@ typedef struct {
2629
int i_offset;
2730
} _PyInstruction;
2831

29-
typedef struct {
32+
typedef struct instruction_sequence {
33+
PyObject_HEAD
3034
_PyInstruction *s_instrs;
3135
int s_allocated;
3236
int s_used;
3337

3438
int s_next_free_label; /* next free label id */
39+
3540
/* Map of a label id to instruction offset (index into s_instrs).
3641
* If s_labelmap is NULL, then each label id is the offset itself.
3742
*/
38-
int *s_labelmap; /* label id --> instr offset */
43+
int *s_labelmap;
3944
int s_labelmap_size;
45+
46+
/* PyList of instruction sequences of nested functions */
47+
PyObject *s_nested;
4048
} _PyInstructionSequence;
4149

4250
typedef struct {
4351
int id;
4452
} _PyJumpTargetLabel;
4553

54+
PyAPI_FUNC(PyObject*)_PyInstructionSequence_New(void);
55+
4656
int _PyInstructionSequence_UseLabel(_PyInstructionSequence *seq, int lbl);
4757
int _PyInstructionSequence_Addop(_PyInstructionSequence *seq,
4858
int opcode, int oparg,
@@ -53,6 +63,8 @@ int _PyInstructionSequence_InsertInstruction(_PyInstructionSequence *seq, int po
5363
int opcode, int oparg, _Py_SourceLocation loc);
5464
void PyInstructionSequence_Fini(_PyInstructionSequence *seq);
5565

66+
extern PyTypeObject _PyInstructionSequence_Type;
67+
#define _PyInstructionSequence_Check(v) Py_IS_TYPE((v), &_PyInstructionSequence_Type)
5668

5769
#ifdef __cplusplus
5870
}

Include/internal/pycore_runtime_init_generated.h

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

Include/internal/pycore_unicodeobject_generated.h

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

Lib/test/support/bytecode_helper.py

+41-44
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import unittest
44
import dis
55
import io
6+
import opcode
67
try:
78
import _testinternalcapi
89
except ImportError:
@@ -68,16 +69,14 @@ class CompilationStepTestCase(unittest.TestCase):
6869
class Label:
6970
pass
7071

71-
def assertInstructionsMatch(self, actual_, expected_):
72-
# get two lists where each entry is a label or
73-
# an instruction tuple. Normalize the labels to the
74-
# instruction count of the target, and compare the lists.
72+
def assertInstructionsMatch(self, actual_seq, expected):
73+
# get an InstructionSequence and an expected list, where each
74+
# entry is a label or an instruction tuple. Construct an expcted
75+
# instruction sequence and compare with the one given.
7576

76-
self.assertIsInstance(actual_, list)
77-
self.assertIsInstance(expected_, list)
78-
79-
actual = self.normalize_insts(actual_)
80-
expected = self.normalize_insts(expected_)
77+
self.assertIsInstance(expected, list)
78+
actual = actual_seq.get_instructions()
79+
expected = self.seq_from_insts(expected).get_instructions()
8180
self.assertEqual(len(actual), len(expected))
8281

8382
# compare instructions
@@ -87,10 +86,8 @@ def assertInstructionsMatch(self, actual_, expected_):
8786
continue
8887
self.assertIsInstance(exp, tuple)
8988
self.assertIsInstance(act, tuple)
90-
# crop comparison to the provided expected values
91-
if len(act) > len(exp):
92-
act = act[:len(exp)]
93-
self.assertEqual(exp, act)
89+
idx = max([p[0] for p in enumerate(exp) if p[1] != -1])
90+
self.assertEqual(exp[:idx], act[:idx])
9491

9592
def resolveAndRemoveLabels(self, insts):
9693
idx = 0
@@ -105,35 +102,37 @@ def resolveAndRemoveLabels(self, insts):
105102

106103
return res
107104

108-
def normalize_insts(self, insts):
109-
""" Map labels to instruction index.
110-
Map opcodes to opnames.
111-
"""
112-
insts = self.resolveAndRemoveLabels(insts)
113-
res = []
114-
for item in insts:
115-
assert isinstance(item, tuple)
116-
opcode, oparg, *loc = item
117-
opcode = dis.opmap.get(opcode, opcode)
118-
if isinstance(oparg, self.Label):
119-
arg = oparg.value
120-
else:
121-
arg = oparg if opcode in self.HAS_ARG else None
122-
opcode = dis.opname[opcode]
123-
res.append((opcode, arg, *loc))
124-
return res
105+
def seq_from_insts(self, insts):
106+
labels = {item for item in insts if isinstance(item, self.Label)}
107+
for i, lbl in enumerate(labels):
108+
lbl.value = i
125109

126-
def complete_insts_info(self, insts):
127-
# fill in omitted fields in location, and oparg 0 for ops with no arg.
128-
res = []
110+
seq = _testinternalcapi.new_instruction_sequence()
129111
for item in insts:
130-
assert isinstance(item, tuple)
131-
inst = list(item)
132-
opcode = dis.opmap[inst[0]]
133-
oparg = inst[1]
134-
loc = inst[2:] + [-1] * (6 - len(inst))
135-
res.append((opcode, oparg, *loc))
136-
return res
112+
if isinstance(item, self.Label):
113+
seq.use_label(item.value)
114+
else:
115+
op = item[0]
116+
if isinstance(op, str):
117+
op = opcode.opmap[op]
118+
arg, *loc = item[1:]
119+
if isinstance(arg, self.Label):
120+
arg = arg.value
121+
loc = loc + [-1] * (4 - len(loc))
122+
seq.addop(op, arg or 0, *loc)
123+
return seq
124+
125+
def check_instructions(self, insts):
126+
for inst in insts:
127+
if isinstance(inst, self.Label):
128+
continue
129+
op, arg, *loc = inst
130+
if isinstance(op, str):
131+
op = opcode.opmap[op]
132+
self.assertEqual(op in opcode.hasarg,
133+
arg is not None,
134+
f"{opcode.opname[op]=} {arg=}")
135+
self.assertTrue(all(isinstance(l, int) for l in loc))
137136

138137

139138
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
@@ -147,10 +146,8 @@ def generate_code(self, ast):
147146
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
148147
class CfgOptimizationTestCase(CompilationStepTestCase):
149148

150-
def get_optimized(self, insts, consts, nlocals=0):
151-
insts = self.normalize_insts(insts)
152-
insts = self.complete_insts_info(insts)
153-
insts = _testinternalcapi.optimize_cfg(insts, consts, nlocals)
149+
def get_optimized(self, seq, consts, nlocals=0):
150+
insts = _testinternalcapi.optimize_cfg(seq, consts, nlocals)
154151
return insts, consts
155152

156153
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")

Lib/test/test_compile.py

+46
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import dis
33
import io
44
import math
5+
import opcode
56
import os
67
import unittest
78
import sys
@@ -11,6 +12,8 @@
1112
import types
1213
import textwrap
1314
import warnings
15+
import _testinternalcapi
16+
1417
from test import support
1518
from test.support import (script_helper, requires_debug_ranges,
1619
requires_specialization, get_c_recursion_limit)
@@ -2419,6 +2422,49 @@ def test_return_inside_async_with_block(self):
24192422
"""
24202423
self.check_stack_size(snippet, async_=True)
24212424

2425+
class TestInstructionSequence(unittest.TestCase):
2426+
def compare_instructions(self, seq, expected):
2427+
self.assertEqual([(opcode.opname[i[0]],) + i[1:] for i in seq.get_instructions()],
2428+
expected)
2429+
2430+
def test_basics(self):
2431+
seq = _testinternalcapi.new_instruction_sequence()
2432+
2433+
def add_op(seq, opname, oparg, bl, bc=0, el=0, ec=0):
2434+
seq.addop(opcode.opmap[opname], oparg, bl, bc, el, el)
2435+
2436+
add_op(seq, 'LOAD_CONST', 1, 1)
2437+
add_op(seq, 'JUMP', lbl1 := seq.new_label(), 2)
2438+
add_op(seq, 'LOAD_CONST', 1, 3)
2439+
add_op(seq, 'JUMP', lbl2 := seq.new_label(), 4)
2440+
seq.use_label(lbl1)
2441+
add_op(seq, 'LOAD_CONST', 2, 4)
2442+
seq.use_label(lbl2)
2443+
add_op(seq, 'RETURN_VALUE', 0, 3)
2444+
2445+
expected = [('LOAD_CONST', 1, 1),
2446+
('JUMP', 4, 2),
2447+
('LOAD_CONST', 1, 3),
2448+
('JUMP', 5, 4),
2449+
('LOAD_CONST', 2, 4),
2450+
('RETURN_VALUE', None, 3),
2451+
]
2452+
2453+
self.compare_instructions(seq, [ex + (0,0,0) for ex in expected])
2454+
2455+
def test_nested(self):
2456+
seq = _testinternalcapi.new_instruction_sequence()
2457+
seq.addop(opcode.opmap['LOAD_CONST'], 1, 1, 0, 0, 0)
2458+
nested = _testinternalcapi.new_instruction_sequence()
2459+
nested.addop(opcode.opmap['LOAD_CONST'], 2, 2, 0, 0, 0)
2460+
2461+
self.compare_instructions(seq, [('LOAD_CONST', 1, 1, 0, 0, 0)])
2462+
self.compare_instructions(nested, [('LOAD_CONST', 2, 2, 0, 0, 0)])
2463+
2464+
seq.add_nested(nested)
2465+
self.compare_instructions(seq, [('LOAD_CONST', 1, 1, 0, 0, 0)])
2466+
self.compare_instructions(seq.get_nested()[0], [('LOAD_CONST', 2, 2, 0, 0, 0)])
2467+
24222468

24232469
if __name__ == "__main__":
24242470
unittest.main()

0 commit comments

Comments
 (0)