| """bytecode_helper - support tools for testing correct bytecode generation""" | |
| import unittest | |
| import dis | |
| import io | |
| import opcode | |
| try: | |
| import _testinternalcapi | |
| except ImportError: | |
| _testinternalcapi = None | |
| _UNSPECIFIED = object() | |
| def instructions_with_positions(instrs, co_positions): | |
| # Return (instr, positions) pairs from the instrs list and co_positions | |
| # iterator. The latter contains items for cache lines and the former | |
| # doesn't, so those need to be skipped. | |
| co_positions = co_positions or iter(()) | |
| for instr in instrs: | |
| yield instr, next(co_positions, ()) | |
| for _, size, _ in (instr.cache_info or ()): | |
| for i in range(size): | |
| next(co_positions, ()) | |
| class BytecodeTestCase(unittest.TestCase): | |
| """Custom assertion methods for inspecting bytecode.""" | |
| def get_disassembly_as_string(self, co): | |
| s = io.StringIO() | |
| dis.dis(co, file=s) | |
| return s.getvalue() | |
| def assertInBytecode(self, x, opname, argval=_UNSPECIFIED): | |
| """Returns instr if opname is found, otherwise throws AssertionError""" | |
| self.assertIn(opname, dis.opmap) | |
| for instr in dis.get_instructions(x): | |
| if instr.opname == opname: | |
| if argval is _UNSPECIFIED or instr.argval == argval: | |
| return instr | |
| disassembly = self.get_disassembly_as_string(x) | |
| if argval is _UNSPECIFIED: | |
| msg = '%s not found in bytecode:\n%s' % (opname, disassembly) | |
| else: | |
| msg = '(%s,%r) not found in bytecode:\n%s' | |
| msg = msg % (opname, argval, disassembly) | |
| self.fail(msg) | |
| def assertNotInBytecode(self, x, opname, argval=_UNSPECIFIED): | |
| """Throws AssertionError if opname is found""" | |
| self.assertIn(opname, dis.opmap) | |
| for instr in dis.get_instructions(x): | |
| if instr.opname == opname: | |
| disassembly = self.get_disassembly_as_string(x) | |
| if argval is _UNSPECIFIED: | |
| msg = '%s occurs in bytecode:\n%s' % (opname, disassembly) | |
| self.fail(msg) | |
| elif instr.argval == argval: | |
| msg = '(%s,%r) occurs in bytecode:\n%s' | |
| msg = msg % (opname, argval, disassembly) | |
| self.fail(msg) | |
| class CompilationStepTestCase(unittest.TestCase): | |
| HAS_ARG = set(dis.hasarg) | |
| HAS_TARGET = set(dis.hasjrel + dis.hasjabs + dis.hasexc) | |
| HAS_ARG_OR_TARGET = HAS_ARG.union(HAS_TARGET) | |
| class Label: | |
| pass | |
| def assertInstructionsMatch(self, actual_seq, expected): | |
| # get an InstructionSequence and an expected list, where each | |
| # entry is a label or an instruction tuple. Construct an expected | |
| # instruction sequence and compare with the one given. | |
| self.assertIsInstance(expected, list) | |
| actual = actual_seq.get_instructions() | |
| expected = self.seq_from_insts(expected).get_instructions() | |
| self.assertEqual(len(actual), len(expected)) | |
| # compare instructions | |
| for act, exp in zip(actual, expected): | |
| if isinstance(act, int): | |
| self.assertEqual(exp, act) | |
| continue | |
| self.assertIsInstance(exp, tuple) | |
| self.assertIsInstance(act, tuple) | |
| idx = max([p[0] for p in enumerate(exp) if p[1] != -1]) | |
| self.assertEqual(exp[:idx], act[:idx]) | |
| def resolveAndRemoveLabels(self, insts): | |
| idx = 0 | |
| res = [] | |
| for item in insts: | |
| assert isinstance(item, (self.Label, tuple)) | |
| if isinstance(item, self.Label): | |
| item.value = idx | |
| else: | |
| idx += 1 | |
| res.append(item) | |
| return res | |
| def seq_from_insts(self, insts): | |
| labels = {item for item in insts if isinstance(item, self.Label)} | |
| for i, lbl in enumerate(labels): | |
| lbl.value = i | |
| seq = _testinternalcapi.new_instruction_sequence() | |
| for item in insts: | |
| if isinstance(item, self.Label): | |
| seq.use_label(item.value) | |
| else: | |
| op = item[0] | |
| if isinstance(op, str): | |
| op = opcode.opmap[op] | |
| arg, *loc = item[1:] | |
| if isinstance(arg, self.Label): | |
| arg = arg.value | |
| loc = loc + [-1] * (4 - len(loc)) | |
| seq.addop(op, arg or 0, *loc) | |
| return seq | |
| def check_instructions(self, insts): | |
| for inst in insts: | |
| if isinstance(inst, self.Label): | |
| continue | |
| op, arg, *loc = inst | |
| if isinstance(op, str): | |
| op = opcode.opmap[op] | |
| self.assertEqual(op in opcode.hasarg, | |
| arg is not None, | |
| f"{opcode.opname[op]=} {arg=}") | |
| self.assertTrue(all(isinstance(l, int) for l in loc)) | |
| @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") | |
| class CodegenTestCase(CompilationStepTestCase): | |
| def generate_code(self, ast): | |
| insts, _ = _testinternalcapi.compiler_codegen(ast, "my_file.py", 0) | |
| return insts | |
| @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") | |
| class CfgOptimizationTestCase(CompilationStepTestCase): | |
| def get_optimized(self, seq, consts, nlocals=0): | |
| insts = _testinternalcapi.optimize_cfg(seq, consts, nlocals) | |
| return insts, consts | |
| @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") | |
| class AssemblerTestCase(CompilationStepTestCase): | |
| def get_code_object(self, filename, insts, metadata): | |
| co = _testinternalcapi.assemble_code_object(filename, insts, metadata) | |
| return co |