diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index 46e106b4..f103af89 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -227,9 +227,7 @@ impl Vm { shape_id = self.kernel_core.shape_forge().make_shape(shape_id, si); } } - let obj = self - .epoch - .alloc(JsObject::new_empty(shape_id, JsValue::from_js_object(proto_ptr))); + let obj = self.alloc_object(JsObject::new_empty(shape_id, JsValue::from_js_object(proto_ptr))); // 预分配数据槽并同步 generation:批量一次性 +nprops,与逐属性路径的终值一致。 let obj_ref = unsafe { &mut *obj }; for _ in 0..nprops { diff --git a/crates/oxide_vm/tests/session_arena_tests.rs b/crates/oxide_vm/tests/session_arena_tests.rs index 0bd440ef..7fc903b9 100644 --- a/crates/oxide_vm/tests/session_arena_tests.rs +++ b/crates/oxide_vm/tests/session_arena_tests.rs @@ -87,3 +87,48 @@ fn test_full_reset_clears_session_state() { let result = run_source(&mut vm, "globalThis.x"); assert!(result.is_undefined()); } + +/// NEW_OBJECT 字面量必须登记进 epoch 追踪表:`{}` 写入超内联容量的属性会分配 +/// hash_props 堆 Box,reset 时经追踪表统一释放;不登记则每个字面量泄漏一个 Box。 +#[test] +fn new_object_literal_tracked_and_freed_on_reset() { + let mut vm = Vm::new(); + let result = run_source( + &mut vm, + "var o = {}; for (var i = 0; i < 40; i++) { o['k' + i] = i; } \ + delete o.k7; o.k8", + ); + assert_eq!(format!("{}", result), "8", "属性写入/删除后读取应正常"); + assert!(vm.epoch_object_count() > 0, "NEW_OBJECT 应登记 epoch 追踪表"); + + let freed_before = vm.session_gc_stats().total_bytes_freed; + vm.reset(); + assert!( + vm.session_gc_stats().total_bytes_freed > freed_before, + "reset 应释放字面量对象的属性向量堆数据" + ); + assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空"); +} + +/// full_reset 同样释放 NEW_OBJECT 字面量:带属性写的 epoch 源对象经 promote 深拷贝 +/// 到 session 后,完全重置同时释放源对象与克隆的堆数据,不得双重释放。 +#[test] +fn new_object_literal_freed_by_full_reset() { + let mut vm = Vm::new(); + let result = run_source( + &mut vm, + "var o = {}; for (var i = 0; i < 40; i++) { o['k' + i] = i; } \ + globalThis.keep = o; o.k7", + ); + assert_eq!(format!("{}", result), "7", "逃逸后属性读取应正常"); + assert!(vm.epoch_object_count() > 0, "字面量源对象在 promote 后仍登记于 epoch 追踪表"); + assert!(vm.session_object_count() > 0, "逃逸克隆应登记于 session 追踪表"); + + vm.full_reset(); + assert_eq!(vm.epoch_object_count(), 0, "full_reset 后 epoch 追踪表应清空"); + assert_eq!(vm.session_object_count(), 0, "full_reset 后 session 追踪表应清空"); + assert!( + run_source(&mut vm, "globalThis.keep").is_undefined(), + "full_reset 后逃逸对象不可再访问" + ); +}