From 1f5302ee3a952fe7309be9e1b973802903dfc1e7 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Fri, 27 Aug 2021 13:40:29 -0500 Subject: [PATCH] Implement arguments exotic object --- boa/examples/classes.rs | 2 +- boa/src/builtins/array/mod.rs | 20 +- boa/src/builtins/function/mod.rs | 47 +-- boa/src/builtins/intrinsics.rs | 50 +++ boa/src/builtins/mod.rs | 1 + boa/src/builtins/object/mod.rs | 2 +- boa/src/context.rs | 26 +- boa/src/lib.rs | 1 - boa/src/object/exotic/arguments.rs | 319 +++++++++++++++++++ boa/src/object/exotic/mod.rs | 1 + boa/src/object/gcobject.rs | 51 ++- boa/src/object/internal_methods/arguments.rs | 261 +++++++++++++++ boa/src/object/internal_methods/mod.rs | 1 + boa/src/object/mod.rs | 67 +++- 14 files changed, 765 insertions(+), 84 deletions(-) create mode 100644 boa/src/builtins/intrinsics.rs create mode 100644 boa/src/object/exotic/arguments.rs create mode 100644 boa/src/object/exotic/mod.rs create mode 100644 boa/src/object/internal_methods/arguments.rs diff --git a/boa/examples/classes.rs b/boa/examples/classes.rs index 1ff5d8364f2..5273ca5aa57 100644 --- a/boa/examples/classes.rs +++ b/boa/examples/classes.rs @@ -14,7 +14,7 @@ use boa::{ // // The fields of the struct are not accessible by Javascript unless we create accessors for them. /// Represents a `Person` object. -#[derive(Debug, Trace, Finalize)] +#[derive(Debug, Clone, Trace, Finalize)] struct Person { /// The name of the person. name: String, diff --git a/boa/src/builtins/array/mod.rs b/boa/src/builtins/array/mod.rs index 1bcc9223542..020511589dc 100644 --- a/boa/src/builtins/array/mod.rs +++ b/boa/src/builtins/array/mod.rs @@ -39,19 +39,11 @@ impl BuiltIn for Array { fn init(context: &mut Context) -> (&'static str, JsValue, Attribute) { let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); - let symbol_iterator = WellKnownSymbols::iterator(); - let get_species = FunctionBuilder::native(context, Self::get_species) .name("get [Symbol.species]") .constructable(false) .build(); - let values_function = FunctionBuilder::native(context, Self::values) - .name("values") - .length(0) - .constructable(false) - .build(); - let array = ConstructorBuilder::with_standard_object( context, Self::constructor, @@ -70,16 +62,8 @@ impl BuiltIn for Array { 0, Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, ) - .property( - "values", - values_function.clone(), - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) - .property( - symbol_iterator, - values_function, - Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) + .method(Self::values, "values", 0) + .method(Self::values, (WellKnownSymbols::iterator(), "values"), 0) .method(Self::concat, "concat", 1) .method(Self::push, "push", 1) .method(Self::index_of, "indexOf", 1) diff --git a/boa/src/builtins/function/mod.rs b/boa/src/builtins/function/mod.rs index 2df399cc2fa..fba1080dcc8 100644 --- a/boa/src/builtins/function/mod.rs +++ b/boa/src/builtins/function/mod.rs @@ -23,6 +23,7 @@ use crate::{ }; use bitflags::bitflags; use dyn_clone::DynClone; + use sealed::Sealed; use std::fmt::{self, Debug}; @@ -51,13 +52,16 @@ pub type NativeFunction = fn(&JsValue, &[JsValue], &mut Context) -> JsResult JsResult + DynCopy + DynClone + 'static + Fn(&JsValue, &[JsValue], &mut Context, &mut JsObject) -> JsResult + + DynCopy + + DynClone + + 'static { } // The `Copy` bound automatically infers `DynCopy` and `DynClone` impl ClosureFunction for T where - T: Fn(&JsValue, &[JsValue], &mut Context) -> JsResult + Copy + 'static + T: Fn(&JsValue, &[JsValue], &mut Context, &mut JsObject) -> JsResult + Copy + 'static { } @@ -122,6 +126,7 @@ pub enum Function { #[unsafe_ignore_trace] function: Box, constructable: bool, + captures: JsObject, }, Ordinary { flags: FunctionFlags, @@ -193,44 +198,6 @@ impl Function { } } -/// Arguments. -/// -/// -pub fn create_unmapped_arguments_object( - arguments_list: &[JsValue], - context: &mut Context, -) -> JsResult { - let len = arguments_list.len(); - let obj = JsObject::new(Object::default()); - // Set length - let length = PropertyDescriptor::builder() - .value(len) - .writable(true) - .enumerable(false) - .configurable(true); - // Define length as a property - crate::object::internal_methods::ordinary_define_own_property( - &obj, - "length".into(), - length.into(), - context, - )?; - let mut index: usize = 0; - while index < len { - let val = arguments_list.get(index).expect("Could not get argument"); - let prop = PropertyDescriptor::builder() - .value(val.clone()) - .writable(true) - .enumerable(true) - .configurable(true); - - obj.insert(index, prop); - index += 1; - } - - Ok(JsValue::new(obj)) -} - /// Creates a new member function of a `Object` or `prototype`. /// /// A function registered using this macro can then be called from Javascript using: diff --git a/boa/src/builtins/intrinsics.rs b/boa/src/builtins/intrinsics.rs new file mode 100644 index 00000000000..86bf7382048 --- /dev/null +++ b/boa/src/builtins/intrinsics.rs @@ -0,0 +1,50 @@ +use crate::{ + builtins::function::BuiltInFunction, + object::{JsObject, Object}, + property::PropertyDescriptor, + Context, JsResult, JsValue, +}; + +use super::function::Function; + +#[derive(Debug, Default)] +pub struct Intrinsics { + throw_type_error: JsObject, +} + +impl Intrinsics { + pub fn init(context: &mut Context) -> Intrinsics { + Self { + throw_type_error: create_throw_type_error(context), + } + } + + pub fn throw_type_error(&self) -> JsObject { + self.throw_type_error.clone() + } +} + +fn create_throw_type_error(context: &mut Context) -> JsObject { + fn throw_type_error(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + context.throw_type_error("generic type error") + } + let mut function = Object::function( + Function::Native { + function: BuiltInFunction(throw_type_error), + constructable: false, + }, + context + .standard_objects() + .function_object() + .prototype() + .into(), + ); + let property = PropertyDescriptor::builder() + .writable(false) + .enumerable(false) + .configurable(false); + function.insert_property("name", property.clone().value("ThrowTypeError")); + function.insert_property("length", property.value(0)); + + JsObject::new(function) +} diff --git a/boa/src/builtins/mod.rs b/boa/src/builtins/mod.rs index bf921f53aba..04072925b27 100644 --- a/boa/src/builtins/mod.rs +++ b/boa/src/builtins/mod.rs @@ -13,6 +13,7 @@ pub mod error; pub mod function; pub mod global_this; pub mod infinity; +pub mod intrinsics; pub mod iterable; pub mod json; pub mod map; diff --git a/boa/src/builtins/object/mod.rs b/boa/src/builtins/object/mod.rs index 4f261c70b14..4ff0d8edaea 100644 --- a/boa/src/builtins/object/mod.rs +++ b/boa/src/builtins/object/mod.rs @@ -475,7 +475,7 @@ impl Object { let o = o.borrow(); match o.kind() { ObjectKind::Array => "Array", - // TODO: Arguments Exotic Objects are currently not supported + ObjectKind::Arguments(_) => "Arguments", ObjectKind::Function(_) => "Function", ObjectKind::Error => "Error", ObjectKind::Boolean(_) => "Boolean", diff --git a/boa/src/context.rs b/boa/src/context.rs index c7da7d06a56..4597ac94915 100644 --- a/boa/src/context.rs +++ b/boa/src/context.rs @@ -4,6 +4,7 @@ use crate::{ builtins::{ self, function::{Function, FunctionFlags, NativeFunction}, + intrinsics::Intrinsics, iterable::IteratorPrototypes, }, class::{Class, ClassBuilder}, @@ -86,7 +87,7 @@ pub struct StandardObjects { symbol: StandardConstructor, error: StandardConstructor, type_error: StandardConstructor, - referece_error: StandardConstructor, + reference_error: StandardConstructor, range_error: StandardConstructor, syntax_error: StandardConstructor, eval_error: StandardConstructor, @@ -109,7 +110,7 @@ impl Default for StandardObjects { symbol: StandardConstructor::default(), error: StandardConstructor::default(), type_error: StandardConstructor::default(), - referece_error: StandardConstructor::default(), + reference_error: StandardConstructor::default(), range_error: StandardConstructor::default(), syntax_error: StandardConstructor::default(), eval_error: StandardConstructor::default(), @@ -173,7 +174,7 @@ impl StandardObjects { #[inline] pub fn reference_error_object(&self) -> &StandardConstructor { - &self.referece_error + &self.reference_error } #[inline] @@ -271,6 +272,9 @@ pub struct Context { /// Cached standard objects and their prototypes. standard_objects: StandardObjects, + /// Cached intrinsic objects + intrinsics: Intrinsics, + /// Whether or not to show trace of instructions being ran pub trace: bool, } @@ -285,6 +289,7 @@ impl Default for Context { #[cfg(feature = "console")] console: Console::default(), iterator_prototypes: IteratorPrototypes::default(), + intrinsics: Intrinsics::default(), standard_objects: Default::default(), trace: false, }; @@ -292,8 +297,9 @@ impl Default for Context { // Add new builtIns to Context Realm // At a later date this can be removed from here and called explicitly, // but for now we almost always want these default builtins - context.create_intrinsics(); + context.init_builtins(); context.iterator_prototypes = IteratorPrototypes::init(&mut context); + context.intrinsics = Intrinsics::init(&mut context); context } } @@ -325,9 +331,9 @@ impl Context { /// Sets up the default global objects within Global #[inline] - fn create_intrinsics(&mut self) { - let _timer = BoaProfiler::global().start_event("create_intrinsics", "interpreter"); - // Create intrinsics, add global objects here + fn init_builtins(&mut self) { + let _timer = BoaProfiler::global().start_event("init_builtins", "interpreter"); + // Create builtin objects, add global objects here builtins::init(self); } @@ -853,6 +859,12 @@ impl Context { &self.standard_objects } + /// Return the intrinsics. + #[inline] + pub fn intrinsics(&self) -> &Intrinsics { + &self.intrinsics + } + /// Set the value of trace on the context pub fn set_trace(&mut self, trace: bool) { self.trace = trace; diff --git a/boa/src/lib.rs b/boa/src/lib.rs index 4027828702e..665c5c9c617 100644 --- a/boa/src/lib.rs +++ b/boa/src/lib.rs @@ -7,7 +7,6 @@ This is an experimental Javascript lexer, parser and compiler written in Rust. C - **profiler** - Enables profiling with measureme (this is mostly internal). **/ - #![doc( html_logo_url = "https://raw.githubusercontent.com/jasonwilliams/boa/master/assets/logo.svg", html_favicon_url = "https://raw.githubusercontent.com/jasonwilliams/boa/master/assets/logo.svg" diff --git a/boa/src/object/exotic/arguments.rs b/boa/src/object/exotic/arguments.rs new file mode 100644 index 00000000000..34e3e272c75 --- /dev/null +++ b/boa/src/object/exotic/arguments.rs @@ -0,0 +1,319 @@ +use crate::{ + environment::lexical_environment::Environment, + object::{FunctionBuilder, JsObject, Object, ObjectData}, + property::PropertyDescriptor, + symbol::{self, WellKnownSymbols}, + syntax::ast::node::FormalParameter, + Context, JsResult, JsValue, +}; + +use gc::{Finalize, Trace}; +use rustc_hash::FxHashSet; + +#[derive(Debug, Clone, Trace, Finalize)] +pub enum Arguments { + UnmappedArguments, + MappedArguments(MappedArguments), +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct MappedArguments(JsObject); + +impl Arguments { + /// Creates a new unmapped Arguments ordinary object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createunmappedargumentsobject + pub(crate) fn create_unmapped_arguments_object( + arguments_list: &[JsValue], + context: &mut Context, + ) -> JsObject { + // 1. Let len be the number of elements in argumentsList. + let len = arguments_list.len(); + + // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%, « [[ParameterMap]] »). + let obj = context.construct_object(); + + // 3. Set obj.[[ParameterMap]] to undefined. + // skipped because the `Arguments` enum ensures ordinary argument objects don't have a `[[ParameterMap]]` + obj.borrow_mut().data = ObjectData::arguments(Arguments::UnmappedArguments); + + // 4. Perform DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), + // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + obj.define_property_or_throw( + "length", + PropertyDescriptor::builder() + .value(len) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("DefinePropertyOrThrow must not fail per the spec"); + + // 5. Let index be 0. + // 6. Repeat, while index < len, + for (index, value) in arguments_list.iter().cloned().enumerate() { + // a. Let val be argumentsList[index]. + // b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). + obj.create_data_property_or_throw(index, value, context) + .expect("CreateDataPropertyOrThrow must not fail per the spec"); + + // c. Set index to index + 1. + } + + // 7. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { + // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }). + obj.define_property_or_throw( + symbol::WellKnownSymbols::iterator(), + PropertyDescriptor::builder() + .value( + context + .standard_objects() + .array_object() + .prototype() + .get_method(context, "values") + .expect("getting a method from a builtin object must not fail") + .unwrap_or_default(), + ) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("DefinePropertyOrThrow must not fail per the spec"); + + let throw_type_error = context.intrinsics().throw_type_error(); + + // 8. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { + // [[Get]]: %ThrowTypeError%, [[Set]]: %ThrowTypeError%, [[Enumerable]]: false, + // [[Configurable]]: false }). + obj.define_property_or_throw( + "callee", + PropertyDescriptor::builder() + .get(throw_type_error.clone()) + .set(throw_type_error) + .enumerable(false) + .configurable(false), + context, + ) + .expect("DefinePropertyOrThrow must not fail per the spec"); + + // 9. Return obj. + obj + } + + /// Creates a new mapped Arguments exotic object. + /// + /// + pub(crate) fn create_mapped_arguments_object( + func: &JsObject, + formals: &[FormalParameter], + arguments_list: &[JsValue], + env: &Environment, + context: &mut Context, + ) -> JsObject { + // 1. Assert: formals does not contain a rest parameter, any binding patterns, or any initializers. + // It may contain duplicate identifiers. + // 2. Let len be the number of elements in argumentsList. + let len = arguments_list.len(); + + // 3. Let obj be ! MakeBasicObject(« [[Prototype]], [[Extensible]], [[ParameterMap]] »). + // 4. Set obj.[[GetOwnProperty]] as specified in 10.4.4.1. + // 5. Set obj.[[DefineOwnProperty]] as specified in 10.4.4.2. + // 6. Set obj.[[Get]] as specified in 10.4.4.3. + // 7. Set obj.[[Set]] as specified in 10.4.4.4. + // 8. Set obj.[[Delete]] as specified in 10.4.4.5. + // 9. Set obj.[[Prototype]] to %Object.prototype%. + let obj = context.construct_object(); + + // 10. Let map be ! OrdinaryObjectCreate(null). + let map = JsObject::new(Object::with_prototype( + JsValue::Null, + ObjectData::ordinary(), + )); + + // 11. Set obj.[[ParameterMap]] to map. + obj.borrow_mut().data = + ObjectData::arguments(Arguments::MappedArguments(MappedArguments(map.clone()))); + + // 12. Let parameterNames be the BoundNames of formals. + // 13. Let numberOfParameters be the number of elements in parameterNames. + let parameter_names: Vec<_> = formals + .iter() + .map(|formal| formal.name().to_owned()) + .collect(); + + // 14. Let index be 0. + // 15. Repeat, while index < len, + for (index, val) in arguments_list.iter().cloned().enumerate() { + // a. Let val be argumentsList[index]. + // b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). + obj.create_data_property_or_throw(index, val, context) + .expect("CreateDataPropertyOrThrow must not fail per the spec"); + // c. Set index to index + 1. + } + + // 16. Perform ! DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), + // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + obj.define_property_or_throw( + "length", + PropertyDescriptor::builder() + .value(len) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("DefinePropertyOrThrow must not fail per the spec"); + + // 17. Let mappedNames be a new empty List. + // using a set to optimize `contains` + let mut mapped_names = FxHashSet::default(); + + // 18. Set index to numberOfParameters - 1. + // 19. Repeat, while index ≥ 0, + // a. Let name be parameterNames[index]. + for (index, name) in parameter_names.iter().enumerate().rev() { + // b. If name is not an element of mappedNames, then + if !mapped_names.contains(name) { + // i. Add name as an element of the list mappedNames. + mapped_names.insert(name); + // ii. If index < len, then + if index < len { + // 1. Let g be MakeArgGetter(name, env). + // https://tc39.es/ecma262/#sec-makearggetter + let g = { + // 1. Let getterClosure be a new Abstract Closure with no parameters that captures + // name and env and performs the following steps when called: + fn getter_closure( + _: &JsValue, + _: &[JsValue], + context: &mut Context, + captures: &mut JsObject, + ) -> JsResult { + if let Some(data) = captures.downcast_ref::<(Environment, String)>() { + // a. Return env.GetBindingValue(name, false). + data.0.get_binding_value(&data.1, false, context) + } else { + context + .throw_type_error("cannot get environment and name from type") + } + } + + // 2. Let getter be ! CreateBuiltinFunction(getterClosure, 0, "", « »). + // 3. NOTE: getter is never directly accessible to ECMAScript code. + // 4. Return getter. + FunctionBuilder::closure_with_captures( + context, + getter_closure, + (env.clone(), name.clone()), + ) + .length(0) + .name("") + .build() + }; + + // 2. Let p be MakeArgSetter(name, env). + // https://tc39.es/ecma262/#sec-makeargsetter + let p = { + // 1. Let setterClosure be a new Abstract Closure with parameters (value) that captures + // name and env and performs the following steps when called: + fn setter_closure( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + captures: &mut JsObject, + ) -> JsResult { + if let Some(data) = captures.downcast_mut::<(Environment, String)>() { + let value = args.get(0).cloned().unwrap_or_default(); + // a. Return env.SetMutableBinding(name, value, false). + data.0 + .set_mutable_binding(&data.1, value, false, context) + .map(|_| JsValue::Undefined) + // Ok(JsValue::Undefined) + } else { + context.throw_type_error( + "cannot get environment and name from provided object ", + ) + } + } + + // 2. Let setter be ! CreateBuiltinFunction(setterClosure, 1, "", « »). + // 3. NOTE: setter is never directly accessible to ECMAScript code. + // 4. Return setter. + FunctionBuilder::closure_with_captures( + context, + setter_closure, + (env.clone(), name.clone()), + ) + .length(1) + .name("") + .build() + }; + + // 3. Perform map.[[DefineOwnProperty]](! ToString(𝔽(index)), PropertyDescriptor { + // [[Set]]: p, [[Get]]: g, [[Enumerable]]: false, [[Configurable]]: true }). + map.__define_own_property__( + index.into(), + PropertyDescriptor::builder() + .set(p) + .get(g) + .enumerable(false) + .configurable(true) + .build(), + context, + ) + .expect("[[DefineOwnProperty]] must not fail per the spec"); + } + } + } + + // 20. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { + // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }). + obj.define_property_or_throw( + WellKnownSymbols::iterator(), + PropertyDescriptor::builder() + .value( + context + .standard_objects() + .array_object() + .prototype() + .get_method(context, "values") + .expect("getting a method from a builtin object must not fail") + .unwrap_or_default(), + ) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("DefinePropertyOrThrow must not fail per the spec"); + + // 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { + // [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + obj.define_property_or_throw( + "callee", + PropertyDescriptor::builder() + .value(func.clone()) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("DefinePropertyOrThrow must not fail per the spec"); + + // 22. Return obj. + obj + } +} + +impl MappedArguments { + pub fn parameter_map(&self) -> JsObject { + self.0.clone() + } +} diff --git a/boa/src/object/exotic/mod.rs b/boa/src/object/exotic/mod.rs new file mode 100644 index 00000000000..0bcb87fca2b --- /dev/null +++ b/boa/src/object/exotic/mod.rs @@ -0,0 +1 @@ +pub(crate) mod arguments; diff --git a/boa/src/object/gcobject.rs b/boa/src/object/gcobject.rs index d971ae2f907..7231d1af5b0 100644 --- a/boa/src/object/gcobject.rs +++ b/boa/src/object/gcobject.rs @@ -2,13 +2,10 @@ //! //! The `JsObject` is a garbage collected Object. -use super::{NativeObject, Object, PROTOTYPE}; +use super::{exotic::arguments::Arguments, NativeObject, Object, PROTOTYPE}; use crate::{ - builtins::function::{ - create_unmapped_arguments_object, ClosureFunction, Function, NativeFunction, - }, + builtins::function::{ClosureFunction, Function, NativeFunction}, environment::{ - environment_record_trait::EnvironmentRecordTrait, function_environment_record::{BindingStatus, FunctionEnvironmentRecord}, lexical_environment::Environment, }, @@ -45,7 +42,10 @@ pub struct JsObject(Gc>); enum FunctionBody { BuiltInFunction(NativeFunction), BuiltInConstructor(NativeFunction), - Closure(Box), + Closure { + function: Box, + captures: JsObject, + }, Ordinary(RcStatementList), } @@ -121,6 +121,7 @@ impl JsObject { /// /// /// + // todo: comment this function with the spec: https://tc39.es/ecma262/#sec-functiondeclarationinstantiation #[track_caller] pub(super) fn call_construct( &self, @@ -151,7 +152,12 @@ impl JsObject { FunctionBody::BuiltInFunction(function.0) } } - Function::Closure { function, .. } => FunctionBody::Closure(function.clone()), + Function::Closure { + function, captures, .. + } => FunctionBody::Closure { + function: function.clone(), + captures: captures.clone(), + }, Function::Ordinary { body, params, @@ -202,14 +208,22 @@ impl JsObject { ); let mut arguments_in_parameter_names = false; + let mut is_simple_parameter_list = true; for param in params.iter() { has_parameter_expressions = has_parameter_expressions || param.init().is_some(); arguments_in_parameter_names = arguments_in_parameter_names || param.name() == "arguments"; + is_simple_parameter_list = is_simple_parameter_list + && !param.is_rest_param() + && param.init().is_none() + // todo: handle pattern destructuring } + // Turn local_env into Environment so it can be cloned + let local_env: Environment = local_env.into(); + // An arguments object is added when all of the following conditions are met // - If not in an arrow function (10.2.11.16) // - If the parameter list does not contain `arguments` (10.2.11.17) @@ -223,19 +237,27 @@ impl JsObject { && !body.function_declared_names().contains("arguments"))) { // Add arguments object - let arguments_obj = create_unmapped_arguments_object(args, context)?; + // todo: handle `strict` mode + let arguments_obj = if !is_simple_parameter_list { + Arguments::create_unmapped_arguments_object(args, context) + } else { + Arguments::create_mapped_arguments_object( + self, params, args, &local_env, context, + ) + }; local_env.create_mutable_binding( "arguments".to_string(), false, true, context, )?; - local_env.initialize_binding("arguments", arguments_obj, context)?; + local_env.initialize_binding( + "arguments", + arguments_obj.into(), + context, + )?; } - // Turn local_env into Environment so it can be cloned - let local_env: Environment = local_env.into(); - // Push the environment first so that it will be used by default parameters context.push_environment(local_env.clone()); @@ -300,7 +322,10 @@ impl JsObject { function(&JsValue::undefined(), args, context) } FunctionBody::BuiltInFunction(function) => function(this_target, args, context), - FunctionBody::Closure(function) => (function)(this_target, args, context), + FunctionBody::Closure { + function, + mut captures, + } => (function)(this_target, args, context, &mut captures), FunctionBody::Ordinary(body) => { let result = body.run(context); let this = context.get_this_binding(); diff --git a/boa/src/object/internal_methods/arguments.rs b/boa/src/object/internal_methods/arguments.rs new file mode 100644 index 00000000000..0c6cbc64522 --- /dev/null +++ b/boa/src/object/internal_methods/arguments.rs @@ -0,0 +1,261 @@ +use crate::{ + object::JsObject, + property::{DescriptorKind, PropertyDescriptor, PropertyKey}, + Context, JsResult, JsValue, +}; + +use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; + +pub(crate) static ARGUMENTS_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = + InternalObjectMethods { + __get_own_property__: arguments_exotic_get_own_property, + __define_own_property__: arguments_exotic_define_own_property, + __get__: arguments_exotic_get, + __set__: arguments_exotic_set, + __delete__: arguments_exotic_delete, + ..ORDINARY_INTERNAL_METHODS + }; + +/// [[GetOwnProperty]] for arguments exotic objects. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-getownproperty-p +#[inline] +pub(crate) fn arguments_exotic_get_own_property( + obj: &JsObject, + key: &PropertyKey, + context: &mut Context, +) -> JsResult> { + // 1. Let desc be OrdinaryGetOwnProperty(args, P). + // 2. If desc is undefined, return desc. + let desc = if let Some(desc) = super::ordinary_get_own_property(obj, key, context)? { + desc + } else { + return Ok(None); + }; + + // 3. Let map be args.[[ParameterMap]]. + let map = obj + .borrow() + .as_mapped_arguments() + .expect("arguments exotic method must only be callable from arguments objects") + .parameter_map(); + + Ok(Some( + // 4. Let isMapped be ! HasOwnProperty(map, P). + // 5. If isMapped is true, then + if map + .has_own_property(key.clone(), context) + .expect("HasOwnProperty must not fail per the spec") + { + // a. Set desc.[[Value]] to Get(map, P). + PropertyDescriptor::builder() + .value(map.get(key.clone(), context)?) + .maybe_writable(desc.writable()) + .maybe_enumerable(desc.enumerable()) + .maybe_configurable(desc.configurable()) + .build() + } else { + // 6. Return desc. + desc + }, + )) +} + +/// [[DefineOwnProperty]] for arguments exotic objects. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-defineownproperty-p-desc +pub(crate) fn arguments_exotic_define_own_property( + obj: &JsObject, + key: PropertyKey, + desc: PropertyDescriptor, + context: &mut Context, +) -> JsResult { + // 1. Let map be args.[[ParameterMap]]. + let map = obj + .borrow() + .as_mapped_arguments() + .expect("arguments exotic method must only be callable from arguments objects") + .parameter_map(); + + // 2. Let isMapped be HasOwnProperty(map, P). + let is_mapped = map.has_own_property(key.clone(), context)?; + + let new_arg_desc = match desc.kind() { + // 4. If isMapped is true and IsDataDescriptor(Desc) is true, then + // a. If Desc.[[Value]] is not present and Desc.[[Writable]] is present and its + // value is false, then + DescriptorKind::Data { + writable: Some(false), + value: None, + } if is_mapped => + // i. Set newArgDesc to a copy of Desc. + // ii. Set newArgDesc.[[Value]] to Get(map, P). + { + PropertyDescriptor::builder() + .value(map.get(key.clone(), context)?) + .writable(false) + .maybe_enumerable(desc.enumerable()) + .maybe_configurable(desc.configurable()) + .build() + } + + // 3. Let newArgDesc be Desc. + _ => desc.clone(), + }; + + // 5. Let allowed be ? OrdinaryDefineOwnProperty(args, P, newArgDesc). + // 6. If allowed is false, return false. + if !super::ordinary_define_own_property(obj, key.clone(), new_arg_desc, context)? { + return Ok(false); + } + + // 7. If isMapped is true, then + if is_mapped { + // a. If IsAccessorDescriptor(Desc) is true, then + if desc.is_accessor_descriptor() { + // i. Call map.[[Delete]](P). + map.__delete__(&key, context)?; + + // b. Else, + } else { + // i. If Desc.[[Value]] is present, then + if let Some(value) = desc.value() { + // 1. Let setStatus be Set(map, P, Desc.[[Value]], false). + let set_status = map.set(key.clone(), value, false, context)?; + + // 2. Assert: setStatus is true because formal parameters mapped by argument + // objects are always writable. + debug_assert!(set_status) + } + + // ii. If Desc.[[Writable]] is present and its value is false, then + if let Some(false) = desc.writable() { + // 1. Call map.[[Delete]](P). + map.__delete__(&key, context)?; + } + } + } + + // 8. Return true. + Ok(true) +} + +/// [[Get]] for arguments exotic objects. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-get-p-receiver +pub(crate) fn arguments_exotic_get( + obj: &JsObject, + key: &PropertyKey, + receiver: JsValue, + context: &mut Context, +) -> JsResult { + // 1. Let map be args.[[ParameterMap]]. + let map = obj + .borrow() + .as_mapped_arguments() + .expect("arguments exotic method must only be callable from arguments objects") + .parameter_map(); + + // 2. Let isMapped be ! HasOwnProperty(map, P). + // 4. Else, + if map + .has_own_property(key.clone(), context) + .expect("HasOwnProperty must not fail per the spec") + { + // a. Assert: map contains a formal parameter mapping for P. + // b. Return Get(map, P). + map.get(key.clone(), context) + + // 3. If isMapped is false, then + } else { + // a. Return ? OrdinaryGet(args, P, Receiver). + super::ordinary_get(obj, key, receiver, context) + } +} + +/// [[Set]] for arguments exotic objects. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-set-p-v-receiver +pub(crate) fn arguments_exotic_set( + obj: &JsObject, + key: PropertyKey, + value: JsValue, + receiver: JsValue, + context: &mut Context, +) -> JsResult { + // 1. If SameValue(args, Receiver) is false, then + // a. Let isMapped be false. + // 2. Else, + if JsValue::same_value(&obj.clone().into(), &receiver) { + // a. Let map be args.[[ParameterMap]]. + let map = obj + .borrow() + .as_mapped_arguments() + .expect("arguments exotic method must only be callable from arguments objects") + .parameter_map(); + + // b. Let isMapped be ! HasOwnProperty(map, P). + // 3. If isMapped is true, then + if map + .has_own_property(key.clone(), context) + .expect("HasOwnProperty must not fail per the spec") + { + // a. Let setStatus be Set(map, P, V, false). + let set_status = map.set(key.clone(), value.clone(), false, context)?; + + // b. Assert: setStatus is true because formal parameters mapped by argument objects are always writable. + debug_assert!(set_status); + } + } + + // 4. Return ? OrdinarySet(args, P, V, Receiver). + super::ordinary_set(obj, key, value, receiver, context) +} + +/// [[Delete]] for arguments exotic objects. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-delete-p +pub(crate) fn arguments_exotic_delete( + obj: &JsObject, + key: &PropertyKey, + context: &mut Context, +) -> JsResult { + // 1. Let map be args.[[ParameterMap]]. + let map = obj + .borrow() + .as_mapped_arguments() + .expect("arguments exotic method must only be callable from arguments objects") + .parameter_map(); + + // 2. Let isMapped be ! HasOwnProperty(map, P). + let is_mapped = map + .has_own_property(key.clone(), context) + .expect("HasOwnProperty must not fail per the spec"); + + // 3. Let result be ? OrdinaryDelete(args, P). + let result = super::ordinary_delete(obj, key, context)?; + + // 4. If result is true and isMapped is true, then + if is_mapped && result { + // a. Call map.[[Delete]](P). + map.__delete__(key, context)?; + } + + // 5. Return result. + Ok(result) +} diff --git a/boa/src/object/internal_methods/mod.rs b/boa/src/object/internal_methods/mod.rs index 220e031f196..70f6be4b89f 100644 --- a/boa/src/object/internal_methods/mod.rs +++ b/boa/src/object/internal_methods/mod.rs @@ -12,6 +12,7 @@ use crate::{ BoaProfiler, Context, JsResult, }; +pub(super) mod arguments; pub(super) mod array; pub(super) mod string; diff --git a/boa/src/object/mod.rs b/boa/src/object/mod.rs index 03f7ca024a7..68322447aca 100644 --- a/boa/src/object/mod.rs +++ b/boa/src/object/mod.rs @@ -17,6 +17,7 @@ use crate::{ property::{Attribute, PropertyDescriptor, PropertyKey}, BoaProfiler, Context, JsBigInt, JsResult, JsString, JsSymbol, JsValue, }; + use std::{ any::Any, fmt::{self, Debug, Display}, @@ -26,6 +27,7 @@ use std::{ #[cfg(test)] mod tests; +pub(crate) mod exotic; mod gcobject; pub(crate) mod internal_methods; mod operations; @@ -36,9 +38,12 @@ pub use gcobject::{JsObject, RecursionLimiter, Ref, RefMut}; use internal_methods::InternalObjectMethods; pub use property_map::*; -use self::internal_methods::{ - array::ARRAY_EXOTIC_INTERNAL_METHODS, string::STRING_EXOTIC_INTERNAL_METHODS, - ORDINARY_INTERNAL_METHODS, +use self::{ + exotic::arguments::{Arguments, MappedArguments}, + internal_methods::{ + arguments::ARGUMENTS_EXOTIC_INTERNAL_METHODS, array::ARRAY_EXOTIC_INTERNAL_METHODS, + string::STRING_EXOTIC_INTERNAL_METHODS, ORDINARY_INTERNAL_METHODS, + }, }; /// Static `prototype`, usually set on constructors as a key to point to their respective prototype object. @@ -109,6 +114,7 @@ pub enum ObjectKind { Ordinary, Date(Date), Global, + Arguments(Arguments), NativeObject(Box), } @@ -273,6 +279,16 @@ impl ObjectData { } } + pub fn arguments(args: Arguments) -> Self { + Self { + internal_methods: match &args { + Arguments::MappedArguments(_) => &ARGUMENTS_EXOTIC_INTERNAL_METHODS, + Arguments::UnmappedArguments => &ORDINARY_INTERNAL_METHODS, + }, + kind: ObjectKind::Arguments(args), + } + } + /// Create the `NativeObject` object data pub fn native_object(native_object: Box) -> Self { Self { @@ -308,6 +324,7 @@ impl Display for ObjectKind { Self::BigInt(_) => "BigInt", Self::Date(_) => "Date", Self::Global => "Global", + Self::Arguments(_) => "Arguments", Self::NativeObject(_) => "NativeObject", } ) @@ -862,6 +879,29 @@ impl Object { } } + /// Checks if it is an `Arguments` object. + #[inline] + pub fn is_arguments(&self) -> bool { + matches!( + self.data, + ObjectData { + kind: ObjectKind::Arguments(_), + .. + } + ) + } + + #[inline] + pub fn as_mapped_arguments(&self) -> Option<&MappedArguments> { + match self.data { + ObjectData { + kind: ObjectKind::Arguments(Arguments::MappedArguments(ref args)), + .. + } => Some(args), + _ => None, + } + } + /// Checks if it an ordinary object. #[inline] pub fn is_ordinary(&self) -> bool { @@ -1109,11 +1149,32 @@ impl<'context> FunctionBuilder<'context> { where F: Fn(&JsValue, &[JsValue], &mut Context) -> JsResult + Copy + 'static, { + Self::closure_with_captures( + context, + move |this, args, context, _| function(this, args, context), + (), + ) + } + + #[inline] + pub fn closure_with_captures( + context: &'context mut Context, + function: F, + captures: C, + ) -> Self + where + F: Fn(&JsValue, &[JsValue], &mut Context, &mut JsObject) -> JsResult + + Copy + + 'static, + C: NativeObject, + { + let captures = JsObject::new(Object::native_object(captures)); Self { context, function: Some(Function::Closure { function: Box::new(function), constructable: false, + captures, }), name: JsString::default(), length: 0,