Files
cpython/Tools/build/generate_slots.py
T
2026-05-05 09:18:04 +02:00

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)