Skip to main content

sim_lib_lang_python/
objects.rs

1//! Python object policy over the language-neutral property store.
2
3use sim_lib_dispatch::{
4    AccessContext, AccessError, AccessorDescriptor, DataDescriptor, Descriptor, PropertyHook,
5    PropertyStore,
6};
7use std::collections::{BTreeMap, BTreeSet};
8
9// conformance: Python object policy checks classes, descriptors, methods, and super.
10
11/// Value stored by the checked object model.
12#[derive(Clone, Debug, Default, Eq, PartialEq)]
13pub enum PythonObjectValue {
14    /// Python `None`.
15    #[default]
16    None,
17    /// A bounded integer specimen value.
18    Int(i64),
19    /// Text.
20    String(String),
21    /// An object identity.
22    Object(u64),
23    /// A function identity.
24    Function(u64),
25    /// A receiver-bound function.
26    BoundMethod {
27        /// Underlying function identity.
28        function: u64,
29        /// Bound instance identity.
30        receiver: u64,
31    },
32}
33
34/// Hook token used to prove descriptor receiver behavior.
35#[derive(Clone, Debug, Default, Eq, PartialEq)]
36pub struct DescriptorHook {
37    /// Stable hook name.
38    pub name: String,
39    /// Value produced by a getter.
40    pub value: PythonObjectValue,
41}
42
43/// A declared Python class and its C3-linearized bases.
44#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct PythonClass {
46    /// Stable class identity.
47    pub id: u64,
48    /// Display name.
49    pub name: String,
50    /// Direct bases in declaration order.
51    pub bases: Vec<u64>,
52    /// C3 method resolution order, including this class.
53    pub mro: Vec<u64>,
54}
55
56/// Failure to construct a consistent class hierarchy.
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub enum ClassError {
59    /// A base class is unknown.
60    UnknownBase(u64),
61    /// The requested bases have no consistent C3 linearization.
62    InconsistentMro,
63}
64
65/// Checked attribute failure.
66#[derive(Clone, Debug, Eq, PartialEq)]
67pub enum AttributeError {
68    /// The object identity is unknown.
69    UnknownObject(u64),
70    /// The class identity is unknown.
71    UnknownClass(u64),
72    /// No attribute was found.
73    Missing(String),
74    /// Shared descriptor traversal failed.
75    Access,
76}
77
78#[derive(Default)]
79struct Hooks {
80    seen_receivers: Vec<u64>,
81}
82impl PropertyHook<u64, String, PythonObjectValue, DescriptorHook> for Hooks {
83    type Error = ();
84    fn get(
85        &mut self,
86        _: &mut AccessContext<u64, String>,
87        hook: &DescriptorHook,
88        receiver: &u64,
89        _: &String,
90    ) -> Result<PythonObjectValue, AccessError<Self::Error>> {
91        self.seen_receivers.push(*receiver);
92        Ok(hook.value.clone())
93    }
94    fn set(
95        &mut self,
96        _: &mut AccessContext<u64, String>,
97        _: &DescriptorHook,
98        receiver: &u64,
99        _: &String,
100        _: PythonObjectValue,
101    ) -> Result<(), AccessError<Self::Error>> {
102        self.seen_receivers.push(*receiver);
103        Ok(())
104    }
105}
106
107/// Python class and instance policy using shared property mechanics.
108#[derive(Default)]
109pub struct PythonObjectSpace {
110    classes: BTreeMap<u64, PythonClass>,
111    instances: BTreeMap<u64, u64>,
112    properties: PropertyStore<u64, String, PythonObjectValue, DescriptorHook>,
113    hooks: Hooks,
114}
115
116impl PythonObjectSpace {
117    /// Declare a class and compute its C3 MRO.
118    pub fn define_class(
119        &mut self,
120        id: u64,
121        name: impl Into<String>,
122        bases: Vec<u64>,
123    ) -> Result<(), ClassError> {
124        let mut sequences = Vec::new();
125        for base in &bases {
126            sequences.push(
127                self.classes
128                    .get(base)
129                    .ok_or(ClassError::UnknownBase(*base))?
130                    .mro
131                    .clone(),
132            );
133        }
134        sequences.push(bases.clone());
135        let mut mro = vec![id];
136        mro.extend(c3_merge(sequences)?);
137        self.classes.insert(
138            id,
139            PythonClass {
140                id,
141                name: name.into(),
142                bases,
143                mro,
144            },
145        );
146        Ok(())
147    }
148
149    /// Allocate an instance of a known class.
150    pub fn instantiate(&mut self, object: u64, class: u64) -> Result<(), AttributeError> {
151        if !self.classes.contains_key(&class) {
152            return Err(AttributeError::UnknownClass(class));
153        }
154        self.instances.insert(object, class);
155        Ok(())
156    }
157
158    /// Return a declared class.
159    pub fn class(&self, id: u64) -> Option<&PythonClass> {
160        self.classes.get(&id)
161    }
162
163    /// Define a plain instance or class attribute.
164    pub fn define_value(&mut self, owner: u64, key: impl Into<String>, value: PythonObjectValue) {
165        self.properties
166            .define(
167                &owner,
168                key.into(),
169                Descriptor::Data(DataDescriptor {
170                    value,
171                    writable: true,
172                    enumerable: true,
173                    configurable: true,
174                }),
175            )
176            .expect("configurable Python value accepts replacement");
177    }
178
179    /// Define a Python data or non-data descriptor on a class.
180    pub fn define_descriptor(
181        &mut self,
182        class: u64,
183        key: impl Into<String>,
184        getter: DescriptorHook,
185        data: bool,
186    ) {
187        self.properties
188            .define(
189                &class,
190                key.into(),
191                Descriptor::Accessor(AccessorDescriptor {
192                    get: Some(getter),
193                    set: data.then(|| DescriptorHook {
194                        name: "set".into(),
195                        value: PythonObjectValue::None,
196                    }),
197                    enumerable: true,
198                    configurable: true,
199                }),
200            )
201            .expect("configurable Python descriptor accepts replacement");
202    }
203
204    /// Resolve an attribute with Python data-descriptor, instance, non-data,
205    /// and class-value precedence.
206    pub fn get(&mut self, object: u64, key: &str) -> Result<PythonObjectValue, AttributeError> {
207        let class = *self
208            .instances
209            .get(&object)
210            .ok_or(AttributeError::UnknownObject(object))?;
211        let mro = self
212            .classes
213            .get(&class)
214            .ok_or(AttributeError::UnknownClass(class))?
215            .mro
216            .clone();
217        let key = key.to_owned();
218        let descriptor_owner = mro
219            .iter()
220            .find(|owner| self.properties.own(owner, &key).is_some())
221            .copied();
222        if let Some(owner) = descriptor_owner
223            && matches!(self.properties.own(&owner, &key), Some(Descriptor::Accessor(a)) if a.set.is_some())
224        {
225            return self.read(&[owner], object, &key);
226        }
227        if self.properties.own(&object, &key).is_some() {
228            return self.read(&[object], object, &key);
229        }
230        let value = self.read(&mro, object, &key)?;
231        Ok(match value {
232            PythonObjectValue::Function(function) => PythonObjectValue::BoundMethod {
233                function,
234                receiver: object,
235            },
236            other => other,
237        })
238    }
239
240    /// Resolve as `super(Current, object)`, starting after `Current` in the MRO.
241    pub fn get_super(
242        &mut self,
243        current: u64,
244        object: u64,
245        key: &str,
246    ) -> Result<PythonObjectValue, AttributeError> {
247        let class = *self
248            .instances
249            .get(&object)
250            .ok_or(AttributeError::UnknownObject(object))?;
251        let mro = &self
252            .classes
253            .get(&class)
254            .ok_or(AttributeError::UnknownClass(class))?
255            .mro;
256        let at = mro
257            .iter()
258            .position(|candidate| *candidate == current)
259            .ok_or(AttributeError::UnknownClass(current))?;
260        let owners = mro[at + 1..].to_vec();
261        let value = self.read(&owners, object, &key.to_owned())?;
262        Ok(match value {
263            PythonObjectValue::Function(function) => PythonObjectValue::BoundMethod {
264                function,
265                receiver: object,
266            },
267            other => other,
268        })
269    }
270
271    fn read(
272        &mut self,
273        owners: &[u64],
274        receiver: u64,
275        key: &String,
276    ) -> Result<PythonObjectValue, AttributeError> {
277        self.properties
278            .get(
279                owners,
280                &receiver,
281                key,
282                &mut AccessContext::new(64),
283                &mut self.hooks,
284            )
285            .map_err(|_| AttributeError::Access)?
286            .ok_or_else(|| AttributeError::Missing(key.clone()))
287    }
288}
289
290fn c3_merge(mut sequences: Vec<Vec<u64>>) -> Result<Vec<u64>, ClassError> {
291    let mut result = Vec::new();
292    loop {
293        sequences.retain(|sequence| !sequence.is_empty());
294        if sequences.is_empty() {
295            return Ok(result);
296        }
297        let candidate = sequences
298            .iter()
299            .map(|sequence| sequence[0])
300            .find(|candidate| {
301                sequences
302                    .iter()
303                    .all(|sequence| !sequence[1..].contains(candidate))
304            })
305            .ok_or(ClassError::InconsistentMro)?;
306        result.push(candidate);
307        for sequence in &mut sequences {
308            if sequence.first() == Some(&candidate) {
309                sequence.remove(0);
310            }
311        }
312        let unique: BTreeSet<_> = result.iter().copied().collect();
313        if unique.len() != result.len() {
314            return Err(ClassError::InconsistentMro);
315        }
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    fn value(value: i64) -> PythonObjectValue {
323        PythonObjectValue::Int(value)
324    }
325    #[test]
326    fn c3_descriptors_binding_and_super_share_property_mechanics() {
327        let mut space = PythonObjectSpace::default();
328        space.define_class(1, "object", vec![]).unwrap();
329        space.define_class(2, "Left", vec![1]).unwrap();
330        space.define_class(3, "Right", vec![1]).unwrap();
331        space.define_class(4, "Diamond", vec![2, 3]).unwrap();
332        assert_eq!(space.class(4).unwrap().mro, vec![4, 2, 3, 1]);
333        space.instantiate(10, 4).unwrap();
334        space.define_descriptor(
335            2,
336            "data",
337            DescriptorHook {
338                name: "data".into(),
339                value: value(1),
340            },
341            true,
342        );
343        space.define_descriptor(
344            2,
345            "nondata",
346            DescriptorHook {
347                name: "nondata".into(),
348                value: value(2),
349            },
350            false,
351        );
352        space.define_value(10, "data", value(11));
353        space.define_value(10, "nondata", value(22));
354        assert_eq!(space.get(10, "data"), Ok(value(1)));
355        assert_eq!(space.get(10, "nondata"), Ok(value(22)));
356        space.define_value(3, "method", PythonObjectValue::Function(7));
357        assert_eq!(
358            space.get_super(2, 10, "method"),
359            Ok(PythonObjectValue::BoundMethod {
360                function: 7,
361                receiver: 10
362            })
363        );
364        assert_eq!(space.hooks.seen_receivers, vec![10]);
365    }
366
367    #[test]
368    fn inconsistent_c3_fails_explicitly() {
369        let mut space = PythonObjectSpace::default();
370        space.define_class(1, "object", vec![]).unwrap();
371        space.define_class(2, "A", vec![1]).unwrap();
372        space.define_class(3, "B", vec![1]).unwrap();
373        space.define_class(4, "AB", vec![2, 3]).unwrap();
374        space.define_class(5, "BA", vec![3, 2]).unwrap();
375        assert_eq!(
376            space.define_class(6, "Impossible", vec![4, 5]),
377            Err(ClassError::InconsistentMro)
378        );
379    }
380}