diff --git a/crates/oxide_builtins/src/module.rs b/crates/oxide_builtins/src/module.rs index 2897f155..86f712b9 100644 --- a/crates/oxide_builtins/src/module.rs +++ b/crates/oxide_builtins/src/module.rs @@ -151,8 +151,8 @@ fn push_entry(table: *mut ModuleNsTable, name_si: u32, origin: ModuleNsOrigin, s /// - 返回 `Initialized(v)`:`Value` 已初始化,或 cell 已初始化。 /// /// # 注意事项 -/// - `Cell` 读依赖「cell 由 `alloc_cell` 分配并登记,至 `full_reset` 才统一释放」 -/// 的生命周期契约;同一 VM 世代内指针有效。 +/// - `Cell` 读依赖「cell 由 `alloc_cell` 分配并登记,参与 mark-sweep 回收、 +/// `full_reset` 为收尾兜底」的生命周期契约;同一 VM 世代内指针有效。 pub fn module_ns_export(obj: &JsObject, key_si: u32) -> Option { let table = ns_table_ptr(obj); if table.is_null() { @@ -177,7 +177,7 @@ pub fn module_ns_export(obj: &JsObject, key_si: u32) -> Option { if cell.is_null() { return Some(ModuleNsQuery::Uninitialized); } - // SAFETY: cell 由 alloc_cell 分配并登记,至 full_reset 才释放。 + // SAFETY: cell 由 alloc_cell 分配并登记,参与 mark-sweep、full_reset 收尾兜底。 let cell_ref = unsafe { &**cell }; if cell_ref.is_initialized() { Some(ModuleNsQuery::Initialized(cell_ref.value)) @@ -215,6 +215,26 @@ pub fn module_ns_native_edges(obj: &JsObject) -> Vec { edges } +/// 收集条目表持有的共享 cell 指针(`Cell` 条目),供 GC mark cell 边。 +/// cell 指针不经对象图、须独立入存活集(与 `module_ns_native_edges` 的值边互补)。 +pub fn module_ns_cell_edges(obj: &JsObject) -> Vec<*mut Cell> { + let table = ns_table_ptr(obj); + if table.is_null() { + return Vec::new(); + } + // SAFETY: table 归本 ns 对象持有,生命周期见 `module_ns_export`。 + let table_ref = unsafe { &*table }; + let mut out = Vec::new(); + for entry in &table_ref.entries { + if let ModuleNsState::Cell(cell) = &entry.state { + if !cell.is_null() { + out.push(*cell); + } + } + } + out +} + /// 原地重写条目表内的 JsValue 边(对象搬移/epoch 晋升后的引用重定位)。 /// /// # 副作用 @@ -554,7 +574,7 @@ pub fn module_set_cell(vm: &mut H, args: &[u8]) -> NativeResult { let Some(cell) = vm.module_frame_cell(cell_idx) else { return type_error(vm, "__moduleSetCell: cell is not available"); }; - // SAFETY: cell 由 alloc_cell 分配并登记,至 full_reset 才释放。 + // SAFETY: cell 由 alloc_cell 分配并登记,参与 mark-sweep、full_reset 收尾兜底。 let value = unsafe { (*cell).value }; // SAFETY: idx 来自刚完成的表内查找,仍在界内。 unsafe { diff --git a/crates/oxide_types/src/object/cell.rs b/crates/oxide_types/src/object/cell.rs index 170c61bf..f1f37919 100644 --- a/crates/oxide_types/src/object/cell.rs +++ b/crates/oxide_types/src/object/cell.rs @@ -1,6 +1,7 @@ //! 闭包共享 cell:JS 值 + 已初始化 / GC 标记位,定长 16 字节。 //! -//! cell 组成闭包的 upvalue 数组(对象 `upvalues` 裸指针区),随闭包释放; +//! cell 组成闭包的 upvalue 数组(对象 `upvalues` 裸指针区),随 mark-sweep +//! 释放(死 cell 随收集释放,full_reset 收尾兜底); //! `flags` 为 1 字节位集加 7 字节固定填充,保证数组元素步长恒定。 use crate::value::JsValue; diff --git a/crates/oxide_vm/src/async_func.rs b/crates/oxide_vm/src/async_func.rs index c4d04773..6fc188ba 100644 --- a/crates/oxide_vm/src/async_func.rs +++ b/crates/oxide_vm/src/async_func.rs @@ -10,7 +10,7 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_boolean, NativeResult}; -use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; +use oxide_types::object::{Cell, JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; use crate::vm::Vm; @@ -556,6 +556,24 @@ pub(crate) fn async_native_string_edges(obj: &JsObject) -> Vec<*mut oxide_types: out } +/// 异步挂起帧 cell_stack 的 cell 指针扁平列表(GC mark cell 边)。 +/// 与 `async_native_string_edges` 经同一字段清单(`suspended.cell_stack`) +/// 产出,cell 指针不经对象图、须独立入存活集。 +pub(crate) fn async_native_cell_edges(obj: &JsObject) -> Vec<*mut Cell> { + let Some(state) = async_state_mut(obj) else { + return Vec::new(); + }; + let mut out = Vec::new(); + for cells in &state.suspended.cell_stack { + for &cell_ptr in cells { + if !cell_ptr.is_null() { + out.push(cell_ptr); + } + } + } + out +} + /// 用转发函数重写状态快照中的所有 JsValue(session GC 移动式清扫 / promote 用)。 pub(crate) fn rewrite_async_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) { let Some(state) = async_state_mut(obj) else { diff --git a/crates/oxide_vm/src/async_generator.rs b/crates/oxide_vm/src/async_generator.rs index e954d846..5a0a6ce2 100644 --- a/crates/oxide_vm/src/async_generator.rs +++ b/crates/oxide_vm/src/async_generator.rs @@ -17,7 +17,7 @@ use std::collections::VecDeque; use oxide_builtins::iterator::make_iter_result; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{NativeResult, VmHost}; -use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; +use oxide_types::object::{Cell, JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; use crate::generator::{DelegateOutcome, GeneratorResumeMode}; @@ -1118,6 +1118,24 @@ pub(crate) fn async_generator_native_string_edges(obj: &JsObject) -> Vec<*mut ox out } +/// 异步生成器挂起帧 cell_stack 的 cell 指针扁平列表(GC mark cell 边)。 +/// 与 `async_generator_native_string_edges` 经同一字段清单(`suspended.cell_stack`) +/// 产出,cell 指针不经对象图、须独立入存活集。 +pub(crate) fn async_generator_native_cell_edges(obj: &JsObject) -> Vec<*mut Cell> { + let Some(state) = async_gen_state_mut(obj) else { + return Vec::new(); + }; + let mut out = Vec::new(); + for cells in &state.suspended.cell_stack { + for &cell_ptr in cells { + if !cell_ptr.is_null() { + out.push(cell_ptr); + } + } + } + out +} + /// 用转发函数重写状态快照中的所有 JsValue(session GC 移动式清扫 / promote 用)。 pub(crate) fn rewrite_async_generator_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) { let Some(state) = async_gen_state_mut(obj) else { diff --git a/crates/oxide_vm/src/generator.rs b/crates/oxide_vm/src/generator.rs index 91f7e5f8..e58a57c4 100644 --- a/crates/oxide_vm/src/generator.rs +++ b/crates/oxide_vm/src/generator.rs @@ -9,7 +9,7 @@ use oxide_builtins::iterator::{is_callable, make_iter_result}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_boolean, NativeResult}; -use oxide_types::object::{JsObject, NativeFnPtr}; +use oxide_types::object::{Cell, JsObject, NativeFnPtr}; use oxide_types::value::JsValue; use crate::vm::Vm; @@ -939,6 +939,24 @@ pub(crate) fn generator_native_string_edges(obj: &JsObject) -> Vec<*mut oxide_ty out } +/// 生成器挂起帧 cell_stack 的 cell 指针扁平列表(GC mark cell 边)。 +/// 与 `generator_native_string_edges` 经同一字段清单(`suspended.cell_stack`) +/// 产出,cell 指针不经对象图、须独立入存活集。 +pub(crate) fn generator_native_cell_edges(obj: &JsObject) -> Vec<*mut Cell> { + let Some(state) = generator_state_mut(obj) else { + return Vec::new(); + }; + let mut out = Vec::new(); + for cells in &state.suspended.cell_stack { + for &cell_ptr in cells { + if !cell_ptr.is_null() { + out.push(cell_ptr); + } + } + } + out +} + /// 用转发函数重写状态快照中的所有 JsValue(session GC 移动式清扫 / promote 用)。 pub(crate) fn rewrite_generator_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) { let Some(state) = generator_state_mut(obj) else { diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 46f9d39b..5c953004 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -3,7 +3,7 @@ use std::mem::size_of; use std::time::Instant; use crate::{vm_debug, vm_info}; -use oxide_types::object::{JsObject, JsString, PropMetaEntry}; +use oxide_types::object::{Cell, JsObject, JsString, PropMetaEntry}; use oxide_types::value::JsValue; use rustc_hash::FxBuildHasher; @@ -32,6 +32,7 @@ pub struct SessionGc { pub(crate) mark_stack: Vec<*mut JsObject>, pub(crate) live_strings: HashSet<*mut JsString, FxBuildHasher>, pub(crate) live_bigints: HashSet<*mut num_bigint::BigInt, FxBuildHasher>, + pub(crate) live_cells: HashSet<*mut Cell, FxBuildHasher>, } impl SessionGc { @@ -47,6 +48,13 @@ impl SessionGc { /// 短路,漏扫其新增引用边。 pub(crate) fn clear_all_marks(&mut self, vm: &mut Vm) { vm_debug!("[GC] clear_all_marks: {} session objects", vm.gc_state.session_object_ptrs.len()); + self.clear_session_marks(vm); + self.clear_epoch_marks(vm); + } + + /// 只清 session 对象表的 mark 位。移动式 sweep 末尾的旧表对象已全释放/克隆 + /// (克隆未标记),清位须与表重写同步执行,独立成臂供调用点按需取用。 + pub(crate) fn clear_session_marks(&mut self, vm: &mut Vm) { for &ptr in &vm.gc_state.session_object_ptrs { if ptr.is_null() { continue; @@ -55,8 +63,11 @@ impl SessionGc { // session_epoch.alloc,在 arena 存活期间有效。 unsafe { (*ptr).set_gc_mark(false) }; } - // epoch 臂同清:mark 的 epoch 臂置位不随 session 清位消除,残留位会让 - // 下一次 mark 的 DFS 短路、漏扫该对象此间新增的边。 + } + + /// 只清 epoch 对象表的 mark 位。mark 的 epoch 臂置位不随 session 清位消除, + /// 残留位会让下一次 mark 的 DFS 短路、漏扫该对象此间新增的边。 + pub(crate) fn clear_epoch_marks(&mut self, vm: &mut Vm) { for &ptr in &vm.gc_state.epoch_object_ptrs { if ptr.is_null() { continue; @@ -138,6 +149,7 @@ impl SessionGc { obj: &JsObject, vm: &Vm, stack: &mut Vec<*mut JsObject>, live_strings: &mut HashSet<*mut JsString, FxBuildHasher>, live_bigints: &mut HashSet<*mut num_bigint::BigInt, FxBuildHasher>, + live_cells: &mut HashSet<*mut Cell, FxBuildHasher>, ) { if let Some(elements) = obj.array_elements_vec() { for &value in elements.iter() { @@ -200,6 +212,9 @@ impl SessionGc { for value in module::module_ns_native_edges(obj) { Self::process_edge(value, vm, stack, live_strings, live_bigints); } + for ptr in module::module_ns_cell_edges(obj) { + live_cells.insert(ptr); + } } if obj.is_generator_obj() { for value in crate::generator::generator_native_edges(obj) { @@ -208,6 +223,9 @@ impl SessionGc { for ptr in crate::generator::generator_native_string_edges(obj) { Self::mark_string_live(live_strings, ptr); } + for ptr in crate::generator::generator_native_cell_edges(obj) { + live_cells.insert(ptr); + } } if obj.is_promise_obj() { for value in crate::promise::promise_native_edges(obj) { @@ -221,6 +239,9 @@ impl SessionGc { for ptr in crate::async_func::async_native_string_edges(obj) { Self::mark_string_live(live_strings, ptr); } + for ptr in crate::async_func::async_native_cell_edges(obj) { + live_cells.insert(ptr); + } } if obj.is_async_generator_obj() { for value in crate::async_generator::async_generator_native_edges(obj) { @@ -229,17 +250,22 @@ impl SessionGc { for ptr in crate::async_generator::async_generator_native_string_edges(obj) { Self::mark_string_live(live_strings, ptr); } + for ptr in crate::async_generator::async_generator_native_cell_edges(obj) { + live_cells.insert(ptr); + } } // RegExp 实例 source/flags 字段持有字符串边。 if obj.is_regexp_obj() { Self::process_edge(obj.get_regexp_source(), vm, stack, live_strings, live_bigints); Self::process_edge(obj.get_regexp_flags(), vm, stack, live_strings, live_bigints); } - // 遍历 upvalue cell 中的引用。 + // 遍历 upvalue cell 中的引用:cell 指针本身入存活集(对象 upvalues 边 + // 是 cell 的根面),值边照常走 process_edge。 for cell_ptr in obj.upvalues_slice() { if cell_ptr.is_null() { continue; } + live_cells.insert(*cell_ptr); let cell = unsafe { &**cell_ptr }; Self::process_edge(cell.value, vm, stack, live_strings, live_bigints); } @@ -449,11 +475,21 @@ impl SessionGc { mark_stack: stack, live_strings, live_bigints, + live_cells, .. } = self; stack.clear(); live_strings.clear(); live_bigints.clear(); + live_cells.clear(); + // 活跃帧 cell_stack 是 cell 的根面(不经对象图):逐层种子入存活集。 + for cell_vec in &vm.cell_stack { + for &cell_ptr in cell_vec { + if !cell_ptr.is_null() { + live_cells.insert(cell_ptr); + } + } + } for ptr in string_seeds { Self::mark_string_live(live_strings, ptr); } @@ -479,7 +515,7 @@ impl SessionGc { continue; } // P 对象根(builtin world / global)不回收,只扫边。 - Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints); + Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints, live_cells); } } @@ -494,7 +530,7 @@ impl SessionGc { continue; } obj.set_gc_mark(true); - Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints); + Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints, live_cells); } } } @@ -741,7 +777,9 @@ impl SessionGc { }) .sum::() as usize; - self.clear_all_marks(vm); + // 移动式 sweep 后旧表对象已全释放/克隆(克隆未标记),session 臂空转, + // 只清 epoch 臂残留位(epoch 对象存活、位残留)。 + self.clear_epoch_marks(vm); let total_ptrs = survivors + dead; if total_ptrs > 0 { @@ -835,6 +873,39 @@ impl SessionGc { freed } + /// 清扫 session upvalue cell:保留 `mark()` 阶段记为存活的部分,其余经 + /// `Box::from_raw` 释放。存活 cell 不被搬移——Box 地址稳定——因此无需 + /// forwarding 表与根指针重写。在 BigInt 清扫之后运行。 + /// + /// cell 不在 `session_bytes_allocated` 账目口径内(与字符串/BigInt 补回口径 + /// 不同),故只累计释放字节统计、不重算存活账目。 + /// 返回释放的字节数。 + pub(crate) fn sweep_session_cells(&mut self, vm: &mut Vm) -> u64 { + let old = vm.gc_state.session_cell_ptrs.borrow_mut().drain(..).collect::>(); + let mut freed = 0u64; + let mut live = Vec::with_capacity(old.len()); + for ptr in old { + if ptr.is_null() { + continue; + } + if self.live_cells.contains(&ptr) { + // 存活——地址不变,无需重写。 + live.push(ptr); + } else { + // SAFETY: ptr 在 session_cell_ptrs 中但不可达,恰好释放一次。 + freed += size_of::() as u64; + unsafe { + drop(Box::from_raw(ptr)); + } + } + } + *vm.gc_state.session_cell_ptrs.borrow_mut() = live; + + self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed); + self.last_collection_bytes_freed = self.last_collection_bytes_freed.saturating_add(freed); + freed + } + /// 完整收集的触发判断:对象、字符串或 BigInt 表非空,且字节账目已达阈值。 pub(crate) fn should_collect(&self, vm: &Vm) -> bool { (!vm.gc_state.session_object_ptrs.is_empty() @@ -889,6 +960,7 @@ impl SessionGc { let mut freed_bytes = self.sweep(vm); freed_bytes += self.sweep_session_strings(vm); freed_bytes += self.sweep_session_bigints(vm); + freed_bytes += self.sweep_session_cells(vm); let elapsed = start.elapsed(); self.total_collections += 1; @@ -957,6 +1029,7 @@ impl SessionGc { vm.gc_state.session_bytes_allocated = object_bytes; let mut freed_bytes = self.sweep_session_strings(vm); freed_bytes += self.sweep_session_bigints(vm); + freed_bytes += self.sweep_session_cells(vm); // 恢复 mark 位不变量:mark 之后必由清位收尾(与 sweep 末尾同点清位)。 // 残留 marked 对象会让下一次完整收集(reset 路径)的 mark DFS 短路漏标, @@ -1098,6 +1171,9 @@ impl SessionGc { } // SAFETY: ptr 来自 session 对象表登记,arena 存活期内有效。 if unsafe { (*ptr).is_gc_marked() } { + // 原地 sweep 不搬移对象:存活对象带位保留,清位并入活分支 + // (收集后无残留,残留 true 使下一次 mark DFS 短路漏标)。 + unsafe { (*ptr).set_gc_mark(false) }; survivors.push(ptr); } else { session_freed += Self::drop_dead_session_object(ptr); @@ -1107,8 +1183,8 @@ impl SessionGc { let live_session = survivors.len() as u64; vm.gc_state.session_object_ptrs = survivors; - // 恢复 mark 位不变量:收集后无残留(残留 true 使下一次 mark DFS 短路漏标)。 - self.clear_all_marks(vm); + // 死 cell 随死对象出表:存活 cell 经 mark 种子 + 对象边入存活集,此处清扫。 + self.sweep_session_cells(vm); // 对象口径重算:存活对象 + 存活串 + BigInt(串/BigInt 本路径不动, // 随公式整体重算保持与 strings-only 口径一致)。 @@ -1242,6 +1318,7 @@ impl Default for SessionGc { mark_stack: Vec::new(), live_strings: HashSet::default(), live_bigints: HashSet::default(), + live_cells: HashSet::default(), } } } diff --git a/crates/oxide_vm/src/session_gc/tests.rs b/crates/oxide_vm/src/session_gc/tests.rs index 13e94464..71f70d30 100644 --- a/crates/oxide_vm/src/session_gc/tests.rs +++ b/crates/oxide_vm/src/session_gc/tests.rs @@ -9,7 +9,8 @@ use std::sync::Arc; use super::*; use crate::vm::{CallFrame, FrameContinuation}; -use oxide_builtins::{array_buffer, data_view, disposable_stack, map, set, typed_array}; +use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, set, typed_array}; +use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule}; use oxide_runtime_api::NativeResult; fn plain_object(vm: &mut Vm) -> *mut JsObject { @@ -456,7 +457,8 @@ fn map_native_storage_is_not_a_normal_object_edge() { let mut stack = Vec::new(); let mut live = HashSet::with_hasher(FxBuildHasher); let mut live_bigints = HashSet::with_hasher(FxBuildHasher); - SessionGc::scan_edges_for_mark(map_obj, &vm, &mut stack, &mut live, &mut live_bigints); + let mut live_cells = HashSet::with_hasher(FxBuildHasher); + SessionGc::scan_edges_for_mark(map_obj, &vm, &mut stack, &mut live, &mut live_bigints, &mut live_cells); assert!(!stack.iter().any(|&ptr| std::ptr::eq(ptr, native_ptr))); } @@ -1216,6 +1218,205 @@ fn cells_freed_by_full_reset_and_reallocatable() { assert_eq!(format!("{result}"), "2", "重置后新 cell 应正常分配与读取"); } +// ── upvalue cell 参与 mark-sweep:死 cell 释放、活 cell 四根面存活 ───────── + +/// 死闭包 cell 字节账目差分:两条同构 arrow 仅 upvalue 数不同(3 vs 0),各走 +/// 独立 VM「创建 → 撤根 → 完整收集」。两次收集字节账目差 = 死 arrow 的 upvalue +/// 列表 Box + 3 个 cell(对象本体、属性区、函数名串在 A/B 恒等相消)。死分支不 +/// 释放 cell(泄漏)时差分少 3×16B,本断言转红。 +#[test] +fn dead_closure_cells_freed_by_sweep_byte_diff() { + let collect_freed = |src: &str| -> u64 { + let mut v = vm_with_threshold(4096); + v.run(&Arc::new(compile(src))).expect("run create"); + v.run(&Arc::new(compile("globalThis.arrow = undefined; 0"))) + .expect("run unroot"); + v.collect_session_gc(); + let stats = v.session_gc_stats(); + assert!(stats.last_collection_objects_dead >= 1, "arrow 闭包应经 sweep 死分支"); + stats.last_collection_bytes_freed + }; + let freed_with_captures = collect_freed( + "(function(){var a = 1; var b = 2; var c = 3; \ + globalThis.arrow = () => a + b + c; return 0;})()", + ); + let freed_no_captures = collect_freed("globalThis.arrow = () => 7; 0"); + let upvalue_box_min = (size_of::>() + 3 * size_of::<*mut Cell>()) as u64; + let cells_min = (3 * size_of::()) as u64; + assert!( + freed_with_captures >= freed_no_captures + upvalue_box_min + cells_min, + "两次收集字节账目差应为死闭包 upvalue 列表 Box + 3 cell(≥{} B),实际差 {}", + upvalue_box_min + cells_min, + freed_with_captures - freed_no_captures + ); +} + +/// 活闭包 cell 跨完整收集存活:global 上的活闭包经对象 upvalues 边可达,其 cell +/// 入存活集;清寄存器仅留 global 根后完整收集,cell 仍登记在 session 表。 +#[test] +fn live_closure_cell_survives_full_collect() { + let mut vm = vm_with_threshold(1); + vm.run(&Arc::new(compile( + "function outer() { var counter = 0; function inc() { return ++counter; } globalThis.inc = inc; return 0; } outer(); 0", + ))) + .expect("run1"); + let inc_val = global_prop_opt(&vm, "inc").expect("inc 应挂在 global 上"); + let obj = unsafe { &*inc_val.as_js_object_ptr() }; + let cells = obj.upvalues_slice(); + assert_eq!(cells.len(), 1, "inc 应捕获 counter 一个 cell"); + let cell_ptr = cells[0]; + assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "活 cell 应登记在表"); + + vm.regs.fill(JsValue::undefined()); + collect(&mut vm); + + // 只断言表成员——sweep 已释放的指针解引用即 UB。 + assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "活 cell 应跨完整收集存活"); +} + +/// 活跃帧 cell_stack 根:内层函数 upvalue cell 在活跃帧 cell_stack 上(低阈值使 +/// 同 run 内触发执行期收集);函数返回后闭包读值正确(cell 未被误释放)。 +#[test] +fn active_frame_cell_stack_root_survives_in_run_collect() { + let mut vm = vm_with_threshold(1); + vm.run(&Arc::new(compile( + "function outer() { \ + var counter = 42; \ + function inc() { return counter; } \ + globalThis.inc = inc; \ + var sink = []; \ + for (var i = 0; i < 500; i++) { sink.push({ x: i }); } \ + return 0; \ + } outer()", + ))) + .expect("run"); + assert!(vm.session_gc_stats().total_collections >= 1, "执行期收集应触发"); + let read = vm.run(&Arc::new(compile("globalThis.inc()"))).expect("read"); + assert_eq!(read, JsValue::int(42), "闭包读值正确(活跃帧 cell_stack 上的 cell 未被误释放)"); +} + +/// 挂起状态盒根:生成器局部变量被嵌套闭包捕获的 cell 在挂起帧 cell_stack 上 +/// (快照入状态盒),挂起态完整收集;恢复后读值正确(cell 未被误释放)。 +#[test] +fn suspended_state_box_cell_root_survives_collect() { + let mut vm = vm_with_low_threshold(); + vm.run(&Arc::new(compile( + "(function(){ \ + function* gen() { \ + var x = 100; \ + function f() { return x; } \ + yield f; \ + return f(); \ + } \ + var g = gen(); g.next(); globalThis.g = g; })(); 0", + ))) + .expect("run"); + + let g_val = global_prop_opt(&vm, "g").expect("g 应挂在 global 上"); + let g_session = vm.promote_object(g_val.as_js_object_ptr()); + let g_obj = unsafe { &*g_session }; + // SAFETY: 生成器状态盒经 Box::into_raw 挂对象构造,生命周期与对象一致。 + let state = g_obj.native_data() as *mut crate::generator::GeneratorState; + let cell_ptr = unsafe { &(*state).suspended } + .cell_stack + .iter() + .flatten() + .copied() + .find(|p| !p.is_null()) + .expect("挂起帧应持有 x cell"); + assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "cell 应登记在表"); + + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(g_session); + collect(&mut vm); + + // 只断言表成员——sweep 已释放的指针解引用即 UB。 + assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "挂起帧 cell 应跨完整收集存活"); + + // 恢复后读值:cell 未被误释放。 + let read = vm.run(&Arc::new(compile("globalThis.g.next().value"))).expect("read"); + assert_eq!(read, JsValue::int(100), "恢复后读值正确(挂起帧 cell 未被误释放)"); +} + +/// 文件加载器:base_dir/specifier join 后读源(依赖模块经真实文件解析)。 +struct NsFileLoader; +impl ModuleSourceLoader for NsFileLoader { + fn resolve( + &mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)], + ) -> Result { + let joined = std::path::Path::new(base_dir).join(specifier); + let full = std::fs::canonicalize(&joined).map_err(|e| format!("cannot read {specifier}: {e}"))?; + let source = std::fs::read_to_string(&full).map_err(|e| format!("cannot read {specifier}: {e}"))?; + Ok(ResolvedModule { + source, + path: full.to_string_lossy().into_owned(), + kind: ModuleKind::Js, + }) + } +} + +/// 测试结束删除临时目录,避免在 crate 树残留文件。 +struct NsCleanup(std::path::PathBuf); +impl Drop for NsCleanup { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// 模块 ns cell 根:依赖模块可重赋导出挂共享 cell(条目表 `Cell` 面),ns 对象 +/// 经 global 可达;清寄存器仅留 ns 根后完整收集,cell 跨移动式 sweep 存活(深拷 +/// 表保留同一 cell 指针),`module_ns_export` 读回正确。 +#[test] +fn module_ns_cell_root_survives_collect() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_ns_cell_gc__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write(dir.join("dep.mjs"), "var x = 42; export { x }; globalThis.f = function(){ x = 100; };") + .expect("write dep"); + std::fs::write(dir.join("main.mjs"), "import * as ns from './dep.mjs'; globalThis.__ns = ns;").expect("write main"); + let _cleanup = NsCleanup(dir.clone()); + + let allocator = oxide_parser::Allocator::default(); + let source = std::fs::read_to_string(dir.join("main.mjs")).expect("read main"); + let program = oxide_parser::parse_module(&allocator, &source).expect("parse module"); + let module = Compiler::new() + .compile_module(&program, dir.join("main.mjs").to_string_lossy().as_ref(), &mut NsFileLoader) + .expect("compile module"); + let mut vm = vm_with_low_threshold(); + vm.run(&Arc::new(module)).expect("module run"); + + let ns_val = global_prop_opt(&vm, "__ns").expect("__ns 应挂在 global 上"); + let ns_obj = unsafe { &*ns_val.as_js_object_ptr() }; + let cell_ptr = module::module_ns_cell_edges(ns_obj) + .into_iter() + .next() + .expect("依赖 ns 应持有 x 的 cell"); + assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "cell 应登记在表"); + + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = ns_val; + collect(&mut vm); + + // 只断言表成员——sweep 已释放的指针解引用即 UB。 + assert!( + vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), + "模块 ns cell 应跨完整收集存活" + ); + + // 移动式 sweep 后 ns 晋升 session(原件出表),从 global 重取克隆读回。 + let ns_val2 = global_prop_opt(&vm, "__ns").expect("__ns 应挂在 global 上"); + let ns_obj2 = unsafe { &*ns_val2.as_js_object_ptr() }; + assert!(module::module_ns_cell_edges(ns_obj2).contains(&cell_ptr), "克隆应共享同一 cell 指针"); + let name_si = vm.kernel_core().perm_interner().intern("x").0; + let query = module::module_ns_export(ns_obj2, name_si).expect("x 应可导出"); + match query { + module::ModuleNsQuery::Initialized(v) => { + assert_eq!(v, JsValue::int(42), "模块 ns cell 读回正确"); + } + module::ModuleNsQuery::Uninitialized => panic!("x 应已初始化,实得 Uninitialized"), + } +} + #[test] fn allocation_does_not_trigger_gc_before_safe_point() { let mut vm = vm_with_threshold(1); diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs index 797112ca..1bd8d62b 100644 --- a/crates/oxide_vm/src/vm_state.rs +++ b/crates/oxide_vm/src/vm_state.rs @@ -36,7 +36,8 @@ pub(crate) struct GcState { pub(crate) session_bigint_ptrs: RefCell>, /// upvalue cell(`Box`)追踪表。`RefCell` 使 `&self` 的分配入口也能 /// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(sweep 只重写 - /// `cell.value` 中的对象引用),只在 full_reset 统一释放。 + /// `cell.value` 中的对象引用);参与 mark-sweep 回收(死 cell 随收集释放), + /// full_reset 为收尾兜底(表已空时 no-op)。 pub(crate) session_cell_ptrs: RefCell>, pub(crate) session_bytes_allocated: usize, /// 执行期 session 堆账目的峰值高水位(`session_bytes_allocated` 的采样上界), @@ -82,8 +83,9 @@ impl GcState { ptr } - /// 释放全部 session 堆 upvalue cell box。仅在完全隔离重置(`full_reset`)时调用, - /// 此时没有存活的 cell_stack / 函数对象 upvalues 会引用它们。 + /// 释放全部 session 堆 upvalue cell box(收尾兜底,幂等)。仅在完全隔离重置 + /// (`full_reset`)时调用,此时没有存活的 cell_stack / 函数对象 upvalues 会引用 + /// 它们;mark-sweep 已释放的死 cell 已出表,表内残留为收集后仍登记的存活 cell。 pub(crate) fn free_cells(&mut self) { for ptr in self.session_cell_ptrs.borrow_mut().drain(..) { // SAFETY: 每个指针来自 alloc_cell 的 Box::into_raw(Box::new(Cell)),