From d19c7e51058c97be3b251722d3b8738aafe6b319 Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Wed, 19 Aug 2026 15:35:12 +0200 Subject: [PATCH] [mypyc] Clear only subtype-owned refs in tp_dealloc of types with built-in base --- mypyc/codegen/emitclass.py | 69 +++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index b15c48092ce57..c3328abb5d931 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -253,6 +253,9 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None: vtable_name = f"{name_prefix}_vtable" traverse_name = f"{name_prefix}_traverse" clear_name = emitter.native_function_name(cl.clear) + clear_subtype_name = f"{clear_name}_subtype" + clear_on_completion_name = emitter.native_function_name(cl.clear_on_completion) + clear_on_completion_subtype_name = f"{clear_on_completion_name}_subtype" dealloc_name = f"{name_prefix}_dealloc" methods_name = f"{name_prefix}_methods" vtable_setup_name = f"{name_prefix}_trait_vtable_setup" @@ -353,13 +356,38 @@ def emit_line() -> None: if not cl.is_acyclic: generate_traverse_for_class(cl, traverse_name, emitter) emit_line() - generate_clear_for_class(cl, cl.clear, emitter) + subtype_clear_name = None + if cl.builtin_base: + # During dealloc, the object has refcount 0, and builtin tp_clear + # implementations are not generally valid to call in that state. In + # particular, dict.tp_clear notifies watchers and requires a live + # object. Split subtype-owned clearing from base-owned clearing so + # tp_clear can still delegate to the base, while tp_dealloc only + # clears mypyc-owned state before delegating to the base deallocator. + generate_subtype_clear_for_class(cl, clear_subtype_name, emitter) + emit_line() + subtype_clear_name = clear_subtype_name + generate_clear_for_class(cl, cl.clear, emitter, subtype_clear_name=subtype_clear_name) emit_line() + if cl.builtin_base: + generate_subtype_clear_for_class( + cl, + clear_on_completion_subtype_name, + emitter, + skip_attrs=cl.attrs_to_keep_alive_on_completion, + ) + emit_line() + subtype_clear_name = clear_on_completion_subtype_name generate_clear_for_class( - cl, cl.clear_on_completion, emitter, skip_attrs=cl.attrs_to_keep_alive_on_completion + cl, + cl.clear_on_completion, + emitter, + subtype_clear_name=subtype_clear_name, + skip_attrs=cl.attrs_to_keep_alive_on_completion, ) emit_line() - generate_dealloc_for_class(cl, dealloc_name, clear_name, bool(del_method), emitter) + dealloc_clear_name = clear_subtype_name if cl.builtin_base else clear_name + generate_dealloc_for_class(cl, dealloc_name, dealloc_clear_name, bool(del_method), emitter) emit_line() if generate_full: assert cl.setup is not None @@ -905,12 +933,41 @@ def generate_traverse_for_class(cl: ClassIR, func_name: str, emitter: Emitter) - def generate_clear_for_class( - cl: ClassIR, func_decl: FuncDecl, emitter: Emitter, skip_attrs: set[str] | None = None + cl: ClassIR, + func_decl: FuncDecl, + emitter: Emitter, + subtype_clear_name: str | None = None, + skip_attrs: set[str] | None = None, ) -> None: if skip_attrs is None: skip_attrs = set() emitter.emit_line("static " + native_function_header(func_decl, emitter)) emitter.emit_line("{") + if subtype_clear_name is not None: + emitter.emit_line(f"int32_t res = {subtype_clear_name}(cpy_r_self);") + emitter.emit_line("if (res != 0) return res;") + emitter.emit_base_tp_function_call( + cl, "tp_clear", "(PyObject *)cpy_r_self", prefix="return " + ) + else: + generate_subtype_clear_body(cl, emitter, skip_attrs) + emitter.emit_line("return 0;") + emitter.emit_line("}") + + +def generate_subtype_clear_for_class( + cl: ClassIR, func_name: str, emitter: Emitter, skip_attrs: set[str] | None = None +) -> None: + if skip_attrs is None: + skip_attrs = set() + emitter.emit_line(f"static int32_t {func_name}(PyObject *cpy_r_self)") + emitter.emit_line("{") + generate_subtype_clear_body(cl, emitter, skip_attrs) + emitter.emit_line("return 0;") + emitter.emit_line("}") + + +def generate_subtype_clear_body(cl: ClassIR, emitter: Emitter, skip_attrs: set[str]) -> None: emitter.emit_line( f"{cl.struct_name(emitter.names)} *self = " f"({cl.struct_name(emitter.names)} *)cpy_r_self;" @@ -921,8 +978,6 @@ def generate_clear_for_class( continue emitter.emit_gc_clear(f"self->{emitter.attr(attr)}", rtype) base_args = "(PyObject *)self" - if cl.builtin_base: - emitter.emit_base_tp_function_call(cl, "tp_clear", base_args) if has_managed_dict(cl, emitter): emitter.emit_line(f"PyObject_ClearManagedDict({base_args});") elif cl.has_dict: @@ -932,8 +987,6 @@ def generate_clear_for_class( emitter.emit_gc_clear( f"*((PyObject **)((char *)self + sizeof(PyObject *) + {base_size}))", object_rprimitive ) - emitter.emit_line("return 0;") - emitter.emit_line("}") def generate_dealloc_for_class(