Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Implement the WeakRef builtin #2438

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
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`
Razican marked this conversation as resolved.
Show resolved Hide resolved
// 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 @@ -100,6 +100,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 @@ -534,6 +536,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 @@ -543,6 +546,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 @@ -576,6 +580,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 @@ -644,6 +660,7 @@ impl ContextBuilder {
.expect("Failed to initialize default icu data.")
}),
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