mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
1262 lines
57 KiB
Rust
1262 lines
57 KiB
Rust
//! vm_support 内联测试:全量重置状态清除与全局重建、分配上限、生成器与 BigInt 存活、动态编译及源文本断言行为。
|
||
use super::*;
|
||
|
||
use oxide_compiler::DefaultCompilerService;
|
||
|
||
fn global_prop(vm: &Vm, name: &str) -> JsValue {
|
||
global_prop_opt(vm, name).expect("global slot should exist")
|
||
}
|
||
|
||
fn global_prop_opt(vm: &Vm, name: &str) -> Option<JsValue> {
|
||
let session = vm.realm.session.borrow();
|
||
let global = session.global_object();
|
||
let si = vm.kernel_core.perm_interner().intern(name).0;
|
||
vm.kernel_core
|
||
.shape_forge()
|
||
.lookup_position(global.shape_id(), si)
|
||
.map(|pos| global.get_prop_at(pos))
|
||
}
|
||
|
||
fn run_source(vm: &mut Vm, source: &str) -> JsValue {
|
||
let allocator = oxide_parser::Allocator::default();
|
||
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
|
||
let module = oxide_compiler::compiler::Compiler::new()
|
||
.compile(&program)
|
||
.expect("compile failed");
|
||
vm.run(&Arc::new(module)).expect("vm run failed")
|
||
}
|
||
|
||
/// 计数往返:两条构造路径(独立核/共享核)的登记与 Drop 注销恰好配对,
|
||
/// 全部 drop 后计数归零,kernel 可干净 drop(Drop 断言无残留)。
|
||
#[test]
|
||
fn active_vms_count_roundtrip() {
|
||
let vm = Vm::new();
|
||
assert_eq!(vm.kernel_core.active_vms(), 1);
|
||
drop(vm);
|
||
|
||
let core = KernelCore::new(KernelConfig::minimal());
|
||
assert_eq!(core.active_vms(), 0);
|
||
let v1 = Vm::with_kernel_core(Arc::clone(&core));
|
||
let v2 = Vm::with_kernel_core(Arc::clone(&core));
|
||
let v3 = Vm::with_kernel_core(Arc::clone(&core));
|
||
assert_eq!(core.active_vms(), 3);
|
||
drop(v3);
|
||
assert_eq!(core.active_vms(), 2);
|
||
drop(v1);
|
||
drop(v2);
|
||
assert_eq!(core.active_vms(), 0);
|
||
drop(core);
|
||
}
|
||
|
||
/// 守卫判别:持活 VM 调 sweep 触发 debug_assert。VM 声明在 kernel 之后,
|
||
/// panic unwind 时先注销计数再 drop kernel,drop 断言不受干扰。
|
||
#[test]
|
||
#[cfg_attr(debug_assertions, should_panic(expected = "no live VMs"))]
|
||
fn sweep_runner_forges_rejects_live_vm() {
|
||
let core = KernelCore::new(KernelConfig::minimal());
|
||
let _vm = Vm::with_kernel_core(Arc::clone(&core));
|
||
core.sweep_runner_forges();
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_with_session_objects_forces_global_rebuild() {
|
||
let mut vm = Vm::new();
|
||
// 池路径场景:`globalThis.Array = {}` 覆盖既有 global 槽——写屏障直通令
|
||
// 新值 `{}` 留存 epoch,global 持 epoch 子引用;global 保留时该指针将悬垂。
|
||
// 覆盖写推进 global generation,快照对比发现脏 global,full_reset 必须重建。
|
||
let _ = run_source(&mut vm, "globalThis.Array = {}; 0");
|
||
let written = global_prop(&vm, "Array");
|
||
// SAFETY: 覆盖值是本 VM 自有的 session 对象,本 session 内指针有效。
|
||
assert!(
|
||
unsafe { (&*written.as_js_object_ptr()).is_session_epoch() },
|
||
"覆盖值应为 session 对象(统一入口 Box 化)"
|
||
);
|
||
let old_global = vm.realm.session.borrow().global_object.as_ptr();
|
||
|
||
vm.full_reset();
|
||
|
||
// global 必须重建:旧 global 与其 session 子引用随 session 释放,Array 恢复内置构造器。
|
||
assert!(!std::ptr::eq(old_global, vm.realm.session.borrow().global_object.as_ptr()));
|
||
assert!(std::ptr::eq(
|
||
global_prop(&vm, "Array").as_js_object_ptr(),
|
||
vm.realm.session.borrow().builtin_world().array_constructor.as_ptr() as *mut JsObject
|
||
));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// P 原型上方法槽的 wrapper 对象原始指针(rebuild 跨轮复用验证用)。
|
||
fn method_wrapper_ptr(vm: &Vm, proto_ptr: *const JsObject, name: &str) -> *const JsObject {
|
||
let si = vm.kernel_core.perm_interner().intern(name).0;
|
||
// SAFETY: proto_ptr 是本 session 的 P 原型,安全点内无并发读者。
|
||
let proto = unsafe { &*proto_ptr };
|
||
let pos = vm
|
||
.kernel_core
|
||
.shape_forge()
|
||
.lookup_position(proto.shape_id(), si)
|
||
.unwrap_or_else(|| panic!("{name} 槽位应存在"));
|
||
let val = proto.get_prop_at(pos);
|
||
assert!(val.is_object(), "{name} 槽位应为 wrapper 对象: {val:?}");
|
||
val.as_js_object_ptr()
|
||
}
|
||
|
||
/// global 属性槽数(槽位原位更新跨轮不追加验证用)。
|
||
fn global_slot_count(vm: &Vm) -> usize {
|
||
let g = vm.realm.session.borrow().global_object.as_ptr() as *mut JsObject;
|
||
// SAFETY: global 是本 session 对象,安全点内无并发读者。
|
||
unsafe { (*g).prop_vec_len() }
|
||
}
|
||
|
||
/// 选择性重建 wrapper 复用与 global 槽原位更新:每轮「原型脏写 + full_reset」
|
||
/// 后存活方法 wrapper 应为同一对象(同 raw 指针、同 shape),释放表计数与
|
||
/// global 属性槽数跨轮不增长,重建后方法行为正确。
|
||
#[test]
|
||
fn full_reset_rebuild_reuses_method_wrappers_across_rounds() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let push_before = method_wrapper_ptr(&vm, vm.realm.session.borrow().builtin_world().array_proto.as_ptr(), "push");
|
||
let push_shape_before = unsafe { &*push_before }.shape_id();
|
||
let registry_before = vm.realm.session.borrow().builtin_world().leaked_object_count();
|
||
let slots_before = global_slot_count(&vm);
|
||
|
||
for i in 0..3u32 {
|
||
// 新键写才 bump 原型世代;四家族逐轮全脏,function 家族重建同时
|
||
// 覆盖 wrapper proto 槽重指路径。
|
||
let source = format!(
|
||
"Object.prototype['w{i}'] = 1; Array.prototype['w{i}'] = 2; String.prototype['w{i}'] = 3; \
|
||
Function.prototype['w{i}'] = 4; 0"
|
||
);
|
||
let _ = run_source(&mut vm, &source);
|
||
vm.full_reset();
|
||
}
|
||
|
||
let push_after = method_wrapper_ptr(&vm, vm.realm.session.borrow().builtin_world().array_proto.as_ptr(), "push");
|
||
assert!(std::ptr::eq(push_before, push_after), "存活方法 wrapper 应复用(同 raw 指针)");
|
||
assert_eq!(push_shape_before, unsafe { &*push_after }.shape_id(), "wrapper shape 应稳定");
|
||
let registry_after = vm.realm.session.borrow().builtin_world().leaked_object_count();
|
||
assert!(
|
||
registry_after <= registry_before,
|
||
"释放表计数跨轮不应增长: {registry_before} -> {registry_after}"
|
||
);
|
||
let slots_after = global_slot_count(&vm);
|
||
assert!(slots_after <= slots_before, "global 槽数跨轮不应增长: {slots_before} -> {slots_after}");
|
||
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
|
||
assert_eq!(run_source(&mut vm, "String.prototype.charCodeAt.call('A', 0)"), JsValue::int(65));
|
||
}
|
||
|
||
/// global 槽位原位更新锚点:错误/资源栈家族脏重建(子类型构造器经 Box 自建
|
||
/// 路径)多轮后 global 属性槽数不增长、Error 槽指向新构造器。
|
||
#[test]
|
||
fn full_reset_rebuild_keeps_global_slot_count_flat() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let slots_before = global_slot_count(&vm);
|
||
|
||
for i in 0..3u32 {
|
||
// 新键写脏错误家族(子类型原型重建触发构造器 Box 路径)与对象家族。
|
||
let source =
|
||
format!("Error.prototype['e{i}'] = 1; TypeError.prototype['e{i}'] = 2; Object.prototype['e{i}'] = 3; 0");
|
||
let _ = run_source(&mut vm, &source);
|
||
vm.full_reset();
|
||
}
|
||
|
||
let slots_after = global_slot_count(&vm);
|
||
assert!(slots_after <= slots_before, "global 槽数跨轮不应增长: {slots_before} -> {slots_after}");
|
||
// Error 槽应指向本轮重建的构造器(非滞留旧指针)。
|
||
assert!(std::ptr::eq(
|
||
global_prop(&vm, "Error").as_js_object_ptr(),
|
||
vm.realm.session.borrow().builtin_world().error_constructor.as_ptr() as *mut JsObject
|
||
));
|
||
assert!(global_prop(&vm, "TypeError").is_object());
|
||
assert_eq!(run_source(&mut vm, "new TypeError('x') instanceof TypeError"), JsValue::bool(true));
|
||
}
|
||
|
||
fn vm_with_low_threshold() -> Vm {
|
||
let mut cfg = KernelConfig::minimal();
|
||
cfg.set_session_gc_threshold(1);
|
||
Vm::with_kernel_core(KernelCore::new(cfg))
|
||
}
|
||
|
||
/// 单 run 分配上限拦截失控分配:死循环持续 push 的 run 须在触达步数上限前
|
||
/// 以 memory limit 错误终止。
|
||
#[test]
|
||
fn run_alloc_cap_stops_runaway_allocation() {
|
||
let mut cfg = KernelConfig::minimal();
|
||
cfg.max_alloc_bytes = Some(256 * 1024);
|
||
let mut vm = Vm::with_kernel_core(KernelCore::new(cfg));
|
||
let allocator = oxide_parser::Allocator::default();
|
||
let program = oxide_parser::parse(&allocator, "var a = []; while (true) { a.push(1); }").expect("parse failed");
|
||
let module = oxide_compiler::compiler::Compiler::new()
|
||
.compile(&program)
|
||
.expect("compile failed");
|
||
let err = vm
|
||
.run(&Arc::new(module))
|
||
.expect_err("runaway allocation must hit the alloc cap");
|
||
assert!(err.contains("memory limit"), "unexpected error: {err}");
|
||
assert!(!err.contains("step limit"), "cap 应先于步数上限生效: {err}");
|
||
}
|
||
|
||
/// 上限不误伤正常规模分配:同配置下 1 万元素数组构造正常完成。
|
||
#[test]
|
||
fn run_alloc_cap_allows_normal_allocation() {
|
||
let mut cfg = KernelConfig::minimal();
|
||
cfg.max_alloc_bytes = Some(4 * 1024 * 1024);
|
||
let mut vm = Vm::with_kernel_core(KernelCore::new(cfg));
|
||
let result = run_source(&mut vm, "var a = []; for (var i = 0; i < 10000; i++) { a.push(i); } a.length");
|
||
assert!(result.is_int(), "expected int length, got {result:?}");
|
||
assert_eq!(result.as_int(), 10000);
|
||
}
|
||
|
||
/// native 终端循环泵送小 JS 重入:每次重入 dispatch 远短于循环内 64 指令采样点,
|
||
/// 顶层 steps 不推进——重入边界每 64 hop 强制采样兜底,失控分配以 memory limit
|
||
/// 终止而非无限循环。
|
||
#[test]
|
||
fn reentry_pump_hits_alloc_cap() {
|
||
let mut cfg = KernelConfig::minimal();
|
||
cfg.max_alloc_bytes = Some(256 * 1024);
|
||
let mut vm = Vm::with_kernel_core(KernelCore::new(cfg));
|
||
// 永不 done 的生成器经鸭子对象交给 toArray:native 循环每步泵送两个短重入
|
||
// (闭包调用 + 生成器恢复)并新产一个结果对象,分配无界增长。
|
||
let source = "var g = (function* () { for (var i = 0; ; ++i) { yield i; } })(); \
|
||
var obj = { next: function () { return g.next(); } }; \
|
||
Iterator.from(obj).toArray();";
|
||
let allocator = oxide_parser::Allocator::default();
|
||
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
|
||
let module = oxide_compiler::compiler::Compiler::new()
|
||
.compile(&program)
|
||
.expect("compile failed");
|
||
let err = vm.run(&Arc::new(module)).expect_err("reentry pump must hit the alloc cap");
|
||
assert!(err.contains("memory limit"), "unexpected error: {err}");
|
||
}
|
||
|
||
/// 直接恢复生成器一步:等价 `it.next()`——回归聚焦 GC 收集后的状态盒
|
||
/// 有效性(原地 sweep 不搬移对象;同 run 内恢复,不经 run 边界换表)。
|
||
fn resume_one_step(vm: &mut Vm, gen: JsValue) -> JsValue {
|
||
match vm.resume_generator(gen, crate::generator::GeneratorResumeMode::Next(JsValue::undefined())) {
|
||
Ok(crate::generator::GeneratorStep::Suspended { value }) => value,
|
||
Ok(crate::generator::GeneratorStep::Completed { value }) => value,
|
||
Ok(other) => panic!(
|
||
"unexpected step: {:?}",
|
||
match other {
|
||
crate::generator::GeneratorStep::Thrown { value } => format!("Thrown({value})"),
|
||
crate::generator::GeneratorStep::SuspendedRaw { value } => format!("SuspendedRaw({value})"),
|
||
_ => String::new(),
|
||
}
|
||
),
|
||
Err(e) => panic!("resume failed: {e}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn generator_survives_object_sweep_and_resumes() {
|
||
let mut vm = vm_with_low_threshold();
|
||
let _ = run_source(&mut vm, "function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0");
|
||
|
||
// 直接触发完整收集(保留执行上下文):存活生成器原地保留、
|
||
// 状态盒不动(走收集入口而非 run 边界:聚焦原地收集本身)。
|
||
vm.maybe_collect_session_gc();
|
||
assert!(vm.session_gc_stats().total_collections > 0, "应触发对象收集");
|
||
|
||
// 从 global 取 sweep 后存活的生成器(同 run,模块表未换发,可恢复)。
|
||
let it = global_prop(&vm, "it");
|
||
assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复第二次 yield");
|
||
}
|
||
|
||
#[test]
|
||
fn generator_captured_upvalue_survives_sweep() {
|
||
let mut vm = vm_with_low_threshold();
|
||
// x 函数作用域局部:被生成器 g 经 cell 捕获(顶层 var 直连全局属性不走 cell)。
|
||
let _ = run_source(
|
||
&mut vm,
|
||
"function outer() { var x = 0; function* g(){ x++; yield x; x++; yield x; } globalThis.it = g(); globalThis.it.next(); return 0; } outer(); 0",
|
||
);
|
||
|
||
vm.maybe_collect_session_gc();
|
||
assert!(vm.session_gc_stats().total_collections > 0, "应触发对象收集");
|
||
|
||
// 挂起帧 cell_stack 与闭包 upvalues 中的 cell 独立堆分配(地址稳定),
|
||
// 恢复后继续读写捕获变量。
|
||
let it = global_prop(&vm, "it");
|
||
assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复捕获变量读写");
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_clean_keeps_session_objects() {
|
||
let mut vm = Vm::new();
|
||
let world_ptr = Arc::as_ptr(&vm.realm.session.borrow().builtin_world);
|
||
let global_ptr = vm.realm.session.borrow().global_object.as_ptr();
|
||
let object_proto_ptr = vm.realm.session.borrow().builtin_world().object_proto.as_ptr();
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.realm.session.borrow().builtin_world)));
|
||
assert!(std::ptr::eq(global_ptr, vm.realm.session.borrow().global_object.as_ptr()));
|
||
assert!(std::ptr::eq(
|
||
object_proto_ptr,
|
||
vm.realm.session.borrow().builtin_world().object_proto.as_ptr()
|
||
));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_global_dirty_rebuilds_global_and_restores_slots() {
|
||
let mut vm = Vm::new();
|
||
let world_ptr = Arc::as_ptr(&vm.realm.session.borrow().builtin_world);
|
||
let global_ptr = vm.realm.session.borrow().global_object.as_ptr();
|
||
let global = unsafe { &mut *(vm.realm.session.borrow().global_object.as_ptr() as *mut JsObject) };
|
||
bindings::bind_global_value(&vm.kernel_core, global, "userGlobal", JsValue::int(99));
|
||
unsafe { &mut *(vm.realm.session.borrow().global_object.as_ptr() as *mut JsObject) }.bump_generation();
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.realm.session.borrow().builtin_world)));
|
||
assert!(!std::ptr::eq(global_ptr, vm.realm.session.borrow().global_object.as_ptr()));
|
||
assert!(global_prop_opt(&vm, "userGlobal").is_none());
|
||
assert!(std::ptr::eq(
|
||
global_prop(&vm, "Array").as_js_object_ptr(),
|
||
vm.realm.session.borrow().builtin_world().array_constructor.as_ptr() as *mut JsObject
|
||
));
|
||
assert!(std::ptr::eq(
|
||
global_prop(&vm, "globalThis").as_js_object_ptr(),
|
||
vm.realm.session.borrow().global_object.as_ptr() as *mut JsObject
|
||
));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_dirty_builtin_rebinds_global_slot() {
|
||
let mut vm = Vm::new();
|
||
let old_object_proto = vm.realm.session.borrow().builtin_world().object_proto.as_ptr();
|
||
let old_array_proto = vm.realm.session.borrow().builtin_world().array_proto.as_ptr();
|
||
unsafe { &mut *(old_array_proto as *mut JsObject) }.bump_generation();
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(std::ptr::eq(
|
||
old_object_proto,
|
||
vm.realm.session.borrow().builtin_world().object_proto.as_ptr()
|
||
));
|
||
assert!(!std::ptr::eq(
|
||
old_array_proto,
|
||
vm.realm.session.borrow().builtin_world().array_proto.as_ptr()
|
||
));
|
||
assert!(std::ptr::eq(
|
||
global_prop(&vm, "Array").as_js_object_ptr(),
|
||
vm.realm.session.borrow().builtin_world().array_constructor.as_ptr() as *mut JsObject
|
||
));
|
||
let constructor_si = vm.kernel_core.perm_interner().intern("constructor").0;
|
||
let session = vm.realm.session.borrow();
|
||
let array_proto = &*session.builtin_world().array_proto;
|
||
let constructor = vm
|
||
.resolve_property(array_proto, constructor_si)
|
||
.expect("Array.prototype.constructor");
|
||
assert!(std::ptr::eq(
|
||
constructor.as_js_object_ptr(),
|
||
vm.realm.session.borrow().builtin_world().array_constructor.as_ptr() as *mut JsObject
|
||
));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_dirty_function_keeps_call_working() {
|
||
let mut vm = Vm::new();
|
||
let function_proto = vm.realm.session.borrow().builtin_world().function_proto.as_ptr();
|
||
unsafe { &mut *(function_proto as *mut JsObject) }.bump_generation();
|
||
|
||
vm.full_reset();
|
||
|
||
// Function 原型家族重建后,未重建家族(array/string/object/map 等)的方法
|
||
// wrapper 是跨重置存活对象,其原型链必须仍能解析出 call/apply/bind:
|
||
// 任何一条路径失效都说明 wrapper 原型指向了已释放的旧 Function 原型。
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push.call([1], 2)"), JsValue::int(2));
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push.apply([], [1, 2, 3])"), JsValue::int(3));
|
||
let replaced = run_source(&mut vm, "String.prototype.replace.call('a', 'a', 'b')");
|
||
assert_eq!(vm.lookup_str(replaced).as_deref(), Some("b"));
|
||
let has = run_source(&mut vm, "Object.prototype.hasOwnProperty.call({x: 1}, 'x')");
|
||
assert_eq!(has, JsValue::bool(true));
|
||
let fixed = run_source(&mut vm, "Number.prototype.toFixed.call(1.5, 1)");
|
||
assert_eq!(vm.lookup_str(fixed).as_deref(), Some("1.5"));
|
||
let in_map = run_source(&mut vm, "Map.prototype.has.call(new Map([[1, 2]]), 1)");
|
||
assert_eq!(in_map, JsValue::bool(true));
|
||
let mapped = run_source(&mut vm, "Array.prototype.map.call([1, 2], function(x){ return x + 1; }).join(',')");
|
||
assert_eq!(vm.lookup_str(mapped).as_deref(), Some("2,3"));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// Function 家族脏重建:未重建家族的方法 wrapper 是跨重置存活对象,其 proto
|
||
/// 槽(绑定时固化为旧 fn_proto)必须被重指到新 fn_proto,旧原型链不可再读。
|
||
#[test]
|
||
fn full_reset_dirty_function_repoints_retained_wrapper_proto() {
|
||
let mut vm = Vm::new();
|
||
let old_fn_proto = vm.realm.session.borrow().builtin_world().function_proto.as_ptr() as *mut JsObject;
|
||
// 保留方法 wrapper:array 家族不重建,wrapper 对象与其 proto 槽跨重置存活。
|
||
let array_proto = vm.realm.session.borrow().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||
let push_si = vm.kernel_core.perm_interner().intern("push").0;
|
||
let push = vm
|
||
.resolve_property(unsafe { &*array_proto }, push_si)
|
||
.expect("Array.prototype.push");
|
||
let push_ptr = push.as_js_object_ptr();
|
||
assert!(std::ptr::eq(unsafe { (*push_ptr).proto().as_js_object_ptr() }, old_fn_proto));
|
||
|
||
unsafe { &mut *old_fn_proto }.bump_generation();
|
||
|
||
vm.full_reset();
|
||
|
||
let new_fn_proto = vm.realm.session.borrow().builtin_world().function_proto.as_ptr() as *mut JsObject;
|
||
assert!(!std::ptr::eq(new_fn_proto, old_fn_proto));
|
||
// 保留 wrapper proto 槽已重指新 fn_proto,call 链走新原型。
|
||
assert!(std::ptr::eq(unsafe { (*push_ptr).proto().as_js_object_ptr() }, new_fn_proto));
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push.call([1], 2)"), JsValue::int(2));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// 新键写脏 object/array/string/function 四家族(S2 脏源形态):full_reset 后
|
||
/// 跨家族读语义保持——保留 wrapper 原型链经重指后取 call、同家族
|
||
/// constructor/prototype 对自洽、保留原型链指向新 Object.prototype。
|
||
#[test]
|
||
fn full_reset_dirty_four_families_cross_family_reads() {
|
||
let mut vm = Vm::new();
|
||
run_source(
|
||
&mut vm,
|
||
"Object.prototype['d'] = 1; Array.prototype['d'] = 2; String.prototype['d'] = 3; Function.prototype['d'] = 4;",
|
||
);
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
|
||
vm.full_reset();
|
||
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push.call([1], 2)"), JsValue::int(2));
|
||
let replaced = run_source(&mut vm, "String.prototype.replace.call('a', 'a', 'b')");
|
||
assert_eq!(vm.lookup_str(replaced).as_deref(), Some("b"));
|
||
let has = run_source(&mut vm, "Object.prototype.hasOwnProperty.call({x: 1}, 'x')");
|
||
assert_eq!(has, JsValue::bool(true));
|
||
assert_eq!(
|
||
run_source(&mut vm, "Object.getPrototypeOf(Array.prototype) === Object.prototype"),
|
||
JsValue::bool(true)
|
||
);
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.constructor === Array"), JsValue::bool(true));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// 未捕获异常侧通道是执行期状态:原生错误后可能残留原值,reset/full_reset
|
||
/// 边界须随执行状态一并清空——否则其持有的 epoch 对象指针在池回收后悬垂
|
||
/// (后续原生错误经 raise_call_error 消费残留值即 UAF)。
|
||
#[test]
|
||
fn reset_drops_stale_uncaught_value() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
vm.last_uncaught_value = Some(JsValue::float(42.0));
|
||
vm.reset();
|
||
assert!(vm.last_uncaught_value.is_none(), "reset 应清空未捕获异常侧通道");
|
||
|
||
vm.last_uncaught_value = Some(JsValue::float(42.0));
|
||
vm.full_reset();
|
||
assert!(vm.last_uncaught_value.is_none(), "full_reset 应清空未捕获异常侧通道");
|
||
}
|
||
|
||
/// 直 session 分配站(函数对象 + prototype 子对象)计入 session 堆账目:
|
||
/// 分配点计数 = 对象头(属性区容量扩张属 promote 同口径的既有盲区,不钉)。
|
||
/// 另含 CREATE_CLOSURE 站将推断函数名物化为 session 串(name 数据属性,
|
||
/// 既有行为),钉值一并计入。
|
||
#[test]
|
||
fn direct_session_alloc_counted_in_session_bytes() {
|
||
let mut vm = Vm::new();
|
||
let base = vm.session_bytes_allocated();
|
||
run_source(&mut vm, "var f = function(){}; 0");
|
||
let delta = vm.session_bytes_allocated() - base;
|
||
let name_len = "f".len();
|
||
assert_eq!(delta, 2 * std::mem::size_of::<JsObject>() + std::mem::size_of::<JsString>() + name_len);
|
||
}
|
||
|
||
/// 标签模板 cooked/raw 数组直 session 分配计入 session 堆账目:
|
||
/// 含模板的 run 账目增量须超出同等函数对象口径(两数组各含对象头 + 元素区)。
|
||
/// 单 run 内完成(标签函数定义 + 模板调用):账目口径与函数对象钉同 run 对齐。
|
||
#[test]
|
||
fn tagged_template_object_counted_in_session_bytes() {
|
||
let mut vm = Vm::new();
|
||
let base = vm.session_bytes_allocated();
|
||
// 对照 run:仅函数对象(本体 + prototype 子对象)。
|
||
run_source(&mut vm, "var tag = function(){ return 0; }; 0");
|
||
let fn_cost = vm.session_bytes_allocated() - base;
|
||
// 实验 run:同函数 + 标签模板(cookeD/raw 两数组)。
|
||
let before = vm.session_bytes_allocated();
|
||
run_source(&mut vm, "var tag2 = function(){ return 0; }; tag2`a${1}b`; 0");
|
||
let both_cost = vm.session_bytes_allocated() - before;
|
||
assert!(
|
||
both_cost > fn_cost + 2 * std::mem::size_of::<JsObject>(),
|
||
"模板数组未计入账目: both={both_cost} fn={fn_cost}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn immutables_filled_once_per_module() {
|
||
let mut vm = Vm::new();
|
||
// `f` 递归(同一子模块进入 4 次),其不可变常量经 OnceLock 只转换一次。
|
||
let result = run_source(&mut vm, "function f(n){ if(n<=0){ return 'done'; } return f(n-1); } f(3)");
|
||
assert!(result.is_string());
|
||
assert_eq!(vm.lookup_str(result).as_deref(), Some("done"));
|
||
// 缓存 = 顶层模块 + 1 个子模块(f);子模块槽由这些调用初始化。
|
||
assert_eq!(vm.current_table().immutables.len(), 2);
|
||
// 子模块常量改走 temp_immutables(避免缓存下标冲突)
|
||
}
|
||
|
||
/// 模块级装载缓存:同模块跨 run(无 full_reset)常量转换只发生一次——
|
||
/// 常量 BigInt 入条目自持存储、不入 session 表,session 表长不随 run 数增长,
|
||
/// 缓存条目不重复创建。
|
||
#[test]
|
||
fn module_cache_converts_constants_once_across_runs() {
|
||
let mut vm = Vm::new();
|
||
let module = compile_module("var x = 10n; x");
|
||
vm.run(&module).expect("run1");
|
||
// 常量 BigInt 走条目自持存储,session 表不留痕。
|
||
assert!(
|
||
vm.realm.gc.borrow().session_bigint_ptrs.borrow().is_empty(),
|
||
"常量 BigInt 不得登记 session 表"
|
||
);
|
||
vm.run(&module).expect("run2");
|
||
// 第二次 run 命中缓存:不重转换、不重登记,表长不增、条目不增。
|
||
assert!(
|
||
vm.realm.gc.borrow().session_bigint_ptrs.borrow().is_empty(),
|
||
"同模块跨 run 不得重复登记常量 BigInt 入 session 表"
|
||
);
|
||
assert_eq!(vm.module_cache.len(), 1, "同模块跨 run 不得新建缓存条目");
|
||
}
|
||
|
||
/// 模块级装载缓存跨 full_reset 存活:条目自持 BigInt 存储不在
|
||
/// session_bigint_ptrs 内,session 拆除后指针不悬垂,常量值仍可读。
|
||
#[test]
|
||
fn module_cache_bigint_readable_after_full_reset() {
|
||
let mut vm = Vm::new();
|
||
let module = compile_module("var x = 10n; x");
|
||
let result = vm.run(&module).expect("run");
|
||
assert!(result.is_bigint());
|
||
vm.full_reset();
|
||
// full_reset 不清装载缓存:条目 BigInt 存储归条目自持,拆除不悬垂。
|
||
let entry = vm.module_cache.get(&Arc::as_ptr(&module)).expect("full_reset 不得清装载缓存");
|
||
let v = entry
|
||
.immutables
|
||
.iter()
|
||
.find(|v| v.is_bigint())
|
||
.copied()
|
||
.expect("常量池含 BigInt 常量");
|
||
assert_eq!(vm.bigint_value(v), &num_bigint::BigInt::from(10));
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_basic_arity() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
// 与等价静态函数返回值逐位一致(引擎函数调用统一产 Double 数值)。
|
||
let expected = run_source(&mut vm, "function f(a,b){return a+b} f(3,4)");
|
||
let result = run_source(&mut vm, "new Function('a','b','return a+b')(3,4)");
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_called_without_new() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
let expected = run_source(&mut vm, "function f(a,b){return a*b} f(6,7)");
|
||
let result = run_source(&mut vm, "Function('a','b','return a*b')(6,7)");
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_empty_body_returns_undefined() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
assert!(run_source(&mut vm, "Function()()").is_undefined());
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_syntax_error_throws_syntax_error() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
let result = run_source(&mut vm, "try{new Function('return {{')}catch(e){e.name}");
|
||
assert!(result.is_string());
|
||
assert_eq!(vm.lookup_str(result).as_deref(), Some("SyntaxError"));
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_nested_closure_renumbering() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
// 匿名函数体声明嵌套函数 g,返回值是引用 g 的闭包:验证子树 flat_id 重编号
|
||
// 与 CREATE_CLOSURE imm16 重写后嵌套调用仍指向正确的子模块。
|
||
let expected = run_source(
|
||
&mut vm,
|
||
"function outer(){var g=function(n){return n*2}; return function(){return g(21)}} outer()()",
|
||
);
|
||
let result = run_source(
|
||
&mut vm,
|
||
"new Function('var g=function(n){return n*2}; return function(){return g(21)}')()()",
|
||
);
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_multiple_in_one_run() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
// 同一 run 内连续创建多个动态函数:验证 base 偏移累计正确。
|
||
let expected = run_source(
|
||
&mut vm,
|
||
"function f1(){return 1} function f2(){return 2} function f3(a){return a*3} f1()+f2()+f3(4)",
|
||
);
|
||
let result = run_source(
|
||
&mut vm,
|
||
"new Function('return 1')() + new Function('return 2')() + new Function('a','return a*3')(4)",
|
||
);
|
||
assert_eq!(result, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_name_and_length() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
let name = run_source(&mut vm, "var f=new Function('a','b','return a'); f.name");
|
||
assert!(name.is_string());
|
||
assert_eq!(vm.lookup_str(name).as_deref(), Some("anonymous"));
|
||
let len = run_source(&mut vm, "var f=new Function('a','b','return a'); f.length");
|
||
assert_eq!(len, JsValue::int(2));
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_comma_split_params_count() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
// 单个实参 "a,b,c" 拼接解析为 3 个形参,length 应为解析后的形参数。
|
||
let result = run_source(&mut vm, "new Function('a,b,c','null').length");
|
||
assert_eq!(result, JsValue::int(3));
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_name_and_length_attributes() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
// length/name 为不可写、不可枚举、可配置的数据属性。
|
||
let attrs = run_source(
|
||
&mut vm,
|
||
"var d=Object.getOwnPropertyDescriptor(new Function('a','return a'),'length'); String(d.value)+d.writable+d.enumerable+d.configurable",
|
||
);
|
||
assert_eq!(vm.lookup_str(attrs).as_deref(), Some("1falsefalsetrue"));
|
||
let name_attrs = run_source(
|
||
&mut vm,
|
||
"var d=Object.getOwnPropertyDescriptor(Function(),'name'); String(d.writable)+d.enumerable+d.configurable",
|
||
);
|
||
assert_eq!(vm.lookup_str(name_attrs).as_deref(), Some("falsefalsetrue"));
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_function_rethrows_to_string_exception() {
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
// 形参 ToString 回调抛出的原始值须原样传播,而非包成 TypeError。
|
||
let result = run_source(&mut vm, "try{new Function({toString:function(){throw 7}})}catch(e){e}");
|
||
assert_eq!(result, JsValue::int(7));
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_refreshes_object_prototype_after_object_dirty() {
|
||
let mut vm = Vm::new();
|
||
let old_object_proto = vm.realm.session.borrow().builtin_world().object_proto.as_ptr();
|
||
unsafe { &mut *(old_object_proto as *mut JsObject) }.bump_generation();
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(!std::ptr::eq(
|
||
old_object_proto,
|
||
vm.realm.session.borrow().builtin_world().object_proto.as_ptr()
|
||
));
|
||
assert!(std::ptr::eq(
|
||
vm.realm.object_prototype.borrow().as_ptr(),
|
||
vm.realm.session.borrow().builtin_world().object_proto.as_ptr()
|
||
));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_object_dirty_rebinds_iterator_family() {
|
||
let mut vm = Vm::new();
|
||
let old_object_proto = vm.realm.session.borrow().builtin_world().object_proto.as_ptr();
|
||
let old_iterator_proto = vm.realm.session.borrow().builtin_world().iterator_proto.as_ptr();
|
||
// 用户修改 Object.prototype:object 家族世代递增,global 未动。
|
||
unsafe { &mut *(old_object_proto as *mut JsObject) }.bump_generation();
|
||
|
||
vm.full_reset();
|
||
|
||
// object 家族与迭代器原型全部重建(新原型链到新 Object.prototype)。
|
||
assert!(!std::ptr::eq(
|
||
old_object_proto,
|
||
vm.realm.session.borrow().builtin_world().object_proto.as_ptr()
|
||
));
|
||
assert!(!std::ptr::eq(
|
||
old_iterator_proto,
|
||
vm.realm.session.borrow().builtin_world().iterator_proto.as_ptr()
|
||
));
|
||
// global 保留(dirty.global=false):其 Iterator 函数对象的 prototype
|
||
// 属性须对齐到重建后的 %IteratorPrototype%。
|
||
let iter_val = global_prop(&vm, "Iterator");
|
||
let si_prototype = vm.kernel_core.perm_interner().intern("prototype").0;
|
||
let iter_obj = unsafe { &*iter_val.as_js_object_ptr() };
|
||
let proto_pos = vm
|
||
.kernel_core
|
||
.shape_forge()
|
||
.lookup_position(iter_obj.shape_id(), si_prototype)
|
||
.expect("Iterator should have prototype slot");
|
||
assert!(std::ptr::eq(
|
||
iter_obj.get_prop_at(proto_pos).as_js_object_ptr(),
|
||
vm.realm.session.borrow().builtin_world().iterator_proto.as_ptr() as *mut JsObject
|
||
));
|
||
// full_reset 后 session 干净;此后 run_source 执行才重新累积世代变化。
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
// 迭代器家族功能完整:原型 next 就位,for-of/spread/Array.from/Iterator.from/
|
||
// Map/Set/String/yield* 全部可用,原型链与 Iterator.prototype 一致。
|
||
let r = run_source(&mut vm, "[...[1,2,3]].join(',')");
|
||
assert_eq!(vm.lookup_str(r).as_deref(), Some("1,2,3"));
|
||
let r = run_source(&mut vm, "Array.from([1,2]).join(',')");
|
||
assert_eq!(vm.lookup_str(r).as_deref(), Some("1,2"));
|
||
let r = run_source(&mut vm, "Iterator.from({next:function(){return {value:42,done:false}}}).next().value");
|
||
assert_eq!(r, JsValue::int(42));
|
||
let r = run_source(&mut vm, "[...new Map([[1,2],[3,4]])].map(function(x){return x.join(':')}).join(';')");
|
||
assert_eq!(vm.lookup_str(r).as_deref(), Some("1:2;3:4"));
|
||
let r = run_source(&mut vm, "[...new Set([1,2,3])].join(',')");
|
||
assert_eq!(vm.lookup_str(r).as_deref(), Some("1,2,3"));
|
||
let r = run_source(&mut vm, "[...'ab'].join(',')");
|
||
assert_eq!(vm.lookup_str(r).as_deref(), Some("a,b"));
|
||
let r = run_source(&mut vm, "function* g(){yield* [1,2]} [...g()].join(',')");
|
||
assert_eq!(vm.lookup_str(r).as_deref(), Some("1,2"));
|
||
let r = run_source(
|
||
&mut vm,
|
||
"Object.getPrototypeOf(Object.getPrototypeOf([].values())) === Iterator.prototype",
|
||
);
|
||
assert_eq!(r, JsValue::bool(true));
|
||
let r = run_source(&mut vm, "var it=[].values(); it[Symbol.iterator]()===it");
|
||
assert_eq!(r, JsValue::bool(true));
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_global_dirty_keeps_iterator_proto_slots_stable() {
|
||
let mut vm = Vm::new();
|
||
let arr_iter_proto = vm.realm.session.borrow().builtin_world().array_iterator_proto.as_ptr() as *mut JsObject;
|
||
let slots_before = unsafe { &*arr_iter_proto }.hash_props_vec().map_or(0, |v| v.len());
|
||
// global 世代递增(用户写 global),builtin 家族未动。
|
||
unsafe { &mut *(vm.realm.session.borrow().global_object.as_ptr() as *mut JsObject) }.bump_generation();
|
||
|
||
vm.full_reset();
|
||
|
||
// builtin 未脏 → 迭代器原型保留原对象且属性槽不膨胀(重复 full_reset 不再追加)。
|
||
let arr_iter_proto_after = vm.realm.session.borrow().builtin_world().array_iterator_proto.as_ptr() as *mut JsObject;
|
||
assert!(std::ptr::eq(arr_iter_proto, arr_iter_proto_after));
|
||
let slots_after = unsafe { &*arr_iter_proto_after }.hash_props_vec().map_or(0, |v| v.len());
|
||
assert_eq!(slots_before, slots_after);
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
// 迭代器功能经保留原型仍完整(run_source 起再次累积世代变化)。
|
||
let r = run_source(&mut vm, "[...[1,2,3]].join(',')");
|
||
assert_eq!(vm.lookup_str(r).as_deref(), Some("1,2,3"));
|
||
let r = run_source(&mut vm, "var it=new Set([1]).values(); it[Symbol.iterator]()===it");
|
||
assert_eq!(r, JsValue::bool(true));
|
||
}
|
||
|
||
#[test]
|
||
fn bigint_literal_arithmetic_and_comparison() {
|
||
let mut vm = Vm::new();
|
||
assert_eq!(run_source(&mut vm, "100n + 23n"), run_source(&mut vm, "123n"));
|
||
assert_eq!(run_source(&mut vm, "100n - 30n"), run_source(&mut vm, "70n"));
|
||
assert_eq!(run_source(&mut vm, "7n * 6n"), run_source(&mut vm, "42n"));
|
||
assert_eq!(run_source(&mut vm, "10n / 4n"), run_source(&mut vm, "2n"));
|
||
assert_eq!(run_source(&mut vm, "10n % 3n"), run_source(&mut vm, "1n"));
|
||
assert_eq!(run_source(&mut vm, "-7n"), run_source(&mut vm, "0n - 7n"));
|
||
assert_eq!(run_source(&mut vm, "123n == 123n"), JsValue::bool(true));
|
||
assert_eq!(run_source(&mut vm, "123n === 123n"), JsValue::bool(true));
|
||
assert_eq!(run_source(&mut vm, "5n < 3n"), JsValue::bool(false));
|
||
assert_eq!(run_source(&mut vm, "5n > 3n"), JsValue::bool(true));
|
||
assert_eq!(run_source(&mut vm, "1n === 1"), JsValue::bool(false));
|
||
let typeof_result = run_source(&mut vm, "typeof 123n");
|
||
assert!(typeof_result.is_string());
|
||
assert_eq!(vm.lookup_str(typeof_result).as_deref(), Some("bigint"));
|
||
}
|
||
|
||
#[test]
|
||
fn bigint_constructor_and_string() {
|
||
let mut vm = Vm::new();
|
||
assert_eq!(run_source(&mut vm, "BigInt(42)"), run_source(&mut vm, "42n"));
|
||
assert_eq!(run_source(&mut vm, "BigInt('123')"), run_source(&mut vm, "123n"));
|
||
assert_eq!(run_source(&mut vm, "BigInt('0x10')"), run_source(&mut vm, "16n"));
|
||
let s = run_source(&mut vm, "String(123n)");
|
||
assert!(s.is_string());
|
||
assert_eq!(vm.lookup_str(s).as_deref(), Some("123"));
|
||
let ts = run_source(&mut vm, "(123n).toString()");
|
||
assert!(ts.is_string());
|
||
assert_eq!(vm.lookup_str(ts).as_deref(), Some("123"));
|
||
assert_eq!(run_source(&mut vm, "Number(5n)"), JsValue::int(5));
|
||
}
|
||
|
||
#[test]
|
||
fn bigint_mixed_type_throws() {
|
||
let mut vm = Vm::new();
|
||
let te = run_source(&mut vm, "try { 1n + 1 } catch(e) { e.name }");
|
||
assert_eq!(vm.lookup_str(te).as_deref(), Some("TypeError"));
|
||
let re = run_source(&mut vm, "try { 1n / 0n } catch(e) { e.name }");
|
||
assert_eq!(vm.lookup_str(re).as_deref(), Some("RangeError"));
|
||
let ne = run_source(&mut vm, "try { new BigInt(1) } catch(e) { e.name }");
|
||
assert_eq!(vm.lookup_str(ne).as_deref(), Some("TypeError"));
|
||
}
|
||
|
||
#[test]
|
||
fn bigint_survives_reset_and_gc() {
|
||
let mut vm = Vm::new();
|
||
let result = run_source(&mut vm, "100n + 23n");
|
||
assert!(result.is_bigint());
|
||
assert_eq!(vm.bigint_value(result), &num_bigint::BigInt::from(123));
|
||
vm.reset();
|
||
// reset 保留 session 字符串/bigint box:值仍可读。
|
||
assert_eq!(vm.bigint_value(result), &num_bigint::BigInt::from(123));
|
||
}
|
||
|
||
#[test]
|
||
fn bigint_wrapped_and_number_comparison() {
|
||
let mut vm = Vm::new();
|
||
// 包装对象 coerce 后双 BigInt 运算。
|
||
assert_eq!(run_source(&mut vm, "Object(2n) / 2n"), run_source(&mut vm, "1n"));
|
||
assert_eq!(run_source(&mut vm, "Object(2n) * 3n"), run_source(&mut vm, "6n"));
|
||
assert_eq!(run_source(&mut vm, "2n + Object(3n)"), run_source(&mut vm, "5n"));
|
||
// BigInt 与 Number 精确关系比较(超出 f64 精度仍精确)。
|
||
assert_eq!(run_source(&mut vm, "9007199254740993n > 9007199254740992"), JsValue::bool(true));
|
||
assert_eq!(run_source(&mut vm, "9007199254740993n < 9007199254740994"), JsValue::bool(true));
|
||
assert_eq!(run_source(&mut vm, "2n < 3"), JsValue::bool(true));
|
||
assert_eq!(run_source(&mut vm, "3n >= 3"), JsValue::bool(true));
|
||
// NaN 关系比较为 false。
|
||
assert_eq!(run_source(&mut vm, "0n < NaN"), JsValue::bool(false));
|
||
// 混合算术抛 TypeError。
|
||
let te = run_source(&mut vm, "try { Object(1n) - 1 } catch(e) { e.name }");
|
||
assert_eq!(vm.lookup_str(te).as_deref(), Some("TypeError"));
|
||
}
|
||
|
||
#[test]
|
||
fn reset_clears_runtime_state_like_rerun() {
|
||
let mut vm = Vm::new();
|
||
vm.regs[1] = JsValue::int(7);
|
||
vm.pc = 3;
|
||
vm.frames.push(crate::vm::CallFrame {
|
||
return_addr: 1,
|
||
function_name: 0,
|
||
frame_id: 0,
|
||
caller_reg_limit: 2,
|
||
caller_active_reg_limit: 2,
|
||
saved_reg_offset: 0,
|
||
spill_offset: 0,
|
||
arguments_base: 0,
|
||
arguments_count: 0,
|
||
saved_this: JsValue::undefined(),
|
||
saved_new_target: JsValue::undefined(),
|
||
callee: JsValue::undefined(),
|
||
arguments_obj: JsValue::undefined(),
|
||
construct_result_reg: None,
|
||
strict: false,
|
||
constructed_this: None,
|
||
is_derived_constructor: false,
|
||
super_called: false,
|
||
continuation: crate::vm::FrameContinuation::None,
|
||
upvalues: std::ptr::slice_from_raw_parts(std::ptr::null(), 0),
|
||
});
|
||
vm.save_stack.push(JsValue::undefined());
|
||
vm.iters
|
||
.for_in_iters
|
||
.push(Box::into_raw(Box::new(crate::vm::ForInIter { keys: Vec::new(), index: 0 })));
|
||
vm.iters.for_of_iters.push(crate::vm_state::ForOfEntry {
|
||
iterator: JsValue::undefined(),
|
||
last_result: JsValue::undefined(),
|
||
is_async: false,
|
||
fast: None,
|
||
fast_value: JsValue::undefined(),
|
||
fast_inner: JsValue::undefined(),
|
||
fast_cursor: 0,
|
||
});
|
||
vm.saved_bytecode_stack.push(Arc::from(vec![oxide_bytecode::opcode::encode(
|
||
oxide_bytecode::opcode::OpCode::HALT,
|
||
0,
|
||
0,
|
||
0,
|
||
)]));
|
||
vm.saved_immutables_stack
|
||
.push(std::ptr::slice_from_raw_parts(std::ptr::null(), 0));
|
||
vm.try_stack.push(crate::vm::TryHandler {
|
||
catch_pc: Some(1),
|
||
finally_pc: None,
|
||
finally_active: false,
|
||
frame_depth: 0,
|
||
for_of_depth: 0,
|
||
dispose_depth: 0,
|
||
});
|
||
vm.exception_value = Some(JsValue::int(2));
|
||
vm.pending_exception = Some(JsValue::int(3));
|
||
vm.pending_error_kind = Some("TypeError");
|
||
|
||
vm.reset();
|
||
|
||
assert_eq!(vm.pc, 0);
|
||
assert!(vm.frames.is_empty());
|
||
assert!(vm.save_stack.is_empty());
|
||
assert!(vm.iters.for_in_iters.is_empty());
|
||
assert!(vm.iters.for_of_iters.is_empty());
|
||
assert!(vm.saved_bytecode_stack.is_empty());
|
||
assert!(vm.saved_immutables_stack.is_empty());
|
||
assert!(vm.try_stack.is_empty());
|
||
assert!(vm.exception_value.is_none());
|
||
assert!(vm.pending_exception.is_none());
|
||
assert!(vm.pending_error_kind.is_none());
|
||
assert!(vm.bytecode.is_empty());
|
||
assert!(vm.immutables().is_empty());
|
||
}
|
||
|
||
/// for-in 内抛错且被调函数无 try/catch、经内联调用路径(内置 map 回调):
|
||
/// unwind 只关 for-of,残留 for-in 体留在 VM 表内;restore 以快照向量整体替换
|
||
/// 表。残留体须先逐条释放,否则堆 Box 随旧向量丢弃。本钉固化 restore 契约:
|
||
/// 还原后 for-in 表与快照一致(顶层调用方无在途 for-in,快照为空)。
|
||
#[test]
|
||
fn inline_for_in_escape_restores_snapshot_and_releases_residual_body() {
|
||
let mut vm = Vm::new();
|
||
let allocator = oxide_parser::Allocator::default();
|
||
let program = oxide_parser::parse(
|
||
&allocator,
|
||
"var o = { a: 1 }; var hits = 0; \
|
||
[0].map(function () { for (var k in o) { hits = hits + 1; throw new Error('boom'); } });",
|
||
)
|
||
.expect("parse failed");
|
||
let module = oxide_compiler::compiler::Compiler::new()
|
||
.compile(&program)
|
||
.expect("compile failed");
|
||
let result = vm.run(&Arc::new(module));
|
||
assert!(result.is_err(), "未捕获异常应返回 Err");
|
||
// 回调 for-in 体经内联路径执行过一轮(hits 记 1),证明场景落在内联调用
|
||
// 路径且残留体确曾入表;restore 后表须还原为快照(顶层为空)。
|
||
assert_eq!(global_prop(&vm, "hits").as_int(), 1, "回调 for-in 体应执行过一轮");
|
||
assert!(vm.iters.for_in_iters.is_empty(), "restore 后 for-in 表应与快照一致(顶层为空)");
|
||
}
|
||
|
||
/// 既有 own 属性值覆盖须推进世代:`Array.prototype.push = 9`(P 对象既有槽、
|
||
/// prop_meta 路径)后 full_reset 必须重建 array 家族,函数值恢复。
|
||
#[test]
|
||
fn full_reset_value_overwrite_on_builtin_proto_rebuilds_family() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let old_array_proto = vm.realm.session.borrow().builtin_world().array_proto.as_ptr();
|
||
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push = 9"), JsValue::int(9));
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push"), JsValue::int(9));
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "值覆盖应推进世代被脏检测捕获");
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(!std::ptr::eq(
|
||
old_array_proto,
|
||
vm.realm.session.borrow().builtin_world().array_proto.as_ptr()
|
||
));
|
||
let t = run_source(&mut vm, "typeof Array.prototype.push");
|
||
assert_eq!(vm.lookup_str(t).as_deref(), Some("function"));
|
||
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// 对象值形态的既有槽覆盖同样须被发现:epoch 对象写入 builtin P 对象不被晋升,
|
||
/// 若漏 bump 则 full_reset 保留旧槽持悬垂指针(UAF);修复后家族重建、槽被丢弃。
|
||
#[test]
|
||
fn full_reset_value_overwrite_with_object_value_drops_stale_pointer() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let old_array_proto = vm.realm.session.borrow().builtin_world().array_proto.as_ptr();
|
||
|
||
let _ = run_source(&mut vm, "Array.prototype.push = {}; 0");
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "对象值覆盖应被脏检测捕获");
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(!std::ptr::eq(
|
||
old_array_proto,
|
||
vm.realm.session.borrow().builtin_world().array_proto.as_ptr()
|
||
));
|
||
let t = run_source(&mut vm, "typeof Array.prototype.push");
|
||
assert_eq!(vm.lookup_str(t).as_deref(), Some("function"));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// 零 session 对象的全局内置值覆盖:`globalThis.Array = 9` 不触发 promote,
|
||
/// 旧 full_reset 兜底(仅在存在 session 对象时强制 bump global)不生效;
|
||
/// 值写原语 bump 后 global 脏,full_reset 重绑回内置构造器。
|
||
#[test]
|
||
fn full_reset_global_builtin_value_overwrite_restores_constructor() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
|
||
let _ = run_source(&mut vm, "globalThis.Array = 9; 0");
|
||
assert!(vm.realm.gc.borrow().session_object_ptrs.is_empty(), "原始值覆盖不产生 session 对象");
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "全局值覆盖应推进世代");
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(std::ptr::eq(
|
||
global_prop(&vm, "Array").as_js_object_ptr(),
|
||
vm.realm.session.borrow().builtin_world().array_constructor.as_ptr() as *mut JsObject
|
||
));
|
||
assert_eq!(run_source(&mut vm, "[1].push(2)"), JsValue::int(2));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// IC 二次写(miss 落既有槽 + 命中直写)与复合赋值(set_member_prop 既有槽)
|
||
/// 两条写路径同样须推进世代,full_reset 后值恢复。
|
||
#[test]
|
||
fn full_reset_value_overwrite_via_ic_and_compound_marks_dirty() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
|
||
// IC 首写 miss 落既有槽、二次写经缓存命中,两次都须 bump。
|
||
let _ = run_source(&mut vm, "Array.prototype.push = 9; Array.prototype.push = 8; 0");
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "IC 二次写应被脏检测捕获");
|
||
vm.full_reset();
|
||
let t = run_source(&mut vm, "typeof Array.prototype.push");
|
||
assert_eq!(vm.lookup_str(t).as_deref(), Some("function"));
|
||
|
||
// 复合赋值读改写走 set_member_prop 既有槽路径,同样须 bump。
|
||
let _ = run_source(&mut vm, "Array.prototype.push = 9; Array.prototype.push += 1; 0");
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "复合赋值应被脏检测捕获");
|
||
vm.full_reset();
|
||
let t = run_source(&mut vm, "typeof Array.prototype.push");
|
||
assert_eq!(vm.lookup_str(t).as_deref(), Some("function"));
|
||
assert_eq!(run_source(&mut vm, "[1, 2, 3].push(4)"), JsValue::int(4));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// wrapper 本体新增 own 属性:写入的是方法 wrapper 自身而非所属 P 对象,
|
||
/// 只采 P 字段的世代快照无法察觉。修复后释放登记表的洁净基线捕获该写,
|
||
/// full_reset 失效其复用键并重建家族,属性不跨轮存活、无悬垂对象槽。
|
||
#[test]
|
||
fn full_reset_wrapper_own_property_does_not_survive() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let push_before = method_wrapper_ptr(&vm, vm.realm.session.borrow().builtin_world().array_proto.as_ptr(), "push");
|
||
|
||
let _ = run_source(&mut vm, "Array.prototype.push.custom = 1; 0");
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "wrapper 本体写应被脏检测捕获");
|
||
|
||
vm.full_reset();
|
||
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push.custom"), JsValue::undefined());
|
||
assert_eq!(run_source(&mut vm, "'custom' in Array.prototype.push"), JsValue::bool(false));
|
||
assert_eq!(run_source(&mut vm, "typeof Array.prototype.push === 'function'"), JsValue::bool(true));
|
||
let push_after = method_wrapper_ptr(&vm, vm.realm.session.borrow().builtin_world().array_proto.as_ptr(), "push");
|
||
assert!(!std::ptr::eq(push_before, push_after), "被写的 wrapper 不应被复用");
|
||
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// wrapper 本体写 epoch 对象值:epoch 对象不晋升、不登记,若 wrapper 跨轮
|
||
/// 存活则槽内指针悬垂。修复后家族重建、旧 wrapper 与槽一并被丢弃。
|
||
#[test]
|
||
fn full_reset_wrapper_object_property_does_not_dangle() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
|
||
let _ = run_source(&mut vm, "Array.prototype.push.custom = {}; 0");
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "wrapper 对象值写应被脏检测捕获");
|
||
|
||
vm.full_reset();
|
||
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push.custom"), JsValue::undefined());
|
||
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// wrapper 既有 own 槽的值覆盖:首次写已建槽后重录快照,第二次写走
|
||
/// `set_prop_storage` 既有槽路径,同样须推进 wrapper 世代被脏检测捕获。
|
||
#[test]
|
||
fn full_reset_wrapper_existing_slot_overwrite_marks_dirty() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let _ = run_source(&mut vm, "Array.prototype.push.custom = 1; 0");
|
||
vm.realm.session.borrow_mut().record_snapshot();
|
||
|
||
let _ = run_source(&mut vm, "Array.prototype.push.custom = 2; 0");
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "wrapper 既有槽覆盖应被脏检测捕获");
|
||
|
||
vm.full_reset();
|
||
|
||
assert_eq!(run_source(&mut vm, "Array.prototype.push.custom"), JsValue::undefined());
|
||
assert_eq!(run_source(&mut vm, "typeof Array.prototype.push === 'function'"), JsValue::bool(true));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// 不可复用宿主对象(`Reflect`)的属性写:登记面无复用键,脏时靠 global
|
||
/// 重建整批换新宿主 Box,旧宿主连同新增属性一并被丢弃。
|
||
#[test]
|
||
fn full_reset_keyless_leaked_object_property_does_not_survive() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
|
||
let _ = run_source(&mut vm, "Reflect.custom = 1; 0");
|
||
assert!(vm.realm.session.borrow().is_dirty_since_snapshot(), "keyless 宿主对象写应被脏检测捕获");
|
||
|
||
vm.full_reset();
|
||
|
||
assert_eq!(run_source(&mut vm, "'custom' in Reflect"), JsValue::bool(false));
|
||
assert_eq!(run_source(&mut vm, "typeof Reflect.apply === 'function'"), JsValue::bool(true));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// global 函数 wrapper(`parseInt`)的属性写:global 函数 wrapper 无 P 家族
|
||
/// 归属,脏时必须连带 global 重建并失效其复用键,否则新增属性跨轮存活。
|
||
#[test]
|
||
fn full_reset_global_function_wrapper_property_does_not_survive() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
|
||
let _ = run_source(&mut vm, "parseInt.custom = 1; 0");
|
||
assert!(
|
||
vm.realm.session.borrow().is_dirty_since_snapshot(),
|
||
"global 函数 wrapper 写应被脏检测捕获"
|
||
);
|
||
|
||
vm.full_reset();
|
||
|
||
assert_eq!(run_source(&mut vm, "'custom' in parseInt"), JsValue::bool(false));
|
||
assert_eq!(run_source(&mut vm, "parseInt('42')"), JsValue::int(42));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// wrapper 写重建后基线已刷新:后续仅 P 对象脏写的轮次复用新 wrapper、
|
||
/// 登记表不增长,不因上一轮 wrapper 写残留基线下沉而每轮全量失效。
|
||
#[test]
|
||
fn wrapper_write_rebuild_then_p_writes_reuse_fresh_wrapper() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let _ = run_source(&mut vm, "Array.prototype.push.custom = 1; 0");
|
||
vm.full_reset();
|
||
|
||
let push_after = method_wrapper_ptr(&vm, vm.realm.session.borrow().builtin_world().array_proto.as_ptr(), "push");
|
||
let registry_after = vm.realm.session.borrow().builtin_world().leaked_object_count();
|
||
|
||
for i in 0..3u32 {
|
||
let source = format!("Array.prototype['w{i}'] = 1; Object.prototype['w{i}'] = 2; 0");
|
||
let _ = run_source(&mut vm, &source);
|
||
vm.full_reset();
|
||
}
|
||
|
||
let push_final = method_wrapper_ptr(&vm, vm.realm.session.borrow().builtin_world().array_proto.as_ptr(), "push");
|
||
assert!(std::ptr::eq(push_after, push_final), "P 写轮应复用 wrapper,不应每轮失效重建");
|
||
assert_eq!(
|
||
vm.realm.session.borrow().builtin_world().leaked_object_count(),
|
||
registry_after,
|
||
"P 写轮释放登记表不应增长"
|
||
);
|
||
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// 假阳性护栏:只写用户对象的良性运行不得误判 wrapper 脏——wrapper 世代
|
||
/// 仅在自身被写时偏离基线,重建收尾的内部写由快照重刷基线吸收。
|
||
#[test]
|
||
fn benign_user_object_writes_do_not_dirty_leaked_objects() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let world_ptr = Arc::as_ptr(&vm.realm.session.borrow().builtin_world);
|
||
|
||
let result = run_source(&mut vm, "(function () { var a = {}; a.x = 1; a.x = 2; return a.x; })()");
|
||
assert_eq!(result, JsValue::int(2));
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot(), "只写用户对象不应误判 wrapper 脏");
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(
|
||
std::ptr::eq(world_ptr, Arc::as_ptr(&vm.realm.session.borrow().builtin_world)),
|
||
"无污染不应重建 world"
|
||
);
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
/// 假阳性护栏:只写用户对象的良性运行不得误判 builtin 脏。
|
||
/// 对象/数组字面量临时对象经统一入口入统一表,run 末不可达,
|
||
/// full_reset 随 session 释放;session 对象在场时防御兜底强制 bump
|
||
/// global(保留 global 持 session 子引用会在收尾后悬垂),builtin world
|
||
/// 不因值写误触发重建。
|
||
#[test]
|
||
fn benign_run_does_not_falsely_dirty_builtins() {
|
||
let mut vm = Vm::new();
|
||
let _ = run_source(&mut vm, "0");
|
||
let world_ptr = Arc::as_ptr(&vm.realm.session.borrow().builtin_world);
|
||
|
||
run_source(&mut vm, "({ x: 1 }).x + [1, 2].length");
|
||
assert_eq!(vm.session_object_count(), 2, "良性表达式应产生两枚临时对象(对象字面量 + 数组字面量)");
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot(), "良性运行不应误判 builtin 脏");
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(
|
||
std::ptr::eq(world_ptr, Arc::as_ptr(&vm.realm.session.borrow().builtin_world)),
|
||
"builtin world 不应重建"
|
||
);
|
||
assert!(!vm.realm.session.borrow().is_dirty_since_snapshot());
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_clears_symbol_state() {
|
||
let mut vm = Vm::new();
|
||
vm.realm.symbols.borrow_mut().intern(Some("shared".to_string()));
|
||
|
||
vm.full_reset();
|
||
|
||
assert!(vm.realm.symbols.borrow().symbol_descriptions.is_empty());
|
||
assert!(vm.realm.symbols.borrow().symbol_registry.is_empty());
|
||
}
|
||
|
||
/// 编译源文本为模块(供步数上限测试直接 `run` 并检查 `Result`)。
|
||
fn compile_module(source: &str) -> Arc<oxide_bytecode::module::CompiledModule> {
|
||
let allocator = oxide_parser::Allocator::default();
|
||
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
|
||
let module = oxide_compiler::compiler::Compiler::new()
|
||
.compile(&program)
|
||
.expect("compile failed");
|
||
Arc::new(module)
|
||
}
|
||
|
||
/// 步数覆盖超限:`set_max_steps(Some(100))` 加无限循环返回 `Err`,文本含 `step limit`。
|
||
#[test]
|
||
fn max_steps_override_exceeded_returns_err() {
|
||
let mut vm = Vm::new();
|
||
vm.set_max_steps(Some(100));
|
||
let module = compile_module("for(;;){}");
|
||
let result = vm.run(&module);
|
||
assert!(result.is_err(), "无限循环带步数覆盖应返回 Err:{result:?}");
|
||
let err = result.unwrap_err();
|
||
assert!(err.contains("step limit"), "错误文本应含 step limit:{err}");
|
||
}
|
||
|
||
/// 覆盖随 `full_reset` 清除:`Some(100)` 超限失败后 `full_reset`,再跑 50 轮循环
|
||
/// (步数远超 100)返回 `Ok`,证明覆盖值未残留(回退内核配置无上限)。
|
||
#[test]
|
||
fn full_reset_clears_max_steps_override() {
|
||
let mut vm = Vm::new();
|
||
vm.set_max_steps(Some(100));
|
||
let infinite = compile_module("for(;;){}");
|
||
assert!(vm.run(&infinite).is_err(), "无限循环应超限");
|
||
|
||
vm.full_reset();
|
||
|
||
// 50 轮循环步数远超 100:覆盖清除后无上限,应返回 Ok。
|
||
let moderate = compile_module("let n = 0; for (let i = 0; i < 50; i++) { n += i; } n");
|
||
assert!(vm.run(&moderate).is_ok(), "full_reset 后 50 轮循环不应被旧覆盖限制");
|
||
}
|
||
|
||
/// `set_max_steps(None)` 回退内核配置:内核配置 `max_steps` 置 `Some(100)` 的独立
|
||
/// kernel 上,覆盖置 None 后超限仍被限(`or` 回退语义)。
|
||
#[test]
|
||
fn max_steps_none_falls_back_to_kernel_config() {
|
||
let mut config = KernelConfig::minimal();
|
||
config.max_steps = Some(100);
|
||
let core = KernelCore::new(config);
|
||
let mut vm = Vm::with_kernel_core(Arc::clone(&core));
|
||
vm.set_max_steps(None);
|
||
let module = compile_module("for(;;){}");
|
||
let result = vm.run(&module);
|
||
assert!(result.is_err(), "内核配置限步下无限循环应返回 Err:{result:?}");
|
||
let err = result.unwrap_err();
|
||
assert!(err.contains("step limit"), "错误文本应含 step limit:{err}");
|
||
}
|