Compare commits

...

11 Commits

Author SHA1 Message Date
Jeong, YunWon
2b061612e5 Fix cron-ci failures: ctypes set_attr, missing features, __func__ AttributeError
- Use cls.set_attr() instead of cls.as_object().set_attr() in ctypes
  to ensure modified() is called and type cache stays valid
- Add host_env feature to cron-ci.yaml for frozen_origname_matches test
- Add stdio feature to cron-ci.yaml for encodings initialization
- Fix __func__ AttributeError in custom_text_test_runner.py
2026-03-19 22:13:28 +09:00
Jeong, YunWon
add34a2f19 Drop old PyObjectRef outside type lock to prevent deadlock
Dropping values inside with_type_lock can trigger weakref callbacks,
which may access attributes (LOAD_ATTR specialization) and re-acquire
the non-reentrant type mutex, causing deadlock.

Return old values from lock closures so they drop after lock release.
2026-03-19 22:13:28 +09:00
Jeong, YunWon
e0886e2fb6 type lock 2026-03-19 22:13:28 +09:00
Jeong, YunWon
328de2e83e Fix Constants newtype usage in init_cleanup_code 2026-03-19 22:13:28 +09:00
Jeong, YunWon
2d4e1f2f5e Extract datastack_frame_size_bytes_for_code, skip monitoring for init_cleanup frames, guard trace dispatch
- Extract datastack_frame_size_bytes_for_code as free function, use it
  to compute init_cleanup stack bytes instead of hardcoded constant
- Add monitoring_disabled_for_code to skip instrumentation for
  synthetic init_cleanup code object in RESUME and execute_instrumented
- Add is_trace_event guard so profile-only events skip trace_func dispatch
- Reformat core.rs (rustfmt)
2026-03-19 22:13:28 +09:00
Jeong, YunWon
15836ca0fc address review: check datastack space for extra_bytes, require CO_OPTIMIZED in vectorcall fast path 2026-03-19 22:13:28 +09:00
Jeong, YunWon
09c3bb1d7f address review: invalidate init cache on type modification, add cspell words 2026-03-19 22:13:28 +09:00
Jeong, YunWon
94e8d54731 Align call-init frame flow and spec cache atomic ordering 2026-03-19 22:13:28 +09:00
Jeong, YunWon
382be9a525 Tighten CALL_ALLOC_AND_ENTER_INIT stack-space guard 2026-03-19 22:13:28 +09:00
Jeong, YunWon
77b46d53ca Align type _spec_cache and latin1 singleton string paths 2026-03-19 22:13:28 +09:00
Jeong, YunWon
bcd618ecc9 Align BINARY_OP_EXTEND with CPython descriptor cache model 2026-03-19 22:13:28 +09:00
8 changed files with 339 additions and 215 deletions

View File

@@ -61,6 +61,9 @@
"dedents", "dedents",
"deduped", "deduped",
"deoptimize", "deoptimize",
"downcastable",
"downcasted",
"dumpable",
"emscripten", "emscripten",
"excs", "excs",
"interps", "interps",

View File

@@ -12,7 +12,7 @@ on:
name: Periodic checks/tasks name: Periodic checks/tasks
env: env:
CARGO_ARGS: --no-default-features --features stdlib,importlib,encodings,ssl-rustls,jit CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls,jit,host_env
PYTHON_VERSION: "3.14.3" PYTHON_VERSION: "3.14.3"
jobs: jobs:
@@ -35,7 +35,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- run: sudo apt-get update && sudo apt-get -y install lcov - run: sudo apt-get update && sudo apt-get -y install lcov
- name: Run cargo-llvm-cov with Rust tests. - name: Run cargo-llvm-cov with Rust tests.
run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --verbose --no-default-features --features stdlib,importlib,encodings,ssl-rustls,jit run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --verbose --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls,jit
- name: Run cargo-llvm-cov with Python snippets. - name: Run cargo-llvm-cov with Python snippets.
run: python scripts/cargo-llvm-cov.py run: python scripts/cargo-llvm-cov.py
continue-on-error: true continue-on-error: true

View File

