Skip to content

Commit 84b95a8

Browse files
youknowdotcodex
andauthored
Disable type caching for custom MRO hierarchies (#9477)
* Disable type caching for custom MRO hierarchies Track permanent version-tag ineligibility after custom MRO publication and bases changes, propagate it through version assignment, and retain existing MCACHE invalidation. Restore canonical metaclass doctests through update_lib. Assisted-by: OpenAI Codex: model version unavailable * Align custom MRO cache field with CPython naming Use tp_versions_used and the CPython ATTR_CACHE_UNUSED sentinel for permanent cache exclusion. Keep the existing zero/sentinel state behavior and atomic ordering; per-type version budgeting remains unimplemented. Assisted-by: OpenAI Codex: model version unavailable --------- Co-authored-by: Codex <[email protected]>
1 parent 9f8540c commit 84b95a8

3 files changed

Lines changed: 65 additions & 12 deletions

File tree

‎Lib/test/test_metaclass.py‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,17 +268,17 @@
268268
...
269269
>>> Base.value
270270
1
271-
>>> WeirdClass.value # TODO: RUSTPYTHON; AttributeError: type object 'WeirdClass' has no attribute 'value' # doctest: +SKIP
271+
>>> WeirdClass.value
272272
1
273273
>>> Base.value = 2
274274
>>> Base.value
275275
2
276-
>>> WeirdClass.value # TODO: RUSTPYTHON; AttributeError: type object 'WeirdClass' has no attribute 'value' # doctest: +SKIP
276+
>>> WeirdClass.value
277277
2
278278
>>> Base.value = 3
279279
>>> Base.value
280280
3
281-
>>> WeirdClass.value # TODO: RUSTPYTHON; AttributeError: type object 'WeirdClass' has no attribute 'value' # doctest: +SKIP
281+
>>> WeirdClass.value
282282
3
283283
284284
"""
@@ -292,8 +292,7 @@
292292
__test__ = {'doctests' : doctests}
293293

294294
def load_tests(loader, tests, pattern):
295-
from test.support.rustpython import DocTestChecker # TODO: RUSTPYTHON
296-
tests.addTest(doctest.DocTestSuite(checker=DocTestChecker()))
295+
tests.addTest(doctest.DocTestSuite())
297296
return tests
298297

299298

‎crates/vm/src/builtins/type.rs‎

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use core::{
3737
cell::Cell,
3838
ops::Deref,
3939
pin::Pin,
40-
sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering},
40+
sync::atomic::{AtomicBool, AtomicPtr, AtomicU16, AtomicU32, Ordering},
4141
};
4242
use indexmap::{IndexMap, map::Entry};
4343
use itertools::Itertools;
@@ -89,6 +89,9 @@ pub struct PyType {
8989
pub heaptype_ext: Option<Pin<Box<HeapTypeExt>>>,
9090
/// Type version tag for inline caching. 0 means unassigned/invalidated.
9191
pub tp_version_tag: AtomicU32,
92+
/// CPython-compatible permanent cache exclusion state. Currently only zero
93+
/// and ATTR_CACHE_UNUSED are used; per-type version budgeting is not implemented.
94+
pub(crate) tp_versions_used: AtomicU16,
9295
}
9396

9497
/// Monotonic counter for type version tags. Once it reaches `u32::MAX`,
@@ -97,6 +100,9 @@ pub struct PyType {
97100
/// generic path).
98101
static NEXT_TYPE_VERSION: AtomicU32 = AtomicU32::new(1);
99102

103+
// CPython _Py_ATTR_CACHE_UNUSED (Include/cpython/object.h).
104+
const ATTR_CACHE_UNUSED: u16 = 30_000;
105+
100106
const MANAGED_DICT_FLAGS: PyTypeFlags =
101107
PyTypeFlags::from_slice(&[PyTypeFlags::HAS_DICT, PyTypeFlags::MANAGED_DICT]);
102108

@@ -721,6 +727,9 @@ impl PyType {
721727
/// Assign a fresh version tag. Returns 0 if the version counter has been
722728
/// exhausted, in which case no new cache entries can be created.
723729
fn assign_version_tag_inner(&self) -> u32 {
730+
if self.tp_versions_used.load(Ordering::Acquire) >= ATTR_CACHE_UNUSED {
731+
return 0;
732+
}
724733
let v = self.tp_version_tag.load(Ordering::Acquire);
725734
if v != 0 {
726735
return v;
@@ -1135,6 +1144,7 @@ impl PyType {
11351144
slots,
11361145
heaptype_ext: Some(Pin::new(Box::new(heaptype_ext))),
11371146
tp_version_tag: AtomicU32::new(0),
1147+
tp_versions_used: AtomicU16::new(0),
11381148
},
11391149
metaclass,
11401150
None,
@@ -1198,6 +1208,7 @@ impl PyType {
11981208
slots,
11991209
heaptype_ext: None,
12001210
tp_version_tag: AtomicU32::new(0),
1211+
tp_versions_used: AtomicU16::new(0),
12011212
},
12021213
metaclass,
12031214
None,
@@ -2793,6 +2804,7 @@ impl Py<PyType> {
27932804
}
27942805
let old_mro = core::mem::replace(&mut *cls.mro.write(), new_mro);
27952806
undo.push((cls.to_owned(), old_mro));
2807+
type_mro_modified(cls, vm);
27962808
cls.modified_inner();
27972809
let subclasses: Vec<PyTypeRef> = cls
27982810
.subclasses
@@ -3785,15 +3797,53 @@ fn mro_invoke(typ: &Py<PyType>, vm: &VirtualMachine) -> PyResult<Vec<PyTypeRef>>
37853797
.collect()
37863798
}
37873799

3800+
// Called under the existing type lock after publishing an MRO. Match
3801+
// type_mro_modified() for both tp_mro and tp_bases: disabling is permanent,
3802+
// including when a later __bases__ update restores an ordinary hierarchy.
3803+
fn type_mro_modified(typ: &Py<PyType>, vm: &VirtualMachine) {
3804+
let custom_mro = !typ.class().is(vm.ctx.types.type_type)
3805+
&& match (
3806+
typ.class().find_name_in_mro_uncached(identifier!(vm, mro)),
3807+
vm.ctx
3808+
.types
3809+
.type_type
3810+
.find_name_in_mro_uncached(identifier!(vm, mro)),
3811+
) {
3812+
(Some(actual), Some(default)) => !actual.is(&default),
3813+
(None, None) => false,
3814+
_ => true,
3815+
};
3816+
let mro = typ.mro_collect();
3817+
let bases = typ.bases.read().clone();
3818+
let disabled = custom_mro
3819+
|| mro.iter().chain(bases.as_slice().iter()).any(|base| {
3820+
base.tp_versions_used.load(Ordering::Acquire) >= ATTR_CACHE_UNUSED
3821+
|| !is_subtype_with_mro(&mro, typ, base)
3822+
});
3823+
if disabled {
3824+
// Retire the old tag before disabling assignment: modified_inner()
3825+
// needs that tag to clear existing MCACHE entries and descendants.
3826+
typ.modified_inner();
3827+
if let Some(ext) = typ.heaptype_ext.as_ref() {
3828+
ext.specialization_cache.invalidate_for_type_modified();
3829+
}
3830+
typ.tp_versions_used
3831+
.store(ATTR_CACHE_UNUSED, Ordering::Release);
3832+
}
3833+
}
3834+
37883835
fn mro_internal(typ: &Py<PyType>, vm: &VirtualMachine) -> PyResult<i32> {
37893836
let old_ptr = typ.mro.read().as_ptr();
37903837
let new_mro = mro_invoke(typ, vm)?;
3791-
if typ.mro.read().as_ptr() != old_ptr {
3792-
return Ok(0);
3793-
}
3794-
*typ.mro.write() = new_mro;
3795-
typ.modified();
3796-
Ok(1)
3838+
PyType::with_type_lock(vm, || {
3839+
if typ.mro.read().as_ptr() != old_ptr {
3840+
return Ok(0);
3841+
}
3842+
*typ.mro.write() = new_mro;
3843+
type_mro_modified(typ, vm);
3844+
typ.modified_inner();
3845+
Ok(1)
3846+
})
37973847
}
37983848

37993849
/// Returns true if the two types have different instance layouts.

‎crates/vm/src/object/core.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3324,6 +3324,7 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy {
33243324
slots: PyType::make_slots(),
33253325
heaptype_ext: None,
33263326
tp_version_tag: core::sync::atomic::AtomicU32::new(0),
3327+
tp_versions_used: core::sync::atomic::AtomicU16::new(0),
33273328
};
33283329
let object_payload = PyType {
33293330
base: unsafe { PyAtomicRef::from_optional_ref_without_retag(None) },
@@ -3334,6 +3335,7 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy {
33343335
slots: object::PyBaseObject::make_slots(),
33353336
heaptype_ext: None,
33363337
tp_version_tag: core::sync::atomic::AtomicU32::new(0),
3338+
tp_versions_used: core::sync::atomic::AtomicU16::new(0),
33373339
};
33383340
let tuple_payload = PyType {
33393341
base: unsafe {
@@ -3348,6 +3350,7 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy {
33483350
slots: tuple::PyTuple::make_slots(),
33493351
heaptype_ext: None,
33503352
tp_version_tag: core::sync::atomic::AtomicU32::new(0),
3353+
tp_versions_used: core::sync::atomic::AtomicU16::new(0),
33513354
};
33523355

33533356
let object_element =
@@ -3395,6 +3398,7 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy {
33953398
slots: PyWeak::make_slots(),
33963399
heaptype_ext: None,
33973400
tp_version_tag: core::sync::atomic::AtomicU32::new(0),
3401+
tp_versions_used: core::sync::atomic::AtomicU16::new(0),
33983402
};
33993403
let weakref_type = PyRef::new_ref(weakref_payload, type_type.clone(), None);
34003404
// Static type: untrack from GC (was tracked by new_ref because PyType has HAS_TRAVERSE)

0 commit comments

Comments
 (0)