blob: 62e1b4b2dea14d41a906afc6b88eb453bb6414ac [file] [log] [blame]
"""Code generation for native classes and related wrappers."""
from __future__ import annotations
from typing import Callable, Mapping, Tuple
from mypyc.codegen.emit import Emitter, HeaderDeclaration, ReturnHandler
from mypyc.codegen.emitfunc import native_function_header
from mypyc.codegen.emitwrapper import (
generate_bin_op_wrapper,
generate_bool_wrapper,
generate_contains_wrapper,
generate_dunder_wrapper,
generate_get_wrapper,
generate_hash_wrapper,
generate_ipow_wrapper,
generate_len_wrapper,
generate_richcompare_wrapper,
generate_set_del_item_wrapper,
)
from mypyc.common import BITMAP_BITS, BITMAP_TYPE, NATIVE_PREFIX, PREFIX, REG_PREFIX
from mypyc.ir.class_ir import ClassIR, VTableEntries
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR
from mypyc.ir.rtypes import RTuple, RType, object_rprimitive
from mypyc.namegen import NameGenerator
from mypyc.sametype import is_same_type
def native_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
return f"{NATIVE_PREFIX}{fn.cname(emitter.names)}"
def wrapper_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
return f"{PREFIX}{fn.cname(emitter.names)}"
# We maintain a table from dunder function names to struct slots they
# correspond to and functions that generate a wrapper (if necessary)
# and return the function name to stick in the slot.
# TODO: Add remaining dunder methods
SlotGenerator = Callable[[ClassIR, FuncIR, Emitter], str]
SlotTable = Mapping[str, Tuple[str, SlotGenerator]]
SLOT_DEFS: SlotTable = {
"__init__": ("tp_init", lambda c, t, e: generate_init_for_class(c, t, e)),
"__call__": ("tp_call", lambda c, t, e: generate_call_wrapper(c, t, e)),
"__str__": ("tp_str", native_slot),
"__repr__": ("tp_repr", native_slot),
"__next__": ("tp_iternext", native_slot),
"__iter__": ("tp_iter", native_slot),
"__hash__": ("tp_hash", generate_hash_wrapper),
"__get__": ("tp_descr_get", generate_get_wrapper),
}
AS_MAPPING_SLOT_DEFS: SlotTable = {
"__getitem__": ("mp_subscript", generate_dunder_wrapper),
"__setitem__": ("mp_ass_subscript", generate_set_del_item_wrapper),
"__delitem__": ("mp_ass_subscript", generate_set_del_item_wrapper),
"__len__": ("mp_length", generate_len_wrapper),
}
AS_SEQUENCE_SLOT_DEFS: SlotTable = {"__contains__": ("sq_contains", generate_contains_wrapper)}
AS_NUMBER_SLOT_DEFS: SlotTable = {
# Unary operations.
"__bool__": ("nb_bool", generate_bool_wrapper),
"__int__": ("nb_int", generate_dunder_wrapper),
"__float__": ("nb_float", generate_dunder_wrapper),
"__neg__": ("nb_negative", generate_dunder_wrapper),
"__pos__": ("nb_positive", generate_dunder_wrapper),
"__abs__": ("nb_absolute", generate_dunder_wrapper),
"__invert__": ("nb_invert", generate_dunder_wrapper),
# Binary operations.
"__add__": ("nb_add", generate_bin_op_wrapper),
"__radd__": ("nb_add", generate_bin_op_wrapper),
"__sub__": ("nb_subtract", generate_bin_op_wrapper),
"__rsub__": ("nb_subtract", generate_bin_op_wrapper),
"__mul__": ("nb_multiply", generate_bin_op_wrapper),
"__rmul__": ("nb_multiply", generate_bin_op_wrapper),
"__mod__": ("nb_remainder", generate_bin_op_wrapper),
"__rmod__": ("nb_remainder", generate_bin_op_wrapper),
"__truediv__": ("nb_true_divide", generate_bin_op_wrapper),
"__rtruediv__": ("nb_true_divide", generate_bin_op_wrapper),
"__floordiv__": ("nb_floor_divide", generate_bin_op_wrapper),
"__rfloordiv__": ("nb_floor_divide", generate_bin_op_wrapper),
"__divmod__": ("nb_divmod", generate_bin_op_wrapper),
"__rdivmod__": ("nb_divmod", generate_bin_op_wrapper),
"__lshift__": ("nb_lshift", generate_bin_op_wrapper),
"__rlshift__": ("nb_lshift", generate_bin_op_wrapper),
"__rshift__": ("nb_rshift", generate_bin_op_wrapper),
"__rrshift__": ("nb_rshift", generate_bin_op_wrapper),
"__and__": ("nb_and", generate_bin_op_wrapper),
"__rand__": ("nb_and", generate_bin_op_wrapper),
"__or__": ("nb_or", generate_bin_op_wrapper),
"__ror__": ("nb_or", generate_bin_op_wrapper),
"__xor__": ("nb_xor", generate_bin_op_wrapper),
"__rxor__": ("nb_xor", generate_bin_op_wrapper),
"__matmul__": ("nb_matrix_multiply", generate_bin_op_wrapper),
"__rmatmul__": ("nb_matrix_multiply", generate_bin_op_wrapper),
# In-place binary operations.
"__iadd__": ("nb_inplace_add", generate_dunder_wrapper),
"__isub__": ("nb_inplace_subtract", generate_dunder_wrapper),
"__imul__": ("nb_inplace_multiply", generate_dunder_wrapper),
"__imod__": ("nb_inplace_remainder", generate_dunder_wrapper),
"__itruediv__": ("nb_inplace_true_divide", generate_dunder_wrapper),
"__ifloordiv__": ("nb_inplace_floor_divide", generate_dunder_wrapper),
"__ilshift__": ("nb_inplace_lshift", generate_dunder_wrapper),
"__irshift__": ("nb_inplace_rshift", generate_dunder_wrapper),
"__iand__": ("nb_inplace_and", generate_dunder_wrapper),
"__ior__": ("nb_inplace_or", generate_dunder_wrapper),
"__ixor__": ("nb_inplace_xor", generate_dunder_wrapper),
"__imatmul__": ("nb_inplace_matrix_multiply", generate_dunder_wrapper),
# Ternary operations. (yes, really)
# These are special cased in generate_bin_op_wrapper().
"__pow__": ("nb_power", generate_bin_op_wrapper),
"__rpow__": ("nb_power", generate_bin_op_wrapper),
"__ipow__": ("nb_inplace_power", generate_ipow_wrapper),
}
AS_ASYNC_SLOT_DEFS: SlotTable = {
"__await__": ("am_await", native_slot),
"__aiter__": ("am_aiter", native_slot),
"__anext__": ("am_anext", native_slot),
}
SIDE_TABLES = [
("as_mapping", "PyMappingMethods", AS_MAPPING_SLOT_DEFS),
("as_sequence", "PySequenceMethods", AS_SEQUENCE_SLOT_DEFS),
("as_number", "PyNumberMethods", AS_NUMBER_SLOT_DEFS),
("as_async", "PyAsyncMethods", AS_ASYNC_SLOT_DEFS),
]
# Slots that need to always be filled in because they don't get
# inherited right.
ALWAYS_FILL = {"__hash__"}
def generate_call_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
if emitter.use_vectorcall():
# Use vectorcall wrapper if supported (PEP 590).
return "PyVectorcall_Call"
else:
# On older Pythons use the legacy wrapper.
return wrapper_slot(cl, fn, emitter)
def slot_key(attr: str) -> str:
"""Map dunder method name to sort key.
Sort reverse operator methods and __delitem__ after others ('x' > '_').
"""
if (attr.startswith("__r") and attr != "__rshift__") or attr == "__delitem__":
return "x" + attr
return attr
def generate_slots(cl: ClassIR, table: SlotTable, emitter: Emitter) -> dict[str, str]:
fields: dict[str, str] = {}
generated: dict[str, str] = {}
# Sort for determinism on Python 3.5
for name, (slot, generator) in sorted(table.items(), key=lambda x: slot_key(x[0])):
method_cls = cl.get_method_and_class(name)
if method_cls and (method_cls[1] == cl or name in ALWAYS_FILL):
if slot in generated:
# Reuse previously generated wrapper.
fields[slot] = generated[slot]
else:
# Generate new wrapper.
name = generator(cl, method_cls[0], emitter)
fields[slot] = name
generated[slot] = name
return fields
def generate_class_type_decl(
cl: ClassIR, c_emitter: Emitter, external_emitter: Emitter, emitter: Emitter
) -> None:
context = c_emitter.context
name = emitter.type_struct_name(cl)
context.declarations[name] = HeaderDeclaration(
f"PyTypeObject *{emitter.type_struct_name(cl)};", needs_export=True
)
# If this is a non-extension class, all we want is the type object decl.
if not cl.is_ext_class:
return
generate_object_struct(cl, external_emitter)
generate_full = not cl.is_trait and not cl.builtin_base
if generate_full:
context.declarations[emitter.native_function_name(cl.ctor)] = HeaderDeclaration(
f"{native_function_header(cl.ctor, emitter)};", needs_export=True
)
def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None:
"""Generate C code for a class.
This is the main entry point to the module.
"""
name = cl.name
name_prefix = cl.name_prefix(emitter.names)
setup_name = f"{name_prefix}_setup"
new_name = f"{name_prefix}_new"
members_name = f"{name_prefix}_members"
getseters_name = f"{name_prefix}_getseters"
vtable_name = f"{name_prefix}_vtable"
traverse_name = f"{name_prefix}_traverse"
clear_name = f"{name_prefix}_clear"
dealloc_name = f"{name_prefix}_dealloc"
methods_name = f"{name_prefix}_methods"
vtable_setup_name = f"{name_prefix}_trait_vtable_setup"
fields: dict[str, str] = {}
fields["tp_name"] = f'"{name}"'
generate_full = not cl.is_trait and not cl.builtin_base
needs_getseters = cl.needs_getseters or not cl.is_generated
if not cl.builtin_base:
fields["tp_new"] = new_name
if generate_full:
fields["tp_dealloc"] = f"(destructor){name_prefix}_dealloc"
fields["tp_traverse"] = f"(traverseproc){name_prefix}_traverse"
fields["tp_clear"] = f"(inquiry){name_prefix}_clear"
if needs_getseters:
fields["tp_getset"] = getseters_name
fields["tp_methods"] = methods_name
def emit_line() -> None:
emitter.emit_line()
emit_line()
# If the class has a method to initialize default attribute
# values, we need to call it during initialization.
defaults_fn = cl.get_method("__mypyc_defaults_setup")
# If there is a __init__ method, we'll use it in the native constructor.
init_fn = cl.get_method("__init__")
# Fill out slots in the type object from dunder methods.
fields.update(generate_slots(cl, SLOT_DEFS, emitter))
# Fill out dunder methods that live in tables hanging off the side.
for table_name, type, slot_defs in SIDE_TABLES:
slots = generate_slots(cl, slot_defs, emitter)
if slots:
table_struct_name = generate_side_table_for_class(cl, table_name, type, slots, emitter)
fields[f"tp_{table_name}"] = f"&{table_struct_name}"
richcompare_name = generate_richcompare_wrapper(cl, emitter)
if richcompare_name:
fields["tp_richcompare"] = richcompare_name
# If the class inherits from python, make space for a __dict__
struct_name = cl.struct_name(emitter.names)
if cl.builtin_base:
base_size = f"sizeof({cl.builtin_base})"
elif cl.is_trait:
base_size = "sizeof(PyObject)"
else:
base_size = f"sizeof({struct_name})"
# Since our types aren't allocated using type() we need to
# populate these fields ourselves if we want them to have correct
# values. PyType_Ready will inherit the offsets from tp_base but
# that isn't what we want.
# XXX: there is no reason for the __weakref__ stuff to be mixed up with __dict__
if cl.has_dict and not has_managed_dict(cl, emitter):
# __dict__ lives right after the struct and __weakref__ lives right after that
# TODO: They should get members in the struct instead of doing this nonsense.
weak_offset = f"{base_size} + sizeof(PyObject *)"
emitter.emit_lines(
f"PyMemberDef {members_name}[] = {{",
f'{{"__dict__", T_OBJECT_EX, {base_size}, 0, NULL}},',
f'{{"__weakref__", T_OBJECT_EX, {weak_offset}, 0, NULL}},',
"{0}",
"};",
)
fields["tp_members"] = members_name
fields["tp_basicsize"] = f"{base_size} + 2*sizeof(PyObject *)"
if emitter.capi_version < (3, 12):
fields["tp_dictoffset"] = base_size
fields["tp_weaklistoffset"] = weak_offset
else:
fields["tp_basicsize"] = base_size
if generate_full:
# Declare setup method that allocates and initializes an object. type is the
# type of the class being initialized, which could be another class if there
# is an interpreted subclass.
emitter.emit_line(f"static PyObject *{setup_name}(PyTypeObject *type);")
assert cl.ctor is not None
emitter.emit_line(native_function_header(cl.ctor, emitter) + ";")
emit_line()
init_fn = cl.get_method("__init__")
generate_new_for_class(cl, new_name, vtable_name, setup_name, init_fn, emitter)
emit_line()
generate_traverse_for_class(cl, traverse_name, emitter)
emit_line()
generate_clear_for_class(cl, clear_name, emitter)
emit_line()
generate_dealloc_for_class(cl, dealloc_name, clear_name, emitter)
emit_line()
if cl.allow_interpreted_subclasses:
shadow_vtable_name: str | None = generate_vtables(
cl, vtable_setup_name + "_shadow", vtable_name + "_shadow", emitter, shadow=True
)
emit_line()
else:
shadow_vtable_name = None
vtable_name = generate_vtables(cl, vtable_setup_name, vtable_name, emitter, shadow=False)
emit_line()
if needs_getseters:
generate_getseter_declarations(cl, emitter)
emit_line()
generate_getseters_table(cl, getseters_name, emitter)
emit_line()
if cl.is_trait:
generate_new_for_trait(cl, new_name, emitter)
generate_methods_table(cl, methods_name, emitter)
emit_line()
flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE"]
if generate_full:
flags.append("Py_TPFLAGS_HAVE_GC")
if cl.has_method("__call__") and emitter.use_vectorcall():
fields["tp_vectorcall_offset"] = "offsetof({}, vectorcall)".format(
cl.struct_name(emitter.names)
)
flags.append("_Py_TPFLAGS_HAVE_VECTORCALL")
if not fields.get("tp_vectorcall"):
# This is just a placeholder to please CPython. It will be
# overridden during setup.
fields["tp_call"] = "PyVectorcall_Call"
if has_managed_dict(cl, emitter):
flags.append("Py_TPFLAGS_MANAGED_DICT")
fields["tp_flags"] = " | ".join(flags)
emitter.emit_line(f"static PyTypeObject {emitter.type_struct_name(cl)}_template_ = {{")
emitter.emit_line("PyVarObject_HEAD_INIT(NULL, 0)")
for field, value in fields.items():
emitter.emit_line(f".{field} = {value},")
emitter.emit_line("};")
emitter.emit_line(
"static PyTypeObject *{t}_template = &{t}_template_;".format(
t=emitter.type_struct_name(cl)
)
)
emitter.emit_line()
if generate_full:
generate_setup_for_class(
cl, setup_name, defaults_fn, vtable_name, shadow_vtable_name, emitter
)
emitter.emit_line()
generate_constructor_for_class(cl, cl.ctor, init_fn, setup_name, vtable_name, emitter)
emitter.emit_line()
if needs_getseters:
generate_getseters(cl, emitter)
def getter_name(cl: ClassIR, attribute: str, names: NameGenerator) -> str:
return names.private_name(cl.module_name, f"{cl.name}_get_{attribute}")
def setter_name(cl: ClassIR, attribute: str, names: NameGenerator) -> str:
return names.private_name(cl.module_name, f"{cl.name}_set_{attribute}")
def generate_object_struct(cl: ClassIR, emitter: Emitter) -> None:
seen_attrs: set[tuple[str, RType]] = set()
lines: list[str] = []
lines += ["typedef struct {", "PyObject_HEAD", "CPyVTableItem *vtable;"]
if cl.has_method("__call__") and emitter.use_vectorcall():
lines.append("vectorcallfunc vectorcall;")
bitmap_attrs = []
for base in reversed(cl.base_mro):
if not base.is_trait:
if base.bitmap_attrs:
# Do we need another attribute bitmap field?
if emitter.bitmap_field(len(base.bitmap_attrs) - 1) not in bitmap_attrs:
for i in range(0, len(base.bitmap_attrs), BITMAP_BITS):
attr = emitter.bitmap_field(i)
if attr not in bitmap_attrs:
lines.append(f"{BITMAP_TYPE} {attr};")
bitmap_attrs.append(attr)
for attr, rtype in base.attributes.items():
if (attr, rtype) not in seen_attrs:
lines.append(f"{emitter.ctype_spaced(rtype)}{emitter.attr(attr)};")
seen_attrs.add((attr, rtype))
if isinstance(rtype, RTuple):
emitter.declare_tuple_struct(rtype)
lines.append(f"}} {cl.struct_name(emitter.names)};")
lines.append("")
emitter.context.declarations[cl.struct_name(emitter.names)] = HeaderDeclaration(
lines, is_type=True
)
def generate_vtables(
base: ClassIR, vtable_setup_name: str, vtable_name: str, emitter: Emitter, shadow: bool
) -> str:
"""Emit the vtables and vtable setup functions for a class.
This includes both the primary vtable and any trait implementation vtables.
The trait vtables go before the main vtable, and have the following layout:
{
CPyType_T1, // pointer to type object
C_T1_trait_vtable, // pointer to array of method pointers
C_T1_offset_table, // pointer to array of attribute offsets
CPyType_T2,
C_T2_trait_vtable,
C_T2_offset_table,
...
}
The method implementations are calculated at the end of IR pass, attribute
offsets are {offsetof(native__C, _x1), offsetof(native__C, _y1), ...}.
To account for both dynamic loading and dynamic class creation,
vtables are populated dynamically at class creation time, so we
emit empty array definitions to store the vtables and a function to
populate them.
If shadow is True, generate "shadow vtables" that point to the
shadow glue methods (which should dispatch via the Python C-API).
Returns the expression to use to refer to the vtable, which might be
different than the name, if there are trait vtables.
"""
def trait_vtable_name(trait: ClassIR) -> str:
return "{}_{}_trait_vtable{}".format(
base.name_prefix(emitter.names),
trait.name_prefix(emitter.names),
"_shadow" if shadow else "",
)
def trait_offset_table_name(trait: ClassIR) -> str:
return "{}_{}_offset_table".format(
base.name_prefix(emitter.names), trait.name_prefix(emitter.names)
)
# Emit array definitions with enough space for all the entries
emitter.emit_line(
"static CPyVTableItem {}[{}];".format(
vtable_name, max(1, len(base.vtable_entries) + 3 * len(base.trait_vtables))
)
)
for trait, vtable in base.trait_vtables.items():
# Trait methods entry (vtable index -> method implementation).
emitter.emit_line(
f"static CPyVTableItem {trait_vtable_name(trait)}[{max(1, len(vtable))}];"
)
# Trait attributes entry (attribute number in trait -> offset in actual struct).
emitter.emit_line(
"static size_t {}[{}];".format(
trait_offset_table_name(trait), max(1, len(trait.attributes))
)
)
# Emit vtable setup function
emitter.emit_line("static bool")
emitter.emit_line(f"{NATIVE_PREFIX}{vtable_setup_name}(void)")
emitter.emit_line("{")
if base.allow_interpreted_subclasses and not shadow:
emitter.emit_line(f"{NATIVE_PREFIX}{vtable_setup_name}_shadow();")
subtables = []
for trait, vtable in base.trait_vtables.items():
name = trait_vtable_name(trait)
offset_name = trait_offset_table_name(trait)
generate_vtable(vtable, name, emitter, [], shadow)
generate_offset_table(offset_name, emitter, trait, base)
subtables.append((trait, name, offset_name))
generate_vtable(base.vtable_entries, vtable_name, emitter, subtables, shadow)
emitter.emit_line("return 1;")
emitter.emit_line("}")
return vtable_name if not subtables else f"{vtable_name} + {len(subtables) * 3}"
def generate_offset_table(
trait_offset_table_name: str, emitter: Emitter, trait: ClassIR, cl: ClassIR
) -> None:
"""Generate attribute offset row of a trait vtable."""
emitter.emit_line(f"size_t {trait_offset_table_name}_scratch[] = {{")
for attr in trait.attributes:
emitter.emit_line(f"offsetof({cl.struct_name(emitter.names)}, {emitter.attr(attr)}),")
if not trait.attributes:
# This is for msvc.
emitter.emit_line("0")
emitter.emit_line("};")
emitter.emit_line(
"memcpy({name}, {name}_scratch, sizeof({name}));".format(name=trait_offset_table_name)
)
def generate_vtable(
entries: VTableEntries,
vtable_name: str,
emitter: Emitter,
subtables: list[tuple[ClassIR, str, str]],
shadow: bool,
) -> None:
emitter.emit_line(f"CPyVTableItem {vtable_name}_scratch[] = {{")
if subtables:
emitter.emit_line("/* Array of trait vtables */")
for trait, table, offset_table in subtables:
emitter.emit_line(
"(CPyVTableItem){}, (CPyVTableItem){}, (CPyVTableItem){},".format(
emitter.type_struct_name(trait), table, offset_table
)
)
emitter.emit_line("/* Start of real vtable */")
for entry in entries:
method = entry.shadow_method if shadow and entry.shadow_method else entry.method
emitter.emit_line(
"(CPyVTableItem){}{}{},".format(
emitter.get_group_prefix(entry.method.decl),
NATIVE_PREFIX,
method.cname(emitter.names),
)
)
# msvc doesn't allow empty arrays; maybe allowing them at all is an extension?
if not entries:
emitter.emit_line("NULL")
emitter.emit_line("};")
emitter.emit_line("memcpy({name}, {name}_scratch, sizeof({name}));".format(name=vtable_name))
def generate_setup_for_class(
cl: ClassIR,
func_name: str,
defaults_fn: FuncIR | None,
vtable_name: str,
shadow_vtable_name: str | None,
emitter: Emitter,
) -> None:
"""Generate a native function that allocates an instance of a class."""
emitter.emit_line("static PyObject *")
emitter.emit_line(f"{func_name}(PyTypeObject *type)")
emitter.emit_line("{")
emitter.emit_line(f"{cl.struct_name(emitter.names)} *self;")
emitter.emit_line(f"self = ({cl.struct_name(emitter.names)} *)type->tp_alloc(type, 0);")
emitter.emit_line("if (self == NULL)")
emitter.emit_line(" return NULL;")
if shadow_vtable_name:
emitter.emit_line(f"if (type != {emitter.type_struct_name(cl)}) {{")
emitter.emit_line(f"self->vtable = {shadow_vtable_name};")
emitter.emit_line("} else {")
emitter.emit_line(f"self->vtable = {vtable_name};")
emitter.emit_line("}")
else:
emitter.emit_line(f"self->vtable = {vtable_name};")
for i in range(0, len(cl.bitmap_attrs), BITMAP_BITS):
field = emitter.bitmap_field(i)
emitter.emit_line(f"self->{field} = 0;")
if cl.has_method("__call__") and emitter.use_vectorcall():
name = cl.method_decl("__call__").cname(emitter.names)
emitter.emit_line(f"self->vectorcall = {PREFIX}{name};")
for base in reversed(cl.base_mro):
for attr, rtype in base.attributes.items():
value = emitter.c_undefined_value(rtype)
# We don't need to set this field to NULL since tp_alloc() already
# zero-initializes `self`.
if value != "NULL":
emitter.emit_line(rf"self->{emitter.attr(attr)} = {value};")
# Initialize attributes to default values, if necessary
if defaults_fn is not None:
emitter.emit_lines(
"if ({}{}((PyObject *)self) == 0) {{".format(
NATIVE_PREFIX, defaults_fn.cname(emitter.names)
),
"Py_DECREF(self);",
"return NULL;",
"}",
)
emitter.emit_line("return (PyObject *)self;")
emitter.emit_line("}")
def generate_constructor_for_class(
cl: ClassIR,
fn: FuncDecl,
init_fn: FuncIR | None,
setup_name: str,
vtable_name: str,
emitter: Emitter,
) -> None:
"""Generate a native function that allocates and initializes an instance of a class."""
emitter.emit_line(f"{native_function_header(fn, emitter)}")
emitter.emit_line("{")
emitter.emit_line(f"PyObject *self = {setup_name}({emitter.type_struct_name(cl)});")
emitter.emit_line("if (self == NULL)")
emitter.emit_line(" return NULL;")
args = ", ".join(["self"] + [REG_PREFIX + arg.name for arg in fn.sig.args])
if init_fn is not None:
emitter.emit_line(
"char res = {}{}{}({});".format(
emitter.get_group_prefix(init_fn.decl),
NATIVE_PREFIX,
init_fn.cname(emitter.names),
args,
)
)
emitter.emit_line("if (res == 2) {")
emitter.emit_line("Py_DECREF(self);")
emitter.emit_line("return NULL;")
emitter.emit_line("}")
# If there is a nontrivial ctor that we didn't define, invoke it via tp_init
elif len(fn.sig.args) > 1:
emitter.emit_line(f"int res = {emitter.type_struct_name(cl)}->tp_init({args});")
emitter.emit_line("if (res < 0) {")
emitter.emit_line("Py_DECREF(self);")
emitter.emit_line("return NULL;")
emitter.emit_line("}")
emitter.emit_line("return self;")
emitter.emit_line("}")
def generate_init_for_class(cl: ClassIR, init_fn: FuncIR, emitter: Emitter) -> str:
"""Generate an init function suitable for use as tp_init.
tp_init needs to be a function that returns an int, and our
__init__ methods return a PyObject. Translate NULL to -1,
everything else to 0.
"""
func_name = f"{cl.name_prefix(emitter.names)}_init"
emitter.emit_line("static int")
emitter.emit_line(f"{func_name}(PyObject *self, PyObject *args, PyObject *kwds)")
emitter.emit_line("{")
if cl.allow_interpreted_subclasses or cl.builtin_base:
emitter.emit_line(
"return {}{}(self, args, kwds) != NULL ? 0 : -1;".format(
PREFIX, init_fn.cname(emitter.names)
)
)
else:
emitter.emit_line("return 0;")
emitter.emit_line("}")
return func_name
def generate_new_for_class(
cl: ClassIR,
func_name: str,
vtable_name: str,
setup_name: str,
init_fn: FuncIR | None,
emitter: Emitter,
) -> None:
emitter.emit_line("static PyObject *")
emitter.emit_line(f"{func_name}(PyTypeObject *type, PyObject *args, PyObject *kwds)")
emitter.emit_line("{")
# TODO: Check and unbox arguments
if not cl.allow_interpreted_subclasses:
emitter.emit_line(f"if (type != {emitter.type_struct_name(cl)}) {{")
emitter.emit_line(
'PyErr_SetString(PyExc_TypeError, "interpreted classes cannot inherit from compiled");'
)
emitter.emit_line("return NULL;")
emitter.emit_line("}")
if not init_fn or cl.allow_interpreted_subclasses or cl.builtin_base or cl.is_serializable():
# Match Python semantics -- __new__ doesn't call __init__.
emitter.emit_line(f"return {setup_name}(type);")
else:
# __new__ of a native class implicitly calls __init__ so that we
# can enforce that instances are always properly initialized. This
# is needed to support always defined attributes.
emitter.emit_line(f"PyObject *self = {setup_name}(type);")
emitter.emit_lines("if (self == NULL)", " return NULL;")
emitter.emit_line(
f"PyObject *ret = {PREFIX}{init_fn.cname(emitter.names)}(self, args, kwds);"
)
emitter.emit_lines("if (ret == NULL)", " return NULL;")
emitter.emit_line("return self;")
emitter.emit_line("}")
def generate_new_for_trait(cl: ClassIR, func_name: str, emitter: Emitter) -> None:
emitter.emit_line("static PyObject *")
emitter.emit_line(f"{func_name}(PyTypeObject *type, PyObject *args, PyObject *kwds)")
emitter.emit_line("{")
emitter.emit_line(f"if (type != {emitter.type_struct_name(cl)}) {{")
emitter.emit_line(
"PyErr_SetString(PyExc_TypeError, "
'"interpreted classes cannot inherit from compiled traits");'
)
emitter.emit_line("} else {")
emitter.emit_line('PyErr_SetString(PyExc_TypeError, "traits may not be directly created");')
emitter.emit_line("}")
emitter.emit_line("return NULL;")
emitter.emit_line("}")
def generate_traverse_for_class(cl: ClassIR, func_name: str, emitter: Emitter) -> None:
"""Emit function that performs cycle GC traversal of an instance."""
emitter.emit_line("static int")
emitter.emit_line(
f"{func_name}({cl.struct_name(emitter.names)} *self, visitproc visit, void *arg)"
)
emitter.emit_line("{")
for base in reversed(cl.base_mro):
for attr, rtype in base.attributes.items():
emitter.emit_gc_visit(f"self->{emitter.attr(attr)}", rtype)
if has_managed_dict(cl, emitter):
emitter.emit_line("_PyObject_VisitManagedDict((PyObject *)self, visit, arg);")
elif cl.has_dict:
struct_name = cl.struct_name(emitter.names)
# __dict__ lives right after the struct and __weakref__ lives right after that
emitter.emit_gc_visit(
f"*((PyObject **)((char *)self + sizeof({struct_name})))", object_rprimitive
)
emitter.emit_gc_visit(
f"*((PyObject **)((char *)self + sizeof(PyObject *) + sizeof({struct_name})))",
object_rprimitive,
)
emitter.emit_line("return 0;")
emitter.emit_line("}")
def generate_clear_for_class(cl: ClassIR, func_name: str, emitter: Emitter) -> None:
emitter.emit_line("static int")
emitter.emit_line(f"{func_name}({cl.struct_name(emitter.names)} *self)")
emitter.emit_line("{")
for base in reversed(cl.base_mro):
for attr, rtype in base.attributes.items():
emitter.emit_gc_clear(f"self->{emitter.attr(attr)}", rtype)
if has_managed_dict(cl, emitter):
emitter.emit_line("_PyObject_ClearManagedDict((PyObject *)self);")
elif cl.has_dict:
struct_name = cl.struct_name(emitter.names)
# __dict__ lives right after the struct and __weakref__ lives right after that
emitter.emit_gc_clear(
f"*((PyObject **)((char *)self + sizeof({struct_name})))", object_rprimitive
)
emitter.emit_gc_clear(
f"*((PyObject **)((char *)self + sizeof(PyObject *) + sizeof({struct_name})))",
object_rprimitive,
)
emitter.emit_line("return 0;")
emitter.emit_line("}")
def generate_dealloc_for_class(
cl: ClassIR, dealloc_func_name: str, clear_func_name: str, emitter: Emitter
) -> None:
emitter.emit_line("static void")
emitter.emit_line(f"{dealloc_func_name}({cl.struct_name(emitter.names)} *self)")
emitter.emit_line("{")
emitter.emit_line("PyObject_GC_UnTrack(self);")
# The trashcan is needed to handle deep recursive deallocations
emitter.emit_line(f"CPy_TRASHCAN_BEGIN(self, {dealloc_func_name})")
emitter.emit_line(f"{clear_func_name}(self);")
emitter.emit_line("Py_TYPE(self)->tp_free((PyObject *)self);")
emitter.emit_line("CPy_TRASHCAN_END(self)")
emitter.emit_line("}")
def generate_methods_table(cl: ClassIR, name: str, emitter: Emitter) -> None:
emitter.emit_line(f"static PyMethodDef {name}[] = {{")
for fn in cl.methods.values():
if fn.decl.is_prop_setter or fn.decl.is_prop_getter:
continue
emitter.emit_line(f'{{"{fn.name}",')
emitter.emit_line(f" (PyCFunction){PREFIX}{fn.cname(emitter.names)},")
flags = ["METH_FASTCALL", "METH_KEYWORDS"]
if fn.decl.kind == FUNC_STATICMETHOD:
flags.append("METH_STATIC")
elif fn.decl.kind == FUNC_CLASSMETHOD:
flags.append("METH_CLASS")
emitter.emit_line(" {}, NULL}},".format(" | ".join(flags)))
# Provide a default __getstate__ and __setstate__
if not cl.has_method("__setstate__") and not cl.has_method("__getstate__"):
emitter.emit_lines(
'{"__setstate__", (PyCFunction)CPyPickle_SetState, METH_O, NULL},',
'{"__getstate__", (PyCFunction)CPyPickle_GetState, METH_NOARGS, NULL},',
)
emitter.emit_line("{NULL} /* Sentinel */")
emitter.emit_line("};")
def generate_side_table_for_class(
cl: ClassIR, name: str, type: str, slots: dict[str, str], emitter: Emitter
) -> str | None:
name = f"{cl.name_prefix(emitter.names)}_{name}"
emitter.emit_line(f"static {type} {name} = {{")
for field, value in slots.items():
emitter.emit_line(f".{field} = {value},")
emitter.emit_line("};")
return name
def generate_getseter_declarations(cl: ClassIR, emitter: Emitter) -> None:
if not cl.is_trait:
for attr in cl.attributes:
emitter.emit_line("static PyObject *")
emitter.emit_line(
"{}({} *self, void *closure);".format(
getter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
)
)
emitter.emit_line("static int")
emitter.emit_line(
"{}({} *self, PyObject *value, void *closure);".format(
setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
)
)
for prop, (getter, setter) in cl.properties.items():
if getter.decl.implicit:
continue
# Generate getter declaration
emitter.emit_line("static PyObject *")
emitter.emit_line(
"{}({} *self, void *closure);".format(
getter_name(cl, prop, emitter.names), cl.struct_name(emitter.names)
)
)
# Generate property setter declaration if a setter exists
if setter:
emitter.emit_line("static int")
emitter.emit_line(
"{}({} *self, PyObject *value, void *closure);".format(
setter_name(cl, prop, emitter.names), cl.struct_name(emitter.names)
)
)
def generate_getseters_table(cl: ClassIR, name: str, emitter: Emitter) -> None:
emitter.emit_line(f"static PyGetSetDef {name}[] = {{")
if not cl.is_trait:
for attr in cl.attributes:
emitter.emit_line(f'{{"{attr}",')
emitter.emit_line(
" (getter){}, (setter){},".format(
getter_name(cl, attr, emitter.names), setter_name(cl, attr, emitter.names)
)
)
emitter.emit_line(" NULL, NULL},")
for prop, (getter, setter) in cl.properties.items():
if getter.decl.implicit:
continue
emitter.emit_line(f'{{"{prop}",')
emitter.emit_line(f" (getter){getter_name(cl, prop, emitter.names)},")
if setter:
emitter.emit_line(f" (setter){setter_name(cl, prop, emitter.names)},")
emitter.emit_line("NULL, NULL},")
else:
emitter.emit_line("NULL, NULL, NULL},")
emitter.emit_line("{NULL} /* Sentinel */")
emitter.emit_line("};")
def generate_getseters(cl: ClassIR, emitter: Emitter) -> None:
if not cl.is_trait:
for i, (attr, rtype) in enumerate(cl.attributes.items()):
generate_getter(cl, attr, rtype, emitter)
emitter.emit_line("")
generate_setter(cl, attr, rtype, emitter)
if i < len(cl.attributes) - 1:
emitter.emit_line("")
for prop, (getter, setter) in cl.properties.items():
if getter.decl.implicit:
continue
rtype = getter.sig.ret_type
emitter.emit_line("")
generate_readonly_getter(cl, prop, rtype, getter, emitter)
if setter:
arg_type = setter.sig.args[1].type
emitter.emit_line("")
generate_property_setter(cl, prop, arg_type, setter, emitter)
def generate_getter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> None:
attr_field = emitter.attr(attr)
emitter.emit_line("static PyObject *")
emitter.emit_line(
"{}({} *self, void *closure)".format(
getter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
)
)
emitter.emit_line("{")
attr_expr = f"self->{attr_field}"
# HACK: Don't consider refcounted values as always defined, since it's possible to
# access uninitialized values via 'gc.get_objects()'. Accessing non-refcounted
# values is benign.
always_defined = cl.is_always_defined(attr) and not rtype.is_refcounted
if not always_defined:
emitter.emit_undefined_attr_check(rtype, attr_expr, "==", "self", attr, cl, unlikely=True)
emitter.emit_line("PyErr_SetString(PyExc_AttributeError,")
emitter.emit_line(f' "attribute {repr(attr)} of {repr(cl.name)} undefined");')
emitter.emit_line("return NULL;")
emitter.emit_line("}")
emitter.emit_inc_ref(f"self->{attr_field}", rtype)
emitter.emit_box(f"self->{attr_field}", "retval", rtype, declare_dest=True)
emitter.emit_line("return retval;")
emitter.emit_line("}")
def generate_setter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> None:
attr_field = emitter.attr(attr)
emitter.emit_line("static int")
emitter.emit_line(
"{}({} *self, PyObject *value, void *closure)".format(
setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
)
)
emitter.emit_line("{")
deletable = cl.is_deletable(attr)
if not deletable:
emitter.emit_line("if (value == NULL) {")
emitter.emit_line("PyErr_SetString(PyExc_AttributeError,")
emitter.emit_line(
f' "{repr(cl.name)} object attribute {repr(attr)} cannot be deleted");'
)
emitter.emit_line("return -1;")
emitter.emit_line("}")
# HACK: Don't consider refcounted values as always defined, since it's possible to
# access uninitialized values via 'gc.get_objects()'. Accessing non-refcounted
# values is benign.
always_defined = cl.is_always_defined(attr) and not rtype.is_refcounted
if rtype.is_refcounted:
attr_expr = f"self->{attr_field}"
if not always_defined:
emitter.emit_undefined_attr_check(rtype, attr_expr, "!=", "self", attr, cl)
emitter.emit_dec_ref(f"self->{attr_field}", rtype)
if not always_defined:
emitter.emit_line("}")
if deletable:
emitter.emit_line("if (value != NULL) {")
if rtype.is_unboxed:
emitter.emit_unbox("value", "tmp", rtype, error=ReturnHandler("-1"), declare_dest=True)
elif is_same_type(rtype, object_rprimitive):
emitter.emit_line("PyObject *tmp = value;")
else:
emitter.emit_cast("value", "tmp", rtype, declare_dest=True)
emitter.emit_lines("if (!tmp)", " return -1;")
emitter.emit_inc_ref("tmp", rtype)
emitter.emit_line(f"self->{attr_field} = tmp;")
if rtype.error_overlap and not always_defined:
emitter.emit_attr_bitmap_set("tmp", "self", rtype, cl, attr)
if deletable:
emitter.emit_line("} else")
emitter.emit_line(f" self->{attr_field} = {emitter.c_undefined_value(rtype)};")
if rtype.error_overlap:
emitter.emit_attr_bitmap_clear("self", rtype, cl, attr)
emitter.emit_line("return 0;")
emitter.emit_line("}")
def generate_readonly_getter(
cl: ClassIR, attr: str, rtype: RType, func_ir: FuncIR, emitter: Emitter
) -> None:
emitter.emit_line("static PyObject *")
emitter.emit_line(
"{}({} *self, void *closure)".format(
getter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
)
)
emitter.emit_line("{")
if rtype.is_unboxed:
emitter.emit_line(
"{}retval = {}{}((PyObject *) self);".format(
emitter.ctype_spaced(rtype), NATIVE_PREFIX, func_ir.cname(emitter.names)
)
)
emitter.emit_error_check("retval", rtype, "return NULL;")
emitter.emit_box("retval", "retbox", rtype, declare_dest=True)
emitter.emit_line("return retbox;")
else:
emitter.emit_line(
f"return {NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self);"
)
emitter.emit_line("}")
def generate_property_setter(
cl: ClassIR, attr: str, arg_type: RType, func_ir: FuncIR, emitter: Emitter
) -> None:
emitter.emit_line("static int")
emitter.emit_line(
"{}({} *self, PyObject *value, void *closure)".format(
setter_name(cl, attr, emitter.names), cl.struct_name(emitter.names)
)
)
emitter.emit_line("{")
if arg_type.is_unboxed:
emitter.emit_unbox("value", "tmp", arg_type, error=ReturnHandler("-1"), declare_dest=True)
emitter.emit_line(
f"{NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, tmp);"
)
else:
emitter.emit_line(
f"{NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, value);"
)
emitter.emit_line("return 0;")
emitter.emit_line("}")
def has_managed_dict(cl: ClassIR, emitter: Emitter) -> bool:
"""Should the class get the Py_TPFLAGS_MANAGED_DICT flag?"""
# On 3.11 and earlier the flag doesn't exist and we use
# tp_dictoffset instead. If a class inherits from Exception, the
# flag conflicts with tp_dictoffset set in the base class.
return (
emitter.capi_version >= (3, 12)
and cl.has_dict
and cl.builtin_base != "PyBaseExceptionObject"
)