use objects::{ObjectPayload, ObjectRef, ObjectStore, TypeConstructor, Value, VmError}; #[derive(Debug)] struct Proxy(ObjectRef); impl ObjectPayload for Proxy { fn as_any(&self) -> &dyn std::any::Any { self } fn kind_name(&self) -> &'static str { "proxy" } fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { visit(self.0); } fn attribute_target(&self, _: &ObjectStore) -> Result, VmError> { Ok(Some(self.0)) } } #[test] fn storage_retargeting_preserves_type_identity_and_releases_old_state() { let mut store = ObjectStore::new().unwrap(); let roots = store.bootstrap_types().unwrap(); let class = store .new_type("Proxy", roots.object, TypeConstructor::Record) .unwrap(); let storage = store.new_object().unwrap(); let old_value = store.new_object().unwrap(); let weak_value = store.downgrade(old_value).unwrap(); store .set_attr(storage, "value", Value::Object(old_value)) .unwrap(); store.release(old_value).unwrap(); let proxy = store.allocate(Proxy(storage)).unwrap(); store.attach_type(proxy, class).unwrap(); store.release(storage).unwrap(); assert_eq!(store.attribute_path(proxy).unwrap(), vec![proxy, storage]); assert_eq!(store.get_attr(proxy, "type").unwrap(), Value::Object(class)); assert_eq!( store.get_attr(proxy, "value").unwrap(), Value::Object(old_value) ); let next = store.new_object().unwrap(); store.replace_payload(proxy, Proxy(next)).unwrap(); store.release(next).unwrap(); assert!(store.upgrade(weak_value).unwrap().is_none()); store .set_attr(proxy, "value", Value::Number(9_i64.into())) .unwrap(); assert_eq!( store.get_attr(next, "value").unwrap(), Value::Number(9_i64.into()) ); store.del_attr(proxy, "value").unwrap(); assert!(store.get_attr(next, "value").is_err()); for object in [proxy, class, roots.object, roots.type_type] { store.release(object).unwrap(); } store.collect_cycles(); assert_eq!(store.object_count(), 0); } #[test] fn cyclic_and_foreign_attribute_storage_fail_without_mutating_fields() { let mut store = ObjectStore::new().unwrap(); let storage = store.new_object().unwrap(); let proxy = store.allocate(Proxy(storage)).unwrap(); store.replace_payload(proxy, Proxy(proxy)).unwrap(); let version = store.change_version(proxy).unwrap(); assert!(store.get_attr(proxy, "field").is_err()); assert!(store.set_attr(proxy, "field", Value::Nil).is_err()); assert_eq!(store.change_version(proxy).unwrap(), version); let other = ObjectStore::new().unwrap(); assert_eq!(other.attribute_path(proxy), Err(VmError::ForeignHandle)); store.release(storage).unwrap(); store.release(proxy).unwrap(); assert_eq!(store.collect_cycles(), 1); } #[derive(Debug)] struct ReflectiveType(ObjectRef); impl ObjectPayload for ReflectiveType { fn as_any(&self) -> &dyn std::any::Any { self } fn kind_name(&self) -> &'static str { "reflective-type" } fn allows_attributes(&self) -> bool { true } fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { visit(self.0); } fn type_target(&self, store: &ObjectStore) -> Result, VmError> { if let Value::Object(receiver) = store.cell_get(self.0)? { store.get_attr(receiver, "selector")?; } Ok(None) } } #[derive(Debug)] struct TypedCheck; impl objects::NativeCallable for TypedCheck { fn signature(&self) -> Option { Some(objects::CallSignature::exact( vec![objects::AnnotationType::Any], objects::AnnotationType::Number, objects::CallEffect::ReadOnly, )) } fn invoke( &self, _: &mut objects::NativeContext<'_>, _: &[Value], ) -> Result { Ok(objects::NativeResult::Return(Value::Number(1.into()))) } } #[test] fn typed_method_storage_cannot_diverge_from_class_dispatch() { let mut store = ObjectStore::new().unwrap(); let roots = store.bootstrap_types().unwrap(); let class = store .new_type("Checked", roots.object, TypeConstructor::Record) .unwrap(); let function = store .allocate(objects::ContextNativeFunction::new( "check", TypedCheck, vec![], )) .unwrap(); store.define_method(class, "check", function).unwrap(); let capability = store .new_typed_trait( "Checked", vec![objects::TypedMethodRequirement { name: "check".into(), signature: objects::CallSignature::exact( vec![objects::AnnotationType::Any], objects::AnnotationType::Number, objects::CallEffect::ReadOnly, ), }], ) .unwrap(); let storage = store.new_object().unwrap(); let proxy = store.allocate(Proxy(storage)).unwrap(); store.attach_type(proxy, class).unwrap(); store.set_attr(storage, "check", Value::Nil).unwrap(); let version = store.change_version(class).unwrap(); assert!(matches!( store.implement_trait(class, capability), Err(VmError::TraitViolation { .. }) )); assert_eq!(store.change_version(class).unwrap(), version); assert!(!store.implements_trait(class, capability).unwrap()); store.del_attr(storage, "check").unwrap(); store.implement_trait(class, capability).unwrap(); let version = store.change_version(storage).unwrap(); assert!(matches!( store.set_attr(proxy, "check", Value::Nil), Err(VmError::TraitViolation { .. }) )); assert_eq!(store.change_version(storage).unwrap(), version); assert!(store.supports(proxy, capability).unwrap()); // A trusted host can access the untyped backing record directly. Every // language/host dispatch gate must detect the resulting invalid instance. store.set_attr(storage, "check", Value::Nil).unwrap(); assert!(matches!( store.supports(proxy, capability), Err(VmError::TraitViolation { .. }) )); assert!(matches!( store.get_attr(proxy, "check"), Err(VmError::TraitViolation { .. }) )); assert!(matches!( store.bind_attr(proxy, "check"), Err(VmError::TraitViolation { .. }) )); assert!(matches!( store.check_method_contract_call(function, proxy, false), Err(VmError::TraitViolation { .. }) )); assert!(matches!( store.check_typed_instance_storage(proxy), Err(VmError::TraitViolation { .. }) )); let candidate = store.new_object().unwrap(); store.set_attr(candidate, "check", Value::Nil).unwrap(); assert!(matches!( store.attach_type(candidate, class), Err(VmError::TraitViolation { .. }) )); assert_eq!(store.type_of(candidate).unwrap(), None); store.del_attr(storage, "check").unwrap(); assert!(store.supports(proxy, capability).unwrap()); assert_eq!( store.get_attr(proxy, "check").unwrap(), Value::Object(function) ); store .check_method_contract_call(function, proxy, false) .unwrap(); let self_cell = store.new_cell(Some(Value::Nil)).unwrap(); let reflective = store.allocate(ReflectiveType(self_cell)).unwrap(); store .set_attr(reflective, "selector", Value::Bool(true)) .unwrap(); store .cell_set(self_cell, Value::Object(reflective)) .unwrap(); store.attach_type(reflective, class).unwrap(); assert_eq!( store.get_attr(reflective, "selector").unwrap(), Value::Bool(true) ); assert!(store.supports(reflective, capability).unwrap()); assert!(matches!( store.set_attr(reflective, "check", Value::Nil), Err(VmError::TraitViolation { .. }) )); // A failed guarded check must not suppress the next one. assert!(matches!( store.set_attr(reflective, "check", Value::Nil), Err(VmError::TraitViolation { .. }) )); store.release(reflective).unwrap(); store.release(self_cell).unwrap(); for object in [ candidate, proxy, storage, function, capability, class, roots.object, roots.type_type, ] { store.release(object).unwrap(); } store.collect_cycles(); assert_eq!(store.object_count(), 0); }