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

2277 lines
97 KiB
Rust
Raw Permalink 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 直接检验构造阶段拒绝。
//! 部分协议场景只核对局部结果/计数,保留的测试根由 VM 析构收尾;显式清理场景另断言失效或空堆。
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>,
// 参数/局部/Cell/名称布局随代码移入,执行帧按它分配和寻址。
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 初始化,globals.value 稍后持有它;拒绝新值后身份及原内容都应保持。
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],
] {
// 每份非法物理编码都必须在代码构造阶段拒绝,不能等到 VM 取指才失败。
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();
// 无实参调用必须准确报告缺少唯一必需参数,不把缺参隐式当作 Nil。
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();
// 分配旧式原生 echo 根,本载荷没有源码代码/全局字段;globals 随后为它建立值边。
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();
// 分配恒定报错的旧式原生回调根,环境另持函数边;后面的源码包装器负责 Call 与回溯位置。
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();
// 归还环境测试根后只剩 globals→function→globals 及代码边,显式 GC 应回收三个对象。
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();
// LoadClosure 返回新建的参数 Cell,并非原参数字符串;结果根保活 Cell,Cell 内部边再保活参数。
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(),
);
// Null 是 VM 协议哨兵而非语言值,源码执行把它用于非法值路径时必须拒绝。
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]
/// 由 Rust 测试框架调用:外层参数提升为两个 Cell,两个新函数共享同一捕获列表,外层清理后仍可读写并使用创建时默认值。
fn bytecode_nested_functions_share_cells_and_bind_defaults_after_outer_returns() {
// 独立 VM 隔离本场景根数、全局绑定与暂停状态。
let mut vm = Vm::new().unwrap();
// 为当前测试建立独立全局环境;测试持初始根,函数可另外持环境内部边。
let globals = vm.new_dict().unwrap();
// 两个外部 Cell 从索引零开始,read 只读取 x,不修改捕获列表。
let read = make_code(
&mut vm,
&[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
free_variables: vec!["x".into(), "sentinel".into()],
..CodeLayout::default()
},
);
// 有一个普通参数槽,StoreDeref 索引一对应 x,sentinel 在索引二保持未改。
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()
},
);
// 栈按键、捕获 Cell、代码构造两个函数,write 默认值在 Cell 段之前。
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()
},
);
// 外层源码函数参数 x/sentinel 提升共享槽,函数返回 dict 持两个内层函数。
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();
}
// dict_get 返回借用函数,pair 根一直保持,因此这里不单独 release 函数。
let Some(Value::Object(read)) = vm.dict_get(pair, &text("read")).unwrap() else {
panic!("expected read");
};
// 另一个闭包借用句柄,closure 列表应与 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"));
// 最后函数对根归还后,内层函数、Cell、代码和环境图一起释放。
vm.release(pair).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
/// 由 Rust 测试框架调用:MakeFunction 运行时拒绝代码/Cell 种类错误、布局数量不符及 Null,失败后临时图与根计数全部恢复。
fn bytecode_function_creation_rejects_bad_inputs_without_leaking_roots() {
// 独立 VM 隔离本场景根数、全局绑定与暂停状态。
let mut vm = Vm::new().unwrap();
// 为当前测试建立独立全局环境;测试持初始根,函数可另外持环境内部边。
let globals = vm.new_dict().unwrap();
// 无参数/自由变量代码,作为多余默认值或多余 Cell 的数量错误基线。
let plain = return_constant(&mut vm, Value::Nil);
// 要求一个外部 Cell 的代码,配合不同错误栈值验证捕获种类/数量。
let captured = make_code(
&mut vm,
&[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
vec![],
CodeLayout {
free_variables: vec!["x".into()],
..CodeLayout::default()
},
);
// 有效但未初始化的 Cell,数量不符不能因它是合法 Cell 而被放过。
let cell = vm.new_cell(None).unwrap();
// 函数默认值和闭包布局不匹配的统一错误,不与载荷种类错误混淆。
let mismatch = VmError::InvalidArgument("function defaults or closure do not match code");
// 分别覆盖非对象代码、错误对象代码、Null、默认/捕获数量不符和无效 Cell 输入。
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(),
),
];
// 每个案例追加合法 Return,确保失败来自函数创建而非缺返回。
for (mut instructions, constants, expected) in cases {
// 补全出口,make_code 仍先验证栈数量,运行再校验值的真实种类。
instructions.push(simple(Opcode::ReturnValue));
// 为当前错误案例单独分配代码根,执行后归还以隔离计数断言。
let code = make_code(&mut vm, &instructions, constants, CodeLayout::default());
// 记录含代码与常量边的当前对象数,执行失败不能残留新函数/默认图。
let count = vm.object_count();
// 记录所有已有代码/Cell/环境强计数,失败不得额外 retain 输入。
let roots =
[plain, captured, cell, globals].map(|reference| vm.ref_count(reference).unwrap());
// 必须实际执行到 MakeFunction 才检验值种类,这不是构造时仅栈高验证能替代的检查。
let error = vm.execute(code, globals).unwrap_err();
// Null 类错误比较类别和原因,不固定各案例的实际指令偏移。
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();
}
// 全部案例结束再归还共享输入根,内部边由 Heap 自动清理。
for reference in [plain, captured, cell, globals] {
vm.release(reference).unwrap();
}
assert_eq!(vm.object_count(), 0);
}
/// 错误用例辅助,仅提供 Null 非法值的类别/原因,实际偏移随各指令序列位置变化。
fn invalid_null() -> VmError {
VmError::InvalidBytecode {
offset: 0,
reason: "NULL call marker used as a value",
}
}
#[test]
/// 由 Rust 测试框架调用:同一工厂两次创建的函数捕获不同内置字典,但共享实时全局字典,全局值优先于各自内置。
fn bytecode_functions_capture_builtins_at_creation_and_share_live_globals() {
// 独立 VM 隔离本场景根数、全局绑定与暂停状态。
let mut vm = Vm::new().unwrap();
// 为当前测试建立独立全局环境;测试持初始根,函数可另外持环境内部边。
let globals = vm.new_dict().unwrap();
// 第一次函数创建时捕获的内置字典,内容为 original。
let original = vm.new_dict().unwrap();
// 改写 __builtins__ 后第二次函数创建捕获此字典,旧函数不重绑定。
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();
// 内部代码由工厂持有,作为 MakeFunction 最后一个栈输入。
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(),
);
// 第一次创建产生独立函数拥有根,其 captured builtins 为原字典。
let Value::Object(first) = vm.execute(factory, globals).unwrap() else {
panic!("expected first function");
};
vm.dict_set(globals, text("__builtins__"), Value::Object(replacement))
.unwrap();
// 第二次创建捕获新字典,但 globals 身份仍与第一函数相同。
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]
/// 由 Rust 测试框架调用:函数和临时默认图已成功创建后,后续原生调用失败仍须由帧清理释放两者。
fn later_execution_failure_releases_a_new_function_and_its_temporary_default() {
// 独立 VM 隔离本场景根数、全局绑定与暂停状态。
let mut vm = Vm::new().unwrap();
// 为当前测试建立独立全局环境;测试持初始根,函数可另外持环境内部边。
let globals = vm.new_dict().unwrap();
// 内部代码由工厂持有,作为 MakeFunction 最后一个栈输入。
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]
/// 由 Rust 测试框架调用:只读切片停放仍持函数/常量图,宿主根全归还并 GC 后可恢复完成,结果归还后堆为空。
fn suspended_read_only_call_keeps_roots_and_releases_them_on_completion() {
use endor_core::ExecutionProgress;
// 独立 VM 隔离本场景根数、全局绑定与暂停状态。
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();
// 先放十六条 Nop 强制跨多个量子,然后装载 shared 返回,共十八条源码指令。
let mut instructions = vec![simple(Opcode::Nop); 16];
instructions.extend([op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)]);
// 常量表保留 shared;切片帧又经函数到代码图保活常量。
let code = make_code(
&mut vm,
&instructions,
vec![Value::Object(shared)],
CodeLayout::default(),
);
// 源码函数拥有代码/环境边,停放帧会另外保留函数执行根。
let function = vm
.new_function("sliced", code, globals, vec![], vec![])
.unwrap();
// 首量子只执行一条 Nop,必须 Pending 而不是重跑准备或提前完成。
let mut progress = vm
.start_read_only_call(
function,
&[],
ExecutionLimits {
max_frames: 8,
max_instructions: 100,
},
1,
)
.unwrap();
// 首量子只消耗一条 Nop,应保存暂停帧并报告累计一条,不能提前完成。
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();
// 暂停期间强制 GC 不能回收帧执行根可达图,检验保活不依赖宿主残留根。
vm.collect_cycles();
// 反复恢复同一帧而非重跑函数,累计计数最终必须十八。
loop {
// Pending 只推进下一量子,Complete 负责读取拥有结果并归还。
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]
/// 由 Rust 测试框架调用,验证只读切片的累计预算失败与主动取消都会清理执行根、恢复写权限;无效量子保留暂停状态。
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();
// 八条 Nop 加载入与返回共十条指令,保证总预算三条时尚未正常结束。
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());
// 第二次取消没有任务可处理,必须安全返回 false。
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]
/// 由 Rust 测试框架调用,交替推进两个托管协程;宿主插入的全局写入应保留,证明恢复从保存的指令位置继续。
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();
// 推进第一份到首次 StoreGlobal 之后,累计两条指令。
assert!(matches!(
vm.resume_coroutine(first, 2).unwrap(),
CoroutineProgress::Pending { instructions: 2 }
));
// 第二份也停在首次写入之后,后续都不应重放这个前缀。
assert!(matches!(
vm.resume_coroutine(second, 2).unwrap(),
CoroutineProgress::Pending { instructions: 2 }
));
// 宿主在两次切片之间改写 x;恢复若重放前缀,会错误覆盖此值。
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"))
);
// 取消尚未完成的第二份协程并释放其帧;再次取消应返回 false。
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]
/// 由 Rust 测试框架调用,构造协程、函数和环境的无外根环,验证未显式取消的停放帧也可随不可达环回收。
fn parked_coroutine_cycles_are_collectable_without_cancellation() {
// 隔离对象堆与执行状态,使最终零对象断言只覆盖当前场景。
let mut vm = Vm::new().unwrap();
// 全局字典由测试持初始根,函数和协程图另有内部引用。
let globals = vm.new_dict().unwrap();
// 函数只返回 Nil,不必执行也能验证停放协程的引用环。
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]
/// 由 Rust 测试框架调用,验证托管协程跨恢复累计指令预算,达到硬上限后记录回溯并清理帧。
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.
// 直接构建三条指令:前两条消耗预算,Return 位于上限之后以强制错误路径。
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();
// 首量子只执行 Nop 并停放,累计已用一条。
assert!(matches!(
vm.resume_coroutine(coroutine, 1).unwrap(),
CoroutineProgress::Pending { instructions: 1 }
));
// 第二次执行 LoadConst 后预算耗尽,尚未 Return,因此应终止而非再次 Pending。
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);
}
/// await 测试专用宿主载荷,由测试分配到 VM;eval 的 Await 指令通过 ObjectPayload 轮询它。
/// 只切换宿主就绪标志,不改变托管引用边;结果对象始终由 trace 报告给 Heap。
#[derive(Debug)]
struct AwaitGate {
// 测试与载荷共享就绪标志;测试 Release 写入,poll_await Acquire 读取。
ready: Arc<std::sync::atomic::AtomicBool>,
// 就绪后返回的借用值;若为对象,分配时由 trace 建立内部拥有边。
value: Value,
}
// 实现宿主对象协议,接入 Store 的种类检查、Heap 的引用遍历和 eval 的等待操作。
impl ObjectPayload for AwaitGate {
/// Store/宿主载荷检查调用,保留具体 Rust 类型以支持 Any 下转。
fn as_any(&self) -> &dyn Any {
self
}
/// 对象种类诊断调用,提供稳定的测试载荷名称。
fn kind_name(&self) -> &'static str {
"await-gate"
}
/// Heap 分配载荷时据此登记对象边快照,回收沿已登记的边处理;立即值无需报告。
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
// 只有对象值携带堆引用;必须报告这一边,使测试释放原根后结果仍存活。
if let Value::Object(reference) = self.value {
visit(reference);
}
}
/// eval 每次进入 Await 时调用;非阻塞返回 Pending 或借用 Ready 值,不自行增加结果根。
fn poll_await(&self) -> Result<endor_core::AwaitPoll, VmError> {
// 与测试的 Release 写入配对;标志可变而 value 的托管引用关系保持不变。
Ok(if self.ready.load(Ordering::Acquire) {
// clone 只复制 Value 表示;eval 在丢弃等待操作数前为结果取得独立根。
endor_core::AwaitPoll::Ready(self.value.clone())
} else {
// 尚未就绪时帧保留原操作数与 Await 位置,下次只重新轮询该指令。
endor_core::AwaitPoll::Pending
})
}
}
#[test]
/// 由 Rust 测试框架调用,验证 Await 停放不重放前缀,等待图保活结果,完成后结果根独立于协程存活。
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));
// 分配最终结果并写入 marker,稍后释放原根以排除宿主根掩盖保活错误。
let value = vm.new_object().unwrap();
vm.set_attr(value, "marker", text("kept")).unwrap();
// 分配等待载荷,Heap 根据 trace 保留 value;宿主标志使用 Arc 共享。
let gate = vm
.allocate(AwaitGate {
ready: ready.clone(),
value: Value::Object(value),
})
.unwrap();
// 环境保存可观察的前缀副作用,等待后宿主将改写它以检测指令重放。
let globals = vm.new_dict().unwrap();
// 先写 step,再载入 gate 并 Await,最后返回等待结果;两次恢复应只重试 Await。
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()
},
);
// 函数拥有代码与环境,代码常量表另持 gate。
let function = vm
.new_function("awaited", code, globals, vec![], vec![])
.unwrap();
// 创建可变托管协程;普通同步入口不支持停放的 Pending 等待。
let coroutine = vm
.new_coroutine(function, &[], ExecutionLimits::default())
.unwrap();
// 首轮应返回 Waiting,携带的借用等待对象必须正是 gate。
assert!(
matches!(vm.resume_coroutine(coroutine, 64).unwrap(), CoroutineProgress::Waiting { awaitable, .. } if awaitable == gate)
);
// 归还结果原根,接下来靠 gate 的内部边保活。
vm.release(value).unwrap();
// 归还等待对象原根,代码常量及停放帧操作数仍持有它。
vm.release(gate).unwrap();
// 等待期间宿主改写前缀结果;恢复后必须保留该写入。
vm.dict_set(globals, text("step"), text("interleaved"))
.unwrap();
// 强制回收检查等待图仍可达,不能把帧上的 await 操作数误判为垃圾。
vm.collect_cycles();
// 发布就绪状态;下一次恢复应从保存的 Await 指令重新轮询。
ready.store(true, Ordering::Release);
// 就绪后应取得拥有结果;不应再次返回 Waiting 或重跑整段代码。
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();
}
// 结果必须仍是有效对象,marker 检查不能依赖已释放的等待图。
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]
/// 由 Rust 测试框架调用,比较切片、同步精确及兼容读取集合;验证无关名称不使名称依赖失效,而遮蔽、命中值变化及对象销毁会失效。
fn named_reads_ignore_unrelated_globals_but_track_missing_shadow_and_payload_kind() {
// 隔离读取作用域与对象生命周期,后续销毁测试不会受其他用例影响。
let mut vm = Vm::new().unwrap();
// 全局字典起初缺 name;这个缺失也是必须记录的观察。
let globals = vm.new_dict().unwrap();
// 回退字典提供 name=builtin,全局缺失后才读取它。
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();
// 只读取名称 name 后返回,避免其他内容读取把依赖扩大为整个字典版本。
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();
// 增加无关名称,不能影响此前 name 缺失与内置命中的精确观察。
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]
/// 由 Rust 测试框架调用,直接在 ObjectStore 检查子区间读取及重验均登记到外层;按名称依赖保持精确,整字典读取仍保守失效。
fn precise_nested_regions_replay_borrowed_dependencies_without_hiding_full_dict_reads() {
// 直接使用 ObjectStore,只验证读取追踪,无需创建可执行函数或运行解释器。
let mut vm = endor_core::ObjectStore::new().unwrap();
// 准备只含 name 的字典,之后添加其他名称来区分精确与整对象观察。
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();
// 首次只读仅观察 name,新增 other 后所有原依赖应仍然有效。
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);
}