mirror of
https://github.com/python/cpython.git
synced 2026-05-06 04:37:33 -04:00
508b49845d
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
400 lines
13 KiB
Python
Executable File
400 lines
13 KiB
Python
Executable File
#!/usr/bin/python
|
|
"""Generate type/module slot files
|
|
"""
|
|
|
|
# See the input file (Python/slots.toml) for a description of its format.
|
|
|
|
import io
|
|
import sys
|
|
import json
|
|
import tomllib
|
|
import argparse
|
|
import functools
|
|
import contextlib
|
|
import collections
|
|
from pathlib import Path
|
|
|
|
GENERATED_BY = 'Generated by Tools/build/generate_slots.py'
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent.parent
|
|
DEFAULT_INPUT_PATH = REPO_ROOT / 'Python/slots.toml'
|
|
INCLUDE_PATH = REPO_ROOT / 'Include'
|
|
DEFAULT_PUBLIC_HEADER_PATH = INCLUDE_PATH / 'slots_generated.h'
|
|
DEFAULT_PRIVATE_HEADER_PATH = INCLUDE_PATH / 'internal/pycore_slots_generated.h'
|
|
DEFAULT_C_PATH = REPO_ROOT / 'Python/slots_generated.c'
|
|
|
|
TABLES = {
|
|
'tp': 'ht_type',
|
|
'am': 'as_async',
|
|
'nb': 'as_number',
|
|
'mp': 'as_mapping',
|
|
'sq': 'as_sequence',
|
|
'bf': 'as_buffer',
|
|
}
|
|
|
|
|
|
class SlotInfo:
|
|
def __init__(self, id, data):
|
|
self.id = id
|
|
self.kind = data['kind']
|
|
self._data = data
|
|
try:
|
|
self.name = data['name']
|
|
except KeyError:
|
|
self.name = '/'.join(data["equivalents"].values())
|
|
else:
|
|
assert self.name.isidentifier
|
|
|
|
@functools.cached_property
|
|
def equivalents(self):
|
|
return self._data['equivalents']
|
|
|
|
@functools.cached_property
|
|
def dtype(self):
|
|
try:
|
|
return self._data['dtype']
|
|
except KeyError:
|
|
if self.is_type_field:
|
|
return 'func'
|
|
raise
|
|
|
|
@functools.cached_property
|
|
def functype(self):
|
|
return self._data['functype']
|
|
|
|
@functools.cached_property
|
|
def is_type_field(self):
|
|
return self._data.get('is_type_field')
|
|
|
|
@functools.cached_property
|
|
def type_field(self):
|
|
assert self.is_type_field
|
|
return self._data.get('field', self.name.removeprefix('Py_'))
|
|
|
|
@functools.cached_property
|
|
def type_table_ident(self):
|
|
assert self.is_type_field
|
|
return self._data.get('table', self.type_field[:2])
|
|
|
|
@functools.cached_property
|
|
def duplicate_handling(self):
|
|
return self._data.get('duplicates', 'reject')
|
|
|
|
@functools.cached_property
|
|
def null_handling(self):
|
|
try:
|
|
return self._data['nulls']
|
|
except KeyError:
|
|
if self.kind == 'compat':
|
|
return 'allow'
|
|
if self.dtype in {'ptr', 'func'}:
|
|
return 'reject'
|
|
return 'allow'
|
|
|
|
@functools.cached_property
|
|
def must_be_static(self):
|
|
return self._data.get('must_be_static', False)
|
|
|
|
|
|
def parse_slots(file):
|
|
toml_contents = tomllib.load(file)
|
|
result = [None] * len(toml_contents)
|
|
for key, data in toml_contents.items():
|
|
slot_id = int(key)
|
|
try:
|
|
if result[slot_id]:
|
|
raise ValueError(f'slot ID {slot_id} repeated')
|
|
result[slot_id] = SlotInfo(slot_id, data)
|
|
except Exception as e:
|
|
e.add_note(f'handling slot {slot_id}')
|
|
raise
|
|
return result
|
|
|
|
|
|
class CWriter:
|
|
"""Simple helper for generating C code"""
|
|
|
|
def __init__(self, file):
|
|
self.file = file
|
|
self.indent = ''
|
|
self(f'/* {GENERATED_BY} */')
|
|
self()
|
|
|
|
def out(self, *args, **kwargs):
|
|
"""print args to the file, with current indent at the start"""
|
|
print(self.indent, end='', file=self.file)
|
|
print(*args, file=self.file, **kwargs)
|
|
|
|
__call__ = out
|
|
|
|
@contextlib.contextmanager
|
|
def block(self, header=None, end=''):
|
|
"""Context for a {}-enclosed block of C"""
|
|
if header is None:
|
|
self.out('{')
|
|
else:
|
|
self.out(header, '{')
|
|
old_indent = self.indent
|
|
self.indent += ' '
|
|
yield
|
|
self.indent = old_indent
|
|
self.out('}' + end)
|
|
|
|
|
|
def write_public_header(f, slots):
|
|
out = CWriter(f)
|
|
out(f'#ifndef _PY_HAVE_SLOTS_GENERATED_H')
|
|
out(f'#define _PY_HAVE_SLOTS_GENERATED_H')
|
|
out()
|
|
out(f'#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15)')
|
|
out(f'#define _Py_SLOT_COMPAT_VALUE(OLD, NEW) NEW')
|
|
out(f'#else')
|
|
out(f'#define _Py_SLOT_COMPAT_VALUE(OLD, NEW) OLD')
|
|
out(f'#endif')
|
|
out()
|
|
compat_ids = {}
|
|
for slot in slots:
|
|
if slot.kind == 'compat':
|
|
for new_name in slot.equivalents.values():
|
|
compat_ids[new_name] = slot.id
|
|
for slot in slots:
|
|
if slot.kind == 'compat':
|
|
continue
|
|
slot_id = slot.id
|
|
if compat := compat_ids.get(slot.name):
|
|
slot_id = f'_Py_SLOT_COMPAT_VALUE({compat}, {slot_id})'
|
|
out(f'#define {slot.name} {slot_id}')
|
|
out()
|
|
out(f'#define _Py_slot_COUNT {len(slots)}')
|
|
out(f'#endif /* _PY_HAVE_SLOTS_GENERATED_H */')
|
|
|
|
|
|
def write_private_header(f, slots):
|
|
out = CWriter(f)
|
|
|
|
def add_case(slot):
|
|
out(out(f' case {slot.id}:'))
|
|
|
|
slots_by_name = {slot.name: slot for slot in slots}
|
|
|
|
out(f'#ifndef _PY_HAVE_INTERNAL_SLOTS_GENERATED_H')
|
|
out(f'#define _PY_HAVE_INTERNAL_SLOTS_GENERATED_H')
|
|
for kind in 'type', 'mod':
|
|
out()
|
|
out(f'static inline uint16_t')
|
|
out(f'_PySlot_resolve_{kind}_slot(uint16_t slot_id)')
|
|
with out.block():
|
|
with out.block('switch (slot_id)'):
|
|
good_slots = []
|
|
for slot in slots:
|
|
if slot.kind == 'compat':
|
|
new_slot = slots_by_name[slot.equivalents[kind]]
|
|
out(f'case {slot.id}:')
|
|
out(f' return {new_slot.name};')
|
|
elif slot.kind in {kind, 'slot'}:
|
|
good_slots.append(f'case {slot.name}:')
|
|
for case in good_slots:
|
|
out(case)
|
|
out(f' return slot_id;')
|
|
out(f'default:')
|
|
out(f' return Py_slot_invalid;')
|
|
out()
|
|
out(f'static inline void*')
|
|
out(f'_PySlot_type_getslot(PyTypeObject *tp, uint16_t slot_id)')
|
|
with out.block():
|
|
with out.block('switch (slot_id)'):
|
|
for slot in slots:
|
|
if slot.is_type_field:
|
|
field = slot.type_field
|
|
table_ident = slot.type_table_ident
|
|
if table_ident == 'tp':
|
|
out(f'case {slot.name}:')
|
|
out(f' return (void*)tp->{field};')
|
|
else:
|
|
if table_ident == 'ht':
|
|
cond = 'tp->tp_flags & Py_TPFLAGS_HEAPTYPE'
|
|
val = f'((PyHeapTypeObject*)tp)->{field}'
|
|
else:
|
|
table = TABLES[table_ident]
|
|
cond = f'tp->tp_{table}'
|
|
val = f'tp->tp_{table}->{field}'
|
|
out(f'case {slot.name}:')
|
|
out(f' if (!({cond})) return NULL;')
|
|
out(f' return (void*){val};')
|
|
out(f'_PySlot_err_bad_slot("PyType_GetSlot", slot_id);')
|
|
out(f'return NULL;')
|
|
out()
|
|
out(f'static inline void')
|
|
out(f'_PySlot_heaptype_apply_field_slot(PyHeapTypeObject *ht,',
|
|
f'PySlot slot)')
|
|
with out.block():
|
|
with out.block('switch (slot.sl_id)'):
|
|
for slot in slots:
|
|
if slot.is_type_field:
|
|
field = slot.type_field
|
|
table_ident = slot.type_table_ident
|
|
if table_ident == 'ht':
|
|
continue
|
|
table = TABLES[table_ident]
|
|
if slot.dtype == 'func':
|
|
functype = f'({slot.functype})'
|
|
else:
|
|
functype = ''
|
|
out(f'case {slot.name}:')
|
|
out(f' ht->{table}.{field} = {functype}slot.sl_{slot.dtype};')
|
|
out(f' break;')
|
|
out()
|
|
out(f'static inline _PySlot_DTYPE')
|
|
out(f'_PySlot_get_dtype(uint16_t slot_id)')
|
|
with out.block():
|
|
with out.block('switch (slot_id)'):
|
|
for slot in slots:
|
|
if slot.kind == 'compat':
|
|
continue
|
|
dtype = slot.dtype
|
|
name = slot.name
|
|
out(f'case {name}: return _PySlot_DTYPE_{dtype.upper()};')
|
|
out(f'default: return _PySlot_DTYPE_VOID;')
|
|
out()
|
|
out(f'static inline _PySlot_PROBLEM_HANDLING')
|
|
out(f'_PySlot_get_duplicate_handling(uint16_t slot_id)')
|
|
with out.block():
|
|
with out.block('switch (slot_id)'):
|
|
results = collections.defaultdict(list)
|
|
for slot in slots:
|
|
if slot.kind == 'compat':
|
|
continue
|
|
handling = slot.duplicate_handling
|
|
results[handling.upper()].append(f'case {slot.name}:')
|
|
results.pop('REJECT')
|
|
for handling, cases in results.items():
|
|
for case in cases:
|
|
out(case)
|
|
out(f' return _PySlot_PROBLEM_{handling};')
|
|
out(f'default:')
|
|
out(f' return _PySlot_PROBLEM_REJECT;')
|
|
out()
|
|
out(f'static inline _PySlot_PROBLEM_HANDLING')
|
|
out(f'_PySlot_get_null_handling(uint16_t slot_id)')
|
|
with out.block():
|
|
with out.block('switch (slot_id)'):
|
|
results = collections.defaultdict(list)
|
|
for slot in slots:
|
|
if slot.kind == 'compat':
|
|
continue
|
|
handling = slot.null_handling
|
|
if handling is None:
|
|
if slot.kind != 'compat' and slot.dtype in {'ptr', 'func'}:
|
|
handling = 'reject'
|
|
else:
|
|
handling = 'allow'
|
|
results[handling.upper()].append(f'case {slot.name}:')
|
|
results.pop('REJECT')
|
|
for handling, cases in results.items():
|
|
for case in cases:
|
|
out(case)
|
|
out(f' return _PySlot_PROBLEM_{handling};')
|
|
out(f'default:')
|
|
out(f' return _PySlot_PROBLEM_REJECT;')
|
|
out()
|
|
out(f'static inline bool')
|
|
out(f'_PySlot_get_must_be_static(uint16_t slot_id)')
|
|
with out.block():
|
|
with out.block('switch (slot_id)'):
|
|
cases = []
|
|
for slot in slots:
|
|
if slot.must_be_static:
|
|
out(f'case {slot.name}: return true;')
|
|
out(f'return false;')
|
|
out()
|
|
out(f'#endif /* _PY_HAVE_INTERNAL_SLOTS_GENERATED_H */')
|
|
|
|
|
|
def write_c(f, slots):
|
|
out = CWriter(f)
|
|
out('#include "Python.h"')
|
|
out('#include "pycore_slots.h" // _PySlot_names')
|
|
out()
|
|
with out.block(f'const char *const _PySlot_names[] =', end=';'):
|
|
for slot in slots:
|
|
out(f'"{slot.name}",')
|
|
out('NULL')
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def replace_file(filename):
|
|
file_path = Path(filename)
|
|
with io.StringIO() as sio:
|
|
yield sio
|
|
try:
|
|
old_text = file_path.read_text()
|
|
except FileNotFoundError:
|
|
old_text = None
|
|
new_text = sio.getvalue()
|
|
if old_text == new_text:
|
|
print(f'{filename}: not modified', file=sys.stderr)
|
|
else:
|
|
print(f'{filename}: writing new content', file=sys.stderr)
|
|
file_path.write_text(new_text)
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) == 1:
|
|
# No sens calling this with no arguments.
|
|
argv.append('--help')
|
|
|
|
parser = argparse.ArgumentParser(prog=argv[0], description=__doc__)
|
|
parser.add_argument(
|
|
'-i', '--input', default=DEFAULT_INPUT_PATH,
|
|
help=f'the input file (default: {DEFAULT_INPUT_PATH})')
|
|
parser.add_argument(
|
|
'--generate-all', action=argparse.BooleanOptionalAction,
|
|
help='write all output files to their default locations')
|
|
parser.add_argument(
|
|
'-j', '--jsonl', action=argparse.BooleanOptionalAction,
|
|
help='write info to stdout in "JSON Lines" format (one JSON per line)')
|
|
outfile_group = parser.add_argument_group(
|
|
'output files',
|
|
description='By default, no files are generated. Use --generate-all '
|
|
+ 'or the options below to generate them.')
|
|
outfile_group.add_argument(
|
|
'-H', '--public-header',
|
|
help='file into which to write the public header')
|
|
outfile_group.add_argument(
|
|
'-I', '--private-header',
|
|
help='file into which to write the private header')
|
|
outfile_group.add_argument(
|
|
'-C', '--cfile',
|
|
help='file into which to write internal C code')
|
|
args = parser.parse_args(argv[1:])
|
|
|
|
if args.generate_all:
|
|
if args.public_header is None:
|
|
args.public_header = DEFAULT_PUBLIC_HEADER_PATH
|
|
if args.private_header is None:
|
|
args.private_header = DEFAULT_PRIVATE_HEADER_PATH
|
|
if args.cfile is None:
|
|
args.cfile = DEFAULT_C_PATH
|
|
|
|
with open(args.input, 'rb') as f:
|
|
slots = parse_slots(f)
|
|
|
|
if args.jsonl:
|
|
for slot in slots:
|
|
print(json.dumps(slot.to_dict()))
|
|
|
|
if args.public_header:
|
|
with replace_file(args.public_header) as f:
|
|
write_public_header(f, slots)
|
|
|
|
if args.private_header:
|
|
with replace_file(args.private_header) as f:
|
|
write_private_header(f, slots)
|
|
|
|
if args.cfile:
|
|
with replace_file(args.cfile) as f:
|
|
write_c(f, slots)
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv)
|