diff --git a/boa_engine/src/builtins/function/mod.rs b/boa_engine/src/builtins/function/mod.rs index ffe64b21531..1abfcc9ba52 100644 --- a/boa_engine/src/builtins/function/mod.rs +++ b/boa_engine/src/builtins/function/mod.rs @@ -453,6 +453,11 @@ impl Function { pub(crate) fn kind_mut(&mut self) -> &mut FunctionKind { &mut self.kind } + + /// Check if `self` is [`FunctionKind::Native`]. + pub(crate) const fn is_native(&self) -> bool { + matches!(self.kind(), FunctionKind::Native { .. }) + } } /// The internal representation of a `Function` object. diff --git a/boa_engine/src/object/builtins/jsfunction.rs b/boa_engine/src/object/builtins/jsfunction.rs index 541873e1651..d252b84bcd8 100644 --- a/boa_engine/src/object/builtins/jsfunction.rs +++ b/boa_engine/src/object/builtins/jsfunction.rs @@ -1,7 +1,9 @@ //! A Rust API wrapper for Boa's `Function` Builtin ECMAScript Object use crate::{ object::{ - internal_methods::function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS}, + internal_methods::function::{ + NATIVE_CONSTRUCTOR_INTERNAL_METHODS, NATIVE_FUNCTION_INTERNAL_METHODS, + }, JsObject, JsObjectType, Object, }, value::TryFromJs, @@ -32,9 +34,9 @@ impl JsFunction { inner: JsObject::from_object_and_vtable( Object::default(), if constructor { - &CONSTRUCTOR_INTERNAL_METHODS + &NATIVE_CONSTRUCTOR_INTERNAL_METHODS } else { - &FUNCTION_INTERNAL_METHODS + &NATIVE_FUNCTION_INTERNAL_METHODS }, ), } diff --git a/boa_engine/src/object/internal_methods/bound_function.rs b/boa_engine/src/object/internal_methods/bound_function.rs index 8fe52973561..cd39eac0cdd 100644 --- a/boa_engine/src/object/internal_methods/bound_function.rs +++ b/boa_engine/src/object/internal_methods/bound_function.rs @@ -10,14 +10,14 @@ use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; /// [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), + __call__: bound_function_exotic_call, ..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), + __call__: bound_function_exotic_call, + __construct__: bound_function_exotic_construct, ..ORDINARY_INTERNAL_METHODS }; diff --git a/boa_engine/src/object/internal_methods/function.rs b/boa_engine/src/object/internal_methods/function.rs index 2b32875a841..011e68c4e06 100644 --- a/boa_engine/src/object/internal_methods/function.rs +++ b/boa_engine/src/object/internal_methods/function.rs @@ -1,11 +1,15 @@ use crate::{ + builtins::function::FunctionKind, + context::intrinsics::StandardConstructors, object::{ internal_methods::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}, - JsObject, + JsObject, ObjectData, }, - Context, JsResult, JsValue, + Context, JsNativeError, JsResult, JsValue, }; +use super::get_prototype_from_constructor; + /// Definitions of the internal object methods for function objects. /// /// More information: @@ -13,14 +17,13 @@ use crate::{ /// /// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects pub(crate) static FUNCTION_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods { - __call__: Some(function_call), - __construct__: None, + __call__: function_call, ..ORDINARY_INTERNAL_METHODS }; pub(crate) static CONSTRUCTOR_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods { - __call__: Some(function_call), - __construct__: Some(function_construct), + __call__: function_call, + __construct__: function_construct, ..ORDINARY_INTERNAL_METHODS }; @@ -31,7 +34,6 @@ pub(crate) static CONSTRUCTOR_INTERNAL_METHODS: InternalObjectMethods = Internal /// Panics if the object is currently mutably borrowed. // // -#[track_caller] fn function_call( obj: &JsObject, this: &JsValue, @@ -47,7 +49,6 @@ fn function_call( /// /// Panics if the object is currently mutably borrowed. // -#[track_caller] fn function_construct( obj: &JsObject, args: &[JsValue], @@ -56,3 +57,134 @@ fn function_construct( ) -> JsResult { obj.construct_internal(args, &new_target.clone().into(), context) } + +/// Definitions of the internal object methods for native function objects. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects +pub(crate) static NATIVE_FUNCTION_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods { + __call__: native_function_call, + ..ORDINARY_INTERNAL_METHODS +}; + +pub(crate) static NATIVE_CONSTRUCTOR_INTERNAL_METHODS: InternalObjectMethods = + InternalObjectMethods { + __call__: native_function_call, + __construct__: native_function_construct, + ..ORDINARY_INTERNAL_METHODS + }; + +/// Call this object. +/// +/// # Panics +/// +/// Panics if the object is currently mutably borrowed. +/// +// +#[track_caller] +pub(crate) fn native_function_call( + obj: &JsObject, + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, +) -> JsResult { + let this_function_object = obj.clone(); + let object = obj.borrow(); + let function_object = object.as_function().expect("not a function"); + let mut realm = function_object.realm().clone(); + + let FunctionKind::Native { + function, + constructor, + } = function_object.kind() + else { + unreachable!("the object should be a native function object") + }; + + let function = function.clone(); + let constructor = *constructor; + drop(object); + + context.swap_realm(&mut realm); + context.vm.native_active_function = Some(this_function_object); + + let result = if constructor.is_some() { + function.call(&JsValue::undefined(), args, context) + } else { + function.call(this, args, context) + } + .map_err(|err| err.inject_realm(context.realm().clone())); + + context.vm.native_active_function = None; + context.swap_realm(&mut realm); + + result +} + +/// Construct an instance of this object with the specified arguments. +/// +/// # Panics +/// +/// Panics if the object is currently mutably borrowed. +// +#[track_caller] +fn native_function_construct( + obj: &JsObject, + args: &[JsValue], + new_target: &JsObject, + context: &mut Context<'_>, +) -> JsResult { + let this_function_object = obj.clone(); + let object = obj.borrow(); + let function_object = object.as_function().expect("not a function"); + let mut realm = function_object.realm().clone(); + + let FunctionKind::Native { + function, + constructor, + .. + } = function_object.kind() + else { + unreachable!("the object should be a native function object"); + }; + + let function = function.clone(); + let constructor = *constructor; + drop(object); + + context.swap_realm(&mut realm); + context.vm.native_active_function = Some(this_function_object); + + let new_target = new_target.clone().into(); + let result = function + .call(&new_target, args, context) + .map_err(|err| err.inject_realm(context.realm().clone())) + .and_then(|v| match v { + JsValue::Object(ref o) => Ok(o.clone()), + val => { + if constructor.expect("must be a constructor").is_base() || val.is_undefined() { + let prototype = get_prototype_from_constructor( + &new_target, + StandardConstructors::object, + context, + )?; + Ok(JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::ordinary(), + )) + } else { + Err(JsNativeError::typ() + .with_message("derived constructor can only return an Object or undefined") + .into()) + } + } + }); + + context.vm.native_active_function = None; + context.swap_realm(&mut realm); + + result +} diff --git a/boa_engine/src/object/internal_methods/mod.rs b/boa_engine/src/object/internal_methods/mod.rs index 9e51f2369e9..0abb1f857f9 100644 --- a/boa_engine/src/object/internal_methods/mod.rs +++ b/boa_engine/src/object/internal_methods/mod.rs @@ -11,7 +11,7 @@ use crate::{ object::JsObject, property::{DescriptorKind, PropertyDescriptor, PropertyKey}, value::JsValue, - Context, JsResult, + Context, JsNativeError, JsResult, }; use boa_profiler::Profiler; @@ -225,11 +225,7 @@ impl JsObject { context: &mut Context<'_>, ) -> JsResult { let _timer = Profiler::global().start_event("Object::__call__", "object"); - self.vtable() - .__call__ - .expect("called `[[Call]]` for object without a `[[Call]]` internal method")( - self, this, args, context, - ) + (self.vtable().__call__)(self, this, args, context) } /// Internal method `[[Construct]]` @@ -248,11 +244,7 @@ impl JsObject { context: &mut Context<'_>, ) -> JsResult { let _timer = Profiler::global().start_event("Object::__construct__", "object"); - self.vtable() - .__construct__ - .expect("called `[[Construct]]` for object without a `[[Construct]]` internal method")( - self, args, new_target, context, - ) + (self.vtable().__construct__)(self, args, new_target, context) } } @@ -279,8 +271,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, + __call__: non_existant_call, + __construct__: non_existant_construct, }; /// The internal representation of the internal methods of a `JsObject`. @@ -307,10 +299,9 @@ 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) __call__: fn(&JsObject, &JsValue, &[JsValue], &mut Context<'_>) -> JsResult, pub(crate) __construct__: - Option) -> JsResult>, + fn(&JsObject, &[JsValue], &JsObject, &mut Context<'_>) -> JsResult, } /// Abstract operation `OrdinaryGetPrototypeOf`. @@ -919,3 +910,27 @@ where // b. Set proto to realm's intrinsic object named intrinsicDefaultProto. Ok(default(realm.intrinsics().constructors()).prototype()) } + +pub(crate) fn non_existant_call( + _obj: &JsObject, + _this: &JsValue, + _args: &[JsValue], + context: &mut Context<'_>, +) -> JsResult { + Err(JsNativeError::typ() + .with_message("only callable objects / functions can be called") + .with_realm(context.realm().clone()) + .into()) +} + +pub(crate) fn non_existant_construct( + _obj: &JsObject, + _args: &[JsValue], + _new_target: &JsObject, + context: &mut Context<'_>, +) -> JsResult { + Err(JsNativeError::typ() + .with_message("object is not constructable") + .with_realm(context.realm().clone()) + .into()) +} diff --git a/boa_engine/src/object/internal_methods/proxy.rs b/boa_engine/src/object/internal_methods/proxy.rs index 6874d8d5d9d..4ebe9554dce 100644 --- a/boa_engine/src/object/internal_methods/proxy.rs +++ b/boa_engine/src/object/internal_methods/proxy.rs @@ -9,6 +9,8 @@ use crate::{ }; use rustc_hash::FxHashSet; +use super::ORDINARY_INTERNAL_METHODS; + /// Definitions of the internal object methods for array exotic objects. /// /// More information: @@ -28,20 +30,19 @@ pub(crate) static PROXY_EXOTIC_INTERNAL_METHODS_BASIC: InternalObjectMethods = __set__: proxy_exotic_set, __delete__: proxy_exotic_delete, __own_property_keys__: proxy_exotic_own_property_keys, - __call__: None, - __construct__: None, + ..ORDINARY_INTERNAL_METHODS }; pub(crate) static PROXY_EXOTIC_INTERNAL_METHODS_WITH_CALL: InternalObjectMethods = InternalObjectMethods { - __call__: Some(proxy_exotic_call), + __call__: proxy_exotic_call, ..PROXY_EXOTIC_INTERNAL_METHODS_BASIC }; pub(crate) static PROXY_EXOTIC_INTERNAL_METHODS_ALL: InternalObjectMethods = InternalObjectMethods { - __call__: Some(proxy_exotic_call), - __construct__: Some(proxy_exotic_construct), + __call__: proxy_exotic_call, + __construct__: proxy_exotic_construct, ..PROXY_EXOTIC_INTERNAL_METHODS_BASIC }; diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index 8d0e56440f6..54d706714c5 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -3,7 +3,10 @@ //! The `JsObject` is a garbage collected Object. use super::{ - internal_methods::{InternalObjectMethods, ARRAY_EXOTIC_INTERNAL_METHODS}, + internal_methods::{ + non_existant_call, non_existant_construct, InternalObjectMethods, + ARRAY_EXOTIC_INTERNAL_METHODS, + }, shape::RootShape, JsPrototype, NativeObject, Object, PrivateName, PropertyMap, }; @@ -955,9 +958,9 @@ Cannot both specify accessors and a value or writable attribute", /// [spec]: https://tc39.es/ecma262/#sec-iscallable #[inline] #[must_use] - #[track_caller] + #[allow(clippy::fn_address_comparisons)] pub fn is_callable(&self) -> bool { - self.inner.vtable.__call__.is_some() + self.inner.vtable.__call__ != non_existant_call } /// It determines if Object is a function object with a `[[Construct]]` internal method. @@ -968,9 +971,9 @@ Cannot both specify accessors and a value or writable attribute", /// [spec]: https://tc39.es/ecma262/#sec-isconstructor #[inline] #[must_use] - #[track_caller] + #[allow(clippy::fn_address_comparisons)] pub fn is_constructor(&self) -> bool { - self.inner.vtable.__construct__.is_some() + self.inner.vtable.__construct__ != non_existant_construct } /// Returns true if the `JsObject` is the global for a Realm diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 6c1f71d4b6a..954864372cf 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -14,7 +14,10 @@ use self::{ bound_function::{ BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS, BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS, }, - function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS}, + function::{ + CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS, + NATIVE_CONSTRUCTOR_INTERNAL_METHODS, NATIVE_FUNCTION_INTERNAL_METHODS, + }, immutable_prototype::IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS, integer_indexed::INTEGER_INDEXED_EXOTIC_INTERNAL_METHODS, module_namespace::MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS, @@ -518,8 +521,13 @@ impl ObjectData { /// Create the `AsyncGeneratorFunction` object data #[must_use] pub fn async_generator_function(function: Function) -> Self { + let internal_methods = if function.is_native() { + &NATIVE_FUNCTION_INTERNAL_METHODS + } else { + &FUNCTION_INTERNAL_METHODS + }; Self { - internal_methods: &FUNCTION_INTERNAL_METHODS, + internal_methods, kind: ObjectKind::GeneratorFunction(function), } } @@ -635,12 +643,15 @@ impl ObjectData { /// Create the `Function` object data #[must_use] pub fn function(function: Function, constructor: bool) -> Self { + let internal_methods = match (constructor, function.is_native()) { + (false, false) => &FUNCTION_INTERNAL_METHODS, + (false, true) => &NATIVE_FUNCTION_INTERNAL_METHODS, + (true, false) => &CONSTRUCTOR_INTERNAL_METHODS, + (true, true) => &NATIVE_CONSTRUCTOR_INTERNAL_METHODS, + }; + Self { - internal_methods: if constructor { - &CONSTRUCTOR_INTERNAL_METHODS - } else { - &FUNCTION_INTERNAL_METHODS - }, + internal_methods, kind: ObjectKind::Function(function), } } @@ -670,8 +681,13 @@ impl ObjectData { /// Create the `GeneratorFunction` object data #[must_use] pub fn generator_function(function: Function) -> Self { + let internal_methods = if function.is_native() { + &NATIVE_FUNCTION_INTERNAL_METHODS + } else { + &FUNCTION_INTERNAL_METHODS + }; Self { - internal_methods: &FUNCTION_INTERNAL_METHODS, + internal_methods, kind: ObjectKind::GeneratorFunction(function), } } diff --git a/boa_engine/src/object/operations.rs b/boa_engine/src/object/operations.rs index a2861c47016..57ddbd16d55 100644 --- a/boa_engine/src/object/operations.rs +++ b/boa_engine/src/object/operations.rs @@ -2,7 +2,7 @@ use crate::{ builtins::{function::ClassFieldDefinition, Array}, context::intrinsics::{StandardConstructor, StandardConstructors}, error::JsNativeError, - object::{JsFunction, JsObject, PrivateElement, PrivateName, CONSTRUCTOR, PROTOTYPE}, + object::{JsObject, PrivateElement, PrivateName, CONSTRUCTOR, PROTOTYPE}, property::{PropertyDescriptor, PropertyDescriptorBuilder, PropertyKey, PropertyNameKind}, realm::Realm, string::utf16, @@ -315,14 +315,14 @@ impl JsObject { args: &[JsValue], context: &mut Context<'_>, ) -> JsResult { - // 1. If argumentsList is not present, set argumentsList to a new empty List. - // 2. If IsCallable(F) is false, throw a TypeError exception. - let function = JsFunction::from_object(self.clone()).ok_or_else(|| { - JsNativeError::typ().with_message("only callable objects / functions can be called") - })?; + // SKIP: 1. If argumentsList is not present, set argumentsList to a new empty List. + // SKIP: 2. If IsCallable(F) is false, throw a TypeError exception. + + // NOTE(HalidOdat): For object's that are not callable we implement a special __call__ internal method + // that throws on call. // 3. Return ? F.[[Call]](V, argumentsList). - function.__call__(this, args, context) + self.__call__(this, args, context) } /// `Construct ( F [ , argumentsList [ , newTarget ] ] )` @@ -1153,14 +1153,16 @@ impl JsValue { args: &[Self], context: &mut Context<'_>, ) -> JsResult { - self.as_callable() - .ok_or_else(|| { - JsNativeError::typ().with_message(format!( + let Some(object) = self.as_object() else { + return Err(JsNativeError::typ() + .with_message(format!( "value with type `{}` is not callable", self.type_of() )) - })? - .__call__(this, args, context) + .into()); + }; + + object.__call__(this, args, context) } /// Abstract operation `( V, P [ , argumentsList ] )` diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index aa689f7b212..d93693f4556 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -1020,26 +1020,8 @@ impl JsObject { let (code, mut environments, class_object, script_or_module) = match function_object.kind() { - FunctionKind::Native { - function, - constructor, - } => { - let function = function.clone(); - let constructor = *constructor; - drop(object); - - context.vm.native_active_function = Some(this_function_object); - - let result = if constructor.is_some() { - function.call(&JsValue::undefined(), args, context) - } else { - function.call(this, args, context) - } - .map_err(|err| err.inject_realm(context.realm().clone())); - - context.vm.native_active_function = None; - - return result; + FunctionKind::Native { .. } => { + unreachable!("should be handled in native function call internal method") } FunctionKind::Ordinary { code, @@ -1228,49 +1210,8 @@ impl JsObject { context.enter_realm(realm); match function_object.kind() { - FunctionKind::Native { - function, - constructor, - .. - } => { - let function = function.clone(); - let constructor = *constructor; - drop(object); - - context.vm.native_active_function = Some(this_function_object); - - let result = function - .call(this_target, args, context) - .map_err(|err| err.inject_realm(context.realm().clone())) - .and_then(|v| match v { - JsValue::Object(ref o) => Ok(o.clone()), - val => { - if constructor.expect("must be a constructor").is_base() - || val.is_undefined() - { - let prototype = get_prototype_from_constructor( - this_target, - StandardConstructors::object, - context, - )?; - Ok(Self::from_proto_and_data_with_shared_shape( - context.root_shape(), - prototype, - ObjectData::ordinary(), - )) - } else { - Err(JsNativeError::typ() - .with_message( - "derived constructor can only return an Object or undefined", - ) - .into()) - } - } - }); - - context.vm.native_active_function = None; - - result + FunctionKind::Native { .. } => { + unreachable!("should be handled in native function construct internal method") } FunctionKind::Ordinary { code, diff --git a/boa_engine/src/vm/opcode/call/mod.rs b/boa_engine/src/vm/opcode/call/mod.rs index 6002dafc9e5..1c279533bbc 100644 --- a/boa_engine/src/vm/opcode/call/mod.rs +++ b/boa_engine/src/vm/opcode/call/mod.rs @@ -42,13 +42,10 @@ impl Operation for CallEval { let func = context.vm.pop(); let this = context.vm.pop(); - let object = match func { - JsValue::Object(ref object) if object.is_callable() => object.clone(), - _ => { - return Err(JsNativeError::typ() - .with_message("not a callable function") - .into()); - } + let Some(object) = func.as_object() else { + return Err(JsNativeError::typ() + .with_message("not a callable function") + .into()); }; // A native function with the name "eval" implies, that is this the built-in eval function. @@ -116,13 +113,10 @@ impl Operation for CallEvalSpread { let func = context.vm.pop(); let this = context.vm.pop(); - let object = match func { - JsValue::Object(ref object) if object.is_callable() => object.clone(), - _ => { - return Err(JsNativeError::typ() - .with_message("not a callable function") - .into()); - } + let Some(object) = func.as_object() else { + return Err(JsNativeError::typ() + .with_message("not a callable function") + .into()); }; // A native function with the name "eval" implies, that is this the built-in eval function. @@ -184,13 +178,10 @@ impl Operation for Call { let func = context.vm.pop(); let this = context.vm.pop(); - let object = match func { - JsValue::Object(ref object) if object.is_callable() => object.clone(), - _ => { - return Err(JsNativeError::typ() - .with_message("not a callable function") - .into()); - } + let Some(object) = func.as_object() else { + return Err(JsNativeError::typ() + .with_message("not a callable function") + .into()); }; let result = object.__call__(&this, &arguments, context)?; @@ -237,13 +228,10 @@ impl Operation for CallSpread { let func = context.vm.pop(); let this = context.vm.pop(); - let object = match func { - JsValue::Object(ref object) if object.is_callable() => object.clone(), - _ => { - return Err(JsNativeError::typ() - .with_message("not a callable function") - .into()) - } + let Some(object) = func.as_object() else { + return Err(JsNativeError::typ() + .with_message("not a callable function") + .into()); }; let result = object.__call__(&this, &arguments, context)?;