Files
endorphin-aster-vm/crates/aster-core/tests/runtime.rs
T
codex-bot 891defb34c
Verify databases and deploy Aster VM / verify-and-deploy (push) Waiting to run
Add Endor builtin objects and lossless unified number representations
2026-10-18 19:54:17 +08:00

481 lines
15 KiB
Rust

use std::any::Any;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use aster_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);
}