Files
endorphin-aster-vm/crates/core/tests/native_context.rs
T

900 lines
33 KiB
Rust

//! NativeContext 临时根、委托、观察依赖与完整签名的公开 API 回归;测试仍遵守远程执行约定。
//! Remote-only regression coverage. Do not execute locally.
use endor_core::{
ChangeKey, ContextNativeFunction, ExecutionLimits, NativeCallable, NativeContext, NativeResult,
ObjectRef, Value, Vm, VmError,
};
#[derive(Debug)]
/// bool 控制是否故意失败,两个临时分配用于观察成功/失败清理差异。
struct Allocate(bool);
impl NativeCallable for Allocate {
/// VM 在 NativeContext 内调用此测试分派器,临时分配与输入由上下文保活,返回/委托后移交输出根。
fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result<NativeResult, VmError> {
// 分配一份上下文临时根,成功返回时仅输出另取根移交,其余根由 Drop 归还。
let value = cx.new_object()?;
cx.set_attr(value, "label", Value::Text("scoped".into()))?;
// 故意创建不返回对象,验证正常/报错都不会泄漏临时根。
let _temporary = cx.new_object()?;
// 故意在完成分配后报错,检验 RAII 不依赖回调正常返回。
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() {
// 独立 VM 隔离测试对象图,所有结果根由场景显式归还。
let mut vm = Vm::new().unwrap();
// 分配上下文函数载荷,captures 管理显式托管边,分派器 Arc 不代替它们。
let function = vm
.allocate(ContextNativeFunction::new(
"allocate",
Allocate(false),
vec![],
))
.unwrap();
// VM 调用已完成根移交,返回对象应只有一份调用者根。
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)]
/// 返回目标借用身份,由包装载荷 captures 保活,NativeContext 为 Invoke 输出取得拥有根。
struct Delegate(ObjectRef);
impl NativeCallable for Delegate {
/// VM 在 NativeContext 内调用此测试分派器,临时分配与输入由上下文保活,返回/委托后移交输出根。
fn invoke(&self, _cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
Ok(NativeResult::Invoke {
function: self.0,
arguments: args.to_vec(),
})
}
}
#[test]
/// 验证 intrinsic 检查读取当前载荷的真实 Rust 实现,不能仅按不变函数身份推断。
fn native_implementation_inspection_follows_payload_replacement() {
// 独立 VM 隔离测试对象图,所有结果根由场景显式归还。
let mut vm = Vm::new().unwrap();
// 分配上下文函数载荷,captures 管理显式托管边,分派器 Arc 不代替它们。
let function = vm
.allocate(ContextNativeFunction::new(
"allocate",
Allocate(false),
vec![],
))
.unwrap();
// 借用当前 ContextNativeFunction 载荷,下转型检查不分配对象或执行回调。
let payload = vm
.payload::<ContextNativeFunction>(function, "native")
.unwrap();
assert!(payload.implementation::<Allocate>().is_some());
assert!(payload.implementation::<Delegate>().is_none());
// 记录更新前版本,替换实现后身份不变但版本必须增加。
let version = vm.change_version(function).unwrap();
// 委托目标有测试根,也会由 Delegate 包装载荷显式 captures 保留。
let target = vm
.allocate(ContextNativeFunction::new(
"target",
Allocate(false),
vec![],
))
.unwrap();
vm.replace_payload(
function,
ContextNativeFunction::new("delegate", Delegate(target), vec![target]),
)
.unwrap();
// 借用当前 ContextNativeFunction 载荷,下转型检查不分配对象或执行回调。
let payload = vm
.payload::<ContextNativeFunction>(function, "native")
.unwrap();
assert!(payload.implementation::<Allocate>().is_none());
assert_eq!(payload.implementation::<Delegate>().unwrap().0, target);
assert!(vm.change_version(function).unwrap() > version);
// 逐项归还测试拥有根,包装载荷内部捕获边由 Heap 自动释放。
for object in [function, target] {
vm.release(object).unwrap();
}
assert_eq!(vm.object_count(), 0);
}
#[test]
/// 验证原生委托与目标共享累计预算,预算失败后目标/参数临时根计数恢复。
fn native_delegation_shares_budget_and_cleans_owned_arguments() {
// 独立 VM 隔离测试对象图,所有结果根由场景显式归还。
let mut vm = Vm::new().unwrap();
// 委托目标有测试根,也会由 Delegate 包装载荷显式 captures 保留。
let target = vm
.allocate(ContextNativeFunction::new(
"target",
Allocate(false),
vec![],
))
.unwrap();
// Delegate 返回 Invoke 而不是递归 vm.call,让外层共享预算与清理流程。
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);
// VM 调用已完成根移交,返回对象应只有一份调用者根。
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() {
// 独立 VM 隔离测试对象图,所有结果根由场景显式归还。
let mut vm = Vm::new().unwrap();
// 原始对象有一份分配根,弱身份或通知记录都不应额外持有它。
let object = vm.new_object().unwrap();
// downgrade 只形成借用弱身份,不延长对象寿命。
let weak = vm.downgrade(object).unwrap();
// 成功升级才取得一份独立外部根,随后必须归还。
let upgraded = vm.upgrade(weak).unwrap().unwrap();
vm.release(upgraded).unwrap();
vm.release(object).unwrap();
// 新对象可能复用同槽,但代际不同不能复活 weak。
let replacement = vm.new_object().unwrap();
assert!(vm.upgrade(weak).unwrap().is_none());
// 另一堆升级旧身份应明确报 ForeignHandle,而不是过期 None。
let mut foreign = Vm::new().unwrap();
assert_eq!(foreign.upgrade(weak), Err(VmError::ForeignHandle));
vm.release(replacement).unwrap();
}
#[test]
/// 同对象/字段通知合并最新版本,容器更新发 Contents,通知不充当 GC 根。
fn changes_coalesce_cover_containers_and_do_not_keep_sources_alive() {
// 独立 VM 隔离测试对象图,所有结果根由场景显式归还。
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();
// 取走通知批次同时重置缓存,数据中的源身份不 retain。
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);
// Cell 更新/清空使用载荷替换,通知键为 Contents,不是普通字段。
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() {
// 独立 VM 隔离测试对象图,所有结果根由场景显式归还。
let mut vm = Vm::new().unwrap();
// 原始对象有一份分配根,弱身份或通知记录都不应额外持有它。
let object = vm.new_object().unwrap();
// 制造超过 4096 个不同字段键,重复同键不足以触发通知溢出。
for index in 0..4100 {
vm.set_attr(object, index.to_string(), Value::Nil).unwrap();
}
// 取走通知批次同时重置缓存,数据中的源身份不 retain。
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 {
/// 输入借用身份,包装载荷 captures 负责保活。
source: ObjectRef,
/// 是否故意写旧输入,触发只读保护错误。
mutate: bool,
}
impl NativeCallable for ReadResult {
/// VM 在 NativeContext 内调用此测试分派器,临时分配与输入由上下文保活,返回/委托后移交输出根。
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
// 借用读取 source 的输入字段,读取路径纳入当前只读依赖收集。
let value = cx.store().get_attr(self.source, "value")?;
// 非法分支试图修改进入只读前已存在的 source,应在写屏障处失败。
if self.mutate {
cx.set_attr(self.source, "value", Value::Number(99_i64.into()))?;
}
// 新结果不在进入只读时的保护快照内,允许初始化 copied 字段。
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() {
// 独立 VM 隔离测试对象图,所有结果根由场景显式归还。
let mut vm = Vm::new().unwrap();
// 输入源在调用前创建并初始化,是只读保护与读取依赖的对象。
let source = vm.new_object().unwrap();
vm.set_attr(source, "value", Value::Number(7_i64.into()))
.unwrap();
// 分配上下文函数载荷,captures 管理显式托管边,分派器 Arc 不代替它们。
let function = vm
.allocate(ContextNativeFunction::new(
"read result",
ReadResult {
source,
mutate: false,
},
vec![source],
))
.unwrap();
// 丢弃场景初始化通知,只检查本次只读派生构造是否发布了输入变更。
vm.take_changes();
// 结果是拥有值,reads 是借用观察集合,两个所有权语义不能混同。
let (value, reads) = vm
.call_read_only_with_limits(function, &[], ExecutionLimits::default())
.unwrap();
assert!(reads.contains(&source));
assert!(vm.take_changes().changes.is_empty());
// 检查新结果 copied 值后归还结果根,不能 release 读取依赖句柄。
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;
// 本测试直接使用 Store 精确分类入口,不需要执行器帧。
let mut store = objects::ObjectStore::new().unwrap();
// 普通 Record 不符合 Number,但有效对象应符合 Any。
let object = store.new_object().unwrap();
store
.set_attr(object, "value", Value::Number(1.into()))
.unwrap();
// 只读范围收集整体观察,子区间分类不会清空外层依赖。
let scope = store.begin_read_only();
// 每个子区间重新开始收集,按 LIFO 结束;重复读取也要在新区域出现。
let token = store.begin_read_tracking().unwrap();
assert!(
store
.matches_annotation(&Value::Object(object), AnnotationType::Any)
.unwrap()
);
// 返回 ReadAccess 区分 content_read,句柄仍不拥有根。
let reads = store.end_read_tracking_details(token).unwrap();
assert!(
reads
.iter()
.any(|read| read.object == object && !read.content_read)
);
// 每个子区间重新开始收集,按 LIFO 结束;重复读取也要在新区域出现。
let token = store.begin_read_tracking().unwrap();
assert!(
!store
.matches_annotation(&Value::Object(object), AnnotationType::Number)
.unwrap()
);
// 异堆对象即使匹配 Any 也先拒绝无效归属。
let mut foreign = objects::ObjectStore::new().unwrap();
// 有效但异堆的测试输入,用于排除 Any 跳过句柄校验的错误实现。
let alien = foreign.new_object().unwrap();
assert!(matches!(
store.matches_annotation(&Value::Object(alien), AnnotationType::Any),
Err(VmError::ForeignHandle)
));
foreign.release(alien).unwrap();
// 补一次普通字段读取,与前面的验证读取合并时 content_read 必须为 true。
store.get_attr(object, "value").unwrap();
// 返回 ReadAccess 区分 content_read,句柄仍不拥有根。
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);
}