From f538cb214cb6ad1114ad39587979991fdb523f16 Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Fri, 17 Feb 2023 21:22:52 +0000 Subject: [PATCH] Implement `WeakMap` (#2597) This Pull Request changes the following: - Implement `WeakMap` buildin object. --- boa_engine/src/builtins/mod.rs | 4 + boa_engine/src/builtins/weak_map/mod.rs | 271 ++++++++++++++++++++++++ boa_engine/src/context/intrinsics.rs | 13 ++ boa_engine/src/object/mod.rs | 48 ++++- boa_gc/src/pointers/weak_map.rs | 8 +- 5 files changed, 338 insertions(+), 6 deletions(-) create mode 100644 boa_engine/src/builtins/weak_map/mod.rs diff --git a/boa_engine/src/builtins/mod.rs b/boa_engine/src/builtins/mod.rs index ba4e1e0431f..e8248f40502 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -32,6 +32,7 @@ pub mod symbol; pub mod typed_array; pub mod uri; pub mod weak; +pub mod weak_map; pub mod weak_set; #[cfg(feature = "intl")] @@ -86,6 +87,7 @@ use crate::{ typed_array::TypedArray, uri::{DecodeUri, DecodeUriComponent, EncodeUri, EncodeUriComponent}, weak::WeakRef, + weak_map::WeakMap, weak_set::WeakSet, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, @@ -248,6 +250,7 @@ impl Intrinsics { DecodeUri::init(&intrinsics); DecodeUriComponent::init(&intrinsics); WeakRef::init(&intrinsics); + WeakMap::init(&intrinsics); WeakSet::init(&intrinsics); #[cfg(feature = "intl")] { @@ -343,6 +346,7 @@ pub(crate) fn set_default_global_bindings(context: &mut Context<'_>) -> JsResult global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; + global_binding::(context)?; global_binding::(context)?; #[cfg(feature = "intl")] diff --git a/boa_engine/src/builtins/weak_map/mod.rs b/boa_engine/src/builtins/weak_map/mod.rs new file mode 100644 index 00000000000..cceacabd3b8 --- /dev/null +++ b/boa_engine/src/builtins/weak_map/mod.rs @@ -0,0 +1,271 @@ +//! Boa's implementation of ECMAScript's `WeakMap` builtin object. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-weakmap-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap + +use crate::{ + builtins::{ + map::add_entries_from_iterable, BuiltInBuilder, BuiltInConstructor, BuiltInObject, + IntrinsicObject, + }, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + object::{internal_methods::get_prototype_from_constructor, JsObject, ObjectData}, + property::Attribute, + symbol::JsSymbol, + Context, JsArgs, JsNativeError, JsResult, JsValue, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; + +#[derive(Debug, Trace, Finalize)] +pub(crate) struct WeakMap; + +impl IntrinsicObject for WeakMap { + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } + + fn init(intrinsics: &Intrinsics) { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + BuiltInBuilder::from_standard_constructor::(intrinsics) + .property( + JsSymbol::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .method(Self::delete, "delete", 1) + .method(Self::get, "get", 1) + .method(Self::has, "has", 1) + .method(Self::set, "set", 2) + .build(); + } +} + +impl BuiltInObject for WeakMap { + const NAME: &'static str = "WeakMap"; + + const ATTRIBUTE: Attribute = Attribute::WRITABLE.union(Attribute::CONFIGURABLE); +} + +impl BuiltInConstructor for WeakMap { + /// The amount of arguments the `WeakMap` constructor takes. + const LENGTH: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::weak_map; + + /// `WeakMap ( [ iterable ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-weakmap-iterable + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/WeakMap + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return Err(JsNativeError::typ() + .with_message("WeakMap: cannot call constructor without `new`") + .into()); + } + + // 2. Let map be ? OrdinaryCreateFromConstructor(NewTarget, "%WeakMap.prototype%", « [[WeakMapData]] »). + // 3. Set map.[[WeakMapData]] to a new empty List. + let map = JsObject::from_proto_and_data( + get_prototype_from_constructor(new_target, StandardConstructors::weak_map, context)?, + ObjectData::weak_map(boa_gc::WeakMap::new()), + ); + + // 4. If iterable is either undefined or null, return map. + let iterable = args.get_or_undefined(0); + if iterable.is_null_or_undefined() { + return Ok(map.into()); + } + + // 5. Let adder be ? Get(map, "set"). + let adder = map.get("set", context)?; + + // 6. If IsCallable(adder) is false, throw a TypeError exception. + if !adder.is_callable() { + return Err(JsNativeError::typ() + .with_message("WeakMap: 'add' is not a function") + .into()); + } + + // 7. Return ? AddEntriesFromIterable(map, iterable, adder). + add_entries_from_iterable(&map, iterable, &adder, context) + } +} + +impl WeakMap { + /// `WeakMap.prototype.delete ( key )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-weakmap.prototype.delete + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/delete + pub(crate) fn delete( + this: &JsValue, + args: &[JsValue], + _context: &mut Context<'_>, + ) -> JsResult { + // 1. Let M be the this value. + // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]). + let Some(obj) = this.as_object() else { + return Err(JsNativeError::typ() + .with_message("WeakMap.delete: called with non-object value") + .into()); + }; + let mut obj_borrow = obj.borrow_mut(); + let m = obj_borrow.as_weak_map_mut().ok_or_else(|| { + JsNativeError::typ().with_message("WeakMap.delete: called with non-object value") + })?; + + // 3. Let entries be M.[[WeakMapData]]. + // 4. If key is not an Object, return false. + let Some(key) = args.get_or_undefined(0).as_object() else { + return Ok(false.into()); + }; + + // 5. For each Record { [[Key]], [[Value]] } p of entries, do + // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then + // i. Set p.[[Key]] to empty. + // ii. Set p.[[Value]] to empty. + // iii. Return true. + // 6. Return false. + Ok(m.remove(key.inner()).is_some().into()) + } + + /// `WeakMap.prototype.get ( key )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-weakmap.prototype.get + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/get + pub(crate) fn get( + this: &JsValue, + args: &[JsValue], + _context: &mut Context<'_>, + ) -> JsResult { + // 1. Let M be the this value. + // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]). + let Some(obj) = this.as_object() else { + return Err(JsNativeError::typ() + .with_message("WeakMap.get: called with non-object value") + .into()); + }; + let mut obj_borrow = obj.borrow_mut(); + let m = obj_borrow.as_weak_map_mut().ok_or_else(|| { + JsNativeError::typ().with_message("WeakMap.get: called with non-object value") + })?; + + // 3. Let entries be M.[[WeakMapData]]. + // 4. If key is not an Object, return undefined. + let Some(key) = args.get_or_undefined(0).as_object() else { + return Ok(JsValue::undefined()); + }; + + // 5. For each Record { [[Key]], [[Value]] } p of entries, do + // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]]. + // 6. Return undefined. + Ok(m.get(key.inner()).unwrap_or_default()) + } + + /// `WeakMap.prototype.has ( key )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-weakmap.prototype.has + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/has + pub(crate) fn has( + this: &JsValue, + args: &[JsValue], + _context: &mut Context<'_>, + ) -> JsResult { + // 1. Let M be the this value. + // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]). + let Some(obj) = this.as_object() else { + return Err(JsNativeError::typ() + .with_message("WeakMap.has: called with non-object value") + .into()); + }; + let mut obj_borrow = obj.borrow_mut(); + let m = obj_borrow.as_weak_map_mut().ok_or_else(|| { + JsNativeError::typ().with_message("WeakMap.has: called with non-object value") + })?; + + // 3. Let entries be M.[[WeakMapData]]. + // 4. If key is not an Object, return false. + let Some(key) = args.get_or_undefined(0).as_object() else { + return Ok(false.into()); + }; + + // 5. For each Record { [[Key]], [[Value]] } p of entries, do + // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return true. + // 6. Return false. + Ok(m.contains_key(key.inner()).into()) + } + + /// `WeakMap.prototype.set ( key, value )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-weakmap.prototype.set + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/set + pub(crate) fn set( + this: &JsValue, + args: &[JsValue], + _context: &mut Context<'_>, + ) -> JsResult { + // 1. Let M be the this value. + // 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]). + let Some(obj) = this.as_object() else { + return Err(JsNativeError::typ() + .with_message("WeakMap.set: called with non-object value") + .into()); + }; + let mut obj_borrow = obj.borrow_mut(); + let m = obj_borrow.as_weak_map_mut().ok_or_else(|| { + JsNativeError::typ().with_message("WeakMap.set: called with non-object value") + })?; + + // 3. Let entries be M.[[WeakMapData]]. + // 4. If key is not an Object, throw a TypeError exception. + let key = args.get_or_undefined(0); + let Some(key) = key.as_object() else { + return Err(JsNativeError::typ() + .with_message(format!( + "WeakMap.set: expected target argument of type `object`, got target of type `{}`", + key.type_of() + )).into()); + }; + + // 5. For each Record { [[Key]], [[Value]] } p of entries, do + // a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then + // i. Set p.[[Value]] to value. + // ii. Return M. + // 6. Let p be the Record { [[Key]]: key, [[Value]]: value }. + // 7. Append p to entries. + m.insert(key.inner(), args.get_or_undefined(1).clone()); + + // 8. Return M. + Ok(this.clone()) + } +} diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index 8717a13a250..a301d66e9ff 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -114,6 +114,7 @@ pub struct StandardConstructors { date_time_format: StandardConstructor, promise: StandardConstructor, weak_ref: StandardConstructor, + weak_map: StandardConstructor, weak_set: StandardConstructor, #[cfg(feature = "intl")] collator: StandardConstructor, @@ -181,6 +182,7 @@ impl Default for StandardConstructors { date_time_format: StandardConstructor::default(), promise: StandardConstructor::default(), weak_ref: StandardConstructor::default(), + weak_map: StandardConstructor::default(), weak_set: StandardConstructor::default(), #[cfg(feature = "intl")] collator: StandardConstructor::default(), @@ -646,6 +648,17 @@ impl StandardConstructors { &self.weak_ref } + /// Returns the `WeakMap` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-weakmap-constructor + #[inline] + pub const fn weak_map(&self) -> &StandardConstructor { + &self.weak_map + } + /// Returns the `WeakSet` constructor. /// /// More information: diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 0793a7527b8..7b5dce90862 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -54,7 +54,7 @@ use crate::{ Context, JsBigInt, JsString, JsSymbol, JsValue, }; -use boa_gc::{custom_trace, Finalize, GcRefCell, Trace, WeakGc, WeakMap}; +use boa_gc::{custom_trace, Finalize, GcRefCell, Trace, WeakGc}; use std::{ any::Any, fmt::{self, Debug}, @@ -270,8 +270,11 @@ pub enum ObjectKind { /// The `WeakRef` object kind. WeakRef(WeakGc>), + /// The `WeakMap` object kind. + WeakMap(boa_gc::WeakMap, JsValue>), + /// The `WeakSet` object kind. - WeakSet(WeakMap, ()>), + WeakSet(boa_gc::WeakMap, ()>), /// The `Intl.Collator` object kind. #[cfg(feature = "intl")] @@ -314,6 +317,7 @@ unsafe impl Trace for ObjectKind { Self::Promise(p) => mark(p), Self::AsyncGenerator(g) => mark(g), Self::WeakRef(wr) => mark(wr), + Self::WeakMap(wm) => mark(wm), Self::WeakSet(ws) => mark(ws), #[cfg(feature = "intl")] Self::DateTimeFormat(f) => mark(f), @@ -631,9 +635,18 @@ impl ObjectData { } } + /// Create the `WeakMap` object data + #[must_use] + pub fn weak_map(weak_map: boa_gc::WeakMap, JsValue>) -> Self { + Self { + kind: ObjectKind::WeakMap(weak_map), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + /// Create the `WeakSet` object data #[must_use] - pub fn weak_set(weak_set: WeakMap, ()>) -> Self { + pub fn weak_set(weak_set: boa_gc::WeakMap, ()>) -> Self { Self { kind: ObjectKind::WeakSet(weak_set), internal_methods: &ORDINARY_INTERNAL_METHODS, @@ -735,6 +748,7 @@ impl Debug for ObjectKind { Self::DataView(_) => "DataView", Self::Promise(_) => "Promise", Self::WeakRef(_) => "WeakRef", + Self::WeakMap(_) => "WeakMap", Self::WeakSet(_) => "WeakSet", #[cfg(feature = "intl")] Self::Collator(_) => "Collator", @@ -1553,9 +1567,33 @@ impl Object { } } + /// Gets the weak map data if the object is a `WeakMap`. + #[inline] + pub const fn as_weak_map(&self) -> Option<&boa_gc::WeakMap, JsValue>> { + match self.data { + ObjectData { + kind: ObjectKind::WeakMap(ref weak_map), + .. + } => Some(weak_map), + _ => None, + } + } + + /// Gets the mutable weak map data if the object is a `WeakMap`. + #[inline] + pub fn as_weak_map_mut(&mut self) -> Option<&mut boa_gc::WeakMap, JsValue>> { + match self.data { + ObjectData { + kind: ObjectKind::WeakMap(ref mut weak_map), + .. + } => Some(weak_map), + _ => None, + } + } + /// Gets the weak set data if the object is a `WeakSet`. #[inline] - pub const fn as_weak_set(&self) -> Option<&WeakMap, ()>> { + pub const fn as_weak_set(&self) -> Option<&boa_gc::WeakMap, ()>> { match self.data { ObjectData { kind: ObjectKind::WeakSet(ref weak_set), @@ -1567,7 +1605,7 @@ impl Object { /// Gets the mutable weak set data if the object is a `WeakSet`. #[inline] - pub fn as_weak_set_mut(&mut self) -> Option<&mut WeakMap, ()>> { + pub fn as_weak_set_mut(&mut self) -> Option<&mut boa_gc::WeakMap, ()>> { match self.data { ObjectData { kind: ObjectKind::WeakSet(ref mut weak_set), diff --git a/boa_gc/src/pointers/weak_map.rs b/boa_gc/src/pointers/weak_map.rs index 1006e1cea35..6a390355118 100644 --- a/boa_gc/src/pointers/weak_map.rs +++ b/boa_gc/src/pointers/weak_map.rs @@ -7,7 +7,7 @@ pub struct WeakMap { pub(crate) inner: Gc, V>>>, } -impl WeakMap { +impl WeakMap { /// Creates a new [`WeakMap`]. #[must_use] #[inline] @@ -32,4 +32,10 @@ impl WeakMap { pub fn contains_key(&self, key: &Gc) -> bool { self.inner.borrow().contains_key(&WeakGc::new(key)) } + + /// Returns a reference to the value corresponding to the key. + #[inline] + pub fn get(&self, key: &Gc) -> Option { + self.inner.borrow().get(&WeakGc::new(key)).cloned() + } }