docs(core): explain attribute stack and function factory regressions

This commit is contained in:
2026-11-15 18:12:45 +08:00
parent bf3313f58d
commit 7d14bce9f3
+66 -2
View File
@@ -887,9 +887,13 @@ fn captured_argument_cells_keep_their_contents_after_the_frame_returns() {
}
#[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,
&[
@@ -911,24 +915,34 @@ fn attribute_operations_and_stack_copies_transfer_roots_without_leaks() {
..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,
&[
@@ -950,9 +964,13 @@ fn comparison_truth_and_forward_conditionals_use_runtime_values() {
}
#[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)],
@@ -966,6 +984,7 @@ fn unbound_names_deleted_locals_and_invalid_null_values_fail_cleanly() {
vm.execute(missing, globals),
Err(VmError::NameNotFound("missing".into()))
);
// 先成功赋值再 DeleteFast,最后读取检验 None 未绑定状态。
let deleted = make_code(
&mut vm,
&[
@@ -985,6 +1004,7 @@ fn unbound_names_deleted_locals_and_invalid_null_values_fail_cleanly() {
vm.execute(deleted, globals),
Err(VmError::UnboundLocal("x".into()))
);
// PushNull 满足抽象栈高度但不是语言 Value,ReturnValue 必须在运行时拒绝。
let null = make_code(
&mut vm,
&[simple(Opcode::PushNull), simple(Opcode::ReturnValue)],
@@ -995,15 +1015,20 @@ fn unbound_names_deleted_locals_and_invalid_null_values_fail_cleanly() {
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,
&[
@@ -1015,6 +1040,7 @@ fn failed_map_construction_cleans_both_partial_map_and_temporary_input() {
vec![Value::Number(1_i64.into())],
CodeLayout::default(),
);
// 失败前存活对象基线,执行临时函数和两层临时图不得泄漏。
let before = vm.object_count();
assert_eq!(
vm.execute(code, globals),
@@ -1024,12 +1050,17 @@ fn failed_map_construction_cleans_both_partial_map_and_temporary_input() {
}
#[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,
&[
@@ -1041,11 +1072,12 @@ fn implicit_receiver_slot_is_distinct_from_the_null_call_marker() {
vec![Value::Object(function), Value::Nil],
CodeLayout::default(),
);
// Nil is an actual implicit argument here, not the internal NULL marker.
// 此处 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(),
@@ -1055,6 +1087,7 @@ fn function_creation_operands_round_trip_and_require_all_inputs() {
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),
@@ -1073,6 +1106,7 @@ fn function_creation_operands_round_trip_and_require_all_inputs() {
);
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(
@@ -1086,6 +1120,7 @@ fn function_creation_operands_round_trip_and_require_all_inputs() {
Err(VmError::InvalidBytecode { .. })
));
}
// 只有一个常量不能满足含默认值或捕获槽的函数构造,缺输入仍须静态拒绝。
for creation in [
Instruction::make_function(1, 0),
Instruction::make_function(0, 1),
@@ -1106,9 +1141,13 @@ fn function_creation_operands_round_trip_and_require_all_inputs() {
}
#[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![],
@@ -1120,7 +1159,9 @@ fn bytecode_defines_global_functions_and_calls_them_with_defaults() {
)
.unwrap()
.with_location("identity", "functions.endor", 2);
// 代码常量由模块/工厂保留,归还测试根后仍用于后续 MakeFunction。
let inner = vm.new_code(inner).unwrap();
// 按默认值、代码顺序压栈创建函数,存全局后再 Call 零显式参数走默认值。
let module = make_code(
&mut vm,
&[
@@ -1139,12 +1180,16 @@ fn bytecode_defines_global_functions_and_calls_them_with_defaults() {
..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();
@@ -1156,6 +1201,7 @@ fn bytecode_defines_global_functions_and_calls_them_with_defaults() {
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");
};
@@ -1166,18 +1212,25 @@ fn bytecode_defines_global_functions_and_calls_them_with_defaults() {
.code(),
inner
);
// 归还为跨重定义而取得的旧函数根,不能把借用全局结果当作拥有根。
vm.release(first).unwrap();
// 模块代码常量边释放,仍由全局函数持有内部代码。
vm.release(module).unwrap();
// 归还环境测试根,若函数与环境互相持有则需显式环回收。
vm.release(globals).unwrap();
// The global function, its code and its globals form a collectible cycle.
// 全局字典持有函数、函数持有全局和代码,失去外部根后形成三个对象的可回收图。
assert_eq!(vm.collect_cycles(), 3);
assert_eq!(vm.object_count(), 0);
}
#[test]
/// 工厂创建临时默认字典,函数返回后默认值仍由函数边保活,每次调用返回同一默认对象的独立根。
fn bytecode_function_defaults_keep_temporary_objects_alive_after_factory_returns() {
// 独立 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)],
@@ -1188,6 +1241,7 @@ fn bytecode_function_defaults_keep_temporary_objects_alive_after_factory_returns
..CodeLayout::default()
},
);
// 先在栈上新建默认空图,再压 tag 和代码,MakeFunction 把前两项作为默认值。
let factory = make_code(
&mut vm,
&[
@@ -1200,26 +1254,36 @@ fn bytecode_function_defaults_keep_temporary_objects_alive_after_factory_returns
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);
}