Files
endorphin-aster-vm/crates/objects/tests/attribute_storage.rs
T

250 lines
8.4 KiB
Rust

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<Option<ObjectRef>, 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<Option<ObjectRef>, 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<objects::CallSignature> {
Some(objects::CallSignature::exact(
vec![objects::AnnotationType::Any],
objects::AnnotationType::Number,
objects::CallEffect::ReadOnly,
))
}
fn invoke(
&self,
_: &mut objects::NativeContext<'_>,
_: &[Value],
) -> Result<objects::NativeResult, VmError> {
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);
}