789 lines
26 KiB
Rust
789 lines
26 KiB
Rust
//! 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<NativeResult, VmError> {
|
|
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<NativeResult, VmError> {
|
|
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<NativeResult, VmError> {
|
|
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<std::sync::atomic::AtomicUsize>,
|
|
delegate: Option<ObjectRef>,
|
|
allocate_result: bool,
|
|
}
|
|
impl NativeCallable for TypedNative {
|
|
fn signature(&self) -> Option<endor_core::CallSignature> {
|
|
Some(self.signature.clone())
|
|
}
|
|
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
|
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<Value, VmError> {
|
|
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<endor_core::CallSignature> {
|
|
Some(self.signature.clone())
|
|
}
|
|
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
|
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);
|
|
}
|