diff --git a/boa/src/builtins/array/mod.rs b/boa/src/builtins/array/mod.rs index e79683cb1af..3927eac9184 100644 --- a/boa/src/builtins/array/mod.rs +++ b/boa/src/builtins/array/mod.rs @@ -49,13 +49,13 @@ impl BuiltIn for Array { let get_species = FunctionBuilder::native(context, Self::get_species) .name("get [Symbol.species]") - .constructable(false) + .constructor(false) .build(); let values_function = FunctionBuilder::native(context, Self::values) .name("values") .length(0) - .constructable(false) + .constructor(false) .build(); let array = ConstructorBuilder::with_standard_object( @@ -376,7 +376,7 @@ impl Array { // 7. If IsConstructor(C) is false, throw a TypeError exception. if let Some(c) = c.as_object() { - if !c.is_constructable() { + if !c.is_constructor() { return Err(context.construct_type_error("Symbol.species must be a constructor")); } // 8. Return ? Construct(C, « 𝔽(length) »). @@ -461,7 +461,7 @@ impl Array { // 5. Else, // a. Let A be ? ArrayCreate(len). let a = match this.as_object() { - Some(object) if object.is_constructable() => object + Some(object) if object.is_constructor() => object .construct(&[len.into()], this, context)? .as_object() .ok_or_else(|| { @@ -1072,9 +1072,12 @@ impl Array { let len = o.length_of_array_like(context)?; // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. let callback = args.get_or_undefined(0); - if !callback.is_function() { - return context.throw_type_error("Array.prototype.map: Callbackfn is not callable"); - } + let callback = match callback { + JsValue::Object(obj) if obj.is_callable() => obj, + _ => { + return context.throw_type_error("Array.prototype.map: Callbackfn is not callable") + } + }; // 4. Let A be ? ArraySpeciesCreate(O, len). let a = Self::array_species_create(&o, len, context)?; @@ -1093,7 +1096,7 @@ impl Array { let k_value = o.get(k, context)?; // ii. Let mappedValue be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »). let mapped_value = - context.call(callback, this_arg, &[k_value, k.into(), this.into()])?; + callback.call(this_arg, &[k_value, k.into(), this.into()], context)?; // iii. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue). a.create_data_property_or_throw(k, mapped_value, context)?; } @@ -1453,9 +1456,10 @@ impl Array { // 3. If ! IsCallable(mapperFunction) is false, throw a TypeError exception. let mapper_function = args.get_or_undefined(0); - if !mapper_function.is_function() { - return context.throw_type_error("flatMap mapper function is not callable"); - } + let mapper_function = match mapper_function { + JsValue::Object(obj) if obj.is_callable() => obj, + _ => return context.throw_type_error("flatMap mapper function is not callable"), + }; // 4. Let A be ? ArraySpeciesCreate(O, 0). let a = Self::array_species_create(&o, 0, context)?; @@ -1467,7 +1471,7 @@ impl Array { source_len as u64, 0, 1, - Some(mapper_function.as_object().unwrap()), + Some(mapper_function.clone()), args.get_or_undefined(1), context, )?; @@ -2110,10 +2114,9 @@ impl Array { context: &mut Context, ) -> JsResult { // 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception. - let comparefn = match args.get(0).cloned() { - // todo: change to `is_callable` inside `JsValue` - Some(fun) if fun.is_function() => fun, - None => JsValue::undefined(), + let comparefn = match args.get_or_undefined(0) { + JsValue::Object(ref obj) if obj.is_callable() => Some(obj), + JsValue::Undefined => None, _ => { return context.throw_type_error( "The comparison function must be either a function or undefined", @@ -2140,11 +2143,11 @@ impl Array { } // 4. If comparefn is not undefined, then - if !comparefn.is_undefined() { + if let Some(cmp) = comparefn { let args = [x.clone(), y.clone()]; // a. Let v be ? ToNumber(? Call(comparefn, undefined, « x, y »)). - let v = context - .call(&comparefn, &JsValue::Undefined, &args)? + let v = cmp + .call(&JsValue::Undefined, &args, context)? .to_number(context)?; // b. If v is NaN, return +0𝔽. // c. Return v. diff --git a/boa/src/builtins/function/mod.rs b/boa/src/builtins/function/mod.rs index a4ea906bb28..f04b554e9a4 100644 --- a/boa/src/builtins/function/mod.rs +++ b/boa/src/builtins/function/mod.rs @@ -13,7 +13,11 @@ use crate::context::StandardObjects; use crate::object::internal_methods::get_prototype_from_constructor; +use crate::JsString; +use crate::property::PropertyKey; +use crate::symbol::WellKnownSymbols; +use crate::value::IntegerOrInfinity; use crate::{ builtins::{Array, BuiltIn}, environment::lexical_environment::Environment, @@ -26,6 +30,7 @@ use crate::{ use bitflags::bitflags; use dyn_clone::DynClone; use sealed::Sealed; +use std::borrow::Cow; use std::fmt::{self, Debug}; use super::JsArgs; @@ -120,12 +125,12 @@ unsafe impl Trace for FunctionFlags { pub enum Function { Native { function: BuiltInFunction, - constructable: bool, + constructor: bool, }, Closure { #[unsafe_ignore_trace] function: Box, - constructable: bool, + constructor: bool, }, Ordinary { flags: FunctionFlags, @@ -187,11 +192,11 @@ impl Function { .expect("Failed to intialize binding"); } - /// Returns true if the function object is constructable. - pub fn is_constructable(&self) -> bool { + /// Returns true if the function object is a constructor. + pub fn is_constructor(&self) -> bool { match self { - Self::Native { constructable, .. } => *constructable, - Self::Closure { constructable, .. } => *constructable, + Self::Native { constructor, .. } => *constructor, + Self::Closure { constructor, .. } => *constructor, Self::Ordinary { flags, .. } => flags.is_constructable(), } } @@ -268,7 +273,7 @@ pub fn make_builtin_fn( let mut function = Object::function( Function::Native { function: function.into(), - constructable: false, + constructor: false, }, interpreter .standard_objects() @@ -314,36 +319,12 @@ impl BuiltInFunctionObject { this.set_data(ObjectData::function(Function::Native { function: BuiltInFunction(|_, _, _| Ok(JsValue::undefined())), - constructable: true, + constructor: true, })); Ok(this) } - fn prototype(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Ok(JsValue::undefined()) - } - - /// `Function.prototype.call` - /// - /// The call() method invokes self with the first argument as the `this` value. - /// - /// More information: - /// - [MDN documentation][mdn] - /// - [ECMAScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.call - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call - fn call(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { - if !this.is_function() { - return context.throw_type_error(format!("{} is not a function", this.display())); - } - let this_arg = args.get_or_undefined(0); - // TODO?: 3. Perform PrepareForTailCall - let start = if !args.is_empty() { 1 } else { 0 }; - context.call(this, this_arg, &args[start..]) - } - - /// `Function.prototype.apply` + /// `Function.prototype.apply ( thisArg, argArray )` /// /// The apply() method invokes self with the first argument as the `this` value /// and the rest of the arguments provided as an array (or an array-like object). @@ -355,18 +336,145 @@ impl BuiltInFunctionObject { /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.apply /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply fn apply(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { - if !this.is_function() { - return context.throw_type_error(format!("{} is not a function", this.display())); - } + // 1. Let func be the this value. + let func = match this { + JsValue::Object(obj) if obj.is_callable() => obj, + // 2. If IsCallable(func) is false, throw a TypeError exception. + _ => return context.throw_type_error(format!("{} is not a function", this.display())), + }; + let this_arg = args.get_or_undefined(0); let arg_array = args.get_or_undefined(1); + // 3. If argArray is undefined or null, then if arg_array.is_null_or_undefined() { + // a. Perform PrepareForTailCall(). // TODO?: 3.a. PrepareForTailCall - return context.call(this, this_arg, &[]); + + // b. Return ? Call(func, thisArg). + return func.call(this_arg, &[], context); } + + // 4. Let argList be ? CreateListFromArrayLike(argArray). let arg_list = arg_array.create_list_from_array_like(&[], context)?; + + // 5. Perform PrepareForTailCall(). // TODO?: 5. PrepareForTailCall - context.call(this, this_arg, &arg_list) + + // 6. Return ? Call(func, thisArg, argList). + func.call(this_arg, &arg_list, context) + } + + /// `Function.prototype.bind ( thisArg, ...args )` + /// + /// The bind() method creates a new function that, when called, has its + /// this keyword set to the provided value, with a given sequence of arguments + /// preceding any provided when the new function is called. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.bind + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind + fn bind(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let Target be the this value. + let target = match this { + JsValue::Object(obj) if obj.is_callable() => obj, + // 2. If IsCallable(Target) is false, throw a TypeError exception. + _ => { + return context + .throw_type_error("cannot bind `this` without a `[[Call]]` internal method") + } + }; + let this_arg = args.get_or_undefined(0).clone(); + let bound_args = args.get(1..).unwrap_or_else(|| &[]).to_vec(); + let arg_count = bound_args.len() as i64; + + // 3. Let F be ? BoundFunctionCreate(Target, thisArg, args). + let f = BoundFunction::create(target.clone(), this_arg, bound_args, context)?; + + // 4. Let L be 0. + let mut l = JsValue::new(0); + + // 5. Let targetHasLength be ? HasOwnProperty(Target, "length"). + // 6. If targetHasLength is true, then + if target.has_own_property("length", context)? { + // a. Let targetLen be ? Get(Target, "length"). + let target_len = target.get("length", context)?; + // b. If Type(targetLen) is Number, then + if target_len.is_number() { + // 1. Let targetLenAsInt be ! ToIntegerOrInfinity(targetLen). + match target_len + .to_integer_or_infinity(context) + .expect("to_integer_or_infinity cannot fail for a number") + { + // i. If targetLen is +∞𝔽, set L to +∞. + IntegerOrInfinity::PositiveInfinity => l = f64::INFINITY.into(), + // ii. Else if targetLen is -∞𝔽, set L to 0. + IntegerOrInfinity::NegativeInfinity => {} + // iii. Else, + IntegerOrInfinity::Integer(target_len) => { + // 2. Assert: targetLenAsInt is finite. + // 3. Let argCount be the number of elements in args. + // 4. Set L to max(targetLenAsInt - argCount, 0). + l = (target_len - arg_count).max(0).into(); + } + } + } + } + + // 7. Perform ! SetFunctionLength(F, L). + f.define_property_or_throw( + "length", + PropertyDescriptor::builder() + .value(l) + .writable(false) + .enumerable(false) + .configurable(true), + context, + ) + .expect("defining the `length` property for a new object should not fail"); + + // 8. Let targetName be ? Get(Target, "name"). + let target_name = target.get("name", context)?; + + // 9. If Type(targetName) is not String, set targetName to the empty String. + let target_name = target_name + .as_string() + .map_or(JsString::new(""), Clone::clone); + + // 10. Perform SetFunctionName(F, targetName, "bound"). + set_function_name(&f, &target_name.into(), Some("bound"), context); + + // 11. Return F. + Ok(f.into()) + } + + /// `Function.prototype.call ( thisArg, ...args )` + /// + /// The call() method calls a function with a given this value and arguments provided individually. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.call + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call + fn call(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let func be the this value. + let func = match this { + JsValue::Object(obj) if obj.is_callable() => obj, + // 2. If IsCallable(func) is false, throw a TypeError exception. + _ => return context.throw_type_error(format!("{} is not a function", this.display())), + }; + + let this_arg = args.get_or_undefined(0); + + // 3. Perform PrepareForTailCall(). + // TODO?: 3. Perform PrepareForTailCall + + // 4. Return ? Call(func, thisArg, args). + func.call(this_arg, args.get(1..).unwrap_or(&[]), context) } #[allow(clippy::wrong_self_convention)] @@ -398,7 +506,7 @@ impl BuiltInFunctionObject { ( Function::Native { function: _, - constructable: _, + constructor: _, }, Some(name), ) => Ok(format!("function {}() {{\n [native Code]\n}}", &name).into()), @@ -445,6 +553,22 @@ impl BuiltInFunctionObject { _ => Ok("TODO".into()), } } + + /// `Function.prototype [ @@hasInstance ] ( V )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function.prototype-@@hasinstance + fn has_instance(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let F be the this value. + // 2. Return ? OrdinaryHasInstance(F, V). + Ok(JsValue::ordinary_has_instance(this, args.get_or_undefined(0), context)?.into()) + } + + fn prototype(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Ok(JsValue::undefined()) + } } impl BuiltIn for BuiltInFunctionObject { @@ -461,9 +585,17 @@ impl BuiltIn for BuiltInFunctionObject { FunctionBuilder::native(context, Self::prototype) .name("") .length(0) - .constructable(false) + .constructor(false) .build_function_prototype(&function_prototype); + let symbol_has_instance = WellKnownSymbols::has_instance(); + + let has_instance = FunctionBuilder::native(context, Self::has_instance) + .name("[Symbol.iterator]") + .length(1) + .constructor(false) + .build(); + let function_object = ConstructorBuilder::with_standard_object( context, Self::constructor, @@ -471,11 +603,137 @@ impl BuiltIn for BuiltInFunctionObject { ) .name(Self::NAME) .length(Self::LENGTH) - .method(Self::call, "call", 1) .method(Self::apply, "apply", 1) + .method(Self::bind, "bind", 1) + .method(Self::call, "call", 1) .method(Self::to_string, "toString", 0) + .property(symbol_has_instance, has_instance, Attribute::default()) .build(); (Self::NAME, function_object.into(), Self::attribute()) } } + +/// Abstract operation `SetFunctionName` +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-setfunctionname +fn set_function_name( + function: &JsObject, + name: &PropertyKey, + prefix: Option<&str>, + context: &mut Context, +) { + // 1. Assert: F is an extensible object that does not have a "name" own property. + // 2. If Type(name) is Symbol, then + let mut name = match name { + PropertyKey::Symbol(sym) => { + // a. Let description be name's [[Description]] value. + if let Some(desc) = sym.description() { + // c. Else, set name to the string-concatenation of "[", description, and "]". + Cow::Owned(JsString::concat_array(&["[", &desc, "]"])) + } else { + // b. If description is undefined, set name to the empty String. + Cow::Owned(JsString::new("")) + } + } + PropertyKey::String(string) => Cow::Borrowed(string), + PropertyKey::Index(index) => Cow::Owned(JsString::new(format!("{}", index))), + }; + + // 3. Else if name is a Private Name, then + // a. Set name to name.[[Description]]. + // todo: implement Private Names + + // 4. If F has an [[InitialName]] internal slot, then + // a. Set F.[[InitialName]] to name. + // todo: implement [[InitialName]] for builtins + + // 5. If prefix is present, then + if let Some(prefix) = prefix { + name = Cow::Owned(JsString::concat_array(&[prefix, " ", &name])); + // b. If F has an [[InitialName]] internal slot, then + // i. Optionally, set F.[[InitialName]] to name. + // todo: implement [[InitialName]] for builtins + } + + // 6. Return ! DefinePropertyOrThrow(F, "name", PropertyDescriptor { [[Value]]: name, + // [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }). + function + .define_property_or_throw( + "name", + PropertyDescriptor::builder() + .value(name.into_owned()) + .writable(false) + .enumerable(false) + .configurable(true), + context, + ) + .expect("defining the `name` property must not fail per the spec"); +} + +/// Binds a `Function Object` when `bind` is called. +#[derive(Debug, Trace, Finalize)] +pub struct BoundFunction { + target_function: JsObject, + this: JsValue, + args: Vec, +} + +impl BoundFunction { + /// Abstract operation `BoundFunctionCreate` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-boundfunctioncreate + pub fn create( + target_function: JsObject, + this: JsValue, + args: Vec, + context: &mut Context, + ) -> JsResult { + // 1. Let proto be ? targetFunction.[[GetPrototypeOf]](). + let proto = target_function.__get_prototype_of__(context)?; + let is_constructor = target_function.is_constructor(); + + // 2. Let internalSlotsList be the internal slots listed in Table 35, plus [[Prototype]] and [[Extensible]]. + // 3. Let obj be ! MakeBasicObject(internalSlotsList). + // 4. Set obj.[[Prototype]] to proto. + // 5. Set obj.[[Call]] as described in 10.4.1.1. + // 6. If IsConstructor(targetFunction) is true, then + // a. Set obj.[[Construct]] as described in 10.4.1.2. + // 7. Set obj.[[BoundTargetFunction]] to targetFunction. + // 8. Set obj.[[BoundThis]] to boundThis. + // 9. Set obj.[[BoundArguments]] to boundArgs. + // 10. Return obj. + Ok(JsObject::new(Object::with_prototype( + proto, + ObjectData::bound_function( + BoundFunction { + target_function, + this, + args, + }, + is_constructor, + ), + ))) + } + + /// Get a reference to the bound function's this. + pub fn this(&self) -> &JsValue { + &self.this + } + + /// Get a reference to the bound function's target function. + pub fn target_function(&self) -> &JsObject { + &self.target_function + } + + /// Get a reference to the bound function's args. + pub fn args(&self) -> &[JsValue] { + self.args.as_slice() + } +} diff --git a/boa/src/builtins/map/mod.rs b/boa/src/builtins/map/mod.rs index 31a553dd6e8..d8f38415a7f 100644 --- a/boa/src/builtins/map/mod.rs +++ b/boa/src/builtins/map/mod.rs @@ -53,19 +53,19 @@ impl BuiltIn for Map { let get_species = FunctionBuilder::native(context, Self::get_species) .name("get [Symbol.species]") - .constructable(false) + .constructor(false) .build(); let get_size = FunctionBuilder::native(context, Self::get_size) .name("get size") .length(0) - .constructable(false) + .constructor(false) .build(); let entries_function = FunctionBuilder::native(context, Self::entries) .name("entries") .length(0) - .constructable(false) + .constructor(false) .build(); let map_object = ConstructorBuilder::with_standard_object( diff --git a/boa/src/builtins/reflect/mod.rs b/boa/src/builtins/reflect/mod.rs index c699f572bff..b9669de8d2f 100644 --- a/boa/src/builtins/reflect/mod.rs +++ b/boa/src/builtins/reflect/mod.rs @@ -110,12 +110,12 @@ impl Reflect { .ok_or_else(|| context.construct_type_error("target must be a function"))?; let args_list = args.get_or_undefined(1); - if !target.is_constructable() { + if !target.is_constructor() { return context.throw_type_error("target must be a constructor"); } let new_target = if let Some(new_target) = args.get(2) { - if new_target.as_object().map(|o| o.is_constructable()) != Some(true) { + if new_target.as_object().map(|o| o.is_constructor()) != Some(true) { return context.throw_type_error("newTarget must be constructor"); } new_target.clone() diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index ca49d3b648e..8f5f1630104 100644 --- a/boa/src/builtins/regexp/mod.rs +++ b/boa/src/builtins/regexp/mod.rs @@ -80,42 +80,42 @@ impl BuiltIn for RegExp { let get_species = FunctionBuilder::native(context, Self::get_species) .name("get [Symbol.species]") - .constructable(false) + .constructor(false) .build(); let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; let get_global = FunctionBuilder::native(context, Self::get_global) .name("get global") - .constructable(false) + .constructor(false) .build(); let get_ignore_case = FunctionBuilder::native(context, Self::get_ignore_case) .name("get ignoreCase") - .constructable(false) + .constructor(false) .build(); let get_multiline = FunctionBuilder::native(context, Self::get_multiline) .name("get multiline") - .constructable(false) + .constructor(false) .build(); let get_dot_all = FunctionBuilder::native(context, Self::get_dot_all) .name("get dotAll") - .constructable(false) + .constructor(false) .build(); let get_unicode = FunctionBuilder::native(context, Self::get_unicode) .name("get unicode") - .constructable(false) + .constructor(false) .build(); let get_sticky = FunctionBuilder::native(context, Self::get_sticky) .name("get sticky") - .constructable(false) + .constructor(false) .build(); let get_flags = FunctionBuilder::native(context, Self::get_flags) .name("get flags") - .constructable(false) + .constructor(false) .build(); let get_source = FunctionBuilder::native(context, Self::get_source) .name("get source") - .constructable(false) + .constructor(false) .build(); let regexp_object = ConstructorBuilder::with_standard_object( context, @@ -785,22 +785,24 @@ impl RegExp { // 2. Assert: Type(S) is String. // 3. Let exec be ? Get(R, "exec"). - let exec = this.get_field("exec", context)?; + let exec = object.get("exec", context)?; // 4. If IsCallable(exec) is true, then - if exec.is_function() { - // a. Let result be ? Call(exec, R, « S »). - let result = context.call(&exec, this, &[input.into()])?; + match exec { + JsValue::Object(ref obj) if obj.is_callable() => { + // a. Let result be ? Call(exec, R, « S »). + let result = context.call(&exec, this, &[input.into()])?; + + // b. If Type(result) is neither Object nor Null, throw a TypeError exception. + if !result.is_object() && !result.is_null() { + return Err(context + .construct_type_error("regexp exec returned neither object nor null")); + } - // b. If Type(result) is neither Object nor Null, throw a TypeError exception. - if !result.is_object() && !result.is_null() { - return Err( - context.construct_type_error("regexp exec returned neither object nor null") - ); + // c. Return result. + return Ok(result.as_object()); } - - // c. Return result. - return Ok(result.as_object()); + _ => {} } // 5. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]). @@ -1281,7 +1283,10 @@ impl RegExp { // 5. Let functionalReplace be IsCallable(replaceValue). let mut replace_value = args.get_or_undefined(1).clone(); - let functional_replace = replace_value.is_function(); + let functional_replace = replace_value + .as_object() + .map(|obj| obj.is_callable()) + .unwrap_or_default(); // 6. If functionalReplace is false, then if !functional_replace { diff --git a/boa/src/builtins/set/mod.rs b/boa/src/builtins/set/mod.rs index 971f3130e70..4ed34a54970 100644 --- a/boa/src/builtins/set/mod.rs +++ b/boa/src/builtins/set/mod.rs @@ -47,11 +47,11 @@ impl BuiltIn for Set { let get_species = FunctionBuilder::native(context, Self::get_species) .name("get [Symbol.species]") - .constructable(false) + .constructor(false) .build(); let size_getter = FunctionBuilder::native(context, Self::size_getter) - .constructable(false) + .constructor(false) .name("get size") .build(); @@ -62,7 +62,7 @@ impl BuiltIn for Set { let values_function = FunctionBuilder::native(context, Self::values) .name("values") .length(0) - .constructable(false) + .constructor(false) .build(); let set_object = ConstructorBuilder::with_standard_object( @@ -147,9 +147,10 @@ impl Set { let adder = set.get_field("add", context)?; // 6 - if !adder.is_function() { - return context.throw_type_error("'add' of 'newTarget' is not a function"); - } + let adder = match adder { + JsValue::Object(ref obj) if obj.is_callable() => obj, + _ => return context.throw_type_error("'add' of 'newTarget' is not a function"), + }; // 7 let iterator_record = get_iterator(iterable, context)?; @@ -163,7 +164,7 @@ impl Set { let next_value = next.value; // d, e - if let Err(status) = context.call(&adder, &set, &[next_value]) { + if let Err(status) = adder.call(&set, &[next_value], context) { return iterator_record.close(Err(status), context); } diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index 303526718cf..cf82d77bd3b 100644 --- a/boa/src/builtins/string/mod.rs +++ b/boa/src/builtins/string/mod.rs @@ -751,7 +751,10 @@ impl String { let search_str = search_value.to_string(context)?; // 5. Let functionalReplace be IsCallable(replaceValue). - let functional_replace = replace_value.is_function(); + let functional_replace = replace_value + .as_object() + .map(|obj| obj.is_callable()) + .unwrap_or_default(); // 6. If functionalReplace is false, then // a. Set replaceValue to ? ToString(replaceValue). @@ -881,7 +884,10 @@ impl String { let search_string = search_value.to_string(context)?; // 5. Let functionalReplace be IsCallable(replaceValue). - let functional_replace = replace_value.is_function(); + let functional_replace = replace_value + .as_object() + .map(|obj| obj.is_callable()) + .unwrap_or_default(); // 6. If functionalReplace is false, then let replace_value_string = if !functional_replace { diff --git a/boa/src/builtins/symbol/mod.rs b/boa/src/builtins/symbol/mod.rs index e7a91cd8989..13b02c409a1 100644 --- a/boa/src/builtins/symbol/mod.rs +++ b/boa/src/builtins/symbol/mod.rs @@ -101,7 +101,7 @@ impl BuiltIn for Symbol { let get_description = FunctionBuilder::native(context, Self::get_description) .name("get description") - .constructable(false) + .constructor(false) .build(); let symbol_object = ConstructorBuilder::with_standard_object( diff --git a/boa/src/context.rs b/boa/src/context.rs index 5cb0792ceee..8aded49381e 100644 --- a/boa/src/context.rs +++ b/boa/src/context.rs @@ -652,7 +652,7 @@ impl Context { let function = FunctionBuilder::native(self, body) .name(name) .length(length) - .constructable(true) + .constructor(true) .build(); self.global_object().insert_property( @@ -686,7 +686,7 @@ impl Context { let function = FunctionBuilder::closure(self, body) .name(name) .length(length) - .constructable(true) + .constructor(true) .build(); self.global_object().insert_property( diff --git a/boa/src/object/gcobject.rs b/boa/src/object/gcobject.rs index 217de3ce5f7..226081ebbe4 100644 --- a/boa/src/object/gcobject.rs +++ b/boa/src/object/gcobject.rs @@ -2,22 +2,12 @@ //! //! The `JsObject` is a garbage collected Object. -use super::{NativeObject, Object, PROTOTYPE}; +use super::{NativeObject, Object}; use crate::{ - builtins::function::{ - create_unmapped_arguments_object, ClosureFunction, Function, NativeFunction, - }, - environment::{ - environment_record_trait::EnvironmentRecordTrait, - function_environment_record::{BindingStatus, FunctionEnvironmentRecord}, - lexical_environment::Environment, - }, - exec::InterpreterState, object::{ObjectData, ObjectKind}, property::{PropertyDescriptor, PropertyKey}, - syntax::ast::node::RcStatementList, value::PreferredType, - Context, Executable, JsResult, JsValue, + Context, JsResult, JsValue, }; use gc::{Finalize, Gc, GcCell, GcCellRef, GcCellRefMut, Trace}; use std::{ @@ -38,17 +28,6 @@ pub type RefMut<'a, T, U> = GcCellRefMut<'a, T, U>; #[derive(Trace, Finalize, Clone, Default)] pub struct JsObject(Gc>); -/// The body of a JavaScript function. -/// -/// This is needed for the call method since we cannot mutate the function itself since we -/// already borrow it so we get the function body clone it then drop the borrow and run the body -enum FunctionBody { - BuiltInFunction(NativeFunction), - BuiltInConstructor(NativeFunction), - Closure(Box), - Ordinary(RcStatementList), -} - impl JsObject { /// Create a new `GcObject` from a `Object`. #[inline] @@ -111,229 +90,6 @@ impl JsObject { std::ptr::eq(lhs.as_ref(), rhs.as_ref()) } - /// Internal implementation of [`call`](#method.call) and [`construct`](#method.construct). - /// - /// # Panics - /// - /// Panics if the object is currently mutably borrowed. - /// - /// - /// - /// - /// - #[track_caller] - pub(super) fn call_construct( - &self, - this_target: &JsValue, - args: &[JsValue], - context: &mut Context, - construct: bool, - ) -> JsResult { - let this_function_object = self.clone(); - let mut has_parameter_expressions = false; - - let body = if let Some(function) = self.borrow().as_function() { - if construct && !function.is_constructable() { - let name = self - .__get__(&"name".into(), self.clone().into(), context)? - .display() - .to_string(); - return context.throw_type_error(format!("{} is not a constructor", name)); - } else { - match function { - Function::Native { - function, - constructable, - } => { - if *constructable || construct { - FunctionBody::BuiltInConstructor(function.0) - } else { - FunctionBody::BuiltInFunction(function.0) - } - } - Function::Closure { function, .. } => FunctionBody::Closure(function.clone()), - Function::Ordinary { - body, - params, - environment, - flags, - } => { - let this = if construct { - // If the prototype of the constructor is not an object, then use the default object - // prototype as prototype for the new object - // see - // see - let proto = this_target.as_object().unwrap().__get__( - &PROTOTYPE.into(), - this_target.clone(), - context, - )?; - let proto = if proto.is_object() { - proto - } else { - context - .standard_objects() - .object_object() - .prototype() - .into() - }; - JsValue::new(Object::create(proto)) - } else { - this_target.clone() - }; - - // Create a new Function environment whose parent is set to the scope of the function declaration (self.environment) - // - let local_env = FunctionEnvironmentRecord::new( - this_function_object.clone(), - if construct || !flags.is_lexical_this_mode() { - Some(this.clone()) - } else { - None - }, - Some(environment.clone()), - // Arrow functions do not have a this binding https://tc39.es/ecma262/#sec-function-environment-records - if flags.is_lexical_this_mode() { - BindingStatus::Lexical - } else { - BindingStatus::Uninitialized - }, - JsValue::undefined(), - context, - )?; - - let mut arguments_in_parameter_names = false; - - 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"; - } - - // 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) - // - If there are default parameters or if lexical names and function names do not contain `arguments` (10.2.11.18) - // - // https://tc39.es/ecma262/#sec-functiondeclarationinstantiation - if !flags.is_lexical_this_mode() - && !arguments_in_parameter_names - && (has_parameter_expressions - || (!body.lexically_declared_names().contains("arguments") - && !body.function_declared_names().contains("arguments"))) - { - // Add arguments object - let arguments_obj = create_unmapped_arguments_object(args, context)?; - local_env.create_mutable_binding("arguments", false, true, context)?; - local_env.initialize_binding("arguments", arguments_obj, 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()); - - // Add argument bindings to the function environment - for (i, param) in params.iter().enumerate() { - // Rest Parameters - if param.is_rest_param() { - function.add_rest_param(param, i, args, context, &local_env); - break; - } - - let value = match args.get(i).cloned() { - None | Some(JsValue::Undefined) => param - .init() - .map(|init| init.run(context).ok()) - .flatten() - .unwrap_or_default(), - Some(value) => value, - }; - - function - .add_arguments_to_environment(param, value, &local_env, context); - } - - if has_parameter_expressions { - // Create a second environment when default parameter expressions are used - // This prevents variables declared in the function body from being - // used in default parameter initializers. - // https://tc39.es/ecma262/#sec-functiondeclarationinstantiation - let second_env = FunctionEnvironmentRecord::new( - this_function_object, - if construct || !flags.is_lexical_this_mode() { - Some(this) - } else { - None - }, - Some(local_env), - // Arrow functions do not have a this binding https://tc39.es/ecma262/#sec-function-environment-records - if flags.is_lexical_this_mode() { - BindingStatus::Lexical - } else { - BindingStatus::Uninitialized - }, - JsValue::undefined(), - context, - )?; - context.push_environment(second_env); - } - - FunctionBody::Ordinary(body.clone()) - } - } - } - } else { - return context.throw_type_error("not a function"); - }; - - match body { - FunctionBody::BuiltInConstructor(function) if construct => { - function(this_target, args, context) - } - FunctionBody::BuiltInConstructor(function) => { - function(&JsValue::undefined(), args, context) - } - FunctionBody::BuiltInFunction(function) => function(this_target, args, context), - FunctionBody::Closure(function) => (function)(this_target, args, context), - FunctionBody::Ordinary(body) => { - let result = body.run(context); - let this = context.get_this_binding(); - - if has_parameter_expressions { - context.pop_environment(); - } - context.pop_environment(); - - if construct { - // https://tc39.es/ecma262/#sec-ecmascript-function-objects-construct-argumentslist-newtarget - // 12. If result.[[Type]] is return, then - if context.executor().get_current_state() == &InterpreterState::Return { - // a. If Type(result.[[Value]]) is Object, return NormalCompletion(result.[[Value]]). - if let Ok(v) = &result { - if v.is_object() { - return result; - } - } - } - - // 13. Else, ReturnIfAbrupt(result). - result?; - - // 14. Return ? constructorEnv.GetThisBinding(). - this - } else if context.executor().get_current_state() == &InterpreterState::Return { - result - } else { - result?; - Ok(JsValue::undefined()) - } - } - } - } - /// Converts an object to a primitive. /// /// Diverges from the spec to prevent a stack overflow when the object is recursive. @@ -387,18 +143,20 @@ impl JsObject { }; // 5. For each name in methodNames in List order, do - let this = JsValue::new(self.clone()); for name in &method_names { // a. Let method be ? Get(O, name). - let method: JsValue = this.get_field(*name, context)?; + let method = self.get(*name, context)?; // b. If IsCallable(method) is true, then - if method.is_function() { - // i. Let result be ? Call(method, O). - let result = context.call(&method, &this, &[])?; - // ii. If Type(result) is not Object, return result. - if !result.is_object() { - return Ok(result); + match method { + JsValue::Object(ref method) if method.is_callable() => { + // i. Let result be ? Call(method, O). + let result = method.call(&self.clone().into(), &[], context)?; + // ii. If Type(result) is not Object, return result. + if !result.is_object() { + return Ok(result); + } } + _ => {} } } @@ -626,55 +384,6 @@ impl JsObject { self.borrow().is_native_object() } - /// Determines if `value` inherits from the instance object inheritance path. - /// - /// More information: - /// - [EcmaScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-ordinaryhasinstance - #[inline] - pub(crate) fn ordinary_has_instance( - &self, - context: &mut Context, - value: &JsValue, - ) -> JsResult { - // 1. If IsCallable(C) is false, return false. - if !self.is_callable() { - return Ok(false); - } - - // TODO: 2. If C has a [[BoundTargetFunction]] internal slot, then - // a. Let BC be C.[[BoundTargetFunction]]. - // b. Return ? InstanceofOperator(O, BC). - - // 3. If Type(O) is not Object, return false. - if let Some(object) = value.as_object() { - // 4. Let P be ? Get(C, "prototype"). - // 5. If Type(P) is not Object, throw a TypeError exception. - if let Some(prototype) = self.get("prototype", context)?.as_object() { - // 6. Repeat, - // a. Set O to ? O.[[GetPrototypeOf]](). - // b. If O is null, return false. - let mut object = object.__get_prototype_of__(context)?; - while let Some(object_prototype) = object.as_object() { - // c. If SameValue(P, O) is true, return true. - if JsObject::equals(&prototype, &object_prototype) { - return Ok(true); - } - // a. Set O to ? O.[[GetPrototypeOf]](). - object = object_prototype.__get_prototype_of__(context)?; - } - - Ok(false) - } else { - Err(context - .construct_type_error("function has non-object prototype in instanceof check")) - } - } else { - Ok(false) - } - } - pub fn to_property_descriptor(&self, context: &mut Context) -> JsResult { // 1 is implemented on the method `to_property_descriptor` of value @@ -868,7 +577,7 @@ impl JsObject { #[inline] #[track_caller] pub fn is_callable(&self) -> bool { - self.borrow().is_callable() + self.borrow().data.internal_methods.__call__.is_some() } /// It determines if Object is a function object with a `[[Construct]]` internal method. @@ -879,8 +588,8 @@ impl JsObject { /// [spec]: https://tc39.es/ecma262/#sec-isconstructor #[inline] #[track_caller] - pub fn is_constructable(&self) -> bool { - self.borrow().is_constructable() + pub fn is_constructor(&self) -> bool { + self.borrow().data.internal_methods.__construct__.is_some() } /// Returns true if the GcObject is the global for a Realm diff --git a/boa/src/object/internal_methods/bound_function.rs b/boa/src/object/internal_methods/bound_function.rs new file mode 100644 index 00000000000..3949cd93d3b --- /dev/null +++ b/boa/src/object/internal_methods/bound_function.rs @@ -0,0 +1,100 @@ +use crate::{object::JsObject, Context, JsResult, JsValue}; + +use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; + +/// Definitions of the internal object methods for function objects. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects +pub(crate) static BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = + InternalObjectMethods { + __call__: Some(bound_function_exotic_call), + __construct__: None, + ..ORDINARY_INTERNAL_METHODS + }; + +pub(crate) static BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = + InternalObjectMethods { + __call__: Some(bound_function_exotic_call), + __construct__: Some(bound_function_exotic_construct), + ..ORDINARY_INTERNAL_METHODS + }; + +/// Internal method `[[Call]]` for Bound Function Exotic Objects +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-bound-function-exotic-objects-call-thisargument-argumentslist +#[track_caller] +#[inline] +fn bound_function_exotic_call( + obj: &JsObject, + _: &JsValue, + arguments_list: &[JsValue], + context: &mut Context, +) -> JsResult { + let obj = obj.borrow(); + let bound_function = obj + .as_bound_function() + .expect("bound function exotic method should only be callable from bound function objects"); + + // 1. Let target be F.[[BoundTargetFunction]]. + let target = bound_function.target_function(); + + // 2. Let boundThis be F.[[BoundThis]]. + let bound_this = bound_function.this(); + + // 3. Let boundArgs be F.[[BoundArguments]]. + let bound_args = bound_function.args(); + + // 4. Let args be the list-concatenation of boundArgs and argumentsList. + let mut args = bound_args.to_vec(); + args.extend_from_slice(arguments_list); + + // 5. Return ? Call(target, boundThis, args). + target.call(bound_this, &args, context) +} + +/// Internal method `[[Construct]]` for Bound Function Exotic Objects +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-bound-function-exotic-objects-construct-argumentslist-newtarget +#[track_caller] +#[inline] +fn bound_function_exotic_construct( + obj: &JsObject, + arguments_list: &[JsValue], + new_target: &JsValue, + context: &mut Context, +) -> JsResult { + let object = obj.borrow(); + let bound_function = object + .as_bound_function() + .expect("bound function exotic method should only be callable from bound function objects"); + + // 1. Let target be F.[[BoundTargetFunction]]. + let target = bound_function.target_function(); + + // 2. Assert: IsConstructor(target) is true. + + // 3. Let boundArgs be F.[[BoundArguments]]. + let bound_args = bound_function.args(); + + // 4. Let args be the list-concatenation of boundArgs and argumentsList. + let mut args = bound_args.to_vec(); + args.extend_from_slice(arguments_list); + + // 5. If SameValue(F, newTarget) is true, set newTarget to target. + let new_target = match new_target { + JsValue::Object(new_target) if JsObject::equals(obj, new_target) => target.clone().into(), + _ => new_target.clone(), + }; + + // 6. Return ? Construct(target, args, newTarget). + target.construct(&args, &new_target, context) +} diff --git a/boa/src/object/internal_methods/function.rs b/boa/src/object/internal_methods/function.rs new file mode 100644 index 00000000000..1bd1b09b939 --- /dev/null +++ b/boa/src/object/internal_methods/function.rs @@ -0,0 +1,302 @@ +use crate::{ + builtins::function::{ + create_unmapped_arguments_object, ClosureFunction, Function, NativeFunction, + }, + environment::{ + environment_record_trait::EnvironmentRecordTrait, + function_environment_record::{BindingStatus, FunctionEnvironmentRecord}, + lexical_environment::Environment, + }, + exec::{Executable, InterpreterState}, + object::{JsObject, Object, PROTOTYPE}, + syntax::ast::node::RcStatementList, + Context, JsResult, JsValue, +}; + +use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; + +/// Definitions of the internal object methods for function objects. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects +pub(crate) static FUNCTION_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods { + __call__: Some(function_call), + __construct__: None, + ..ORDINARY_INTERNAL_METHODS +}; + +pub(crate) static CONSTRUCTOR_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods { + __call__: Some(function_call), + __construct__: Some(function_construct), + ..ORDINARY_INTERNAL_METHODS +}; + +/// The body of a JavaScript function. +/// +/// This is needed for the call method since we cannot mutate the function itobj since we +/// already borrow it so we get the function body clone it then drop the borrow and run the body +enum FunctionBody { + BuiltInFunction(NativeFunction), + BuiltInConstructor(NativeFunction), + Closure(Box), + Ordinary(RcStatementList), +} + +/// Call this object. +/// +/// # Panics +/// +/// Panics if the object is currently mutably borrowed. +// +// +#[track_caller] +#[inline] +fn function_call( + obj: &JsObject, + this: &JsValue, + args: &[JsValue], + context: &mut Context, +) -> JsResult { + call_construct(obj, this, args, context, false) +} + +/// Construct an instance of this object with the specified arguments. +/// +/// # Panics +/// +/// Panics if the object is currently mutably borrowed. +// +#[track_caller] +#[inline] +fn function_construct( + obj: &JsObject, + args: &[JsValue], + new_target: &JsValue, + context: &mut Context, +) -> JsResult { + call_construct(obj, new_target, args, context, true) +} + +/// Internal implementation of [`call`](#method.call) and [`construct`](#method.construct). +/// +/// # Panics +/// +/// Panics if the object is currently mutably borrowed. +/// +/// +/// +/// +/// +#[track_caller] +fn call_construct( + obj: &JsObject, + this_target: &JsValue, + args: &[JsValue], + context: &mut Context, + construct: bool, +) -> JsResult { + let this_function_object = obj.clone(); + let mut has_parameter_expressions = false; + + let body = if let Some(function) = obj.borrow().as_function() { + if construct && !function.is_constructor() { + let name = obj + .__get__(&"name".into(), obj.clone().into(), context)? + .display() + .to_string(); + return context.throw_type_error(format!("{} is not a constructor", name)); + } else { + match function { + Function::Native { + function, + constructor: constructable, + } => { + if *constructable || construct { + FunctionBody::BuiltInConstructor(function.0) + } else { + FunctionBody::BuiltInFunction(function.0) + } + } + Function::Closure { function, .. } => FunctionBody::Closure(function.clone()), + Function::Ordinary { + body, + params, + environment, + flags, + } => { + let this = if construct { + // If the prototype of the constructor is not an object, then use the default object + // prototype as prototype for the new object + // see + // see + let proto = this_target.as_object().unwrap().__get__( + &PROTOTYPE.into(), + this_target.clone(), + context, + )?; + let proto = if proto.is_object() { + proto + } else { + context + .standard_objects() + .object_object() + .prototype() + .into() + }; + JsValue::new(Object::create(proto)) + } else { + this_target.clone() + }; + + // Create a new Function environment whose parent is set to the scope of the function declaration (obj.environment) + // + let local_env = FunctionEnvironmentRecord::new( + this_function_object.clone(), + if construct || !flags.is_lexical_this_mode() { + Some(this.clone()) + } else { + None + }, + Some(environment.clone()), + // Arrow functions do not have a this binding https://tc39.es/ecma262/#sec-function-environment-records + if flags.is_lexical_this_mode() { + BindingStatus::Lexical + } else { + BindingStatus::Uninitialized + }, + JsValue::undefined(), + context, + )?; + + let mut arguments_in_parameter_names = false; + + 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"; + } + + // 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) + // - If there are default parameters or if lexical names and function names do not contain `arguments` (10.2.11.18) + // + // https://tc39.es/ecma262/#sec-functiondeclarationinstantiation + if !flags.is_lexical_this_mode() + && !arguments_in_parameter_names + && (has_parameter_expressions + || (!body.lexically_declared_names().contains("arguments") + && !body.function_declared_names().contains("arguments"))) + { + // Add arguments object + let arguments_obj = create_unmapped_arguments_object(args, context)?; + local_env.create_mutable_binding("arguments", false, true, context)?; + local_env.initialize_binding("arguments", arguments_obj, 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()); + + // Add argument bindings to the function environment + for (i, param) in params.iter().enumerate() { + // Rest Parameters + if param.is_rest_param() { + function.add_rest_param(param, i, args, context, &local_env); + break; + } + + let value = match args.get(i).cloned() { + None | Some(JsValue::Undefined) => param + .init() + .map(|init| init.run(context).ok()) + .flatten() + .unwrap_or_default(), + Some(value) => value, + }; + + function.add_arguments_to_environment(param, value, &local_env, context); + } + + if has_parameter_expressions { + // Create a second environment when default parameter expressions are used + // This prevents variables declared in the function body from being + // used in default parameter initializers. + // https://tc39.es/ecma262/#sec-functiondeclarationinstantiation + let second_env = FunctionEnvironmentRecord::new( + this_function_object, + if construct || !flags.is_lexical_this_mode() { + Some(this) + } else { + None + }, + Some(local_env), + // Arrow functions do not have a this binding https://tc39.es/ecma262/#sec-function-environment-records + if flags.is_lexical_this_mode() { + BindingStatus::Lexical + } else { + BindingStatus::Uninitialized + }, + JsValue::undefined(), + context, + )?; + context.push_environment(second_env); + } + + FunctionBody::Ordinary(body.clone()) + } + } + } + } else { + return context.throw_type_error("not a function"); + }; + + match body { + FunctionBody::BuiltInConstructor(function) if construct => { + function(this_target, args, context) + } + FunctionBody::BuiltInConstructor(function) => { + function(&JsValue::undefined(), args, context) + } + FunctionBody::BuiltInFunction(function) => function(this_target, args, context), + FunctionBody::Closure(function) => (function)(this_target, args, context), + FunctionBody::Ordinary(body) => { + let result = body.run(context); + let this = context.get_this_binding(); + + if has_parameter_expressions { + context.pop_environment(); + } + context.pop_environment(); + + if construct { + // https://tc39.es/ecma262/#sec-ecmascript-function-objects-construct-argumentslist-newtarget + // 12. If result.[[Type]] is return, then + if context.executor().get_current_state() == &InterpreterState::Return { + // a. If Type(result.[[Value]]) is Object, return NormalCompletion(result.[[Value]]). + if let Ok(v) = &result { + if v.is_object() { + return result; + } + } + } + + // 13. Else, ReturnIfAbrupt(result). + result?; + + // 14. Return ? constructorEnv.GetThisBinding(). + this + } else if context.executor().get_current_state() == &InterpreterState::Return { + result + } else { + result?; + Ok(JsValue::undefined()) + } + } + } +} diff --git a/boa/src/object/internal_methods/mod.rs b/boa/src/object/internal_methods/mod.rs index 154008411b4..2691dd973a3 100644 --- a/boa/src/object/internal_methods/mod.rs +++ b/boa/src/object/internal_methods/mod.rs @@ -16,6 +16,8 @@ use crate::{ use super::PROTOTYPE; pub(super) mod array; +pub(super) mod bound_function; +pub(super) mod function; pub(super) mod string; impl JsObject { @@ -207,6 +209,50 @@ impl JsObject { let func = self.borrow().data.internal_methods.__own_property_keys__; func(self, context) } + + /// Internal method `[[Call]]` + /// + /// Call this object if it has a `[[Call]]` internal method. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects-call-thisargument-argumentslist + #[inline] + #[track_caller] + pub(crate) fn __call__( + &self, + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let func = self.borrow().data.internal_methods.__call__; + func.expect("called `[[Call]]` for object without a `[[Call]]` internal method")( + self, this, args, context, + ) + } + + /// Internal method `[[Construct]]` + /// + /// Construct a new instance of this object if this object has a `[[Construct]]` internal method. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects-construct-argumentslist-newtarget + #[inline] + #[track_caller] + pub(crate) fn __construct__( + &self, + args: &[JsValue], + new_target: &JsValue, + context: &mut Context, + ) -> JsResult { + let func = self.borrow().data.internal_methods.__construct__; + func.expect("called `[[Construct]]` for object without a `[[Construct]]` internal method")( + self, args, new_target, context, + ) + } } /// Definitions of the internal object methods for ordinary objects. @@ -232,6 +278,8 @@ pub(crate) static ORDINARY_INTERNAL_METHODS: InternalObjectMethods = InternalObj __set__: ordinary_set, __delete__: ordinary_delete, __own_property_keys__: ordinary_own_property_keys, + __call__: None, + __construct__: None, }; /// The internal representation of the internal methods of a `JsObject`. @@ -242,6 +290,7 @@ pub(crate) static ORDINARY_INTERNAL_METHODS: InternalObjectMethods = InternalObj /// /// For a guide on how to implement exotic internal methods, see `ORDINARY_INTERNAL_METHODS`. #[derive(Clone, Copy)] +#[allow(clippy::type_complexity)] pub(crate) struct InternalObjectMethods { pub(crate) __get_prototype_of__: fn(&JsObject, &mut Context) -> JsResult, pub(crate) __set_prototype_of__: fn(&JsObject, JsValue, &mut Context) -> JsResult, @@ -257,6 +306,10 @@ pub(crate) struct InternalObjectMethods { fn(&JsObject, PropertyKey, JsValue, JsValue, &mut Context) -> JsResult, pub(crate) __delete__: fn(&JsObject, &PropertyKey, &mut Context) -> JsResult, pub(crate) __own_property_keys__: fn(&JsObject, &mut Context) -> JsResult>, + pub(crate) __call__: + Option JsResult>, + pub(crate) __construct__: + Option JsResult>, } /// Abstract operation `OrdinaryGetPrototypeOf`. diff --git a/boa/src/object/mod.rs b/boa/src/object/mod.rs index c4a01fcc208..c10869cddc3 100644 --- a/boa/src/object/mod.rs +++ b/boa/src/object/mod.rs @@ -3,7 +3,7 @@ use crate::{ builtins::{ array::array_iterator::ArrayIterator, - function::{Function, NativeFunction}, + function::{BoundFunction, Function, NativeFunction}, map::map_iterator::MapIterator, map::ordered_map::OrderedMap, regexp::regexp_string_iterator::RegExpStringIterator, @@ -38,7 +38,12 @@ pub use operations::IntegrityLevel; pub use property_map::*; use self::internal_methods::{ - array::ARRAY_EXOTIC_INTERNAL_METHODS, string::STRING_EXOTIC_INTERNAL_METHODS, + array::ARRAY_EXOTIC_INTERNAL_METHODS, + bound_function::{ + BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS, BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS, + }, + function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS}, + string::STRING_EXOTIC_INTERNAL_METHODS, ORDINARY_INTERNAL_METHODS, }; @@ -100,6 +105,7 @@ pub enum ObjectKind { Boolean(bool), ForInIterator(ForInIterator), Function(Function), + BoundFunction(BoundFunction), Set(OrderedSet), SetIterator(SetIterator), String(JsString), @@ -189,8 +195,24 @@ impl ObjectData { /// Create the `Function` object data pub fn function(function: Function) -> Self { Self { + internal_methods: if function.is_constructor() { + &CONSTRUCTOR_INTERNAL_METHODS + } else { + &FUNCTION_INTERNAL_METHODS + }, kind: ObjectKind::Function(function), - internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + + /// Create the `BoundFunction` object data + pub fn bound_function(bound_function: BoundFunction, constructor: bool) -> Self { + Self { + kind: ObjectKind::BoundFunction(bound_function), + internal_methods: if constructor { + &BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS + } else { + &BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS + }, } } @@ -293,6 +315,7 @@ impl Display for ObjectKind { Self::ArrayIterator(_) => "ArrayIterator", Self::ForInIterator(_) => "ForInIterator", Self::Function(_) => "Function", + Self::BoundFunction(_) => "BoundFunction", Self::RegExp(_) => "RegExp", Self::RegExpStringIterator(_) => "RegExpStringIterator", Self::Map(_) => "Map", @@ -436,38 +459,6 @@ impl Object { &self.data.kind } - /// It determines if Object is a callable function with a `[[Call]]` internal method. - /// - /// More information: - /// - [EcmaScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-iscallable - #[inline] - // todo: functions are not the only objects that are callable. - // todo: e.g. https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-call-thisargument-argumentslist - pub fn is_callable(&self) -> bool { - matches!( - self.data, - ObjectData { - kind: ObjectKind::Function(_), - .. - } - ) - } - - /// It determines if Object is a function object with a `[[Construct]]` internal method. - /// - /// More information: - /// - [EcmaScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-isconstructor - #[inline] - // todo: functions are not the only objects that are constructable. - // todo: e.g. https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-construct-argumentslist-newtarget - pub fn is_constructable(&self) -> bool { - matches!(self.data, ObjectData{kind: ObjectKind::Function(ref f), ..} if f.is_constructable()) - } - /// Checks if it an `Array` object. #[inline] pub fn is_array(&self) -> bool { @@ -726,6 +717,17 @@ impl Object { } } + #[inline] + pub fn as_bound_function(&self) -> Option<&BoundFunction> { + match self.data { + ObjectData { + kind: ObjectKind::BoundFunction(ref bound_function), + .. + } => Some(bound_function), + _ => None, + } + } + /// Checks if it a Symbol object. #[inline] pub fn is_symbol(&self) -> bool { @@ -1119,7 +1121,7 @@ impl<'context> FunctionBuilder<'context> { context, function: Some(Function::Native { function: function.into(), - constructable: false, + constructor: false, }), name: JsString::default(), length: 0, @@ -1136,7 +1138,7 @@ impl<'context> FunctionBuilder<'context> { context, function: Some(Function::Closure { function: Box::new(function), - constructable: false, + constructor: false, }), name: JsString::default(), length: 0, @@ -1170,10 +1172,10 @@ impl<'context> FunctionBuilder<'context> { /// /// The default is `false`. #[inline] - pub fn constructable(&mut self, yes: bool) -> &mut Self { + pub fn constructor(&mut self, yes: bool) -> &mut Self { match self.function.as_mut() { - Some(Function::Native { constructable, .. }) => *constructable = yes, - Some(Function::Closure { constructable, .. }) => *constructable = yes, + Some(Function::Native { constructor, .. }) => *constructor = yes, + Some(Function::Closure { constructor, .. }) => *constructor = yes, _ => unreachable!(), } self @@ -1275,7 +1277,7 @@ impl<'context> ObjectInitializer<'context> { let function = FunctionBuilder::native(self.context, function) .name(binding.name) .length(length) - .constructable(false) + .constructor(false) .build(); self.object.borrow_mut().insert_property( @@ -1385,7 +1387,7 @@ impl<'context> ConstructorBuilder<'context> { let function = FunctionBuilder::native(self.context, function) .name(binding.name) .length(length) - .constructable(false) + .constructor(false) .build(); self.prototype.borrow_mut().insert_property( @@ -1414,7 +1416,7 @@ impl<'context> ConstructorBuilder<'context> { let function = FunctionBuilder::native(self.context, function) .name(binding.name) .length(length) - .constructable(false) + .constructor(false) .build(); self.constructor_object.borrow_mut().insert_property( @@ -1586,7 +1588,7 @@ impl<'context> ConstructorBuilder<'context> { // Create the native function let function = Function::Native { function: self.constructor_function.into(), - constructable: self.constructable, + constructor: self.constructable, }; let length = PropertyDescriptor::builder() diff --git a/boa/src/object/operations.rs b/boa/src/object/operations.rs index d4938419ee8..e028f2f4a87 100644 --- a/boa/src/object/operations.rs +++ b/boa/src/object/operations.rs @@ -293,7 +293,13 @@ impl JsObject { args: &[JsValue], context: &mut Context, ) -> JsResult { - self.call_construct(this, args, context, false) + // 1. If argumentsList is not present, set argumentsList to a new empty List. + // 2. If IsCallable(F) is false, throw a TypeError exception. + if !self.is_callable() { + return context.throw_type_error("not a function"); + } + // 3. Return ? F.[[Call]](V, argumentsList). + self.__call__(this, args, context) } /// Construct an instance of this object with the specified arguments. @@ -310,7 +316,10 @@ impl JsObject { new_target: &JsValue, context: &mut Context, ) -> JsResult { - self.call_construct(new_target, args, context, true) + // 1. If newTarget is not present, set newTarget to F. + // 2. If argumentsList is not present, set argumentsList to a new empty List. + // 3. Return ? F.[[Construct]](argumentsList, newTarget). + self.__construct__(args, new_target, context) } /// Make the object [`sealed`][IntegrityLevel::Sealed] or [`frozen`][IntegrityLevel::Frozen]. @@ -481,7 +490,7 @@ impl JsObject { // 7. If IsConstructor(S) is true, return S. // 8. Throw a TypeError exception. if let Some(obj) = s.as_object() { - if obj.is_constructable() { + if obj.is_constructor() { Ok(s) } else { context.throw_type_error("property 'constructor' is not a constructor") @@ -634,4 +643,67 @@ impl JsValue { // 7. Return list. Ok(list) } + + /// Abstract operation `OrdinaryHasInstance ( C, O )` + /// + /// More information: + /// - [EcmaScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-ordinaryhasinstance + pub fn ordinary_has_instance( + function: &JsValue, + object: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. If IsCallable(C) is false, return false. + let function = match function { + JsValue::Object(obj) if obj.is_callable() => obj, + _ => return Ok(false), + }; + + // 2. If C has a [[BoundTargetFunction]] internal slot, then + if let Some(bound_function) = function.borrow().as_bound_function() { + // a. Let BC be C.[[BoundTargetFunction]]. + // b. Return ? InstanceofOperator(O, BC). + return JsValue::instance_of( + object, + &bound_function.target_function().clone().into(), + context, + ); + } + + let mut object = if let Some(obj) = object.as_object() { + obj + } else { + // 3. If Type(O) is not Object, return false. + return Ok(false); + }; + + // 4. Let P be ? Get(C, "prototype"). + let prototype = function.get("prototype", context)?; + + let prototype = if let Some(obj) = prototype.as_object() { + obj + } else { + // 5. If Type(P) is not Object, throw a TypeError exception. + return Err(context + .construct_type_error("function has non-object prototype in instanceof check")); + }; + + // 6. Repeat, + loop { + // a. Set O to ? O.[[GetPrototypeOf]](). + object = match object.__get_prototype_of__(context)? { + JsValue::Object(ref obj) => obj.clone(), + // b. If O is null, return false. + JsValue::Null => return Ok(false), + _ => unreachable!(), + }; + + // c. If SameValue(P, O) is true, return true. + if JsObject::equals(&object, &prototype) { + return Ok(true); + } + } + } } diff --git a/boa/src/syntax/ast/node/new/mod.rs b/boa/src/syntax/ast/node/new/mod.rs index c9fafe9cc81..bb1f96a1fab 100644 --- a/boa/src/syntax/ast/node/new/mod.rs +++ b/boa/src/syntax/ast/node/new/mod.rs @@ -72,7 +72,7 @@ impl Executable for New { } match func_object { - JsValue::Object(ref object) => { + JsValue::Object(ref object) if object.is_constructor() => { object.construct(&v_args, &object.clone().into(), context) } _ => context diff --git a/boa/src/syntax/ast/node/operator/bin_op/mod.rs b/boa/src/syntax/ast/node/operator/bin_op/mod.rs index 7531017d0a2..321cb7d6051 100644 --- a/boa/src/syntax/ast/node/operator/bin_op/mod.rs +++ b/boa/src/syntax/ast/node/operator/bin_op/mod.rs @@ -1,7 +1,6 @@ use crate::{ exec::Executable, gc::{Finalize, Trace}, - symbol::WellKnownSymbols, syntax::ast::{ node::Node, op::{self, AssignOp, BitOp, CompOp, LogOp, NumOp}, @@ -146,30 +145,7 @@ impl Executable for BinOp { let key = x.to_property_key(context)?; context.has_property(&y, &key)? } - CompOp::InstanceOf => { - if let Some(object) = y.as_object() { - let key = WellKnownSymbols::has_instance(); - - match object.get_method(context, key)? { - Some(instance_of_handler) => { - instance_of_handler.call(&y, &[x], context)?.to_boolean() - } - None if object.is_callable() => { - object.ordinary_has_instance(context, &x)? - } - None => { - return context.throw_type_error( - "right-hand side of 'instanceof' is not callable", - ); - } - } - } else { - return context.throw_type_error(format!( - "right-hand side of 'instanceof' should be an object, got {}", - y.type_of() - )); - } - } + CompOp::InstanceOf => x.instance_of(&y, context)?, })) } op::BinOp::Log(op) => Ok(match op { diff --git a/boa/src/value/mod.rs b/boa/src/value/mod.rs index 5e7ab67ec0c..25405cd6f56 100644 --- a/boa/src/value/mod.rs +++ b/boa/src/value/mod.rs @@ -141,12 +141,6 @@ impl JsValue { } } - /// Returns true if the value is a function - #[inline] - pub fn is_function(&self) -> bool { - matches!(self, Self::Object(o) if o.is_function()) - } - /// Returns true if the value is undefined. #[inline] pub fn is_undefined(&self) -> bool { diff --git a/boa/src/value/operations.rs b/boa/src/value/operations.rs index 23f6dde28c5..cfd1dae3aeb 100644 --- a/boa/src/value/operations.rs +++ b/boa/src/value/operations.rs @@ -388,6 +388,45 @@ impl JsValue { }) } + /// Abstract operation `InstanceofOperator ( V, target )` + /// + /// More information: + /// - [EcmaScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-instanceofoperator + #[inline] + pub fn instance_of(&self, target: &JsValue, context: &mut Context) -> JsResult { + if let Some(object) = target.as_object() { + let key = WellKnownSymbols::has_instance(); + + // 2. Let instOfHandler be ? GetMethod(target, @@hasInstance). + match object.get_method(context, key)? { + // 3. If instOfHandler is not undefined, then + Some(instance_of_handler) => { + // a. Return ! ToBoolean(? Call(instOfHandler, target, « V »)). + Ok(instance_of_handler + .call(target, std::slice::from_ref(self), context)? + .to_boolean()) + } + None if object.is_callable() => { + // 5. Return ? OrdinaryHasInstance(target, V). + JsValue::ordinary_has_instance(target, self, context) + } + None => { + // 4. If IsCallable(target) is false, throw a TypeError exception. + Err(context + .construct_type_error("right-hand side of 'instanceof' is not callable")) + } + } + } else { + // 1. If Type(target) is not Object, throw a TypeError exception. + Err(context.construct_type_error(format!( + "right-hand side of 'instanceof' should be an object, got {}", + target.type_of() + ))) + } + } + #[inline] pub fn neg(&self, context: &mut Context) -> JsResult { Ok(match *self { diff --git a/boa/src/vm/mod.rs b/boa/src/vm/mod.rs index d9f2e263d33..756dbdba15b 100644 --- a/boa/src/vm/mod.rs +++ b/boa/src/vm/mod.rs @@ -3,8 +3,8 @@ //! plus an interpreter to execute those instructions use crate::{ - builtins::Array, environment::lexical_environment::VariableScope, symbol::WellKnownSymbols, - BoaProfiler, Context, JsResult, JsValue, + builtins::Array, environment::lexical_environment::VariableScope, BoaProfiler, Context, + JsResult, JsValue, }; mod code_block; @@ -198,28 +198,7 @@ impl<'a> Vm<'a> { Opcode::InstanceOf => { let y = self.pop(); let x = self.pop(); - let value = if let Some(object) = y.as_object() { - let key = WellKnownSymbols::has_instance(); - - match object.get_method(self.context, key)? { - Some(instance_of_handler) => instance_of_handler - .call(&y, &[x], self.context)? - .to_boolean(), - None if object.is_callable() => { - object.ordinary_has_instance(self.context, &x)? - } - None => { - return Err(self.context.construct_type_error( - "right-hand side of 'instanceof' is not callable", - )); - } - } - } else { - return Err(self.context.construct_type_error(format!( - "right-hand side of 'instanceof' should be an object, got {}", - y.type_of() - ))); - }; + let value = x.instance_of(&y, self.context)?; self.push(value); }