diff --git a/crates/core/tests/execution.rs b/crates/core/tests/execution.rs index 4905aaf..3533ecb 100644 --- a/crates/core/tests/execution.rs +++ b/crates/core/tests/execution.rs @@ -1289,9 +1289,13 @@ fn bytecode_function_defaults_keep_temporary_objects_alive_after_factory_returns } #[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)], @@ -1301,6 +1305,7 @@ fn bytecode_nested_functions_share_cells_and_bind_defaults_after_outer_returns() ..CodeLayout::default() }, ); + // 有一个普通参数槽,StoreDeref 索引一对应 x,sentinel 在索引二保持未改。 let write = make_code( &mut vm, &[ @@ -1317,6 +1322,7 @@ fn bytecode_nested_functions_share_cells_and_bind_defaults_after_outer_returns() ..CodeLayout::default() }, ); + // 栈按键、捕获 Cell、代码构造两个函数,write 默认值在 Cell 段之前。 let outer = make_code( &mut vm, &[ @@ -1348,21 +1354,26 @@ fn bytecode_nested_functions_share_cells_and_bind_defaults_after_outer_returns() ..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"); }; @@ -1380,15 +1391,21 @@ fn bytecode_nested_functions_share_cells_and_bind_defaults_after_outer_returns() 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)], @@ -1398,8 +1415,11 @@ fn bytecode_function_creation_rejects_bad_inputs_without_leaking_roots() { ..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)], @@ -1476,13 +1496,20 @@ fn bytecode_function_creation_rejects_bad_inputs_without_leaking_roots() { 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, @@ -1499,14 +1526,17 @@ fn bytecode_function_creation_rejects_bad_inputs_without_leaking_roots() { [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, @@ -1515,10 +1545,15 @@ fn invalid_null() -> VmError { } #[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(); @@ -1526,6 +1561,7 @@ fn bytecode_functions_capture_builtins_at_creation_and_share_live_globals() { .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)], @@ -1535,6 +1571,7 @@ fn bytecode_functions_capture_builtins_at_creation_and_share_live_globals() { ..CodeLayout::default() }, ); + // 工厂字节码使用函数创建协议,输入根保留至新函数成功持有所有边。 let factory = make_code( &mut vm, &[ @@ -1545,14 +1582,17 @@ fn bytecode_functions_capture_builtins_at_creation_and_share_live_globals() { 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(); } @@ -1568,9 +1608,13 @@ fn bytecode_functions_capture_builtins_at_creation_and_share_live_globals() { } #[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)], @@ -1581,9 +1625,11 @@ fn later_execution_failure_releases_a_new_function_and_its_temporary_default() { ..CodeLayout::default() }, ); + // 原生失败回调在函数和默认图已创建后才进入,验证后续错误的整体清理。 let fail = vm .allocate(EndorNativeFunctionObject::new("fail", fail)) .unwrap(); + // 工厂字节码使用函数创建协议,输入根保留至新函数成功持有所有边。 let factory = make_code( &mut vm, &[ @@ -1598,8 +1644,11 @@ fn later_execution_failure_releases_a_new_function_and_its_temporary_default() { 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), @@ -1607,29 +1656,38 @@ fn later_execution_failure_releases_a_new_function_and_its_temporary_default() { ); 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, @@ -1650,12 +1708,19 @@ fn suspended_read_only_call_keeps_roots_and_releases_them_on_completion() { .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 { @@ -1669,6 +1734,7 @@ fn suspended_read_only_call_keeps_roots_and_releases_them_on_completion() { vm.get_attr(shared, "value").unwrap(), Value::Number(7_i64.into()) ); + // 最后结果根归还后,暂停帧/函数等已清理,输入也应不再有残余边。 vm.release_value(value).unwrap(); break; } @@ -1679,22 +1745,31 @@ fn suspended_read_only_call_keeps_roots_and_releases_them_on_completion() { } #[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, &[], @@ -1705,34 +1780,50 @@ fn suspended_call_cancellation_and_total_budget_restore_read_only_state() { 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, &[ @@ -1749,26 +1840,33 @@ fn managed_coroutines_interleave_without_replaying_writes() { ..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!( + // 推进第一份到首次 StoreGlobal 之后,累计两条指令。 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, @@ -1783,9 +1881,12 @@ fn managed_coroutines_interleave_without_replaying_writes() { 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(); } @@ -1793,32 +1894,45 @@ fn managed_coroutines_interleave_without_replaying_writes() { } #[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( @@ -1833,9 +1947,11 @@ fn managed_coroutine_hard_budget_accumulates_and_error_releases_frames() { .unwrap(), ) .unwrap(); + // 函数持有代码和全局环境,后续协程或暂停帧再持有函数。 let function = vm .new_function("limited", code, globals, vec![], vec![]) .unwrap(); + // 创建托管协程对象,停放状态保留帧、函数与环境图。 let coroutine = vm .new_coroutine( function, @@ -1844,20 +1960,26 @@ fn managed_coroutine_hard_budget_accumulates_and_error_releases_frames() { max_frames: 4, // The second resume reaches the cumulative limit before // the third instruction, without replaying the first. + // 硬上限跨量子累计;第二次恢复不能获得新的两条预算。 max_instructions: 2, }, ) .unwrap(); assert!(matches!( + // 首量子只执行 Nop 并停放,累计已用一条。 vm.resume_coroutine(coroutine, 1).unwrap(), CoroutineProgress::Pending { instructions: 1 } )); assert!(matches!( + // 第二次执行 LoadConst 后预算耗尽,尚未 Return,因此应终止而非再次 Pending。 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(); }