docs(core): explain closure roots and coroutine cleanup tests

This commit is contained in:
2026-11-15 18:17:31 +08:00
parent 7d14bce9f3
commit 28b305b503
+122
View File
@@ -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();
}