@@ -276,7 +276,6 @@ pub struct TypeSpecializationCache {
pub init: PyAtomicRef<Option<PyFunction>>, pub init: PyAtomicRef<Option<PyFunction>>,
pub getitem: PyAtomicRef<Option<PyFunction>>, pub getitem: PyAtomicRef<Option<PyFunction>>,
pub getitem_version: AtomicU32, pub getitem_version: AtomicU32,
// Serialize cache writes/invalidation similar to CPython's BEGIN_TYPE_LOCK.
write_lock: PyMutex<()>, write_lock: PyMutex<()>,
retired: PyRwLock<Vec<PyObjectRef>>, retired: PyRwLock<Vec<PyObjectRef>>,
} }
@@ -302,9 +301,6 @@ impl TypeSpecializationCache {
#[inline] #[inline]
fn swap_init(&self, new_init: Option<PyRef<PyFunction>>, vm: Option<&VirtualMachine>) { fn swap_init(&self, new_init: Option<PyRef<PyFunction>>, vm: Option<&VirtualMachine>) {
if let Some(vm) = vm { if let Some(vm) = vm {
// Keep replaced refs alive for the currently executing frame, matching
// CPython-style "old pointer remains valid during ongoing execution"
// without accumulating global retired refs.
self.init.swap_to_temporary_refs(new_init, vm); self.init.swap_to_temporary_refs(new_init, vm);
return; return;
} }
@@ -329,8 +325,6 @@ impl TypeSpecializationCache {
#[inline] #[inline]
fn invalidate_for_type_modified(&self) { fn invalidate_for_type_modified(&self) {
let _guard = self.write_lock.lock(); let _guard = self.write_lock.lock();
// _spec_cache contract: type modification invalidates all cached
// specialization functions.
self.swap_init(None, None); self.swap_init(None, None);
self.swap_getitem(None, None); self.swap_getitem(None, None);
} }
@@ -457,9 +451,15 @@ fn is_subtype_with_mro(a_mro: &[PyTypeRef], a: &Py<PyType>, b: &Py<PyType>) -> b
} }
impl PyType { impl PyType {
#[inline]
fn with_type_lock<R>(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R {
let _guard = vm.state.type_mutex.lock();
f()
}
/// Assign a fresh version tag. Returns 0 if the version counter has been /// Assign a fresh version tag. Returns 0 if the version counter has been
/// exhausted, in which case no new cache entries can be created. /// exhausted, in which case no new cache entries can be created.
pub fn assign_version_tag(&self) -> u32 { fn assign_version_tag_inner(&self) -> u32 {
let v = self.tp_version_tag.load(Ordering::Acquire); let v = self.tp_version_tag.load(Ordering::Acquire);
if v != 0 { if v != 0 {
return v; return v;
@@ -467,7 +467,7 @@ impl PyType {
// Assign versions to all direct bases first (MRO invariant). // Assign versions to all direct bases first (MRO invariant).
for base in self.bases.read().iter() { for base in self.bases.read().iter() {
if base.assign_version_tag() == 0 { if base.assign_version_tag_inner() == 0 {
return 0; return 0;
} }
} }
@@ -487,8 +487,23 @@ impl PyType {
} }
} }
pub fn assign_version_tag(&self) -> u32 {
self.assign_version_tag_inner()
}
pub(crate) fn version_for_specialization(&self, vm: &VirtualMachine) -> u32 {
Self::with_type_lock(vm, || {
let version = self.tp_version_tag.load(Ordering::Acquire);
if version == 0 {
self.assign_version_tag_inner()
} else {
version
}
})
}
/// Invalidate this type's version tag and cascade to all subclasses. /// Invalidate this type's version tag and cascade to all subclasses.
pub fn modified(&self) { fn modified_inner(&self) {
if let Some(ext) = self.heaptype_ext.as_ref() { if let Some(ext) = self.heaptype_ext.as_ref() {
ext.specialization_cache.invalidate_for_type_modified(); ext.specialization_cache.invalidate_for_type_modified();
} }
@@ -505,11 +520,15 @@ impl PyType {
let subclasses = self.subclasses.read(); let subclasses = self.subclasses.read();
for weak_ref in subclasses.iter() { for weak_ref in subclasses.iter() {
if let Some(sub) = weak_ref.upgrade() { if let Some(sub) = weak_ref.upgrade() {
sub.downcast_ref::<PyType>().unwrap().modified(); sub.downcast_ref::<PyType>().unwrap().modified_inner();
} }
} }
} }
pub fn modified(&self) {
self.modified_inner();
}
pub fn new_simple_heap( pub fn new_simple_heap(
name: &str, name: &str,
base: &Py<PyType>, base: &Py<PyType>,
@@ -898,6 +917,74 @@ impl PyType {
self.find_name_in_mro(attr_name) self.find_name_in_mro(attr_name)
} }
/// CPython-style `_PyType_LookupRefAndVersion` equivalent for interned names.
/// Returns the observed lookup result and the type version used for the lookup.
pub(crate) fn lookup_ref_and_version_interned(
&self,
name: &'static PyStrInterned,
vm: &VirtualMachine,
) -> (Option<PyObjectRef>, u32) {
let version = self.tp_version_tag.load(Ordering::Acquire);
if version != 0 {
let idx = type_cache_hash(version, name);
let entry = &TYPE_CACHE[idx];
let name_ptr = name as *const _ as *mut _;
loop {
let seq1 = entry.begin_read();
let entry_version = entry.version.load(Ordering::Acquire);
let type_version = self.tp_version_tag.load(Ordering::Acquire);
if entry_version != type_version
|| !core::ptr::eq(entry.name.load(Ordering::Relaxed), name_ptr)
{
break;
}
let ptr = entry.value.load(Ordering::Acquire);
if ptr.is_null() {
if entry.end_read(seq1) {
return (None, entry_version);
}
continue;
}
if let Some(cloned) = unsafe { PyObject::try_to_owned_from_ptr(ptr) } {
let same_ptr = core::ptr::eq(entry.value.load(Ordering::Relaxed), ptr);
if same_ptr && entry.end_read(seq1) {
return (Some(cloned), entry_version);
}
drop(cloned);
continue;
}
break;
}
}
Self::with_type_lock(vm, || {
let assigned = if self.tp_version_tag.load(Ordering::Acquire) == 0 {
self.assign_version_tag_inner()
} else {
self.tp_version_tag.load(Ordering::Acquire)
};
let result = self.find_name_in_mro_uncached(name);
if assigned != 0
&& !TYPE_CACHE_CLEARING.load(Ordering::Acquire)
&& self.tp_version_tag.load(Ordering::Acquire) == assigned
{
let idx = type_cache_hash(assigned, name);
let entry = &TYPE_CACHE[idx];
let name_ptr = name as *const _ as *mut _;
entry.begin_write();
entry.version.store(0, Ordering::Release);
let new_ptr = result.as_ref().map_or(core::ptr::null_mut(), |found| {
&**found as *const PyObject as *mut _
});
entry.value.store(new_ptr, Ordering::Relaxed);
entry.name.store(name_ptr, Ordering::Relaxed);
entry.version.store(assigned, Ordering::Release);
entry.end_write();
}
(result, assigned)
})
}
/// Cache __init__ for CALL_ALLOC_AND_ENTER_INIT specialization. /// Cache __init__ for CALL_ALLOC_AND_ENTER_INIT specialization.
/// The cache is valid only when guarded by the type version check. /// The cache is valid only when guarded by the type version check.
pub(crate) fn cache_init_for_specialization( pub(crate) fn cache_init_for_specialization(
@@ -912,6 +999,7 @@ impl PyType {
if tp_version == 0 { if tp_version == 0 {
return false; return false;
} }
Self::with_type_lock(vm, || {
if self.tp_version_tag.load(Ordering::Acquire) != tp_version { if self.tp_version_tag.load(Ordering::Acquire) != tp_version {
return false; return false;
} }
@@ -921,6 +1009,7 @@ impl PyType {
} }
ext.specialization_cache.swap_init(Some(init), Some(vm)); ext.specialization_cache.swap_init(Some(init), Some(vm));
true true
})
} }
/// Read cached __init__ for CALL_ALLOC_AND_ENTER_INIT specialization. /// Read cached __init__ for CALL_ALLOC_AND_ENTER_INIT specialization.
@@ -954,6 +1043,7 @@ impl PyType {
if tp_version == 0 { if tp_version == 0 {
return false; return false;
} }
Self::with_type_lock(vm, || {
let _guard = ext.specialization_cache.write_lock.lock(); let _guard = ext.specialization_cache.write_lock.lock();
if self.tp_version_tag.load(Ordering::Acquire) != tp_version { if self.tp_version_tag.load(Ordering::Acquire) != tp_version {
return false; return false;
@@ -962,18 +1052,18 @@ impl PyType {
if func_version == 0 { if func_version == 0 {
return false; return false;
} }
ext.specialization_cache
.swap_getitem(Some(getitem), Some(vm));
ext.specialization_cache ext.specialization_cache
.getitem_version .getitem_version
.store(func_version, Ordering::Relaxed); .store(func_version, Ordering::Release);
ext.specialization_cache
.swap_getitem(Some(getitem), Some(vm));
true true
})
} }
/// Read cached __getitem__ for BINARY_OP_SUBSCR_GETITEM specialization. /// Read cached __getitem__ for BINARY_OP_SUBSCR_GETITEM specialization.
pub(crate) fn get_cached_getitem_for_specialization(&self) -> Option<(PyRef<PyFunction>, u32)> { pub(crate) fn get_cached_getitem_for_specialization(&self) -> Option<(PyRef<PyFunction>, u32)> {
let ext = self.heaptype_ext.as_ref()?; let ext = self.heaptype_ext.as_ref()?;
// Match CPython check order: pointer (Acquire) then function version.
let getitem = ext let getitem = ext
.specialization_cache .specialization_cache
.getitem .getitem
@@ -981,7 +1071,7 @@ impl PyType {
let cached_version = ext let cached_version = ext
.specialization_cache .specialization_cache
.getitem_version .getitem_version
.load(Ordering::Relaxed); .load(Ordering::Acquire);
if cached_version == 0 { if cached_version == 0 {
return None; return None;
} }
@@ -1334,6 +1424,7 @@ impl PyType {
// // TODO: how to uniquely identify the subclasses to remove? // // TODO: how to uniquely identify the subclasses to remove?
// } // }
Self::with_type_lock(vm, || {
*zelf.bases.write() = bases; *zelf.bases.write() = bases;
// Recursively update the mros of this class and all subclasses // Recursively update the mros of this class and all subclasses
fn update_mro_recursively(cls: &PyType, vm: &VirtualMachine) -> PyResult<()> { fn update_mro_recursively(cls: &PyType, vm: &VirtualMachine) -> PyResult<()> {
@@ -1352,7 +1443,7 @@ impl PyType {
update_mro_recursively(zelf, vm)?; update_mro_recursively(zelf, vm)?;
// Invalidate inline caches // Invalidate inline caches
zelf.modified(); zelf.modified_inner();
// TODO: do any old slots need to be cleaned up first? // TODO: do any old slots need to be cleaned up first?
zelf.init_slots(&vm.ctx); zelf.init_slots(&vm.ctx);
@@ -1366,6 +1457,8 @@ impl PyType {
.unwrap(), .unwrap(),
); );
} }
Ok(())
})?;
Ok(()) Ok(())
} }
@@ -1457,20 +1550,31 @@ impl PyType {
))); )));
} }
let mut attrs = self.attributes.write(); let annotate_key = identifier!(vm, __annotate__);
// First try __annotate__, in case that's been set explicitly let annotate_func_key = identifier!(vm, __annotate_func__);
if let Some(annotate) = attrs.get(identifier!(vm, __annotate__)).cloned() { let attrs = self.attributes.read();
if let Some(annotate) = attrs.get(annotate_key).cloned() {
return Ok(annotate); return Ok(annotate);
} }
// Then try __annotate_func__ if let Some(annotate) = attrs.get(annotate_func_key).cloned() {
if let Some(annotate) = attrs.get(identifier!(vm, __annotate_func__)).cloned() {
// TODO: Apply descriptor tp_descr_get if needed
return Ok(annotate); return Ok(annotate);
} }
// Set __annotate_func__ = None and return None drop(attrs);
let none = vm.ctx.none(); let none = vm.ctx.none();
attrs.insert(identifier!(vm, __annotate_func__), none.clone()); let (result, _prev) = Self::with_type_lock(vm, || {
Ok(none) let mut attrs = self.attributes.write();
if let Some(annotate) = attrs.get(annotate_key).cloned() {
return (annotate, None);
}
if let Some(annotate) = attrs.get(annotate_func_key).cloned() {
return (annotate, None);
}
self.modified_inner();
let prev = attrs.insert(annotate_func_key, none.clone());
(none, prev)
});
Ok(result)
} }
#[pygetset(setter)] #[pygetset(setter)]
@@ -1493,20 +1597,27 @@ impl PyType {
return Err(vm.new_type_error("__annotate__ must be callable or None")); return Err(vm.new_type_error("__annotate__ must be callable or None"));
} }
let _prev_values = Self::with_type_lock(vm, || {
self.modified_inner();
let mut attrs = self.attributes.write(); let mut attrs = self.attributes.write();
// Clear cached annotations only when setting to a new callable let removed = if !vm.is_none(&value) {
if !vm.is_none(&value) { attrs.swap_remove(identifier!(vm, __annotations_cache__))
attrs.swap_remove(identifier!(vm, __annotations_cache__)); } else {
} None
attrs.insert(identifier!(vm, __annotate_func__), value.clone()); };
let prev = attrs.insert(identifier!(vm, __annotate_func__), value);
(removed, prev)
});
Ok(()) Ok(())
} }
#[pygetset] #[pygetset]
fn __annotations__(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> { fn __annotations__(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
let annotations_key = identifier!(vm, __annotations__);
let annotations_cache_key = identifier!(vm, __annotations_cache__);
let attrs = self.attributes.read(); let attrs = self.attributes.read();
if let Some(annotations) = attrs.get(identifier!(vm, __annotations__)).cloned() { if let Some(annotations) = attrs.get(annotations_key).cloned() {
// Ignore the __annotations__ descriptor stored on type itself. // Ignore the __annotations__ descriptor stored on type itself.
if !annotations.class().is(vm.ctx.types.getset_type) { if !annotations.class().is(vm.ctx.types.getset_type) {
if vm.is_none(&annotations) if vm.is_none(&annotations)
@@ -1521,8 +1632,7 @@ impl PyType {
))); )));
} }
} }
// Then try __annotations_cache__ if let Some(annotations) = attrs.get(annotations_cache_key).cloned() {
if let Some(annotations) = attrs.get(identifier!(vm, __annotations_cache__)).cloned() {
if vm.is_none(&annotations) if vm.is_none(&annotations)
|| annotations.class().is(vm.ctx.types.dict_type) || annotations.class().is(vm.ctx.types.dict_type)
|| self.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) || self.slots.flags.has_feature(PyTypeFlags::HEAPTYPE)
@@ -1559,11 +1669,21 @@ impl PyType {
vm.ctx.new_dict().into() vm.ctx.new_dict().into()
}; };
// Cache the result in __annotations_cache__ let (result, _prev) = Self::with_type_lock(vm, || {
self.attributes let mut attrs = self.attributes.write();
.write() if let Some(existing) = attrs.get(annotations_key).cloned()
.insert(identifier!(vm, __annotations_cache__), annotations.clone()); && !existing.class().is(vm.ctx.types.getset_type)
Ok(annotations) {
return (existing, None);
}
if let Some(existing) = attrs.get(annotations_cache_key).cloned() {
return (existing, None);
}
self.modified_inner();
let prev = attrs.insert(annotations_cache_key, annotations.clone());
(annotations, prev)
});
Ok(result)
} }
#[pygetset(setter)] #[pygetset(setter)]
@@ -1579,43 +1699,43 @@ impl PyType {
))); )));
} }
let _prev_values = Self::with_type_lock(vm, || {
self.modified_inner();
let mut attrs = self.attributes.write(); let mut attrs = self.attributes.write();
let has_annotations = attrs.contains_key(identifier!(vm, __annotations__)); let has_annotations = attrs.contains_key(identifier!(vm, __annotations__));
let mut prev = Vec::new();
match value { match value {
crate::function::PySetterValue::Assign(value) => { crate::function::PySetterValue::Assign(value) => {
// SET path: store the value (including None)
let key = if has_annotations { let key = if has_annotations {
identifier!(vm, __annotations__) identifier!(vm, __annotations__)
} else { } else {
identifier!(vm, __annotations_cache__) identifier!(vm, __annotations_cache__)
}; };
attrs.insert(key, value); prev.extend(attrs.insert(key, value));
if has_annotations { if has_annotations {
attrs.swap_remove(identifier!(vm, __annotations_cache__)); prev.extend(attrs.swap_remove(identifier!(vm, __annotations_cache__)));
} }
} }
crate::function::PySetterValue::Delete => { crate::function::PySetterValue::Delete => {
// DELETE path: remove the key
let removed = if has_annotations { let removed = if has_annotations {
attrs attrs.swap_remove(identifier!(vm, __annotations__))
.swap_remove(identifier!(vm, __annotations__))
.is_some()
} else { } else {
attrs attrs.swap_remove(identifier!(vm, __annotations_cache__))
.swap_remove(identifier!(vm, __annotations_cache__))
.is_some()
}; };
if !removed { if removed.is_none() {
return Err(vm.new_attribute_error("__annotations__")); return Err(vm.new_attribute_error("__annotations__"));
} }
prev.extend(removed);
if has_annotations { if has_annotations {
attrs.swap_remove(identifier!(vm, __annotations_cache__)); prev.extend(attrs.swap_remove(identifier!(vm, __annotations_cache__)));
} }
} }
} }
attrs.swap_remove(identifier!(vm, __annotate_func__)); prev.extend(attrs.swap_remove(identifier!(vm, __annotate_func__)));
attrs.swap_remove(identifier!(vm, __annotate__)); prev.extend(attrs.swap_remove(identifier!(vm, __annotate__)));
Ok(prev)
})?;
Ok(()) Ok(())
} }
@@ -1648,9 +1768,13 @@ impl PyType {
#[pygetset(setter)] #[pygetset(setter)]
fn set___module__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { fn set___module__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
self.check_set_special_type_attr(identifier!(vm, __module__), vm)?; self.check_set_special_type_attr(identifier!(vm, __module__), vm)?;
let _prev_values = Self::with_type_lock(vm, || {
self.modified_inner();
let mut attributes = self.attributes.write(); let mut attributes = self.attributes.write();
attributes.swap_remove(identifier!(vm, __firstlineno__)); let removed = attributes.swap_remove(identifier!(vm, __firstlineno__));
attributes.insert(identifier!(vm, __module__), value); let prev = attributes.insert(identifier!(vm, __module__), value);
(removed, prev)
});
Ok(()) Ok(())
} }
@@ -1772,24 +1896,26 @@ impl PyType {
value: PySetterValue<PyTupleRef>, value: PySetterValue<PyTupleRef>,
vm: &VirtualMachine, vm: &VirtualMachine,
) -> PyResult<()> { ) -> PyResult<()> {
match value {
PySetterValue::Assign(ref val) => {
let key = identifier!(vm, __type_params__); let key = identifier!(vm, __type_params__);
match value {
PySetterValue::Assign(val) => {
self.check_set_special_type_attr(key, vm)?; self.check_set_special_type_attr(key, vm)?;
self.modified(); let _prev_value = Self::with_type_lock(vm, || {
self.attributes.write().insert(key, val.clone().into()); self.modified_inner();
self.attributes.write().insert(key, val.into())
});
} }
PySetterValue::Delete => { PySetterValue::Delete => {
// For delete, we still need to check if the type is immutable
if self.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) { if self.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"cannot delete '__type_params__' attribute of immutable type '{}'", "cannot delete '__type_params__' attribute of immutable type '{}'",
self.slot_name() self.slot_name()
))); )));
} }
let key = identifier!(vm, __type_params__); let _prev_value = Self::with_type_lock(vm, || {
self.modified(); self.modified_inner();
self.attributes.write().shift_remove(&key); self.attributes.write().shift_remove(&key)
});
} }
} }
Ok(()) Ok(())
@@ -2397,10 +2523,12 @@ impl Py<PyType> {
// Check if we can set this special type attribute // Check if we can set this special type attribute
self.check_set_special_type_attr(identifier!(vm, __doc__), vm)?; self.check_set_special_type_attr(identifier!(vm, __doc__), vm)?;
// Set the __doc__ in the type's dict let _prev_value = PyType::with_type_lock(vm, || {
self.modified_inner();
self.attributes self.attributes
.write() .write()
.insert(identifier!(vm, __doc__), value); .insert(identifier!(vm, __doc__), value)
});
Ok(()) Ok(())
} }
@@ -2462,13 +2590,17 @@ impl SetAttr for PyType {
} }
let assign = value.is_assign(); let assign = value.is_assign();
// Drop old value OUTSIDE the type lock to avoid deadlock:
// dropping may trigger weakref callbacks → method calls →
// LOAD_ATTR specialization → version_for_specialization → type lock.
let _prev_value = Self::with_type_lock(vm, || {
// Invalidate inline caches before modifying attributes. // Invalidate inline caches before modifying attributes.
// This ensures other threads see the version invalidation before // This ensures other threads see the version invalidation before
// any attribute changes, preventing use-after-free of cached descriptors. // any attribute changes, preventing use-after-free of cached descriptors.
zelf.modified(); zelf.modified_inner();
if let PySetterValue::Assign(value) = value { if let PySetterValue::Assign(value) = value {
zelf.attributes.write().insert(attr_name, value); Ok(zelf.attributes.write().insert(attr_name, value))
} else { } else {
let prev_value = zelf.attributes.write().shift_remove(attr_name); // TODO: swap_remove applicable? let prev_value = zelf.attributes.write().shift_remove(attr_name); // TODO: swap_remove applicable?
if prev_value.is_none() { if prev_value.is_none() {
@@ -2478,7 +2610,9 @@ impl SetAttr for PyType {
attr_name, attr_name,
))); )));
} }
Ok(prev_value)
} }
})?;
if attr_name.as_wtf8().starts_with("__") && attr_name.as_wtf8().ends_with("__") { if attr_name.as_wtf8().starts_with("__") && attr_name.as_wtf8().ends_with("__") {
if assign { if assign {

View File

@@ -1051,7 +1051,7 @@ struct ExecutingFrame<'a> {
} }
#[inline] #[inline]
fn specialization_compact_int_value(i: &PyInt, vm: &VirtualMachine) -> Option<isize> { fn cpython_compact_int_value(i: &PyInt, vm: &VirtualMachine) -> Option<isize> {
// _PyLong_IsCompact(): a one-digit PyLong (base 2^30), // _PyLong_IsCompact(): a one-digit PyLong (base 2^30),
// i.e. abs(value) <= 2^30 - 1. // i.e. abs(value) <= 2^30 - 1.
const CPYTHON_COMPACT_LONG_ABS_MAX: i64 = (1i64 << 30) - 1; const CPYTHON_COMPACT_LONG_ABS_MAX: i64 = (1i64 << 30) - 1;
@@ -1066,7 +1066,7 @@ fn specialization_compact_int_value(i: &PyInt, vm: &VirtualMachine) -> Option<is
#[inline] #[inline]
fn compact_int_from_obj(obj: &PyObject, vm: &VirtualMachine) -> Option<isize> { fn compact_int_from_obj(obj: &PyObject, vm: &VirtualMachine) -> Option<isize> {
obj.downcast_ref_if_exact::<PyInt>(vm) obj.downcast_ref_if_exact::<PyInt>(vm)
.and_then(|i| specialization_compact_int_value(i, vm)) .and_then(|i| cpython_compact_int_value(i, vm))
} }
#[inline] #[inline]
@@ -1075,7 +1075,7 @@ fn exact_float_from_obj(obj: &PyObject, vm: &VirtualMachine) -> Option<f64> {
} }
#[inline] #[inline]
fn specialization_nonnegative_compact_index(i: &PyInt, vm: &VirtualMachine) -> Option<usize> { fn cpython_nonnegative_compact_index(i: &PyInt, vm: &VirtualMachine) -> Option<usize> {
// _PyLong_IsNonNegativeCompact(): a single base-2^30 digit. // _PyLong_IsNonNegativeCompact(): a single base-2^30 digit.
const CPYTHON_COMPACT_LONG_MAX: u64 = (1u64 << 30) - 1; const CPYTHON_COMPACT_LONG_MAX: u64 = (1u64 << 30) - 1;
let v = i.try_to_primitive::<u64>(vm).ok()?; let v = i.try_to_primitive::<u64>(vm).ok()?;
@@ -3999,7 +3999,7 @@ impl ExecutingFrame<'_> {
let value = self.pop_value(); let value = self.pop_value();
if let Some(list) = obj.downcast_ref_if_exact::<PyList>(vm) if let Some(list) = obj.downcast_ref_if_exact::<PyList>(vm)
&& let Some(int_idx) = idx.downcast_ref_if_exact::<PyInt>(vm) && let Some(int_idx) = idx.downcast_ref_if_exact::<PyInt>(vm)
&& let Some(i) = specialization_nonnegative_compact_index(int_idx, vm) && let Some(i) = cpython_nonnegative_compact_index(int_idx, vm)
{ {
let mut vec = list.borrow_vec_mut(); let mut vec = list.borrow_vec_mut();
if i < vec.len() { if i < vec.len() {
@@ -4099,7 +4099,7 @@ impl ExecutingFrame<'_> {
if let (Some(list), Some(idx)) = ( if let (Some(list), Some(idx)) = (
a.downcast_ref_if_exact::<PyList>(vm), a.downcast_ref_if_exact::<PyList>(vm),
b.downcast_ref_if_exact::<PyInt>(vm), b.downcast_ref_if_exact::<PyInt>(vm),
) && let Some(i) = specialization_nonnegative_compact_index(idx, vm) ) && let Some(i) = cpython_nonnegative_compact_index(idx, vm)
{ {
let vec = list.borrow_vec(); let vec = list.borrow_vec();
if i < vec.len() { if i < vec.len() {
@@ -4119,7 +4119,7 @@ impl ExecutingFrame<'_> {
if let (Some(tuple), Some(idx)) = ( if let (Some(tuple), Some(idx)) = (
a.downcast_ref_if_exact::<PyTuple>(vm), a.downcast_ref_if_exact::<PyTuple>(vm),
b.downcast_ref_if_exact::<PyInt>(vm), b.downcast_ref_if_exact::<PyInt>(vm),
) && let Some(i) = specialization_nonnegative_compact_index(idx, vm) ) && let Some(i) = cpython_nonnegative_compact_index(idx, vm)
{ {
let elements = tuple.as_slice(); let elements = tuple.as_slice();
if i < elements.len() { if i < elements.len() {
@@ -4161,7 +4161,7 @@ impl ExecutingFrame<'_> {
if let (Some(a_str), Some(b_int)) = ( if let (Some(a_str), Some(b_int)) = (
a.downcast_ref_if_exact::<PyStr>(vm), a.downcast_ref_if_exact::<PyStr>(vm),
b.downcast_ref_if_exact::<PyInt>(vm), b.downcast_ref_if_exact::<PyInt>(vm),
) && let Some(i) = specialization_nonnegative_compact_index(b_int, vm) ) && let Some(i) = cpython_nonnegative_compact_index(b_int, vm)
&& let Ok(ch) = a_str.getitem_by_index(vm, i as isize) && let Ok(ch) = a_str.getitem_by_index(vm, i as isize)
&& ch.is_ascii() && ch.is_ascii()
{ {
@@ -4774,9 +4774,6 @@ impl ExecutingFrame<'_> {
&& let Some(init_func) = cls.get_cached_init_for_specialization(cached_version) && let Some(init_func) = cls.get_cached_init_for_specialization(cached_version)
&& let Some(cls_alloc) = cls.slots.alloc.load() && let Some(cls_alloc) = cls.slots.alloc.load()
{ {
// Match CPython's `code->co_framesize + _Py_InitCleanup.co_framesize`
// shape, using RustPython's datastack-backed frame size
// equivalent for the extra shim frame.
let init_cleanup_stack_bytes = let init_cleanup_stack_bytes =
datastack_frame_size_bytes_for_code(&vm.ctx.init_cleanup_code) datastack_frame_size_bytes_for_code(&vm.ctx.init_cleanup_code)
.expect("_Py_InitCleanup shim is not a generator/coroutine"); .expect("_Py_InitCleanup shim is not a generator/coroutine");
@@ -4787,9 +4784,8 @@ impl ExecutingFrame<'_> {
) { ) {
return self.execute_call_vectorcall(nargs, vm); return self.execute_call_vectorcall(nargs, vm);
} }
// CPython creates `_Py_InitCleanup` + `__init__` frames here. // Two frames are created: `_Py_InitCleanup` + `__init__`.
// Keep the guard conservative and deopt when the effective // Guard recursion limit accordingly and fall back.
// recursion budget for those two frames is not available.
if self.specialization_call_recursion_guard_with_extra_frames(vm, 1) { if self.specialization_call_recursion_guard_with_extra_frames(vm, 1) {
return self.execute_call_vectorcall(nargs, vm); return self.execute_call_vectorcall(nargs, vm);
} }
@@ -5191,8 +5187,8 @@ impl ExecutingFrame<'_> {
a.downcast_ref_if_exact::<PyInt>(vm), a.downcast_ref_if_exact::<PyInt>(vm),
b.downcast_ref_if_exact::<PyInt>(vm), b.downcast_ref_if_exact::<PyInt>(vm),
) && let (Some(a_val), Some(b_val)) = ( ) && let (Some(a_val), Some(b_val)) = (
specialization_compact_int_value(a_int, vm), cpython_compact_int_value(a_int, vm),
specialization_compact_int_value(b_int, vm), cpython_compact_int_value(b_int, vm),
) { ) {
let op = self.compare_op_from_arg(arg); let op = self.compare_op_from_arg(arg);
let result = op.eval_ord(a_val.cmp(&b_val)); let result = op.eval_ord(a_val.cmp(&b_val));
@@ -7320,10 +7316,7 @@ impl ExecutingFrame<'_> {
.load() .load()
.is_some_and(|f| f as usize == PyBaseObject::getattro as *const () as usize); .is_some_and(|f| f as usize == PyBaseObject::getattro as *const () as usize);
if !is_default_getattro { if !is_default_getattro {
let mut type_version = cls.tp_version_tag.load(Acquire); let type_version = cls.version_for_specialization(_vm);
if type_version == 0 {
type_version = cls.assign_version_tag();
}
if type_version != 0 if type_version != 0
&& !oparg.is_method() && !oparg.is_method()
&& !self.specialization_eval_frame_active(_vm) && !self.specialization_eval_frame_active(_vm)
@@ -7361,10 +7354,7 @@ impl ExecutingFrame<'_> {
} }
// Get or assign type version // Get or assign type version
let mut type_version = cls.tp_version_tag.load(Acquire); let type_version = cls.version_for_specialization(_vm);
if type_version == 0 {
type_version = cls.assign_version_tag();
}
if type_version == 0 { if type_version == 0 {
// Version counter overflow — backoff to avoid re-attempting every execution // Version counter overflow — backoff to avoid re-attempting every execution
unsafe { unsafe {
@@ -7584,10 +7574,7 @@ impl ExecutingFrame<'_> {
let owner_type = obj.downcast_ref::<PyType>().unwrap(); let owner_type = obj.downcast_ref::<PyType>().unwrap();
// Get or assign type version for the type object itself // Get or assign type version for the type object itself
let mut type_version = owner_type.tp_version_tag.load(Acquire); let type_version = owner_type.version_for_specialization(_vm);
if type_version == 0 {
type_version = owner_type.assign_version_tag();
}
if type_version == 0 { if type_version == 0 {
unsafe { unsafe {
self.code.instructions.write_adaptive_counter( self.code.instructions.write_adaptive_counter(
@@ -7622,10 +7609,7 @@ impl ExecutingFrame<'_> {
} }
let mut metaclass_version = 0; let mut metaclass_version = 0;
if !mcl.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) { if !mcl.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
metaclass_version = mcl.tp_version_tag.load(Acquire); metaclass_version = mcl.version_for_specialization(_vm);
if metaclass_version == 0 {
metaclass_version = mcl.assign_version_tag();
}
if metaclass_version == 0 { if metaclass_version == 0 {
unsafe { unsafe {
self.code.instructions.write_adaptive_counter( self.code.instructions.write_adaptive_counter(
@@ -7797,7 +7781,7 @@ impl ExecutingFrame<'_> {
bytecode::BinaryOperator::Subscr => { bytecode::BinaryOperator::Subscr => {
let b_is_nonnegative_int = b let b_is_nonnegative_int = b
.downcast_ref_if_exact::<PyInt>(vm) .downcast_ref_if_exact::<PyInt>(vm)
.is_some_and(|i| specialization_nonnegative_compact_index(i, vm).is_some()); .is_some_and(|i| cpython_nonnegative_compact_index(i, vm).is_some());
if a.downcast_ref_if_exact::<PyList>(vm).is_some() && b_is_nonnegative_int { if a.downcast_ref_if_exact::<PyList>(vm).is_some() && b_is_nonnegative_int {
Some(Instruction::BinaryOpSubscrListInt) Some(Instruction::BinaryOpSubscrListInt)
} else if a.downcast_ref_if_exact::<PyTuple>(vm).is_some() && b_is_nonnegative_int { } else if a.downcast_ref_if_exact::<PyTuple>(vm).is_some() && b_is_nonnegative_int {
@@ -7812,16 +7796,14 @@ impl ExecutingFrame<'_> {
Some(Instruction::BinaryOpSubscrListSlice) Some(Instruction::BinaryOpSubscrListSlice)
} else { } else {
let cls = a.class(); let cls = a.class();
let (getitem, type_version) =
cls.lookup_ref_and_version_interned(identifier!(vm, __getitem__), vm);
if cls.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) if cls.slots.flags.has_feature(PyTypeFlags::HEAPTYPE)
&& !self.specialization_eval_frame_active(vm) && !self.specialization_eval_frame_active(vm)
&& let Some(_getitem) = cls.get_attr(identifier!(vm, __getitem__)) && let Some(_getitem) = getitem
&& let Some(func) = _getitem.downcast_ref_if_exact::<PyFunction>(vm) && let Some(func) = _getitem.downcast_ref_if_exact::<PyFunction>(vm)
&& func.can_specialize_call(2) && func.can_specialize_call(2)
{ {
let mut type_version = cls.tp_version_tag.load(Acquire);
if type_version == 0 {
type_version = cls.assign_version_tag();
}
if type_version != 0 { if type_version != 0 {
if cls.cache_getitem_for_specialization( if cls.cache_getitem_for_specialization(
func.to_owned(), func.to_owned(),
@@ -8360,11 +8342,8 @@ impl ExecutingFrame<'_> {
&& cls_new_fn as usize == obj_new_fn as usize && cls_new_fn as usize == obj_new_fn as usize
&& cls_alloc_fn as usize == obj_alloc_fn as usize && cls_alloc_fn as usize == obj_alloc_fn as usize
{ {
let init = cls.get_attr(identifier!(vm, __init__)); let (init, version) =
let mut version = cls.tp_version_tag.load(Acquire); cls.lookup_ref_and_version_interned(identifier!(vm, __init__), vm);
if version == 0 {
version = cls.assign_version_tag();
}
if version == 0 { if version == 0 {
unsafe { unsafe {
self.code.instructions.write_adaptive_counter( self.code.instructions.write_adaptive_counter(
@@ -8620,8 +8599,8 @@ impl ExecutingFrame<'_> {
a.downcast_ref_if_exact::<PyInt>(vm), a.downcast_ref_if_exact::<PyInt>(vm),
b.downcast_ref_if_exact::<PyInt>(vm), b.downcast_ref_if_exact::<PyInt>(vm),
) { ) {
if specialization_compact_int_value(a_int, vm).is_some() if cpython_compact_int_value(a_int, vm).is_some()
&& specialization_compact_int_value(b_int, vm).is_some() && cpython_compact_int_value(b_int, vm).is_some()
{ {
Some(Instruction::CompareOpInt) Some(Instruction::CompareOpInt)
} else { } else {
@@ -8684,10 +8663,7 @@ impl ExecutingFrame<'_> {
&& cls.slots.as_sequence.length.load().is_none() && cls.slots.as_sequence.length.load().is_none()
{ {
// Cache type version for ToBoolAlwaysTrue guard // Cache type version for ToBoolAlwaysTrue guard
let mut type_version = cls.tp_version_tag.load(Acquire); let type_version = cls.version_for_specialization(vm);
if type_version == 0 {
type_version = cls.assign_version_tag();
}
if type_version != 0 { if type_version != 0 {
unsafe { unsafe {
self.code self.code
@@ -8919,7 +8895,7 @@ impl ExecutingFrame<'_> {
idx.downcast_ref_if_exact::<PyInt>(vm), idx.downcast_ref_if_exact::<PyInt>(vm),
) { ) {
let list_len = list.borrow_vec().len(); let list_len = list.borrow_vec().len();
if specialization_nonnegative_compact_index(int_idx, vm).is_some_and(|i| i < list_len) { if cpython_nonnegative_compact_index(int_idx, vm).is_some_and(|i| i < list_len) {
Some(Instruction::StoreSubscrListInt) Some(Instruction::StoreSubscrListInt)
} else { } else {
None None
@@ -9025,10 +9001,7 @@ impl ExecutingFrame<'_> {
} }
// Get or assign type version // Get or assign type version
let mut type_version = cls.tp_version_tag.load(Acquire); let type_version = cls.version_for_specialization(vm);
if type_version == 0 {
type_version = cls.assign_version_tag();
}
if type_version == 0 { if type_version == 0 {
unsafe { unsafe {
self.code.instructions.write_adaptive_counter( self.code.instructions.write_adaptive_counter(

View File

@@ -840,6 +840,7 @@ pub mod module {
reinit_mutex_after_fork(&vm.state.atexit_funcs); reinit_mutex_after_fork(&vm.state.atexit_funcs);
reinit_mutex_after_fork(&vm.state.global_trace_func); reinit_mutex_after_fork(&vm.state.global_trace_func);
reinit_mutex_after_fork(&vm.state.global_profile_func); reinit_mutex_after_fork(&vm.state.global_profile_func);
reinit_mutex_after_fork(&vm.state.type_mutex);
reinit_mutex_after_fork(&vm.state.monitoring); reinit_mutex_after_fork(&vm.state.monitoring);
// PyGlobalState parking_lot::Mutex locks // PyGlobalState parking_lot::Mutex locks

View File

@@ -115,6 +115,7 @@ where
switch_interval: AtomicCell::new(0.005), switch_interval: AtomicCell::new(0.005),
global_trace_func: PyMutex::default(), global_trace_func: PyMutex::default(),
global_profile_func: PyMutex::default(), global_profile_func: PyMutex::default(),
type_mutex: PyMutex::default(),
#[cfg(feature = "threading")] #[cfg(feature = "threading")]
main_thread_ident: AtomicCell::new(0), main_thread_ident: AtomicCell::new(0),
#[cfg(feature = "threading")] #[cfg(feature = "threading")]

View File

@@ -599,6 +599,8 @@ pub struct PyGlobalState {
pub global_trace_func: PyMutex<Option<PyObjectRef>>, pub global_trace_func: PyMutex<Option<PyObjectRef>>,
/// Global profile function for all threads (set by sys._setprofileallthreads) /// Global profile function for all threads (set by sys._setprofileallthreads)
pub global_profile_func: PyMutex<Option<PyObjectRef>>, pub global_profile_func: PyMutex<Option<PyObjectRef>>,
/// Global type mutation/versioning mutex for CPython-style FT type operations.
pub type_mutex: PyMutex<()>,
/// Main thread identifier (pthread_self on Unix) /// Main thread identifier (pthread_self on Unix)
#[cfg(feature = "threading")] #[cfg(feature = "threading")]
pub main_thread_ident: AtomicCell<u64>, pub main_thread_ident: AtomicCell<u64>,

View File

@@ -38,6 +38,16 @@ from functools import reduce
from unittest.runner import registerResult, result from unittest.runner import registerResult, result
def _get_method_dict(test):
"""Get the __dict__ of the underlying function for a test method.
Works for both bound methods (__func__.__dict__) and plain functions.
"""
method = getattr(test, test._testMethodName)
func = getattr(method, '__func__', method)
return func.__dict__
class TablePrinter(object): class TablePrinter(object):
# Modified from https://github.com/agramian/table-printer, same license as above # Modified from https://github.com/agramian/table-printer, same license as above
"Print a list of dicts as a table" "Print a list of dicts as a table"
@@ -325,7 +335,7 @@ class CustomTextTestResult(result.TestResult):
self.stream.writeln("TEST SUITE: %s" % self.suite) self.stream.writeln("TEST SUITE: %s" % self.suite)
self.stream.writeln("Description: %s" % self.getSuiteDescription(test)) self.stream.writeln("Description: %s" % self.getSuiteDescription(test))
try: try:
name_override = getattr(test, test._testMethodName).__func__.__dict__[ name_override = _get_method_dict(test)[
"test_case_name" "test_case_name"
] ]
except: except:
@@ -382,30 +392,30 @@ class CustomTextTestResult(result.TestResult):
).__func__.__dict__ and set([s.lower() for s in self.test_types]) == set( ).__func__.__dict__ and set([s.lower() for s in self.test_types]) == set(
[ [
s.lower() s.lower()
for s in getattr(test, test._testMethodName).__func__.__dict__[ for s in _get_method_dict(test)[
"test_type" "test_type"
] ]
] ]
): ):
pass pass
else: else:
getattr(test, test._testMethodName).__func__.__dict__[ _get_method_dict(test)[
"__unittest_skip_why__" "__unittest_skip_why__"
] = 'Test run specified to only run tests of type "%s"' % ",".join( ] = 'Test run specified to only run tests of type "%s"' % ",".join(
self.test_types self.test_types
) )
getattr(test, test._testMethodName).__func__.__dict__[ _get_method_dict(test)[
"__unittest_skip__" "__unittest_skip__"
] = True ] = True
if "skip_device" in getattr(test, test._testMethodName).__func__.__dict__: if "skip_device" in _get_method_dict(test):
for device in getattr(test, test._testMethodName).__func__.__dict__[ for device in _get_method_dict(test)[
"skip_device" "skip_device"
]: ]:
if self.config and device.lower() in self.config["device_name"].lower(): if self.config and device.lower() in self.config["device_name"].lower():
getattr(test, test._testMethodName).__func__.__dict__[ _get_method_dict(test)[
"__unittest_skip_why__" "__unittest_skip_why__"
] = "Test is marked to be skipped on %s" % device ] = "Test is marked to be skipped on %s" % device
getattr(test, test._testMethodName).__func__.__dict__[ _get_method_dict(test)[
"__unittest_skip__" "__unittest_skip__"
] = True ] = True
break break