Skip to content
Next Next commit
gh-105481: expose opcode metadata via the _opcode module
  • Loading branch information
iritkatriel committed Jul 12, 2023
commit d65881b968f866beb625ee4802a283e3a910cb54
24 changes: 18 additions & 6 deletions Include/internal/pycore_opcode_metadata.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions Lib/test/test__opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,70 @@

class OpcodeTests(unittest.TestCase):

def check_bool_function_result(self, func, ops, expected):
for op in ops:
if isinstance(op, str):
op = dis.opmap[op]
with self.subTest(opcode=op, func=func):
self.assertIsInstance(func(op), bool)
self.assertEqual(func(op), expected)

def test_invalid_opcodes(self):
invalid = [-100, -1, 255, 512, 513, 1000]
self.check_bool_function_result(_opcode.is_valid, invalid, False)
self.check_bool_function_result(_opcode.has_arg, invalid, False)
self.check_bool_function_result(_opcode.has_const, invalid, False)
self.check_bool_function_result(_opcode.has_name, invalid, False)
self.check_bool_function_result(_opcode.has_jump, invalid, False)

def test_is_valid(self):
names = [
'CACHE',
'POP_TOP',
'IMPORT_NAME',
'JUMP',
'INSTRUMENTED_RETURN_VALUE',
]
opcodes = [dis.opmap[opname] for opname in names]
self.check_bool_function_result(_opcode.is_valid, opcodes, True)

def test_has_arg(self):
has_arg = ['SWAP', 'LOAD_FAST', 'INSTRUMENTED_POP_JUMP_IF_TRUE', 'JUMP']
no_arg = ['SETUP_WITH', 'POP_TOP', 'NOP', 'CACHE']
self.check_bool_function_result(_opcode.has_arg, has_arg, True)
self.check_bool_function_result(_opcode.has_arg, no_arg, False)

def test_has_const(self):
has_const = ['LOAD_CONST', 'RETURN_CONST', 'KW_NAMES']
no_const = ['SETUP_WITH', 'POP_TOP', 'NOP', 'CACHE']
self.check_bool_function_result(_opcode.has_const, has_const, True)
self.check_bool_function_result(_opcode.has_const, no_const, False)

def test_has_name(self):
has_name = ['STORE_NAME', 'DELETE_ATTR', 'STORE_GLOBAL', 'IMPORT_FROM',
'LOAD_FROM_DICT_OR_GLOBALS']
no_name = ['SETUP_WITH', 'POP_TOP', 'NOP', 'CACHE']
self.check_bool_function_result(_opcode.has_name, has_name, True)
self.check_bool_function_result(_opcode.has_name, no_name, False)

def test_has_jump(self):
has_jump = ['FOR_ITER', 'JUMP_FORWARD', 'JUMP', 'POP_JUMP_IF_TRUE', 'SEND']
no_jump = ['SETUP_WITH', 'POP_TOP', 'NOP', 'CACHE']
self.check_bool_function_result(_opcode.has_jump, has_jump, True)
self.check_bool_function_result(_opcode.has_jump, no_jump, False)

# the following test is part of the refactor, it will be removed soon
def test_against_legacy_bool_values(self):
# limiting to ops up to ENTER_EXECUTOR, because everything after that
# is not currently categorized correctly in opcode.py.
for op in range(0, opcode.opmap['ENTER_EXECUTOR']):
with self.subTest(op=op):
if opcode.opname[op] != f'<{op}>':
self.assertEqual(op in dis.hasarg, _opcode.has_arg(op))
self.assertEqual(op in dis.hasconst, _opcode.has_const(op))
self.assertEqual(op in dis.hasname, _opcode.has_name(op))
self.assertEqual(op in dis.hasjrel, _opcode.has_jump(op))

def test_stack_effect(self):
self.assertEqual(stack_effect(dis.opmap['POP_TOP']), -1)
self.assertEqual(stack_effect(dis.opmap['BUILD_SLICE'], 0), -1)
Expand Down
89 changes: 89 additions & 0 deletions Modules/_opcode.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#include "Python.h"
#include "opcode.h"
#include "internal/pycore_code.h"
#define NEED_OPCODE_METADATA
#include "internal/pycore_opcode_metadata.h"
#undef NEED_OPCODE_METADATA

/*[clinic input]
module _opcode
Expand Down Expand Up @@ -59,6 +62,87 @@ _opcode_stack_effect_impl(PyObject *module, int opcode, PyObject *oparg,
return effect;
}

/*[clinic input]

_opcode.is_valid -> bool

opcode: int

Return True if opcode is valid, False otherwise.
[clinic start generated code]*/

static int
_opcode_is_valid_impl(PyObject *module, int opcode)
/*[clinic end generated code: output=b0d918ea1d073f65 input=fe23e0aa194ddae0]*/
{
return IS_VALID_OPCODE(opcode);
}

/*[clinic input]

_opcode.has_arg -> bool

opcode: int

Return True if the opcode uses its oparg, False otherwise.
[clinic start generated code]*/

static int
_opcode_has_arg_impl(PyObject *module, int opcode)
/*[clinic end generated code: output=7a062d3b2dcc0815 input=93d878ba6361db5f]*/
{
return IS_VALID_OPCODE(opcode) && OPCODE_HAS_ARG(opcode);
}

/*[clinic input]

_opcode.has_const -> bool

opcode: int

Return True if the opcode accesses a constant, False otherwise.
[clinic start generated code]*/

static int
_opcode_has_const_impl(PyObject *module, int opcode)
/*[clinic end generated code: output=c646d5027c634120 input=a6999e4cf13f9410]*/
{
return IS_VALID_OPCODE(opcode) && OPCODE_HAS_CONST(opcode);
}

/*[clinic input]

_opcode.has_name -> bool

opcode: int

Return True if the opcode accesses an attribute by name, False otherwise.
[clinic start generated code]*/

static int
_opcode_has_name_impl(PyObject *module, int opcode)
/*[clinic end generated code: output=b49a83555c2fa517 input=448aa5e4bcc947ba]*/
{
return IS_VALID_OPCODE(opcode) && OPCODE_HAS_NAME(opcode);
}

/*[clinic input]

_opcode.has_jump -> bool

opcode: int

Return True if the opcode has a jump target, False otherwise.
[clinic start generated code]*/

static int
_opcode_has_jump_impl(PyObject *module, int opcode)
/*[clinic end generated code: output=e9c583c669f1c46a input=35f711274357a0c3]*/
{
return IS_VALID_OPCODE(opcode) && OPCODE_HAS_JUMP(opcode);
}


/*[clinic input]

_opcode.get_specialization_stats
Expand All @@ -80,6 +164,11 @@ _opcode_get_specialization_stats_impl(PyObject *module)
static PyMethodDef
opcode_functions[] = {
_OPCODE_STACK_EFFECT_METHODDEF
_OPCODE_IS_VALID_METHODDEF
_OPCODE_HAS_ARG_METHODDEF
_OPCODE_HAS_CONST_METHODDEF
_OPCODE_HAS_NAME_METHODDEF
_OPCODE_HAS_JUMP_METHODDEF
_OPCODE_GET_SPECIALIZATION_STATS_METHODDEF
{NULL, NULL, 0, NULL}
};
Expand Down
Loading