docs(core): explain bytecode validation and basic execution regressions

This commit is contained in:
2026-11-15 18:09:50 +08:00
parent 0c69dd83ba
commit 2ddb26c541
+68 -1
View File
@@ -1,3 +1,6 @@
//! 通过手工指令验证 VM 求值、根生命周期及暂停行为,测试框架直接调用,无需源码解析器。
//! op/simple/make_code 统一构造已验证代码;错误编码用 EndorCode 直接检验构造阶段拒绝。
use std::any::Any;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -8,15 +11,19 @@ use endor_core::{
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],
@@ -26,6 +33,7 @@ fn make_code(
vm.new_code(EndorCode::from_instructions(instructions, constants, layout).unwrap())
.unwrap()
}
/// 其他执行测试复用的最小返回常量代码,根由调用测试归还。
fn return_constant(vm: &mut Vm, value: Value) -> ObjectRef {
make_code(
vm,
@@ -36,41 +44,57 @@ fn return_constant(vm: &mut Vm, value: Value) -> ObjectRef {
}
#[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,
&[
@@ -82,6 +106,7 @@ fn opt_in_assignment_keeps_alias_identity_ordinary_values_and_root_accounting()
simple(Opcode::ReturnValue),
],
vec![Value::Object(old), Value::Object(new)],
// 局部模板使用 locals,另一分支使用 names;不能把两类索引混用。
if local {
CodeLayout {
locals: vec!["value".into()],
@@ -94,6 +119,7 @@ fn opt_in_assignment_keeps_alias_identity_ordinary_values_and_root_accounting()
}
},
);
// 执行结果拥有根,读取完成后必须单独归还,不能当作纯借用复制。
let value = vm.execute(code, globals).unwrap();
assert_eq!(value, Value::Object(old));
assert_eq!(
@@ -107,8 +133,11 @@ fn opt_in_assignment_keeps_alias_identity_ordinary_values_and_root_accounting()
.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();
@@ -118,13 +147,19 @@ fn opt_in_assignment_keeps_alias_identity_ordinary_values_and_root_accounting()
}
#[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,
&[
@@ -151,7 +186,9 @@ fn failed_opt_in_assignment_preserves_target_and_cleans_execution_roots() {
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();
@@ -160,28 +197,38 @@ fn failed_opt_in_assignment_preserves_target_and_cleans_execution_roots() {
}
#[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],
@@ -195,7 +242,8 @@ fn malformed_wordcode_and_unaligned_jumps_are_rejected_at_construction() {
Err(VmError::InvalidBytecode { .. })
));
}
// Jump target 2 is inside a prefixed LOAD_CONST starting at code unit 1.
// 跳转目标 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!(
@@ -209,7 +257,9 @@ fn malformed_wordcode_and_unaligned_jumps_are_rejected_at_construction() {
}
#[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)],
@@ -225,12 +275,14 @@ fn verifier_rejects_underflow_fallthrough_bad_cells_and_inconsistent_stack_joins
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()],
@@ -247,10 +299,14 @@ fn verifier_rejects_underflow_fallthrough_bad_cells_and_inconsistent_stack_joins
}
#[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,
&[
@@ -274,20 +330,25 @@ fn execute_moves_values_between_constants_local_slots_and_globals() {
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))
@@ -295,13 +356,19 @@ impl ObjectPayload for Tick {
}
#[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,
&[