Browse Source

Implement the `WeakRef` builtin (#2438)

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)).
pull/2441/head
José Julián Espina 2 years ago
parent
commit
79f638d667
  1. 5
      boa_engine/src/builtins/array/mod.rs
  2. 42
      boa_engine/src/builtins/mod.rs
  3. 4
      boa_engine/src/builtins/object/mod.rs
  4. 3
      boa_engine/src/builtins/weak/mod.rs
  5. 166
      boa_engine/src/builtins/weak/weak_ref.rs
  6. 7
      boa_engine/src/context/intrinsics.rs
  7. 17
      boa_engine/src/context/mod.rs
  8. 11
      boa_engine/src/object/jsobject.rs
  9. 25
      boa_engine/src/object/mod.rs
  10. 1
      boa_engine/src/profiler.rs
  11. 2
      boa_engine/src/tests.rs
  12. 3
      boa_engine/src/value/mod.rs
  13. 4
      boa_engine/src/value/operations.rs
  14. 2
      boa_engine/src/vm/code_block.rs
  15. 4
      boa_engine/src/vm/opcode/binary_ops/mod.rs
  16. 6
      test_ignore.txt

5
boa_engine/src/builtins/array/mod.rs

@ -420,10 +420,7 @@ impl Array {
JsValue::Object(o) if o.is_callable() => Some(o), JsValue::Object(o) if o.is_callable() => Some(o),
_ => { _ => {
return Err(JsNativeError::typ() return Err(JsNativeError::typ()
.with_message(format!( .with_message(format!("`{}` is not callable", mapfn.type_of()))
"{} is not a function",
mapfn.type_of().to_std_string_escaped()
))
.into()) .into())
} }
}; };

42
boa_engine/src/builtins/mod.rs

