Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions boa_engine/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub mod symbol;
pub mod typed_array;
pub mod uri;
pub mod weak;
pub mod weak_set;

#[cfg(feature = "intl")]
pub mod intl;
Expand Down Expand Up @@ -85,6 +86,7 @@ use crate::{
typed_array::TypedArray,
uri::{DecodeUri, DecodeUriComponent, EncodeUri, EncodeUriComponent},
weak::WeakRef,
weak_set::WeakSet,
},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
js_string,
Expand Down Expand Up @@ -246,6 +248,7 @@ impl Intrinsics {
DecodeUri::init(&intrinsics);
DecodeUriComponent::init(&intrinsics);
WeakRef::init(&intrinsics);
WeakSet::init(&intrinsics);
#[cfg(feature = "intl")]
{
intl::Intl::init(&intrinsics);
Expand Down Expand Up @@ -340,6 +343,7 @@ pub(crate) fn set_default_global_bindings(context: &mut Context<'_>) -> JsResult
global_binding::<DecodeUri>(context)?;
global_binding::<DecodeUriComponent>(context)?;
global_binding::<WeakRef>(context)?;
global_binding::<WeakSet>(context)?;

#[cfg(feature = "intl")]
global_binding::<intl::Intl>(context)?;
Expand Down
260 changes: 260 additions & 0 deletions boa_engine/src/builtins/weak_set/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
//! Boa's implementation of ECMAScript's `WeakSet` builtin object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-weakset-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet

mod weak_ordered_set;

#[cfg(test)]
mod tests;

use crate::{
builtins::{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;

pub(crate) use weak_ordered_set::WeakOrderedSet;

#[derive(Debug, Clone, Trace, Finalize)]
pub(crate) struct WeakSet(WeakOrderedSet);

impl IntrinsicObject for WeakSet {
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::<Self>(intrinsics)
.property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.method(Self::add, "add", 1)
.method(Self::delete, "delete", 1)
.method(Self::has, "has", 1)
.build();
}
}

impl BuiltInObject for WeakSet {
const NAME: &'static str = "WeakSet";

const ATTRIBUTE: Attribute = Attribute::WRITABLE.union(Attribute::CONFIGURABLE);
}

impl BuiltInConstructor for WeakSet {
/// The amount of arguments the `WeakSet` constructor takes.
const LENGTH: usize = 0;

const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::weak_set;

/// `WeakSet ( [ iterable ] )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-weakset-iterable
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/WeakSet
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, throw a TypeError exception.
if new_target.is_undefined() {
return Err(JsNativeError::typ()
.with_message("WeakSet: cannot call constructor without `new`")
.into());
}

// 2. Let set be ? OrdinaryCreateFromConstructor(NewTarget, "%WeakSet.prototype%", « [[WeakSetData]] »).
// 3. Set set.[[WeakSetData]] to a new empty List.
let weak_set = JsObject::from_proto_and_data(
get_prototype_from_constructor(new_target, StandardConstructors::weak_set, context)?,
ObjectData::weak_set(WeakOrderedSet::default()),
);

// 4. If iterable is either undefined or null, return set.
let iterable = args.get_or_undefined(0);
if iterable.is_null_or_undefined() {
return Ok(weak_set.into());
}

// 5. Let adder be ? Get(set, "add").
let adder = weak_set.get("add", context)?;

// 6. If IsCallable(adder) is false, throw a TypeError exception.
let adder = adder
.as_callable()
.ok_or_else(|| JsNativeError::typ().with_message("WeakSet: 'add' is not a function"))?;

// 7. Let iteratorRecord be ? GetIterator(iterable).
let iterator_record = iterable.clone().get_iterator(context, None, None)?;

// 8. Repeat,
// a. Let next be ? IteratorStep(iteratorRecord).
while let Some(next) = iterator_record.step(context)? {
// c. Let nextValue be ? IteratorValue(next).
let next_value = next.value(context)?;

// d. Let status be Completion(Call(adder, set, « nextValue »)).
// e. IfAbruptCloseIterator(status, iteratorRecord).
if let Err(status) = adder.call(&weak_set.clone().into(), &[next_value], context) {
return iterator_record.close(Err(status), context);
}
}

// b. If next is false, return set.
Ok(weak_set.into())
}
}

impl WeakSet {
/// `WeakSet.prototype.add( value )`
///
/// The add() method appends a new object to the end of a `WeakSet` object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-weakset.prototype.add
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/add
pub(crate) fn add(
this: &JsValue,
args: &[JsValue],
_context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let S be the this value.
// 2. Perform ? RequireInternalSlot(S, [[WeakSetData]]).
let Some(obj) = this.as_object() else {
return Err(JsNativeError::typ()
.with_message("WeakSet.add: called with non-object value")
.into());
};
let mut obj_borrow = obj.borrow_mut();
let o = obj_borrow.as_weak_set_mut().ok_or_else(|| {
JsNativeError::typ().with_message("WeakSet.add: called with non-object value")
})?;

// 3. If Type(value) is not Object, throw a TypeError exception.
let value = args.get_or_undefined(0);
let Some(value) = args.get_or_undefined(0).as_object() else {
return Err(JsNativeError::typ()
.with_message(format!(
"WeakSet.add: expected target argument of type `object`, got target of type `{}`",
value.type_of()
)).into());
};

// 4. Let entries be the List that is S.[[WeakSetData]].
// 5. For each element e of entries, do
if o.contains(value) {
// a. If e is not empty and SameValue(e, value) is true, then
// i. Return S.
return Ok(this.clone());
}

// 6. Append value as the last element of entries.
o.add(value);

// 7. Return S.
Ok(this.clone())
}

/// `WeakSet.prototype.delete( value )`
///
/// The delete() method removes the specified element from a `WeakSet` object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-weakset.prototype.delete
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/delete
pub(crate) fn delete(
this: &JsValue,
args: &[JsValue],
_context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let S be the this value.
// 2. Perform ? RequireInternalSlot(S, [[WeakSetData]]).
let Some(obj) = this.as_object() else {
return Err(JsNativeError::typ()
.with_message("WeakSet.delete: called with non-object value")
.into());
};
let mut obj_borrow = obj.borrow_mut();
let o = obj_borrow.as_weak_set_mut().ok_or_else(|| {
JsNativeError::typ().with_message("WeakSet.delete: called with non-object value")
})?;

// 3. If Type(value) is not Object, return false.
let value = args.get_or_undefined(0);
let Some(value) = value.as_object() else {
return Ok(false.into());
};

// 4. Let entries be the List that is S.[[WeakSetData]].
// 5. For each element e of entries, do
// a. If e is not empty and SameValue(e, value) is true, then
// i. Replace the element of entries whose value is e with an element whose value is empty.
// ii. Return true.
// 6. Return false.
Ok(o.delete(value).into())
}

/// `WeakSet.prototype.has( value )`
///
/// The has() method returns a boolean indicating whether an object exists in a `WeakSet` or not.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-weakset.prototype.has
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet/has
pub(crate) fn has(
this: &JsValue,
args: &[JsValue],
_context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let S be the this value.
// 2. Perform ? RequireInternalSlot(S, [[WeakSetData]]).
let Some(obj) = this.as_object() else {
return Err(JsNativeError::typ()
.with_message("WeakSet.has: called with non-object value")
.into());
};
let obj_borrow = obj.borrow();
let o = obj_borrow.as_weak_set().ok_or_else(|| {
JsNativeError::typ().with_message("WeakSet.has: called with non-object value")
})?;

// 3. Let entries be the List that is S.[[WeakSetData]].
// 4. If Type(value) is not Object, return false.
let value = args.get_or_undefined(0);
let Some(value) = value.as_object() else {
return Ok(false.into());
};

// 5. For each element e of entries, do
// a. If e is not empty and SameValue(e, value) is true, return true.
// 6. Return false.
Ok(o.contains(value).into())
}
}
33 changes: 33 additions & 0 deletions boa_engine/src/builtins/weak_set/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use crate::Context;
use boa_parser::Source;

#[test]
fn weak_ref_collected() {
let context = &mut Context::default();

let weak_set = context
.eval(Source::from_bytes(
r#"
let set;
{
let obj = {a: 5, b: 6};
set = new WeakSet([obj]);
}
set
"#,
))
.unwrap();

boa_gc::force_collect();

assert!(
weak_set
.as_object()
.unwrap()
.borrow()
.as_weak_set()
.unwrap()
.all_collected(),
"Objects in WeakSet should be collected after the last reference to the objects is dropped."
);
}
Loading