2273 lines
97 KiB
Rust
2273 lines
97 KiB
Rust
//! 通过手工指令验证 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],
|
||
] {
|
||
// 每份非法物理编码都必须在代码构造阶段拒绝,不能等到 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();
|
||
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
|
||
let function = vm
|
||
.allocate(EndorNativeFunctionObject::new("echo", echo))
|
||
.unwrap();
|
||
vm.dict_set(globals, text("echo"), Value::Object(function))
|
||
.unwrap();
|
||
// 调用前/后按场景归还测试函数根;全局字典可能仍持有函数内部边。
|
||
vm.release(function).unwrap();
|
||
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
|
||
let code = make_code(
|
||
&mut vm,
|
||
&[
|
||
op(Opcode::LoadGlobal, 0),
|
||
simple(Opcode::PushNull),
|
||
op(Opcode::BuildMap, 0),
|
||
op(Opcode::Call, 1),
|
||
simple(Opcode::ReturnValue),
|
||
],
|
||
vec![],
|
||
CodeLayout {
|
||
names: vec!["echo".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
// 执行结果拥有根可跨越代码与全局生命周期。
|
||
let result = vm.execute(code, globals).unwrap();
|
||
// 移除代码常量边后,返回图必须自己保留所需对象;不是只靠旧帧保活。
|
||
vm.release(code).unwrap();
|
||
// 归还环境测试根,临时执行函数已清理,结果不能依赖其持续存在。
|
||
vm.release(globals).unwrap();
|
||
assert_eq!(vm.object_count(), 1);
|
||
vm.release_value(result).unwrap();
|
||
assert_eq!(vm.object_count(), 0);
|
||
}
|
||
|
||
/// 原生辅助恒定报错,使执行器在 Call 处回溯与清理临时参数。
|
||
fn fail(_arguments: &[Value]) -> Result<Value, VmError> {
|
||
Err(VmError::InvalidArgument("host failure"))
|
||
}
|
||
|
||
#[test]
|
||
/// 错误后对象数恢复调用前,回溯保留故障 Call 偏移及显式代码位置元数据。
|
||
fn native_failure_unwinds_temporary_roots_and_keeps_execution_locations() {
|
||
// 独立 VM 隔离调用帧、全局与根计数断言。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
|
||
let function = vm
|
||
.allocate(EndorNativeFunctionObject::new("fail", fail))
|
||
.unwrap();
|
||
vm.dict_set(globals, text("fail"), Value::Object(function))
|
||
.unwrap();
|
||
// 附显式名称、文件和起始行,用故障回溯断言实际保存的元数据。
|
||
let code = EndorCode::from_instructions(
|
||
&[
|
||
op(Opcode::LoadGlobal, 0),
|
||
simple(Opcode::PushNull),
|
||
op(Opcode::BuildMap, 0),
|
||
op(Opcode::Call, 1),
|
||
simple(Opcode::ReturnValue),
|
||
],
|
||
vec![],
|
||
CodeLayout {
|
||
names: vec!["fail".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
)
|
||
.unwrap()
|
||
.with_location("outer", "demo.endor", 12);
|
||
// 将已验证代码分配为拥有根,临时执行函数之后另外持代码边。
|
||
let code = vm.new_code(code).unwrap();
|
||
// 记录代码/全局等场景根数,失败后不得残留临时字典、函数或帧根。
|
||
let before = vm.object_count();
|
||
assert_eq!(
|
||
vm.execute(code, globals),
|
||
Err(VmError::InvalidArgument("host failure"))
|
||
);
|
||
assert_eq!(vm.object_count(), before);
|
||
assert_eq!(vm.last_traceback()[0].function, "outer");
|
||
assert_eq!(vm.last_traceback()[0].instruction_offset, 3);
|
||
assert_eq!(vm.last_traceback()[0].filename, "demo.endor");
|
||
assert_eq!(vm.last_traceback()[0].first_line, 12);
|
||
}
|
||
|
||
#[test]
|
||
/// 递归源码调用由显式帧向量推进,触及深度硬上限后全部执行根恢复,环境环可显式回收。
|
||
fn recursion_uses_explicit_frames_and_unwinds_at_the_frame_limit() {
|
||
// 独立 VM 隔离调用帧、全局与根计数断言。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
|
||
let code = make_code(
|
||
&mut vm,
|
||
&[
|
||
op(Opcode::LoadGlobal, 0),
|
||
simple(Opcode::PushNull),
|
||
op(Opcode::Call, 0),
|
||
simple(Opcode::ReturnValue),
|
||
],
|
||
vec![],
|
||
CodeLayout {
|
||
names: vec!["recurse".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
|
||
let function = vm
|
||
.new_function("recurse", code, globals, vec![], vec![])
|
||
.unwrap();
|
||
vm.dict_set(globals, text("recurse"), Value::Object(function))
|
||
.unwrap();
|
||
// 递归深度会临时保留多份函数根,退出后应恢复精确初始计数。
|
||
let roots = vm.ref_count(function).unwrap();
|
||
assert_eq!(
|
||
vm.call_with_limits(
|
||
function,
|
||
&[],
|
||
ExecutionLimits {
|
||
max_frames: 64,
|
||
max_instructions: 1000
|
||
}
|
||
),
|
||
Err(VmError::ExecutionLimit("frames"))
|
||
);
|
||
assert_eq!(vm.last_traceback().len(), 64);
|
||
assert_eq!(vm.ref_count(function).unwrap(), roots);
|
||
// 调用前/后按场景归还测试函数根;全局字典可能仍持有函数内部边。
|
||
vm.release(function).unwrap();
|
||
// 移除代码常量边后,返回图必须自己保留所需对象;不是只靠旧帧保活。
|
||
vm.release(code).unwrap();
|
||
// 归还环境测试根,临时执行函数已清理,结果不能依赖其持续存在。
|
||
vm.release(globals).unwrap();
|
||
assert_eq!(vm.collect_cycles(), 3);
|
||
}
|
||
|
||
#[test]
|
||
/// 栈高合法的无限回边仍须由硬指令预算终止,不能泄漏临时函数或帧。
|
||
fn instruction_limit_stops_a_valid_infinite_loop_without_leaking_frames() {
|
||
// 独立 VM 隔离调用帧、全局与根计数断言。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
|
||
let code = make_code(
|
||
&mut vm,
|
||
&[op(Opcode::JumpBackward, 1)],
|
||
vec![],
|
||
CodeLayout::default(),
|
||
);
|
||
// 记录代码/全局等场景根数,失败后不得残留临时字典、函数或帧根。
|
||
let before = vm.object_count();
|
||
assert_eq!(
|
||
vm.execute_with_limits(
|
||
code,
|
||
globals,
|
||
ExecutionLimits {
|
||
max_frames: 8,
|
||
max_instructions: 3
|
||
}
|
||
),
|
||
Err(VmError::ExecutionLimit("instructions"))
|
||
);
|
||
assert_eq!(vm.object_count(), before);
|
||
assert_eq!(vm.last_traceback().len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
/// 函数捕获创建时内置字典身份,全局同名键仍优先,改写 __builtins__ 不重绑定旧函数。
|
||
fn function_globals_shadow_builtins_and_capture_the_original_builtins_dictionary() {
|
||
// 独立 VM 隔离调用帧、全局与根计数断言。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 原内置字典由函数创建时捕获,不因稍后 globals.__builtins__ 改变而替换身份。
|
||
let builtins = vm.new_dict().unwrap();
|
||
vm.dict_set(builtins, text("answer"), text("builtin"))
|
||
.unwrap();
|
||
vm.dict_set(globals, text("__builtins__"), Value::Object(builtins))
|
||
.unwrap();
|
||
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
|
||
let code = make_code(
|
||
&mut vm,
|
||
&[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)],
|
||
vec![],
|
||
CodeLayout {
|
||
names: vec!["answer".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
|
||
let function = vm
|
||
.new_function("read", code, globals, vec![], vec![])
|
||
.unwrap();
|
||
// 新内置字典仅更新 globals 字段,用于证明旧函数仍引用原内置空间。
|
||
let replacement = vm.new_dict().unwrap();
|
||
vm.dict_set(globals, text("__builtins__"), Value::Object(replacement))
|
||
.unwrap();
|
||
// 归还原内置测试根后仍应由函数 captured builtins 强边保活。
|
||
vm.release(builtins).unwrap();
|
||
assert_eq!(vm.call(function, &[]).unwrap(), text("builtin"));
|
||
vm.dict_set(globals, text("answer"), text("global"))
|
||
.unwrap();
|
||
assert_eq!(vm.call(function, &[]).unwrap(), text("global"));
|
||
}
|
||
|
||
#[test]
|
||
/// 多个函数共享同一 Cell,Load/Store/DeleteDeref 对内容变化立即可见,clear 后读取报未绑定。
|
||
fn shared_free_variable_cells_are_read_written_and_deleted_by_bytecode() {
|
||
// 独立 VM 隔离调用帧、全局与根计数断言。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 共享槽初值 first,read/write/clear 函数各自持有同一 Cell 强边。
|
||
let cell = vm.new_cell(Some(text("first"))).unwrap();
|
||
// 没有局部槽,唯一自由变量 Cell 位于 localsplus 索引零。
|
||
let read_code = make_code(
|
||
&mut vm,
|
||
&[op(Opcode::LoadDeref, 0), simple(Opcode::ReturnValue)],
|
||
vec![],
|
||
CodeLayout {
|
||
free_variables: vec!["x".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
// 有一个普通参数槽,外部 Cell 追加在索引一,StoreDeref 不能误写局部槽。
|
||
let write_code = make_code(
|
||
&mut vm,
|
||
&[
|
||
op(Opcode::LoadFast, 0),
|
||
op(Opcode::StoreDeref, 1),
|
||
op(Opcode::LoadConst, 0),
|
||
simple(Opcode::ReturnValue),
|
||
],
|
||
vec![Value::Nil],
|
||
CodeLayout {
|
||
parameters: vec!["value".into()],
|
||
locals: vec!["value".into()],
|
||
free_variables: vec!["x".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
// DeleteDeref 清空 Cell 而非写 Nil,随后返回直接 Nil。
|
||
let clear_code = make_code(
|
||
&mut vm,
|
||
&[
|
||
op(Opcode::DeleteDeref, 0),
|
||
op(Opcode::LoadConst, 0),
|
||
simple(Opcode::ReturnValue),
|
||
],
|
||
vec![Value::Nil],
|
||
CodeLayout {
|
||
free_variables: vec!["x".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
// 只读捕获值的函数,共享槽未复制成独立快照。
|
||
let read = vm
|
||
.new_function("read", read_code, globals, vec![], vec![cell])
|
||
.unwrap();
|
||
// 把显式参数写到同一捕获 Cell,其他闭包应立即看到新值。
|
||
let write = vm
|
||
.new_function("write", write_code, globals, vec![], vec![cell])
|
||
.unwrap();
|
||
// 删除同一捕获变量后 read 映射 EmptyCell 为具名 UnboundLocal。
|
||
let clear = vm
|
||
.new_function("clear", clear_code, globals, vec![], vec![cell])
|
||
.unwrap();
|
||
assert_eq!(vm.call(read, &[]).unwrap(), text("first"));
|
||
vm.call(write, &[text("updated")]).unwrap();
|
||
assert_eq!(vm.call(read, &[]).unwrap(), text("updated"));
|
||
vm.call(clear, &[]).unwrap();
|
||
assert_eq!(vm.call(read, &[]), Err(VmError::UnboundLocal("x".into())));
|
||
}
|
||
|
||
#[test]
|
||
/// 参数提升为 Cell 后通过 LoadClosure 返回,原帧与参数测试根释放仍由 Cell 边保活内容。
|
||
fn captured_argument_cells_keep_their_contents_after_the_frame_returns() {
|
||
// 独立 VM 隔离调用帧、全局与根计数断言。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 函数执行环境的拥有根,函数也经载荷强边持有该字典。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 手工已验证代码,操作数布局与后面的参数/自由变量元数据一一对应。
|
||
let code = make_code(
|
||
&mut vm,
|
||
&[op(Opcode::LoadClosure, 0), simple(Opcode::ReturnValue)],
|
||
vec![],
|
||
CodeLayout {
|
||
parameters: vec!["x".into()],
|
||
locals: vec!["x".into()],
|
||
cell_variables: vec!["x".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
// 创建源码/原生函数拥有根,代码/全局/默认值内部边由分配协议另计。
|
||
let function = vm
|
||
.new_function("capture", code, globals, vec![], vec![])
|
||
.unwrap();
|
||
// 参数对象有测试根,帧初始化提升 Cell 时另建立内容强边。
|
||
let value = vm.new_string("captured").unwrap();
|
||
// 函数把已有参数/默认值作为结果,身份不变但结果仍取得独立拥有根。
|
||
let returned = vm.call(function, &[Value::Object(value)]).unwrap();
|
||
// 归还原参数测试根,返回的 Cell 内容边仍须保活它。
|
||
vm.release(value).unwrap();
|
||
// LoadClosure 返回的是 Cell 本身,不是 Cell 当前内容。
|
||
let Value::Object(cell) = returned else {
|
||
panic!("expected cell");
|
||
};
|
||
assert_eq!(vm.cell_get(cell).unwrap(), Value::Object(value));
|
||
assert_eq!(vm.str(value).unwrap(), "captured");
|
||
// 最后 Cell 结果根归还后其内容边释放,原字符串身份应立即失效。
|
||
vm.release(cell).unwrap();
|
||
assert!(matches!(vm.object(value), Err(VmError::InvalidHandle)));
|
||
}
|
||
|
||
#[test]
|
||
/// StoreAttr/LoadAttr 与 Copy/Swap/PopTop 组合后仅保留宿主、属性和结果三份根,不残留复制栈根。
|
||
fn attribute_operations_and_stack_copies_transfer_roots_without_leaks() {
|
||
// 独立 VM 隔离堆计数和全局/回溯状态。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 手工已验证指令,按下面布局解释局部槽和 names 索引。
|
||
let code = make_code(
|
||
&mut vm,
|
||
&[
|
||
op(Opcode::LoadFast, 1),
|
||
op(Opcode::LoadFast, 0),
|
||
op(Opcode::StoreAttr, 0),
|
||
op(Opcode::LoadFast, 0),
|
||
op(Opcode::LoadAttr, 0),
|
||
op(Opcode::Copy, 1),
|
||
op(Opcode::Swap, 2),
|
||
simple(Opcode::PopTop),
|
||
simple(Opcode::ReturnValue),
|
||
],
|
||
vec![],
|
||
CodeLayout {
|
||
parameters: vec!["record".into(), "value".into()],
|
||
locals: vec!["record".into(), "value".into()],
|
||
names: vec!["x".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
// 源码/原生函数有一份测试根,调用时另行保留帧/协议根。
|
||
let function = vm
|
||
.new_function("assign", code, globals, vec![], vec![])
|
||
.unwrap();
|
||
// 属性写入目标拥有测试根,字段还会独立持有 value 边。
|
||
let record = vm.new_object().unwrap();
|
||
// 原值测试根保留到最后,调用返回和属性边应各增加一份持有。
|
||
let value = vm.new_string("value").unwrap();
|
||
// 返回对象必须独立拥有,栈 Copy 产生的额外根应已被 PopTop 清理。
|
||
let returned = vm
|
||
.call(function, &[Value::Object(record), Value::Object(value)])
|
||
.unwrap();
|
||
assert_eq!(vm.ref_count(value).unwrap(), 3); // host, attribute, return value
|
||
// 先归还调用结果根,再释放 record 去掉字段边,value 最终只剩测试根。
|
||
vm.release_value(returned).unwrap();
|
||
// 属性 owner 最后根消失,其字段拥有边随对象释放。
|
||
vm.release(record).unwrap();
|
||
assert_eq!(vm.ref_count(value).unwrap(), 1);
|
||
}
|
||
|
||
#[test]
|
||
/// 文本比较经 ToBool/UnaryNot 后控制前向分支,检验跳转基点与运行时真值。
|
||
fn comparison_truth_and_forward_conditionals_use_runtime_values() {
|
||
// 独立 VM 隔离堆计数和全局/回溯状态。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 手工已验证指令,按下面布局解释局部槽和 names 索引。
|
||
let code = make_code(
|
||
&mut vm,
|
||
&[
|
||
op(Opcode::LoadConst, 0),
|
||
op(Opcode::LoadConst, 1),
|
||
Instruction::compare(CompareOp::Lt),
|
||
simple(Opcode::ToBool),
|
||
simple(Opcode::UnaryNot),
|
||
op(Opcode::PopJumpIfTrue, 2),
|
||
op(Opcode::LoadConst, 2),
|
||
simple(Opcode::ReturnValue),
|
||
op(Opcode::LoadConst, 3),
|
||
simple(Opcode::ReturnValue),
|
||
],
|
||
vec![text("a"), text("b"), text("less"), text("other")],
|
||
CodeLayout::default(),
|
||
);
|
||
assert_eq!(vm.execute(code, globals).unwrap(), text("less"));
|
||
}
|
||
|
||
#[test]
|
||
/// 缺全局、删除局部后读取和 Null 逃逸分别报错,随后成功执行应清掉旧回溯。
|
||
fn unbound_names_deleted_locals_and_invalid_null_values_fail_cleanly() {
|
||
// 独立 VM 隔离堆计数和全局/回溯状态。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 测试执行环境本身有根,函数可能另持全局边形成待收集环。
|
||
let globals = vm.new_dict().unwrap();
|
||
// 名称表合法但环境无该键,属于执行期 NameNotFound 而非索引验证错误。
|
||
let missing = make_code(
|
||
&mut vm,
|
||
&[op(Opcode::LoadGlobal, 0), simple(Opcode::ReturnValue)],
|
||
vec![],
|
||
CodeLayout {
|
||
names: vec!["missing".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
assert_eq!(
|
||
vm.execute(missing, globals),
|
||
Err(VmError::NameNotFound("missing".into()))
|
||
);
|
||
// 先成功赋值再 DeleteFast,最后读取检验 None 未绑定状态。
|
||
let deleted = make_code(
|
||
&mut vm,
|
||
&[
|
||
op(Opcode::LoadConst, 0),
|
||
op(Opcode::StoreFast, 0),
|
||
op(Opcode::DeleteFast, 0),
|
||
op(Opcode::LoadFast, 0),
|
||
simple(Opcode::ReturnValue),
|
||
],
|
||
vec![Value::Nil],
|
||
CodeLayout {
|
||
locals: vec!["x".into()],
|
||
..CodeLayout::default()
|
||
},
|
||
);
|
||
assert_eq!(
|
||
vm.execute(deleted, globals),
|
||
Err(VmError::UnboundLocal("x".into()))
|
||
);
|
||
// PushNull 满足抽象栈高度但不是语言 Value,ReturnValue 必须在运行时拒绝。
|
||
let null = make_code(
|
||
&mut vm,
|
||
&[simple(Opcode::PushNull), simple(Opcode::ReturnValue)],
|
||
vec![],
|
||
CodeLayout::default(),
|
||
);
|
||
// 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);
|
||
}
|