@@ -81,14 +81,28 @@ use core::{
8181#[ derive( Debug ) ]
8282pub ( super ) struct Erased ;
8383
84- /// Default dealloc: handles __del__, weakref clearing, and memory free.
84+ /// Default dealloc: handles __del__, weakref clearing, tp_clear, and memory free.
8585/// Equivalent to subtype_dealloc in CPython.
8686pub ( super ) unsafe fn default_dealloc < T : PyPayload > ( obj : * mut PyObject ) {
8787 let obj_ref = unsafe { & * ( obj as * const PyObject ) } ;
8888 if let Err ( ( ) ) = obj_ref. drop_slow_inner ( ) {
8989 return ; // resurrected by __del__
9090 }
91+
92+ // Extract child references before deallocation to break circular refs (tp_clear).
93+ // This ensures that when edges are dropped after the object is freed,
94+ // any pointers back to this object are already gone.
95+ let mut edges = Vec :: new ( ) ;
96+ if let Some ( clear_fn) = obj_ref. 0 . vtable . clear {
97+ unsafe { clear_fn ( obj, & mut edges) } ;
98+ }
99+
100+ // Deallocate the object memory
91101 drop ( unsafe { Box :: from_raw ( obj as * mut PyInner < T > ) } ) ;
102+
103+ // Drop child references - may trigger recursive destruction.
104+ // The object is already deallocated, so circular refs are broken.
105+ drop ( edges) ;
92106}
93107pub ( super ) unsafe fn debug_obj < T : PyPayload + core:: fmt:: Debug > (
94108 x : & PyObject ,
@@ -105,6 +119,12 @@ pub(super) unsafe fn try_traverse_obj<T: PyPayload>(x: &PyObject, tracer_fn: &mu
105119 payload. try_traverse ( tracer_fn)
106120}
107121
122+ /// Call `try_clear` on payload to extract child references (tp_clear)
123+ pub ( super ) unsafe fn try_clear_obj < T : PyPayload > ( x : * mut PyObject , out : & mut Vec < PyObjectRef > ) {
124+ let x = unsafe { & mut * ( x as * mut PyInner < T > ) } ;
125+ x. payload . try_clear ( out) ;
126+ }
127+
108128bitflags:: bitflags! {
109129 /// GC bits for free-threading support (like ob_gc_bits in Py_GIL_DISABLED)
110130 /// These bits are stored in a separate atomic field for lock-free access.
@@ -963,10 +983,27 @@ impl PyObject {
963983 /// _PyGC_SET_FINALIZED in Py_GIL_DISABLED mode.
964984 #[ inline]
965985 fn set_gc_finalized ( & self ) {
966- // Atomic RMW to avoid clobbering other concurrent bit updates.
986+ self . set_gc_bit ( GcBits :: FINALIZED ) ;
987+ }
988+
989+ /// Set a GC bit atomically.
990+ #[ inline]
991+ pub ( crate ) fn set_gc_bit ( & self , bit : GcBits ) {
992+ self . 0 . gc_bits . fetch_or ( bit. bits ( ) , Ordering :: Relaxed ) ;
993+ }
994+
995+ /// _PyObject_GC_TRACK
996+ #[ inline]
997+ pub ( crate ) fn set_gc_tracked ( & self ) {
998+ self . set_gc_bit ( GcBits :: TRACKED ) ;
999+ }
1000+
1001+ /// _PyObject_GC_UNTRACK
1002+ #[ inline]
1003+ pub ( crate ) fn clear_gc_tracked ( & self ) {
9671004 self . 0
9681005 . gc_bits
969- . fetch_or ( GcBits :: FINALIZED . bits ( ) , Ordering :: Relaxed ) ;
1006+ . fetch_and ( ! GcBits :: TRACKED . bits ( ) , Ordering :: Relaxed ) ;
9701007 }
9711008
9721009 #[ inline( always) ] // the outer function is never inlined
@@ -1046,13 +1083,9 @@ impl PyObject {
10461083 * self . 0 . slots [ offset] . write ( ) = value;
10471084 }
10481085
1049- /// Check if this object is tracked by the garbage collector.
1050- /// Returns true if the object has a trace function or has an instance dict.
1086+ /// _PyObject_GC_IS_TRACKED
10511087 pub fn is_gc_tracked ( & self ) -> bool {
1052- if self . 0 . vtable . trace . is_some ( ) {
1053- return true ;
1054- }
1055- self . 0 . dict . is_some ( )
1088+ GcBits :: from_bits_retain ( self . 0 . gc_bits . load ( Ordering :: Relaxed ) ) . contains ( GcBits :: TRACKED )
10561089 }
10571090
10581091 /// Get the referents (objects directly referenced) of this object.
@@ -1277,13 +1310,28 @@ impl<T: PyPayload> PyRef<T> {
12771310 }
12781311}
12791312
1280- impl < T : PyPayload + core:: fmt:: Debug > PyRef < T > {
1313+ impl < T : PyPayload + crate :: object :: MaybeTraverse + core:: fmt:: Debug > PyRef < T > {
12811314 #[ inline( always) ]
12821315 pub fn new_ref ( payload : T , typ : crate :: builtins:: PyTypeRef , dict : Option < PyDictRef > ) -> Self {
1316+ let has_dict = dict. is_some ( ) ;
1317+ let is_heaptype = typ. heaptype_ext . is_some ( ) ;
12831318 let inner = Box :: into_raw ( PyInner :: new ( payload, typ, dict) ) ;
1284- Self {
1285- ptr : unsafe { NonNull :: new_unchecked ( inner. cast :: < Py < T > > ( ) ) } ,
1319+ let ptr = unsafe { NonNull :: new_unchecked ( inner. cast :: < Py < T > > ( ) ) } ;
1320+
1321+ // Track object if:
1322+ // - HAS_TRAVERSE is true (Rust payload implements Traverse), OR
1323+ // - has instance dict (user-defined class instances), OR
1324+ // - heap type (all heap type instances are GC-tracked, like Py_TPFLAGS_HAVE_GC)
1325+ if <T as crate :: object:: MaybeTraverse >:: HAS_TRAVERSE || has_dict || is_heaptype {
1326+ let gc = crate :: gc_state:: gc_state ( ) ;
1327+ unsafe {
1328+ gc. track_object ( ptr. cast ( ) ) ;
1329+ }
1330+ // Check if automatic GC should run
1331+ gc. maybe_collect ( ) ;
12861332 }
1333+
1334+ Self { ptr }
12871335 }
12881336}
12891337
@@ -1546,6 +1594,12 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
15461594 heaptype_ext : None ,
15471595 } ;
15481596 let weakref_type = PyRef :: new_ref ( weakref_type, type_type. clone ( ) , None ) ;
1597+ // Static type: untrack from GC (was tracked by new_ref because PyType has HAS_TRAVERSE)
1598+ unsafe {
1599+ crate :: gc_state:: gc_state ( )
1600+ . untrack_object ( core:: ptr:: NonNull :: from ( weakref_type. as_object ( ) ) ) ;
1601+ }
1602+ weakref_type. as_object ( ) . clear_gc_tracked ( ) ;
15491603 // weakref's mro is [weakref, object]
15501604 weakref_type. mro . write ( ) . insert ( 0 , weakref_type. clone ( ) ) ;
15511605
0 commit comments