diff --git a/nova_vm/src/ecmascript/abstract_operations/testing_and_comparison.rs b/nova_vm/src/ecmascript/abstract_operations/testing_and_comparison.rs index f7926777f..76789b66c 100644 --- a/nova_vm/src/ecmascript/abstract_operations/testing_and_comparison.rs +++ b/nova_vm/src/ecmascript/abstract_operations/testing_and_comparison.rs @@ -277,8 +277,7 @@ pub(crate) fn same_value<'a, V1: Copy + Into>, V2: Copy + Into( agent: &impl PrimitiveHeapIndexable, x: impl Copy + Into>, diff --git a/nova_vm/src/ecmascript/abstract_operations/type_conversion.rs b/nova_vm/src/ecmascript/abstract_operations/type_conversion.rs index 28b0e7057..b2a1571a5 100644 --- a/nova_vm/src/ecmascript/abstract_operations/type_conversion.rs +++ b/nova_vm/src/ecmascript/abstract_operations/type_conversion.rs @@ -60,8 +60,7 @@ pub enum PreferredType { /// returns either a normal completion containing an ECMAScript language value /// or a throw completion. It converts its input argument to a non-Object type. /// If an object is capable of converting to more than one primitive type, it -/// may use the optional hint preferredType to favour that type. It performs -/// the following steps when called: +/// may use the optional hint preferredType to favour that type. /// /// > NOTE: When ToPrimitive is called without a hint, then it generally /// > behaves as if the hint were NUMBER. However, objects may over-ride this diff --git a/nova_vm/src/ecmascript/builtins/arguments.rs b/nova_vm/src/ecmascript/builtins/arguments.rs index 4da307d37..500a87ce7 100644 --- a/nova_vm/src/ecmascript/builtins/arguments.rs +++ b/nova_vm/src/ecmascript/builtins/arguments.rs @@ -44,79 +44,6 @@ use crate::{ use super::{ScopedArgumentsList, ordinary::shape::ObjectShape}; -// 10.4.4.1 [[GetOwnProperty]] ( P ) - -// The [[GetOwnProperty]] internal method of an arguments exotic object args takes argument P (a property key) and returns a normal completion containing either a Property Descriptor or undefined. It performs the following steps when called: - -// 1. Let desc be OrdinaryGetOwnProperty(args, P). -// 2. If desc is undefined, return undefined. -// 3. Let map be args.[[ParameterMap]]. -// 4. Let isMapped be ! HasOwnProperty(map, P). -// 5. If isMapped is true, then -// a. Set desc.[[Value]] to ! Get(map, P). -// 6. Return desc. - -// 10.4.4.2 [[DefineOwnProperty]] ( P, Desc ) - -// The [[DefineOwnProperty]] internal method of an arguments exotic object args takes arguments P (a property key) and Desc (a Property Descriptor) and returns a normal completion containing a Boolean. It performs the following steps when called: - -// 1. Let map be args.[[ParameterMap]]. -// 2. Let isMapped be ! HasOwnProperty(map, P). -// 3. Let newArgDesc be Desc. -// 4. If isMapped is true and IsDataDescriptor(Desc) is true, then -// a. If Desc does not have a [[Value]] field, Desc has a [[Writable]] field, and Desc.[[Writable]] is false, then -// i. Set newArgDesc to a copy of Desc. -// ii. Set newArgDesc.[[Value]] to ! Get(map, P). -// 5. Let allowed be ! OrdinaryDefineOwnProperty(args, P, newArgDesc). -// 6. If allowed is false, return false. -// 7. If isMapped is true, then -// a. If IsAccessorDescriptor(Desc) is true, then -// i. Perform ! map.[[Delete]](P). -// b. Else, -// i. If Desc has a [[Value]] field, then -// 1. Assert: The following Set will succeed, since formal parameters mapped by arguments objects are always writable. -// 2. Perform ! Set(map, P, Desc.[[Value]], false). -// ii. If Desc has a [[Writable]] field and Desc.[[Writable]] is false, then -// 1. Perform ! map.[[Delete]](P). -// 8. Return true. - -// 10.4.4.3 [[Get]] ( P, Receiver ) - -// The [[Get]] internal method of an arguments exotic object args takes arguments P (a property key) and Receiver (an ECMAScript language value) and returns either a normal completion containing an ECMAScript language value or a throw completion. It performs the following steps when called: - -// 1. Let map be args.[[ParameterMap]]. -// 2. Let isMapped be ! HasOwnProperty(map, P). -// 3. If isMapped is false, then -// a. Return ? OrdinaryGet(args, P, Receiver). -// 4. Else, -// a. Assert: map contains a formal parameter mapping for P. -// b. Return ! Get(map, P). - -// 10.4.4.4 [[Set]] ( P, V, Receiver ) - -// The [[Set]] internal method of an arguments exotic object args takes arguments P (a property key), V (an ECMAScript language value), and Receiver (an ECMAScript language value) and returns either a normal completion containing a Boolean or a throw completion. It performs the following steps when called: - -// 1. If SameValue(args, Receiver) is false, then -// a. Let isMapped be false. -// 2. Else, -// a. Let map be args.[[ParameterMap]]. -// b. Let isMapped be ! HasOwnProperty(map, P). -// 3. If isMapped is true, then -// a. Assert: The following Set will succeed, since formal parameters mapped by arguments objects are always writable. -// b. Perform ! Set(map, P, V, false). -// 4. Return ? OrdinarySet(args, P, V, Receiver). - -// 10.4.4.5 [[Delete]] ( P ) - -// The [[Delete]] internal method of an arguments exotic object args takes argument P (a property key) and returns either a normal completion containing a Boolean or a throw completion. It performs the following steps when called: - -// 1. Let map be args.[[ParameterMap]]. -// 2. Let isMapped be ! HasOwnProperty(map, P). -// 3. Let result be ? OrdinaryDelete(args, P). -// 4. If result is true and isMapped is true, then -// a. Perform ! map.[[Delete]](P). -// 5. Return result. - /// ### [10.4.4.6 CreateUnmappedArgumentsObject ( argumentsList )](https://tc39.es/ecma262/#sec-createunmappedargumentsobject) /// /// The abstract operation CreateUnmappedArgumentsObject takes argument @@ -217,61 +144,3 @@ pub(crate) fn create_unmapped_arguments_object<'a, 'b>( // 9. Return obj. Ok(Object::Arguments(obj)) } - -// 10.4.4.7 CreateMappedArgumentsObject ( func, formals, argumentsList, env ) - -// The abstract operation CreateMappedArgumentsObject takes arguments func (an Object), formals (a Parse Node), argumentsList (a List of ECMAScript language values), and env (an Environment Record) and returns an arguments exotic object. It performs the following steps when called: - -// 1. Assert: formals does not contain a rest parameter, any binding patterns, or any initializers. It may contain duplicate identifiers. -// 2. Let len be the number of elements in argumentsList. -// 3. Let obj be MakeBasicObject(« [[Prototype]], [[Extensible]], [[ParameterMap]] »). -// 4. Set obj.[[GetOwnProperty]] as specified in 10.4.4.1. -// 5. Set obj.[[DefineOwnProperty]] as specified in 10.4.4.2. -// 6. Set obj.[[Get]] as specified in 10.4.4.3. -// 7. Set obj.[[Set]] as specified in 10.4.4.4. -// 8. Set obj.[[Delete]] as specified in 10.4.4.5. -// 9. Set obj.[[Prototype]] to %Object.prototype%. -// 10. Let map be OrdinaryObjectCreate(null). -// 11. Set obj.[[ParameterMap]] to map. -// 12. Let parameterNames be the BoundNames of formals. -// 13. Let numberOfParameters be the number of elements in parameterNames. -// 14. Let index be 0. -// 15. Repeat, while index < len, -// a. Let val be argumentsList[index]. -// b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). -// c. Set index to index + 1. -// 16. Perform ! DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). -// 17. Let mappedNames be a new empty List. -// 18. Set index to numberOfParameters - 1. -// 19. Repeat, while index ≥ 0, -// a. Let name be parameterNames[index]. -// b. If mappedNames does not contain name, then -// i. Append name to mappedNames. -// ii. If index < len, then -// 1. Let g be MakeArgGetter(name, env). -// 2. Let p be MakeArgSetter(name, env). -// 3. Perform ! map.[[DefineOwnProperty]](! ToString(𝔽(index)), PropertyDescriptor { [[Set]]: p, [[Get]]: g, [[Enumerable]]: false, [[Configurable]]: true }). -// c. Set index to index - 1. -// 20. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). -// 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). -// 22. Return obj. - -// 10.4.4.7.1 MakeArgGetter ( name, env ) - -// The abstract operation MakeArgGetter takes arguments name (a String) and env (an Environment Record) and returns a function object. It creates a built-in function object that when executed returns the value bound for name in env. It performs the following steps when called: - -// 1. Let getterClosure be a new Abstract Closure with no parameters that captures name and env and performs the following steps when called: -// a. Return env.GetBindingValue(name, false). -// 2. Let getter be CreateBuiltinFunction(getterClosure, 0, "", « »). -// 3. NOTE: getter is never directly accessible to ECMAScript code. -// 4. Return getter. - -// 10.4.4.7.2 MakeArgSetter ( name, env ) - -// The abstract operation MakeArgSetter takes arguments name (a String) and env (an Environment Record) and returns a function object. It creates a built-in function object that when executed sets the value bound for name in env. It performs the following steps when called: - -// 1. Let setterClosure be a new Abstract Closure with parameters (value) that captures name and env and performs the following steps when called: -// a. Return ! env.SetMutableBinding(name, value, false). -// 2. Let setter be CreateBuiltinFunction(setterClosure, 1, "", « »). -// 3. NOTE: setter is never directly accessible to ECMAScript code. -// 4. Return setter. diff --git a/nova_vm/src/ecmascript/builtins/finalization_registry.rs b/nova_vm/src/ecmascript/builtins/finalization_registry.rs index 4676ef01c..0041b78be 100644 --- a/nova_vm/src/ecmascript/builtins/finalization_registry.rs +++ b/nova_vm/src/ecmascript/builtins/finalization_registry.rs @@ -2,16 +2,22 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use core::ops::{Index, IndexMut}; +use soavec::SoAVec; use crate::{ ecmascript::{ - execution::{Agent, ProtoIntrinsics}, - types::{InternalMethods, InternalSlots, Object, OrdinaryObject, Value}, + builtins::finalization_registry::data::{ + FinalizationRegistryRecordMut, FinalizationRegistryRecordRef, + }, + execution::{ + Agent, FinalizationRegistryCleanupJob, ProtoIntrinsics, Realm, WeakKey, + agent::{InnerJob, Job}, + }, + types::{Function, InternalMethods, InternalSlots, Object, OrdinaryObject, Value}, }, engine::{ context::{Bindable, bindable_handle}, - rootable::{HeapRootData, HeapRootRef, Rootable}, + rootable::HeapRootData, }, heap::{ CompactionLists, CreateHeapData, Heap, HeapMarkAndSweep, HeapSweepWeakReference, @@ -19,112 +25,224 @@ use crate::{ }, }; -use self::data::FinalizationRegistryHeapData; +use self::data::FinalizationRegistryRecord; pub mod data; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] -pub struct FinalizationRegistry<'a>(BaseIndex<'a, FinalizationRegistryHeapData<'static>>); +pub struct FinalizationRegistry<'a>(BaseIndex<'a, FinalizationRegistryRecord<'static>>); +bindable_handle!(FinalizationRegistry); -impl FinalizationRegistry<'_> { - pub(crate) const fn _def() -> Self { - Self(BaseIndex::from_u32_index(0)) - } +impl<'fr> FinalizationRegistry<'fr> { + pub(crate) const _DEF: Self = Self(BaseIndex::from_u32_index(u32::MAX - 1)); pub(crate) const fn get_index(self) -> usize { self.0.into_index() } -} -bindable_handle!(FinalizationRegistry); + pub(crate) fn get_cleanup_queue(self, agent: &mut Agent) -> (Function<'fr>, Vec>) { + self.get_mut(agent).cleanup.get_cleanup_queue() + } -impl<'a> From> for Value<'a> { - fn from(value: FinalizationRegistry<'a>) -> Self { - Value::FinalizationRegistry(value) + pub(crate) fn add_cleanups(self, agent: &mut Agent, queue: Vec>) { + if queue.is_empty() { + return; + } + let do_request_cleanup = self.get_mut(agent).cleanup.push_cleanup_queue(queue); + if do_request_cleanup { + agent + .host_hooks + .enqueue_finalization_registry_cleanup_job(Job { + realm: None, + inner: InnerJob::FinalizationRegistry(FinalizationRegistryCleanupJob::new( + agent, self, + )), + }); + } } -} -impl<'a> From> for Object<'a> { - fn from(value: FinalizationRegistry<'a>) -> Self { - Object::FinalizationRegistry(value) + pub(crate) fn enqueue_cleanup_jobs(agent: &mut Agent) { + let frs_to_enqueue = agent + .heap + .finalization_registrys + .as_mut_slice() + .cleanup + .iter_mut() + .enumerate() + .filter_map(|(i, record)| { + let i = i as u32; + if record.needs_cleanup() { + Some(FinalizationRegistry(BaseIndex::from_u32_index(i))) + } else { + None + } + }) + .collect::>(); + for fr in frs_to_enqueue { + agent + .host_hooks + .enqueue_finalization_registry_cleanup_job(Job { + realm: None, + inner: InnerJob::FinalizationRegistry(FinalizationRegistryCleanupJob::new( + agent, fr, + )), + }); + } } -} -impl<'a> InternalSlots<'a> for FinalizationRegistry<'a> { - const DEFAULT_PROTOTYPE: ProtoIntrinsics = ProtoIntrinsics::FinalizationRegistry; + /// # Safety + /// + /// FinalizationRegistry must be previously uninitialised. + pub(crate) unsafe fn initialise( + self, + agent: &mut Agent, + realm: Realm, + cleanup_callback: Function, + ) { + // SAFETY: precondition. + unsafe { + self.get_mut(agent) + .cleanup + .initialise(realm, cleanup_callback) + }; + } - #[inline(always)] - fn get_backing_object(self, agent: &Agent) -> Option> { - agent[self].object_index + pub(crate) fn register( + self, + agent: &mut Agent, + target: WeakKey<'fr>, + held_value: Value<'fr>, + unregister_token: Option>, + ) { + self.get_mut(agent) + .cells + .register(target, held_value, unregister_token); } - fn set_backing_object(self, agent: &mut Agent, backing_object: OrdinaryObject<'static>) { - assert!(agent[self].object_index.replace(backing_object).is_none()); + pub(crate) fn unregister(self, agent: &mut Agent, unregister_token: WeakKey<'fr>) -> bool { + self.get_mut(agent).cells.unregister(unregister_token) } -} -impl<'a> InternalMethods<'a> for FinalizationRegistry<'a> {} + #[inline(always)] + fn get<'a>(self, agent: &'a Agent) -> FinalizationRegistryRecordRef<'a, 'fr> { + self.get_direct(&agent.heap.finalization_registrys) + } -impl Index> for Agent { - type Output = FinalizationRegistryHeapData<'static>; + #[inline(always)] + fn get_mut<'a>(self, agent: &'a mut Agent) -> FinalizationRegistryRecordMut<'a, 'fr> { + self.get_direct_mut(&mut agent.heap.finalization_registrys) + } - fn index(&self, index: FinalizationRegistry) -> &Self::Output { - &self.heap.finalization_registrys[index] + #[inline(always)] + fn get_direct<'a>( + self, + finalization_registrys: &'a SoAVec>, + ) -> FinalizationRegistryRecordRef<'a, 'fr> { + finalization_registrys + .get(self.0.into_u32_index()) + .expect("Invalid FinalizationRegistry reference") + } + + #[inline(always)] + fn get_direct_mut<'a>( + self, + finalization_registrys: &'a mut SoAVec>, + ) -> FinalizationRegistryRecordMut<'a, 'fr> { + // SAFETY: Lifetime transmute to thread GC lifetime to temporary heap + // reference. + unsafe { + core::mem::transmute::< + FinalizationRegistryRecordMut<'a, 'static>, + FinalizationRegistryRecordMut<'a, 'fr>, + >( + finalization_registrys + .get_mut(self.0.into_u32_index()) + .expect("Invalid FinalizationRegistry reference"), + ) + } } } -impl IndexMut> for Agent { - fn index_mut(&mut self, index: FinalizationRegistry) -> &mut Self::Output { - &mut self.heap.finalization_registrys[index] +impl<'a> From> for Value<'a> { + fn from(value: FinalizationRegistry<'a>) -> Self { + Value::FinalizationRegistry(value) } } -impl Index> for Vec> { - type Output = FinalizationRegistryHeapData<'static>; +impl<'a> From> for Object<'a> { + fn from(value: FinalizationRegistry<'a>) -> Self { + Object::FinalizationRegistry(value) + } +} - fn index(&self, index: FinalizationRegistry) -> &Self::Output { - self.get(index.get_index()) - .expect("FinalizationRegistry out of bounds") +impl From> for HeapRootData { + fn from(value: FinalizationRegistry<'_>) -> Self { + HeapRootData::FinalizationRegistry(value.unbind()) } } -impl IndexMut> for Vec> { - fn index_mut(&mut self, index: FinalizationRegistry) -> &mut Self::Output { - self.get_mut(index.get_index()) - .expect("FinalizationRegistry out of bounds") +impl<'a> TryFrom> for FinalizationRegistry<'a> { + type Error = (); + + fn try_from(value: Value<'a>) -> Result { + match value { + Value::FinalizationRegistry(fr) => Ok(fr), + _ => Err(()), + } } } -impl Rootable for FinalizationRegistry<'_> { - type RootRepr = HeapRootRef; +impl<'a> TryFrom> for FinalizationRegistry<'a> { + type Error = (); - fn to_root_repr(value: Self) -> Result { - Err(HeapRootData::FinalizationRegistry(value.unbind())) + fn try_from(value: Object<'a>) -> Result { + match value { + Object::FinalizationRegistry(fr) => Ok(fr), + _ => Err(()), + } } +} + +impl TryFrom for FinalizationRegistry<'_> { + type Error = (); - fn from_root_repr(value: &Self::RootRepr) -> Result { - Err(*value) + fn try_from(value: HeapRootData) -> Result { + match value { + HeapRootData::FinalizationRegistry(fr) => Ok(fr), + _ => Err(()), + } } +} - fn from_heap_ref(heap_ref: HeapRootRef) -> Self::RootRepr { - heap_ref +impl<'fr> InternalSlots<'fr> for FinalizationRegistry<'fr> { + const DEFAULT_PROTOTYPE: ProtoIntrinsics = ProtoIntrinsics::FinalizationRegistry; + + #[inline(always)] + fn get_backing_object(self, agent: &Agent) -> Option> { + self.get(agent).object_index.unbind() } - fn from_heap_data(heap_data: HeapRootData) -> Option { - match heap_data { - HeapRootData::FinalizationRegistry(object) => Some(object), - _ => None, - } + fn set_backing_object(self, agent: &mut Agent, backing_object: OrdinaryObject<'static>) { + assert!( + self.get_mut(agent) + .object_index + .replace(backing_object) + .is_none() + ); } } -impl<'a> CreateHeapData, FinalizationRegistry<'a>> for Heap { - fn create(&mut self, data: FinalizationRegistryHeapData<'a>) -> FinalizationRegistry<'a> { - self.finalization_registrys.push(data.unbind()); - self.alloc_counter += core::mem::size_of::>(); +impl<'a> InternalMethods<'a> for FinalizationRegistry<'a> {} - FinalizationRegistry(BaseIndex::last(&self.finalization_registrys)) +impl<'a> CreateHeapData, FinalizationRegistry<'a>> for Heap { + fn create(&mut self, data: FinalizationRegistryRecord<'a>) -> FinalizationRegistry<'a> { + let i = self.finalization_registrys.len(); + self.finalization_registrys + .push(data.unbind()) + .expect("Failed to allocate FinalizationRegistry"); + self.alloc_counter += core::mem::size_of::>(); + FinalizationRegistry(BaseIndex::from_u32_index(i)) } } diff --git a/nova_vm/src/ecmascript/builtins/finalization_registry/data.rs b/nova_vm/src/ecmascript/builtins/finalization_registry/data.rs index bc4287e7a..83023cc23 100644 --- a/nova_vm/src/ecmascript/builtins/finalization_registry/data.rs +++ b/nova_vm/src/ecmascript/builtins/finalization_registry/data.rs @@ -2,27 +2,247 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use ahash::AHashMap; +use soavec_derive::SoAble; + use crate::{ - ecmascript::types::OrdinaryObject, - engine::context::bindable_handle, - heap::{CompactionLists, HeapMarkAndSweep, WorkQueues}, + ecmascript::{ + execution::{Realm, WeakKey}, + types::{Function, OrdinaryObject, Value}, + }, + engine::context::{Bindable, bindable_handle}, + heap::{CompactionLists, HeapMarkAndSweep, HeapSweepWeakReference, WorkQueues}, }; -#[derive(Debug, Clone, Default)] -pub struct FinalizationRegistryHeapData<'a> { - pub(crate) object_index: Option>, +/// \[\[Cells]] +/// +/// This maps a _cell_.\[\[WeakRefTarget]] to a _cell_.\[\[HeldValue]]. +#[derive(Debug, Default)] +pub(crate) struct Cells<'a> { + /// This maps a _cell_.\[\[WeakRefTarget]] to a _cell_.\[\[HeldValue]]. + cells_weak_ref_target_to_held_value: AHashMap, Value<'a>>, + /// This maps a _cell_.\[\[UnregisterToken]] to a _cell_.\[\[WeakRefTarget]]. + cells_unregister_token_to_weak_ref_target: AHashMap, WeakKey<'a>>, } -bindable_handle!(FinalizationRegistryHeapData); +impl Cells<'_> { + pub(super) fn register( + &mut self, + weak_ref_target: WeakKey, + held_value: Value, + unregister_token: Option, + ) { + self.cells_weak_ref_target_to_held_value + .insert(weak_ref_target.unbind(), held_value.unbind()); + if let Some(unregister_token) = unregister_token { + self.cells_unregister_token_to_weak_ref_target + .insert(unregister_token.unbind(), weak_ref_target.unbind()); + } + } + + pub(super) fn unregister(&mut self, unregister_token: WeakKey) -> bool { + // 4. Let removed be false. + // 5. For each Record { [[WeakRefTarget]], [[HeldValue]], [[UnregisterToken]] } + // cell of finalizationRegistry.[[Cells]], do + // a. If cell.[[UnregisterToken]] is not empty and + // SameValue(cell.[[UnregisterToken]], unregisterToken) is true, + // then + if let Some(weak_ref_target) = self + .cells_unregister_token_to_weak_ref_target + .remove(&unregister_token.unbind()) + { + // i. Remove cell from finalizationRegistry.[[Cells]]. + self.cells_weak_ref_target_to_held_value + .remove(&weak_ref_target) + .unwrap(); + // ii. Set removed to true. + true + } else { + // 6. Return removed. + false + } + } +} -impl HeapMarkAndSweep for FinalizationRegistryHeapData<'static> { +#[derive(Debug)] +pub(crate) struct CleanupRecord<'a> { + cleanup_queue: Vec>, + /// \[\[CleanupCallback]] + callback: Function<'a>, + /// \[\[Realm]] + realm: Realm<'a>, + cleanup_requested: bool, +} +bindable_handle!(CleanupRecord); + +impl Default for CleanupRecord<'_> { + fn default() -> Self { + Self { + cleanup_queue: Default::default(), + // Note: impossible value currently. + callback: Function::BuiltinPromiseCollectorFunction, + realm: const { Realm::from_u32(u32::MAX - 1) }, + cleanup_requested: false, + } + } +} + +impl<'fr> CleanupRecord<'fr> { + pub(super) fn needs_cleanup(&mut self) -> bool { + if !self.cleanup_queue.is_empty() && !self.cleanup_requested { + // We request cleanup by returning true from this method. + self.cleanup_requested = true; + true + } else { + false + } + } + + /// # Safety + /// + /// FinalizationRegistry must be previously uninitialised. + pub(super) unsafe fn initialise(&mut self, realm: Realm, cleanup_callback: Function) { + debug_assert_eq!(self.realm, const { Realm::from_u32(u32::MAX - 1) }); + debug_assert_eq!(self.callback, Function::BuiltinPromiseCollectorFunction); + self.realm = realm.unbind(); + self.callback = cleanup_callback.unbind(); + } + + pub(super) fn get_cleanup_queue(&mut self) -> (Function<'fr>, Vec>) { + self.cleanup_requested = false; + (self.callback, core::mem::take(&mut self.cleanup_queue)) + } + + pub(super) fn push_cleanup_queue(&mut self, queue: Vec>) -> bool { + self.cleanup_queue.extend(queue); + if !self.cleanup_requested { + // We haven't requested cleanup yet, so we should do it now. + self.cleanup_requested = true; + true + } else { + // We're waiting for cleanup, no need to request it again. + false + } + } +} + +#[derive(Debug, Default, SoAble)] +pub(crate) struct FinalizationRegistryRecord<'a> { + /// \[\[Cells]] + pub(super) cells: Cells<'a>, + pub(super) cleanup: CleanupRecord<'a>, + pub(super) object_index: Option>, +} +bindable_handle!(FinalizationRegistryRecord); + +impl HeapMarkAndSweep for FinalizationRegistryRecordRef<'_, 'static> { fn mark_values(&self, queues: &mut WorkQueues) { - let Self { object_index } = self; + let Self { + cells: + Cells { + cells_weak_ref_target_to_held_value, + // Note: cells_unregister_token_to_weak_ref_target holds + // neither key nor value strongly and thus performs no + // marking. + cells_unregister_token_to_weak_ref_target: _, + }, + cleanup, + object_index, + } = self; + for value in cells_weak_ref_target_to_held_value.values() { + value.mark_values(queues); + } + cleanup.mark_values(queues); object_index.mark_values(queues); } + fn sweep_values(&mut self, _: &CompactionLists) { + unreachable!() + } +} + +impl HeapMarkAndSweep for FinalizationRegistryRecordMut<'_, 'static> { + fn mark_values(&self, _: &mut WorkQueues) { + unreachable!() + } + fn sweep_values(&mut self, compactions: &CompactionLists) { - let Self { object_index } = self; + let Self { + cells: + Cells { + cells_weak_ref_target_to_held_value, + cells_unregister_token_to_weak_ref_target, + }, + cleanup, + object_index, + } = self; + cleanup.sweep_values(compactions); object_index.sweep_values(compactions); + if cells_weak_ref_target_to_held_value.is_empty() { + cells_unregister_token_to_weak_ref_target.clear(); + return; + } + let old_cells = core::mem::replace( + cells_weak_ref_target_to_held_value, + AHashMap::with_capacity(cells_weak_ref_target_to_held_value.len()), + ); + for (weak_ref_target, mut held_value) in old_cells { + held_value.sweep_values(compactions); + let new_weak_ref_target = weak_ref_target.sweep_weak_reference(compactions); + if let Some(new_weak_ref_target) = new_weak_ref_target { + // [[WeakRefTarget]] still lives, add it back to + // cells_weak_ref_target_to_held_value. + cells_weak_ref_target_to_held_value.insert(new_weak_ref_target, held_value); + } else { + cleanup.cleanup_queue.push(held_value); + } + } + if cells_weak_ref_target_to_held_value.is_empty() + || cells_unregister_token_to_weak_ref_target.is_empty() + { + cells_unregister_token_to_weak_ref_target.clear(); + return; + } + let old_token_map = core::mem::replace( + cells_unregister_token_to_weak_ref_target, + AHashMap::with_capacity(cells_unregister_token_to_weak_ref_target.len()), + ); + for (unregister_token, weak_ref_target) in old_token_map { + let unregister_token = unregister_token.sweep_weak_reference(compactions); + let weak_ref_target = weak_ref_target.sweep_weak_reference(compactions); + if let (Some(unregister_token), Some(weak_ref_target)) = + (unregister_token, weak_ref_target) + { + // Both the unregister token and the weak_ref_target still + // live, so we must continue tracking them. + cells_unregister_token_to_weak_ref_target.insert(unregister_token, weak_ref_target); + } + } + } +} + +impl HeapMarkAndSweep for CleanupRecord<'static> { + fn mark_values(&self, queues: &mut WorkQueues) { + let Self { + callback, + realm, + cleanup_queue, + cleanup_requested: _, + } = self; + callback.mark_values(queues); + realm.mark_values(queues); + cleanup_queue.mark_values(queues); + } + + fn sweep_values(&mut self, compactions: &CompactionLists) { + let Self { + callback, + realm, + cleanup_queue, + cleanup_requested: _, + } = self; + callback.sweep_values(compactions); + realm.sweep_values(compactions); + cleanup_queue.sweep_values(compactions); } } diff --git a/nova_vm/src/ecmascript/builtins/managing_memory/finalization_registry_objects/finalization_registry_constructor.rs b/nova_vm/src/ecmascript/builtins/managing_memory/finalization_registry_objects/finalization_registry_constructor.rs index 17692926d..d15646bb7 100644 --- a/nova_vm/src/ecmascript/builtins/managing_memory/finalization_registry_objects/finalization_registry_constructor.rs +++ b/nova_vm/src/ecmascript/builtins/managing_memory/finalization_registry_objects/finalization_registry_constructor.rs @@ -2,14 +2,23 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::hint::unreachable_unchecked; + use crate::{ ecmascript::{ + abstract_operations::testing_and_comparison::is_callable, builders::builtin_function_builder::BuiltinFunctionBuilder, - builtins::{ArgumentsList, Behaviour, Builtin, BuiltinIntrinsicConstructor}, - execution::{Agent, JsResult, Realm}, - types::{BUILTIN_STRING_MEMORY, IntoObject, Object, String, Value}, + builtins::{ + ArgumentsList, Behaviour, Builtin, BuiltinIntrinsicConstructor, + ordinary::ordinary_create_from_constructor, + }, + execution::{Agent, JsResult, ProtoIntrinsics, Realm, agent::ExceptionType}, + types::{BUILTIN_STRING_MEMORY, Function, IntoObject, IntoValue, Object, String, Value}, + }, + engine::{ + context::{Bindable, GcScope}, + rootable::Scopable, }, - engine::context::GcScope, heap::IntrinsicConstructorIndexes, }; @@ -26,14 +35,86 @@ impl BuiltinIntrinsicConstructor for FinalizationRegistryConstructor { } impl FinalizationRegistryConstructor { + /// ### [26.2.1.1 FinalizationRegistry ( cleanupCallback )](https://tc39.es/ecma262/#sec-finalization-registry-cleanup-callback) fn constructor<'gc>( agent: &mut Agent, _this_value: Value, - _arguments: ArgumentsList, - _new_target: Option, - gc: GcScope<'gc, '_>, + arguments: ArgumentsList, + new_target: Option, + mut gc: GcScope<'gc, '_>, ) -> JsResult<'gc, Value<'gc>> { - Err(agent.todo("FinalizationRegistry", gc.into_nogc())) + let cleanup_callback = arguments.get(0).bind(gc.nogc()); + // 1. If NewTarget is undefined, throw a TypeError exception. + let Some(new_target) = new_target else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "FinalizationRegistry Constructor requires 'new'", + gc.into_nogc(), + )); + }; + let Ok(new_target) = Function::try_from(new_target) else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Function Proxies not yet supported", + gc.into_nogc(), + )); + }; + // 2. If IsCallable(cleanupCallback) is false, throw a TypeError + // exception. + let Some(cleanup_callback) = is_callable(cleanup_callback, gc.nogc()) else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "cleanupCallback is not a function", + gc.into_nogc(), + )); + }; + let cleanup_callback = cleanup_callback.scope(agent, gc.nogc()); + // 3. Let finalizationRegistry be ? + // OrdinaryCreateFromConstructor( + // NewTarget, + // "%FinalizationRegistry.prototype%", + // « [[Realm]], [[CleanupCallback]], [[Cells]] » + // ). + let Object::FinalizationRegistry(finalization_registry) = ordinary_create_from_constructor( + agent, + new_target, + ProtoIntrinsics::FinalizationRegistry, + gc.reborrow(), + ) + .unbind()? + else { + // SAFETY: ProtoIntrinsics guarded. + unsafe { unreachable_unchecked() } + }; + let gc = gc.into_nogc(); + let finalization_registry = finalization_registry.bind(gc); + // SAFETY: not shared. + let cleanup_callback = unsafe { cleanup_callback.take(agent) }.bind(gc); + // 4. Let fn be the active function object. + // 5. Set finalizationRegistry.[[Realm]] to fn.[[Realm]]. + let realm = match agent.active_function_object(gc) { + Function::BoundFunction(_) => { + unreachable!("bound function constructing FinalizationRegistry") + } + Function::BuiltinFunction(f) => agent[f].realm.bind(gc), + Function::ECMAScriptFunction(f) => agent[f].ecmascript_function.realm.bind(gc), + Function::BuiltinConstructorFunction(f) => agent[f].realm.bind(gc), + Function::BuiltinPromiseResolvingFunction(_) => { + unreachable!("builtin promise resolving function constructing FinalizationRegistry") + } + Function::BuiltinPromiseFinallyFunction(_) => { + unreachable!("builtin promise finally function constructing FinalizationRegistry") + } + Function::BuiltinPromiseCollectorFunction => todo!(), + Function::BuiltinProxyRevokerFunction => todo!(), + }; + // 6. Set finalizationRegistry.[[CleanupCallback]] to + // HostMakeJobCallback(cleanupCallback). + // 7. Set finalizationRegistry.[[Cells]] to a new empty List. + // SAFETY: initialising new FR. + unsafe { finalization_registry.initialise(agent, realm, cleanup_callback) }; + // 8. Return finalizationRegistry. + Ok(finalization_registry.into_value()) } pub(crate) fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>) { diff --git a/nova_vm/src/ecmascript/builtins/managing_memory/finalization_registry_objects/finalization_registry_prototype.rs b/nova_vm/src/ecmascript/builtins/managing_memory/finalization_registry_objects/finalization_registry_prototype.rs index 604906dae..f31eaf54f 100644 --- a/nova_vm/src/ecmascript/builtins/managing_memory/finalization_registry_objects/finalization_registry_prototype.rs +++ b/nova_vm/src/ecmascript/builtins/managing_memory/finalization_registry_objects/finalization_registry_prototype.rs @@ -4,12 +4,15 @@ use crate::{ ecmascript::{ + abstract_operations::testing_and_comparison::same_value, builders::ordinary_object_builder::OrdinaryObjectBuilder, - builtins::{ArgumentsList, Behaviour, Builtin}, - execution::{Agent, JsResult, Realm}, + builtins::{ + ArgumentsList, Behaviour, Builtin, finalization_registry::FinalizationRegistry, + }, + execution::{Agent, JsResult, Realm, agent::ExceptionType, can_be_held_weakly}, types::{BUILTIN_STRING_MEMORY, IntoValue, String, Value}, }, - engine::context::GcScope, + engine::context::{Bindable, GcScope, NoGcScope}, heap::WellKnownSymbolIndexes, }; @@ -29,22 +32,105 @@ impl Builtin for FinalizationRegistryPrototypeUnregister { } impl FinalizationRegistryPrototype { + /// ### [26.2.3.2 FinalizationRegistry.prototype.register ( target, heldValue \[ , unregisterToken \] )](https://tc39.es/ecma262/#sec-finalization-registry.prototype) + /// + /// > NOTE: Based on the algorithms and definitions in this specification, + /// > _cell_.\[\[HeldValue]] is live when _finalizationRegistry_.\[\[Cells]] + /// > contains _cell_; however, this does not necessarily mean that + /// > _cell_.\[\[UnregisterToken]] or _cell_.\[\[Target]] are live. For + /// > example, registering an object with itself as its unregister token + /// > would not keep the object alive forever. fn register<'gc>( agent: &mut Agent, - _this_value: Value, - _: ArgumentsList, + this_value: Value, + arguments: ArgumentsList, gc: GcScope<'gc, '_>, ) -> JsResult<'gc, Value<'gc>> { - Err(agent.todo("FinalizationRegistry.prototype.register", gc.into_nogc())) + let gc = gc.into_nogc(); + let target = arguments.get(0).bind(gc); + let held_value = arguments.get(1).bind(gc); + let unregister_token = arguments.get(2).bind(gc); + // 1. Let finalizationRegistry be the this value. + let finalization_registry = this_value.bind(gc); + // 2. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]). + let finalization_registry = + require_internal_slot_finalization_registry(agent, finalization_registry, gc)?; + // 3. If CanBeHeldWeakly(target) is false, throw a TypeError exception. + let Some(target) = can_be_held_weakly(agent, target) else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "expected target to be an object or symbol", + gc, + )); + }; + // 4. If SameValue(target, heldValue) is true, throw a TypeError exception. + if same_value(agent, target, held_value) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "target cannot be the held value", + gc, + )); + } + // 5. If CanBeHeldWeakly(unregisterToken) is false, then + let unregister_token = if unregister_token.is_undefined() { + // b. Set unregisterToken to empty. + None + } else if let Some(unregister_token) = can_be_held_weakly(agent, unregister_token) { + Some(unregister_token) + } else { + // a. If unregisterToken is not undefined, + // throw a TypeError exception. + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "unregisterToken must be undefined, object, or a symbol", + gc, + )); + }; + // 6. Let cell be the Record { + // [[WeakRefTarget]]: target, + // [[HeldValue]]: heldValue, + // [[UnregisterToken]]: unregisterToken + // }. + // 7. Append cell to finalizationRegistry.[[Cells]]. + finalization_registry.register(agent, target, held_value, unregister_token); + // 8. Return undefined. + Ok(Value::Undefined) } + /// ### [26.2.3.3 FinalizationRegistry.prototype.unregister ( unregisterToken )](https://tc39.es/ecma262/#sec-finalization-registry.prototype.unregister) fn unregister<'gc>( agent: &mut Agent, - _this_value: Value, - _: ArgumentsList, + this_value: Value, + arguments: ArgumentsList, gc: GcScope<'gc, '_>, ) -> JsResult<'gc, Value<'gc>> { - Err(agent.todo("FinalizationRegistry.prototype.unregister", gc.into_nogc())) + let gc = gc.into_nogc(); + let unregister_token = arguments.get(0).bind(gc); + // 1. Let finalizationRegistry be the this value. + let finalization_registry = this_value.bind(gc); + // 2. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]). + let finalization_registry = + require_internal_slot_finalization_registry(agent, finalization_registry, gc)?; + // 3. If CanBeHeldWeakly(unregisterToken) is false, throw a TypeError exception. + let Some(unregister_token) = can_be_held_weakly(agent, unregister_token) else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "unregisterToken must be undefined, object, or a symbol", + gc, + )); + }; + // 4. Let removed be false. + // 5. For each Record { [[WeakRefTarget]], [[HeldValue]], [[UnregisterToken]] } + // cell of finalizationRegistry.[[Cells]], do + // a. If cell.[[UnregisterToken]] is not empty and + // SameValue(cell.[[UnregisterToken]], unregisterToken) is true, + // then + // i. Remove cell from finalizationRegistry.[[Cells]]. + // ii. Set removed to true. + // 6. Return removed. + Ok(finalization_registry + .unregister(agent, unregister_token) + .into_value()) } pub(crate) fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>) { @@ -70,3 +156,18 @@ impl FinalizationRegistryPrototype { .build(); } } + +fn require_internal_slot_finalization_registry<'gc>( + agent: &mut Agent, + finalization_registry: Value<'gc>, + gc: NoGcScope<'gc, '_>, +) -> JsResult<'gc, FinalizationRegistry<'gc>> { + // 1. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]). + FinalizationRegistry::try_from(finalization_registry).map_err(|_| { + agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Expected this to be FinalizationRegistry", + gc, + ) + }) +} diff --git a/nova_vm/src/ecmascript/builtins/ordinary.rs b/nova_vm/src/ecmascript/builtins/ordinary.rs index fa3a46156..4abaef773 100644 --- a/nova_vm/src/ecmascript/builtins/ordinary.rs +++ b/nova_vm/src/ecmascript/builtins/ordinary.rs @@ -67,7 +67,7 @@ use super::{ use super::{ ArrayHeapData, async_generator_objects::AsyncGeneratorHeapData, control_abstraction_objects::generator_objects::GeneratorHeapData, error::ErrorHeapData, - finalization_registry::data::FinalizationRegistryHeapData, + finalization_registry::data::FinalizationRegistryRecord, indexed_collections::array_objects::array_iterator_objects::array_iterator::ArrayIteratorHeapData, keyed_collections::map_objects::map_iterator_objects::map_iterator::MapIteratorHeapData, map::data::MapHeapData, module::Module, primitive_objects::PrimitiveObjectRecord, @@ -1725,7 +1725,7 @@ pub(crate) fn ordinary_object_create_with_intrinsics<'a>( .into_object(), ProtoIntrinsics::FinalizationRegistry => agent .heap - .create(FinalizationRegistryHeapData::default()) + .create(FinalizationRegistryRecord::default()) .into_object(), #[cfg(feature = "proposal-float16array")] ProtoIntrinsics::Float16Array => { diff --git a/nova_vm/src/ecmascript/execution/agent.rs b/nova_vm/src/ecmascript/execution/agent.rs index c4472df7d..fff2a4647 100644 --- a/nova_vm/src/ecmascript/execution/agent.rs +++ b/nova_vm/src/ecmascript/execution/agent.rs @@ -26,7 +26,7 @@ use crate::ecmascript::builtins::shared_array_buffer::SharedArrayBuffer; #[cfg(feature = "atomics")] use crate::ecmascript::builtins::structured_data::atomics_object::WaitAsyncJob; #[cfg(feature = "weak-refs")] -use crate::ecmascript::execution::clear_kept_objects; +use crate::ecmascript::execution::{FinalizationRegistryCleanupJob, clear_kept_objects}; use crate::{ ecmascript::{ abstract_operations::type_conversion::to_string, @@ -248,6 +248,8 @@ pub(crate) enum InnerJob { PromiseReaction(PromiseReactionJob), #[cfg(feature = "atomics")] WaitAsync(WaitAsyncJob), + #[cfg(feature = "weak-refs")] + FinalizationRegistry(FinalizationRegistryCleanupJob), } pub struct Job { @@ -288,6 +290,11 @@ impl Job { InnerJob::PromiseReaction(job) => job.run(agent, gc), #[cfg(feature = "atomics")] InnerJob::WaitAsync(job) => job.run(agent, gc), + #[cfg(feature = "weak-refs")] + InnerJob::FinalizationRegistry(job) => { + job.run(agent, gc); + Ok(()) + } }; if pushed_context { @@ -387,6 +394,33 @@ pub trait HostHooks: core::fmt::Debug { /// requirements in 9.5. fn enqueue_timeout_job(&self, timeout_job: Job, milliseconds: u64); + /// ### [9.9.4.1 HostEnqueueFinalizationRegistryCleanupJob ( finalizationRegistry )](https://tc39.es/ecma262/#sec-host-cleanup-finalization-registry) + /// + /// The host-defined abstract operation + /// HostEnqueueFinalizationRegistryCleanupJob takes argument + /// _finalizationRegistry_ (a FinalizationRegistry) and returns unused. + /// + /// Let _cleanupJob_ be a new Job Abstract Closure with no parameters that + /// captures _finalizationRegistry_ and performs the following steps when + /// called: + /// + /// ```text + /// 1. Let cleanupResult be + /// Completion(CleanupFinalizationRegistry(finalizationRegistry)). + /// 2. If cleanupResult is an abrupt completion, perform any host-defined + /// steps for reporting the error. + /// 3. Return unused. + /// ``` + /// + /// An implementation of HostEnqueueFinalizationRegistryCleanupJob + /// schedules cleanupJob to be performed at some future time, if possible. + /// It must also conform to the requirements in 9.5. + #[allow(unused_variables)] + #[cfg(feature = "weak-refs")] + fn enqueue_finalization_registry_cleanup_job(&self, job: Job) { + // By default, just ignore cleanup. + } + /// ### [27.2.1.9 HostPromiseRejectionTracker ( promise, operation )](https://tc39.es/ecma262/#sec-host-promise-rejection-tracker) #[allow(unused_variables)] fn promise_rejection_tracker( diff --git a/nova_vm/src/ecmascript/execution/weak_ref_and_finalization_registry.rs b/nova_vm/src/ecmascript/execution/weak_ref_and_finalization_registry.rs index 204bf640a..6caa14530 100644 --- a/nova_vm/src/ecmascript/execution/weak_ref_and_finalization_registry.rs +++ b/nova_vm/src/ecmascript/execution/weak_ref_and_finalization_registry.rs @@ -6,11 +6,19 @@ use crate::{ ecmascript::{ - builtins::fundamental_objects::symbol_objects::symbol_constructor::key_for_symbol, - execution::{Agent, weak_key::WeakKey}, + abstract_operations::operations_on_objects::call_function, + builtins::{ + ArgumentsList, finalization_registry::FinalizationRegistry, + fundamental_objects::symbol_objects::symbol_constructor::key_for_symbol, + }, + execution::{Agent, JsResult, weak_key::WeakKey}, types::{Object, Value}, }, - engine::context::NoGcScope, + engine::{ + Global, ScopableCollection, + context::{Bindable, GcScope, NoGcScope}, + rootable::Scopable, + }, }; use super::agent::{ExceptionType, JsError}; @@ -47,6 +55,121 @@ pub(crate) fn add_to_kept_objects(agent: &mut Agent, _value: WeakKey) { // 3. Return unused. } +pub(crate) struct FinalizationRegistryCleanupJob { + finalization_registry: Global>, +} + +impl FinalizationRegistryCleanupJob { + pub(crate) fn new(agent: &mut Agent, finalization_registry: FinalizationRegistry) -> Self { + Self { + finalization_registry: Global::new(agent, finalization_registry.unbind()), + } + } + pub(crate) fn run<'gc>(self, agent: &mut Agent, gc: GcScope<'gc, '_>) { + let finalization_registry = self.finalization_registry.take(agent).bind(gc.nogc()); + // 1. Let cleanupResult be + // Completion(CleanupFinalizationRegistry(finalizationRegistry)). + let cleanup_result = + cleanup_finalization_registry(agent, finalization_registry.unbind(), gc); + // 2. If cleanupResult is an abrupt completion, perform any host-defined steps for reporting the error. + if cleanup_result.is_err() { + let _ = cleanup_result; + } + // 3. Return unused. + } +} + +/// ### [9.12 CleanupFinalizationRegistry ( finalizationRegistry )](https://tc39.es/ecma262/#sec-cleanup-finalization-registry) +/// +/// The abstract operation CleanupFinalizationRegistry takes argument +/// finalizationRegistry (a FinalizationRegistry) and returns either a normal +/// completion containing unused or a throw completion. +pub(crate) fn cleanup_finalization_registry<'gc>( + agent: &mut Agent, + finalization_registry: FinalizationRegistry, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, ()> { + let finalization_registry = finalization_registry.bind(gc.nogc()); + // 1. Assert: finalizationRegistry has [[Cells]] and [[CleanupCallback]] + // internal slots. + // 2. Let callback be finalizationRegistry.[[CleanupCallback]]. + let (callback, mut queue) = finalization_registry.get_cleanup_queue(agent); + if queue.is_empty() { + return Ok(()); + } + let value = queue.pop().unwrap(); + // 3. While finalizationRegistry.[[Cells]] contains a Record cell such that + // cell.[[WeakRefTarget]] is empty, an implementation may perform the + // following steps: + // a. Choose any such cell. + // b. Remove cell from finalizationRegistry.[[Cells]]. + // c. Perform ? HostCallJobCallback(callback, undefined, « cell.[[HeldValue]] »). + if queue.len() == 1 { + // 2. Return ? Call(jobCallback.[[Callback]], V, argumentsList). + let _ = call_function( + agent, + callback.unbind(), + Value::Undefined, + Some(ArgumentsList::from_mut_value(&mut value.unbind())), + gc, + )?; + } else { + let scoped_callback = callback.scope(agent, gc.nogc()); + let finalization_registry = finalization_registry.scope(agent, gc.nogc()); + let queue = queue.scope(agent, gc.nogc()); + let result = call_function( + agent, + callback.unbind(), + Value::Undefined, + Some(ArgumentsList::from_mut_value(&mut value.unbind())), + gc.reborrow(), + ) + .unbind() + .bind(gc.nogc()); + let mut err_and_i = None; + if let Err(err) = result { + err_and_i = Some((err.unbind(), 0)); + } else { + for (i, value) in queue.iter(agent).enumerate() { + let value = value.get(gc.nogc()); + let result = call_function( + agent, + scoped_callback.get(agent), + Value::Undefined, + Some(ArgumentsList::from_mut_value(&mut value.unbind())), + gc.reborrow(), + ) + .unbind() + .bind(gc.nogc()); + if let Err(err) = result { + err_and_i = Some((err.unbind(), i)); + break; + } + } + } + // If an error was thrown by a cleanup callback, we interrupt any + // further cleanup and add any leftover cleanups back into the + // FinalizationRegistry. This will re-request cleanup of the registry + // if necessary. + if let Some((err, i)) = err_and_i { + let err = err.unbind(); + let gc = gc.into_nogc(); + let err = err.bind(gc); + let mut queue = queue.take(agent).bind(gc); + // Drain all elements that were found so far. + queue.drain(0..i); + let finalization_registry = unsafe { finalization_registry.take(agent) }.bind(gc); + let _ = unsafe { scoped_callback.take(agent) }; + if !queue.is_empty() { + finalization_registry.add_cleanups(agent, queue); + } + return Err(err); + } + } + // 4. Return unused. + Ok(()) +} + /// ### [9.13 CanBeHeldWeakly ( v )](https://tc39.es/ecma262/#sec-canbeheldweakly) /// /// The abstract operation CanBeHeldWeakly takes argument v (an ECMAScript diff --git a/nova_vm/src/ecmascript/types/language/value.rs b/nova_vm/src/ecmascript/types/language/value.rs index 651dc7d43..9105bd6ee 100644 --- a/nova_vm/src/ecmascript/types/language/value.rs +++ b/nova_vm/src/ecmascript/types/language/value.rs @@ -341,7 +341,7 @@ pub(crate) const PRIMITIVE_OBJECT_DISCRIMINANT: u8 = pub(crate) const ARGUMENTS_DISCRIMINANT: u8 = value_discriminant(Value::Arguments(OrdinaryObject::_def())); pub(crate) const FINALIZATION_REGISTRY_DISCRIMINANT: u8 = - value_discriminant(Value::FinalizationRegistry(FinalizationRegistry::_def())); + value_discriminant(Value::FinalizationRegistry(FinalizationRegistry::_DEF)); pub(crate) const MAP_DISCRIMINANT: u8 = value_discriminant(Value::Map(Map::_def())); pub(crate) const PROMISE_DISCRIMINANT: u8 = value_discriminant(Value::Promise(Promise::_def())); pub(crate) const PROXY_DISCRIMINANT: u8 = value_discriminant(Value::Proxy(Proxy::_def())); diff --git a/nova_vm/src/heap.rs b/nova_vm/src/heap.rs index e0e73f969..02bfa0a1a 100644 --- a/nova_vm/src/heap.rs +++ b/nova_vm/src/heap.rs @@ -73,7 +73,7 @@ use crate::{ }, embedder_object::data::EmbedderObjectHeapData, error::ErrorHeapData, - finalization_registry::data::FinalizationRegistryHeapData, + finalization_registry::data::FinalizationRegistryRecord, indexed_collections::array_objects::array_iterator_objects::array_iterator::ArrayIteratorHeapData, keyed_collections::map_objects::map_iterator_objects::map_iterator::MapIteratorHeapData, map::data::MapHeapData, @@ -156,7 +156,7 @@ pub(crate) struct Heap { pub(crate) errors: Vec>, /// Stores compiled bytecodes pub(crate) executables: Vec>, - pub(crate) finalization_registrys: Vec>, + pub(crate) finalization_registrys: SoAVec>, pub(crate) generators: Vec>, pub(crate) globals: RefCell>, pub(crate) maps: Vec>, @@ -323,7 +323,7 @@ impl Heap { errors: Vec::with_capacity(1024), executables: Vec::with_capacity(1024), source_codes: Vec::with_capacity(0), - finalization_registrys: Vec::with_capacity(0), + finalization_registrys: SoAVec::with_capacity(0).expect("Failed to allocate Heap"), generators: Vec::with_capacity(1024), globals: RefCell::new(Vec::with_capacity(1024)), maps: Vec::with_capacity(128), diff --git a/nova_vm/src/heap/heap_bits.rs b/nova_vm/src/heap/heap_bits.rs index 92cbe3436..6b623cb1f 100644 --- a/nova_vm/src/heap/heap_bits.rs +++ b/nova_vm/src/heap/heap_bits.rs @@ -316,7 +316,7 @@ impl HeapBits { let errors = vec![false; heap.errors.len()]; let executables = vec![false; heap.executables.len()]; let source_codes = vec![false; heap.source_codes.len()]; - let finalization_registrys = vec![false; heap.finalization_registrys.len()]; + let finalization_registrys = vec![false; heap.finalization_registrys.len() as usize]; let function_environments = vec![false; heap.environments.function.len()]; let generators = vec![false; heap.generators.len()]; let global_environments = vec![false; heap.environments.global.len()]; @@ -508,7 +508,9 @@ impl WorkQueues { errors: Vec::with_capacity(heap.errors.len() / 4), executables: Vec::with_capacity(heap.executables.len() / 4), source_codes: Vec::with_capacity(heap.source_codes.len() / 4), - finalization_registrys: Vec::with_capacity(heap.finalization_registrys.len() / 4), + finalization_registrys: Vec::with_capacity( + heap.finalization_registrys.len() as usize / 4, + ), function_environments: Vec::with_capacity(heap.environments.function.len() / 4), generators: Vec::with_capacity(heap.generators.len() / 4), global_environments: Vec::with_capacity(heap.environments.global.len() / 4), @@ -1733,7 +1735,7 @@ pub(crate) fn sweep_heap_vector_values( }); } -pub(crate) fn sweep_heap_soa_vector_values( +pub(crate) fn sweep_heap_soa_vector_values( vec: &mut SoAVec, compactions: &CompactionLists, bits: &[bool], diff --git a/nova_vm/src/heap/heap_gc.rs b/nova_vm/src/heap/heap_gc.rs index c4adbb7cf..b2c258261 100644 --- a/nova_vm/src/heap/heap_gc.rs +++ b/nova_vm/src/heap/heap_gc.rs @@ -108,7 +108,7 @@ pub fn heap_gc(agent: &mut Agent, root_realms: &mut [Option>], gc bits.symbols[0..WellKnownSymbolIndexes::Unscopables as usize].fill(true); queues.object_shapes.push(ObjectShape::NULL); agent.mark_values(&mut queues); - + let mut has_finalization_registrys = false; while !queues.is_empty() { let Heap { #[cfg(feature = "array-buffer")] @@ -595,6 +595,9 @@ pub fn heap_gc(agent: &mut Agent, root_realms: &mut [Option>], gc let mut finalization_registry_marks: Box<[FinalizationRegistry]> = queues.finalization_registrys.drain(..).collect(); finalization_registry_marks.sort(); + if !finalization_registry_marks.is_empty() { + has_finalization_registrys = true; + } finalization_registry_marks.iter().for_each(|&idx| { let index = idx.get_index(); if let Some(marked) = bits.finalization_registrys.get_mut(index) { @@ -603,7 +606,9 @@ pub fn heap_gc(agent: &mut Agent, root_realms: &mut [Option>], gc return; } *marked = true; - finalization_registrys.get(index).mark_values(&mut queues); + finalization_registrys + .get(index as u32) + .mark_values(&mut queues); } }); let mut generator_marks: Box<[Generator]> = queues.generators.drain(..).collect(); @@ -1442,6 +1447,9 @@ pub fn heap_gc(agent: &mut Agent, root_realms: &mut [Option>], gc } sweep(agent, &bits, root_realms, gc); + if has_finalization_registrys { + FinalizationRegistry::enqueue_cleanup_jobs(agent); + } ndt::gc_done!(|| ()); } @@ -1907,7 +1915,7 @@ fn sweep( } if !finalization_registrys.is_empty() { s.spawn(|| { - sweep_heap_vector_values( + sweep_heap_soa_vector_values( finalization_registrys, &compactions, &bits.finalization_registrys, diff --git a/tests/expectations.json b/tests/expectations.json index 23062b13f..63b0f23af 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -414,38 +414,9 @@ "built-ins/Error/isError/prop-desc.js": "FAIL", "built-ins/Error/isError/symbols.js": "FAIL", "built-ins/Error/proto-from-ctor-realm.js": "FAIL", - "built-ins/FinalizationRegistry/instance-extensible.js": "FAIL", - "built-ins/FinalizationRegistry/is-a-constructor.js": "FAIL", - "built-ins/FinalizationRegistry/newtarget-prototype-is-not-object.js": "FAIL", "built-ins/FinalizationRegistry/proto-from-ctor-realm.js": "FAIL", - "built-ins/FinalizationRegistry/prototype-from-newtarget-abrupt.js": "FAIL", - "built-ins/FinalizationRegistry/prototype-from-newtarget-custom.js": "FAIL", - "built-ins/FinalizationRegistry/prototype-from-newtarget.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/custom-this.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/heldValue-same-as-target.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/holdings-any-value-type.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/not-a-constructor.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/return-undefined-register-itself.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/return-undefined-register-object.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/return-undefined-register-symbol.js": "FAIL", "built-ins/FinalizationRegistry/prototype/register/this-does-not-have-internal-target-throws.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/this-not-object-throws.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/throws-when-target-cannot-be-held-weakly.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/throws-when-unregisterToken-not-undefined-and-cannot-be-held-weakly.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/unregisterToken-same-as-holdings-and-target.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/unregisterToken-same-as-holdings.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/register/unregisterToken-same-as-target.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/unregister/custom-this.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/unregister/not-a-constructor.js": "FAIL", "built-ins/FinalizationRegistry/prototype/unregister/this-does-not-have-internal-cells-throws.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/unregister/this-not-object-throws.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/unregister/throws-when-unregisterToken-cannot-be-held-weakly.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/unregister/unregister-object-token.js": "FAIL", - "built-ins/FinalizationRegistry/prototype/unregister/unregister-symbol-token.js": "FAIL", - "built-ins/FinalizationRegistry/returns-new-object-from-constructor.js": "FAIL", - "built-ins/FinalizationRegistry/target-not-callable-throws.js": "FAIL", - "built-ins/FinalizationRegistry/undefined-newtarget-throws.js": "FAIL", - "built-ins/FinalizationRegistry/unnaffected-by-poisoned-cleanupCallback.js": "FAIL", "built-ins/Function/15.3.5.4_2-12gs.js": "FAIL", "built-ins/Function/15.3.5.4_2-14gs.js": "FAIL", "built-ins/Function/15.3.5.4_2-75gs.js": "FAIL", @@ -931,7 +902,6 @@ "built-ins/Object/prototype/toString/proxy-revoked-during-get-call.js": "FAIL", "built-ins/Object/prototype/toString/proxy-revoked.js": "FAIL", "built-ins/Object/prototype/toString/symbol-tag-weakmap-builtin.js": "FAIL", - "built-ins/Object/seal/seal-finalizationregistry.js": "FAIL", "built-ins/Object/seal/seal-weakmap.js": "FAIL", "built-ins/Promise/all/S25.4.4.1_A4.1_T1.js": "FAIL", "built-ins/Promise/all/call-resolve-element-after-return.js": "FAIL", diff --git a/tests/metrics.json b/tests/metrics.json index 571f0129b..806ee5daf 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,8 +1,8 @@ { "results": { "crash": 72, - "fail": 7643, - "pass": 39639, + "fail": 7613, + "pass": 39669, "skip": 3325, "timeout": 17, "unresolved": 37