From 473f73a1f69ed08da703b16c8d7c3dce60861d09 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 12 Sep 2026 18:10:03 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8E=9F=E7=94=9F=E7=9B=92=20mark=20?= =?UTF-8?q?=E8=BE=B9=E6=94=B6=E9=9B=86=E5=8E=BB=E5=AF=B9=E8=B1=A1=E9=A2=84?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E2=80=94=E2=80=94=E7=9B=92=E5=86=85=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2/BigInt=20=E8=BE=B9=E5=B9=B6=E5=85=A5=20live?= =?UTF-8?q?=20=E6=A0=87=E8=AE=B0=E9=97=AD=E5=90=88=20sweep=20=E6=82=AC?= =?UTF-8?q?=E5=9E=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/disposable_stack.rs | 12 +- crates/oxide_builtins/src/map.rs | 11 +- crates/oxide_builtins/src/set.rs | 11 +- crates/oxide_vm/src/async_func.rs | 26 +-- crates/oxide_vm/src/async_generator.rs | 32 ++- crates/oxide_vm/src/generator.rs | 20 +- crates/oxide_vm/src/promise.rs | 22 +-- crates/oxide_vm/src/session_gc.rs | 119 ++++++++++- crates/oxide_vm/tests/gc_runtime_tests.rs | 186 ++++++++++++++++++ 9 files changed, 349 insertions(+), 90 deletions(-) diff --git a/crates/oxide_builtins/src/disposable_stack.rs b/crates/oxide_builtins/src/disposable_stack.rs index b0aab232..029565bf 100644 --- a/crates/oxide_builtins/src/disposable_stack.rs +++ b/crates/oxide_builtins/src/disposable_stack.rs @@ -533,7 +533,8 @@ pub fn get_capability_ptr(obj: &JsObject) -> *mut DisposeCapability { obj.native_data() as *mut DisposeCapability } -/// 收集栈内作为对象引用的 value/method(GC 根边),供跨 epoch 遍历/重写时追踪。 +/// 收集栈内全部 value/method 边(GC mark 边):对象/字符串/BigInt 均产出, +/// 消费侧按值类型分发到对象栈与存活集。 pub fn dispose_edges(obj: &JsObject) -> Vec { if !obj.is_disposable_stack_obj() && !obj.is_async_disposable_stack_obj() { return Vec::new(); @@ -544,14 +545,7 @@ pub fn dispose_edges(obj: &JsObject) -> Vec { } // SAFETY: native_data 持有 `alloc_capability` 写入的有效 Box 指针,本函数 // 只在对象存活期间被 GC/绑定层调用。 - unsafe { - (*ptr) - .entries - .iter() - .flat_map(|entry| [entry.value, entry.method]) - .filter(|value: &JsValue| value.is_object()) - .collect() - } + unsafe { (*ptr).entries.iter().flat_map(|entry| [entry.value, entry.method]).collect() } } /// 克隆状态盒到新对象,用 `rewrite` 改写其中的对象引用 diff --git a/crates/oxide_builtins/src/map.rs b/crates/oxide_builtins/src/map.rs index b7c21a92..1c6edc13 100644 --- a/crates/oxide_builtins/src/map.rs +++ b/crates/oxide_builtins/src/map.rs @@ -68,7 +68,8 @@ fn alloc_map(vm: &mut H) -> *mut JsObject { vm.alloc_object(obj) } -/// 收集 Map 中作为对象引用的键和值(GC 根边),供跨 epoch 遍历/重写时追踪。 +/// 收集 Map 内全部键值边(GC mark 边):对象/字符串/BigInt 均产出, +/// 消费侧按值类型分发到对象栈与存活集。 pub fn map_native_edges(obj: &JsObject) -> Vec { if !obj.is_map() { return Vec::new(); @@ -77,13 +78,7 @@ pub fn map_native_edges(obj: &JsObject) -> Vec { if inner.is_null() { return Vec::new(); } - unsafe { - (*inner) - .iter() - .flat_map(|(key, value)| [key.0, *value]) - .filter(|value: &JsValue| value.is_object()) - .collect() - } + unsafe { (*inner).iter().flat_map(|(key, value)| [key.0, *value]).collect() } } /// 克隆 Map 的 native 数据到新对象,用 `rewrite` 改写其中的对象引用 diff --git a/crates/oxide_builtins/src/set.rs b/crates/oxide_builtins/src/set.rs index 094b7397..e8a283a5 100644 --- a/crates/oxide_builtins/src/set.rs +++ b/crates/oxide_builtins/src/set.rs @@ -110,7 +110,8 @@ fn alloc_set(vm: &mut H) -> *mut JsObject { vm.alloc_object(obj) } -/// 收集 Set 中作为对象引用的元素(GC 根边),供跨 epoch 遍历/重写时追踪。 +/// 收集 Set 内全部元素边(GC mark 边):对象/字符串/BigInt 均产出, +/// 消费侧按值类型分发到对象栈与存活集。 pub fn set_native_edges(obj: &JsObject) -> Vec { if !obj.is_set() { return Vec::new(); @@ -119,13 +120,7 @@ pub fn set_native_edges(obj: &JsObject) -> Vec { if inner.is_null() { return Vec::new(); } - unsafe { - (*inner) - .iter() - .map(|key| key.0) - .filter(|value: &JsValue| value.is_object()) - .collect() - } + unsafe { (*inner).iter().map(|key| key.0).collect() } } /// 克隆 Set 的 native 数据到新对象,用 `rewrite` 改写其中的对象引用。 diff --git a/crates/oxide_vm/src/async_func.rs b/crates/oxide_vm/src/async_func.rs index b368bfb4..2192fbd8 100644 --- a/crates/oxide_vm/src/async_func.rs +++ b/crates/oxide_vm/src/async_func.rs @@ -492,28 +492,20 @@ pub(crate) fn async_holds_suspended_for_in(obj: &JsObject) -> bool { !state.suspended.for_in_iters.is_empty() } -/// 异步状态内所有对象引用的扁平列表(GC mark 边)。 +/// 异步状态内全部引用边的扁平列表(GC mark 边):对象/字符串/BigInt 均产出, +/// 消费侧按值类型分发到对象栈与存活集。 pub(crate) fn async_native_edges(obj: &JsObject) -> Vec { let Some(state) = async_state_mut(obj) else { return Vec::new(); }; let mut edges = Vec::new(); - let push = |v: JsValue, edges: &mut Vec| { - if v.is_object() { - edges.push(v); - } - }; - push(state.callee, &mut edges); - edges.extend(state.args.iter().copied().filter(|v| v.is_object())); - push(state.result, &mut edges); - push(state.promise, &mut edges); - push(state.resolve, &mut edges); - push(state.reject, &mut edges); - state.suspended.for_each_value(|v| { - if v.is_object() { - edges.push(v); - } - }); + edges.push(state.callee); + edges.extend(state.args.iter().copied()); + edges.push(state.result); + edges.push(state.promise); + edges.push(state.resolve); + edges.push(state.reject); + state.suspended.for_each_value(|v| edges.push(v)); edges } diff --git a/crates/oxide_vm/src/async_generator.rs b/crates/oxide_vm/src/async_generator.rs index b160aa36..889e1b52 100644 --- a/crates/oxide_vm/src/async_generator.rs +++ b/crates/oxide_vm/src/async_generator.rs @@ -878,35 +878,27 @@ pub(crate) fn async_generator_holds_suspended_for_in(obj: &JsObject) -> bool { !state.suspended.for_in_iters.is_empty() } -/// 异步生成器状态内所有对象引用的扁平列表(GC mark 边)。 +/// 异步生成器状态内全部引用边的扁平列表(GC mark 边):对象/字符串/BigInt +/// 均产出,消费侧按值类型分发到对象栈与存活集。 pub(crate) fn async_generator_native_edges(obj: &JsObject) -> Vec { let Some(state) = async_gen_state_mut(obj) else { return Vec::new(); }; let mut edges = Vec::new(); - let push = |v: JsValue, edges: &mut Vec| { - if v.is_object() { - edges.push(v); - } - }; - push(state.callee, &mut edges); - edges.extend(state.args.iter().copied().filter(|v| v.is_object())); - push(state.result, &mut edges); + edges.push(state.callee); + edges.extend(state.args.iter().copied()); + edges.push(state.result); for req in state.queue.iter() { - push(req.promise, &mut edges); - push(req.resolve, &mut edges); - push(req.reject, &mut edges); + edges.push(req.promise); + edges.push(req.resolve); + edges.push(req.reject); } if let Some(req) = &state.current { - push(req.promise, &mut edges); - push(req.resolve, &mut edges); - push(req.reject, &mut edges); + edges.push(req.promise); + edges.push(req.resolve); + edges.push(req.reject); } - state.suspended.for_each_value(|v| { - if v.is_object() { - edges.push(v); - } - }); + state.suspended.for_each_value(|v| edges.push(v)); edges } diff --git a/crates/oxide_vm/src/generator.rs b/crates/oxide_vm/src/generator.rs index 2dd1dc27..03d617b4 100644 --- a/crates/oxide_vm/src/generator.rs +++ b/crates/oxide_vm/src/generator.rs @@ -873,25 +873,17 @@ pub(crate) fn generator_holds_suspended_for_in(obj: &JsObject) -> bool { !state.suspended.for_in_iters.is_empty() } -/// 生成器状态内所有对象引用的扁平列表(GC mark 边)。 +/// 生成器状态内全部引用边的扁平列表(GC mark 边):对象/字符串/BigInt 均产出, +/// 消费侧按值类型分发到对象栈与存活集。 pub(crate) fn generator_native_edges(obj: &JsObject) -> Vec { let Some(state) = generator_state_mut(obj) else { return Vec::new(); }; let mut edges = Vec::new(); - let push = |v: JsValue, edges: &mut Vec| { - if v.is_object() { - edges.push(v); - } - }; - push(state.callee, &mut edges); - edges.extend(state.args.iter().copied().filter(|v| v.is_object())); - push(state.result, &mut edges); - state.suspended.for_each_value(|v| { - if v.is_object() { - edges.push(v); - } - }); + edges.push(state.callee); + edges.extend(state.args.iter().copied()); + edges.push(state.result); + state.suspended.for_each_value(|v| edges.push(v)); edges } diff --git a/crates/oxide_vm/src/promise.rs b/crates/oxide_vm/src/promise.rs index 6d781ea2..26b38d52 100644 --- a/crates/oxide_vm/src/promise.rs +++ b/crates/oxide_vm/src/promise.rs @@ -1811,30 +1811,26 @@ fn promise_state_mut(obj: &JsObject) -> Option<&mut PromiseState> { } } -/// Promise 状态盒内所有对象引用(GC mark 边)。 +/// Promise 状态盒内全部值边(GC mark 边):对象/字符串/BigInt 均产出, +/// 消费侧按值类型分发到对象栈与存活集。 pub(crate) fn promise_native_edges(obj: &JsObject) -> Vec { let Some(state) = promise_state_ref(obj) else { return Vec::new(); }; let mut edges = Vec::new(); - let push = |v: JsValue, edges: &mut Vec| { - if v.is_object() { - edges.push(v); - } - }; - push(state.result, &mut edges); - push(state.resolve_fn, &mut edges); - push(state.reject_fn, &mut edges); + edges.push(state.result); + edges.push(state.resolve_fn); + edges.push(state.reject_fn); // 结算链指针:克隆恒为 session 表成员,凭这条边在原件/旧克隆仍存活时被 // 同轮置活,结算传导不会读到悬垂克隆。 if !state.promoted_clone.is_null() { edges.push(JsValue::from_js_object(state.promoted_clone)); } for r in &state.reactions { - push(r.promise, &mut edges); - push(r.resolve, &mut edges); - push(r.reject, &mut edges); - push(r.handler, &mut edges); + edges.push(r.promise); + edges.push(r.resolve); + edges.push(r.reject); + edges.push(r.handler); } edges } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 288a0905..7000de89 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -1180,7 +1180,7 @@ mod tests { use super::*; use crate::vm::{CallFrame, FrameContinuation}; - use oxide_builtins::{array_buffer, data_view, map, set, typed_array}; + use oxide_builtins::{array_buffer, data_view, disposable_stack, map, set, typed_array}; use oxide_runtime_api::NativeResult; fn plain_object(vm: &mut Vm) -> *mut JsObject { @@ -1499,6 +1499,37 @@ mod tests { val } + /// 分配一个原型指向 DisposableStack.prototype 的占位对象并写入寄存器, + /// 作为构造器调用的 `this`。 + fn dispose_stack_this(vm: &mut Vm, reg: u8) -> JsValue { + let proto = vm.session.builtin_world().disposable_stack_proto.as_ptr() as *mut JsObject; + let obj = vm.epoch.alloc(JsObject::new_empty( + oxide_kernel::shape_forge::EMPTY_SHAPE_ID, + JsValue::from_js_object(proto), + )); + // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 + unsafe { (*obj).set_is_epoch(true) }; + let val = JsValue::from_js_object(obj); + vm.regs[reg as usize] = val; + val + } + + /// 置函数位标志的占位对象(通过 `is_callable` 判定),供 adopt 的 + /// onDispose 槽使用;测试不调用它。 + fn function_placeholder(vm: &mut Vm) -> JsValue { + let proto = vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject; + let obj = vm.epoch.alloc(JsObject::new_empty( + oxide_kernel::shape_forge::EMPTY_SHAPE_ID, + JsValue::from_js_object(proto), + )); + // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 + unsafe { + (*obj).set_is_epoch(true); + (*obj).set_function(true); + } + JsValue::from_js_object(obj) + } + #[test] fn reset_maybe_collect_collects_after_threshold() { let mut vm = vm_with_low_threshold(); @@ -1624,6 +1655,92 @@ mod tests { assert_eq!(vm.gc_state.session_object_ptrs.len(), 2); } + /// 盒持唯一引用的 session 串与 BigInt 经 Map native 边进入存活集: + /// 清寄存器仅留 Map 根后完整收集,两值仍登记在 session 表。 + #[test] + fn session_gc_traces_map_string_and_bigint_value() { + let mut vm = vm_with_low_threshold(); + map_this(&mut vm, 3); + let map_value = native_ok(map::map_constructor(&mut vm, &[3])); + let str = vm.new_string("map-box-string"); + let str_ptr = str.as_string_ptr_mut(); + let bi = vm.new_bigint(num_bigint::BigInt::from(121932631112635269u128)); + let bi_ptr = bi.as_bigint_ptr() as *mut num_bigint::BigInt; + vm.regs[0] = map_value; + vm.regs[1] = JsValue::int(1); + vm.regs[2] = str; + native_ok(map::map_set(&mut vm, &[0, 1, 2])); + vm.regs[1] = JsValue::int(2); + vm.regs[2] = bi; + native_ok(map::map_set(&mut vm, &[0, 1, 2])); + + let map_session = vm.promote_object(map_value.as_js_object_ptr()); + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(map_session); + collect(&mut vm); + + // 只断言表成员——sweep 已释放的指针解引用即 UB。 + assert!(vm.gc_state.session_string_ptrs.contains(&str_ptr)); + assert!(vm.gc_state.session_bigint_ptrs.borrow().contains(&bi_ptr)); + } + + /// 盒持唯一引用的 session 串与 BigInt 经 Set native 边进入存活集: + /// 清寄存器仅留 Set 根后完整收集,两值仍登记在 session 表。 + #[test] + fn session_gc_traces_set_string_and_bigint_value() { + let mut vm = vm_with_low_threshold(); + set_this(&mut vm, 2); + let set_value = native_ok(set::set_constructor(&mut vm, &[2])); + let str = vm.new_string("set-box-string"); + let str_ptr = str.as_string_ptr_mut(); + let bi = vm.new_bigint(num_bigint::BigInt::from(987654321987654321u128)); + let bi_ptr = bi.as_bigint_ptr() as *mut num_bigint::BigInt; + vm.regs[0] = set_value; + vm.regs[1] = str; + native_ok(set::set_add(&mut vm, &[0, 1])); + vm.regs[1] = bi; + native_ok(set::set_add(&mut vm, &[0, 1])); + + let set_session = vm.promote_object(set_value.as_js_object_ptr()); + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(set_session); + collect(&mut vm); + + // 只断言表成员——sweep 已释放的指针解引用即 UB。 + assert!(vm.gc_state.session_string_ptrs.contains(&str_ptr)); + assert!(vm.gc_state.session_bigint_ptrs.borrow().contains(&bi_ptr)); + } + + /// 盒持唯一引用的 session 串与 BigInt 经资源栈条目边进入存活集: + /// adopt 入栈后清寄存器仅留栈根,完整收集后两值仍登记在 session 表。 + #[test] + fn session_gc_traces_dispose_stack_string_and_bigint_value() { + let mut vm = vm_with_low_threshold(); + dispose_stack_this(&mut vm, 3); + let stack_value = native_ok(disposable_stack::disposable_stack_constructor(&mut vm, &[3])); + let on_dispose = function_placeholder(&mut vm); + + let str = vm.new_string("stack-box-string"); + let str_ptr = str.as_string_ptr_mut(); + let bi = vm.new_bigint(num_bigint::BigInt::from(4611686018427387904u128)); + let bi_ptr = bi.as_bigint_ptr() as *mut num_bigint::BigInt; + vm.regs[0] = stack_value; + vm.regs[1] = str; + vm.regs[2] = on_dispose; + native_ok(disposable_stack::disposable_stack_adopt(&mut vm, &[0, 1, 2])); + vm.regs[1] = bi; + native_ok(disposable_stack::disposable_stack_adopt(&mut vm, &[0, 1, 2])); + + let stack_session = vm.promote_object(stack_value.as_js_object_ptr()); + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(stack_session); + collect(&mut vm); + + // 只断言表成员——sweep 已释放的指针解引用即 UB。 + assert!(vm.gc_state.session_string_ptrs.contains(&str_ptr)); + assert!(vm.gc_state.session_bigint_ptrs.borrow().contains(&bi_ptr)); + } + #[test] fn session_gc_keeps_shared_array_buffer_alive_through_view_native_edges() { let mut vm = vm_with_low_threshold(); diff --git a/crates/oxide_vm/tests/gc_runtime_tests.rs b/crates/oxide_vm/tests/gc_runtime_tests.rs index 8afb05b2..886fe677 100644 --- a/crates/oxide_vm/tests/gc_runtime_tests.rs +++ b/crates/oxide_vm/tests/gc_runtime_tests.rs @@ -335,3 +335,189 @@ fn dead_closure_upvalues_freed_by_sweep_without_double_free() { freed_with_captures - freed_no_captures ); } + +// ── 原生盒内字符串/BigInt 边存活(mark 边收集去对象预过滤) ───────────────── + +/// 盒持唯一引用 session 串(Promise 结算值):churn 窗口内执行期收集 +/// 仅能经状态盒边闭合存活,结算交付后读回精确值。churn 串与盒串同长: +/// 若 box 释放后其内存被同长分配复用,读回即错值。 +#[test] +fn promise_result_string_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let first = compile( + "(function(){ \ + var s = 'prombox'.repeat(8); \ + var p = Promise.resolve(s); \ + p.then(function(v){ globalThis.got = v; }); \ + globalThis.p = p; })(); \ + for (var i = 0; i < 2000; i++) { var t = 'z'.repeat(56); } \ + 0", + ); + vm.run(&Arc::new(first)).expect("run1"); + + let second = compile("globalThis.got"); + let result = vm.run(&Arc::new(second)).expect("run2"); + let text = vm.lookup_str(result).expect("got 应为字符串").to_string(); + + assert_eq!(text, "prombox".repeat(8)); + assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期字符串 GC"); +} + +/// 盒持唯一引用 session BigInt(Promise 结算值,运行时乘积非常量池字面量): +/// 同窗口存活闭合经 live_bigints,结算交付后读回精确值。 +#[test] +fn promise_result_bigint_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let first = compile( + "(function(){ \ + var b = 987654321n * 123456789n; \ + var p = Promise.resolve(b); \ + p.then(function(v){ globalThis.got = v; }); \ + globalThis.p = p; })(); \ + for (var i = 0; i < 2000; i++) { var t = 'churn' + i; } \ + 0", + ); + vm.run(&Arc::new(first)).expect("run1"); + + let second = compile("globalThis.got"); + let result = vm.run(&Arc::new(second)).expect("run2"); + + let expected = num_bigint::BigInt::from(987654321u64) * num_bigint::BigInt::from(123456789u64); + assert!(result.is_bigint(), "got 应为 BigInt"); + assert_eq!(vm.bigint_value(result), &expected); + assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期 GC"); +} + +/// 挂起生成器帧寄存器持唯一引用 session BigInt:yield 挂起后仅状态盒 +/// 可达,churn 窗口收集闭合存活,恢复读回精确值。 +#[test] +fn suspended_generator_bigint_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let module = compile( + "(function(){ \ + function* gen() { var b = 987654321n * 123456789n; yield 1; return b; } \ + var g = gen(); g.next(); globalThis.g = g; })(); \ + for (var i = 0; i < 2000; i++) { var t = 'churn' + i; } \ + globalThis.g.next().value", + ); + let result = vm.run(&Arc::new(module)).expect("run"); + + let expected = num_bigint::BigInt::from(987654321u64) * num_bigint::BigInt::from(123456789u64); + assert!(result.is_bigint(), "恢复返回值应为 BigInt"); + assert_eq!(vm.bigint_value(result), &expected); + assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期 GC"); +} + +/// 挂起异步函数帧持唯一引用 session BigInt:await 挂起后仅状态盒可达, +/// 微任务恢复结算后读回精确值。 +#[test] +fn suspended_async_function_bigint_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let first = compile( + "(function(){ \ + async function f() { var b = 987654321n * 123456789n; await 0; return b; } \ + var p = f(); \ + p.then(function(v){ globalThis.got = v; }); \ + globalThis.p = p; })(); \ + for (var i = 0; i < 2000; i++) { var t = 'churn' + i; } \ + 0", + ); + vm.run(&Arc::new(first)).expect("run1"); + + let second = compile("globalThis.got"); + let result = vm.run(&Arc::new(second)).expect("run2"); + + let expected = num_bigint::BigInt::from(987654321u64) * num_bigint::BigInt::from(123456789u64); + assert!(result.is_bigint(), "got 应为 BigInt"); + assert_eq!(vm.bigint_value(result), &expected); + assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期 GC"); +} + +/// 挂起异步生成器帧持唯一引用 session BigInt:首次 next 挂起后仅状态盒 +/// 可达,挂起期 churn 窗口的收集闭合存活,同 run 内恢复并交付精确值。 +#[test] +fn suspended_async_generator_bigint_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let first = compile( + "async function* gen() { var b = 987654321n * 123456789n; yield 1; return b; } \ + async function run() { \ + var it = gen(); globalThis.g = it; \ + var p1 = it.next(); \ + for (var i = 0; i < 2000; i++) { var t = 'churn' + i; } \ + await p1; \ + return (await it.next()).value; } \ + run().then(function(v){ globalThis.got = v; })", + ); + vm.run(&Arc::new(first)).expect("run1"); + + let second = compile("globalThis.got"); + let result = vm.run(&Arc::new(second)).expect("run2"); + + let expected = num_bigint::BigInt::from(987654321u64) * num_bigint::BigInt::from(123456789u64); + assert!(result.is_bigint(), "got 应为 BigInt"); + assert_eq!(vm.bigint_value(result), &expected); + assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期 GC"); +} + +/// 盒持唯一引用 session 串(Map 值):churn 窗口内收集经盒边闭合存活, +/// get 读回精确值。churn 串与盒串同长:若 box 释放后其内存被同长 +/// 分配复用,读回即错值。 +#[test] +fn map_value_string_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let module = compile( + "(function(){ \ + var s = 'mapbox'.repeat(8); \ + var m = new Map(); m.set('k', s); globalThis.m = m; })(); \ + for (var i = 0; i < 2000; i++) { var t = 'z'.repeat(48); } \ + globalThis.m.get('k')", + ); + let result = vm.run(&Arc::new(module)).expect("run"); + let text = vm.lookup_str(result).expect("map 值应为字符串").to_string(); + + assert_eq!(text, "mapbox".repeat(8)); + assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期字符串 GC"); +} + +/// 盒持唯一引用 session 串(Set 元素):churn 窗口内收集经盒边闭合存活, +/// values 迭代读回精确值。churn 串与盒串同长:若 box 释放后其内存被 +/// 同长分配复用,读回即错值。 +#[test] +fn set_value_string_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let module = compile( + "(function(){ \ + var s = 'setbox'.repeat(8); \ + var st = new Set(); st.add(s); globalThis.st = st; })(); \ + for (var i = 0; i < 2000; i++) { var t = 'z'.repeat(48); } \ + globalThis.st.values().next().value", + ); + let result = vm.run(&Arc::new(module)).expect("run"); + let text = vm.lookup_str(result).expect("set 元素应为字符串").to_string(); + + assert_eq!(text, "setbox".repeat(8)); + assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期字符串 GC"); +} + +/// 盒持唯一引用 session 串(DisposableStack adopt 值):churn 窗口内收集 +/// 经条目边闭合存活,dispose 回调交付后读回精确值。churn 串与盒串同长: +/// 若 box 释放后其内存被同长分配复用,读回即错值。 +#[test] +fn disposable_stack_value_string_survives_runtime_gc() { + let mut vm = vm_with_threshold(512); + let module = compile( + "(function(){ \ + var s = 'stackbox'.repeat(8); \ + var st = new DisposableStack(); \ + st.adopt(s, function(v){ globalThis.got = v; }); \ + globalThis.st = st; })(); \ + for (var i = 0; i < 2000; i++) { var t = 'z'.repeat(64); } \ + globalThis.st.dispose(); \ + globalThis.got", + ); + let result = vm.run(&Arc::new(module)).expect("run"); + let text = vm.lookup_str(result).expect("adopt 值应为字符串").to_string(); + + assert_eq!(text, "stackbox".repeat(8)); + assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期字符串 GC"); +}