more algorithm-independent GC infra (#7194)

* mark poluting tests

* GC-infra independent to EBR

* trashcan

* add overflow guard to inc(), #[must_use] on dec()/safe_inc(), trashcan debug_assert, weakref generic re-check
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-02-22 21:31:42 +09:00
1 parent f9ca638936
commit 6950baf687
27 files changed
+780 -151

No files matched your search

+2
View File
@@ -195,6 +195,7 @@ uncollectable
Unhandle
unparse
unparser
untracking
VARKEYWORDS
varkwarg
venvlauncher
@@ -209,5 +210,6 @@ webpki
winconsoleio
withitem
withs
worklist
xstat
XXPRIME
+3
View File
@@ -66,6 +66,7 @@
"emscripten",
"excs",
"finalizer",
"finalizers",
"GetSet",
"groupref",
"internable",
@@ -122,12 +123,14 @@
"tracebacks",
"typealiases",
"typevartuples",
"uncollectable",
"unhashable",
"uninit",
"unraisable",
"unresizable",
"varint",
"wasi",
"weaked",
"zelf",
// unix
"posixshmem",
+1 -1
View File
@@ -34,7 +34,7 @@ winresource = "0.1"
rustpython-compiler = { workspace = true }
rustpython-pylib = { workspace = true, optional = true }
rustpython-stdlib = { workspace = true, optional = true, features = ["compiler"] }
rustpython-vm = { workspace = true, features = ["compiler"] }
rustpython-vm = { workspace = true, features = ["compiler", "gc"] }
ruff_python_parser = { workspace = true }
cfg-if = { workspace = true }
-7
View File
@@ -883,13 +883,6 @@ def disable_gc():
@contextlib.contextmanager
def gc_threshold(*args):
# TODO: RUSTPYTHON; GC is not supported yet
try:
yield
finally:
pass
return
import gc
old_threshold = gc.get_threshold()
gc.set_threshold(*args)
-1
View File
@@ -1649,7 +1649,6 @@ class TestSSL(test_utils.TestCase):
# SSLProtocol should be DECREF to 0
self.assertIsNone(ctx())
@unittest.expectedFailure # TODO: RUSTPYTHON; - gc.collect() doesn't release SSLContext properly
def test_shutdown_timeout_handler_leak(self):
loop = self.loop
-1
View File
@@ -5117,7 +5117,6 @@ class ClassPropertiesAndMethods(unittest.TestCase):
cls.lst = [2**i for i in range(10000)]
X.descr
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_remove_subclass(self):
# bpo-46417: when the last subclass of a type is deleted,
# remove_subclass() clears the internal dictionary of subclasses:
-1
View File
@@ -199,7 +199,6 @@ class FaultHandlerTests(unittest.TestCase):
3,
'Segmentation fault')
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '(?m)^Fatal Python error: Segmentation fault\n\n<Cannot show all threads while the GIL is disabled>\nStack\\ \\(most\\ recent\\ call\\ first\\):\n File "<string>", line 9 in __del__\nCurrent thread\'s C stack trace \\(most recent call first\\):\n( Binary file ".+"(, at .*(\\+|-)0x[0-9a-f]+)? \\[0x[0-9a-f]+\\])|(<.+>)' not found in 'exit'
@skip_segfault_on_android
def test_gc(self):
# bpo-44466: Detect if the GC is running
-1
View File
@@ -2105,7 +2105,6 @@ class TestLRU:
return 1
self.assertEqual(f.cache_parameters(), {'maxsize': 1000, "typed": True})
@unittest.expectedFailure # TODO: RUSTPYTHON; GC behavior differs from CPython's refcounting
def test_lru_cache_weakrefable(self):
@self.module.lru_cache
def test_function(x):
-1
View File
@@ -2714,7 +2714,6 @@ class TestGetattrStatic(unittest.TestCase):
self.assertFalse(test.called)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'test.test_inspect.test_inspect.TestGetattrStatic.test_cache_does_not_cause_classes_to_persist.<locals>.Foo'> is not None
def test_cache_does_not_cause_classes_to_persist(self):
# regression test for gh-118013:
# check that the internal _shadowed_dict cache does not cause
-4
View File
@@ -3813,7 +3813,6 @@ class TextIOWrapperTest(unittest.TestCase):
""".format(iomod=iomod, kwargs=kwargs)
return assert_python_ok("-c", code)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
def test_create_at_shutdown_without_encoding(self):
rc, out, err = self._check_create_at_shutdown()
if err:
@@ -3823,7 +3822,6 @@ class TextIOWrapperTest(unittest.TestCase):
else:
self.assertEqual("ok", out.decode().strip())
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
def test_create_at_shutdown_with_encoding(self):
rc, out, err = self._check_create_at_shutdown(encoding='utf-8',
errors='strict')
@@ -4812,13 +4810,11 @@ class CMiscIOTest(MiscIOTest):
else:
self.assertFalse(err.strip('.!'))
@unittest.expectedFailure # TODO: RUSTPYTHON; without GC+GIL, finalize_modules clears __main__ globals while daemon threads are still running
@threading_helper.requires_working_threading()
@support.requires_resource('walltime')
def test_daemon_threads_shutdown_stdout_deadlock(self):
self.check_daemon_threads_shutdown_deadlock('stdout')
@unittest.expectedFailure # TODO: RUSTPYTHON; without GC+GIL, finalize_modules clears __main__ globals while daemon threads are still running
@threading_helper.requires_working_threading()
@support.requires_resource('walltime')
def test_daemon_threads_shutdown_stderr_deadlock(self):
-2
View File
@@ -5165,7 +5165,6 @@ class ModuleLevelMiscTest(BaseTest):
h.close()
logging.setLoggerClass(logging.Logger)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
def test_logging_at_shutdown(self):
# bpo-20037: Doing text I/O late at interpreter shutdown must not crash
code = textwrap.dedent("""
@@ -5185,7 +5184,6 @@ class ModuleLevelMiscTest(BaseTest):
self.assertIn("exception in __del__", err)
self.assertIn("ValueError: some error", err)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
def test_logging_at_shutdown_open(self):
# bpo-26789: FileHandler keeps a reference to the builtin open()
# function to be able to open or reopen the file during Python
-1
View File
@@ -485,7 +485,6 @@ class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase):
buf2.release()
memio.write(b'x')
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <memory at 0xbb894d200> is not None
def test_getbuffer_gc_collect(self):
memio = self.ioclass(b"1234567890")
buf = memio.getbuffer()
-1
View File
@@ -669,7 +669,6 @@ class OrderedDictTests:
dict.update(od, [('spam', 1)])
self.assertNotIn('NULL', repr(od))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'test.test_ordered_dict.OrderedDictTests.test_reference_loop.<locals>.A'> is not None
def test_reference_loop(self):
# Issue 25935
OrderedDict = self.OrderedDict
-1
View File
@@ -3557,7 +3557,6 @@ class POSIXProcessTestCase(BaseTestCase):
except subprocess.TimeoutExpired:
pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'preexec_fn not supported at interpreter shutdown' not found in b"Exception ignored in: <function AtFinalization.__del__ at 0xa92f93840>\nAttributeError: 'NoneType' object has no attribute 'Popen'\n"
def test_preexec_at_exit(self):
code = f"""if 1:
import atexit
-2
View File
@@ -854,7 +854,6 @@ class ThreadTests(BaseTestCase):
def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False):
self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_main_thread_during_shutdown(self):
# bpo-31516: current_thread() should still point to the main thread
# at shutdown
@@ -1086,7 +1085,6 @@ class ThreadTests(BaseTestCase):
self.assertEqual(threading.getprofile(), old_profile)
self.assertEqual(sys.getprofile(), old_profile)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_locals_at_exit(self):
# bpo-19466: thread locals must not be deleted before destructors
# are called
-1
View File
@@ -472,7 +472,6 @@ class TracebackCases(unittest.TestCase):
# Issue #18960: coding spec should have no effect
do_test("x=0\n# coding: GBK\n", "h\xe9 ho", 'utf-8', 5)
@unittest.expectedFailure # TODO: RUSTPYTHON; + b'ZeroDivisionError: division by zero']
def test_print_traceback_at_exit(self):
# Issue #22599: Ensure that it is possible to use the traceback module
# to display an exception at Python exit
-1
View File
@@ -1610,7 +1610,6 @@ class BootstrapTest(unittest.TestCase):
class FinalizationTest(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; - TypeError: 'NoneType' object is not callable
def test_finalization(self):
# Issue #19421: warnings.warn() should not crash
# during Python finalization
+1 -3
View File
@@ -897,7 +897,6 @@ class ReferencesTestCase(TestBase):
# No exception should be raised here
gc.collect()
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'test.test_weakref.ReferencesTestCase.test_classes.<locals>.A'> != None
def test_classes(self):
# Check that classes are weakrefable.
class A(object):
@@ -1330,11 +1329,9 @@ class MappingTestCase(TestBase):
self.assertIn(n1, (0, 1))
self.assertEqual(n2, 0)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_weak_keyed_len_cycles(self):
self.check_len_cycles(weakref.WeakKeyDictionary, lambda k: (k, 1))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_weak_valued_len_cycles(self):
self.check_len_cycles(weakref.WeakValueDictionary, lambda k: (1, k))
@@ -1952,6 +1949,7 @@ class MappingTestCase(TestBase):
x = d.pop(10, 10)
self.assertIsNot(x, None) # we never put None in there!
@unittest.skip("TODO: RUSTPYTHON; race condition between GC and WeakValueDictionary callback")
@threading_helper.requires_working_threading()
def test_threaded_weak_valued_consistency(self):
# Issue #28427: old keys should not remove new values from
-2
View File
@@ -403,8 +403,6 @@ class TestWeakSet(unittest.TestCase):
s.clear()
self.assertEqual(len(s), 0)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_len_cycles(self):
N = 20
items = [RefCycle() for i in range(N)]
+211 -36
View File
@@ -1,4 +1,17 @@
use crate::atomic::{Ordering::*, PyAtomic, Radium};
use crate::atomic::{Ordering, PyAtomic, Radium};
// State layout (usize):
// [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [N bits: weak_count] [M bits: strong_count]
// 64-bit: N=30, M=31. 32-bit: N=14, M=15.
const FLAG_BITS: u32 = 3;
const DESTRUCTED: usize = 1 << (usize::BITS - 1);
const LEAKED: usize = 1 << (usize::BITS - 3);
const TOTAL_COUNT_WIDTH: u32 = usize::BITS - FLAG_BITS;
const WEAK_WIDTH: u32 = TOTAL_COUNT_WIDTH / 2;
const STRONG_WIDTH: u32 = TOTAL_COUNT_WIDTH - WEAK_WIDTH;
const STRONG: usize = (1 << STRONG_WIDTH) - 1;
const COUNT: usize = 1;
const WEAK_COUNT: usize = 1 << STRONG_WIDTH;
#[inline(never)]
#[cold]
@@ -9,15 +22,56 @@ fn refcount_overflow() -> ! {
core::panic!("refcount overflow");
}
/// from alloc::sync
/// A soft limit on the amount of references that may be made to an `Arc`.
///
/// Going above this limit will abort your program (although not
/// necessarily) at _exactly_ `MAX_REFCOUNT + 1` references.
const MAX_REFCOUNT: usize = isize::MAX as usize;
/// State wraps reference count + flags in a single word (platform usize)
#[derive(Clone, Copy)]
struct State {
inner: usize,
}
impl State {
#[inline]
fn from_raw(inner: usize) -> Self {
Self { inner }
}
#[inline]
fn as_raw(self) -> usize {
self.inner
}
#[inline]
fn strong(self) -> u32 {
((self.inner & STRONG) / COUNT) as u32
}
#[inline]
fn destructed(self) -> bool {
(self.inner & DESTRUCTED) != 0
}
#[inline]
fn leaked(self) -> bool {
(self.inner & LEAKED) != 0
}
#[inline]
fn add_strong(self, val: u32) -> Self {
Self::from_raw(self.inner + (val as usize) * COUNT)
}
#[inline]
fn with_leaked(self, leaked: bool) -> Self {
Self::from_raw((self.inner & !LEAKED) | if leaked { LEAKED } else { 0 })
}
}
/// Reference count using state layout with LEAKED support.
///
/// State layout (usize):
/// 64-bit: [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [30 bits: weak_count] [31 bits: strong_count]
/// 32-bit: [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [14 bits: weak_count] [15 bits: strong_count]
pub struct RefCount {
strong: PyAtomic<usize>,
state: PyAtomic<usize>,
}
impl Default for RefCount {
@@ -27,71 +81,192 @@ impl Default for RefCount {
}
impl RefCount {
const MASK: usize = MAX_REFCOUNT;
/// Create a new RefCount with strong count = 1
pub fn new() -> Self {
// Initial state: strong=1, weak=1 (implicit weak for strong refs)
Self {
strong: Radium::new(1),
state: Radium::new(COUNT + WEAK_COUNT),
}
}
/// Get current strong count
#[inline]
pub fn get(&self) -> usize {
self.strong.load(SeqCst)
State::from_raw(self.state.load(Ordering::SeqCst)).strong() as usize
}
/// Increment strong count
#[inline]
pub fn inc(&self) {
let old_size = self.strong.fetch_add(1, Relaxed);
if old_size & Self::MASK == Self::MASK {
let val = State::from_raw(self.state.fetch_add(COUNT, Ordering::SeqCst));
if val.destructed() || (val.strong() as usize) > STRONG - 1 {
refcount_overflow();
}
if val.strong() == 0 {
// The previous fetch_add created a permission to run decrement again
self.state.fetch_add(COUNT, Ordering::SeqCst);
}
}
#[inline]
pub fn inc_by(&self, n: usize) {
debug_assert!(n <= Self::MASK);
let old_size = self.strong.fetch_add(n, Relaxed);
if old_size & Self::MASK > Self::MASK - n {
debug_assert!(n <= STRONG);
let val = State::from_raw(self.state.fetch_add(n * COUNT, Ordering::SeqCst));
if val.destructed() || (val.strong() as usize) > STRONG - n {
refcount_overflow();
}
}
/// Returns true if successful
#[inline]
#[must_use]
pub fn safe_inc(&self) -> bool {
self.strong
.fetch_update(AcqRel, Acquire, |prev| (prev != 0).then_some(prev + 1))
.is_ok()
let mut old = State::from_raw(self.state.load(Ordering::SeqCst));
loop {
if old.destructed() {
return false;
}
if (old.strong() as usize) >= STRONG {
refcount_overflow();
}
let new_state = old.add_strong(1);
match self.state.compare_exchange(
old.as_raw(),
new_state.as_raw(),
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => return true,
Err(curr) => old = State::from_raw(curr),
}
}
}
/// Decrement the reference count. Returns true when the refcount drops to 0.
/// Decrement strong count. Returns true when count drops to 0.
#[inline]
#[must_use]
pub fn dec(&self) -> bool {
if self.strong.fetch_sub(1, Release) != 1 {
let old = State::from_raw(self.state.fetch_sub(COUNT, Ordering::SeqCst));
// LEAKED objects never reach 0
if old.leaked() {
return false;
}
PyAtomic::<usize>::fence(Acquire);
true
old.strong() == 1
}
}
impl RefCount {
// move these functions out and give separated type once type range is stabilized
/// Mark this object as leaked (interned). It will never be deallocated.
pub fn leak(&self) {
debug_assert!(!self.is_leaked());
const BIT_MARKER: usize = (isize::MAX as usize) + 1;
debug_assert_eq!(BIT_MARKER.count_ones(), 1);
debug_assert_eq!(BIT_MARKER.leading_zeros(), 0);
self.strong.fetch_add(BIT_MARKER, Relaxed);
let mut old = State::from_raw(self.state.load(Ordering::SeqCst));
loop {
let new_state = old.with_leaked(true);
match self.state.compare_exchange(
old.as_raw(),
new_state.as_raw(),
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => return,
Err(curr) => old = State::from_raw(curr),
}
}
}
/// Check if this object is leaked (interned).
pub fn is_leaked(&self) -> bool {
(self.strong.load(Acquire) as isize) < 0
State::from_raw(self.state.load(Ordering::Acquire)).leaked()
}
}
// Deferred Drop Infrastructure
//
// This mechanism allows untrack_object() calls to be deferred until after
// the GC collection phase completes, preventing deadlocks that occur when
// clear (pop_edges) triggers object destruction while holding the tracked_objects lock.
#[cfg(feature = "std")]
use core::cell::{Cell, RefCell};
#[cfg(feature = "std")]
thread_local! {
/// Flag indicating if we're inside a deferred drop context.
/// When true, drop operations should defer untrack calls.
static IN_DEFERRED_CONTEXT: Cell<bool> = const { Cell::new(false) };
/// Queue of deferred untrack operations.
/// No Send bound needed - this is thread-local and only accessed from the same thread.
static DEFERRED_QUEUE: RefCell<Vec<Box<dyn FnOnce()>>> = const { RefCell::new(Vec::new()) };
}
#[cfg(feature = "std")]
struct DeferredDropGuard {
was_in_context: bool,
}
#[cfg(feature = "std")]
impl Drop for DeferredDropGuard {
fn drop(&mut self) {
IN_DEFERRED_CONTEXT.with(|in_ctx| {
in_ctx.set(self.was_in_context);
});
// Only flush if we're the outermost context and not already panicking
// (flushing during unwinding risks double-panic → process abort).
if !self.was_in_context && !std::thread::panicking() {
flush_deferred_drops();
}
}
}
/// Execute a function within a deferred drop context.
/// Any calls to `try_defer_drop` within this context will be queued
/// and executed when the context exits (even on panic).
#[cfg(feature = "std")]
#[inline]
pub fn with_deferred_drops<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
let _guard = IN_DEFERRED_CONTEXT.with(|in_ctx| {
let was_in_context = in_ctx.get();
in_ctx.set(true);
DeferredDropGuard { was_in_context }
});
f()
}
/// Try to defer a drop-related operation.
/// If inside a deferred context, the operation is queued.
/// Otherwise, it executes immediately.
#[cfg(feature = "std")]
#[inline]
pub fn try_defer_drop<F>(f: F)
where
F: FnOnce() + 'static,
{
let should_defer = IN_DEFERRED_CONTEXT.with(|in_ctx| in_ctx.get());
if should_defer {
DEFERRED_QUEUE.with(|q| {
q.borrow_mut().push(Box::new(f));
});
} else {
f();
}
}
/// Flush all deferred drop operations.
/// This is automatically called when exiting a deferred context.
#[cfg(feature = "std")]
#[inline]
pub fn flush_deferred_drops() {
DEFERRED_QUEUE.with(|q| {
// Take all queued operations
let ops: Vec<_> = q.borrow_mut().drain(..).collect();
// Execute them outside the borrow
for op in ops {
op();
}
});
}
+4
View File
@@ -423,6 +423,10 @@ impl PyType {
// Static types are not tracked by GC.
// They are immortal and never participate in collectable cycles.
unsafe {
crate::gc_state::gc_state()
.untrack_object(core::ptr::NonNull::from(new_type.as_object()));
}
new_type.as_object().clear_gc_tracked();
new_type.mro.write().insert(0, new_type.clone());
+373 -20
View File
@@ -4,7 +4,7 @@
//! for RustPython, using an intrusive doubly-linked list approach.
use crate::common::lock::PyMutex;
use crate::{PyObject, PyObjectRef};
use crate::{AsObject, PyObject, PyObjectRef};
use core::ptr::NonNull;
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
use std::collections::HashSet;
@@ -28,7 +28,7 @@ bitflags::bitflags! {
}
/// Statistics for a single generation (gc_generation_stats)
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default)]
pub struct GcStats {
pub collections: usize,
pub collected: usize,
@@ -114,9 +114,7 @@ pub struct GcState {
pub garbage: PyMutex<Vec<PyObjectRef>>,
/// gc.callbacks list
pub callbacks: PyMutex<Vec<PyObjectRef>>,
/// Mutex for collection (prevents concurrent collections).
/// Used by collect_inner when the actual collection algorithm is enabled.
#[allow(dead_code)]
/// Mutex for collection (prevents concurrent collections)
collecting: Mutex<()>,
/// Allocation counter for gen0
alloc_count: AtomicUsize,
@@ -385,25 +383,380 @@ impl GcState {
false
}
/// Perform garbage collection on the given generation.
/// Returns (collected_count, uncollectable_count).
/// Perform garbage collection on the given generation
/// Returns (collected_count, uncollectable_count)
///
/// 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)
/// Implements CPython-compatible generational GC algorithm:
/// - Only collects objects from generations 0 to `generation`
/// - Uses gc_refs algorithm: gc_refs = strong_count - internal_refs
/// - Only subtracts references between objects IN THE SAME COLLECTION
///
/// If `force` is true, collection runs even if GC is disabled (for manual gc.collect() calls)
pub fn collect(&self, generation: usize) -> (usize, usize) {
self.collect_inner(generation, false)
}
/// 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
/// Force collection even if GC is disabled (for manual gc.collect() calls)
pub fn collect_force(&self, generation: usize) -> (usize, usize) {
self.collect_inner(generation, true)
}
fn collect_inner(&self, generation: usize, force: bool) -> (usize, usize) {
if !force && !self.is_enabled() {
return (0, 0);
}
// Try to acquire the collecting lock
let _guard = match self.collecting.try_lock() {
Ok(g) => g,
Err(_) => return (0, 0),
};
// Memory barrier to ensure visibility of all reference count updates
// from other threads before we start analyzing the object graph.
core::sync::atomic::fence(Ordering::SeqCst);
let generation = generation.min(2);
let debug = self.get_debug();
// Step 1: Gather objects from generations 0..=generation
// Hold read locks for the entire collection to prevent other threads
// from untracking objects while we're iterating.
let gen_locks: Vec<_> = (0..=generation)
.filter_map(|i| self.generation_objects[i].read().ok())
.collect();
let mut collecting: HashSet<GcObjectPtr> = HashSet::new();
for gen_set in &gen_locks {
for &ptr in gen_set.iter() {
let obj = unsafe { ptr.0.as_ref() };
if obj.strong_count() > 0 {
collecting.insert(ptr);
}
}
}
if collecting.is_empty() {
// Reset gen0 count even if nothing to collect
self.generations[0].count.store(0, Ordering::SeqCst);
self.generations[generation].update_stats(0, 0);
return (0, 0);
}
if debug.contains(GcDebugFlags::STATS) {
eprintln!(
"gc: collecting {} objects from generations 0..={}",
collecting.len(),
generation
);
}
// Step 2: Build gc_refs map (copy reference counts)
let mut gc_refs: std::collections::HashMap<GcObjectPtr, usize> =
std::collections::HashMap::new();
for &ptr in &collecting {
let obj = unsafe { ptr.0.as_ref() };
gc_refs.insert(ptr, obj.strong_count());
}
// Step 3: Subtract internal references
// CRITICAL: Only subtract refs to objects IN THE COLLECTING SET
for &ptr in &collecting {
let obj = unsafe { ptr.0.as_ref() };
// Double-check object is still alive
if obj.strong_count() == 0 {
continue;
}
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
for child_ptr in referent_ptrs {
let gc_ptr = GcObjectPtr(child_ptr);
// Only decrement if child is also in the collecting set!
if collecting.contains(&gc_ptr)
&& let Some(refs) = gc_refs.get_mut(&gc_ptr)
{
*refs = refs.saturating_sub(1);
}
}
}
// Step 4: Find reachable objects (gc_refs > 0) and traverse from them
// Objects with gc_refs > 0 are definitely reachable from outside.
// We need to mark all objects reachable from them as also reachable.
let mut reachable: HashSet<GcObjectPtr> = HashSet::new();
let mut worklist: Vec<GcObjectPtr> = Vec::new();
// Start with objects that have gc_refs > 0
for (&ptr, &refs) in &gc_refs {
if refs > 0 {
reachable.insert(ptr);
worklist.push(ptr);
}
}
// Traverse reachable objects to find more reachable ones
while let Some(ptr) = worklist.pop() {
let obj = unsafe { ptr.0.as_ref() };
if obj.is_gc_tracked() {
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
for child_ptr in referent_ptrs {
let gc_ptr = GcObjectPtr(child_ptr);
// If child is in collecting set and not yet marked reachable
if collecting.contains(&gc_ptr) && reachable.insert(gc_ptr) {
worklist.push(gc_ptr);
}
}
}
}
// Step 5: Find unreachable objects (in collecting but not in reachable)
let unreachable: Vec<GcObjectPtr> = collecting.difference(&reachable).copied().collect();
if debug.contains(GcDebugFlags::STATS) {
eprintln!(
"gc: {} reachable, {} unreachable",
reachable.len(),
unreachable.len()
);
}
if unreachable.is_empty() {
// No cycles found - promote survivors to next generation
drop(gen_locks); // Release read locks before promoting
self.promote_survivors(generation, &collecting);
// Reset gen0 count
self.generations[0].count.store(0, Ordering::SeqCst);
self.generations[generation].update_stats(0, 0);
return (0, 0);
}
// Release read locks before finalization phase.
// This allows other threads to untrack objects while we finalize.
drop(gen_locks);
// Step 6: Finalize unreachable objects and handle resurrection
// 6a: Get references to all unreachable objects
let unreachable_refs: Vec<crate::PyObjectRef> = unreachable
.iter()
.filter_map(|ptr| {
let obj = unsafe { ptr.0.as_ref() };
if obj.strong_count() > 0 {
Some(obj.to_owned())
} else {
None
}
})
.collect();
if unreachable_refs.is_empty() {
self.promote_survivors(generation, &reachable);
// Reset gen0 count
self.generations[0].count.store(0, Ordering::SeqCst);
self.generations[generation].update_stats(0, 0);
return (0, 0);
}
// 6b: Record initial strong counts (for resurrection detection)
// Each object has +1 from unreachable_refs, so initial count includes that
let initial_counts: std::collections::HashMap<GcObjectPtr, usize> = unreachable_refs
.iter()
.map(|obj| {
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
(ptr, obj.strong_count())
})
.collect();
// 6c: Clear existing weakrefs BEFORE calling __del__
// This invalidates existing weakrefs, but new weakrefs created during __del__
// will still work (WeakRefList::add restores inner.obj if cleared)
//
// CRITICAL: We use a two-phase approach to match CPython behavior:
// Phase 1: Clear ALL weakrefs (set inner.obj = None) and collect callbacks
// Phase 2: Invoke ALL callbacks
// This ensures that when a callback runs, ALL weakrefs to unreachable objects
// are already dead (return None when called).
let mut all_callbacks: Vec<(crate::PyRef<crate::object::PyWeak>, crate::PyObjectRef)> =
Vec::new();
for obj_ref in &unreachable_refs {
let callbacks = obj_ref.gc_clear_weakrefs_collect_callbacks();
all_callbacks.extend(callbacks);
}
// Phase 2: Now call all callbacks - at this point ALL weakrefs are cleared
for (wr, cb) in all_callbacks {
if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) {
// Report the exception via run_unraisable
crate::vm::thread::with_vm(&cb, |vm| {
vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone());
});
}
// If with_vm returns None, we silently skip - no VM available to handle errors
}
// 6d: Call __del__ on all unreachable objects
// This allows resurrection to work correctly
// Skip objects that have already been finalized (prevents multiple __del__ calls)
for obj_ref in &unreachable_refs {
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj_ref.as_ref()));
let already_finalized = if let Ok(finalized) = self.finalized_objects.read() {
finalized.contains(&ptr)
} else {
false
};
if !already_finalized {
// Mark as finalized BEFORE calling __del__
// This ensures is_finalized() returns True inside __del__
if let Ok(mut finalized) = self.finalized_objects.write() {
finalized.insert(ptr);
}
obj_ref.try_call_finalizer();
}
}
// 6d: Detect resurrection - strong_count increased means object was resurrected
// Step 1: Find directly resurrected objects (strong_count increased)
let mut resurrected_set: HashSet<GcObjectPtr> = HashSet::new();
let unreachable_set: HashSet<GcObjectPtr> = unreachable.iter().copied().collect();
for obj in &unreachable_refs {
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
let initial = initial_counts.get(&ptr).copied().unwrap_or(1);
if obj.strong_count() > initial {
resurrected_set.insert(ptr);
}
}
// Step 2: Transitive resurrection - objects reachable from resurrected are also resurrected
// This is critical for cases like: Lazarus resurrects itself, its cargo should also survive
let mut worklist: Vec<GcObjectPtr> = resurrected_set.iter().copied().collect();
while let Some(ptr) = worklist.pop() {
let obj = unsafe { ptr.0.as_ref() };
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
for child_ptr in referent_ptrs {
let child_gc_ptr = GcObjectPtr(child_ptr);
// If child is in unreachable set and not yet marked as resurrected
if unreachable_set.contains(&child_gc_ptr) && resurrected_set.insert(child_gc_ptr) {
worklist.push(child_gc_ptr);
}
}
}
// Step 3: Partition into resurrected and truly dead
let (resurrected, truly_dead): (Vec<_>, Vec<_>) =
unreachable_refs.into_iter().partition(|obj| {
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
resurrected_set.contains(&ptr)
});
let resurrected_count = resurrected.len();
if debug.contains(GcDebugFlags::STATS) {
eprintln!(
"gc: {} resurrected, {} truly dead",
resurrected_count,
truly_dead.len()
);
}
// 6e: Break cycles ONLY for truly dead objects (not resurrected)
// Compute collected count: exclude instance dicts that are also in truly_dead.
// In CPython 3.12+, instance dicts are managed inline and not separately tracked,
// so they don't count toward the collected total.
let collected = {
let dead_ptrs: HashSet<usize> = truly_dead
.iter()
.map(|obj| obj.as_ref() as *const PyObject as usize)
.collect();
let instance_dict_count = truly_dead
.iter()
.filter(|obj| {
if let Some(dict_ref) = obj.dict() {
dead_ptrs.contains(&(dict_ref.as_object() as *const PyObject as usize))
} else {
false
}
})
.count();
truly_dead.len() - instance_dict_count
};
// 6e-1: If DEBUG_SAVEALL is set, save truly dead objects to garbage
if debug.contains(GcDebugFlags::SAVEALL) {
let mut garbage_guard = self.garbage.lock();
for obj_ref in truly_dead.iter() {
garbage_guard.push(obj_ref.clone());
}
}
if !truly_dead.is_empty() {
// 6g: Break cycles by clearing references (tp_clear)
// Weakrefs were already cleared in step 6c, but new weakrefs created
// during __del__ (step 6d) can still be upgraded.
//
// Clear and destroy objects within a deferred drop context.
// This prevents deadlocks from untrack calls during destruction.
rustpython_common::refcount::with_deferred_drops(|| {
for obj_ref in truly_dead.iter() {
if obj_ref.gc_has_clear() {
let edges = unsafe { obj_ref.gc_clear() };
drop(edges);
}
}
// Drop truly_dead references, triggering actual deallocation
drop(truly_dead);
});
}
// 6f: Resurrected objects stay in tracked_objects (they're still alive)
// Just drop our references to them
drop(resurrected);
// Promote survivors (reachable objects) to next generation
self.promote_survivors(generation, &reachable);
// Reset gen0 count after collection (enables automatic GC to trigger again)
self.generations[0].count.store(0, Ordering::SeqCst);
(0, 0)
self.generations[generation].update_stats(collected, 0);
(collected, 0)
}
/// Promote surviving objects to the next generation
fn promote_survivors(&self, from_gen: usize, survivors: &HashSet<GcObjectPtr>) {
if from_gen >= 2 {
return; // Already in oldest generation
}
let next_gen = from_gen + 1;
for &ptr in survivors {
// Remove from current generation
for gen_idx in 0..=from_gen {
if let Ok(mut gen_set) = self.generation_objects[gen_idx].write()
&& gen_set.remove(&ptr)
{
// Decrement count for source generation
let count = self.generations[gen_idx].count.load(Ordering::SeqCst);
if count > 0 {
self.generations[gen_idx]
.count
.fetch_sub(1, Ordering::SeqCst);
}
// Add to next generation
if let Ok(mut next_set) = self.generation_objects[next_gen].write()
&& next_set.insert(ptr)
{
// Increment count for target generation
self.generations[next_gen]
.count
.fetch_add(1, Ordering::SeqCst);
}
break;
}
}
}
}
/// Get count of frozen objects
+128 -15
View File
@@ -81,6 +81,77 @@ use core::{
#[derive(Debug)]
pub(super) struct Erased;
/// Trashcan mechanism to limit recursive deallocation depth (Py_TRASHCAN).
/// Without this, deeply nested structures (e.g. 200k-deep list) cause stack overflow
/// during deallocation because each level adds a stack frame.
mod trashcan {
use core::cell::Cell;
/// Maximum nesting depth for deallocation before deferring.
/// CPython uses UNWIND_NO_NESTING = 50.
const TRASHCAN_LIMIT: usize = 50;
type DeallocFn = unsafe fn(*mut super::PyObject);
type DeallocQueue = Vec<(*mut super::PyObject, DeallocFn)>;
thread_local! {
static DEALLOC_DEPTH: Cell<usize> = const { Cell::new(0) };
static DEALLOC_QUEUE: Cell<DeallocQueue> = const { Cell::new(Vec::new()) };
}
/// Try to begin deallocation. Returns true if we should proceed,
/// false if the object was deferred (depth exceeded).
#[inline]
pub(super) unsafe fn begin(
obj: *mut super::PyObject,
dealloc: unsafe fn(*mut super::PyObject),
) -> bool {
DEALLOC_DEPTH.with(|d| {
let depth = d.get();
if depth >= TRASHCAN_LIMIT {
// Depth exceeded: defer this deallocation
DEALLOC_QUEUE.with(|q| {
let mut queue = q.take();
queue.push((obj, dealloc));
q.set(queue);
});
false
} else {
d.set(depth + 1);
true
}
})
}
/// End deallocation and process any deferred objects if at outermost level.
#[inline]
pub(super) unsafe fn end() {
let depth = DEALLOC_DEPTH.with(|d| {
let depth = d.get();
debug_assert!(depth > 0, "trashcan::end called without matching begin");
let depth = depth - 1;
d.set(depth);
depth
});
if depth == 0 {
// Process deferred deallocations iteratively
loop {
let next = DEALLOC_QUEUE.with(|q| {
let mut queue = q.take();
let item = queue.pop();
q.set(queue);
item
});
if let Some((obj, dealloc)) = next {
unsafe { dealloc(obj) };
} else {
break;
}
}
}
}
}
/// Default dealloc: handles __del__, weakref clearing, tp_clear, and memory free.
/// Equivalent to subtype_dealloc.
pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
@@ -89,11 +160,28 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
return; // resurrected by __del__
}
// Extract child references before deallocation to break circular refs (tp_clear).
// This ensures that when edges are dropped after the object is freed,
// any pointers back to this object are already gone.
// Trashcan: limit recursive deallocation depth to prevent stack overflow
if !unsafe { trashcan::begin(obj, default_dealloc::<T>) } {
return; // deferred to queue
}
let vtable = obj_ref.0.vtable;
// Untrack from GC BEFORE deallocation.
if obj_ref.is_gc_tracked() {
let ptr = unsafe { NonNull::new_unchecked(obj) };
rustpython_common::refcount::try_defer_drop(move || {
// untrack_object only removes the pointer address from a HashSet.
// It does NOT dereference the pointer, so it's safe even after deallocation.
unsafe {
crate::gc_state::gc_state().untrack_object(ptr);
}
});
}
// Extract child references before deallocation to break circular refs (tp_clear)
let mut edges = Vec::new();
if let Some(clear_fn) = obj_ref.0.vtable.clear {
if let Some(clear_fn) = vtable.clear {
unsafe { clear_fn(obj, &mut edges) };
}
@@ -103,6 +191,9 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
// Drop child references - may trigger recursive destruction.
// The object is already deallocated, so circular refs are broken.
drop(edges);
// Trashcan: decrement depth and process deferred objects at outermost level
unsafe { trashcan::end() };
}
pub(super) unsafe fn debug_obj<T: PyPayload + core::fmt::Debug>(
x: &PyObject,
@@ -316,20 +407,24 @@ impl WeakRefList {
dict: Option<PyDictRef>,
) -> PyRef<PyWeak> {
let is_generic = cls_is_weakref && callback.is_none();
let _lock = weakref_lock::lock(obj as *const PyObject as usize);
// try_reuse_basic_ref: reuse cached generic weakref
if is_generic {
let generic_ptr = self.generic.load(Ordering::Relaxed);
if !generic_ptr.is_null() {
let generic = unsafe { &*generic_ptr };
if generic.0.ref_count.safe_inc() {
return unsafe { PyRef::from_raw(generic_ptr) };
// Try reuse under lock first (fast path, no allocation)
{
let _lock = weakref_lock::lock(obj as *const PyObject as usize);
if is_generic {
let generic_ptr = self.generic.load(Ordering::Relaxed);
if !generic_ptr.is_null() {
let generic = unsafe { &*generic_ptr };
if generic.0.ref_count.safe_inc() {
return unsafe { PyRef::from_raw(generic_ptr) };
}
}
}
}
// Allocate new PyWeak with wr_object pointing to referent
// Allocate OUTSIDE the stripe lock. PyRef::new_ref may trigger
// maybe_collect → GC → WeakRefList::clear on another object that
// hashes to the same stripe, which would deadlock on the spinlock.
let weak_payload = PyWeak {
pointers: Pointers::new(),
wr_object: Radium::new(obj as *const PyObject as *mut PyObject),
@@ -338,6 +433,24 @@ impl WeakRefList {
};
let weak = PyRef::new_ref(weak_payload, cls, dict);
// Re-acquire lock for linked list insertion
let _lock = weakref_lock::lock(obj as *const PyObject as usize);
// Re-check: another thread may have inserted a generic ref while we
// were allocating outside the lock. If so, reuse it and drop ours.
if is_generic {
let generic_ptr = self.generic.load(Ordering::Relaxed);
if !generic_ptr.is_null() {
let generic = unsafe { &*generic_ptr };
if generic.0.ref_count.safe_inc() {
// Nullify wr_object so drop_inner won't unlink an
// un-inserted node (which would corrupt the list head).
weak.wr_object.store(ptr::null_mut(), Ordering::Relaxed);
return unsafe { PyRef::from_raw(generic_ptr) };
}
}
}
// Insert into linked list under stripe lock
let node_ptr = NonNull::from(&*weak);
unsafe {
@@ -1070,7 +1183,7 @@ impl PyObject {
// Undo the temporary resurrection. Always remove both
// temporary refs; the second dec returns true only when
// ref_count drops to 0 (no resurrection).
zelf.0.ref_count.dec();
let _ = zelf.0.ref_count.dec();
zelf.0.ref_count.dec()
});
match ret {
@@ -1147,7 +1260,7 @@ impl PyObject {
/// 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
/// PyObject_CallFinalizerFromDealloc
pub fn try_call_finalizer(&self) {
let del = self.class().slots.del.load();
if let Some(slot_del) = del
+1
View File
@@ -51,6 +51,7 @@ pub fn check_signals(vm: &VirtualMachine) -> PyResult<()> {
trigger_signals(vm)
}
#[inline(never)]
#[cold]
fn trigger_signals(vm: &VirtualMachine) -> PyResult<()> {
+16 -9
View File
@@ -387,11 +387,12 @@ impl Interpreter {
/// Finalization steps (matching Py_FinalizeEx):
/// 1. Flush stdout and stderr.
/// 1. Handle exit exception and turn it to exit code.
/// 1. Wait for thread shutdown (call threading._shutdown).
/// 1. Mark vm as finalizing.
/// 1. Call threading._shutdown() to join non-daemon threads.
/// 1. Run atexit exit functions.
/// 1. Finalize modules (clear module dicts in reverse import order).
/// 1. Mark vm as finalized.
/// 1. Set finalizing flag (suppresses unraisable exceptions from __del__).
/// 1. Forced GC collection pass (collect cycles while builtins are available).
/// 1. Module finalization (finalize_modules).
/// 1. Final stdout/stderr flush.
///
/// Note that calling `finalize` is not necessary by purpose though.
pub fn finalize(self, exc: Option<PyBaseExceptionRef>) -> u32 {
@@ -419,13 +420,19 @@ impl Interpreter {
);
}
// Mark as finalizing AFTER thread shutdown
vm.state.finalizing.store(true, Ordering::Release);
// Run atexit exit functions
// Run atexit handlers before setting finalizing flag.
// This allows unraisable exceptions from atexit handlers to be reported.
atexit::_run_exitfuncs(vm);
// Finalize modules: clear module dicts in reverse import order
// Now suppress unraisable exceptions from daemon threads and __del__
// methods during the rest of shutdown.
vm.state.finalizing.store(true, Ordering::Release);
// GC pass - collect cycles before module cleanup
crate::gc_state::gc_state().collect_force(2);
// Module finalization: remove modules from sys.modules, GC collect
// (while builtins is still available for __del__), then clear module dicts.
vm.finalize_modules();
vm.flush_std();
+39 -40
View File
@@ -730,26 +730,25 @@ impl VirtualMachine {
// Phase 2: Remove all modules from sys.modules (set values to None),
// and collect weakrefs to modules preserving import order.
// Also keeps strong refs (module_refs) to prevent premature deallocation.
// CPython uses _PyGC_CollectNoFail() here to collect __globals__ cycles;
// since RustPython has no working GC, we keep modules alive through
// Phase 4 so their dicts can be explicitly cleared.
let (module_weakrefs, module_refs) = self.finalize_remove_modules();
// No strong refs are kept — modules freed when their last ref drops.
let module_weakrefs = self.finalize_remove_modules();
// Phase 3: Clear sys.modules dict
self.finalize_clear_modules_dict();
// Phase 4: Clear module dicts in reverse import order using 2-pass algorithm.
// All modules are still alive (held by module_refs), so all weakrefs are valid.
// This breaks __globals__ cycles: dict entries set to None → functions freed →
// __globals__ refs dropped → dict refcount decreases.
// Phase 4: GC collect — modules removed from sys.modules are freed,
// exposing cycles (e.g., dict ↔ function.__globals__). GC collects
// these and calls __del__ while module dicts are still intact.
crate::gc_state::gc_state().collect_force(2);
// Phase 5: Clear module dicts in reverse import order using 2-pass algorithm.
// Skip builtins and sys — those are cleared last.
self.finalize_clear_module_dicts(&module_weakrefs);
// Drop strong refs → modules freed with already-cleared dicts.
// No __globals__ cycles remain (broken by Phase 4).
drop(module_refs);
// Phase 6: GC collect — pick up anything freed by dict clearing.
crate::gc_state::gc_state().collect_force(2);
// Phase 5: Clear sys and builtins dicts last
// Phase 7: Clear sys and builtins dicts last
self.finalize_clear_sys_builtins_dict();
}
@@ -793,17 +792,17 @@ impl VirtualMachine {
let _ = self.builtins.dict().set_item("_", none, self);
}
/// Phase 2: Set all sys.modules values to None and collect weakrefs to modules.
/// Returns (weakrefs for Phase 4, strong refs to keep modules alive).
fn finalize_remove_modules(&self) -> (Vec<(String, PyRef<PyWeak>)>, Vec<PyObjectRef>) {
/// Phase 2: Set all sys.modules values to None and collect weakrefs.
/// No strong refs are kept — modules are freed when removed from sys.modules
/// (if nothing else references them), allowing GC to collect their cycles.
fn finalize_remove_modules(&self) -> Vec<(String, PyRef<PyWeak>)> {
let mut module_weakrefs = Vec::new();
let mut module_refs = Vec::new();
let Ok(modules) = self.sys_module.get_attr(identifier!(self, modules), self) else {
return (module_weakrefs, module_refs);
return module_weakrefs;
};
let Some(modules_dict) = modules.downcast_ref::<PyDict>() else {
return (module_weakrefs, module_refs);
return module_weakrefs;
};
let none = self.ctx.none();
@@ -815,19 +814,18 @@ impl VirtualMachine {
.map(|s| s.as_str().to_owned())
.unwrap_or_default();
// Save weakref and strong ref to module for later clearing
if value.downcast_ref::<PyModule>().is_some() {
if let Ok(weak) = value.downgrade(None, self) {
module_weakrefs.push((name, weak));
}
module_refs.push(value.clone());
// Save weakref to module (for later dict clearing)
if value.downcast_ref::<PyModule>().is_some()
&& let Ok(weak) = value.downgrade(None, self)
{
module_weakrefs.push((name, weak));
}
// Set the value to None in sys.modules
let _ = modules_dict.set_item(&*key, none.clone(), self);
}
(module_weakrefs, module_refs)
module_weakrefs
}
/// Phase 3: Clear sys.modules dict.
@@ -839,18 +837,13 @@ impl VirtualMachine {
}
}
/// Phase 4: Clear module dicts in reverse import order using 2-pass algorithm.
/// Without GC, only clear __main__ — other modules' __del__ handlers
/// need their globals intact. CPython can clear ALL module dicts because
/// _PyGC_CollectNoFail() finalizes cycle-participating objects beforehand.
/// Phase 5: Clear module dicts in reverse import order.
/// Skip builtins and sys — those are cleared last in Phase 7.
fn finalize_clear_module_dicts(&self, module_weakrefs: &[(String, PyRef<PyWeak>)]) {
for (name, weakref) in module_weakrefs.iter().rev() {
// Only clear __main__ — user objects with __del__ get finalized
// while other modules' globals remain intact for their __del__ handlers.
if name != "__main__" {
continue;
}
let builtins_dict = self.builtins.dict();
let sys_dict = self.sys_module.dict();
for (_name, weakref) in module_weakrefs.iter().rev() {
let Some(module_obj) = weakref.upgrade() else {
continue;
};
@@ -858,7 +851,13 @@ impl VirtualMachine {
continue;
};
Self::module_clear_dict(&module.dict(), self);
let dict = module.dict();
// Skip builtins and sys — they are cleared last
if dict.is(&builtins_dict) || dict.is(&sys_dict) {
continue;
}
Self::module_clear_dict(&dict, self);
}
}
@@ -875,7 +874,7 @@ impl VirtualMachine {
}
if let Some(key_str) = key.downcast_ref::<PyStr>() {
let name = key_str.as_str();
if name.starts_with('_') && name != "__builtins__" && name != "__spec__" {
if name.starts_with('_') && name != "__builtins__" {
let _ = dict.set_item(name, none.clone(), vm);
}
}
@@ -888,14 +887,13 @@ impl VirtualMachine {
}
if let Some(key_str) = key.downcast_ref::<PyStr>()
&& key_str.as_str() != "__builtins__"
&& key_str.as_str() != "__spec__"
{
let _ = dict.set_item(key_str.as_str(), none.clone(), vm);
}
}
}
/// Phase 5: Clear sys and builtins dicts last.
/// Phase 7: Clear sys and builtins dicts last.
fn finalize_clear_sys_builtins_dict(&self) {
Self::module_clear_dict(&self.sys_module.dict(), self);
Self::module_clear_dict(&self.builtins.dict(), self);
@@ -1148,6 +1146,7 @@ impl VirtualMachine {
self.frames.borrow_mut().pop();
#[cfg(feature = "threading")]
crate::vm::thread::pop_thread_frame();
self.recursion_depth.update(|d| d - 1);
}
+1
View File
@@ -43,6 +43,7 @@ thread_local! {
/// while the owning thread is writing).
pub(crate) static CURRENT_FRAME: AtomicPtr<Frame> =
const { AtomicPtr::new(core::ptr::null_mut()) };
}
scoped_tls::scoped_thread_local!(static VM_CURRENT: VirtualMachine);