docs(core): explain bytecode validation and basic execution regressions
This commit is contained in:
@@ -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,
|
||||
&[
|
||||
|
||||
Reference in New Issue
Block a user