@ -33,6 +33,7 @@ pub mod symbol;
pub mod typed_array; pub mod typed_array;
pub mod undefined; pub mod undefined;
pub mod uri; pub mod uri;
pub mod weak;
#[cfg(feature = "console")] #[cfg(feature = "console")]
pub mod console; pub mod console;
@ -82,44 +83,42 @@ use crate::{
builtins::{ builtins::{
array_buffer::ArrayBuffer, async_generator::AsyncGenerator, array_buffer::ArrayBuffer, async_generator::AsyncGenerator,
async_generator_function::AsyncGeneratorFunction, generator::Generator, 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}, property::{Attribute, PropertyDescriptor},
Context, JsValue, Context, JsValue,
}; };
/// Trait representing a global built-in object such as `Math`, `Object` or /// Trait representing a global built-in object such as `Math`, `Object` or `String`.
/// `String`.
/// ///
/// This trait must be implemented for any global built-in accessible from /// This trait must be implemented for any global built-in accessible from JavaScript.
/// Javascript.
pub(crate) trait BuiltIn { pub(crate) trait BuiltIn {
/// Binding name of the built-in inside the global object. /// Binding name of the built-in inside the global object.
/// ///
/// E.g. If you want access the properties of a `Complex` built-in /// E.g. If you want access the properties of a `Complex` built-in with the name `Cplx` you must
/// with the name `Cplx` you must assign `"Cplx"` to this constant, /// assign `"Cplx"` to this constant, making any property inside it accessible from Javascript
/// making any property inside it accessible from Javascript as `Cplx.prop` /// as `Cplx.prop`
const NAME: &'static str; const NAME: &'static str;
/// Property attribute flags of the built-in. /// Property attribute flags of the built-in. Check [`Attribute`] for more information.
/// Check [Attribute] for more information.
const ATTRIBUTE: Attribute = Attribute::WRITABLE const ATTRIBUTE: Attribute = Attribute::WRITABLE
.union(Attribute::NON_ENUMERABLE) .union(Attribute::NON_ENUMERABLE)
.union(Attribute::CONFIGURABLE); .union(Attribute::CONFIGURABLE);
/// Initialization code for the built-in. /// 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 /// # Note
/// ///
/// A return value of `None` indicates that the value must not be added as /// A return value of `None` indicates that the value must not be added as a global attribute
/// a global attribute for the global object. /// for the global object.
fn init(context: &mut Context) -> Option<JsValue>; fn init(context: &mut Context) -> Option<JsValue>;
} }
/// Utility function that checks if a type implements `BuiltIn` before /// Utility function that checks if a type implements `BuiltIn` before initializing it as a global
/// initializing it as a global built-in. /// built-in.
#[inline] #[inline]
fn init_builtin<B: BuiltIn>(context: &mut Context) { fn init_builtin<B: BuiltIn>(context: &mut Context) {
if let Some(value) = B::init(context) { if let Some(value) = B::init(context) {
@ -195,7 +194,8 @@ pub fn init(context: &mut Context) {
AsyncFunction, AsyncFunction,
AsyncGenerator, AsyncGenerator,
AsyncGeneratorFunction, AsyncGeneratorFunction,
Uri Uri,
WeakRef
}; };
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
@ -206,17 +206,15 @@ pub fn init(context: &mut Context) {
} }
pub trait JsArgs { pub trait JsArgs {
/// Utility function to `get` a parameter from /// Utility function to `get` a parameter from a `[JsValue]` or default to `JsValue::Undefined`
/// a `[JsValue]` or default to `JsValue::Undefined`
/// if `get` returns `None`. /// if `get` returns `None`.
/// ///
/// Call this if you are thinking of calling something similar to /// Call this if you are thinking of calling something similar to
/// `args.get(n).cloned().unwrap_or_default()` or /// `args.get(n).cloned().unwrap_or_default()` or
/// `args.get(n).unwrap_or(&undefined)`. /// `args.get(n).unwrap_or(&undefined)`.
/// ///
/// This returns a reference for efficiency, in case /// This returns a reference for efficiency, in case you only need to call methods of `JsValue`,
/// you only need to call methods of `JsValue`, so /// so try to minimize calling `clone`.
/// try to minimize calling `clone`.
fn get_or_undefined(&self, index: usize) -> &JsValue; fn get_or_undefined(&self, index: usize) -> &JsValue;
} }

4
boa_engine/src/builtins/object/mod.rs

@ -628,8 +628,8 @@ impl Object {
val => { val => {
return Err(JsNativeError::typ() return Err(JsNativeError::typ()
.with_message(format!( .with_message(format!(
"expected an object or null, got {}", "expected an object or null, got `{}`",
val.type_of().to_std_string_escaped() val.type_of()
)) ))
.into()) .into())
} }

3
boa_engine/src/builtins/weak/mod.rs

@ -0,0 +1,3 @@
mod weak_ref;
pub(crate) use weak_ref::WeakRef;

166
boa_engine/src/builtins/weak/weak_ref.rs

@ -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
boa_engine/src/context/intrinsics.rs

@ -116,6 +116,7 @@ pub struct StandardConstructors {
data_view: StandardConstructor, data_view: StandardConstructor,
date_time_format: StandardConstructor, date_time_format: StandardConstructor,
promise: StandardConstructor, promise: StandardConstructor,
weak_ref: StandardConstructor,
} }
impl Default for StandardConstructors { impl Default for StandardConstructors {
@ -175,6 +176,7 @@ impl Default for StandardConstructors {
data_view: StandardConstructor::default(), data_view: StandardConstructor::default(),
date_time_format: StandardConstructor::default(), date_time_format: StandardConstructor::default(),
promise: StandardConstructor::default(), promise: StandardConstructor::default(),
weak_ref: StandardConstructor::default(),
}; };
// The value of `Array.prototype` is the Array prototype object. // The value of `Array.prototype` is the Array prototype object.
@ -402,6 +404,11 @@ impl StandardConstructors {
pub fn promise(&self) -> &StandardConstructor { pub fn promise(&self) -> &StandardConstructor {
&self.promise &self.promise
} }
#[inline]
pub fn weak_ref(&self) -> &StandardConstructor {
&self.weak_ref
}
} }
/// Cached intrinsic objects /// Cached intrinsic objects

17
boa_engine/src/context/mod.rs

@ -104,6 +104,8 @@ pub struct Context {
pub(crate) vm: Vm, pub(crate) vm: Vm,
pub(crate) promise_job_queue: VecDeque<JobCallback>, pub(crate) promise_job_queue: VecDeque<JobCallback>,
pub(crate) kept_alive: Vec<JsObject>,
} }
impl Default for Context { impl Default for Context {
@ -538,6 +540,7 @@ impl Context {
self.realm.set_global_binding_number(); self.realm.set_global_binding_number();
let result = self.run(); let result = self.run();
self.vm.pop_frame(); self.vm.pop_frame();
self.clear_kept_objects();
self.run_queued_jobs()?; self.run_queued_jobs()?;
let (result, _) = result?; let (result, _) = result?;
Ok(result) Ok(result)
@ -547,6 +550,7 @@ impl Context {
fn run_queued_jobs(&mut self) -> JsResult<()> { fn run_queued_jobs(&mut self) -> JsResult<()> {
while let Some(job) = self.promise_job_queue.pop_front() { while let Some(job) = self.promise_job_queue.pop_front() {
job.call_job_callback(&JsValue::Undefined, &[], self)?; job.call_job_callback(&JsValue::Undefined, &[], self)?;
self.clear_kept_objects();
} }
Ok(()) Ok(())
} }
@ -580,6 +584,18 @@ impl Context {
// TODO // TODO
self.promise_job_queue.push_back(job); 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. /// Builder for the [`Context`] type.
/// ///
@ -661,6 +677,7 @@ impl ContextBuilder {
#[cfg(feature = "fuzz")] #[cfg(feature = "fuzz")]
instructions_remaining: self.instructions_remaining, instructions_remaining: self.instructions_remaining,
promise_job_queue: VecDeque::new(), promise_job_queue: VecDeque::new(),
kept_alive: Vec::new(),
}; };
// Add new builtIns to Context Realm // Add new builtIns to Context Realm

11
boa_engine/src/object/jsobject.rs

@ -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 { impl AsRef<GcCell<Object>> for JsObject {
@ -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 { impl PartialEq for JsObject {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
Self::equals(self, other) Self::equals(self, other)

25
boa_engine/src/object/mod.rs

@ -56,7 +56,7 @@ use crate::{
Context, JsBigInt, JsResult, JsString, JsSymbol, JsValue, Context, JsBigInt, JsResult, JsString, JsSymbol, JsValue,
}; };
use boa_gc::{custom_trace, Finalize, Trace}; use boa_gc::{custom_trace, Finalize, GcCell, Trace, WeakGc};
use boa_interner::Sym; use boa_interner::Sym;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::{ use std::{
@ -193,6 +193,7 @@ pub enum ObjectKind {
NativeObject(Box<dyn NativeObject>), NativeObject(Box<dyn NativeObject>),
IntegerIndexed(IntegerIndexed), IntegerIndexed(IntegerIndexed),
Promise(Promise), Promise(Promise),
WeakRef(WeakGc<GcCell<Object>>),
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
DateTimeFormat(Box<DateTimeFormat>), DateTimeFormat(Box<DateTimeFormat>),
} }
@ -222,6 +223,7 @@ unsafe impl Trace for ObjectKind {
Self::DateTimeFormat(f) => mark(f), Self::DateTimeFormat(f) => mark(f),
Self::Promise(p) => mark(p), Self::Promise(p) => mark(p),
Self::AsyncGenerator(g) => mark(g), Self::AsyncGenerator(g) => mark(g),
Self::WeakRef(wr) => mark(wr),
Self::RegExp(_) Self::RegExp(_)
| Self::BigInt(_) | Self::BigInt(_)
| Self::Boolean(_) | Self::Boolean(_)
@ -512,6 +514,14 @@ impl ObjectData {
} }
} }
/// Creates the `WeakRef` object data
pub fn weak_ref(weak_ref: WeakGc<GcCell<Object>>) -> Self {
Self {
kind: ObjectKind::WeakRef(weak_ref),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `NativeObject` object data /// Create the `NativeObject` object data
pub fn native_object(native_object: Box<dyn NativeObject>) -> Self { pub fn native_object(native_object: Box<dyn NativeObject>) -> Self {
Self { Self {
@ -576,6 +586,7 @@ impl Display for ObjectKind {
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
Self::DateTimeFormat(_) => "DateTimeFormat", Self::DateTimeFormat(_) => "DateTimeFormat",
Self::Promise(_) => "Promise", Self::Promise(_) => "Promise",
Self::WeakRef(_) => "WeakRef",
}) })
} }
} }
@ -1424,6 +1435,18 @@ impl Object {
} }
} }
/// Gets the `WeakRef`data if the object is a `WeakRef`.
#[inline]
pub fn as_weak_ref(&self) -> Option<&WeakGc<GcCell<Object>>> {
match self.data {
ObjectData {
kind: ObjectKind::WeakRef(ref weak_ref),
..
} => Some(weak_ref),
_ => None,
}
}
/// Return `true` if it is a native object and the native type is `T`. /// Return `true` if it is a native object and the native type is `T`.
#[inline] #[inline]
pub fn is<T>(&self) -> bool pub fn is<T>(&self) -> bool

1
boa_engine/src/profiler.rs

@ -1 +0,0 @@

2
boa_engine/src/tests.rs

@ -2720,7 +2720,7 @@ fn instanceofoperator_rhs_not_object() {
assert_eq!( assert_eq!(
&exec(scenario), &exec(scenario),
"\"TypeError: right-hand side of 'instanceof' should be an object, got number\"" "\"TypeError: right-hand side of 'instanceof' should be an object, got `number`\""
); );
} }

3
boa_engine/src/value/mod.rs

@ -949,7 +949,7 @@ impl JsValue {
/// - [ECMAScript reference][spec] /// - [ECMAScript reference][spec]
/// ///
/// [spec]: https://tc39.es/ecma262/#sec-typeof-operator /// [spec]: https://tc39.es/ecma262/#sec-typeof-operator
pub fn type_of(&self) -> JsString { pub fn type_of(&self) -> &'static str {
match *self { match *self {
Self::Rational(_) | Self::Integer(_) => "number", Self::Rational(_) | Self::Integer(_) => "number",
Self::String(_) => "string", Self::String(_) => "string",
@ -966,7 +966,6 @@ impl JsValue {
} }
} }
} }
.into()
} }
/// Abstract operation `IsArray ( argument )` /// Abstract operation `IsArray ( argument )`

