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

2015 lines
73 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]
fn attribute_operations_and_stack_copies_transfer_roots_without_leaks() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
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();
let record = vm.new_object().unwrap();
let value = vm.new_string("value").unwrap();
let returned = vm
.call(function, &[Value::Object(record), Value::Object(value)])
.unwrap();
assert_eq!(vm.ref_count(value).unwrap(), 3); // host, attribute, return value
vm.release_value(returned).unwrap();
vm.release(record).unwrap();
assert_eq!(vm.ref_count(value).unwrap(), 1);
}
#[test]
fn comparison_truth_and_forward_conditionals_use_runtime_values() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
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]
fn unbound_names_deleted_locals_and_invalid_null_values_fail_cleanly() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
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()))
);
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()))
);
let null = make_code(
&mut vm,
&[simple(Opcode::PushNull), simple(Opcode::ReturnValue)],
vec![],
CodeLayout::default(),
);
assert!(matches!(
vm.execute(null, globals),
Err(VmError::InvalidBytecode { .. })
));
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() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
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]
fn implicit_receiver_slot_is_distinct_from_the_null_call_marker() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
let function = vm
.allocate(EndorNativeFunctionObject::new("echo", echo))
.unwrap();
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 is an actual implicit argument here, not the internal NULL marker.
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]
);
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]
fn bytecode_defines_global_functions_and_calls_them_with_defaults() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
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);
let inner = vm.new_code(inner).unwrap();
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"));
let Some(Value::Object(first)) = vm.dict_get(globals, &text("identity")).unwrap() else {
panic!("expected a function");
};
vm.retain(first).unwrap();
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();
// The global function, its code and its globals form a collectible cycle.
assert_eq!(vm.collect_cycles(), 3);
assert_eq!(vm.object_count(), 0);
}
#[test]
fn bytecode_function_defaults_keep_temporary_objects_alive_after_factory_returns() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
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()
},
);
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();
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);
}