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, edges: Vec, } 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 { Ok(self.0.to_string()) } fn is_true(&self) -> Result { Ok(self.0 != 0) } fn hash(&self, _identity: ObjectRef) -> Result { Ok(self.0 as u64) } fn compare( &self, other: &dyn ObjectPayload, operation: CompareOp, ) -> Result, VmError> { let Some(other) = other.as_any().downcast_ref::() 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, edges: Vec) -> 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, VmError> { Ok(Some(operation == CompareOp::Gt)) } fn hash(&self, _identity: ObjectRef) -> Result { 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 { 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(), ""); 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 { 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); }