@@ -208,8 +208,39 @@ static int _decref_visitor(HPyField *pf, void *arg)
208208 return 0 ;
209209}
210210
211- static void hpytype_clear (PyObject * self )
211+ /* Python 3.13+ requires proper handling of Py_TPFLAGS_MANAGED_DICT.
212+ * Types that inherit from object get this flag, and must call
213+ * PyObject_VisitManagedDict in tp_traverse and PyObject_ClearManagedDict
214+ * in tp_clear/tp_dealloc. Without this, attribute lookup crashes.
215+ */
216+ #if PY_VERSION_HEX >= 0x030D0000
217+ #define HPY_HAS_MANAGED_DICT 1
218+ #else
219+ #define HPY_HAS_MANAGED_DICT 0
220+ #endif
221+
222+ #if HPY_HAS_MANAGED_DICT
223+ /* Generic tp_traverse for HPy types that don't define their own HPy_tp_traverse.
224+ * This is needed on Python 3.13+ because we need to visit the managed dict.
225+ * For types WITH a user-defined traverse, the trampoline calls
226+ * call_traverseproc_from_trampoline which handles managed dict. */
227+ static int hpytype_traverse (PyObject * self , cpy_visitproc visit , void * arg )
228+ {
229+ /* On Python 3.13+, we must visit the managed dict */
230+ PyObject_VisitManagedDict (self , visit , arg );
231+ /* Visit the type object (required for heap types) */
232+ Py_VISIT (Py_TYPE (self ));
233+ return 0 ;
234+ }
235+ #endif /* HPY_HAS_MANAGED_DICT */
236+
237+ static int hpytype_clear (PyObject * self )
212238{
239+ #if HPY_HAS_MANAGED_DICT
240+ /* On Python 3.13+, we must clear the managed dict */
241+ PyObject_ClearManagedDict (self );
242+ #endif
243+
213244 // call tp_traverse on all the HPy types of the hierarchy
214245 PyTypeObject * tp = Py_TYPE (self );
215246 PyTypeObject * base = tp ;
@@ -223,6 +254,7 @@ static void hpytype_clear(PyObject *self)
223254 }
224255 base = base -> tp_base ;
225256 }
257+ return 0 ;
226258}
227259
228260/* this is a generic tp_dealloc which we use for all the user-defined HPy
@@ -243,6 +275,13 @@ static void hpytype_dealloc(PyObject *self)
243275 // decref and clear all the HPyFields
244276 hpytype_clear (self );
245277
278+ #if HPY_HAS_MANAGED_DICT
279+ /* On Python 3.13+, ensure managed dict is cleared before dealloc.
280+ * Note: hpytype_clear already calls PyObject_ClearManagedDict, but
281+ * calling it again is safe and ensures proper cleanup. */
282+ PyObject_ClearManagedDict (self );
283+ #endif
284+
246285 // call tp_destroy on all the HPy types of the hierarchy
247286 PyTypeObject * base = tp ;
248287 while (base ) {
@@ -668,8 +707,18 @@ create_slot_defs(HPyType_Spec *hpyspec, HPyType_Extra_t *extra,
668707 hpyslot_count ++ ; // Py_tp_getset
669708 if (needs_dealloc )
670709 hpyslot_count ++ ; // Py_tp_dealloc
710+ #if HPY_HAS_MANAGED_DICT
711+ /* On Python 3.13+, we always need tp_traverse and tp_clear to handle
712+ * the managed dict properly, even for types without user-defined traverse.
713+ * If user provides HPy_tp_traverse, their trampoline handles managed dict.
714+ * If not, we add hpytype_traverse. Either way, we add hpytype_clear. */
715+ if (!has_tp_traverse (hpyspec ))
716+ hpyslot_count ++ ; // Py_tp_traverse (only if user didn't provide one)
717+ hpyslot_count ++ ; // Py_tp_clear (always needed on 3.13+)
718+ #else
671719 if (has_tp_traverse (hpyspec ))
672720 hpyslot_count ++ ; // Py_tp_clear
721+ #endif
673722
674723 // allocate the result PyType_Slot array
675724 HPy_ssize_t total_slot_count = hpyslot_count + legacy_slot_count ;
@@ -824,10 +873,26 @@ create_slot_defs(HPyType_Spec *hpyspec, HPyType_Extra_t *extra,
824873 result [dst_idx ++ ] = (PyType_Slot ){Py_tp_dealloc , (void * )hpytype_dealloc };
825874 }
826875
876+ #if HPY_HAS_MANAGED_DICT
877+ /* On Python 3.13+, we always need tp_traverse and tp_clear to handle
878+ * the managed dict properly. Py_TPFLAGS_MANAGED_DICT is inherited from
879+ * object, and requires calling PyObject_VisitManagedDict/ClearManagedDict.
880+ * If user provides HPy_tp_traverse, their trampoline handles managed dict
881+ * via call_traverseproc_from_trampoline. Otherwise, add hpytype_traverse.
882+ *
883+ * IMPORTANT: Py_TPFLAGS_MANAGED_DICT requires Py_TPFLAGS_HAVE_GC to be set.
884+ * Since managed dict is inherited from object, we must ensure HAVE_GC is set. */
885+ * flags |= Py_TPFLAGS_HAVE_GC ;
886+ if (!has_tp_traverse (hpyspec )) {
887+ result [dst_idx ++ ] = (PyType_Slot ){Py_tp_traverse , (void * )hpytype_traverse };
888+ }
889+ result [dst_idx ++ ] = (PyType_Slot ){Py_tp_clear , (void * )hpytype_clear };
890+ #else
827891 // add a tp_clear, if the user provided a tp_traverse
828892 if (has_tp_traverse (hpyspec )) {
829893 result [dst_idx ++ ] = (PyType_Slot ){Py_tp_clear , (void * )hpytype_clear };
830894 }
895+ #endif
831896
832897 // add the NULL sentinel at the end
833898 result [dst_idx ++ ] = (PyType_Slot ){0 , NULL };
@@ -956,6 +1021,13 @@ static bool has_tp_traverse(HPyType_Spec *hpyspec)
9561021
9571022static bool needs_hpytype_dealloc (HPyType_Spec * hpyspec )
9581023{
1024+ #if HPY_HAS_MANAGED_DICT
1025+ /* On Python 3.13+, we always need hpytype_dealloc because we set
1026+ * Py_TPFLAGS_HAVE_GC for proper managed dict handling. The dealloc
1027+ * must call PyObject_GC_UnTrack and PyObject_ClearManagedDict. */
1028+ (void )hpyspec ; /* unused */
1029+ return true;
1030+ #else
9591031 if (hpyspec -> defines != NULL )
9601032 for (int i = 0 ; hpyspec -> defines [i ] != NULL ; i ++ ) {
9611033 HPyDef * def = hpyspec -> defines [i ];
@@ -964,6 +1036,7 @@ static bool needs_hpytype_dealloc(HPyType_Spec *hpyspec)
9641036 return true;
9651037 }
9661038 return false;
1039+ #endif
9671040}
9681041
9691042static int check_have_gc_and_tp_traverse (HPyContext * ctx , HPyType_Spec * hpyspec )
@@ -1585,6 +1658,26 @@ _HPy_HIDDEN int call_traverseproc_from_trampoline(HPyFunc_traverseproc tp_traver
15851658 cpy_visitproc cpy_visit ,
15861659 void * cpy_arg )
15871660{
1661+ /* Visit the type object (required for heap types on Python 3.9+) */
1662+ #if PY_VERSION_HEX >= 0x03090000
1663+ {
1664+ PyObject * _py_type = (PyObject * )Py_TYPE (self );
1665+ int res = cpy_visit (_py_type , cpy_arg );
1666+ if (res )
1667+ return res ;
1668+ }
1669+ #endif
1670+
1671+ #if HPY_HAS_MANAGED_DICT
1672+ /* On Python 3.13+, we must visit the managed dict */
1673+ {
1674+ int res = PyObject_VisitManagedDict (self , cpy_visit , cpy_arg );
1675+ if (res )
1676+ return res ;
1677+ }
1678+ #endif
1679+
1680+ /* Now call the user's traverse implementation */
15881681 hpy2cpy_visit_args_t args = { cpy_visit , cpy_arg };
15891682 return tp_traverse (_pyobj_as_struct (self ), hpy2cpy_visit , & args );
15901683}
0 commit comments