diff --git a/crates/oxide_vm/src/promise.rs b/crates/oxide_vm/src/promise.rs index 77641c9..87e31e6 100644 --- a/crates/oxide_vm/src/promise.rs +++ b/crates/oxide_vm/src/promise.rs @@ -43,6 +43,9 @@ pub(crate) struct PromiseState { /// 本 promise 能力上的 resolve/reject 闭包(thenable 委托入队时取用)。 pub resolve_fn: JsValue, pub reject_fn: JsValue, + /// resolve/reject 是否已被调用过(Resolve Promise Functions 的 alreadyResolved)。 + /// 首次调用后置位,后续任何 resolve/reject(含 thenable 委托期间)均 no-op。 + pub already_resolved: bool, } /// 一条微任务(job)。 @@ -69,6 +72,8 @@ const MAX_DRAIN_JOBS: usize = 1_000_000; /// 闭包函数对象上存目标 promise 的属性名。 const PROMISE_PROP: &str = "__oxide_promise__"; +/// thenable 委托结算代理标志:绕过 alreadyResolved(委托是真正结算路径)。 +const DELEGATED_PROP: &str = "__oxide_delegated__"; /// finally 处理器上存 onFinally 回调的属性名。 const ON_FINALLY_PROP: &str = "__oxide_on_finally__"; /// finally 处理器上区分 reject 角色的属性名。 @@ -115,6 +120,7 @@ impl Vm { reactions: Vec::new(), resolve_fn: JsValue::undefined(), reject_fn: JsValue::undefined(), + already_resolved: false, }); obj.set_native_data(Box::into_raw(state) as *mut u8); JsValue::from_js_object(ptr) @@ -124,8 +130,8 @@ impl Vm { /// 目标 promise 的 native 闭包,同时写入状态盒供 thenable 委托取用。 pub(crate) fn new_promise_capability(&mut self) -> (JsValue, JsValue, JsValue) { let promise = self.create_promise_object(); - let resolve = self.make_resolve_reject_fn(promise, false); - let reject = self.make_resolve_reject_fn(promise, true); + let resolve = self.make_resolve_reject_fn(promise, false, false); + let reject = self.make_resolve_reject_fn(promise, true, false); let state = self.promise_state_ptr(promise); let state = unsafe { &mut *state }; state.resolve_fn = resolve; @@ -211,7 +217,10 @@ impl Vm { } /// 构造携带目标 promise 的 native 闭包(resolve 或 reject 角色)。 - fn make_resolve_reject_fn(&mut self, promise: JsValue, reject_role: bool) -> JsValue { + /// `delegated` 为 true 表示 thenable 委托用的结算代理:绕过 alreadyResolved + /// (委托是唯一真正的结算路径,由 then 的 resolve/reject 触发),但 state != Pending + /// 仍阻止二次结算。 + fn make_resolve_reject_fn(&mut self, promise: JsValue, reject_role: bool, delegated: bool) -> JsValue { let native_fn: NativeFn = if reject_role { promise_reject_closure } else { promise_resolve_closure }; let fn_proto = self.session.builtin_world().fn_proto_val(); let mut func = JsObject::new_empty(EMPTY_SHAPE_ID, fn_proto); @@ -223,6 +232,8 @@ impl Vm { let obj = unsafe { &mut *ptr }; let si = self.kernel_core.perm_interner().intern(PROMISE_PROP).0; self.set_or_create_prop_value(obj, si, promise); + let dsi = self.kernel_core.perm_interner().intern(DELEGATED_PROP).0; + self.set_or_create_prop_value(obj, dsi, JsValue::bool(delegated)); self.add_fn_name_length(obj, "", 1); JsValue::from_js_object(ptr) } @@ -241,6 +252,17 @@ impl Vm { } } + /// 当前 resolve/reject 闭包是否为 thenable 委托结算代理(绕过 alreadyResolved)。 + fn callee_delegated_flag(&self) -> bool { + let callee = self.regs[254]; + if !callee.is_object() { + return false; + } + let obj = unsafe { &*callee.as_js_object_ptr() }; + let si = self.kernel_core.perm_interner().intern(DELEGATED_PROP).0; + self.resolve_property(obj, si).map(|v| v.is_bool() && v.as_bool()).unwrap_or(false) + } + /// 取出 Promise 状态盒指针(调用方须先校验 `is_promise_value`)。 fn promise_state_ptr(&self, promise: JsValue) -> *mut PromiseState { unsafe { (*promise.as_js_object_ptr()).native_data() as *mut PromiseState } @@ -289,6 +311,9 @@ impl Vm { /// - 可能入队 Thenable 微任务,或直接 settle 目标 promise。 /// - then getter 抛错时拒绝目标 promise(错误在内部消化,调用方无需处理)。 pub(crate) fn resolve_promise(&mut self, promise: JsValue, x: JsValue) -> Result<(), String> { + // alreadyResolved 守卫在 resolve 闭包层(首次调用置位,含 thenable 委托期间)。 + // 本函数自身不检查:委托闭包调用它时须放行(委托才是真正结算路径); + // 二次结算由 fulfill/reject 的 state != Pending 守卫兜底。 if oxide_runtime_api::same_value(x, promise) { let err = oxide_builtins::error::create_type_error(self, "Chaining cycle detected for promise"); return self.reject_promise(promise, err); @@ -307,10 +332,14 @@ impl Vm { } }; if oxide_builtins::iterator::is_callable(then) { + // thenable 委托期间本 promise 仍未 settle,但 already_resolved 已置位, + // 后续 executor 的 resolve/reject 均 no-op。委托用独立结算代理闭包 + // (绕过 alreadyResolved——委托才是真正结算路径),仍受 state != Pending 守卫。 let state = self.promise_state_ptr(promise); - let state = unsafe { &*state }; - let resolve = state.resolve_fn; - let reject = state.reject_fn; + let state = unsafe { &mut *state }; + state.already_resolved = true; + let resolve = self.make_resolve_reject_fn(promise, false, true); + let reject = self.make_resolve_reject_fn(promise, true, true); self.enqueue(Microtask::Thenable { thenable: x, then, @@ -662,14 +691,15 @@ fn promise_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } let obj = unsafe { &mut *this_val.as_js_object_ptr() }; obj.type_tag = JsObject::OBJ_TYPE_PROMISE; - let resolve = vm.make_resolve_reject_fn(this_val, false); - let reject = vm.make_resolve_reject_fn(this_val, true); + let resolve = vm.make_resolve_reject_fn(this_val, false, false); + let reject = vm.make_resolve_reject_fn(this_val, true, false); let state = Box::new(PromiseState { state: PromiseStateKind::Pending, result: JsValue::undefined(), reactions: Vec::new(), resolve_fn: resolve, reject_fn: reject, + already_resolved: false, }); obj.set_native_data(Box::into_raw(state) as *mut u8); let executor = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; @@ -691,11 +721,23 @@ fn promise_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } /// resolve 闭包:`resolve(x)` 对目标 promise 执行 PromiseResolve。 +/// 非委托闭包入口检查 alreadyResolved(首次调用置位,后续 no-op); +/// 委托闭包(thenable 的 resolve)绕过该检查直接结算。 fn promise_resolve_closure(vm: &mut Vm, args: &[u8]) -> NativeResult { let Some(promise) = vm.promise_from_callee() else { return NativeResult::Err(oxide_builtins::error::create_type_error(vm, "resolve function is invalid")); }; + let delegated = vm.callee_delegated_flag(); let x = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if !delegated { + // 非委托:alreadyResolved 守卫——首次调用置位,含 thenable 委托期间。 + let state = vm.promise_state_ptr(promise); + let state = unsafe { &mut *state }; + if state.already_resolved { + return NativeResult::Ok(JsValue::undefined()); + } + state.already_resolved = true; + } if let Err(e) = vm.resolve_promise(promise, x) { let exc = vm .last_uncaught_value @@ -711,7 +753,16 @@ fn promise_reject_closure(vm: &mut Vm, args: &[u8]) -> NativeResult { let Some(promise) = vm.promise_from_callee() else { return NativeResult::Err(oxide_builtins::error::create_type_error(vm, "reject function is invalid")); }; + let delegated = vm.callee_delegated_flag(); let reason = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if !delegated { + let state = vm.promise_state_ptr(promise); + let state = unsafe { &mut *state }; + if state.already_resolved { + return NativeResult::Ok(JsValue::undefined()); + } + state.already_resolved = true; + } let _ = vm.reject_promise(promise, reason); NativeResult::Ok(JsValue::undefined()) } @@ -1635,6 +1686,7 @@ pub(crate) fn clone_promise_native_with_rewrite( .collect(), resolve_fn: rewrite(state.resolve_fn), reject_fn: rewrite(state.reject_fn), + already_resolved: state.already_resolved, }; new.set_native_data(Box::into_raw(Box::new(cloned)) as *mut u8); } diff --git a/crates/oxide_vm/tests/promise_tests.rs b/crates/oxide_vm/tests/promise_tests.rs index 8063aa3..41ba8de 100644 --- a/crates/oxide_vm/tests/promise_tests.rs +++ b/crates/oxide_vm/tests/promise_tests.rs @@ -99,3 +99,55 @@ fn custom_ctor_failure_is_type_error() { .unwrap(); assert_eq!(vm.lookup_str(result).as_deref(), Some("TypeError")); } + +#[test] +fn resolve_thenable_then_reject_keeps_fulfilled() { + // P3:resolve(thenable) 置位 alreadyResolved,随后同调用内 reject 不覆盖。 + let mut vm = Vm::new(); + let (ok, val) = settled( + &mut vm, + "new Promise((res, rej) => { res({ then(res2) { res2({ then(r3) { r3('final'); } }); } }); rej('oops'); }).then(v => v, e => 'rejected:' + e)", + ); + assert!(ok); + assert_eq!(vm.lookup_str(val).as_deref(), Some("final")); +} + +#[test] +fn double_resolve_second_noop() { + let mut vm = Vm::new(); + let (ok, val) = settled(&mut vm, "new Promise((res) => { res(1); res(2); }).then(v => v)"); + assert!(ok); + assert_eq!(val.as_int(), 1); +} + +#[test] +fn reject_then_resolve_keeps_rejected() { + let mut vm = Vm::new(); + let (ok, val) = settled(&mut vm, "new Promise((res, rej) => { rej('e'); res(1); }).then(v => v, e => 'rejected:' + e)"); + assert!(ok); + assert_eq!(vm.lookup_str(val).as_deref(), Some("rejected:e")); +} + +#[test] +fn thenable_resolve_after_reject_noop() { + // reject 先行置位,随后 resolve(thenable) 不得触发委托结算。 + let mut vm = Vm::new(); + let (ok, val) = settled( + &mut vm, + "new Promise((res, rej) => { rej('e'); res({ then(r) { r(1); } }); }).then(v => v, e => 'rejected:' + e)", + ); + assert!(ok); + assert_eq!(vm.lookup_str(val).as_deref(), Some("rejected:e")); +} + +#[test] +fn thenable_chain_after_already_resolved_noop() { + // 双重 resolve(thenable):第二次委托不结算,结果保持第一次。 + let mut vm = Vm::new(); + let (ok, val) = settled( + &mut vm, + "new Promise((res) => { res({ then(r) { r(5); } }); res({ then(r) { r(9); } }); }).then(v => v)", + ); + assert!(ok); + assert_eq!(val.as_int(), 5); +}