use sim_lib_dispatch::{
AccessContext, AccessError, AccessorDescriptor, DataDescriptor, Descriptor, PropertyHook,
PropertyStore,
};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum PythonObjectValue {
#[default]
None,
Int(i64),
String(String),
Object(u64),
Function(u64),
BoundMethod {
function: u64,
receiver: u64,
},
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DescriptorHook {
pub name: String,
pub value: PythonObjectValue,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PythonClass {
pub id: u64,
pub name: String,
pub bases: Vec<u64>,
pub mro: Vec<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ClassError {
UnknownBase(u64),
InconsistentMro,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AttributeError {
UnknownObject(u64),
UnknownClass(u64),
Missing(String),
Access,
}
#[derive(Default)]
struct Hooks {
seen_receivers: Vec<u64>,
}
impl PropertyHook<u64, String, PythonObjectValue, DescriptorHook> for Hooks {
type Error = ();
fn get(
&mut self,
_: &mut AccessContext<u64, String>,
hook: &DescriptorHook,
receiver: &u64,
_: &String,
) -> Result<PythonObjectValue, AccessError<Self::Error>> {
self.seen_receivers.push(*receiver);
Ok(hook.value.clone())
}
fn set(
&mut self,
_: &mut AccessContext<u64, String>,
_: &DescriptorHook,
receiver: &u64,
_: &String,
_: PythonObjectValue,
) -> Result<(), AccessError<Self::Error>> {
self.seen_receivers.push(*receiver);
Ok(())
}
}
#[derive(Default)]
pub struct PythonObjectSpace {
classes: BTreeMap<u64, PythonClass>,
instances: BTreeMap<u64, u64>,
properties: PropertyStore<u64, String, PythonObjectValue, DescriptorHook>,
hooks: Hooks,
}
impl PythonObjectSpace {
pub fn define_class(
&mut self,
id: u64,
name: impl Into<String>,
bases: Vec<u64>,
) -> Result<(), ClassError> {
let mut sequences = Vec::new();
for base in &bases {
sequences.push(
self.classes
.get(base)
.ok_or(ClassError::UnknownBase(*base))?
.mro
.clone(),
);
}
sequences.push(bases.clone());
let mut mro = vec![id];
mro.extend(c3_merge(sequences)?);
self.classes.insert(
id,
PythonClass {
id,
name: name.into(),
bases,
mro,
},
);
Ok(())
}
pub fn instantiate(&mut self, object: u64, class: u64) -> Result<(), AttributeError> {
if !self.classes.contains_key(&class) {
return Err(AttributeError::UnknownClass(class));
}
self.instances.insert(object, class);
Ok(())
}
pub fn class(&self, id: u64) -> Option<&PythonClass> {
self.classes.get(&id)
}
pub fn define_value(&mut self, owner: u64, key: impl Into<String>, value: PythonObjectValue) {
self.properties
.define(
&owner,
key.into(),
Descriptor::Data(DataDescriptor {
value,
writable: true,
enumerable: true,
configurable: true,
}),
)
.expect("configurable Python value accepts replacement");
}
pub fn define_descriptor(
&mut self,
class: u64,
key: impl Into<String>,
getter: DescriptorHook,
data: bool,
) {
self.properties
.define(
&class,
key.into(),
Descriptor::Accessor(AccessorDescriptor {
get: Some(getter),
set: data.then(|| DescriptorHook {
name: "set".into(),
value: PythonObjectValue::None,
}),
enumerable: true,
configurable: true,
}),
)
.expect("configurable Python descriptor accepts replacement");
}
pub fn get(&mut self, object: u64, key: &str) -> Result<PythonObjectValue, AttributeError> {
let class = *self
.instances
.get(&object)
.ok_or(AttributeError::UnknownObject(object))?;
let mro = self
.classes
.get(&class)
.ok_or(AttributeError::UnknownClass(class))?
.mro
.clone();
let key = key.to_owned();
let descriptor_owner = mro
.iter()
.find(|owner| self.properties.own(owner, &key).is_some())
.copied();
if let Some(owner) = descriptor_owner
&& matches!(self.properties.own(&owner, &key), Some(Descriptor::Accessor(a)) if a.set.is_some())
{
return self.read(&[owner], object, &key);
}
if self.properties.own(&object, &key).is_some() {
return self.read(&[object], object, &key);
}
let value = self.read(&mro, object, &key)?;
Ok(match value {
PythonObjectValue::Function(function) => PythonObjectValue::BoundMethod {
function,
receiver: object,
},
other => other,
})
}
pub fn get_super(
&mut self,
current: u64,
object: u64,
key: &str,
) -> Result<PythonObjectValue, AttributeError> {
let class = *self
.instances
.get(&object)
.ok_or(AttributeError::UnknownObject(object))?;
let mro = &self
.classes
.get(&class)
.ok_or(AttributeError::UnknownClass(class))?
.mro;
let at = mro
.iter()
.position(|candidate| *candidate == current)
.ok_or(AttributeError::UnknownClass(current))?;
let owners = mro[at + 1..].to_vec();
let value = self.read(&owners, object, &key.to_owned())?;
Ok(match value {
PythonObjectValue::Function(function) => PythonObjectValue::BoundMethod {
function,
receiver: object,
},
other => other,
})
}
fn read(
&mut self,
owners: &[u64],
receiver: u64,
key: &String,
) -> Result<PythonObjectValue, AttributeError> {
self.properties
.get(
owners,
&receiver,
key,
&mut AccessContext::new(64),
&mut self.hooks,
)
.map_err(|_| AttributeError::Access)?
.ok_or_else(|| AttributeError::Missing(key.clone()))
}
}
fn c3_merge(mut sequences: Vec<Vec<u64>>) -> Result<Vec<u64>, ClassError> {
let mut result = Vec::new();
loop {
sequences.retain(|sequence| !sequence.is_empty());
if sequences.is_empty() {
return Ok(result);
}
let candidate = sequences
.iter()
.map(|sequence| sequence[0])
.find(|candidate| {
sequences
.iter()
.all(|sequence| !sequence[1..].contains(candidate))
})
.ok_or(ClassError::InconsistentMro)?;
result.push(candidate);
for sequence in &mut sequences {
if sequence.first() == Some(&candidate) {
sequence.remove(0);
}
}
let unique: BTreeSet<_> = result.iter().copied().collect();
if unique.len() != result.len() {
return Err(ClassError::InconsistentMro);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn value(value: i64) -> PythonObjectValue {
PythonObjectValue::Int(value)
}
#[test]
fn c3_descriptors_binding_and_super_share_property_mechanics() {
let mut space = PythonObjectSpace::default();
space.define_class(1, "object", vec![]).unwrap();
space.define_class(2, "Left", vec![1]).unwrap();
space.define_class(3, "Right", vec![1]).unwrap();
space.define_class(4, "Diamond", vec![2, 3]).unwrap();
assert_eq!(space.class(4).unwrap().mro, vec![4, 2, 3, 1]);
space.instantiate(10, 4).unwrap();
space.define_descriptor(
2,
"data",
DescriptorHook {
name: "data".into(),
value: value(1),
},
true,
);
space.define_descriptor(
2,
"nondata",
DescriptorHook {
name: "nondata".into(),
value: value(2),
},
false,
);
space.define_value(10, "data", value(11));
space.define_value(10, "nondata", value(22));
assert_eq!(space.get(10, "data"), Ok(value(1)));
assert_eq!(space.get(10, "nondata"), Ok(value(22)));
space.define_value(3, "method", PythonObjectValue::Function(7));
assert_eq!(
space.get_super(2, 10, "method"),
Ok(PythonObjectValue::BoundMethod {
function: 7,
receiver: 10
})
);
assert_eq!(space.hooks.seen_receivers, vec![10]);
}
#[test]
fn inconsistent_c3_fails_explicitly() {
let mut space = PythonObjectSpace::default();
space.define_class(1, "object", vec![]).unwrap();
space.define_class(2, "A", vec![1]).unwrap();
space.define_class(3, "B", vec![1]).unwrap();
space.define_class(4, "AB", vec![2, 3]).unwrap();
space.define_class(5, "BA", vec![3, 2]).unwrap();
assert_eq!(
space.define_class(6, "Impossible", vec![4, 5]),
Err(ClassError::InconsistentMro)
);
}
}