Files
OxideJS-New/crates/oxide_vm/src/vm_support/tests.rs
T

1117 lines
50 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! vm_support 内联测试:全量重置状态清除与全局重建、分配上限、生成器与 BigInt 存活、动态编译及源文本断言行为。
use super::*;
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 global = vm.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.session.global_object.as_ptr();
vm.full_reset();
// global 必须重建:旧 global 与其 session 子引用随 session 释放,Array 恢复内置构造器。
assert!(!std::ptr::eq(old_global, vm.session.global_object.as_ptr()));
assert!(std::ptr::eq(
global_prop(&vm, "Array").as_js_object_ptr(),
vm.session.builtin_world().array_constructor.as_ptr() as *mut JsObject
));
assert!(!vm.session.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.session.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.session.builtin_world().array_proto.as_ptr(), "push");
let push_shape_before = unsafe { &*push_before }.shape_id();
let registry_before = vm.session.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.session.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.session.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.session.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.session.builtin_world);
let global_ptr = vm.session.global_object.as_ptr();
let object_proto_ptr = vm.session.builtin_world().object_proto.as_ptr();
vm.full_reset();
assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world)));
assert!(std::ptr::eq(global_ptr, vm.session.global_object.as_ptr()));
assert!(std::ptr::eq(object_proto_ptr, vm.session.builtin_world().object_proto.as_ptr()));
assert!(!vm.session.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.session.builtin_world);
let global_ptr = vm.session.global_object.as_ptr();
let global = unsafe { &mut *(vm.session.global_object.as_ptr() as *mut JsObject) };
bindings::bind_global_value(&vm.kernel_core, global, "userGlobal", JsValue::int(99));
unsafe { &mut *(vm.session.global_object.as_ptr() as *mut JsObject) }.bump_generation();
vm.full_reset();
assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world)));
assert!(!std::ptr::eq(global_ptr, vm.session.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.session.builtin_world().array_constructor.as_ptr() as *mut JsObject
));
assert!(std::ptr::eq(
global_prop(&vm, "globalThis").as_js_object_ptr(),
vm.session.global_object.as_ptr() as *mut JsObject
));
assert!(!vm.session.is_dirty_since_snapshot());
}
#[test]
fn full_reset_dirty_builtin_rebinds_global_slot() {
let mut vm = Vm::new();
let old_object_proto = vm.session.builtin_world().object_proto.as_ptr();
let old_array_proto = vm.session.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.session.builtin_world().object_proto.as_ptr()));
assert!(!std::ptr::eq(old_array_proto, vm.session.builtin_world().array_proto.as_ptr()));
assert!(std::ptr::eq(
global_prop(&vm, "Array").as_js_object_ptr(),
vm.session.builtin_world().array_constructor.as_ptr() as *mut JsObject
));
let constructor_si = vm.kernel_core.perm_interner().intern("constructor").0;
let array_proto = &*vm.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.session.builtin_world().array_constructor.as_ptr() as *mut JsObject
));
assert!(!vm.session.is_dirty_since_snapshot());
}
#[test]
fn full_reset_dirty_function_keeps_call_working() {
let mut vm = Vm::new();
let function_proto = vm.session.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.session.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.session.builtin_world().function_proto.as_ptr() as *mut JsObject;
// 保留方法 wrapper:array 家族不重建,wrapper 对象与其 proto 槽跨重置存活。
let array_proto = vm.session.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.session.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.session.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.session.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.session.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(避免缓存下标冲突)
}
#[test]
fn dynamic_function_basic_arity() {
let mut vm = Vm::new();
// 与等价静态函数返回值逐位一致(引擎函数调用统一产 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();
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();
assert!(run_source(&mut vm, "Function()()").is_undefined());
}
#[test]
fn dynamic_function_syntax_error_throws_syntax_error() {
let mut vm = Vm::new();
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();
// 匿名函数体声明嵌套函数 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();
// 同一 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();
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();
// 单个实参 "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();
// 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();
// 形参 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.session.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.session.builtin_world().object_proto.as_ptr()));
assert!(std::ptr::eq(
vm.object_prototype.as_ptr(),
vm.session.builtin_world().object_proto.as_ptr()
));
assert!(!vm.session.is_dirty_since_snapshot());
}
#[test]
fn full_reset_object_dirty_rebinds_iterator_family() {
let mut vm = Vm::new();
let old_object_proto = vm.session.builtin_world().object_proto.as_ptr();
let old_iterator_proto = vm.session.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.session.builtin_world().object_proto.as_ptr()));
assert!(!std::ptr::eq(old_iterator_proto, vm.session.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.session.builtin_world().iterator_proto.as_ptr() as *mut JsObject
));
// full_reset 后 session 干净;此后 run_source 执行才重新累积世代变化。
assert!(!vm.session.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.session.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.session.global_object.as_ptr() as *mut JsObject) }.bump_generation();
vm.full_reset();
// builtin 未脏 → 迭代器原型保留原对象且属性槽不膨胀(重复 full_reset 不再追加)。
let arr_iter_proto_after = vm.session.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.session.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,
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(),
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,
});
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.session.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.session.is_dirty_since_snapshot(), "值覆盖应推进世代被脏检测捕获");
vm.full_reset();
assert!(!std::ptr::eq(old_array_proto, vm.session.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.session.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.session.builtin_world().array_proto.as_ptr();
let _ = run_source(&mut vm, "Array.prototype.push = {}; 0");
assert!(vm.session.is_dirty_since_snapshot(), "对象值覆盖应被脏检测捕获");
vm.full_reset();
assert!(!std::ptr::eq(old_array_proto, vm.session.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.session.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.gc_state.session_object_ptrs.is_empty(), "原始值覆盖不产生 session 对象");
assert!(vm.session.is_dirty_since_snapshot(), "全局值覆盖应推进世代");
vm.full_reset();
assert!(std::ptr::eq(
global_prop(&vm, "Array").as_js_object_ptr(),
vm.session.builtin_world().array_constructor.as_ptr() as *mut JsObject
));
assert_eq!(run_source(&mut vm, "[1].push(2)"), JsValue::int(2));
assert!(!vm.session.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.session.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.session.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.session.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.session.builtin_world().array_proto.as_ptr(), "push");
let _ = run_source(&mut vm, "Array.prototype.push.custom = 1; 0");
assert!(vm.session.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.session.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.session.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.session.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.session.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.session.record_snapshot();
let _ = run_source(&mut vm, "Array.prototype.push.custom = 2; 0");
assert!(vm.session.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.session.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.session.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.session.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.session.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.session.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.session.builtin_world().array_proto.as_ptr(), "push");
let registry_after = vm.session.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.session.builtin_world().array_proto.as_ptr(), "push");
assert!(std::ptr::eq(push_after, push_final), "P 写轮应复用 wrapper,不应每轮失效重建");
assert_eq!(
vm.session.builtin_world().leaked_object_count(),
registry_after,
"P 写轮释放登记表不应增长"
);
assert_eq!(run_source(&mut vm, "[1, 2].push(3)"), JsValue::int(3));
assert!(!vm.session.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.session.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.session.is_dirty_since_snapshot(), "只写用户对象不应误判 wrapper 脏");
vm.full_reset();
assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world)), "无污染不应重建 world");
assert!(!vm.session.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.session.builtin_world);
run_source(&mut vm, "({ x: 1 }).x + [1, 2].length");
assert_eq!(vm.session_object_count(), 2, "良性表达式应产生两枚临时对象(对象字面量 + 数组字面量)");
assert!(!vm.session.is_dirty_since_snapshot(), "良性运行不应误判 builtin 脏");
vm.full_reset();
assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world)), "builtin world 不应重建");
assert!(!vm.session.is_dirty_since_snapshot());
}
#[test]
fn full_reset_clears_symbol_state() {
let mut vm = Vm::new();
vm.symbols.intern(Some("shared".to_string()));
vm.full_reset();
assert_eq!(vm.symbols.symbol_counter, 0);
assert!(vm.symbols.symbol_descriptions.is_empty());
assert!(vm.symbols.symbol_registry.is_empty());
}