diff --git a/crates/oxide_vm/src/promise.rs b/crates/oxide_vm/src/promise.rs index 38a2e791..6d781ea2 100644 --- a/crates/oxide_vm/src/promise.rs +++ b/crates/oxide_vm/src/promise.rs @@ -46,8 +46,11 @@ pub(crate) struct PromiseState { /// resolve/reject 是否已被调用过(Resolve Promise Functions 的 alreadyResolved)。 /// 首次调用后置位,后续任何 resolve/reject(含 thenable 委托期间)均 no-op。 pub already_resolved: bool, - /// 原件 pending 时被晋升出的 session 克隆指针(原件结算时传导到克隆); - /// 非 pending 原件恒为空指针。 + /// 结算传导链指针:非空时指向本 promise 晋升出的 session 克隆,原件结算时 + /// 结果沿它传导到克隆;再晋升场景旧克隆的残留反应被取回归并进新克隆, + /// 指针更新为最新克隆。非 pending 原件恒为空指针。本指针在 + /// `promise_native_edges` 登记为 mark 边:克隆恒为 session 表成员,凭这条 + /// 边在原件仍存活时被同轮置活,结算不会读到悬垂克隆。 pub promoted_clone: *mut JsObject, } @@ -1822,6 +1825,11 @@ pub(crate) fn promise_native_edges(obj: &JsObject) -> Vec { push(state.result, &mut edges); push(state.resolve_fn, &mut edges); push(state.reject_fn, &mut edges); + // 结算链指针:克隆恒为 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); @@ -1832,6 +1840,10 @@ pub(crate) fn promise_native_edges(obj: &JsObject) -> Vec { } /// 用转发函数重写状态盒中的所有 JsValue(session GC 移动式清扫 / promote 用)。 +/// +/// 结算链指针是裸指针边、不在本函数改写面:晋升路径克隆新分配于 session +/// arena(原件地址不变),同 arena 改写场景指针天然有效;搬移换址场景由 +/// 调用方在重写完成后经 `repoint_promise_promoted_clone` 按转发表重定位。 pub(crate) fn rewrite_promise_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) { let Some(state) = promise_state_mut(obj) else { return; @@ -1848,31 +1860,116 @@ pub(crate) fn rewrite_promise_native(obj: &JsObject, mut rewrite: impl FnMut(JsV } /// 深拷贝状态盒到新对象(promote / sweep 用):新对象持独立 Box,源盒可安全释放。 +/// +/// 源的反应**迁移**(非复制)进新对象:源盒随 epoch 释放,留在源上的反应会 +/// 永久丢失、原件侧消费者永不触发;每条反应的 JsValue 随状态盒其余字段同一 +/// pass 晋升/改写。结算链指针按原值保留(晋升场景由 +/// `migrate_settlement_to_newest_clone` 接链,搬移场景按转发表重定位)。 pub(crate) fn clone_promise_native_with_rewrite( old: &JsObject, new: &mut JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue, ) { - let Some(state) = promise_state_ref(old) else { - return; + // 先取全部 Copy 字段,避免与随后的可变 take 别名校验冲突。 + let (state_kind, prev_clone, result, resolve_fn, reject_fn, already_resolved) = { + let Some(state) = promise_state_ref(old) else { + return; + }; + ( + state.state, + state.promoted_clone, + state.result, + state.resolve_fn, + state.reject_fn, + state.already_resolved, + ) }; - let pending = state.state == PromiseStateKind::Pending; + + // 源反应迁入新对象:源盒随 epoch 释放,反应留源即永久丢失。 + let mut reactions: Vec = Vec::new(); + if let Some(src) = promise_state_mut(old) { + for r in std::mem::take(&mut src.reactions) { + reactions.push(rewrite_reaction(r, &mut rewrite)); + } + } + let cloned = PromiseState { - state: state.state, - result: rewrite(state.result), - // 不复制原件反应:原件结算时自带触发,克隆仅累积晋升后新挂的反应,避免双重触发。 - reactions: Vec::new(), - resolve_fn: rewrite(state.resolve_fn), - reject_fn: rewrite(state.reject_fn), - already_resolved: state.already_resolved, - promoted_clone: std::ptr::null_mut(), + state: state_kind, + result: rewrite(result), + reactions, + resolve_fn: rewrite(resolve_fn), + reject_fn: rewrite(reject_fn), + already_resolved, + // 结算链指针按原值保留(不改写源指针):晋升后继步 + // `migrate_settlement_to_newest_clone` 负责接链与更新源指针, + // 搬移后继步按转发表重定位。 + promoted_clone: prev_clone, }; new.set_native_data(Box::into_raw(Box::new(cloned)) as *mut u8); - // 原件 pending 时记下克隆指针:原件结算时把结果传导到克隆,克隆上晋升后新挂 - // 的反应方随之触发(顶层 var 读侧引用克隆,原件随 epoch 释放,结算须在原件还活着时传导)。 - if pending { - if let Some(src) = promise_state_mut(old) { - src.promoted_clone = new as *mut JsObject; +} + +/// 晋升路径专属:新克隆取代前一克隆成为结算枢纽——前一克隆的残留反应取回归并 +/// 进新克隆,旧克隆的结算改链到新克隆,原件的传导指针指向新克隆。任一结算 +/// 入口(原件 / 旧克隆 / 新克隆)都沿链落到携全部反应的最新克隆,反应恰触发 +/// 一次。 +/// +/// # 边界与前提 +/// - 仅 epoch 原件晋升后调用(`promote_object_inner` 的 Promise 分支,紧随 +/// `clone_promise_native_with_rewrite`);session 搬移(GC sweep)不改结算 +/// 拓扑,不得调用。 +/// - 源非 pending 时无事可做直接返回(已结算原件不再传导)。 +pub(crate) fn migrate_settlement_to_newest_clone( + old: &JsObject, new: &mut JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue, +) { + // 链上的前克隆 = 源当前指向的克隆(克隆步只保留不改写源指针)。 + let (pending, prev) = { + let Some(src) = promise_state_ref(old) else { + return; + }; + (src.state == PromiseStateKind::Pending, src.promoted_clone) + }; + if !pending { + return; + } + // 前克隆残留并入本克隆(旧克隆腾空,防双触发),并把它的结算改链到新克隆。 + if !prev.is_null() { + // SAFETY: prev 是早前晋升产生的 session 克隆,session arena 存活期内有效。 + if let Some(prev_state) = promise_state_mut(unsafe { &*prev }) { + if let Some(dst) = promise_state_mut(new) { + for r in std::mem::take(&mut prev_state.reactions) { + dst.reactions.push(rewrite_reaction(r, &mut rewrite)); + } + } + prev_state.promoted_clone = new as *mut JsObject; } } + // 新克隆即最新:自身无后继,原件传导指针指向它。 + if let Some(dst) = promise_state_mut(new) { + dst.promoted_clone = std::ptr::null_mut(); + } + if let Some(src) = promise_state_mut(old) { + src.promoted_clone = new as *mut JsObject; + } +} + +/// 按给定转发改写结算链指针(裸指针边、非 JsValue),session GC 搬移后调用。 +pub(crate) fn repoint_promise_promoted_clone(obj: &JsObject, forward: impl FnOnce(*mut JsObject) -> *mut JsObject) { + let Some(state) = promise_state_mut(obj) else { + return; + }; + if !state.promoted_clone.is_null() { + state.promoted_clone = forward(state.promoted_clone); + } +} + +/// 改写单条反应的四条 JsValue 引用(派生 promise 能力与处理器),源反应迁移 +/// 与再晋升合并共用。 +fn rewrite_reaction JsValue>(r: PromiseReaction, rewrite: &mut F) -> PromiseReaction { + PromiseReaction { + promise: rewrite(r.promise), + resolve: rewrite(r.resolve), + reject: rewrite(r.reject), + handler: rewrite(r.handler), + is_fulfill: r.is_fulfill, + } } /// 只读核算 Promise 状态盒字节(不释放),供 GC 账目核算。 diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index bd3cadcc..9482c877 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -78,10 +78,14 @@ impl Vm { self.promote_value_if_epoch_object(value, forwarding) }); } else if src_ref.is_promise_obj() { - // Promise 状态盒深拷贝到新对象:源盒随 epoch 释放,互不共享。 + // Promise 状态盒深拷贝到新对象:源盒随 epoch 释放,互不共享; + // 反应迁入新克隆并接结算链(原件/旧克隆结算沿链传导到最新克隆)。 crate::promise::clone_promise_native_with_rewrite(src_ref, dst_ref, |value| { self.promote_value_if_epoch_object(value, forwarding) }); + crate::promise::migrate_settlement_to_newest_clone(src_ref, dst_ref, |value| { + self.promote_value_if_epoch_object(value, forwarding) + }); } else if src_ref.is_async_obj() { // 异步状态盒深拷贝到新对象:源盒随 epoch 释放,互不共享。 crate::async_func::clone_async_native_with_rewrite(src_ref, dst_ref, |value| { diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 6c6c72b5..288a0905 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -624,6 +624,11 @@ impl SessionGc { crate::generator::rewrite_generator_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); } else if obj.is_promise_obj() { crate::promise::rewrite_promise_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); + // 结算链指针是裸指针边(非 JsValue):搬移换址后按转发表重定位, + // 旧克隆的结算传导仍指向后继克隆的新址。 + crate::promise::repoint_promise_promoted_clone(obj, |ptr| { + rewrite_forwarded_value(JsValue::from_js_object(ptr), &forwarding).as_js_object_ptr() + }); } else if obj.is_async_obj() { crate::async_func::rewrite_async_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); } else if obj.is_async_generator_obj() { @@ -2335,4 +2340,85 @@ mod tests { "元素值跨收集可读" ); } + + // ── Promise 晋升结算传导(promise × promote × settle 组合) ────────────── + + /// 结算矩阵钉:结算走原件(barrier 窗口,闭包仍指 epoch 原件)、消费者 + /// 注册在晋升后的克隆上。 + /// + /// 原件结算沿结算链传导到最新克隆,克隆侧(晋升后登记)的反应随之触发, + /// 回归保传导通路。 + #[test] + fn promise_settle_via_original_propagates_to_clone_reaction() { + let mut vm = vm_with_threshold(65536); + vm.run(&Arc::new(compile( + "(function(){ \ + var r; \ + var p = new Promise(function(res){ r = res; }); \ + globalThis.res = r; \ + globalThis.p = p; \ + globalThis.p.then(function(v){ globalThis.got = v; }); \ + r(42); })(); 0", + ))) + .expect("run1"); + + assert_eq!( + global_prop_opt(&vm, "got"), + Some(JsValue::int(42)), + "原件结算应传导到克隆并触发克隆侧消费者" + ); + } + + /// 结算矩阵钉:登记侧原件晋升前、结算走改写后的克隆引用(本洞主钉)。 + /// + /// 单 run 内完成全拓扑:执行器把结算闭包写向 global 时逃逸写屏障连带晋升 + /// 目标 promise(克隆一);`.then` 登记在 epoch 原件上;原件再写向 global + /// 触发再晋升——反应迁入最新克隆、旧克隆接进结算链。IIFE 返回后的顶层 + /// 指令边界触发执行期收集(阈值 1),原件随 epoch 释放;随后结算入口是 + /// 克隆一(闭包目标槽在晋升时已改写到它),反应在最新克隆上——结算须沿 + /// 链触达全部迁移反应,消费者(同 run 末微任务触发,字节码子模块表仍有效) + /// 方能落值。修复前克隆反应为空、原件已释放,反应静默永久丢失。 + #[test] + fn promise_promote_migrates_pre_promote_reactions_to_clone() { + let mut vm = vm_with_threshold(1); + vm.run(&Arc::new(compile( + "(function(){ \ + var p = new Promise(function(res){ globalThis.res = res; }); \ + p.then(function(v){ globalThis.got = v; }); \ + globalThis.p = p; })(); \ + globalThis.res(42); 0", + ))) + .expect("run1"); + + assert!(vm.session_gc_stats().total_collections >= 1, "执行期收集应已触发(原件随 epoch 释放)"); + assert_eq!( + global_prop_opt(&vm, "got"), + Some(JsValue::int(42)), + "晋升前登记的消费者应在结算走克隆后被触发" + ); + } + + /// 结算矩阵钉:reject 变体——登记侧原件晋升前、拒绝走改写后的克隆引用。 + /// + /// 与完成路径同拓扑(单 run、同 run 末微任务触发):反应随晋升迁入,拒绝 + /// 走克隆沿结算链传导,仅 reject 角色的反应以拒绝原因触发。 + #[test] + fn promise_promote_migrates_reject_reactions_to_clone() { + let mut vm = vm_with_threshold(1); + vm.run(&Arc::new(compile( + "(function(){ \ + var p = new Promise(function(_res, rej){ globalThis.rej = rej; }); \ + p.then(undefined, function(e){ globalThis.got = e; }); \ + globalThis.p = p; })(); \ + globalThis.rej('boom'); 0", + ))) + .expect("run1"); + + assert!(vm.session_gc_stats().total_collections >= 1, "执行期收集应已触发(原件随 epoch 释放)"); + assert_eq!( + vm.lookup_str(global_prop_opt(&vm, "got").expect("got 应被设置")), + Some("boom".to_string()), + "晋升前登记的拒绝消费者应在拒绝走克隆后被触发" + ); + } }