use std::{collections::HashSet, hash::Hash};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DataDescriptor<V> {
pub value: V,
pub writable: bool,
pub enumerable: bool,
pub configurable: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AccessorDescriptor<H> {
pub get: Option<H>,
pub set: Option<H>,
pub enumerable: bool,
pub configurable: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Descriptor<V, H> {
Data(DataDescriptor<V>),
Accessor(AccessorDescriptor<H>),
}
impl<V, H> Descriptor<V, H> {
fn configurable(&self) -> bool {
match self {
Self::Data(value) => value.configurable,
Self::Accessor(value) => value.configurable,
}
}
fn enumerable(&self) -> bool {
match self {
Self::Data(value) => value.enumerable,
Self::Accessor(value) => value.enumerable,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DefineError {
InvariantViolation,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum AccessKind {
Get,
Set,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AccessError<E> {
BudgetExhausted,
RecursiveReentry,
Hook(E),
}
pub struct AccessContext<O, K> {
remaining: usize,
active: HashSet<(AccessKind, O, K)>,
}
impl<O, K> AccessContext<O, K>
where
O: Clone + Eq + Hash,
K: Clone + Eq + Hash,
{
pub fn new(work_budget: usize) -> Self {
Self {
remaining: work_budget,
active: HashSet::new(),
}
}
pub fn remaining(&self) -> usize {
self.remaining
}
fn charge<E>(&mut self) -> Result<(), AccessError<E>> {
self.remaining = self
.remaining
.checked_sub(1)
.ok_or(AccessError::BudgetExhausted)?;
Ok(())
}
pub fn intercept<T, E>(
&mut self,
kind: AccessKind,
receiver: &O,
key: &K,
call: impl FnOnce(&mut Self) -> Result<T, AccessError<E>>,
) -> Result<T, AccessError<E>> {
self.charge()?;
let signature = (kind, receiver.clone(), key.clone());
if !self.active.insert(signature.clone()) {
return Err(AccessError::RecursiveReentry);
}
let result = call(self);
self.active.remove(&signature);
result
}
}
pub trait PropertyHook<O, K, V, H> {
type Error;
fn get(
&mut self,
context: &mut AccessContext<O, K>,
hook: &H,
receiver: &O,
key: &K,
) -> Result<V, AccessError<Self::Error>>;
fn set(
&mut self,
context: &mut AccessContext<O, K>,
hook: &H,
receiver: &O,
key: &K,
value: V,
) -> Result<(), AccessError<Self::Error>>;
}
#[derive(Clone, Debug)]
struct OwnProperty<K, V, H> {
key: K,
descriptor: Descriptor<V, H>,
}
type PropertyObject<O, K, V, H> = (O, Vec<OwnProperty<K, V, H>>);
#[derive(Clone, Debug, Default)]
pub struct PropertyStore<O, K, V, H> {
objects: Vec<PropertyObject<O, K, V, H>>,
}
impl<O, K, V, H> PropertyStore<O, K, V, H> {
pub const fn new() -> Self {
Self {
objects: Vec::new(),
}
}
}
impl<O, K, V, H> PropertyStore<O, K, V, H>
where
O: Clone + Eq + Hash,
K: Clone + Eq + Hash,
V: Clone + PartialEq,
H: Clone + PartialEq,
{
fn properties(&self, owner: &O) -> Option<&[OwnProperty<K, V, H>]> {
self.objects
.iter()
.find(|(candidate, _)| candidate == owner)
.map(|(_, properties)| properties.as_slice())
}
fn properties_mut(&mut self, owner: &O) -> &mut Vec<OwnProperty<K, V, H>> {
if let Some(index) = self
.objects
.iter()
.position(|(candidate, _)| candidate == owner)
{
return &mut self.objects[index].1;
}
self.objects.push((owner.clone(), Vec::new()));
&mut self.objects.last_mut().expect("object was inserted").1
}
pub fn own(&self, owner: &O, key: &K) -> Option<&Descriptor<V, H>> {
self.properties(owner)?
.iter()
.find(|property| &property.key == key)
.map(|property| &property.descriptor)
}
pub fn define(
&mut self,
owner: &O,
key: K,
descriptor: Descriptor<V, H>,
) -> Result<(), DefineError> {
let properties = self.properties_mut(owner);
if let Some(property) = properties.iter_mut().find(|property| property.key == key) {
if !compatible_redefinition(&property.descriptor, &descriptor) {
return Err(DefineError::InvariantViolation);
}
property.descriptor = descriptor;
} else {
properties.push(OwnProperty { key, descriptor });
}
Ok(())
}
pub fn delete(&mut self, owner: &O, key: &K) -> Result<bool, DefineError> {
let Some((_, properties)) = self
.objects
.iter_mut()
.find(|(candidate, _)| candidate == owner)
else {
return Ok(false);
};
let Some(index) = properties.iter().position(|property| &property.key == key) else {
return Ok(false);
};
if !properties[index].descriptor.configurable() {
return Err(DefineError::InvariantViolation);
}
properties.remove(index);
Ok(true)
}
pub fn own_keys(&self, owner: &O, enumerable_only: bool) -> Vec<K> {
self.properties(owner)
.unwrap_or_default()
.iter()
.filter(|property| !enumerable_only || property.descriptor.enumerable())
.map(|property| property.key.clone())
.collect()
}
pub fn get<E>(
&self,
owners: &[O],
receiver: &O,
key: &K,
context: &mut AccessContext<O, K>,
hooks: &mut impl PropertyHook<O, K, V, H, Error = E>,
) -> Result<Option<V>, AccessError<E>> {
let mut visited = HashSet::new();
for owner in owners {
context.charge()?;
if !visited.insert(owner.clone()) {
continue;
}
let Some(descriptor) = self.own(owner, key) else {
continue;
};
return match descriptor {
Descriptor::Data(data) => Ok(Some(data.value.clone())),
Descriptor::Accessor(accessor) => match &accessor.get {
Some(hook) => context
.intercept(AccessKind::Get, receiver, key, |context| {
hooks.get(context, hook, receiver, key)
})
.map(Some),
None => Ok(None),
},
};
}
Ok(None)
}
pub fn set<E>(
&mut self,
owners: &[O],
receiver: &O,
key: &K,
value: V,
context: &mut AccessContext<O, K>,
hooks: &mut impl PropertyHook<O, K, V, H, Error = E>,
) -> Result<bool, AccessError<E>> {
let mut visited = HashSet::new();
for owner in owners {
context.charge()?;
if !visited.insert(owner.clone()) {
continue;
}
let Some(descriptor) = self.own(owner, key).cloned() else {
continue;
};
return match descriptor {
Descriptor::Data(data) if data.writable => {
let property = self
.properties_mut(owner)
.iter_mut()
.find(|property| &property.key == key)
.expect("descriptor was found");
let Descriptor::Data(data) = &mut property.descriptor else {
unreachable!("cloned descriptor kind remains stable")
};
data.value = value;
Ok(true)
}
Descriptor::Data(_) => Ok(false),
Descriptor::Accessor(accessor) => match accessor.set {
Some(hook) => context
.intercept(AccessKind::Set, receiver, key, |context| {
hooks.set(context, &hook, receiver, key, value)
})
.map(|()| true),
None => Ok(false),
},
};
}
Ok(false)
}
}
fn compatible_redefinition<V: PartialEq, H: PartialEq>(
current: &Descriptor<V, H>,
replacement: &Descriptor<V, H>,
) -> bool {
if current.configurable() {
return true;
}
match (current, replacement) {
(Descriptor::Data(old), Descriptor::Data(new)) => {
!new.configurable
&& old.enumerable == new.enumerable
&& (old.writable || !new.writable)
&& (old.writable || old.value == new.value)
}
(Descriptor::Accessor(old), Descriptor::Accessor(new)) => {
!new.configurable
&& old.enumerable == new.enumerable
&& old.get == new.get
&& old.set == new.set
}
_ => false,
}
}