4
boa_engine/src/value/operations.rs

@ -421,8 +421,8 @@ impl JsValue {
if !target.is_object() { if !target.is_object() {
return Err(JsNativeError::typ() return Err(JsNativeError::typ()
.with_message(format!( .with_message(format!(
"right-hand side of 'instanceof' should be an object, got {}", "right-hand side of 'instanceof' should be an object, got `{}`",
target.type_of().to_std_string_escaped() target.type_of()
)) ))
.into()); .into());
} }

2
boa_engine/src/vm/code_block.rs

@ -434,7 +434,7 @@ impl ToInternedString for CodeBlock {
for (i, value) in self.literals.iter().enumerate() { for (i, value) in self.literals.iter().enumerate() {
f.push_str(&format!( f.push_str(&format!(
" {i:04}: <{}> {}\n", " {i:04}: <{}> {}\n",
value.type_of().to_std_string_escaped(), value.type_of(),
value.display() value.display()
)); ));
} }

4
boa_engine/src/vm/opcode/binary_ops/mod.rs

@ -86,8 +86,8 @@ impl Operation for In {
if !rhs.is_object() { if !rhs.is_object() {
return Err(JsNativeError::typ() return Err(JsNativeError::typ()
.with_message(format!( .with_message(format!(
"right-hand side of 'in' should be an object, got {}", "right-hand side of 'in' should be an object, got `{}`",
rhs.type_of().to_std_string_escaped() rhs.type_of()
)) ))
.into()); .into());
} }

6
test_ignore.txt

@ -9,6 +9,7 @@ feature:Temporal
feature:tail-call-optimization feature:tail-call-optimization
feature:ShadowRealm feature:ShadowRealm
feature:FinalizationRegistry feature:FinalizationRegistry
feature:FinalizationRegistry.prototype.cleanupSome
feature:Atomics feature:Atomics
feature:dynamic_import feature:dynamic_import
feature:decorators feature:decorators
@ -29,6 +30,11 @@ feature:Intl.Locale
// Non-standard // Non-standard
feature:caller feature:caller
// Stage 3 proposals
// https://github.com/tc39/proposal-symbols-as-weakmap-keys
feature:symbols-as-weakmap-keys
// These generate a stack overflow // These generate a stack overflow
tco-call tco-call
tco-member tco-member

Loading…
Cancel
Save