Skip to content

Commit

Permalink
Implement the WeakRef builtin (#2438)
Browse files Browse the repository at this point in the history
52/60 tests passing. The remaining tests are either features not implemented ([FinalizationRegistry](https://tc39.es/ecma262/multipage/managing-memory.html#sec-finalization-registry-objects)) or features still in development ([symbols-as-weakmap-keys](https://github.com/tc39/proposal-symbols-as-weakmap-keys)).
  • Loading branch information
jedel1043 committed Nov 15, 2022
1 parent c1b5f38 commit 79f638d
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 38 deletions.
5 changes: 1 addition & 4 deletions boa_engine/src/builtins/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,7 @@ impl Array {
JsValue::Object(o) if o.is_callable() => Some(o),
_ => {
return Err(JsNativeError::typ()
.with_message(format!(
"{} is not a function",
mapfn.type_of().to_std_string_escaped()
))
.with_message(format!("`{}` is not callable", mapfn.type_of()))
.into())
}
};
Expand Down
42 changes: 20 additions & 22 deletions boa_engine/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod symbol;
pub mod typed_array;
pub mod undefined;
pub mod uri;
pub mod weak;

#[cfg(feature = "console")]
pub mod console;
Expand Down Expand Up @@ -82,44 +83,42 @@ use crate::{
builtins::{
array_buffer::ArrayBuffer, async_generator::AsyncGenerator,
async_generator_function::AsyncGeneratorFunction, generator::Generator,
generator_function::GeneratorFunction, typed_array::TypedArray, uri::Uri,
generator_function::GeneratorFunction, typed_array::TypedArray, uri::Uri, weak::WeakRef,
},
property::{Attribute, PropertyDescriptor},
Context, JsValue,
};

/// Trait representing a global built-in object such as `Math`, `Object` or
/// `String`.
/// Trait representing a global built-in object such as `Math`, `Object` or `String`.
///
/// This trait must be implemented for any global built-in accessible from
/// Javascript.
/// This trait must be implemented for any global built-in accessible from JavaScript.
pub(crate) trait BuiltIn {
/// Binding name of the built-in inside the global object.
///
/// E.g. If you want access the properties of a `Complex` built-in
/// with the name `Cplx` you must assign `"Cplx"` to this constant,
/// making any property inside it accessible from Javascript as `Cplx.prop`
/// E.g. If you want access the properties of a `Complex` built-in with the name `Cplx` you must
/// assign `"Cplx"` to this constant, making any property inside it accessible from Javascript
/// as `Cplx.prop`
const NAME: &'static str;

/// Property attribute flags of the built-in.
/// Check [Attribute] for more information.
/// Property attribute flags of the built-in. Check [`Attribute`] for more information.
const ATTRIBUTE: Attribute = Attribute::WRITABLE
.union(Attribute::NON_ENUMERABLE)
.union(Attribute::CONFIGURABLE);

/// Initialization code for the built-in.
/// This is where the methods, properties, static methods and the constructor
/// of a built-in must be initialized to be accessible from Javascript.
///
/// This is where the methods, properties, static methods and the constructor of a built-in must
/// be initialized to be accessible from Javascript.
///
/// # Note
///
/// A return value of `None` indicates that the value must not be added as
/// a global attribute for the global object.
/// A return value of `None` indicates that the value must not be added as a global attribute
/// for the global object.
fn init(context: &mut Context) -> Option<JsValue>;
}

/// Utility function that checks if a type implements `BuiltIn` before
/// initializing it as a global built-in.
/// Utility function that checks if a type implements `BuiltIn` before initializing it as a global
/// built-in.
#[inline]
fn init_builtin<B: BuiltIn>(context: &mut Context) {
if let Some(value) = B::init(context) {
Expand Down Expand Up @@ -195,7 +194,8 @@ pub fn init(context: &mut Context) {
AsyncFunction,
AsyncGenerator,
AsyncGeneratorFunction,
Uri
Uri,
WeakRef
};

#[cfg(feature = "intl")]
Expand All @@ -206,17 +206,15 @@ pub fn init(context: &mut Context) {
}

pub trait JsArgs {
/// Utility function to `get` a parameter from
/// a `[JsValue]` or default to `JsValue::Undefined`
/// Utility function to `get` a parameter from a `[JsValue]` or default to `JsValue::Undefined`
/// if `get` returns `None`.
///
/// Call this if you are thinking of calling something similar to
/// `args.get(n).cloned().unwrap_or_default()` or
/// `args.get(n).unwrap_or(&undefined)`.
///
/// This returns a reference for efficiency, in case
/// you only need to call methods of `JsValue`, so
/// try to minimize calling `clone`.
/// This returns a reference for efficiency, in case you only need to call methods of `JsValue`,
/// so try to minimize calling `clone`.
fn get_or_undefined(&self, index: usize) -> &JsValue;
}

Expand Down
4 changes: 2 additions & 2 deletions boa_engine/src/builtins/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,8 +628,8 @@ impl Object {
val => {
return Err(JsNativeError::typ()
.with_message(format!(
"expected an object or null, got {}",
val.type_of().to_std_string_escaped()
"expected an object or null, got `{}`",
val.type_of()
))
.into())
}
Expand Down
3 changes: 3 additions & 0 deletions boa_engine/src/builtins/weak/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod weak_ref;

pub(crate) use weak_ref::WeakRef;
166 changes: 166 additions & 0 deletions boa_engine/src/builtins/weak/weak_ref.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use boa_gc::{Finalize, Trace, WeakGc};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};

use crate::{
builtins::{BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
symbol::WellKnownSymbols,
Context, JsNativeError, JsResult, JsValue,
};

/// The [`WeakRef`][wr] builtin object.
///
/// [wr]: https://tc39.es/ecma262/#sec-weak-ref-objects
#[derive(Debug, Clone, Trace, Finalize)]
pub(crate) struct WeakRef;

impl BuiltIn for WeakRef {
const NAME: &'static str = "WeakRef";

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

fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
ConstructorBuilder::with_standard_constructor(
context,
WeakRef::constructor,
context.intrinsics().constructors().weak_ref().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.property(
WellKnownSymbols::to_string_tag(),
"WeakRef",
Attribute::CONFIGURABLE,
)
.method(WeakRef::deref, "deref", 0)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}

impl WeakRef {
/// The amount of arguments the `WeakRef` constructor takes.
pub(crate) const LENGTH: usize = 1;

/// Constructor [`WeakRef ( target )`][cons]
///
/// [cons]: https://tc39.es/ecma262/#sec-weak-ref-target
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// If NewTarget is undefined, throw a TypeError exception.
if new_target.is_undefined() {
return Err(JsNativeError::typ()
.with_message("WeakRef: cannot call constructor without `new`")
.into());
}

// 2. If target is not an Object, throw a TypeError exception.
let target = args.get(0).and_then(JsValue::as_object).ok_or_else(|| {
JsNativeError::typ().with_message(format!(
"WeakRef: expected target argument of type `object`, got target of type `{}`",
args.get_or_undefined(0).type_of()
))
})?;

// 3. Let weakRef be ? OrdinaryCreateFromConstructor(NewTarget, "%WeakRef.prototype%", « [[WeakRefTarget]] »).
// 5. Set weakRef.[[WeakRefTarget]] to target.
let weak_ref = JsObject::from_proto_and_data(
get_prototype_from_constructor(new_target, StandardConstructors::weak_ref, context)?,
ObjectData::weak_ref(WeakGc::new(target.inner())),
);

// 4. Perform AddToKeptObjects(target).
context.kept_alive.push(target.clone());

// 6. Return weakRef.
Ok(weak_ref.into())
}

/// Method [`WeakRef.prototype.deref ( )`][spec].
///
/// If the referenced object hasn't been collected, this method promotes a `WeakRef` into a
/// proper [`JsObject`], or returns `undefined` otherwise.
///
/// [spec]: https://tc39.es/ecma262/#sec-weak-ref.prototype.deref
pub(crate) fn deref(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let weakRef be the this value.
// 2. Perform ? RequireInternalSlot(weakRef, [[WeakRefTarget]]).
let weak_ref = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message(format!(
"WeakRef.prototype.deref: expected `this` to be an `object`, got value of type `{}`",
this.type_of()
))
})?;

let weak_ref = weak_ref.as_weak_ref().ok_or_else(|| {
JsNativeError::typ()
.with_message("WeakRef.prototype.deref: expected `this` to be a `WeakRef` object")
})?;

// 3. Return WeakRefDeref(weakRef).

// `WeakRefDeref`
// https://tc39.es/ecma262/multipage/managing-memory.html#sec-weakrefderef
// 1. Let target be weakRef.[[WeakRefTarget]].
// 2. If target is not empty, then
if let Some(object) = weak_ref.upgrade() {
let object = JsObject::from(object);

// a. Perform AddToKeptObjects(target).
context.kept_alive.push(object.clone());

// b. Return target.
Ok(object.into())
} else {
// 3. Return undefined.
Ok(JsValue::undefined())
}
}
}

#[cfg(test)]
mod tests {
use crate::{Context, JsValue};

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

assert!(context
.eval(
r#"
var ptr;
{
let obj = {a: 5, b: 6};
ptr = new WeakRef(obj);
}
ptr.deref()
"#
)
.unwrap()
.is_object());

boa_gc::force_collect();

assert_eq!(
context
.eval(
r#"
ptr.deref()
"#
)
.unwrap(),
JsValue::undefined()
)
}
}
7 changes: 7 additions & 0 deletions boa_engine/src/context/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub struct StandardConstructors {
data_view: StandardConstructor,
date_time_format: StandardConstructor,
promise: StandardConstructor,
weak_ref: StandardConstructor,
}

impl Default for StandardConstructors {
Expand Down Expand Up @@ -175,6 +176,7 @@ impl Default for StandardConstructors {
data_view: StandardConstructor::default(),
date_time_format: StandardConstructor::default(),
promise: StandardConstructor::default(),
weak_ref: StandardConstructor::default(),
};

// The value of `Array.prototype` is the Array prototype object.
Expand Down Expand Up @@ -402,6 +404,11 @@ impl StandardConstructors {
pub fn promise(&self) -> &StandardConstructor {
&self.promise
}

#[inline]
pub fn weak_ref(&self) -> &StandardConstructor {
&self.weak_ref
}
}

/// Cached intrinsic objects
Expand Down
17 changes: 17 additions & 0 deletions boa_engine/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub struct Context {
pub(crate) vm: Vm,

pub(crate) promise_job_queue: VecDeque<JobCallback>,

pub(crate) kept_alive: Vec<JsObject>,
}

impl Default for Context {
Expand Down Expand Up @@ -538,6 +540,7 @@ impl Context {
self.realm.set_global_binding_number();
let result = self.run();
self.vm.pop_frame();
self.clear_kept_objects();
self.run_queued_jobs()?;
let (result, _) = result?;
Ok(result)
Expand All @@ -547,6 +550,7 @@ impl Context {
fn run_queued_jobs(&mut self) -> JsResult<()> {
while let Some(job) = self.promise_job_queue.pop_front() {
job.call_job_callback(&JsValue::Undefined, &[], self)?;
self.clear_kept_objects();
}
Ok(())
}
Expand Down Expand Up @@ -580,6 +584,18 @@ impl Context {
// TODO
self.promise_job_queue.push_back(job);
}

/// Abstract operation [`ClearKeptObjects`][clear].
///
/// Clears all objects maintained alive by calls to the [`AddToKeptObjects`][add] abstract
/// operation, used within the [`WeakRef`][weak] constructor.
///
/// [clear]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-clear-kept-objects
/// [add]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-addtokeptobjects
/// [weak]: https://tc39.es/ecma262/multipage/managing-memory.html#sec-weak-ref-objects
pub fn clear_kept_objects(&mut self) {
self.kept_alive.clear();
}
}
/// Builder for the [`Context`] type.
///
Expand Down Expand Up @@ -661,6 +677,7 @@ impl ContextBuilder {
#[cfg(feature = "fuzz")]
instructions_remaining: self.instructions_remaining,
promise_job_queue: VecDeque::new(),
kept_alive: Vec::new(),
};

// Add new builtIns to Context Realm
Expand Down
11 changes: 11 additions & 0 deletions boa_engine/src/object/jsobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,10 @@ Cannot both specify accessors and a value or writable attribute",
}
)
}

pub(crate) fn inner(&self) -> &Gc<GcCell<Object>> {
&self.inner
}
}

impl AsRef<GcCell<Object>> for JsObject {
Expand All @@ -745,6 +749,13 @@ impl AsRef<GcCell<Object>> for JsObject {
}
}

impl From<Gc<GcCell<Object>>> for JsObject {
#[inline]
fn from(inner: Gc<GcCell<Object>>) -> Self {
JsObject { inner }
}
}

impl PartialEq for JsObject {
fn eq(&self, other: &Self) -> bool {
Self::equals(self, other)
Expand Down
Loading

0 comments on commit 79f638d

Please sign in to comment.