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

2079 lines
79 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 通过手工指令验证 VM 求值、根生命周期及暂停行为,测试框架直接调用,无需源码解析器。
//! op/simple/make_code 统一构造已验证代码;错误编码用 EndorCode 直接检验构造阶段拒绝。
use std::any::Any;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use endor_core::{
CodeLayout, CompareOp, EndorCode, EndorCodeObject, EndorFunctionObject,
EndorNativeFunctionObject, ExecutionLimits, Instruction, ObjectPayload, ObjectRef, Opcode,
Value, Vm, VmError,
};
/// 测试辅助,构造完整 u32 操作数逻辑指令,实际扩展前缀由编码器生成。
fn op(opcode: Opcode, argument: u32) -> Instruction {
Instruction::new(opcode, argument)
}
/// 测试辅助,构造参数为零的无操作数指令,不手写物理编码。
fn simple(opcode: Opcode) -> Instruction {
Instruction::simple(opcode)
}
/// 测试辅助,创建直接文本 Value,不分配堆字符串。
fn text(value: &str) -> Value {
Value::Text(value.into())
}
/// 测试辅助,编码并验证指令/常量/布局后分配代码拥有根;失败直接使测试失败。
fn make_code(
vm: &mut Vm,
instructions: &[Instruction],
constants: Vec<Value>,
layout: CodeLayout,
) -> ObjectRef {
vm.new_code(EndorCode::from_instructions(instructions, constants, layout).unwrap())
.unwrap()
}
/// 其他执行测试复用的最小返回常量代码,根由调用测试归还。
fn return_constant(vm: &mut Vm, value: Value) -> ObjectRef {
make_code(
vm,
&[op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)],
vec![value],
CodeLayout::default(),
)
}
#[derive(Debug)]
/// 可选原地赋值测试载荷;只保存 Rust 原子数,不含可变托管对象边。
struct SharedReference(AtomicUsize);
impl ObjectPayload for SharedReference {
/// Store 下转型识别测试载荷;与语言名义类型分开。
fn as_any(&self) -> &dyn Any {
self
}
/// 测试错误/诊断类别。
fn kind_name(&self) -> &'static str {
"shared-reference"
}
/// Store 可选旧式原地赋值协议,仅支持同类且先校验后原子更新,不改变托管引用边。
fn assign_from(&self, source: &dyn ObjectPayload) -> Result<bool, VmError> {
// 异类载荷返回 false 表示不做原地赋值,不是错误,也不修改目标。
let Some(source) = source.as_any().downcast_ref::<Self>() else {
return Ok(false);
};
// 先读候选内容再校验,拒绝值不能留下半写状态。
let value = source.0.load(Ordering::SeqCst);
// 注入明确协议错误,验证执行器清理而不是静默重绑定。
if value == 99 {
return Err(VmError::InvalidArgument("rejected replacement"));
}
// 只更新宿主原子状态,不引入未 trace 的托管引用边。
self.0.store(value, Ordering::SeqCst);
Ok(true)
}
}
#[test]
/// 局部与全局赋值都保留共享对象旧身份并更新内容,已有 alias 可见,清理不泄漏根。
fn opt_in_assignment_keeps_alias_identity_ordinary_values_and_root_accounting() {
// 同一字节码模板分别覆盖 StoreFast 与 StoreGlobal,避免只测试一条赋值入口。
for local in [false, true] {
// 独立 VM 隔离对象计数/全局绑定,清理断言只针对本场景。
let mut vm = Vm::new().unwrap();
// 共享目标以 1 初始化,同时另由 alias 持有;赋值后身份应保持。
let old = vm.allocate(SharedReference(AtomicUsize::new(1))).unwrap();
// 候选共享值初始化为 2,复制内容到 old 不等于让 alias 指向此新身份。
let new = vm.allocate(SharedReference(AtomicUsize::new(2))).unwrap();
// 函数执行环境与名称表索引对应,字典内部边独立于测试外部根。
let globals = vm.new_dict().unwrap();
vm.dict_set(globals, text("alias"), Value::Object(old))
.unwrap();
// 只切换变量访问策略,操作数零在两类布局中分别代表槽/全局名。
let (store, load) = if local {
(Opcode::StoreFast, Opcode::LoadFast)
} else {
(Opcode::StoreGlobal, Opcode::LoadGlobal)
};
// 操作数和布局在构造时校验,execute 期间测试实际值流与引用行为。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(store, 0),
op(Opcode::LoadConst, 1),
op(store, 0),
op(load, 0),
simple(Opcode::ReturnValue),
],
vec![Value::Object(old), Value::Object(new)],
// 局部模板使用 locals,另一分支使用 names;不能把两类索引混用。
if local {
CodeLayout {
locals: vec!["value".into()],
..CodeLayout::default()
}
} else {
CodeLayout {
names: vec!["value".into()],
..CodeLayout::default()
}
},
);
// 执行结果拥有根,读取完成后必须单独归还,不能当作纯借用复制。
let value = vm.execute(code, globals).unwrap();
assert_eq!(value, Value::Object(old));
assert_eq!(
vm.dict_get(globals, &text("alias")).unwrap(),
Some(Value::Object(old))
);
assert_eq!(
vm.payload::<SharedReference>(old, "shared-reference")
.unwrap()
.0
.load(Ordering::SeqCst),
2
);
// 归还结果根后,代码常量、全局 alias 和测试根仍按各自拥有关系存在。
vm.release_value(value).unwrap();
// 归还代码根同时移除常量表内部边,不重复释放常量对象的测试根。
vm.release(code).unwrap();
// 移除全局内部边,使后续测试根归还后图真正为空。
vm.dict_clear(globals).unwrap();
vm.release(globals).unwrap();
vm.release(old).unwrap();
vm.release(new).unwrap();
assert_eq!(vm.object_count(), 0);
}
}
#[test]
/// 赋值协议拒绝新值时旧对象内容/全局绑定不变,执行错误根由统一帧清理归还。
fn failed_opt_in_assignment_preserves_target_and_cleans_execution_roots() {
// 独立 VM 隔离对象计数/全局绑定,清理断言只针对本场景。
let mut vm = Vm::new().unwrap();
// 共享目标以 1 初始化,同时另由 alias 持有;赋值后身份应保持。
let old = vm.allocate(SharedReference(AtomicUsize::new(1))).unwrap();
// 以哨兵值 99 触发校验拒绝,不允许污染旧目标值。
let rejected = vm.allocate(SharedReference(AtomicUsize::new(99))).unwrap();
// 函数执行环境与名称表索引对应,字典内部边独立于测试外部根。
let globals = vm.new_dict().unwrap();
vm.dict_set(globals, text("value"), Value::Object(old))
.unwrap();
// 操作数和布局在构造时校验,execute 期间测试实际值流与引用行为。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::StoreGlobal, 0),
op(Opcode::LoadConst, 1),
simple(Opcode::ReturnValue),
],
vec![Value::Object(rejected), Value::Nil],
CodeLayout {
names: vec!["value".into()],
..CodeLayout::default()
},
);
assert!(vm.execute(code, globals).is_err());
assert_eq!(
vm.payload::<SharedReference>(old, "shared-reference")
.unwrap()
.0
.load(Ordering::SeqCst),
1
);
assert_eq!(
vm.dict_get(globals, &text("value")).unwrap(),
Some(Value::Object(old))
);
// 归还代码根同时移除常量表内部边,不重复释放常量对象的测试根。
vm.release(code).unwrap();
// 移除全局内部边,使后续测试根归还后图真正为空。
vm.dict_clear(globals).unwrap();
vm.release(globals).unwrap();
vm.release(old).unwrap();
vm.release(rejected).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
/// 检查四字节参数前缀编码,再用第 256 号常量验证扩展操作数执行与偏移。
fn wordcode_extended_operands_decode_and_execute_without_truncation() {
// 只编码操作数不做常量索引验证,用固定字节向量证明高位未被截断。
let encoded = Instruction::encode_all(&[op(Opcode::LoadConst, 0x01020304)]).unwrap();
assert_eq!(encoded, vec![255, 1, 255, 2, 255, 3, 1, 4]);
// 独立 VM 隔离对象计数/全局绑定,清理断言只针对本场景。
let mut vm = Vm::new().unwrap();
// 常量表至少 257 项才能合法引用索引 256。
let mut constants = vec![Value::Nil; 257];
// 将边界索引放置独特值,执行返回可检出仅保留低八位的错误。
constants[256] = text("extended");
// 操作数和布局在构造时校验,execute 期间测试实际值流与引用行为。
let code = make_code(
&mut vm,
&[op(Opcode::LoadConst, 256), simple(Opcode::ReturnValue)],
constants,
CodeLayout::default(),
);
// 借用已验证逻辑指令检查起始/末尾代码单元位置及最大栈高。
let executable = vm.payload::<EndorCodeObject>(code, "code").unwrap().code();
assert_eq!(executable.instructions()[0].offset, 0);
assert_eq!(executable.instructions()[0].next_offset, 2);
assert_eq!(executable.stack_size(), 1);
// 函数执行环境与名称表索引对应,字典内部边独立于测试外部根。
let globals = vm.new_dict().unwrap();
assert_eq!(vm.execute(code, globals).unwrap(), text("extended"));
}
#[test]
/// 空/截断/未知/前缀过多/无参带数据及跳入前缀中部在执行前拒绝。
fn malformed_wordcode_and_unaligned_jumps_are_rejected_at_construction() {
// 各非法物理流独立走 EndorCode 构造,均须返回 InvalidBytecode。
for bytes in [
vec![],
vec![0],
vec![254, 0],
vec![255, 1],
vec![255, 1, 255, 1, 255, 1, 255, 1, 1, 0],
vec![13, 1],
] {
assert!(matches!(
EndorCode::new(bytes, vec![Value::Nil], vec![], vec![]),
Err(VmError::InvalidBytecode { .. })
));
}
// 跳转目标 2 位于从代码单元 1 开始的扩展 LOAD_CONST 内部,不能作为逻辑起点。
// 故意跳至带前缀 LOAD_CONST 的第二单元,而非该逻辑指令首前缀。
let bytes = vec![14, 1, 255, 1, 1, 0, 13, 0];
assert!(EndorCode::new(bytes, vec![Value::Nil; 257], vec![], vec![]).is_err());
assert!(
EndorCode::from_instructions(
&[op(Opcode::LoadConst, 1), simple(Opcode::ReturnValue)],
vec![Value::Nil],
CodeLayout::default()
)
.is_err()
);
}
#[test]
/// 检查栈下溢、落出代码、变高回边、无 Cell 槽与分支汇合高度不一致。
fn verifier_rejects_underflow_fallthrough_bad_cells_and_inconsistent_stack_joins() {
// 每个逻辑指令序列隔离一种栈/控制流/槽布局错误。
let cases = [
vec![simple(Opcode::PopTop), simple(Opcode::ReturnValue)],
vec![op(Opcode::LoadConst, 0)],
vec![op(Opcode::LoadConst, 0), op(Opcode::JumpBackward, 2)],
vec![op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
vec![
op(Opcode::LoadConst, 0),
op(Opcode::PopJumpIfFalse, 2),
op(Opcode::LoadConst, 0),
op(Opcode::JumpForward, 1),
simple(Opcode::Nop),
op(Opcode::LoadConst, 0),
simple(Opcode::ReturnValue),
],
];
// 相同常量和空布局下逐例验证,不能用执行时崩溃替代静态拒绝。
for instructions in cases {
assert!(
EndorCode::from_instructions(&instructions, vec![Value::Nil], CodeLayout::default())
.is_err()
);
}
// 把局部 captured 明确标为 Cell,再用 LoadFast 证明访问操作码必须匹配槽种类。
let layout = CodeLayout {
locals: vec!["captured".into()],
cell_variables: vec!["captured".into()],
..CodeLayout::default()
};
assert!(
EndorCode::from_instructions(
&[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)],
vec![],
layout
)
.is_err()
);
}
#[test]
/// 验证 Load/StoreFast、全局存取和返回构成完整值流,成功不产生错误回溯。
fn execute_moves_values_between_constants_local_slots_and_globals() {
// 独立 VM 隔离对象计数/全局绑定,清理断言只针对本场景。
let mut vm = Vm::new().unwrap();
assert_eq!(vm.object_count(), 0);
// 函数执行环境与名称表索引对应,字典内部边独立于测试外部根。
let globals = vm.new_dict().unwrap();
// 操作数和布局在构造时校验,execute 期间测试实际值流与引用行为。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::StoreFast, 0),
op(Opcode::LoadFast, 0),
op(Opcode::StoreGlobal, 0),
op(Opcode::LoadGlobal, 0),
simple(Opcode::ReturnValue),
],
vec![text("hello")],
CodeLayout {
locals: vec!["local".into()],
names: vec!["saved".into()],
..CodeLayout::default()
},
);
assert_eq!(vm.execute(code, globals).unwrap(), text("hello"));
assert_eq!(
vm.dict_get(globals, &text("saved")).unwrap(),
Some(text("hello"))
);
assert!(vm.last_traceback().is_empty());
// 归还代码根同时移除常量表内部边,不重复释放常量对象的测试根。
vm.release(code).unwrap();
vm.release(globals).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[derive(Debug)]
/// 驱动循环条件的无语言类型原生载荷,Arc 计数器由测试在 VM 外观察。
struct Tick(Arc<AtomicUsize>);
impl ObjectPayload for Tick {
/// Store 下转型识别测试载荷;与语言名义类型分开。
fn as_any(&self) -> &dyn Any {
self
}
/// 测试错误/诊断类别。
fn kind_name(&self) -> &'static str {
"tick"
}
/// Tick 旧式原生回调,前三次返回真、第四次为假,驱动可终止的字节码回边循环。
fn call(&self, arguments: &[Value]) -> Result<Value, VmError> {
assert!(arguments.is_empty());
Ok(Value::Bool(self.0.fetch_add(1, Ordering::SeqCst) < 3))
}
}
#[test]
/// 条件分支从指令末尾计算相对距离,回边重进 tick 调用,最后返回 done。
fn relative_branches_and_back_edges_execute_a_loop() {
// 独立 VM 隔离对象计数/全局绑定,清理断言只针对本场景。
let mut vm = Vm::new().unwrap();
// VM 外原子计数用于确认循环调用次数,不通过返回文本猜测回边是否执行。
let count = Arc::new(AtomicUsize::new(0));
// Tick 回调载荷持有 Rust Arc,返回直接 Bool,无托管捕获边。
let tick = vm.allocate(Tick(count.clone())).unwrap();
// 函数执行环境与名称表索引对应,字典内部边独立于测试外部根。
let globals = vm.new_dict().unwrap();
vm.dict_set(globals, text("tick"), Value::Object(tick))
.unwrap();
// 操作数和布局在构造时校验,execute 期间测试实际值流与引用行为。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadGlobal, 0),
simple(Opcode::PushNull),
op(Opcode::Call, 0),
op(Opcode::PopJumpIfFalse, 1),
op(Opcode::JumpBackward, 5),
op(Opcode::LoadConst, 0),
simple(Opcode::ReturnValue),
],
vec![text("done")],
CodeLayout {
names: vec!["tick".into()],
..CodeLayout::default()
},
);
assert_eq!(vm.execute(code, globals).unwrap(), text("done"));
assert_eq!(count.load(Ordering::SeqCst), 4);
}
#[test]
/// 验证直接默认参数和嵌套显式参数的对象结果均新增一份拥有根,归还后恢复原计数。
fn nested_language_calls_bind_defaults_and_return_owned_argument_objects() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 单参数源码函数只读参数再返回,默认值与显式实参应走相同拥有结果协议。
let identity = make_code(
&mut vm,
&[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
parameters: vec!["value".into()],
locals: vec!["value".into()],
..CodeLayout::default()
},
);
// 默认字符串先有测试根,函数默认值表另持内部边。
let default = vm.new_string("default").unwrap();
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
let function = vm
.new_function(
"identity",
identity,
globals,
vec![Value::Object(default)],
vec![],
)
.unwrap();
// 保存调用前计数,结果交付多一根,归还结果后必须恢复。
let before = vm.ref_count(default).unwrap();
// 函数把已有参数/默认值作为结果,身份不变但结果仍取得独立拥有根。
let returned = vm.call(function, &[]).unwrap();
assert_eq!(returned, Value::Object(default));
assert_eq!(vm.ref_count(default).unwrap(), before + 1);
// 只归还本次调用结果根,不触碰默认表/常量/测试输入的所有权。
vm.release_value(returned).unwrap();
assert_eq!(vm.ref_count(default).unwrap(), before);
vm.dict_set(globals, text("identity"), Value::Object(function))
.unwrap();
// 显式实参放进包装代码常量表,也有独立测试根。
let argument = vm.new_string("argument").unwrap();
// 包装指令按函数、Null、参数布局调用 identity,检验跨两个源码帧的引用转交。
let wrapper = make_code(
&mut vm,
&[
op(Opcode::LoadGlobal, 0),
simple(Opcode::PushNull),
op(Opcode::LoadConst, 0),
op(Opcode::Call, 1),
simple(Opcode::ReturnValue),
],
vec![Value::Object(argument)],
CodeLayout {
names: vec!["identity".into()],
..CodeLayout::default()
},
);
// 保存调用前计数,结果交付多一根,归还结果后必须恢复。
let before = vm.ref_count(argument).unwrap();
// 临时执行函数与嵌套帧清理后,结果对象仍须保持拥有。
let returned = vm.execute(wrapper, globals).unwrap();
assert_eq!(returned, Value::Object(argument));
assert_eq!(vm.ref_count(argument).unwrap(), before + 1);
// 只归还本次调用结果根,不触碰默认表/常量/测试输入的所有权。
vm.release_value(returned).unwrap();
assert_eq!(vm.ref_count(argument).unwrap(), before);
}
#[test]
/// 参数过少/过多及异堆参数均在帧提交前拒绝,函数根计数不得因失败增加。
fn argument_count_and_foreign_argument_failures_do_not_retain_inputs() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
let code = make_code(
&mut vm,
&[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
parameters: vec!["value".into()],
locals: vec!["value".into()],
..CodeLayout::default()
},
);
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
let function = vm
.new_function("identity", code, globals, vec![], vec![])
.unwrap();
assert!(matches!(
vm.call(function, &[]),
Err(VmError::ArgumentCount {
required: 1,
provided: 0,
..
})
));
assert!(matches!(
vm.call(function, &[Value::Nil, Value::Nil]),
Err(VmError::ArgumentCount {
maximum: 1,
provided: 2,
..
})
));
// 另一堆产生真正有效但归属不同的参数,不能当本堆对象保留。
let mut other = Vm::new().unwrap();
// 异堆参数在执行源码之前拒绝,不改变当前函数计数。
let foreign = other.new_object().unwrap();
// 保存调用前计数,结果交付多一根,归还结果后必须恢复。
let before = vm.ref_count(function).unwrap();
assert_eq!(
vm.call(function, &[Value::Object(foreign)]),
Err(VmError::ForeignHandle)
);
assert_eq!(vm.ref_count(function).unwrap(), before);
}
#[test]
/// BuildMap 返回根在代码/全局释放后独立保活结果字典和子对象,释放结果才清空图。
fn freshly_built_maps_and_their_children_survive_frame_cleanup() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 子字符串先由测试根和代码常量持有,最终转由结果字典边保活。
let child = vm.new_string("owned by result").unwrap();
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::LoadConst, 1),
op(Opcode::BuildMap, 1),
simple(Opcode::ReturnValue),
],
vec![text("child"), Value::Object(child)],
CodeLayout::default(),
);
// 归还测试根,代码常量在实际执行前仍维持子对象存活。
vm.release(child).unwrap();
// 执行结果拥有根可跨越代码与全局生命周期。
let result = vm.execute(code, globals).unwrap();
// 移除代码常量边后,返回图必须自己保留所需对象;不是只靠旧帧保活。
vm.release(code).unwrap();
// 归还环境测试根,临时执行函数已清理,结果不能依赖其持续存在。
vm.release(globals).unwrap();
// 结果字典只有一份外部根,其 child 值是独立内部强边。
let Value::Object(dictionary) = result else {
panic!("expected a map");
};
assert_eq!(vm.ref_count(dictionary).unwrap(), 1);
assert_eq!(
vm.dict_get(dictionary, &text("child")).unwrap(),
Some(Value::Object(child))
);
assert_eq!(vm.str(child).unwrap(), "owned by result");
// 结果最后根归还,子对象的最后字典边随之释放,堆应归零。
vm.release(dictionary).unwrap();
assert_eq!(vm.object_count(), 0);
}
/// 原生辅助只借用返回首参数,空参数错误;对象结果拥有根由 VM 补充。
fn echo(arguments: &[Value]) -> Result<Value, VmError> {
arguments
.first()
.cloned()
.ok_or(VmError::InvalidArgument("expected an argument"))
}
#[test]
/// 原生 echo 返回临时 BuildMap 实参,输入弹栈前必须先保留结果根。
fn native_return_of_a_temporary_argument_is_retained_before_inputs_are_popped() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
let function = vm
.allocate(EndorNativeFunctionObject::new("echo", echo))
.unwrap();
vm.dict_set(globals, text("echo"), Value::Object(function))
.unwrap();
// 调用前/后按场景归还测试函数根;全局字典可能仍持有函数内部边。
vm.release(function).unwrap();
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadGlobal, 0),
simple(Opcode::PushNull),
op(Opcode::BuildMap, 0),
op(Opcode::Call, 1),
simple(Opcode::ReturnValue),
],
vec![],
CodeLayout {
names: vec!["echo".into()],
..CodeLayout::default()
},
);
// 执行结果拥有根可跨越代码与全局生命周期。
let result = vm.execute(code, globals).unwrap();
// 移除代码常量边后,返回图必须自己保留所需对象;不是只靠旧帧保活。
vm.release(code).unwrap();
// 归还环境测试根,临时执行函数已清理,结果不能依赖其持续存在。
vm.release(globals).unwrap();
assert_eq!(vm.object_count(), 1);
vm.release_value(result).unwrap();
assert_eq!(vm.object_count(), 0);
}
/// 原生辅助恒定报错,使执行器在 Call 处回溯与清理临时参数。
fn fail(_arguments: &[Value]) -> Result<Value, VmError> {
Err(VmError::InvalidArgument("host failure"))
}
#[test]
/// 错误后对象数恢复调用前,回溯保留故障 Call 偏移及显式代码位置元数据。
fn native_failure_unwinds_temporary_roots_and_keeps_execution_locations() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
let function = vm
.allocate(EndorNativeFunctionObject::new("fail", fail))
.unwrap();
vm.dict_set(globals, text("fail"), Value::Object(function))
.unwrap();
// 附显式名称、文件和起始行,用故障回溯断言实际保存的元数据。
let code = EndorCode::from_instructions(
&[
op(Opcode::LoadGlobal, 0),
simple(Opcode::PushNull),
op(Opcode::BuildMap, 0),
op(Opcode::Call, 1),
simple(Opcode::ReturnValue),
],
vec![],
CodeLayout {
names: vec!["fail".into()],
..CodeLayout::default()
},
)
.unwrap()
.with_location("outer", "demo.endor", 12);
// 将已验证代码分配为拥有根,临时执行函数之后另外持代码边。
let code = vm.new_code(code).unwrap();
// 记录代码/全局等场景根数,失败后不得残留临时字典、函数或帧根。
let before = vm.object_count();
assert_eq!(
vm.execute(code, globals),
Err(VmError::InvalidArgument("host failure"))
);
assert_eq!(vm.object_count(), before);
assert_eq!(vm.last_traceback()[0].function, "outer");
assert_eq!(vm.last_traceback()[0].instruction_offset, 3);
assert_eq!(vm.last_traceback()[0].filename, "demo.endor");
assert_eq!(vm.last_traceback()[0].first_line, 12);
}
#[test]
/// 递归源码调用由显式帧向量推进,触及深度硬上限后全部执行根恢复,环境环可显式回收。
fn recursion_uses_explicit_frames_and_unwinds_at_the_frame_limit() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadGlobal, 0),
simple(Opcode::PushNull),
op(Opcode::Call, 0),
simple(Opcode::ReturnValue),
],
vec![],
CodeLayout {
names: vec!["recurse".into()],
..CodeLayout::default()
},
);
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
let function = vm
.new_function("recurse", code, globals, vec![], vec![])
.unwrap();
vm.dict_set(globals, text("recurse"), Value::Object(function))
.unwrap();
// 递归深度会临时保留多份函数根,退出后应恢复精确初始计数。
let roots = vm.ref_count(function).unwrap();
assert_eq!(
vm.call_with_limits(
function,
&[],
ExecutionLimits {
max_frames: 64,
max_instructions: 1000
}
),
Err(VmError::ExecutionLimit("frames"))
);
assert_eq!(vm.last_traceback().len(), 64);
assert_eq!(vm.ref_count(function).unwrap(), roots);
// 调用前/后按场景归还测试函数根;全局字典可能仍持有函数内部边。
vm.release(function).unwrap();
// 移除代码常量边后,返回图必须自己保留所需对象;不是只靠旧帧保活。
vm.release(code).unwrap();
// 归还环境测试根,临时执行函数已清理,结果不能依赖其持续存在。
vm.release(globals).unwrap();
assert_eq!(vm.collect_cycles(), 3);
}
#[test]
/// 栈高合法的无限回边仍须由硬指令预算终止,不能泄漏临时函数或帧。
fn instruction_limit_stops_a_valid_infinite_loop_without_leaking_frames() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
let code = make_code(
&mut vm,
&[op(Opcode::JumpBackward, 1)],
vec![],
CodeLayout::default(),
);
// 记录代码/全局等场景根数,失败后不得残留临时字典、函数或帧根。
let before = vm.object_count();
assert_eq!(
vm.execute_with_limits(
code,
globals,
ExecutionLimits {
max_frames: 8,
max_instructions: 3
}
),
Err(VmError::ExecutionLimit("instructions"))
);
assert_eq!(vm.object_count(), before);
assert_eq!(vm.last_traceback().len(), 1);
}
#[test]
/// 函数捕获创建时内置字典身份,全局同名键仍优先,改写 __builtins__ 不重绑定旧函数。
fn function_globals_shadow_builtins_and_capture_the_original_builtins_dictionary() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 原内置字典由函数创建时捕获,不因稍后 globals.__builtins__ 改变而替换身份。
let builtins = vm.new_dict().unwrap();
vm.dict_set(builtins, text("answer"), text("builtin"))
.unwrap();
vm.dict_set(globals, text("__builtins__"), Value::Object(builtins))
.unwrap();
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
let code = make_code(
&mut vm,
&[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
names: vec!["answer".into()],
..CodeLayout::default()
},
);
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
let function = vm
.new_function("read", code, globals, vec![], vec![])
.unwrap();
// 新内置字典仅更新 globals 字段,用于证明旧函数仍引用原内置空间。
let replacement = vm.new_dict().unwrap();
vm.dict_set(globals, text("__builtins__"), Value::Object(replacement))
.unwrap();
// 归还原内置测试根后仍应由函数 captured builtins 强边保活。
vm.release(builtins).unwrap();
assert_eq!(vm.call(function, &[]).unwrap(), text("builtin"));
vm.dict_set(globals, text("answer"), text("global"))
.unwrap();
assert_eq!(vm.call(function, &[]).unwrap(), text("global"));
}
#[test]
/// 多个函数共享同一 Cell,Load/Store/DeleteDeref 对内容变化立即可见,clear 后读取报未绑定。
fn shared_free_variable_cells_are_read_written_and_deleted_by_bytecode() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 共享槽初值 first,read/write/clear 函数各自持有同一 Cell 强边。
let cell = vm.new_cell(Some(text("first"))).unwrap();
// 没有局部槽,唯一自由变量 Cell 位于 localsplus 索引零。
let read_code = make_code(
&mut vm,
&[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
free_variables: vec!["x".into()],
..CodeLayout::default()
},
);
// 有一个普通参数槽,外部 Cell 追加在索引一,StoreDeref 不能误写局部槽。
let write_code = make_code(
&mut vm,
&[
op(Opcode::LoadFast, 0),
op(Opcode::StoreDeref, 1),
op(Opcode::LoadConst, 0),
simple(Opcode::ReturnValue),
],
vec![Value::Nil],
CodeLayout {
parameters: vec!["value".into()],
locals: vec!["value".into()],
free_variables: vec!["x".into()],
..CodeLayout::default()
},
);
// DeleteDeref 清空 Cell 而非写 Nil,随后返回直接 Nil。
let clear_code = make_code(
&mut vm,
&[
op(Opcode::DeleteDeref, 0),
op(Opcode::LoadConst, 0),
simple(Opcode::ReturnValue),
],
vec![Value::Nil],
CodeLayout {
free_variables: vec!["x".into()],
..CodeLayout::default()
},
);
// 只读捕获值的函数,共享槽未复制成独立快照。
let read = vm
.new_function("read", read_code, globals, vec![], vec![cell])
.unwrap();
// 把显式参数写到同一捕获 Cell,其他闭包应立即看到新值。
let write = vm
.new_function("write", write_code, globals, vec![], vec![cell])
.unwrap();
// 删除同一捕获变量后 read 映射 EmptyCell 为具名 UnboundLocal。
let clear = vm
.new_function("clear", clear_code, globals, vec![], vec![cell])
.unwrap();
assert_eq!(vm.call(read, &[]).unwrap(), text("first"));
vm.call(write, &[text("updated")]).unwrap();
assert_eq!(vm.call(read, &[]).unwrap(), text("updated"));
vm.call(clear, &[]).unwrap();
assert_eq!(vm.call(read, &[]), Err(VmError::UnboundLocal("x".into())));
}
#[test]
/// 参数提升为 Cell 后通过 LoadClosure 返回,原帧与参数测试根释放仍由 Cell 边保活内容。
fn captured_argument_cells_keep_their_contents_after_the_frame_returns() {
// 独立 VM 隔离调用帧、全局与根计数断言。
let mut vm = Vm::new().unwrap();
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
let globals = vm.new_dict().unwrap();
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
let code = make_code(
&mut vm,
&[op(Opcode::LoadClosure, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
parameters: vec!["x".into()],
locals: vec!["x".into()],
cell_variables: vec!["x".into()],
..CodeLayout::default()
},
);
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
let function = vm
.new_function("capture", code, globals, vec![], vec![])
.unwrap();
// 参数对象有测试根,帧初始化提升 Cell 时另建立内容强边。
let value = vm.new_string("captured").unwrap();
// 函数把已有参数/默认值作为结果,身份不变但结果仍取得独立拥有根。
let returned = vm.call(function, &[Value::Object(value)]).unwrap();
// 归还原参数测试根,返回的 Cell 内容边仍须保活它。
vm.release(value).unwrap();
// LoadClosure 返回的是 Cell 本身,不是 Cell 当前内容。
let Value::Object(cell) = returned else {
panic!("expected cell");
};
assert_eq!(vm.cell_get(cell).unwrap(), Value::Object(value));
assert_eq!(vm.str(value).unwrap(), "captured");
// 最后 Cell 结果根归还后其内容边释放,原字符串身份应立即失效。
vm.release(cell).unwrap();
assert!(matches!(vm.object(value), Err(VmError::InvalidHandle)));
}
#[test]
/// StoreAttr/LoadAttr 与 Copy/Swap/PopTop 组合后仅保留宿主、属性和结果三份根,不残留复制栈根。
fn attribute_operations_and_stack_copies_transfer_roots_without_leaks() {
// 独立 VM 隔离堆计数和全局/回溯状态。
let mut vm = Vm::new().unwrap();
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
let globals = vm.new_dict().unwrap();
// 手工已验证指令,按下面布局解释局部槽和 names 索引。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadFast, 1),
op(Opcode::LoadFast, 0),
op(Opcode::StoreAttr, 0),
op(Opcode::LoadFast, 0),
op(Opcode::LoadAttr, 0),
op(Opcode::Copy, 1),
op(Opcode::Swap, 2),
simple(Opcode::PopTop),
simple(Opcode::ReturnValue),
],
vec![],
CodeLayout {
parameters: vec!["record".into(), "value".into()],
locals: vec!["record".into(), "value".into()],
names: vec!["x".into()],
..CodeLayout::default()
},
);
// 源码/原生函数有一份测试根,调用时另行保留帧/协议根。
let function = vm
.new_function("assign", code, globals, vec![], vec![])
.unwrap();
// 属性写入目标拥有测试根,字段还会独立持有 value 边。
let record = vm.new_object().unwrap();
// 原值测试根保留到最后,调用返回和属性边应各增加一份持有。
let value = vm.new_string("value").unwrap();
// 返回对象必须独立拥有,栈 Copy 产生的额外根应已被 PopTop 清理。
let returned = vm
.call(function, &[Value::Object(record), Value::Object(value)])
.unwrap();
assert_eq!(vm.ref_count(value).unwrap(), 3); // host, attribute, return value
// 先归还调用结果根,再释放 record 去掉字段边,value 最终只剩测试根。
vm.release_value(returned).unwrap();
// 属性 owner 最后根消失,其字段拥有边随对象释放。
vm.release(record).unwrap();
assert_eq!(vm.ref_count(value).unwrap(), 1);
}
#[test]
/// 文本比较经 ToBool/UnaryNot 后控制前向分支,检验跳转基点与运行时真值。
fn comparison_truth_and_forward_conditionals_use_runtime_values() {
// 独立 VM 隔离堆计数和全局/回溯状态。
let mut vm = Vm::new().unwrap();
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
let globals = vm.new_dict().unwrap();
// 手工已验证指令,按下面布局解释局部槽和 names 索引。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::LoadConst, 1),
Instruction::compare(CompareOp::Lt),
simple(Opcode::ToBool),
simple(Opcode::UnaryNot),
op(Opcode::PopJumpIfTrue, 2),
op(Opcode::LoadConst, 2),
simple(Opcode::ReturnValue),
op(Opcode::LoadConst, 3),
simple(Opcode::ReturnValue),
],
vec![text("a"), text("b"), text("less"), text("other")],
CodeLayout::default(),
);
assert_eq!(vm.execute(code, globals).unwrap(), text("less"));
}
#[test]
/// 缺全局、删除局部后读取和 Null 逃逸分别报错,随后成功执行应清掉旧回溯。
fn unbound_names_deleted_locals_and_invalid_null_values_fail_cleanly() {
// 独立 VM 隔离堆计数和全局/回溯状态。
let mut vm = Vm::new().unwrap();
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
let globals = vm.new_dict().unwrap();
// 名称表合法但环境无该键,属于执行期 NameNotFound 而非索引验证错误。
let missing = make_code(
&mut vm,
&[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
names: vec!["missing".into()],
..CodeLayout::default()
},
);
assert_eq!(
vm.execute(missing, globals),
Err(VmError::NameNotFound("missing".into()))
);
// 先成功赋值再 DeleteFast,最后读取检验 None 未绑定状态。
let deleted = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::StoreFast, 0),
op(Opcode::DeleteFast, 0),
op(Opcode::LoadFast, 0),
simple(Opcode::ReturnValue),
],
vec![Value::Nil],
CodeLayout {
locals: vec!["x".into()],
..CodeLayout::default()
},
);
assert_eq!(
vm.execute(deleted, globals),
Err(VmError::UnboundLocal("x".into()))
);
// PushNull 满足抽象栈高度但不是语言 Value,ReturnValue 必须在运行时拒绝。
let null = make_code(
&mut vm,
&[simple(Opcode::PushNull), simple(Opcode::ReturnValue)],
vec![],
CodeLayout::default(),
);
assert!(matches!(
vm.execute(null, globals),
Err(VmError::InvalidBytecode { .. })
));
// 错误后执行简单成功代码,验证 last_traceback 不残留前一次故障。
let success = return_constant(&mut vm, Value::Nil);
assert_eq!(vm.execute(success, globals).unwrap(), Value::Nil);
assert!(vm.last_traceback().is_empty());
}
#[test]
/// 不可哈希数值键使外层建图失败,半成品和作为值的临时空图都须被清理。
fn failed_map_construction_cleans_both_partial_map_and_temporary_input() {
// 独立 VM 隔离堆计数和全局/回溯状态。
let mut vm = Vm::new().unwrap();
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
let globals = vm.new_dict().unwrap();
// 手工已验证指令,按下面布局解释局部槽和 names 索引。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::BuildMap, 0),
op(Opcode::BuildMap, 1),
simple(Opcode::ReturnValue),
],
vec![Value::Number(1_i64.into())],
CodeLayout::default(),
);
// 失败前存活对象基线,执行临时函数和两层临时图不得泄漏。
let before = vm.object_count();
assert_eq!(
vm.execute(code, globals),
Err(VmError::UnsupportedOperation("numeric hash"))
);
assert_eq!(vm.object_count(), before);
}
#[test]
/// 调用协议位置的 Value::Nil 是真实隐式参数,不得像专用 Null 标记那样被忽略。
fn implicit_receiver_slot_is_distinct_from_the_null_call_marker() {
// 独立 VM 隔离堆计数和全局/回溯状态。
let mut vm = Vm::new().unwrap();
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
let globals = vm.new_dict().unwrap();
// 源码/原生函数有一份测试根,调用时另行保留帧/协议根。
let function = vm
.allocate(EndorNativeFunctionObject::new("echo", echo))
.unwrap();
// 手工已验证指令,按下面布局解释局部槽和 names 索引。
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::LoadConst, 1),
op(Opcode::Call, 0),
simple(Opcode::ReturnValue),
],
vec![Value::Object(function), Value::Nil],
CodeLayout::default(),
);
// 此处 Nil 会作为真实隐式参数传入 echo,只有 StackEntry::Null 才省略协议位置。
assert_eq!(vm.execute(code, globals).unwrap(), Value::Nil);
}
#[test]
/// 验证默认值/闭包计数打包编码和边界值;构造校验要求完整输入栈高,但不验证运行时载荷种类。
fn function_creation_operands_round_trip_and_require_all_inputs() {
assert_eq!(
Instruction::encode_all(&[Instruction::make_function(1, 1)]).unwrap(),
vec![255, 1, 255, 0, 28, 1]
);
assert_eq!(
Instruction::encode_all(&[Instruction::make_function(u16::MAX, u16::MAX)]).unwrap(),
vec![255, 255, 255, 255, 255, 255, 28, 255]
);
// 这里只验证 MakeFunction 逻辑输入数量/编码,常量 Nil 占位并不实际执行构造。
let code = EndorCode::from_instructions(
&[
op(Opcode::LoadConst, 0),
op(Opcode::LoadConst, 0),
op(Opcode::LoadConst, 0),
Instruction::make_function(1, 1),
simple(Opcode::ReturnValue),
],
vec![Value::Nil],
CodeLayout::default(),
)
.unwrap();
assert_eq!(
code.instructions()[3].instruction,
Instruction::make_function(1, 1)
);
assert_eq!(code.instructions()[3].next_offset, 6);
assert_eq!(code.stack_size(), 3);
// 不同计数组合都在空栈上缺少代码对象项,必须构造阶段报栈下溢。
for (defaults, closure) in [(0, 0), (1, 0), (0, 1), (1, 1), (u16::MAX, u16::MAX)] {
assert!(matches!(
EndorCode::from_instructions(
&[
Instruction::make_function(defaults, closure),
simple(Opcode::ReturnValue)
],
vec![],
CodeLayout::default(),
),
Err(VmError::InvalidBytecode { .. })
));
}
// 只有一个常量不能满足含默认值或捕获槽的函数构造,缺输入仍须静态拒绝。
for creation in [
Instruction::make_function(1, 0),
Instruction::make_function(0, 1),
] {
assert!(
EndorCode::from_instructions(
&[
op(Opcode::LoadConst, 0),
creation,
simple(Opcode::ReturnValue)
],
vec![Value::Nil],
CodeLayout::default(),
)
.is_err()
);
}
}
#[test]
/// MakeFunction 在每次模块执行创建新函数身份,代码可共享,默认值/显式实参都能调用。
fn bytecode_defines_global_functions_and_calls_them_with_defaults() {
// 独立 VM 隔离堆计数和全局/回溯状态。
let mut vm = Vm::new().unwrap();
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
let globals = vm.new_dict().unwrap();
// 内部 identity 的参数读取直接返回,代码诊断位置给新函数命名。
let inner = EndorCode::from_instructions(
&[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
parameters: vec!["value".into()],
locals: vec!["value".into()],
..CodeLayout::default()
},
)
.unwrap()
.with_location("identity", "functions.endor", 2);
// 代码常量由模块/工厂保留,归还测试根后仍用于后续 MakeFunction。
let inner = vm.new_code(inner).unwrap();
// 按默认值、代码顺序压栈创建函数,存全局后再 Call 零显式参数走默认值。
let module = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::LoadConst, 1),
Instruction::make_function(1, 0),
op(Opcode::StoreGlobal, 0),
op(Opcode::LoadGlobal, 0),
simple(Opcode::PushNull),
op(Opcode::Call, 0),
simple(Opcode::ReturnValue),
],
vec![text("default"), Value::Object(inner)],
CodeLayout {
names: vec!["identity".into()],
..CodeLayout::default()
},
);
// 归还内层代码测试根;外层代码常量边接管保活,不能仅靠裸句柄继续使用。
vm.release(inner).unwrap();
assert_eq!(vm.execute(module, globals).unwrap(), text("default"));
// 全局读取返回借用函数身份,要跨后续重定义存活必须显式 retain。
let Some(Value::Object(first)) = vm.dict_get(globals, &text("identity")).unwrap() else {
panic!("expected a function");
};
// 保留旧函数独立根,模块再次执行替换全局后还可比较它与新函数身份。
vm.retain(first).unwrap();
// 借用函数元数据确认 name/code/globals 来自创建上下文。
let data = vm
.payload::<EndorFunctionObject>(first, "function")
.unwrap();
assert_eq!(data.name(), "identity");
assert_eq!(data.code(), inner);
assert_eq!(data.globals(), globals);
assert_eq!(
vm.call(first, &[text("explicit")]).unwrap(),
text("explicit")
);
assert_eq!(vm.execute(module, globals).unwrap(), text("default"));
// 再次执行得到不同函数对象,但两者仍共享同一个代码对象。
let Some(Value::Object(second)) = vm.dict_get(globals, &text("identity")).unwrap() else {
panic!("expected a second function");
};
assert_ne!(first, second);
assert_eq!(
vm.payload::<EndorFunctionObject>(second, "function")
.unwrap()
.code(),
inner
);
// 归还为跨重定义而取得的旧函数根,不能把借用全局结果当作拥有根。
vm.release(first).unwrap();
// 模块代码常量边释放,仍由全局函数持有内部代码。
vm.release(module).unwrap();
// 归还环境测试根,若函数与环境互相持有则需显式环回收。
vm.release(globals).unwrap();
// 全局字典持有函数、函数持有全局和代码,失去外部根后形成三个对象的可回收图。
assert_eq!(vm.collect_cycles(), 3);
assert_eq!(vm.object_count(), 0);
}
#[test]
/// 工厂创建临时默认字典,函数返回后默认值仍由函数边保活,每次调用返回同一默认对象的独立根。
fn bytecode_function_defaults_keep_temporary_objects_alive_after_factory_returns() {
// 独立 VM 隔离堆计数和全局/回溯状态。
let mut vm = Vm::new().unwrap();
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
let globals = vm.new_dict().unwrap();
// 内部函数有一个必填与两个默认参数,返回中间 defaulted 槽。
let inner = make_code(
&mut vm,
&[op(Opcode::LoadFast, 1), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
parameters: vec!["required".into(), "defaulted".into(), "tag".into()],
locals: vec!["required".into(), "defaulted".into(), "tag".into()],
..CodeLayout::default()
},
);
// 先在栈上新建默认空图,再压 tag 和代码,MakeFunction 把前两项作为默认值。
let factory = make_code(
&mut vm,
&[
op(Opcode::BuildMap, 0),
op(Opcode::LoadConst, 1),
op(Opcode::LoadConst, 0),
Instruction::make_function(2, 0),
simple(Opcode::ReturnValue),
],
vec![Value::Object(inner), text("tag")],
CodeLayout::default(),
);
// 归还内层代码测试根;外层代码常量边接管保活,不能仅靠裸句柄继续使用。
vm.release(inner).unwrap();
// 工厂结果函数拥有一根,默认字典由其 defaults 强边持有。
let Value::Object(function) = vm.execute(factory, globals).unwrap() else {
panic!("expected a function");
};
// 工厂帧和代码释放后,结果函数必须独立保留代码/环境/默认图。
vm.release(factory).unwrap();
// 归还环境测试根,若函数与环境互相持有则需显式环回收。
vm.release(globals).unwrap();
assert!(matches!(
vm.call(function, &[]),
Err(VmError::ArgumentCount { required: 1, .. })
));
// 第一次默认调用返回默认字典对象,需独立归还调用结果根。
let first = vm.call(function, &[Value::Nil]).unwrap();
// 第二次返回同一默认对象,默认表达式不应每次重新求值创建新图。
let second = vm.call(function, &[Value::Nil]).unwrap();
assert_eq!(first, second); // Defaults are evaluated once per creation.
// 默认图此时有函数默认边加两份结果根,共三份强引用。
let Value::Object(default) = first else {
panic!("expected a default map");
};
assert_eq!(vm.ref_count(default).unwrap(), 3); // Function and two results.
// 释放函数与默认值内部边,两个结果根仍使默认图存活。
vm.release(function).unwrap();
// 仅归还第一调用结果根,第二结果仍独立拥有一根。
vm.release_value(first).unwrap();
assert_eq!(vm.ref_count(default).unwrap(), 1);
// 最后结果根归还,默认图及其余无根对象应全部释放。
vm.release_value(second).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn bytecode_nested_functions_share_cells_and_bind_defaults_after_outer_returns() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let read = make_code(
&mut vm,
&[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
free_variables: vec!["x".into(), "sentinel".into()],
..CodeLayout::default()
},
);
let write = make_code(
&mut vm,
&[
op(Opcode::LoadFast, 0),
op(Opcode::StoreDeref, 1),
op(Opcode::LoadDeref, 1),
simple(Opcode::ReturnValue),
],
vec![],
CodeLayout {
parameters: vec!["value".into()],
locals: vec!["value".into()],
free_variables: vec!["x".into(), "sentinel".into()],
..CodeLayout::default()
},
);
let outer = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::LoadClosure, 0),
op(Opcode::LoadClosure, 1),
op(Opcode::LoadConst, 1),
Instruction::make_function(0, 2),
op(Opcode::LoadConst, 2),
op(Opcode::LoadConst, 3),
op(Opcode::LoadClosure, 0),
op(Opcode::LoadClosure, 1),
op(Opcode::LoadConst, 4),
Instruction::make_function(1, 2),
op(Opcode::BuildMap, 2),
simple(Opcode::ReturnValue),
],
vec![
text("read"),
Value::Object(read),
text("write"),
text("default"),
Value::Object(write),
],
CodeLayout {
parameters: vec!["x".into(), "sentinel".into()],
locals: vec!["x".into(), "sentinel".into()],
cell_variables: vec!["x".into(), "sentinel".into()],
..CodeLayout::default()
},
);
let outer_function = vm
.new_function("outer", outer, globals, vec![], vec![])
.unwrap();
let Value::Object(pair) = vm
.call(outer_function, &[text("first"), text("sentinel")])
.unwrap()
else {
panic!("expected a function pair");
};
for reference in [read, write, outer, outer_function, globals] {
vm.release(reference).unwrap();
}
let Some(Value::Object(read)) = vm.dict_get(pair, &text("read")).unwrap() else {
panic!("expected read");
};
let Some(Value::Object(write)) = vm.dict_get(pair, &text("write")).unwrap() else {
panic!("expected write");
};
assert_eq!(
vm.payload::<EndorFunctionObject>(read, "function")
.unwrap()
.closure(),
vm.payload::<EndorFunctionObject>(write, "function")
.unwrap()
.closure(),
);
assert_eq!(vm.collect_cycles(), 0);
assert_eq!(vm.call(read, &[]).unwrap(), text("first"));
assert_eq!(vm.call(write, &[text("updated")]).unwrap(), text("updated"));
assert_eq!(vm.call(read, &[]).unwrap(), text("updated"));
assert_eq!(vm.call(write, &[]).unwrap(), text("default"));
assert_eq!(vm.call(read, &[]).unwrap(), text("default"));
vm.release(pair).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn bytecode_function_creation_rejects_bad_inputs_without_leaking_roots() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let plain = return_constant(&mut vm, Value::Nil);
let captured = make_code(
&mut vm,
&[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
free_variables: vec!["x".into()],
..CodeLayout::default()
},
);
let cell = vm.new_cell(None).unwrap();
let mismatch = VmError::InvalidArgument("function defaults or closure do not match code");
let cases = [
(
vec![op(Opcode::LoadConst, 0), Instruction::make_function(0, 0)],
vec![Value::Nil],
VmError::ObjectKindMismatch("code"),
),
(
vec![op(Opcode::BuildMap, 0), Instruction::make_function(0, 0)],
vec![],
VmError::ObjectKindMismatch("code"),
),
(
vec![simple(Opcode::PushNull), Instruction::make_function(0, 0)],
vec![],
invalid_null(),
),
(
vec![
op(Opcode::BuildMap, 0),
op(Opcode::LoadConst, 0),
Instruction::make_function(1, 0),
],
vec![Value::Object(plain)],
mismatch.clone(),
),
(
vec![op(Opcode::LoadConst, 0), Instruction::make_function(0, 0)],
vec![Value::Object(captured)],
mismatch.clone(),
),
(
vec![
op(Opcode::LoadConst, 0),
op(Opcode::LoadConst, 1),
Instruction::make_function(0, 1),
],
vec![Value::Object(cell), Value::Object(plain)],
mismatch,
),
(
vec![
op(Opcode::LoadConst, 0),
op(Opcode::LoadConst, 1),
Instruction::make_function(0, 1),
],
vec![Value::Nil, Value::Object(captured)],
VmError::ObjectKindMismatch("cell"),
),
(
vec![
op(Opcode::BuildMap, 0),
op(Opcode::LoadConst, 0),
Instruction::make_function(0, 1),
],
vec![Value::Object(captured)],
VmError::ObjectKindMismatch("cell"),
),
(
vec![
simple(Opcode::PushNull),
op(Opcode::LoadConst, 0),
Instruction::make_function(0, 1),
],
vec![Value::Object(captured)],
invalid_null(),
),
(
vec![
simple(Opcode::PushNull),
op(Opcode::LoadConst, 0),
Instruction::make_function(1, 0),
],
vec![Value::Object(plain)],
invalid_null(),
),
];
for (mut instructions, constants, expected) in cases {
instructions.push(simple(Opcode::ReturnValue));
let code = make_code(&mut vm, &instructions, constants, CodeLayout::default());
let count = vm.object_count();
let roots =
[plain, captured, cell, globals].map(|reference| vm.ref_count(reference).unwrap());
let error = vm.execute(code, globals).unwrap_err();
if matches!(expected, VmError::InvalidBytecode { .. }) {
assert!(matches!(
error,
VmError::InvalidBytecode {
reason: "NULL call marker used as a value",
..
}
));
} else {
assert_eq!(error, expected);
}
assert_eq!(vm.object_count(), count);
assert_eq!(
[plain, captured, cell, globals].map(|reference| vm.ref_count(reference).unwrap()),
roots
);
vm.release(code).unwrap();
}
for reference in [plain, captured, cell, globals] {
vm.release(reference).unwrap();
}
assert_eq!(vm.object_count(), 0);
}
fn invalid_null() -> VmError {
VmError::InvalidBytecode {
offset: 0,
reason: "NULL call marker used as a value",
}
}
#[test]
fn bytecode_functions_capture_builtins_at_creation_and_share_live_globals() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let original = vm.new_dict().unwrap();
let replacement = vm.new_dict().unwrap();
vm.dict_set(original, text("answer"), text("original"))
.unwrap();
vm.dict_set(replacement, text("answer"), text("replacement"))
.unwrap();
vm.dict_set(globals, text("__builtins__"), Value::Object(original))
.unwrap();
let inner = make_code(
&mut vm,
&[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
names: vec!["answer".into()],
..CodeLayout::default()
},
);
let factory = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
Instruction::make_function(0, 0),
simple(Opcode::ReturnValue),
],
vec![Value::Object(inner)],
CodeLayout::default(),
);
let Value::Object(first) = vm.execute(factory, globals).unwrap() else {
panic!("expected first function");
};
vm.dict_set(globals, text("__builtins__"), Value::Object(replacement))
.unwrap();
let Value::Object(second) = vm.execute(factory, globals).unwrap() else {
panic!("expected second function");
};
for reference in [original, replacement, factory, inner, globals] {
vm.release(reference).unwrap();
}
assert_eq!(vm.call(first, &[]).unwrap(), text("original"));
assert_eq!(vm.call(second, &[]).unwrap(), text("replacement"));
vm.dict_set(globals, text("answer"), text("global"))
.unwrap();
assert_eq!(vm.call(first, &[]).unwrap(), text("global"));
assert_eq!(vm.call(second, &[]).unwrap(), text("global"));
vm.release(first).unwrap();
vm.release(second).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn later_execution_failure_releases_a_new_function_and_its_temporary_default() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let inner = make_code(
&mut vm,
&[op(Opcode::LoadFast, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
parameters: vec!["value".into()],
locals: vec!["value".into()],
..CodeLayout::default()
},
);
let fail = vm
.allocate(EndorNativeFunctionObject::new("fail", fail))
.unwrap();
let factory = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
simple(Opcode::PushNull),
op(Opcode::BuildMap, 0),
op(Opcode::LoadConst, 1),
Instruction::make_function(1, 0),
op(Opcode::Call, 1),
simple(Opcode::ReturnValue),
],
vec![Value::Object(fail), Value::Object(inner)],
CodeLayout::default(),
);
vm.release(fail).unwrap();
vm.release(inner).unwrap();
let count = vm.object_count();
assert_eq!(
vm.execute(factory, globals),
Err(VmError::InvalidArgument("host failure"))
);
assert_eq!(vm.object_count(), count);
vm.release(factory).unwrap();
vm.release(globals).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn suspended_read_only_call_keeps_roots_and_releases_them_on_completion() {
use endor_core::ExecutionProgress;
let mut vm = Vm::new().unwrap();
let shared = vm.new_object().unwrap();
vm.set_attr(shared, "value", Value::Number(7_i64.into()))
.unwrap();
let globals = vm.new_dict().unwrap();
let mut instructions = vec![simple(Opcode::Nop); 16];
instructions.extend([op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)]);
let code = make_code(
&mut vm,
&instructions,
vec![Value::Object(shared)],
CodeLayout::default(),
);
let function = vm
.new_function("sliced", code, globals, vec![], vec![])
.unwrap();
let mut progress = vm
.start_read_only_call(
function,
&[],
ExecutionLimits {
max_frames: 8,
max_instructions: 100,
},
1,
)
.unwrap();
assert!(matches!(
progress,
ExecutionProgress::Pending { instructions: 1 }
));
assert!(
vm.set_attr(shared, "value", Value::Number(8_i64.into()))
.is_err()
);
assert!(vm.call(function, &[]).is_err());
vm.release(function).unwrap();
vm.release(code).unwrap();
vm.release(globals).unwrap();
vm.release(shared).unwrap();
vm.collect_cycles();
loop {
match progress {
ExecutionProgress::Pending { .. } => progress = vm.resume_read_only_call(2).unwrap(),
ExecutionProgress::Complete {
value,
instructions,
..
} => {
assert_eq!(instructions, 18);
assert_eq!(value, Value::Object(shared));
assert_eq!(
vm.get_attr(shared, "value").unwrap(),
Value::Number(7_i64.into())
);
vm.release_value(value).unwrap();
break;
}
}
}
assert!(!vm.has_suspended_call());
assert_eq!(vm.object_count(), 0);
}
#[test]
fn suspended_call_cancellation_and_total_budget_restore_read_only_state() {
let mut vm = Vm::new().unwrap();
let shared = vm.new_object().unwrap();
let globals = vm.new_dict().unwrap();
let mut instructions = vec![simple(Opcode::Nop); 8];
instructions.extend([op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)]);
let code = make_code(
&mut vm,
&instructions,
vec![Value::Object(shared)],
CodeLayout::default(),
);
let function = vm
.new_function("budget", code, globals, vec![], vec![])
.unwrap();
let roots = vm.ref_count(function).unwrap();
vm.start_read_only_call(
function,
&[],
ExecutionLimits {
max_frames: 8,
max_instructions: 3,
},
1,
)
.unwrap();
assert!(vm.resume_read_only_call(0).is_err());
assert!(vm.has_suspended_call());
vm.resume_read_only_call(1).unwrap();
assert_eq!(
vm.resume_read_only_call(1).unwrap_err(),
VmError::ExecutionLimit("instructions")
);
assert!(!vm.has_suspended_call());
assert_eq!(vm.ref_count(function).unwrap(), roots);
vm.set_attr(shared, "after", Value::Bool(true)).unwrap();
vm.start_read_only_call(function, &[], ExecutionLimits::default(), 1)
.unwrap();
assert!(vm.cancel_read_only_call().unwrap());
assert!(!vm.cancel_read_only_call().unwrap());
assert_eq!(vm.ref_count(function).unwrap(), roots);
vm.set_attr(shared, "after", Value::Bool(false)).unwrap();
vm.release(function).unwrap();
vm.release(code).unwrap();
vm.release(globals).unwrap();
vm.release(shared).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn managed_coroutines_interleave_without_replaying_writes() {
use endor_core::CoroutineProgress;
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::StoreGlobal, 0),
op(Opcode::LoadConst, 1),
op(Opcode::StoreGlobal, 1),
op(Opcode::LoadConst, 2),
simple(Opcode::ReturnValue),
],
vec![text("first"), text("second"), Value::Nil],
CodeLayout {
names: vec!["x".into(), "y".into()],
..CodeLayout::default()
},
);
let function = vm
.new_function("work", code, globals, vec![], vec![])
.unwrap();
let first = vm
.new_coroutine(function, &[], ExecutionLimits::default())
.unwrap();
let second = vm
.new_coroutine(function, &[], ExecutionLimits::default())
.unwrap();
assert!(matches!(
vm.resume_coroutine(first, 2).unwrap(),
CoroutineProgress::Pending { instructions: 2 }
));
assert!(matches!(
vm.resume_coroutine(second, 2).unwrap(),
CoroutineProgress::Pending { instructions: 2 }
));
vm.dict_set(globals, text("x"), text("interleaved"))
.unwrap();
assert!(matches!(
vm.resume_coroutine(first, 16).unwrap(),
CoroutineProgress::Complete {
value: Value::Nil,
instructions: 6
}
));
assert_eq!(
vm.dict_get(globals, &text("x")).unwrap(),
Some(text("interleaved"))
);
assert_eq!(
vm.dict_get(globals, &text("y")).unwrap(),
Some(text("second"))
);
assert!(vm.cancel_coroutine(second).unwrap());
assert!(!vm.cancel_coroutine(second).unwrap());
assert!(vm.resume_coroutine(first, 1).is_err());
for object in [first, second, function, code, globals] {
vm.release(object).unwrap();
}
assert_eq!(vm.object_count(), 0);
}
#[test]
fn parked_coroutine_cycles_are_collectable_without_cancellation() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let code = return_constant(&mut vm, Value::Nil);
let function = vm
.new_function("cycle", code, globals, vec![], vec![])
.unwrap();
let coroutine = vm
.new_coroutine(function, &[], ExecutionLimits::default())
.unwrap();
vm.dict_set(globals, text("task"), Value::Object(coroutine))
.unwrap();
for object in [coroutine, function, code, globals] {
vm.release(object).unwrap();
}
assert!(vm.collect_cycles() > 0);
assert_eq!(vm.object_count(), 0);
}
#[test]
fn managed_coroutine_hard_budget_accumulates_and_error_releases_frames() {
use endor_core::CoroutineProgress;
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
// Reaching ReturnValue at the limit succeeds; keep a third instruction
// beyond the hard limit to exercise failure before the next Pending yield.
let code = vm
.new_code(
EndorCode::from_instructions(
&[
Instruction::simple(Opcode::Nop),
Instruction::new(Opcode::LoadConst, 0),
Instruction::simple(Opcode::ReturnValue),
],
vec![Value::Nil],
CodeLayout::default(),
)
.unwrap(),
)
.unwrap();
let function = vm
.new_function("limited", code, globals, vec![], vec![])
.unwrap();
let coroutine = vm
.new_coroutine(
function,
&[],
ExecutionLimits {
max_frames: 4,
// The second resume reaches the cumulative limit before
// the third instruction, without replaying the first.
max_instructions: 2,
},
)
.unwrap();
assert!(matches!(
vm.resume_coroutine(coroutine, 1).unwrap(),
CoroutineProgress::Pending { instructions: 1 }
));
assert!(matches!(
vm.resume_coroutine(coroutine, 1),
Err(VmError::ExecutionLimit("instructions"))
));
assert!(!vm.last_traceback().is_empty());
assert!(!vm.cancel_coroutine(coroutine).unwrap());
for object in [coroutine, function, code, globals] {
vm.release(object).unwrap();
}
assert_eq!(vm.object_count(), 0);
}
#[derive(Debug)]
struct AwaitGate {
ready: Arc<std::sync::atomic::AtomicBool>,
value: Value,
}
impl ObjectPayload for AwaitGate {
fn as_any(&self) -> &dyn Any {
self
}
fn kind_name(&self) -> &'static str {
"await-gate"
}
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
if let Value::Object(reference) = self.value {
visit(reference);
}
}
fn poll_await(&self) -> Result<endor_core::AwaitPoll, VmError> {
Ok(if self.ready.load(Ordering::Acquire) {
endor_core::AwaitPoll::Ready(self.value.clone())
} else {
endor_core::AwaitPoll::Pending
})
}
}
#[test]
fn await_keeps_operand_and_owned_object_result_without_replaying_prefix() {
use endor_core::CoroutineProgress;
let mut vm = Vm::new().unwrap();
let ready = Arc::new(std::sync::atomic::AtomicBool::new(false));
let value = vm.new_object().unwrap();
vm.set_attr(value, "marker", text("kept")).unwrap();
let gate = vm
.allocate(AwaitGate {
ready: ready.clone(),
value: Value::Object(value),
})
.unwrap();
let globals = vm.new_dict().unwrap();
let code = make_code(
&mut vm,
&[
op(Opcode::LoadConst, 0),
op(Opcode::StoreGlobal, 0),
op(Opcode::LoadConst, 1),
simple(Opcode::Await),
simple(Opcode::ReturnValue),
],
vec![text("prefix"), Value::Object(gate)],
CodeLayout {
names: vec!["step".into()],
..CodeLayout::default()
},
);
let function = vm
.new_function("awaited", code, globals, vec![], vec![])
.unwrap();
let coroutine = vm
.new_coroutine(function, &[], ExecutionLimits::default())
.unwrap();
assert!(
matches!(vm.resume_coroutine(coroutine, 64).unwrap(), CoroutineProgress::Waiting { awaitable, .. } if awaitable == gate)
);
vm.release(value).unwrap();
vm.release(gate).unwrap();
vm.dict_set(globals, text("step"), text("interleaved"))
.unwrap();
vm.collect_cycles();
ready.store(true, Ordering::Release);
let CoroutineProgress::Complete { value: result, .. } =
vm.resume_coroutine(coroutine, 64).unwrap()
else {
panic!("ready await must complete")
};
assert_eq!(
vm.dict_get(globals, &text("step")).unwrap(),
Some(text("interleaved"))
);
for object in [coroutine, function, code, globals] {
vm.release(object).unwrap();
}
let Value::Object(reference) = result else {
panic!("owned object result")
};
assert_eq!(vm.get_attr(reference, "marker").unwrap(), text("kept"));
vm.release_value(result).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn named_reads_ignore_unrelated_globals_but_track_missing_shadow_and_payload_kind() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let builtins = vm.new_dict().unwrap();
vm.dict_set(builtins, text("name"), text("builtin"))
.unwrap();
vm.dict_set(globals, text("__builtins__"), Value::Object(builtins))
.unwrap();
let code = make_code(
&mut vm,
&[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
names: vec!["name".into()],
..CodeLayout::default()
},
);
let function = vm
.new_function("read-name", code, globals, vec![], vec![])
.unwrap();
let endor_core::ExecutionProgress::Complete {
value,
reads,
dependencies,
..
} = vm
.start_read_only_call(function, &[], ExecutionLimits::default(), 64)
.unwrap()
else {
panic!("complete")
};
assert_eq!(value, text("builtin"));
assert!(reads.contains(&globals) && reads.contains(&builtins));
vm.release_value(value).unwrap();
vm.dict_set(globals, text("_"), text("unrelated")).unwrap();
let (value, synchronous) = vm
.call_read_only_precise_with_limits(function, &[], ExecutionLimits::default())
.unwrap();
assert_eq!(value, text("builtin"));
assert_eq!(synchronous.dependencies, dependencies);
assert_eq!(synchronous.objects, reads);
vm.release_value(value).unwrap();
let (value, conservative) = vm
.call_read_only_with_limits(function, &[], ExecutionLimits::default())
.unwrap();
assert_eq!(conservative, reads);
vm.release_value(value).unwrap();
assert!(
dependencies
.iter()
.all(|dependency| vm.object_store().read_dependency_valid(dependency))
);
vm.dict_set(globals, text("name"), text("shadow")).unwrap();
assert!(
dependencies
.iter()
.any(|dependency| !vm.object_store().read_dependency_valid(dependency))
);
vm.dict_delete(globals, &text("name")).unwrap();
assert!(
dependencies
.iter()
.all(|dependency| vm.object_store().read_dependency_valid(dependency))
);
vm.dict_set(builtins, text("name"), text("changed"))
.unwrap();
assert!(
dependencies
.iter()
.any(|dependency| !vm.object_store().read_dependency_valid(dependency))
);
for object in [function, code, globals, builtins] {
vm.release(object).unwrap();
}
assert_eq!(vm.object_count(), 0);
assert!(
dependencies
.iter()
.any(|dependency| !vm.object_store().read_dependency_valid(dependency))
);
}
#[test]
fn precise_nested_regions_replay_borrowed_dependencies_without_hiding_full_dict_reads() {
let mut vm = endor_core::ObjectStore::new().unwrap();
let globals = vm.new_dict().unwrap();
vm.dict_set(globals, text("name"), text("value")).unwrap();
let scope = vm.begin_read_only();
let token = vm.begin_read_tracking().unwrap();
vm.dict_get_name(globals, "name").unwrap();
let tracked = vm.end_read_tracking_precise(token).unwrap();
assert!(
tracked
.accesses
.iter()
.any(|access| access.object == globals)
);
for dependency in &tracked.dependencies {
assert!(vm.read_dependency_valid(dependency));
}
let reads = vm.end_read_only_precise(scope).unwrap();
vm.dict_set(globals, text("other"), Value::Nil).unwrap();
assert!(
reads
.dependencies
.iter()
.all(|dependency| vm.read_dependency_valid(dependency))
);
let scope = vm.begin_read_only();
vm.dict_items(globals).unwrap();
let reads = vm.end_read_only_precise(scope).unwrap();
vm.dict_set(globals, text("third"), Value::Nil).unwrap();
assert!(
reads
.dependencies
.iter()
.any(|dependency| !vm.read_dependency_valid(dependency))
);
vm.release(globals).unwrap();
assert_eq!(vm.object_count(), 0);
}