//! Remote-only regression coverage. Do not execute locally. use endor_core::{ ChangeKey, ContextNativeFunction, ExecutionLimits, NativeCallable, NativeContext, NativeResult, ObjectRef, Value, Vm, VmError, }; #[derive(Debug)] struct Allocate(bool); impl NativeCallable for Allocate { fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result { let value = cx.new_object()?; cx.set_attr(value, "label", Value::Text("scoped".into()))?; let _temporary = cx.new_object()?; if self.0 { return Err(VmError::InvalidArgument("intentional failure")); } Ok(NativeResult::Return(Value::Object(value))) } } #[test] fn scoped_allocations_return_one_root_and_clean_up_errors() { let mut vm = Vm::new().unwrap(); let function = vm .allocate(ContextNativeFunction::new( "allocate", Allocate(false), vec![], )) .unwrap(); let value = vm.call(function, &[]).unwrap(); let Value::Object(object) = value else { panic!("expected object") }; assert_eq!(vm.ref_count(object).unwrap(), 1); assert_eq!(vm.object_count(), 2); vm.release_value(value).unwrap(); vm.replace_payload( function, ContextNativeFunction::new("fail", Allocate(true), vec![]), ) .unwrap(); assert!(vm.call(function, &[]).is_err()); assert_eq!(vm.object_count(), 1); vm.release(function).unwrap(); assert_eq!(vm.object_count(), 0); } #[derive(Debug)] struct Delegate(ObjectRef); impl NativeCallable for Delegate { fn invoke(&self, _cx: &mut NativeContext<'_>, args: &[Value]) -> Result { Ok(NativeResult::Invoke { function: self.0, arguments: args.to_vec(), }) } } #[test] fn native_delegation_shares_budget_and_cleans_owned_arguments() { let mut vm = Vm::new().unwrap(); let target = vm .allocate(ContextNativeFunction::new( "target", Allocate(false), vec![], )) .unwrap(); let delegate = vm .allocate(ContextNativeFunction::new( "delegate", Delegate(target), vec![target], )) .unwrap(); let arg = vm.new_object().unwrap(); assert!( vm.call_with_limits( delegate, &[Value::Object(arg)], ExecutionLimits { max_frames: 1, max_instructions: 1 } ) .is_err() ); assert_eq!(vm.ref_count(arg).unwrap(), 1); assert_eq!(vm.ref_count(target).unwrap(), 2); let value = vm.call(delegate, &[]).unwrap(); vm.release_value(value).unwrap(); vm.release(delegate).unwrap(); vm.release(target).unwrap(); vm.release(arg).unwrap(); assert_eq!(vm.object_count(), 0); } #[test] fn weak_references_expire_without_retaining_reused_slots() { let mut vm = Vm::new().unwrap(); let object = vm.new_object().unwrap(); let weak = vm.downgrade(object).unwrap(); let upgraded = vm.upgrade(weak).unwrap().unwrap(); vm.release(upgraded).unwrap(); vm.release(object).unwrap(); let replacement = vm.new_object().unwrap(); assert!(vm.upgrade(weak).unwrap().is_none()); let mut foreign = Vm::new().unwrap(); assert_eq!(foreign.upgrade(weak), Err(VmError::ForeignHandle)); vm.release(replacement).unwrap(); } #[test] fn changes_coalesce_cover_containers_and_do_not_keep_sources_alive() { let mut vm = Vm::new().unwrap(); let object = vm.new_object().unwrap(); vm.set_attr(object, "value", Value::Bool(false)).unwrap(); vm.set_attr(object, "value", Value::Bool(true)).unwrap(); let version = vm.change_version(object).unwrap(); let changes = vm.take_changes(); assert!(!changes.rescan_required); assert_eq!(changes.changes.len(), 1); assert_eq!(changes.changes[0].key, ChangeKey::Attribute("value".into())); assert_eq!(changes.changes[0].version, version); let cell = vm.new_cell(None).unwrap(); vm.cell_set(cell, Value::Object(object)).unwrap(); vm.cell_clear(cell).unwrap(); assert_eq!(vm.take_changes().changes[0].key, ChangeKey::Contents); vm.set_attr(object, "self", Value::Object(object)).unwrap(); vm.release(object).unwrap(); assert_eq!(vm.collect_cycles(), 1); vm.release(cell).unwrap(); assert_eq!(vm.object_count(), 0); } #[test] fn notification_overflow_requires_rescan_instead_of_losing_changes_silently() { let mut vm = Vm::new().unwrap(); let object = vm.new_object().unwrap(); for index in 0..4100 { vm.set_attr(object, index.to_string(), Value::Nil).unwrap(); } let changes = vm.take_changes(); assert!(changes.rescan_required); assert!(changes.changes.is_empty()); vm.set_attr(object, "after", Value::Nil).unwrap(); assert_eq!(vm.take_changes().changes.len(), 1); vm.release(object).unwrap(); } #[derive(Debug)] struct ReadResult { source: ObjectRef, mutate: bool, } impl NativeCallable for ReadResult { fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { let value = cx.store().get_attr(self.source, "value")?; if self.mutate { cx.set_attr(self.source, "value", Value::Number(99_i64.into()))?; } let result = cx.new_object()?; cx.set_attr(result, "copied", value)?; Ok(NativeResult::Return(Value::Object(result))) } } #[test] fn read_only_calls_track_borrowed_dependencies_allow_results_and_restore_after_error() { let mut vm = Vm::new().unwrap(); let source = vm.new_object().unwrap(); vm.set_attr(source, "value", Value::Number(7_i64.into())) .unwrap(); let function = vm .allocate(ContextNativeFunction::new( "read result", ReadResult { source, mutate: false, }, vec![source], )) .unwrap(); vm.take_changes(); let (value, reads) = vm .call_read_only_with_limits(function, &[], ExecutionLimits::default()) .unwrap(); assert!(reads.contains(&source)); assert!(vm.take_changes().changes.is_empty()); let Value::Object(result) = value else { panic!("result") }; assert_eq!( vm.get_attr(result, "copied").unwrap(), Value::Number(7_i64.into()) ); vm.release(result).unwrap(); vm.replace_payload( function, ContextNativeFunction::new( "illegal write", ReadResult { source, mutate: true, }, vec![source], ), ) .unwrap(); assert!( vm.call_read_only_with_limits(function, &[], ExecutionLimits::default()) .is_err() ); assert_eq!( vm.get_attr(source, "value").unwrap(), Value::Number(7_i64.into()) ); vm.set_attr(source, "value", Value::Number(8_i64.into())) .unwrap(); vm.release(function).unwrap(); vm.release(source).unwrap(); assert_eq!(vm.object_count(), 0); } #[test] fn annotation_reads_remain_dependencies_without_becoming_program_content_reads() { use endor_core::AnnotationType; let mut store = objects::ObjectStore::new().unwrap(); let object = store.new_object().unwrap(); store .set_attr(object, "value", Value::Number(1.into())) .unwrap(); let scope = store.begin_read_only(); let token = store.begin_read_tracking().unwrap(); assert!( store .matches_annotation(&Value::Object(object), AnnotationType::Any) .unwrap() ); let reads = store.end_read_tracking_details(token).unwrap(); assert!( reads .iter() .any(|read| read.object == object && !read.content_read) ); let token = store.begin_read_tracking().unwrap(); assert!( !store .matches_annotation(&Value::Object(object), AnnotationType::Number) .unwrap() ); let mut foreign = objects::ObjectStore::new().unwrap(); let alien = foreign.new_object().unwrap(); assert!(matches!( store.matches_annotation(&Value::Object(alien), AnnotationType::Any), Err(VmError::ForeignHandle) )); foreign.release(alien).unwrap(); store.get_attr(object, "value").unwrap(); let reads = store.end_read_tracking_details(token).unwrap(); assert!( reads .iter() .any(|read| read.object == object && read.content_read) ); assert!(store.end_read_only(scope).unwrap().contains(&object)); store.release(object).unwrap(); assert_eq!(store.object_count(), 0); } #[derive(Debug)] struct TypedNative { signature: endor_core::CallSignature, calls: std::sync::Arc, delegate: Option, allocate_result: bool, } impl NativeCallable for TypedNative { fn signature(&self) -> Option { Some(self.signature.clone()) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); if let Some(function) = self.delegate { return Ok(NativeResult::Invoke { function, arguments: args.to_vec(), }); } Ok(NativeResult::Return(if self.allocate_result { Value::Object(cx.new_object()?) } else { args.last().cloned().unwrap_or(Value::Nil) })) } } #[test] fn native_types_preflight_arguments_and_clean_wrong_result_roots() { use endor_core::{AnnotationType as T, CallEffect, CallSignature}; use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; let mut vm = Vm::new().unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let function = vm .allocate(ContextNativeFunction::new( "typed", TypedNative { signature: CallSignature::exact(vec![T::Number], T::Number, CallEffect::ReadOnly), calls: calls.clone(), delegate: None, allocate_result: true, }, vec![], )) .unwrap(); assert!( matches!(vm.call(function, &[Value::Text("bad".into())]), Err(VmError::TypeMismatch { position, .. }) if position == "parameter 1") ); assert_eq!(calls.load(Ordering::SeqCst), 0); assert!( matches!(vm.call(function, &[Value::Number(7.into())]), Err(VmError::TypeMismatch { position, .. }) if position == "return") ); assert_eq!(calls.load(Ordering::SeqCst), 1); assert_eq!(vm.object_count(), 1); vm.release(function).unwrap(); assert_eq!(vm.object_count(), 0); } #[test] fn delegated_source_results_obey_every_native_contract_without_extra_frames() { use endor_core::{ AnnotationType as T, CallEffect, CallSignature, CodeLayout, EndorCode, Instruction, Opcode, }; use std::sync::{Arc, atomic::AtomicUsize}; let mut vm = Vm::new().unwrap(); let code = EndorCode::from_instructions( &[ Instruction::new(Opcode::LoadConst, 0), Instruction::simple(Opcode::ReturnValue), ], vec![Value::Text("source result".into())], CodeLayout::default(), ) .unwrap(); let code = vm.new_code(code).unwrap(); let globals = vm.new_dict().unwrap(); let source = vm .new_function("source", code, globals, vec![], vec![]) .unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let inner = vm .allocate(ContextNativeFunction::new( "inner", TypedNative { signature: CallSignature::exact(vec![], T::String, CallEffect::ReadOnly), calls: calls.clone(), delegate: Some(source), allocate_result: false, }, vec![source], )) .unwrap(); let outer = vm .allocate(ContextNativeFunction::new( "outer", TypedNative { signature: CallSignature::exact(vec![], T::Number, CallEffect::ReadOnly), calls, delegate: Some(inner), allocate_result: false, }, vec![inner], )) .unwrap(); let limits = ExecutionLimits { max_frames: 1, max_instructions: 100, }; assert_eq!( vm.call_with_limits(inner, &[], limits).unwrap(), Value::Text("source result".into()) ); assert!( matches!(vm.call_with_limits(outer, &[], limits), Err(VmError::TypeMismatch { function, position, .. }) if function == "outer" && position == "return") ); assert!(matches!( vm.call_with_limits( outer, &[], ExecutionLimits { max_frames: 0, ..limits } ), Err(VmError::ExecutionLimit("frames")) )); for object in [outer, inner, source, globals, code] { vm.release(object).unwrap(); } vm.collect_cycles(); assert_eq!(vm.object_count(), 0); } #[test] fn signatures_trace_nominal_types_and_bound_receivers_use_the_actual_contract() { use endor_core::{AnnotationType as T, CallEffect, CallSignature, TypeConstructor}; use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; let mut vm = Vm::new().unwrap(); let roots = vm.bootstrap_types().unwrap(); let definition = vm .new_type("Receiver", roots.object, TypeConstructor::Record) .unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let function = vm .allocate(ContextNativeFunction::new( "echo", TypedNative { signature: CallSignature::exact( vec![T::Nominal(definition), T::Number], T::Number, CallEffect::ReadOnly, ), calls: calls.clone(), delegate: None, allocate_result: false, }, vec![], )) .unwrap(); // Metadata alone traces the definition. let weak = vm.downgrade(definition).unwrap(); vm.define_method(definition, "echo", function).unwrap(); let receiver = vm.new_object().unwrap(); vm.attach_type(receiver, definition).unwrap(); vm.release(definition).unwrap(); let Value::Object(bound) = vm.bind_attr(receiver, "echo").unwrap() else { panic!("bound method") }; assert_eq!( vm.call_arity(bound).unwrap(), Some(endor_core::CallArity::exact(1)) ); assert!(vm.call(bound, &[Value::Bool(true)]).is_err()); assert_eq!(calls.load(Ordering::SeqCst), 0); assert_eq!( vm.call(bound, &[Value::Number(9.into())]).unwrap(), Value::Number(9.into()) ); for object in [bound, receiver, function, roots.object, roots.type_type] { vm.release(object).unwrap(); } vm.collect_cycles(); assert!(vm.upgrade(weak).unwrap().is_none()); assert_eq!(vm.object_count(), 0); } #[test] fn declared_host_effects_are_rejected_before_read_only_dispatch() { use endor_core::{AnnotationType as T, CallEffect, CallSignature}; use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; for effect in [CallEffect::Mutating, CallEffect::External] { let mut vm = Vm::new().unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let function = vm .allocate(ContextNativeFunction::new( "effect", TypedNative { signature: CallSignature::exact(vec![], T::Unit, effect), calls: calls.clone(), delegate: None, allocate_result: false, }, vec![], )) .unwrap(); assert!( vm.call_read_only_with_limits(function, &[], ExecutionLimits::default()) .is_err() ); assert_eq!(calls.load(Ordering::SeqCst), 0); assert_eq!(vm.call(function, &[]).unwrap(), Value::Nil); vm.release(function).unwrap(); assert_eq!(vm.object_count(), 0); } } #[test] fn legacy_native_signatures_check_optional_variadic_types_and_metadata() { use endor_core::{AnnotationType as T, CallArity, CallEffect, CallSignature, NativeFunction}; fn echo(args: &[Value]) -> Result { Ok(args.last().cloned().unwrap_or(Value::Nil)) } let mut vm = Vm::new().unwrap(); let function = vm .allocate( NativeFunction::new("legacy", echo).with_signature(CallSignature { arity: CallArity::range(0, usize::MAX), parameters: vec![], variadic: Some(T::Number.into()), returns: T::Number.into(), effect: CallEffect::ReadOnly, }), ) .unwrap(); assert_eq!( vm.call( function, &[Value::Number(1.into()), Value::Number(2.into())] ) .unwrap(), Value::Number(2.into()) ); assert!(vm.call(function, &[Value::Bool(true)]).is_err()); assert!( matches!(vm.call(function, &[]), Err(VmError::TypeMismatch { position, .. }) if position == "return") ); let invalid = vm .allocate( NativeFunction::new("invalid", echo) .with_signature(CallSignature::exact( vec![T::Number], T::Number, CallEffect::ReadOnly, )) .with_arity(CallArity::exact(0)), ) .unwrap(); assert!(vm.call_arity(invalid).is_err()); assert!(vm.call(invalid, &[]).is_err()); vm.release(invalid).unwrap(); vm.release(function).unwrap(); assert_eq!(vm.object_count(), 0); } #[derive(Debug)] struct EraseThenDelegate { signature: endor_core::CallSignature, target: ObjectRef, } impl NativeCallable for EraseThenDelegate { fn signature(&self) -> Option { Some(self.signature.clone()) } fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { cx.replace_payload(cx.callee(), endor_core::Record)?; Ok(NativeResult::Invoke { function: self.target, arguments: vec![], }) } } #[test] fn delegated_return_type_survives_removal_of_the_original_signature_edges() { use endor_core::{ AnnotationType as T, CallEffect, CallSignature, CodeLayout, EndorCode, Instruction, Opcode, TypeConstructor, }; let mut vm = Vm::new().unwrap(); let roots = vm.bootstrap_types().unwrap(); let definition = vm .new_type("Transient", roots.object, TypeConstructor::Record) .unwrap(); let weak = vm.downgrade(definition).unwrap(); let code = EndorCode::from_instructions( &[ Instruction::new(Opcode::LoadConst, 0), Instruction::simple(Opcode::ReturnValue), ], vec![Value::Nil], CodeLayout::default(), ) .unwrap(); let code = vm.new_code(code).unwrap(); let globals = vm.new_dict().unwrap(); let source = vm .new_function("source", code, globals, vec![], vec![]) .unwrap(); let mut signature = CallSignature::exact(vec![], T::Any, CallEffect::Mutating); signature.returns = endor_core::CallType::Union(vec![T::Nominal(definition), T::Number]); let wrapper = vm .allocate(ContextNativeFunction::new( "erased", EraseThenDelegate { signature, target: source, }, vec![source], )) .unwrap(); vm.release(definition).unwrap(); assert!( matches!(vm.call(wrapper, &[]), Err(VmError::TypeMismatch { function, expected, .. }) if function == "erased" && expected == "Transient | Number") ); assert!(vm.upgrade(weak).unwrap().is_none()); for object in [ wrapper, source, code, globals, roots.object, roots.type_type, ] { vm.release(object).unwrap(); } vm.collect_cycles(); assert_eq!(vm.object_count(), 0); } #[test] fn native_union_and_nullable_contracts_accept_each_branch_and_reject_invalid_metadata() { use endor_core::{AnnotationType as T, CallEffect, CallSignature, CallType}; use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; let mut vm = Vm::new().unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let mut signature = CallSignature::exact(vec![T::Any], T::Any, CallEffect::ReadOnly); signature.parameters[0] = CallType::Union(vec![T::String, T::Bool]); signature.returns = signature.parameters[0].clone(); let function = vm .allocate(ContextNativeFunction::new( "union", TypedNative { signature, calls: calls.clone(), delegate: None, allocate_result: false, }, vec![], )) .unwrap(); for value in [ Value::Text("capture".into()), Value::Bool(true), Value::Bool(false), ] { assert_eq!( vm.call(function, std::slice::from_ref(&value)).unwrap(), value ); } assert!( matches!(vm.call(function, &[Value::Number(3.into())]), Err(VmError::TypeMismatch { expected, .. }) if expected == "String | Bool") ); assert_eq!(calls.load(Ordering::SeqCst), 3); let mut signature = CallSignature::exact(vec![T::Any], T::Any, CallEffect::ReadOnly); signature.parameters[0] = CallType::nullable(T::Number); signature.returns = signature.parameters[0].clone(); vm.replace_payload( function, ContextNativeFunction::new( "nullable", TypedNative { signature, calls: calls.clone(), delegate: None, allocate_result: false, }, vec![], ), ) .unwrap(); for value in [Value::Nil, Value::Number(2.into())] { assert_eq!( vm.call(function, std::slice::from_ref(&value)).unwrap(), value ); } let record = vm.new_object().unwrap(); let mut signature = CallSignature::exact(vec![T::Any], T::Any, CallEffect::ReadOnly); // A matching first branch must not hide a later invalid nominal definition. signature.parameters[0] = CallType::Union(vec![T::Any, T::Nominal(record)]); vm.replace_payload( function, ContextNativeFunction::new( "invalid union", TypedNative { signature, calls: calls.clone(), delegate: None, allocate_result: false, }, vec![], ), ) .unwrap(); assert!(vm.call(function, &[Value::Nil]).is_err()); assert_eq!(calls.load(Ordering::SeqCst), 5); let mut signature = CallSignature::exact(vec![], T::Any, CallEffect::ReadOnly); signature.returns = CallType::Union(vec![]); vm.replace_payload( function, ContextNativeFunction::new( "empty union", TypedNative { signature, calls: calls.clone(), delegate: None, allocate_result: false, }, vec![], ), ) .unwrap(); assert!(vm.call(function, &[]).is_err()); assert_eq!(calls.load(Ordering::SeqCst), 5); vm.release(record).unwrap(); vm.release(function).unwrap(); assert_eq!(vm.object_count(), 0); } #[test] fn delegated_effects_keep_the_original_read_only_scope_and_check_the_actual_target() { use endor_core::{AnnotationType as T, CallEffect, CallSignature}; use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; let mut vm = Vm::new().unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let target_calls = Arc::new(AtomicUsize::new(0)); let target = vm .allocate(ContextNativeFunction::new( "target", TypedNative { signature: CallSignature::exact(vec![], T::Unit, CallEffect::Mutating), calls: target_calls.clone(), delegate: None, allocate_result: false, }, vec![], )) .unwrap(); let wrapper = vm .allocate(ContextNativeFunction::new( "wrapper", TypedNative { signature: CallSignature::exact(vec![], T::Unit, CallEffect::Delegated), calls: calls.clone(), delegate: Some(target), allocate_result: false, }, vec![target], )) .unwrap(); assert!( vm.call_read_only_with_limits(wrapper, &[], ExecutionLimits::default()) .is_err() ); assert_eq!(calls.load(Ordering::SeqCst), 1); assert_eq!(target_calls.load(Ordering::SeqCst), 0); assert_eq!(vm.call(wrapper, &[]).unwrap(), Value::Nil); vm.replace_payload( target, ContextNativeFunction::new( "pure", TypedNative { signature: CallSignature::exact(vec![], T::Unit, CallEffect::ReadOnly), calls: target_calls.clone(), delegate: None, allocate_result: false, }, vec![], ), ) .unwrap(); assert_eq!( vm.call_read_only_with_limits(wrapper, &[], ExecutionLimits::default()) .unwrap() .0, Value::Nil ); for object in [wrapper, target] { vm.release(object).unwrap(); } assert_eq!(vm.object_count(), 0); }