Skip to main content

sim_lib_lang_python/
objects.rs

1//! Python object policy over the language-neutral property store.
2
3use 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// conformance: Python object policy checks classes, descriptors, methods, and super.
18
19/// Value stored by the checked object model.
20#[derive(Clone, Debug, Default, Eq, PartialEq)]
21pub enum PythonObjectValue {
22    /// Python `None`.
23    #[default]
24    None,
25    /// A bounded integer specimen value.
26    Int(i64),
27    /// Text.
28    String(String),
29    /// An object identity.
30    Object(u64),
31    /// A function identity.
32    Function(u64),
33    /// A receiver-bound function.
34    BoundMethod {
35        /// Underlying function identity.
36        function: u64,
37        /// Bound instance identity.
38        receiver: u64,
39    },
40}
41
42/// Hook token used to prove descriptor receiver behavior.
43#[derive(Clone, Debug, Default, Eq, PartialEq)]
44pub struct DescriptorHook {
45    /// Stable hook name.
46    pub name: String,
47    /// Value produced by a getter.
48    pub value: PythonObjectValue,
49}
50
51/// A declared Python class and its C3-linearized bases.
52#[derive(Clone, Debug)]
53pub struct PythonClass {
54    /// Runtime class identity. Python never substitutes an integer surrogate.
55    pub identity: ClassRef,
56    /// Display name.
57    pub name: String,
58    /// Direct bases in declaration order.
59    pub descriptor: ClassDescriptor,
60    /// C3 method resolution order, including this class.
61    pub mro: Vec<ClassRef>,
62    cache_root: ClassRoot,
63}
64
65/// Failure to construct a consistent class hierarchy.
66#[derive(Clone, Debug, Eq, PartialEq)]
67pub enum ClassError {
68    /// A base class is unknown.
69    UnknownBase(ClassId),
70    /// The supplied value is not a class or its descriptor is malformed.
71    InvalidClass,
72    /// The requested bases have no consistent C3 linearization.
73    InconsistentMro,
74}
75
76/// Checked attribute failure.
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum AttributeError {
79    /// The object identity is unknown.
80    UnknownObject(u64),
81    /// The class identity is unknown.
82    UnknownClass(u64),
83    /// No attribute was found.
84    Missing(String),
85    /// Shared descriptor traversal failed.
86    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
118/// Python class and instance policy using shared property mechanics.
119pub 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    /// Declare a class and compute its C3 MRO.
188    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    /// Allocate an instance of a known class.
270    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    /// Return a declared class.
284    pub fn class(&self, id: ClassId) -> Option<&PythonClass> {
285        self.classes.get(&id)
286    }
287
288    /// Releases a declared class root so its shared derived MRO can be reclaimed.
289    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    /// Collects unreachable class descriptors and ephemeron-owned derived MRO values.
300    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    /// Define a plain instance or class attribute.
308    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    /// Define a Python data or non-data descriptor on a class.
324    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    /// Resolve an attribute with Python data-descriptor, instance, non-data,
349    /// and class-value precedence.
350    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    /// Resolve as `super(Current, object)`, starting after `Current` in the MRO.
387    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}