481 lines
15 KiB
Rust
481 lines
15 KiB
Rust
use std::any::Any;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
use endor_core::{CompareOp, NativeFunction, ObjectPayload, ObjectRef, Record, Value, Vm, VmError};
|
|
|
|
#[derive(Debug)]
|
|
struct Resource {
|
|
drops: Arc<AtomicUsize>,
|
|
edges: Vec<ObjectRef>,
|
|
}
|
|
|
|
impl Drop for Resource {
|
|
fn drop(&mut self) {
|
|
self.drops.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
impl ObjectPayload for Resource {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
|
|
fn kind_name(&self) -> &'static str {
|
|
"resource"
|
|
}
|
|
|
|
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
|
for reference in &self.edges {
|
|
visit(*reference);
|
|
}
|
|
}
|
|
|
|
fn allows_attributes(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Number(i64);
|
|
|
|
impl ObjectPayload for Number {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
|
|
fn kind_name(&self) -> &'static str {
|
|
"number"
|
|
}
|
|
|
|
fn repr(&self, _identity: ObjectRef) -> Result<String, VmError> {
|
|
Ok(self.0.to_string())
|
|
}
|
|
|
|
fn is_true(&self) -> Result<bool, VmError> {
|
|
Ok(self.0 != 0)
|
|
}
|
|
|
|
fn hash(&self, _identity: ObjectRef) -> Result<u64, VmError> {
|
|
Ok(self.0 as u64)
|
|
}
|
|
|
|
fn compare(
|
|
&self,
|
|
other: &dyn ObjectPayload,
|
|
operation: CompareOp,
|
|
) -> Result<Option<bool>, VmError> {
|
|
let Some(other) = other.as_any().downcast_ref::<Self>() else {
|
|
return Ok(None);
|
|
};
|
|
Ok(Some(match operation {
|
|
CompareOp::Eq => self.0 == other.0,
|
|
CompareOp::Ne => self.0 != other.0,
|
|
CompareOp::Lt => self.0 < other.0,
|
|
CompareOp::Le => self.0 <= other.0,
|
|
CompareOp::Gt => self.0 > other.0,
|
|
CompareOp::Ge => self.0 >= other.0,
|
|
}))
|
|
}
|
|
}
|
|
|
|
fn resource(vm: &mut Vm, drops: &Arc<AtomicUsize>, edges: Vec<ObjectRef>) -> ObjectRef {
|
|
vm.allocate(Resource {
|
|
drops: drops.clone(),
|
|
edges,
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn starts_empty_without_language_classes_or_a_type_system() {
|
|
let vm = Vm::new().unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn defaults_provide_identity_representation_hash_and_truth() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let first = vm.new_object().unwrap();
|
|
let second = vm.new_object().unwrap();
|
|
assert_eq!(vm.object(first).unwrap().header().identity(), first);
|
|
assert!(vm.is_same(first, first).unwrap());
|
|
assert!(!vm.is_same(first, second).unwrap());
|
|
assert!(vm.repr(first).unwrap().contains(&first.to_string()));
|
|
assert_eq!(vm.repr(first).unwrap(), vm.str(first).unwrap());
|
|
assert_eq!(vm.hash(first).unwrap(), vm.hash(first).unwrap());
|
|
assert!(vm.is_true(first).unwrap());
|
|
assert!(vm.compare(first, first, CompareOp::Eq).unwrap());
|
|
assert!(vm.compare(first, second, CompareOp::Ne).unwrap());
|
|
assert_eq!(
|
|
vm.compare(first, second, CompareOp::Lt),
|
|
Err(VmError::UnsupportedOperation("ordering"))
|
|
);
|
|
assert_eq!(
|
|
vm.call(first, &[]),
|
|
Err(VmError::UnsupportedOperation("call"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_references_control_lifetime_and_copies_are_borrowed() {
|
|
let drops = Arc::new(AtomicUsize::new(0));
|
|
let mut vm = Vm::new().unwrap();
|
|
let owned = resource(&mut vm, &drops, vec![]);
|
|
let borrowed = owned;
|
|
assert_eq!(vm.ref_count(borrowed).unwrap(), 1);
|
|
vm.retain(owned).unwrap();
|
|
assert_eq!(vm.ref_count(owned).unwrap(), 2);
|
|
vm.release(owned).unwrap();
|
|
assert_eq!(drops.load(Ordering::SeqCst), 0);
|
|
vm.release(owned).unwrap();
|
|
assert_eq!(drops.load(Ordering::SeqCst), 1);
|
|
assert_eq!(vm.object_count(), 0);
|
|
assert!(matches!(vm.object(borrowed), Err(VmError::InvalidHandle)));
|
|
assert_eq!(vm.release(borrowed), Err(VmError::InvalidHandle));
|
|
}
|
|
|
|
#[test]
|
|
fn attributes_keep_objects_alive_and_delete_releases_them() {
|
|
let drops = Arc::new(AtomicUsize::new(0));
|
|
let mut vm = Vm::new().unwrap();
|
|
let owner = vm.new_object().unwrap();
|
|
let child = resource(&mut vm, &drops, vec![]);
|
|
vm.set_attr(owner, "child", Value::Object(child)).unwrap();
|
|
assert_eq!(vm.ref_count(child).unwrap(), 2);
|
|
vm.release(child).unwrap();
|
|
assert_eq!(vm.ref_count(child).unwrap(), 1);
|
|
assert_eq!(vm.release(child), Err(VmError::NoOwnedReference));
|
|
assert_eq!(vm.get_attr(owner, "child").unwrap(), Value::Object(child));
|
|
vm.del_attr(owner, "child").unwrap();
|
|
assert_eq!(drops.load(Ordering::SeqCst), 1);
|
|
assert_eq!(
|
|
vm.get_attr(owner, "child"),
|
|
Err(VmError::AttributeNotFound("child".into()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn replacing_the_same_edge_does_not_destroy_the_target() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let owner = vm.new_object().unwrap();
|
|
let target = vm.new_object().unwrap();
|
|
vm.set_attr(owner, "child", Value::Object(target)).unwrap();
|
|
vm.release(target).unwrap();
|
|
vm.set_attr(owner, "child", Value::Object(target)).unwrap();
|
|
assert_eq!(vm.ref_count(target).unwrap(), 1);
|
|
vm.set_attr(owner, "child", Value::Number(7_i64.into()))
|
|
.unwrap();
|
|
assert!(matches!(vm.object(target), Err(VmError::InvalidHandle)));
|
|
assert_eq!(
|
|
vm.get_attr(owner, "child").unwrap(),
|
|
Value::Number(7_i64.into())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn borrowed_attribute_results_can_be_retained_by_the_host() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let parent = vm.new_object().unwrap();
|
|
let child = vm.new_object().unwrap();
|
|
vm.set_attr(parent, "child", Value::Object(child)).unwrap();
|
|
vm.release(child).unwrap();
|
|
let Value::Object(borrowed) = vm.get_attr(parent, "child").unwrap() else {
|
|
panic!("expected an object");
|
|
};
|
|
vm.retain(borrowed).unwrap();
|
|
vm.release(parent).unwrap();
|
|
assert_eq!(vm.ref_count(borrowed).unwrap(), 1);
|
|
vm.release(borrowed).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_foreign_and_stale_writes_preserve_the_old_attribute() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let mut other = Vm::new().unwrap();
|
|
let owner = vm.new_object().unwrap();
|
|
let foreign = other.new_object().unwrap();
|
|
let stale = vm.new_object().unwrap();
|
|
vm.release(stale).unwrap();
|
|
vm.set_attr(owner, "value", Value::Number(1_i64.into()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
vm.set_attr(owner, "value", Value::Object(foreign)),
|
|
Err(VmError::ForeignHandle)
|
|
);
|
|
assert_eq!(
|
|
vm.set_attr(owner, "value", Value::Object(stale)),
|
|
Err(VmError::InvalidHandle)
|
|
);
|
|
assert_eq!(
|
|
vm.get_attr(owner, "value").unwrap(),
|
|
Value::Number(1_i64.into())
|
|
);
|
|
assert_eq!(vm.retain(foreign), Err(VmError::ForeignHandle));
|
|
assert_eq!(vm.release(foreign), Err(VmError::ForeignHandle));
|
|
assert_eq!(vm.is_same(owner, foreign), Err(VmError::ForeignHandle));
|
|
}
|
|
|
|
#[test]
|
|
fn released_handles_cannot_alias_new_objects_or_new_vms() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let old = vm.new_object().unwrap();
|
|
vm.release(old).unwrap();
|
|
let new = vm.new_object().unwrap();
|
|
assert_ne!(old, new);
|
|
assert_eq!(vm.repr(old), Err(VmError::InvalidHandle));
|
|
let other = Vm::new().unwrap();
|
|
assert!(matches!(other.object(new), Err(VmError::ForeignHandle)));
|
|
}
|
|
|
|
#[test]
|
|
fn boxed_objects_keep_their_addresses_when_the_slot_vector_grows() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let first = vm.new_object().unwrap();
|
|
let address = std::ptr::from_ref(vm.object(first).unwrap());
|
|
for _ in 0..4096 {
|
|
vm.new_object().unwrap();
|
|
}
|
|
assert_eq!(std::ptr::from_ref(vm.object(first).unwrap()), address);
|
|
}
|
|
|
|
#[test]
|
|
fn zero_reference_chains_are_released_without_recursive_destructors() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let head = vm.new_object().unwrap();
|
|
let mut tail = head;
|
|
for _ in 0..4096 {
|
|
let next = vm.new_object().unwrap();
|
|
vm.set_attr(tail, "next", Value::Object(next)).unwrap();
|
|
vm.release(next).unwrap();
|
|
tail = next;
|
|
}
|
|
vm.release(head).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cycles_remain_while_rooted_and_are_collected_when_unreachable() {
|
|
let drops = Arc::new(AtomicUsize::new(0));
|
|
let mut vm = Vm::new().unwrap();
|
|
let first = resource(&mut vm, &drops, vec![]);
|
|
let second = resource(&mut vm, &drops, vec![]);
|
|
vm.set_attr(first, "next", Value::Object(second)).unwrap();
|
|
vm.set_attr(second, "next", Value::Object(first)).unwrap();
|
|
vm.release(second).unwrap();
|
|
assert_eq!(vm.collect_cycles(), 0);
|
|
vm.release(first).unwrap();
|
|
assert_eq!(vm.object_count(), 2);
|
|
assert_eq!(vm.collect_cycles(), 2);
|
|
assert_eq!(vm.collect_cycles(), 0);
|
|
assert_eq!(drops.load(Ordering::SeqCst), 2);
|
|
assert!(matches!(vm.object(first), Err(VmError::InvalidHandle)));
|
|
assert!(matches!(vm.object(second), Err(VmError::InvalidHandle)));
|
|
}
|
|
|
|
#[test]
|
|
fn cycle_collection_preserves_rooted_targets_and_updates_counts() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let live = vm.new_object().unwrap();
|
|
let cycle = vm.new_object().unwrap();
|
|
vm.set_attr(cycle, "self", Value::Object(cycle)).unwrap();
|
|
vm.set_attr(cycle, "live", Value::Object(live)).unwrap();
|
|
vm.release(cycle).unwrap();
|
|
assert_eq!(vm.ref_count(live).unwrap(), 2);
|
|
assert_eq!(vm.collect_cycles(), 1);
|
|
assert_eq!(vm.ref_count(live).unwrap(), 1);
|
|
vm.release(live).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn payload_edges_are_owned_and_duplicate_edges_are_counted() {
|
|
let drops = Arc::new(AtomicUsize::new(0));
|
|
let mut vm = Vm::new().unwrap();
|
|
let target = vm.new_object().unwrap();
|
|
let holder = resource(&mut vm, &drops, vec![target, target]);
|
|
assert_eq!(vm.ref_count(target).unwrap(), 3);
|
|
vm.release(target).unwrap();
|
|
vm.release(holder).unwrap();
|
|
assert_eq!(vm.object_count(), 0);
|
|
assert_eq!(drops.load(Ordering::SeqCst), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn payload_edges_participate_in_cycle_tracing() {
|
|
let drops = Arc::new(AtomicUsize::new(0));
|
|
let mut vm = Vm::new().unwrap();
|
|
let record = vm.new_object().unwrap();
|
|
let native = resource(&mut vm, &drops, vec![record]);
|
|
vm.set_attr(record, "native", Value::Object(native))
|
|
.unwrap();
|
|
vm.release(record).unwrap();
|
|
vm.release(native).unwrap();
|
|
assert_eq!(vm.collect_cycles(), 2);
|
|
assert_eq!(drops.load(Ordering::SeqCst), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_payload_edges_abort_allocation_without_retaining_valid_edges() {
|
|
let drops = Arc::new(AtomicUsize::new(0));
|
|
let mut vm = Vm::new().unwrap();
|
|
let mut other = Vm::new().unwrap();
|
|
let local = vm.new_object().unwrap();
|
|
let foreign = other.new_object().unwrap();
|
|
assert_eq!(
|
|
vm.allocate(Resource {
|
|
drops: drops.clone(),
|
|
edges: vec![local, foreign],
|
|
}),
|
|
Err(VmError::ForeignHandle)
|
|
);
|
|
assert_eq!(vm.ref_count(local).unwrap(), 1);
|
|
assert_eq!(vm.object_count(), 1);
|
|
assert_eq!(drops.load(Ordering::SeqCst), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn rust_payload_operations_override_defaults_without_language_types() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let zero = vm.allocate(Number(0)).unwrap();
|
|
let first = vm.allocate(Number(42)).unwrap();
|
|
let equal = vm.allocate(Number(42)).unwrap();
|
|
assert!(!vm.is_same(first, equal).unwrap());
|
|
assert!(vm.compare(first, equal, CompareOp::Eq).unwrap());
|
|
assert!(vm.compare(zero, first, CompareOp::Lt).unwrap());
|
|
assert_eq!(vm.hash(first).unwrap(), vm.hash(equal).unwrap());
|
|
assert_eq!(vm.str(first).unwrap(), "42");
|
|
assert!(!vm.is_true(zero).unwrap());
|
|
assert!(vm.is_true(first).unwrap());
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ReflectedOrdering;
|
|
|
|
impl ObjectPayload for ReflectedOrdering {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
|
|
fn kind_name(&self) -> &'static str {
|
|
"reflected-ordering"
|
|
}
|
|
|
|
fn compare(
|
|
&self,
|
|
_other: &dyn ObjectPayload,
|
|
operation: CompareOp,
|
|
) -> Result<Option<bool>, VmError> {
|
|
Ok(Some(operation == CompareOp::Gt))
|
|
}
|
|
|
|
fn hash(&self, _identity: ObjectRef) -> Result<u64, VmError> {
|
|
Err(VmError::UnsupportedOperation("hash"))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reflected_operations_and_explicitly_unsupported_hashing_are_respected() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let left = vm.allocate(Record).unwrap();
|
|
let right = vm.allocate(ReflectedOrdering).unwrap();
|
|
assert!(vm.compare(left, right, CompareOp::Lt).unwrap());
|
|
assert_eq!(vm.hash(right), Err(VmError::UnsupportedOperation("hash")));
|
|
}
|
|
|
|
fn add(arguments: &[Value]) -> Result<Value, VmError> {
|
|
match arguments {
|
|
[Value::Number(left), Value::Number(right)] => {
|
|
let (left, right) = left
|
|
.as_i64()
|
|
.zip(right.as_i64())
|
|
.ok_or(VmError::InvalidArgument("expected two integers"))?;
|
|
left.checked_add(right)
|
|
.map(|value| Value::Number(value.into()))
|
|
.ok_or(VmError::InvalidArgument("integer overflow"))
|
|
}
|
|
_ => Err(VmError::InvalidArgument("expected two integers")),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_calls_dispatch_validate_arguments_and_propagate_errors() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let function = vm.allocate(NativeFunction::new("add", add)).unwrap();
|
|
assert_eq!(vm.repr(function).unwrap(), "<native-function add>");
|
|
assert_eq!(
|
|
vm.call(
|
|
function,
|
|
&[Value::Number(2_i64.into()), Value::Number(3_i64.into())]
|
|
)
|
|
.unwrap(),
|
|
Value::Number(5_i64.into())
|
|
);
|
|
assert_eq!(
|
|
vm.call(function, &[]),
|
|
Err(VmError::InvalidArgument("expected two integers"))
|
|
);
|
|
assert_eq!(
|
|
vm.call(
|
|
function,
|
|
&[Value::Number(i64::MAX.into()), Value::Number(1_i64.into())]
|
|
),
|
|
Err(VmError::InvalidArgument("integer overflow"))
|
|
);
|
|
assert_eq!(
|
|
vm.set_attr(function, "x", Value::Nil),
|
|
Err(VmError::UnsupportedOperation("attributes"))
|
|
);
|
|
let mut other = Vm::new().unwrap();
|
|
let foreign = other.new_object().unwrap();
|
|
assert_eq!(
|
|
vm.call(function, &[Value::Object(foreign)]),
|
|
Err(VmError::ForeignHandle)
|
|
);
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ReturnsObject(ObjectRef);
|
|
|
|
impl ObjectPayload for ReturnsObject {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
|
|
fn kind_name(&self) -> &'static str {
|
|
"invalid-return"
|
|
}
|
|
|
|
fn call(&self, _arguments: &[Value]) -> Result<Value, VmError> {
|
|
Ok(Value::Object(self.0))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_results_cannot_smuggle_foreign_or_dead_handles_into_the_vm() {
|
|
let mut vm = Vm::new().unwrap();
|
|
let mut other = Vm::new().unwrap();
|
|
let foreign = other.new_object().unwrap();
|
|
let invalid_return = vm.allocate(ReturnsObject(foreign)).unwrap();
|
|
assert_eq!(vm.call(invalid_return, &[]), Err(VmError::ForeignHandle));
|
|
let dead = vm.new_object().unwrap();
|
|
vm.release(dead).unwrap();
|
|
let invalid_return = vm.allocate(ReturnsObject(dead)).unwrap();
|
|
assert_eq!(vm.call(invalid_return, &[]), Err(VmError::InvalidHandle));
|
|
}
|
|
|
|
#[test]
|
|
fn dropping_the_vm_cleans_up_rooted_objects_and_uncollected_cycles() {
|
|
let drops = Arc::new(AtomicUsize::new(0));
|
|
{
|
|
let mut vm = Vm::new().unwrap();
|
|
let rooted = resource(&mut vm, &drops, vec![]);
|
|
let cycle = resource(&mut vm, &drops, vec![]);
|
|
vm.set_attr(cycle, "self", Value::Object(cycle)).unwrap();
|
|
vm.release(cycle).unwrap();
|
|
assert_eq!(vm.ref_count(rooted).unwrap(), 1);
|
|
}
|
|
assert_eq!(drops.load(Ordering::SeqCst), 2);
|
|
}
|