From 2a3bfc0e808ed1cbd6709cc27f63d917718cc1b5 Mon Sep 17 00:00:00 2001 From: Matthew Parkinson Date: Fri, 16 Jan 2026 13:06:06 +0000 Subject: [PATCH 1/2] Add tp_reachable This adds a new slot to the type that can provide a custom traversal for handling immutable/freezing of the code. Currently if this is not provided it falls back to tp_traverse. --- Doc/includes/typestruct.h | 3 +++ Include/cpython/object.h | 3 +++ Include/typeslots.h | 4 ++++ Objects/dictobject.c | 47 +++++++++++++++++++++++++++++++++++++++ Objects/listobject.c | 7 ++++++ Objects/tupleobject.c | 7 ++++++ Objects/typeobject.c | 32 ++++++++++++++++++++++++++ Objects/typeslots.inc | 1 + Objects/unicodeobject.c | 8 +++++++ Python/immutability.c | 20 ++++++++--------- 10 files changed, 122 insertions(+), 10 deletions(-) diff --git a/Doc/includes/typestruct.h b/Doc/includes/typestruct.h index 0d1d85ce9741b2..a773d3291f3118 100644 --- a/Doc/includes/typestruct.h +++ b/Doc/includes/typestruct.h @@ -92,4 +92,7 @@ typedef struct _typeobject { * Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere). */ uint16_t tp_versions_used; + + /* call function for all referenced objects (includes non-cyclic refs) */ + traverseproc tp_reachable; } PyTypeObject; diff --git a/Include/cpython/object.h b/Include/cpython/object.h index 9ad33af3d69a23..7f62f79fb421a4 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -239,6 +239,9 @@ struct _typeobject { * Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere). */ uint16_t tp_versions_used; + + /* call function for all referenced objects (includes non-cyclic refs) */ + traverseproc tp_reachable; }; #define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used) diff --git a/Include/typeslots.h b/Include/typeslots.h index a7f3017ec02e92..53e8527ad992a7 100644 --- a/Include/typeslots.h +++ b/Include/typeslots.h @@ -94,3 +94,7 @@ /* New in 3.14 */ #define Py_tp_token 83 #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030F0000 +/* New in 3.15 */ +#define Py_tp_reachable 84 +#endif diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 6b55f3edbd913b..815bddc61769d4 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4748,6 +4748,52 @@ dict_traverse(PyObject *op, visitproc visit, void *arg) return 0; } +static int +dict_reachable(PyObject *op, visitproc visit, void *arg) +{ + PyDictObject *mp = (PyDictObject *)op; + PyDictKeysObject *keys = mp->ma_keys; + Py_ssize_t n = keys->dk_nentries; + + if (DK_IS_UNICODE(keys)) { + PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); + if (_PyDict_HasSplitTable(mp)) { + PyObject **values = mp->ma_values->values; + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *value = values[i]; + if (value == NULL) { + continue; + } + Py_VISIT(entries[i].me_key); + Py_VISIT(value); + } + } + else { + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *value = entries[i].me_value; + if (value == NULL) { + continue; + } + Py_VISIT(entries[i].me_key); + Py_VISIT(value); + } + } + } + else { + PyDictKeyEntry *entries = DK_ENTRIES(keys); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *value = entries[i].me_value; + if (value == NULL) { + continue; + } + Py_VISIT(entries[i].me_key); + Py_VISIT(value); + } + } + + return 0; +} + static int dict_tp_clear(PyObject *op) { @@ -5072,6 +5118,7 @@ PyTypeObject PyDict_Type = { PyObject_GC_Del, /* tp_free */ .tp_vectorcall = dict_vectorcall, .tp_version_tag = _Py_TYPE_VERSION_DICT, + .tp_reachable = dict_reachable, }; /* For backward compatibility with old dictionary interface */ diff --git a/Objects/listobject.c b/Objects/listobject.c index a52eb6e0bb5a1e..797906045641c4 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3463,6 +3463,12 @@ list_traverse(PyObject *self, visitproc visit, void *arg) return 0; } +static int +list_reachable(PyObject *self, visitproc visit, void *arg) +{ + return list_traverse(self, visit, arg); +} + static PyObject * list_richcompare_impl(PyObject *v, PyObject *w, int op) { @@ -3985,6 +3991,7 @@ PyTypeObject PyList_Type = { PyObject_GC_Del, /* tp_free */ .tp_vectorcall = list_vectorcall, .tp_version_tag = _Py_TYPE_VERSION_LIST, + .tp_reachable = list_reachable, }; /*********************** List Iterator **************************/ diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 2cd8903e493752..8dd6febc8eff47 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -632,6 +632,12 @@ tuple_traverse(PyObject *self, visitproc visit, void *arg) return 0; } +static int +tuple_reachable(PyObject *self, visitproc visit, void *arg) +{ + return tuple_traverse(self, visit, arg); +} + static PyObject * tuple_richcompare(PyObject *v, PyObject *w, int op) { @@ -911,6 +917,7 @@ PyTypeObject PyTuple_Type = { PyObject_GC_Del, /* tp_free */ .tp_vectorcall = tuple_vectorcall, .tp_version_tag = _Py_TYPE_VERSION_TUPLE, + .tp_reachable = tuple_reachable, }; /* The following function breaks the notion that tuples are immutable: diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 0bdb672056d02b..dad48b2a59b639 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -214,6 +214,9 @@ slot_tp_setattro(PyObject *self, PyObject *name, PyObject *value); static PyObject * slot_tp_call(PyObject *self, PyObject *args, PyObject *kwds); +static int +type_reachable(PyObject *self, visitproc visit, void *arg); + static inline PyTypeObject * type_from_ref(PyObject *ref) { @@ -6975,6 +6978,30 @@ type_traverse(PyObject *self, visitproc visit, void *arg) return 0; } +static int +type_reachable(PyObject *self, visitproc visit, void *arg) +{ + PyTypeObject *type = PyTypeObject_CAST(self); + + Py_VISIT(lookup_tp_dict(type)); + Py_VISIT(type->tp_cache); + Py_VISIT(lookup_tp_mro(type)); + Py_VISIT(lookup_tp_bases(type)); + Py_VISIT(type->tp_base); + Py_VISIT(lookup_tp_subclasses(type)); + Py_VISIT(type->tp_weaklist); + + if (type->tp_flags & Py_TPFLAGS_HEAPTYPE) { + PyHeapTypeObject *ht = (PyHeapTypeObject *)type; + Py_VISIT(ht->ht_module); + Py_VISIT(ht->ht_name); + Py_VISIT(ht->ht_qualname); + Py_VISIT(ht->ht_slots); + } + + return 0; +} + static int type_clear(PyObject *self) { @@ -7079,6 +7106,7 @@ PyTypeObject PyType_Type = { PyObject_GC_Del, /* tp_free */ type_is_gc, /* tp_is_gc */ .tp_vectorcall = type_vectorcall, + .tp_reachable = type_reachable, }; @@ -8503,6 +8531,10 @@ inherit_special(PyTypeObject *type, PyTypeObject *base) #undef COPYVAL + if (type->tp_reachable == NULL) { + type->tp_reachable = base->tp_reachable; + } + /* Setup fast subclass flags */ PyObject *mro = lookup_tp_mro(base); unsigned long flags = 0; diff --git a/Objects/typeslots.inc b/Objects/typeslots.inc index 642160fe0bd8bc..17af795308538d 100644 --- a/Objects/typeslots.inc +++ b/Objects/typeslots.inc @@ -82,3 +82,4 @@ {offsetof(PyAsyncMethods, am_send), offsetof(PyTypeObject, tp_as_async)}, {-1, offsetof(PyTypeObject, tp_vectorcall)}, {-1, offsetof(PyHeapTypeObject, ht_token)}, +{-1, offsetof(PyTypeObject, tp_reachable)}, diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index d4549b70d4dabc..3d4b5ff30fea30 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -14629,6 +14629,13 @@ errors defaults to 'strict'."); static PyObject *unicode_iter(PyObject *seq); +static int +unicode_reachable(PyObject *self, visitproc visit, void *arg) +{ + // Strings do not own references to other PyObjects. + return 0; +} + PyTypeObject PyUnicode_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "str", /* tp_name */ @@ -14673,6 +14680,7 @@ PyTypeObject PyUnicode_Type = { unicode_new, /* tp_new */ PyObject_Free, /* tp_free */ .tp_vectorcall = unicode_vectorcall, + .tp_reachable = unicode_reachable, }; /* Initialize the Unicode implementation */ diff --git a/Python/immutability.c b/Python/immutability.c index dce167b2df1d7f..a7a1f75ef42c89 100644 --- a/Python/immutability.c +++ b/Python/immutability.c @@ -1526,9 +1526,12 @@ int traverse_freeze(PyObject* obj, struct FreezeState* freeze_state) } else { - traverseproc traverse = Py_TYPE(obj)->tp_traverse; - if(traverse != NULL){ - SUCCEEDS(traverse(obj, (visitproc)freeze_visit, freeze_state)); + traverseproc references = Py_TYPE(obj)->tp_reachable; + if (references == NULL) { + references = Py_TYPE(obj)->tp_traverse; + } + if(references != NULL){ + SUCCEEDS(references(obj, (visitproc)freeze_visit, freeze_state)); } } @@ -1550,12 +1553,9 @@ int traverse_freeze(PyObject* obj, struct FreezeState* freeze_state) } } - // The default tp_traverse will not visit the type object if it is - // not heap allocated, so we need to do that manually here to freeze - // the statically allocated types that are reachable. - if (!(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_HEAPTYPE)) { - SUCCEEDS(freeze_visit(_PyObject_CAST(Py_TYPE(obj)), freeze_state)); - } + // Always freeze the type object itself; tp_reachable implementations + // may omit visiting the type (e.g., dict-derived heap types). + SUCCEEDS(freeze_visit(_PyObject_CAST(Py_TYPE(obj)), freeze_state)); return 0; @@ -1678,4 +1678,4 @@ int _PyImmutability_Freeze(PyObject* obj) deallocate_FreezeState(&freeze_state); TRACE_MERMAID_END(); return result; -} \ No newline at end of file +} From 40d1fcec990c0a90438b49fc6e154dbf6ca72d3e Mon Sep 17 00:00:00 2001 From: Matthew Parkinson Date: Fri, 16 Jan 2026 13:48:50 +0000 Subject: [PATCH 2/2] Add type to reachable. This removes a special case from the code. --- Objects/dictobject.c | 2 ++ Objects/listobject.c | 1 + Objects/tupleobject.c | 1 + Objects/typeobject.c | 17 +++++++++++++++++ Objects/unicodeobject.c | 4 +++- Python/immutability.c | 4 ---- 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 815bddc61769d4..60c7a54680502f 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4755,6 +4755,8 @@ dict_reachable(PyObject *op, visitproc visit, void *arg) PyDictKeysObject *keys = mp->ma_keys; Py_ssize_t n = keys->dk_nentries; + Py_VISIT(_PyObject_CAST(Py_TYPE(op))); + if (DK_IS_UNICODE(keys)) { PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); if (_PyDict_HasSplitTable(mp)) { diff --git a/Objects/listobject.c b/Objects/listobject.c index 797906045641c4..10208026981717 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3466,6 +3466,7 @@ list_traverse(PyObject *self, visitproc visit, void *arg) static int list_reachable(PyObject *self, visitproc visit, void *arg) { + Py_VISIT(_PyObject_CAST(Py_TYPE(self))); return list_traverse(self, visit, arg); } diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 8dd6febc8eff47..e0ac16ddc08a44 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -635,6 +635,7 @@ tuple_traverse(PyObject *self, visitproc visit, void *arg) static int tuple_reachable(PyObject *self, visitproc visit, void *arg) { + Py_VISIT(_PyObject_CAST(Py_TYPE(self))); return tuple_traverse(self, visit, arg); } diff --git a/Objects/typeobject.c b/Objects/typeobject.c index dad48b2a59b639..95a89f9d68556a 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6947,6 +6947,9 @@ PyDoc_STRVAR(type_doc, "type(object) -> the object's type\n" "type(name, bases, dict, **kwds) -> a new type"); +static int +object_reachable(PyObject *self, visitproc visit, void *arg); + static int type_traverse(PyObject *self, visitproc visit, void *arg) { @@ -8366,8 +8369,22 @@ PyTypeObject PyBaseObject_Type = { PyType_GenericAlloc, /* tp_alloc */ object_new, /* tp_new */ PyObject_Free, /* tp_free */ + .tp_reachable = object_reachable, }; +static int +object_reachable(PyObject *self, visitproc visit, void *arg) +{ + Py_VISIT(_PyObject_CAST(Py_TYPE(self))); + + traverseproc traverse = Py_TYPE(self)->tp_traverse; + if (traverse != NULL) { + return traverse(self, visit, arg); + } + + return 0; +} + static int type_add_method(PyTypeObject *type, PyMethodDef *meth) diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 3d4b5ff30fea30..b51b278397cab6 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -14632,7 +14632,9 @@ static PyObject *unicode_iter(PyObject *seq); static int unicode_reachable(PyObject *self, visitproc visit, void *arg) { - // Strings do not own references to other PyObjects. + // Strings do not own references to other PyObjects, but we still + // report reachability to the type object. + Py_VISIT(_PyObject_CAST(Py_TYPE(self))); return 0; } diff --git a/Python/immutability.c b/Python/immutability.c index a7a1f75ef42c89..50ee13ade2a9cc 100644 --- a/Python/immutability.c +++ b/Python/immutability.c @@ -1553,10 +1553,6 @@ int traverse_freeze(PyObject* obj, struct FreezeState* freeze_state) } } - // Always freeze the type object itself; tp_reachable implementations - // may omit visiting the type (e.g., dict-derived heap types). - SUCCEEDS(freeze_visit(_PyObject_CAST(Py_TYPE(obj)), freeze_state)); - return 0; error: