Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.dict/cpython.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fielddesc
fieldlist
fileutils
finalbody
finalizers
flowgraph
formatfloat
freevar
Expand Down
22 changes: 21 additions & 1 deletion crates/vm/src/gc_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,22 @@ impl GcState {

/// Check if automatic GC should run and run it if needed.
/// Called after object allocation.
/// Currently a stub — returns false.
/// Returns true if GC was run, false otherwise.
pub fn maybe_collect(&self) -> bool {
if !self.is_enabled() {
return false;
}

// _PyObject_GC_Alloc checks thresholds

// Check gen0 threshold
let count0 = self.generations[0].count.load(Ordering::SeqCst) as u32;
let threshold0 = self.generations[0].threshold();
if threshold0 > 0 && count0 >= threshold0 {
self.collect(0);
return true;
}

false
}

Expand All @@ -377,12 +391,18 @@ impl GcState {
/// Currently a stub — the actual collection algorithm requires EBR
/// and will be added in a follow-up.
pub fn collect(&self, _generation: usize) -> (usize, usize) {
// gc_collect_main
// Reset gen0 count even though we're not actually collecting
self.generations[0].count.store(0, Ordering::SeqCst);
(0, 0)
}

/// Force collection even if GC is disabled (for manual gc.collect() calls).
/// gc.collect() always runs regardless of gc.isenabled()
/// Currently a stub.
pub fn collect_force(&self, _generation: usize) -> (usize, usize) {
// Reset gen0 count even though we're not actually collecting
self.generations[0].count.store(0, Ordering::SeqCst);
(0, 0)
}

Expand Down
218 changes: 183 additions & 35 deletions crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ use core::{
pub(super) struct Erased;

/// Default dealloc: handles __del__, weakref clearing, tp_clear, and memory free.
/// Equivalent to subtype_dealloc in CPython.
/// Equivalent to subtype_dealloc.
pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
let obj_ref = unsafe { &*(obj as *const PyObject) };
if let Err(()) = obj_ref.drop_slow_inner() {
Expand Down Expand Up @@ -383,58 +383,101 @@ impl WeakRefList {
weak
}

/// PyObject_ClearWeakRefs: clear all weakrefs when the referent dies.
/// Clear all weakrefs and call their callbacks.
/// Called when the owner object is being dropped.
// PyObject_ClearWeakRefs
fn clear(&self, obj: &PyObject) {
let obj_addr = obj as *const PyObject as usize;
let mut to_callback: Vec<(PyRef<PyWeak>, PyObjectRef)> = Vec::new();
let _lock = weakref_lock::lock(obj_addr);

{
let _lock = weakref_lock::lock(obj_addr);
// Clear generic cache
self.generic.store(ptr::null_mut(), Ordering::Relaxed);

// Walk the list, collecting weakrefs with callbacks
let mut current = NonNull::new(self.head.load(Ordering::Relaxed));
while let Some(node) = current {
let next = unsafe { WeakLink::pointers(node).as_ref().get_next() };
// Walk the list, collecting weakrefs with callbacks
let mut callbacks: Vec<(PyRef<PyWeak>, PyObjectRef)> = Vec::new();
let mut current = NonNull::new(self.head.load(Ordering::Relaxed));
while let Some(node) = current {
let next = unsafe { WeakLink::pointers(node).as_ref().get_next() };

let wr = unsafe { node.as_ref() };
let wr = unsafe { node.as_ref() };

// Set wr_object to null (marks weakref as dead)
wr.0.payload
.wr_object
.store(ptr::null_mut(), Ordering::Relaxed);
// Mark weakref as dead
wr.0.payload
.wr_object
.store(ptr::null_mut(), Ordering::Relaxed);

// Unlink from list
unsafe {
let mut ptrs = WeakLink::pointers(node);
ptrs.as_mut().set_prev(None);
ptrs.as_mut().set_next(None);
}
// Unlink from list
unsafe {
let mut ptrs = WeakLink::pointers(node);
ptrs.as_mut().set_prev(None);
ptrs.as_mut().set_next(None);
}

// Collect callback if weakref is still alive (strong_count > 0)
if wr.0.ref_count.get() > 0 {
let cb = unsafe { wr.0.payload.callback.get().replace(None) };
if let Some(cb) = cb {
to_callback.push((wr.to_owned(), cb));
}
// Collect callback if present and weakref is still alive
if wr.0.ref_count.get() > 0 {
let cb = unsafe { wr.0.payload.callback.get().replace(None) };
if let Some(cb) = cb {
callbacks.push((wr.to_owned(), cb));
}

current = next;
}

self.head.store(ptr::null_mut(), Ordering::Relaxed);
self.generic.store(ptr::null_mut(), Ordering::Relaxed);
current = next;
}
self.head.store(ptr::null_mut(), Ordering::Relaxed);

// Call callbacks without holding the lock
for (wr, cb) in to_callback {
// Invoke callbacks outside the lock
drop(_lock);
for (wr, cb) in callbacks {
crate::vm::thread::with_vm(&cb, |vm| {
// TODO: handle unraisable exception
let wr_obj: PyObjectRef = wr.clone().into();
let _ = cb.call((wr_obj,), vm);
let _ = cb.call((wr.clone(),), vm);
});
}
}

/// Clear all weakrefs but DON'T call callbacks. Instead, return them for later invocation.
/// Used by GC to ensure ALL weakrefs are cleared BEFORE any callbacks are invoked.
/// handle_weakrefs() clears all weakrefs first, then invokes callbacks.
fn clear_for_gc_collect_callbacks(&self, obj: &PyObject) -> Vec<(PyRef<PyWeak>, PyObjectRef)> {
let obj_addr = obj as *const PyObject as usize;
let _lock = weakref_lock::lock(obj_addr);

// Clear generic cache
self.generic.store(ptr::null_mut(), Ordering::Relaxed);

let mut callbacks = Vec::new();
let mut current = NonNull::new(self.head.load(Ordering::Relaxed));
while let Some(node) = current {
let next = unsafe { WeakLink::pointers(node).as_ref().get_next() };

let wr = unsafe { node.as_ref() };

// Mark weakref as dead
wr.0.payload
.wr_object
.store(ptr::null_mut(), Ordering::Relaxed);

// Unlink from list
unsafe {
let mut ptrs = WeakLink::pointers(node);
ptrs.as_mut().set_prev(None);
ptrs.as_mut().set_next(None);
}

// Collect callback without invoking
if wr.0.ref_count.get() > 0 {
let cb = unsafe { wr.0.payload.callback.get().replace(None) };
if let Some(cb) = cb {
callbacks.push((wr.to_owned(), cb));
}
}

current = next;
}
self.head.store(ptr::null_mut(), Ordering::Relaxed);

callbacks
}

fn count(&self, obj: &PyObject) -> usize {
let _lock = weakref_lock::lock(obj as *const PyObject as usize);
let mut count = 0usize;
Expand Down Expand Up @@ -1044,13 +1087,20 @@ impl PyObject {
}

// __del__ should only be called once (like _PyGC_FINALIZED check in GIL_DISABLED)
// We call __del__ BEFORE clearing weakrefs to allow the finalizer to access
// the object's weak references if needed.
let del = self.class().slots.del.load();
if let Some(slot_del) = del
&& !self.gc_finalized()
{
self.set_gc_finalized();
call_slot_del(self, slot_del)?;
}

// Clear weak refs AFTER __del__.
// Note: This differs from GC behavior which clears weakrefs before finalizers,
// but for direct deallocation (drop_slow_inner), we need to allow the finalizer
// to run without triggering use-after-free from WeakRefList operations.
if let Some(wrl) = self.weak_ref_list() {
wrl.clear(self);
}
Expand Down Expand Up @@ -1097,6 +1147,104 @@ impl PyObject {
});
result
}

/// Call __del__ if present, without triggering object deallocation.
/// Used by GC to call finalizers before breaking cycles.
/// This allows proper resurrection detection.
/// CPython: PyObject_CallFinalizerFromDealloc in Objects/object.c
pub fn try_call_finalizer(&self) {
let del = self.class().slots.del.load();
if let Some(slot_del) = del
&& !self.gc_finalized()
{
// Mark as finalized BEFORE calling __del__ to prevent double-call
// This ensures drop_slow_inner() won't call __del__ again
self.set_gc_finalized();
let result = crate::vm::thread::with_vm(self, |vm| {
if let Err(e) = slot_del(self, vm)
&& let Some(del_method) = self.get_class_attr(identifier!(vm, __del__))
{
vm.run_unraisable(e, None, del_method);
}
});
let _ = result;
}
}

/// Clear weakrefs but collect callbacks instead of calling them.
/// This is used by GC to ensure ALL weakrefs are cleared BEFORE any callbacks run.
/// Returns collected callbacks as (PyRef<PyWeak>, callback) pairs.
// = handle_weakrefs
pub fn gc_clear_weakrefs_collect_callbacks(&self) -> Vec<(PyRef<PyWeak>, PyObjectRef)> {
if let Some(wrl) = self.weak_ref_list() {
wrl.clear_for_gc_collect_callbacks(self)
} else {
vec![]
}
}

/// Get raw pointers to referents without incrementing reference counts.
/// This is used during GC to avoid reference count manipulation.
/// tp_traverse visits objects without incref
///
/// # Safety
/// The returned pointers are only valid as long as the object is alive
/// and its contents haven't been modified.
pub unsafe fn gc_get_referent_ptrs(&self) -> Vec<NonNull<PyObject>> {
let mut result = Vec::new();
// Traverse the entire object including dict and slots
self.0.traverse(&mut |child: &PyObject| {
result.push(NonNull::from(child));
});
result
}

/// Pop edges from this object for cycle breaking.
/// Returns extracted child references that were removed from this object (tp_clear).
/// This is used during garbage collection to break circular references.
///
/// # Safety
/// - ptr must be a valid pointer to a PyObject
/// - The caller must have exclusive access (no other references exist)
/// - This is only safe during GC when the object is unreachable
pub unsafe fn gc_clear_raw(ptr: *mut PyObject) -> Vec<PyObjectRef> {
let mut result = Vec::new();
let obj = unsafe { &*ptr };

// 1. Clear payload-specific references (vtable.clear / tp_clear)
if let Some(clear_fn) = obj.0.vtable.clear {
unsafe { clear_fn(ptr, &mut result) };
}

// 2. Clear member slots (subtype_clear)
for slot in obj.0.slots.iter() {
if let Some(val) = slot.write().take() {
result.push(val);
}
}

result
}

/// Clear this object for cycle breaking (tp_clear).
/// This version takes &self but should only be called during GC
/// when exclusive access is guaranteed.
///
/// # Safety
/// - The caller must guarantee exclusive access (no other references exist)
/// - This is only safe during GC when the object is unreachable
pub unsafe fn gc_clear(&self) -> Vec<PyObjectRef> {
// SAFETY: During GC collection, this object is unreachable (gc_refs == 0),
// meaning no other code has a reference to it. The only references are
// internal cycle references which we're about to break.
unsafe { Self::gc_clear_raw(self as *const _ as *mut PyObject) }
}

/// Check if this object has clear capability (tp_clear)
// Py_TPFLAGS_HAVE_GC types have tp_clear
pub fn gc_has_clear(&self) -> bool {
self.0.vtable.clear.is_some() || self.0.dict.is_some() || !self.0.slots.is_empty()
}
Comment on lines +1210 to +1247
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if dict is traversed (would be counted in GC) but not cleared
# Look for Traverse implementation on PyInner or dict handling

echo "=== Checking Traverse implementation for dict ==="
rg -n "impl.*Traverse.*for.*PyInner" crates/vm/src/ -A 20

echo ""
echo "=== Checking if InstanceDict implements clear ==="
rg -n "InstanceDict" crates/vm/src/object/ -A 5 -B 2

echo ""
echo "=== Checking CPython-style subtype_clear patterns ==="
rg -n "subtype_clear|tp_clear.*dict" crates/vm/src/

Repository: RustPython/RustPython

Length of output: 7211


🏁 Script executed:

#!/bin/bash
# Need to check if PyDict type itself has GC clearing
echo "=== Check PyDict type and its clear/tp_clear implementation ==="
rg -n "PyDict|tp_clear" crates/vm/src/builtins/dict.rs -A 3 -B 1 | head -100

echo ""
echo "=== Search for any dict.clear in gc_clear context ==="
rg -n "gc_clear" crates/vm/src/object/core.rs -B 2 -A 10

echo ""
echo "=== Check if dict entries are cleared as part of PyDict's vtable.clear ==="
rg -n "vtable.*clear" crates/vm/src/ -A 5 | grep -A 5 "dict"

echo ""
echo "=== Check try_clear_obj function to see how clearing works ==="
rg -n "try_clear_obj" crates/vm/src/ -A 15

Repository: RustPython/RustPython

Length of output: 9356


🏁 Script executed:

#!/bin/bash
# Check if PyDict implements try_clear or MaybeClear
echo "=== Search for try_clear impl for PyDict ==="
rg -n "impl.*try_clear.*PyDict|impl.*MaybeClear.*PyDict" crates/vm/src/ -A 10

echo ""
echo "=== Check MaybeClear trait definition ==="
rg -n "trait MaybeClear|fn try_clear" crates/vm/src/object/core.rs -A 8

echo ""
echo "=== Verify: does PyDict instance have GC clearing? Search for PyPayload impl ==="
rg -n "impl PyPayload for PyDict" crates/vm/src/ -A 20

echo ""
echo "=== Check what happens when an instance dict field exists ==="
rg -n "instance.*dict|dict.*instance" crates/vm/src/object/core.rs -B 2 -A 2 | head -60

Repository: RustPython/RustPython

Length of output: 3017


🏁 Script executed:

#!/bin/bash
# Check default try_clear implementation
echo "=== Search for default try_clear implementation ==="
rg -n "fn try_clear|impl.*try_clear" crates/vm/src/object/core.rs -B 2 -A 5

echo ""
echo "=== Search for PyPayload trait try_clear method ==="
rg -n "trait PyPayload" crates/vm/src/object/payload.rs -A 30

echo ""
echo "=== Verify dict is not cleared by looking at gc_clear_raw in detail ==="
sed -n '1210,1230p' crates/vm/src/object/core.rs

echo ""
echo "=== Check if there's any dict clearing elsewhere in gc flow ==="
rg -n "dict.*clear\|clear.*dict" crates/vm/src/gc.rs

Repository: RustPython/RustPython

Length of output: 2410


🏁 Script executed:

#!/bin/bash
# Find try_clear trait definition more broadly
echo "=== Search for try_clear trait definition ==="
rg -n "try_clear" crates/vm/src/object/ -B 3 -A 3 | head -80

echo ""
echo "=== Find GC collection code ==="
fd "*.rs" crates/vm/src/ | xargs grep -l "gc_clear\|GC\|garbage" | head -10

echo ""
echo "=== Search for where gc_clear_raw is called ==="
rg -n "gc_clear_raw\|gc_clear\(" crates/vm/src/ -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 2770


🏁 Script executed:

#!/bin/bash
# Search for where gc_clear_raw is called with proper escaping
echo "=== Search for where gc_clear_raw is called ==="
rg "gc_clear_raw|gc_clear" crates/vm/src/ -B 1 -A 1

echo ""
echo "=== Check HAS_CLEAR definition ==="
rg "HAS_CLEAR" crates/vm/src/object/ -B 3 -A 3

echo ""
echo "=== Check if MaybeClear trait has HAS_CLEAR ==="
rg "trait MaybeClear|HAS_CLEAR" crates/vm/src/object/ -B 2 -A 5

Repository: RustPython/RustPython

Length of output: 3657


🏁 Script executed:

#!/bin/bash
# Search more specifically for where gc_clear is called in the codebase
echo "=== Search for calls to gc_clear (not definitions) ==="
rg "\.gc_clear\(|gc_clear_raw\(" crates/vm/src/ -B 2 -A 2

echo ""
echo "=== Search in all files for gc_clear usage ==="
git ls-files crates/vm/src/ | xargs grep -l "gc_clear" | head -20

echo ""
echo "=== Check the actual file that calls gc_clear ==="
git ls-files crates/vm/src/ | xargs grep "\.gc_clear\(" -B 3 -A 3

Repository: RustPython/RustPython

Length of output: 1099


🏁 Script executed:

#!/bin/bash
# Fix the grep search
echo "=== Search for calls to gc_clear ==="
rg "gc_clear" crates/vm/src/ | grep -v "pub unsafe fn\|pub fn\|Clear this object\|SAFETY"

echo ""
echo "=== Check what files call gc_clear_raw or gc_clear ==="
rg "gc_clear" crates/ --type rust | grep -v "pub unsafe\|pub fn\|//\|Clear this"

echo ""
echo "=== Look for GC-related module ==="
fd "gc\|collect" crates/vm/src/ --type f

echo ""
echo "=== Check imports of gc_clear in traverse_object or related files ==="
rg "use.*gc_clear|from.*gc_clear" crates/vm/src/

Repository: RustPython/RustPython

Length of output: 468


🏁 Script executed:

#!/bin/bash
# Search for GC collection, garbage collection, or cycle-related code
echo "=== Search for garbage collection or cycle handling ==="
rg "garbage|collection|cycle|dealloc" crates/vm/src/ --type rust | head -50

echo ""
echo "=== Check if gc_clear is public API that's exported ==="
rg "pub.*gc_clear|pub use.*gc_clear" crates/vm/src/

echo ""
echo "=== Search broader for deallocation and cleanup ==="
rg "dealloc|__del__|finalize|cleanup" crates/vm/src/object/ -B 1 -A 3 | head -60

echo ""
echo "=== Check what gc_has_clear is used for ==="
rg "gc_has_clear" crates/vm/src/ -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 9664


🏁 Script executed:

#!/bin/bash
# Check default_dealloc implementation fully
echo "=== Full default_dealloc implementation ==="
rg "pub\(super\) unsafe fn default_dealloc" crates/vm/src/object/core.rs -A 30

echo ""
echo "=== Check drop_slow_inner implementation ==="
rg "fn drop_slow_inner" crates/vm/src/object/core.rs -A 20

echo ""
echo "=== Check stdlib/gc.rs for actual GC collection logic ==="
head -200 crates/vm/src/stdlib/gc.rs | tail -100

Repository: RustPython/RustPython

Length of output: 5769


🏁 Script executed:

#!/bin/bash
# Now check if gc_has_clear is actually used for GC collection decisions
echo "=== Full gc.rs file to understand GC collection ==="
cat crates/vm/src/stdlib/gc.rs

echo ""
echo "=== Search for gc_has_clear usage ==="
rg "gc_has_clear" crates/ -B 3 -A 3

Repository: RustPython/RustPython

Length of output: 8982


🏁 Script executed:

#!/bin/bash
# Search for all usages of gc_has_clear across the entire codebase
echo "=== Search ALL usages of gc_has_clear ==="
rg "gc_has_clear" crates/ -r '$0' | head -20

echo ""
echo "=== Check gc_state module for actual collection logic ==="
fd "gc_state" crates/ --type f

echo ""
echo "=== Now let's verify: does dict actually participate in cycle detection? ==="
echo "Looking at how dict entries are part of GC traversal..."
rg "impl.*Traverse.*for.*InstanceDict|impl.*Traverse.*for.*PyDict" crates/vm/src/ -A 5

Repository: RustPython/RustPython

Length of output: 1163


🏁 Script executed:

#!/bin/bash
# Get the complete clear implementation for PyDict
echo "=== Full clear() implementation for PyDict ==="
rg "impl Traverse for PyDict" crates/vm/src/builtins/dict.rs -A 20

echo ""
echo "=== Verify: does PyDict set HAS_CLEAR? ==="
rg "impl.*PyPayload.*for.*PyDict" crates/vm/src/builtins/dict.rs -A 10

echo ""
echo "=== Check: MaybeTraverse implementation for PyDict ==="
rg "impl.*MaybeTraverse.*for.*PyDict|HAS_TRAVERSE.*PyDict\|HAS_CLEAR.*PyDict" crates/vm/src/ -B 5 -A 10

Repository: RustPython/RustPython

Length of output: 1108


🏁 Script executed:

#!/bin/bash
# Check if PyDict implements MaybeTraverse
echo "=== Check if PyDict has MaybeTraverse impl ==="
rg "impl.*MaybeTraverse.*for.*PyDict|impl.*Traverse.*for.*PyDict" crates/vm/src/builtins/dict.rs -B 2 -A 15

echo ""
echo "=== Check if Traverse impl for PyDict means HAS_CLEAR is true ==="
echo "Looking at how Traverse.clear maps to HAS_CLEAR..."
rg "Traverse.*clear\|clear.*Traverse" crates/vm/src/object/ -B 3 -A 3

echo ""
echo "=== Now the key question: when an object has dict field, is its clear() called? ==="
echo "Look at gc_clear_raw again and how it calls vtable.clear"
sed -n '1210,1230p' crates/vm/src/object/core.rs

echo ""
echo "=== Check if dict.clear() is invoked somewhere else ==="
rg "dict.*clear\(\)|\.clear\(\).*dict" crates/vm/src/object/ -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 1658


🏁 Script executed:

#!/bin/bash
# Let me trace through the logic one more time to be absolutely sure
echo "=== Confirm: instance dict field is NOT cleared in gc_clear_raw ==="
sed -n '1210,1230p' crates/vm/src/object/core.rs

echo ""
echo "=== Instance dict is a field in PyInner struct ==="
sed -n '160,170p' crates/vm/src/object/core.rs

echo ""
echo "=== InstanceDict has a clear capability? Check Traverse impl for InstanceDict ==="
rg "impl.*Traverse.*for.*InstanceDict" crates/vm/src/ -A 10

echo ""
echo "=== The KEY test: if an object instance has dict, should dict entries be cleared? ==="
echo "Let's check how the slots are cleared to understand the pattern..."
sed -n '1219,1230p' crates/vm/src/object/core.rs

echo ""
echo "=== Compare with default_dealloc to see if it clears dict ==="
sed -n '1140,1165p' crates/vm/src/object/core.rs

Repository: RustPython/RustPython

Length of output: 4000


🏁 Script executed:

#!/bin/bash
# Final verification: check InstanceDict methods and implementation
echo "=== Full InstanceDict implementation ==="
sed -n '643,700p' crates/vm/src/object/core.rs

echo ""
echo "=== Check if InstanceDict has any clear-like method ==="
rg "impl.*InstanceDict" crates/vm/src/object/core.rs -A 30

echo ""
echo "=== Check PyDict drain_entries method ==="
rg "drain_entries" crates/vm/src/builtins/dict.rs -B 3 -A 10

echo ""
echo "=== Final confirmation: slots.iter() in gc_clear_raw ==="
echo "Slots are cleared by iterating and taking ownership. Dict is NOT."
echo "This confirms the bug: dict field is not cleared while slots are."

Repository: RustPython/RustPython

Length of output: 3358


Instance dict entries are not cleared during GC cycle breaking, but gc_has_clear() indicates they should be.

gc_has_clear() returns true when self.0.dict.is_some() (line 1246), signaling that this object can clear its references. However, gc_clear_raw() only clears:

  1. Payload-specific references via vtable.clear
  2. Member slots (lines 1219–1228)

The instance dict field (self.0.dict) is never cleared. Since dict entries are traversed by GC (traverse_object.rs lines 68, 90), they participate in cycle detection; if they form cycles, those cycles will not be broken during GC collection.

Add dict clearing to gc_clear_raw:

Suggested fix
// 3. Clear instance dict
if let Some(ref instance_dict) = obj.0.dict {
    let old_dict = instance_dict.replace(vm.ctx.new_dict());
    result.push(old_dict.into());
}

Or remove dict.is_some() from gc_has_clear() if dict clearing is intentionally deferred.

🤖 Prompt for AI Agents
In `@crates/vm/src/object/core.rs` around lines 1210 - 1247, gc_clear_raw
currently clears vtable-held refs and slots but never clears the instance dict
even though gc_has_clear() returns true when self.0.dict.is_some(); add a
dict-clearing step in gc_clear_raw that takes the dict out of obj.0.dict (e.g.
use take() or replace(None) on obj.0.dict), push the removed dict into the
result Vec as a PyObjectRef, and thereby break cycles involving instance dict
entries; alternatively, if dicts should not be cleared here, remove the
dict.is_some() check from gc_has_clear() instead.

}

impl Borrow<PyObject> for PyObjectRef {
Expand Down
Loading