1use sim_kernel::{
4 ClassId, ClassRef, Cx, Expr, MatchScore, Shape, ShapeDoc, ShapeMatch, ShapeRef, Value,
5};
6use sim_lib_class::{
7 C3Policy, CacheError, ClassCache, ClassDescriptor, ClassDescriptorInput, ClassIdentity,
8 ClassRoot, DeclaredParent, LineageBudget,
9};
10use sim_lib_dispatch::{
11 AccessContext, AccessError, AccessorDescriptor, DataDescriptor, Descriptor, PropertyHook,
12 PropertyStore,
13};
14use sim_lib_gc_tracing::{CollectionLimits, CollectionReceipt};
15use std::{collections::BTreeMap, sync::Arc};
16
17#[derive(Clone, Debug, Default, Eq, PartialEq)]
21pub enum PythonObjectValue {
22 #[default]
24 None,
25 Int(i64),
27 String(String),
29 Object(u64),
31 Function(u64),
33 BoundMethod {
35 function: u64,
37 receiver: u64,
39 },
40}
41
42#[derive(Clone, Debug, Default, Eq, PartialEq)]
44pub struct DescriptorHook {
45 pub name: String,
47 pub value: PythonObjectValue,
49}
50
51#[derive(Clone, Debug)]
53pub struct PythonClass {
54 pub identity: ClassRef,
56 pub name: String,
58 pub descriptor: ClassDescriptor,
60 pub mro: Vec<ClassRef>,
62 cache_root: ClassRoot,
63}
64
65#[derive(Clone, Debug, Eq, PartialEq)]
67pub enum ClassError {
68 UnknownBase(ClassId),
70 InvalidClass,
72 InconsistentMro,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum AttributeError {
79 UnknownObject(u64),
81 UnknownClass(u64),
83 Missing(String),
85 Access,
87}
88
89#[derive(Default)]
90struct Hooks {
91 seen_receivers: Vec<u64>,
92}
93impl PropertyHook<u64, String, PythonObjectValue, DescriptorHook> for Hooks {
94 type Error = ();
95 fn get(
96 &mut self,
97 _: &mut AccessContext<u64, String>,
98 hook: &DescriptorHook,
99 receiver: &u64,
100 _: &String,
101 ) -> Result<PythonObjectValue, AccessError<Self::Error>> {
102 self.seen_receivers.push(*receiver);
103 Ok(hook.value.clone())
104 }
105 fn set(
106 &mut self,
107 _: &mut AccessContext<u64, String>,
108 _: &DescriptorHook,
109 receiver: &u64,
110 _: &String,
111 _: PythonObjectValue,
112 ) -> Result<(), AccessError<Self::Error>> {
113 self.seen_receivers.push(*receiver);
114 Ok(())
115 }
116}
117
118pub struct PythonObjectSpace {
120 classes: BTreeMap<ClassId, PythonClass>,
121 cache_ids: BTreeMap<sim_lib_mutation::ManagedId, ClassId>,
122 cache: ClassCache<()>,
123 instances: BTreeMap<u64, ClassId>,
124 properties: PropertyStore<u64, String, PythonObjectValue, DescriptorHook>,
125 hooks: Hooks,
126}
127
128impl Default for PythonObjectSpace {
129 fn default() -> Self {
130 Self {
131 classes: BTreeMap::new(),
132 cache_ids: BTreeMap::new(),
133 cache: ClassCache::new(4096).expect("fixed positive Python class-cache capacity"),
134 instances: BTreeMap::new(),
135 properties: PropertyStore::default(),
136 hooks: Hooks::default(),
137 }
138 }
139}
140
141struct PythonAnyShape;
142impl Shape for PythonAnyShape {
143 fn check_value(&self, _: &mut Cx, _: Value) -> sim_kernel::Result<ShapeMatch> {
144 Ok(ShapeMatch::accept(MatchScore::exact(1)))
145 }
146 fn check_expr(&self, _: &mut Cx, _: &Expr) -> sim_kernel::Result<ShapeMatch> {
147 Ok(ShapeMatch::accept(MatchScore::exact(1)))
148 }
149 fn describe(&self, _: &mut Cx) -> sim_kernel::Result<ShapeDoc> {
150 Ok(ShapeDoc::new("python-object"))
151 }
152}
153
154impl PythonObjectSpace {
155 pub(crate) fn is_subclass(&self, class: ClassId, candidate: ClassId) -> bool {
156 self.classes.get(&class).is_some_and(|declared| {
157 declared.mro.iter().any(|entry| {
158 entry
159 .object()
160 .as_class()
161 .is_some_and(|entry| entry.id() == candidate)
162 })
163 })
164 }
165
166 pub(crate) fn subclass_work(&self, class: ClassId, candidate: ClassId, limit: usize) -> usize {
167 let Some(declared) = self.classes.get(&class) else {
168 return 0;
169 };
170 let required = declared
171 .mro
172 .iter()
173 .position(|entry| {
174 entry
175 .object()
176 .as_class()
177 .is_some_and(|entry| entry.id() == candidate)
178 })
179 .map_or(declared.mro.len(), |at| at + 1);
180 if required > limit {
181 limit.saturating_add(1)
182 } else {
183 required
184 }
185 }
186
187 pub fn define_class(
189 &mut self,
190 cx: &Cx,
191 identity: ClassRef,
192 bases: Vec<ClassRef>,
193 ) -> Result<(), ClassError> {
194 let class = identity
195 .object()
196 .as_class()
197 .ok_or(ClassError::InvalidClass)?;
198 let id = class.id();
199 let name = class.symbol().name.to_string();
200 let mut parent_roots = Vec::with_capacity(bases.len());
201 let mut parents = Vec::with_capacity(bases.len());
202 for base in &bases {
203 let parent = base.object().as_class().ok_or(ClassError::InvalidClass)?;
204 let stored = self
205 .classes
206 .get(&parent.id())
207 .ok_or(ClassError::UnknownBase(parent.id()))?;
208 parent_roots.push(stored.cache_root);
209 parents.push(DeclaredParent::resolved(
210 ClassIdentity::checked(parent.id(), parent.symbol().clone())
211 .map_err(|_| ClassError::InvalidClass)?,
212 base.clone(),
213 ));
214 }
215 let shape: ShapeRef = cx
216 .factory()
217 .opaque(Arc::new(PythonAnyShape))
218 .map_err(|_| ClassError::InvalidClass)?;
219 let descriptor = ClassDescriptor::new(ClassDescriptorInput {
220 identity: ClassIdentity::checked(id, class.symbol().clone())
221 .map_err(|_| ClassError::InvalidClass)?,
222 parents,
223 constructor_shape: shape.clone(),
224 instance_shape: shape,
225 members: Vec::new(),
226 read_construction: None,
227 metadata: Vec::new(),
228 })
229 .map_err(|_| ClassError::InconsistentMro)?;
230 let cache_root = self
231 .cache
232 .allocate_class(&parent_roots, Vec::new())
233 .map_err(map_cache_error)?;
234 self.cache_ids.insert(cache_root.id(), id);
235 let derived = match self.cache.derived(cache_root, &C3Policy, lineage_budget()) {
236 Ok(derived) => derived,
237 Err(error) => {
238 self.cache_ids.remove(&cache_root.id());
239 self.cache.release(cache_root).map_err(map_cache_error)?;
240 return Err(map_cache_error(error));
241 }
242 };
243 let mro = derived
244 .view
245 .linearization
246 .iter()
247 .map(|managed| {
248 let class_id = self.cache_ids[managed];
249 if class_id == id {
250 identity.clone()
251 } else {
252 self.classes[&class_id].identity.clone()
253 }
254 })
255 .collect();
256 self.classes.insert(
257 id,
258 PythonClass {
259 identity,
260 name,
261 descriptor,
262 mro,
263 cache_root,
264 },
265 );
266 Ok(())
267 }
268
269 pub fn instantiate(&mut self, object: u64, class: ClassRef) -> Result<(), AttributeError> {
271 let id = class
272 .object()
273 .as_class()
274 .ok_or(AttributeError::UnknownClass(u64::MAX))?
275 .id();
276 if !self.classes.contains_key(&id) {
277 return Err(AttributeError::UnknownClass(u64::from(id.0)));
278 }
279 self.instances.insert(object, id);
280 Ok(())
281 }
282
283 pub fn class(&self, id: ClassId) -> Option<&PythonClass> {
285 self.classes.get(&id)
286 }
287
288 pub fn release_class(&mut self, id: ClassId) -> Result<(), ClassError> {
290 let class = self
291 .classes
292 .remove(&id)
293 .ok_or(ClassError::UnknownBase(id))?;
294 self.cache
295 .release(class.cache_root)
296 .map_err(map_cache_error)
297 }
298
299 pub fn collect_classes(
301 &mut self,
302 limits: CollectionLimits,
303 ) -> Result<CollectionReceipt, ClassError> {
304 self.cache.collect(limits).map_err(map_cache_error)
305 }
306
307 pub fn define_value(&mut self, owner: u64, key: impl Into<String>, value: PythonObjectValue) {
309 self.properties
310 .define(
311 &owner,
312 key.into(),
313 Descriptor::Data(DataDescriptor {
314 value,
315 writable: true,
316 enumerable: true,
317 configurable: true,
318 }),
319 )
320 .expect("configurable Python value accepts replacement");
321 }
322
323 pub fn define_descriptor(
325 &mut self,
326 class: u64,
327 key: impl Into<String>,
328 getter: DescriptorHook,
329 data: bool,
330 ) {
331 self.properties
332 .define(
333 &class,
334 key.into(),
335 Descriptor::Accessor(AccessorDescriptor {
336 get: Some(getter),
337 set: data.then(|| DescriptorHook {
338 name: "set".into(),
339 value: PythonObjectValue::None,
340 }),
341 enumerable: true,
342 configurable: true,
343 }),
344 )
345 .expect("configurable Python descriptor accepts replacement");
346 }
347
348 pub fn get(&mut self, object: u64, key: &str) -> Result<PythonObjectValue, AttributeError> {
351 let class = *self
352 .instances
353 .get(&object)
354 .ok_or(AttributeError::UnknownObject(object))?;
355 let mro = self
356 .classes
357 .get(&class)
358 .ok_or(AttributeError::UnknownClass(u64::from(class.0)))?
359 .mro
360 .iter()
361 .map(class_id)
362 .collect::<Result<Vec<_>, _>>()?;
363 let key = key.to_owned();
364 let descriptor_owner = mro
365 .iter()
366 .find(|owner| self.properties.own(owner, &key).is_some())
367 .copied();
368 if let Some(owner) = descriptor_owner
369 && matches!(self.properties.own(&owner, &key), Some(Descriptor::Accessor(a)) if a.set.is_some())
370 {
371 return self.read(&[owner], object, &key);
372 }
373 if self.properties.own(&object, &key).is_some() {
374 return self.read(&[object], object, &key);
375 }
376 let value = self.read(&mro, object, &key)?;
377 Ok(match value {
378 PythonObjectValue::Function(function) => PythonObjectValue::BoundMethod {
379 function,
380 receiver: object,
381 },
382 other => other,
383 })
384 }
385
386 pub fn get_super(
388 &mut self,
389 current: u64,
390 object: u64,
391 key: &str,
392 ) -> Result<PythonObjectValue, AttributeError> {
393 let class = *self
394 .instances
395 .get(&object)
396 .ok_or(AttributeError::UnknownObject(object))?;
397 let mro = self
398 .classes
399 .get(&class)
400 .ok_or(AttributeError::UnknownClass(u64::from(class.0)))?
401 .mro
402 .iter()
403 .map(class_id)
404 .collect::<Result<Vec<_>, _>>()?;
405 let at = mro
406 .iter()
407 .position(|candidate| *candidate == current)
408 .ok_or(AttributeError::UnknownClass(current))?;
409 let owners = mro[at + 1..].to_vec();
410 let value = self.read(&owners, object, &key.to_owned())?;
411 Ok(match value {
412 PythonObjectValue::Function(function) => PythonObjectValue::BoundMethod {
413 function,
414 receiver: object,
415 },
416 other => other,
417 })
418 }
419
420 fn read(
421 &mut self,
422 owners: &[u64],
423 receiver: u64,
424 key: &String,
425 ) -> Result<PythonObjectValue, AttributeError> {
426 self.properties
427 .get(
428 owners,
429 &receiver,
430 key,
431 &mut AccessContext::new(64),
432 &mut self.hooks,
433 )
434 .map_err(|_| AttributeError::Access)?
435 .ok_or_else(|| AttributeError::Missing(key.clone()))
436 }
437}
438
439fn lineage_budget() -> LineageBudget {
440 LineageBudget {
441 nodes: 4096,
442 work: 1_000_000,
443 }
444}
445fn map_cache_error(error: CacheError) -> ClassError {
446 match error {
447 CacheError::Lineage(_) => ClassError::InconsistentMro,
448 _ => ClassError::InvalidClass,
449 }
450}
451fn class_id(class: &ClassRef) -> Result<u64, AttributeError> {
452 class
453 .object()
454 .as_class()
455 .map(|class| u64::from(class.id().0))
456 .ok_or(AttributeError::Access)
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use sim_kernel::Symbol;
463 fn class(cx: &Cx, id: u32, name: &str) -> ClassRef {
464 cx.factory()
465 .class_stub(ClassId(id), Symbol::qualified("python", name))
466 .unwrap()
467 }
468 fn value(value: i64) -> PythonObjectValue {
469 PythonObjectValue::Int(value)
470 }
471 #[test]
472 fn c3_descriptors_binding_and_super_share_property_mechanics() {
473 let mut space = PythonObjectSpace::default();
474 let cx = sim_kernel::testing::bare_cx();
475 let object = class(&cx, 1, "object");
476 let left = class(&cx, 2, "Left");
477 let right = class(&cx, 3, "Right");
478 let diamond = class(&cx, 4, "Diamond");
479 space.define_class(&cx, object.clone(), vec![]).unwrap();
480 space
481 .define_class(&cx, left.clone(), vec![object.clone()])
482 .unwrap();
483 space
484 .define_class(&cx, right.clone(), vec![object.clone()])
485 .unwrap();
486 space
487 .define_class(&cx, diamond.clone(), vec![left.clone(), right.clone()])
488 .unwrap();
489 assert_eq!(
490 space.class(ClassId(4)).unwrap().mro,
491 vec![diamond.clone(), left, right, object]
492 );
493 space.instantiate(10, diamond).unwrap();
494 space.define_descriptor(
495 2,
496 "data",
497 DescriptorHook {
498 name: "data".into(),
499 value: value(1),
500 },
501 true,
502 );
503 space.define_descriptor(
504 2,
505 "nondata",
506 DescriptorHook {
507 name: "nondata".into(),
508 value: value(2),
509 },
510 false,
511 );
512 space.define_value(10, "data", value(11));
513 space.define_value(10, "nondata", value(22));
514 assert_eq!(space.get(10, "data"), Ok(value(1)));
515 assert_eq!(space.get(10, "nondata"), Ok(value(22)));
516 space.define_value(3, "method", PythonObjectValue::Function(7));
517 assert_eq!(
518 space.get_super(2, 10, "method"),
519 Ok(PythonObjectValue::BoundMethod {
520 function: 7,
521 receiver: 10
522 })
523 );
524 assert_eq!(space.hooks.seen_receivers, vec![10]);
525 }
526
527 #[test]
528 fn inconsistent_c3_fails_explicitly() {
529 let mut space = PythonObjectSpace::default();
530 let cx = sim_kernel::testing::bare_cx();
531 let object = class(&cx, 1, "object");
532 let a = class(&cx, 2, "A");
533 let b = class(&cx, 3, "B");
534 let ab = class(&cx, 4, "AB");
535 let ba = class(&cx, 5, "BA");
536 space.define_class(&cx, object.clone(), vec![]).unwrap();
537 space
538 .define_class(&cx, a.clone(), vec![object.clone()])
539 .unwrap();
540 space.define_class(&cx, b.clone(), vec![object]).unwrap();
541 space
542 .define_class(&cx, ab.clone(), vec![a.clone(), b.clone()])
543 .unwrap();
544 space.define_class(&cx, ba.clone(), vec![b, a]).unwrap();
545 assert_eq!(
546 space.define_class(&cx, class(&cx, 6, "Impossible"), vec![ab, ba]),
547 Err(ClassError::InconsistentMro)
548 );
549
550 let cyclic = class(&cx, 7, "Cyclic");
551 assert_eq!(
552 space.define_class(&cx, cyclic.clone(), vec![cyclic]),
553 Err(ClassError::UnknownBase(ClassId(7)))
554 );
555 }
556
557 #[test]
558 fn unreachable_python_class_reclaims_ephemeron_owned_mro() {
559 let cx = sim_kernel::testing::bare_cx();
560 let mut space = PythonObjectSpace::default();
561 let class = class(&cx, 20, "Temporary");
562 space.define_class(&cx, class, vec![]).unwrap();
563 space.release_class(ClassId(20)).unwrap();
564 let receipt = space
565 .collect_classes(CollectionLimits {
566 objects: 16,
567 edges: 16,
568 stack: 16,
569 work: 128,
570 clears: 16,
571 finalizers: 0,
572 })
573 .unwrap();
574 assert_eq!(receipt.cleared_ephemerons.len(), 1);
575 assert_eq!(space.cache.managed_len(), 1);
576 }
577}