diff --git a/crates/vm/src/stdlib/_functools.rs b/crates/vm/src/stdlib/_functools.rs index 0a8c61332..962002ee8 100644 --- a/crates/vm/src/stdlib/_functools.rs +++ b/crates/vm/src/stdlib/_functools.rs @@ -179,16 +179,19 @@ mod _functools { } } - // Placeholder singleton for partial arguments - // The singleton is stored as _instance on the type class + // Keep identity in interpreter state, not on the shared static type. #[pyattr] #[allow(non_snake_case)] fn Placeholder(vm: &VirtualMachine) -> PyObjectRef { + let cell = &vm.state.functools_placeholder; + if let Some(placeholder) = cell.get() { + return placeholder.clone(); + } + // Allocate outside the cell initializer: allocation may trigger GC. + // Another thread can publish first; always return the published winner. let placeholder = PyPlaceholderType.into_pyobject(vm); - // Store the singleton on the type class for slot_new to find - let typ = placeholder.class(); - typ.set_attr(vm.ctx.intern_str("_instance"), placeholder.clone()); - placeholder + let _ = cell.set(placeholder); + cell.get().expect("Placeholder was initialized").clone() } #[pyattr] @@ -199,16 +202,11 @@ mod _functools { impl Constructor for PyPlaceholderType { type Args = FuncArgs; - fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn slot_new(_cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { if !args.args.is_empty() || !args.kwargs.is_empty() { return Err(vm.new_type_error("_PlaceholderType takes no arguments")); } - // Return the singleton stored on the type class - if let Some(instance) = cls.get_attr(vm.ctx.intern_str("_instance")) { - return Ok(instance); - } - // Fallback: create a new instance (shouldn't happen for base type after module init) - Ok(Self.into_pyobject(vm)) + Ok(Placeholder(vm)) } fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { diff --git a/crates/vm/src/vm/interpreter.rs b/crates/vm/src/vm/interpreter.rs index 3eeca7c34..324f17574 100644 --- a/crates/vm/src/vm/interpreter.rs +++ b/crates/vm/src/vm/interpreter.rs @@ -169,6 +169,7 @@ where import_time_header: AtomicBool::new(false), lazy_imports, struct_format_cache: crate::buffer::FormatSpecCache::default(), + functools_placeholder: Default::default(), finalizing: AtomicBool::new(false), #[cfg(feature = "threading")] finalizing_thread_ident: AtomicCell::new(0), diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 6e1a74a22..6fd5107d4 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -879,6 +879,8 @@ pub struct PyGlobalState { pub(crate) import_time_header: AtomicBool, pub(crate) lazy_imports: crate::lazy_import::LazyImportsState, pub struct_format_cache: crate::buffer::FormatSpecCache, + /// Placeholder identity shared by all threads and fresh imports in this interpreter. + pub(crate) functools_placeholder: crate::common::lock::OnceCell, pub finalizing: AtomicBool, /// The thread performing finalization, which need not be the process main thread. #[cfg(feature = "threading")]