Skip to content
Merged
27 changes: 27 additions & 0 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None:

setup_name = f"{name_prefix}_setup"
new_name = f"{name_prefix}_new"
finalize_name = f"{name_prefix}_finalize"
members_name = f"{name_prefix}_members"
getseters_name = f"{name_prefix}_getseters"
vtable_name = f"{name_prefix}_vtable"
Expand All @@ -226,6 +227,10 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None:
fields["tp_dealloc"] = f"(destructor){name_prefix}_dealloc"
fields["tp_traverse"] = f"(traverseproc){name_prefix}_traverse"
fields["tp_clear"] = f"(inquiry){name_prefix}_clear"
# Populate .tp_finalize and generate a finalize method only if __del__ is defined for this class.
del_method = next((e.method for e in cl.vtable_entries if e.name == "__del__"), None)
if del_method:
fields["tp_finalize"] = f"(destructor){finalize_name}"
if needs_getseters:
fields["tp_getset"] = getseters_name
fields["tp_methods"] = methods_name
Expand Down Expand Up @@ -308,6 +313,9 @@ def emit_line() -> None:
emit_line()
generate_dealloc_for_class(cl, dealloc_name, clear_name, emitter)
emit_line()
if del_method:
generate_finalize_for_class(del_method, finalize_name, emitter)
emit_line()

if cl.allow_interpreted_subclasses:
shadow_vtable_name: str | None = generate_vtables(
Expand Down Expand Up @@ -779,6 +787,9 @@ def generate_dealloc_for_class(
emitter.emit_line("static void")
emitter.emit_line(f"{dealloc_func_name}({cl.struct_name(emitter.names)} *self)")
emitter.emit_line("{")
emitter.emit_line("if (Py_TYPE(self)->tp_finalize) {")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid the runtime check if the caller would pass a flag indicating whether there is a __del__ method defined for the class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

emitter.emit_line(" Py_TYPE(self)->tp_finalize((PyObject *)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})")
Expand All @@ -788,6 +799,22 @@ def generate_dealloc_for_class(
emitter.emit_line("}")


def generate_finalize_for_class(
del_method: FuncIR, finalize_func_name: str, emitter: Emitter
) -> None:
emitter.emit_line("static void")
emitter.emit_line(f"{finalize_func_name}(PyObject *self)")
emitter.emit_line("{")
emitter.emit_line(
"{}{}{}(self);".format(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we preserve the current exception status, as suggested in the docs: https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_finalize

Copy link
Contributor Author

@advait-dixit advait-dixit Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

emitter.get_group_prefix(del_method.decl),
NATIVE_PREFIX,
del_method.cname(emitter.names),
)
)
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():
Expand Down
26 changes: 26 additions & 0 deletions mypyc/test-data/run-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2719,3 +2719,29 @@ print(native.A(ints=[1, -17]).ints)

[out]
\[1, -17]

[case testDel]
class A:
def __del__(self):
print("deleting A...")

class B:
def __del__(self):
print("deleting B...")

class C(B):
def __init__(self):
self.a = A()

def __del__(self):
print("deleting C...")
super().__del__()

[file driver.py]
import native
native.C()

[out]
deleting C...
deleting B...
deleting A...
Loading