| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
Unverified review from Codex DetailsThe direction fixes the immediate managed-dict symptom, but it introduces two blocking lifecycle bugs for compiled dict subclasses. I reviewed PR head f86a3b246 and tested it locally. Findings
The generated builtin-base deallocator calls: Py_TYPE(self)->tp_base->tp_dealloc((PyObject *)self);Generated from emitclass.py. That is unsafe when the object is an instance of a subclass. Example: class A(dict):
pass
class B(A):
pass
def f() -> None:
b = B()
del bFor a B instance:
I reproduced this with a scratch mypyc build from the PR. A plain class A(dict); class B(A); del B() segfaulted immediately. If this approach is kept, the deallocator must not use the dynamic type to find the base deallocator for the current generated dealloc function. It would need to call the current class’s static base deallocator, not Py_TYPE(self)->tp_base.
The PR installs custom GC slots for managed-dict classes here: emitclass.py. But the generated traversal only does: PyObject_VisitManagedDict((PyObject *)self, visit, arg);from emitclass.py, and clear only does: PyObject_ClearManagedDict((PyObject *)self);from emitclass.py. For a dict subclass, the instance owns two separate reference containers:
The PR visits/clears the first but no longer delegates to dict’s traversal/clear for the second. So cycles through dict entries become invisible to cyclic GC. I reproduced this: events = []
class CyclicDict(dict):
def __init__(self) -> None:
self["self"] = self
def __del__(self) -> None:
events.append("deleted")
c = CyclicDict()
del c
gc.collect()
assert events == ["deleted"]Pure Python collects it and calls __del__. With the PR-compiled version, events stays empty: the object leaks because GC does not see the self-reference inside the dict storage. This matches the CPython C API contract: tp_traverse is how GC detects cycles, and tp_clear must break cycles. The managed-dict helper calls are required, but they are additive; they do not replace traversal/clearing of base-owned references. Docs: https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse and https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_clear.
The PR keeps Py_TPFLAGS_MANAGED_DICT enabled for dict subclasses via has_managed_dict, then tries to generate enough lifecycle slots to satisfy CPython. That has to compose:
The two bugs above are both symptoms of that composition problem. I still think the simpler fix is better: do not use managed dicts for builtin subclasses. Use managed dicts only when cl.builtin_base is None, and for PyDictObject subclasses on Python 3.12+ keep the old explicit tp_dictoffset / tp_weaklistoffset layout. That avoids needing to wrap dict’s lifecycle at all.
The new tests cover the direct original lifetime shape well: run-dicts.test. The interpreted driver part is especially important. I would add at least: class A(dict):
pass
class B(A):
pass
def test() -> None:
b = B()
del band: import gc
events: list[str] = []
class D(dict):
def __init__(self) -> None:
self["self"] = self
def __del__(self) -> None:
events.append("deleted")
def test() -> None:
d = D()
del d
gc.collect()
assert events == ["deleted"]The second test specifically catches missing base dict traversal/clear.
The PR emits: if (rv < 0) return rv;for PyObject_VisitManagedDict. Py_VISIT conventionally returns on any nonzero visitor result. I would use: if (rv) return rv;This is smaller than the lifecycle bugs above, but it is the usual traversal contract. I didn't check anything it says. Apologies if spam, but posting since it did well on minimising the repro |
Sorry, something went wrong.
|
thanks @hauntsaninja the blocking issues were legitimate and should be fixed now. i've also added tests similar to the code snippets.
not sure about this one since the docs suggest that the old layout will be deprecated so we might have to continue using managed dicts. but either way i think using the suggested approach would be a big change so it would be best to do it in another PR. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Fixes #21133
Types with the Py_TPFLAGS_MANAGED_DICT flag must call PyObject_VisitManagedDict and PyObject_ClearManagedDict in their tp_traverse / tp_clear functions according to docs but the types generated by mypyc currently don't do this. We don't generate these functions at all so they get inherited from the base class.
Failure to call these functions may result in a segfault in python 3.14 when accessing the managed dict after its owner has been deallocated. I believe the crash happens because the logic for types with the Py_TPFLAGS_INLINE_VALUES flag in PyObject_ClearManagedDict is not run. The condition to add this flag has changed in 3.14 vs 3.13 so generated types with Py_TPFLAGS_MANAGED_DICT get it in 3.14.
To fix, generate tp_clear, tp_traverse, and tp_dealloc for types with managed dicts. Also add a special case in these functions for classes with built-in bases to call the pointer of the base class.