From cbd24a5072c1c8323e7db18f27ae791e556a7b89 Mon Sep 17 00:00:00 2001 From: Haled Odat <8566042+HalidOdat@users.noreply.github.com> Date: Tue, 8 Aug 2023 08:51:54 +0200 Subject: [PATCH] Refactor ordinary VM calling - Prevent recursing in `__call__` and `__construct__` internal methods --- boa_engine/src/builtins/eval/mod.rs | 8 +- boa_engine/src/builtins/generator/mod.rs | 24 +- boa_engine/src/builtins/json/mod.rs | 13 +- boa_engine/src/bytecompiler/class.rs | 2 +- boa_engine/src/bytecompiler/declarations.rs | 7 +- boa_engine/src/bytecompiler/expression/mod.rs | 1 + boa_engine/src/bytecompiler/jump_control.rs | 7 +- boa_engine/src/bytecompiler/mod.rs | 3 +- boa_engine/src/context/mod.rs | 34 +- boa_engine/src/module/source.rs | 59 +-- .../object/internal_methods/bound_function.rs | 49 ++- .../src/object/internal_methods/function.rs | 306 ++++++++++++++-- boa_engine/src/object/internal_methods/mod.rs | 91 +++-- .../src/object/internal_methods/proxy.rs | 50 +-- boa_engine/src/object/operations.rs | 48 ++- boa_engine/src/script.rs | 15 +- boa_engine/src/vm/call_frame/mod.rs | 37 +- boa_engine/src/vm/code_block.rs | 340 +----------------- boa_engine/src/vm/flowgraph/mod.rs | 10 +- boa_engine/src/vm/mod.rs | 190 ++++++++-- boa_engine/src/vm/opcode/await/mod.rs | 18 +- boa_engine/src/vm/opcode/call/mod.rs | 64 ++-- .../src/vm/opcode/control_flow/return.rs | 53 ++- boa_engine/src/vm/opcode/environment/mod.rs | 123 ++++--- boa_engine/src/vm/opcode/generator/mod.rs | 43 +-- .../src/vm/opcode/generator/yield_stm.rs | 14 +- boa_engine/src/vm/opcode/get/argument.rs | 47 +++ boa_engine/src/vm/opcode/get/mod.rs | 2 + boa_engine/src/vm/opcode/mod.rs | 46 ++- boa_engine/src/vm/opcode/new/mod.rs | 43 +-- .../src/vm/opcode/rest_parameter/mod.rs | 43 +-- 31 files changed, 1039 insertions(+), 751 deletions(-) create mode 100644 boa_engine/src/vm/opcode/get/argument.rs diff --git a/boa_engine/src/builtins/eval/mod.rs b/boa_engine/src/builtins/eval/mod.rs index 9fad389b595..b32481f344d 100644 --- a/boa_engine/src/builtins/eval/mod.rs +++ b/boa_engine/src/builtins/eval/mod.rs @@ -256,9 +256,15 @@ impl Eval { } let env_fp = context.vm.environments.len() as u32; + let environments = context.vm.environments.clone(); + let realm = context.realm().clone(); context .vm - .push_frame(CallFrame::new(code_block, None, None).with_env_fp(env_fp)); + .push_frame(CallFrame::new(code_block, None, environments, realm).with_env_fp(env_fp)); + + context.vm.push(JsValue::undefined()); // Push `this` value. + context.vm.push(JsValue::undefined()); // No function object, so push undefined. + context.realm().resize_global_env(); let record = context.run(); diff --git a/boa_engine/src/builtins/generator/mod.rs b/boa_engine/src/builtins/generator/mod.rs index fc063eaab85..dde61eb23d4 100644 --- a/boa_engine/src/builtins/generator/mod.rs +++ b/boa_engine/src/builtins/generator/mod.rs @@ -12,7 +12,6 @@ use crate::{ builtins::iterable::create_iter_result_object, context::intrinsics::Intrinsics, - environments::EnvironmentStack, error::JsNativeError, js_string, object::{JsObject, CONSTRUCTOR}, @@ -60,36 +59,28 @@ unsafe impl Trace for GeneratorState { /// context/vm before the generator execution starts/resumes and after it has ended/yielded. #[derive(Debug, Clone, Trace, Finalize)] pub(crate) struct GeneratorContext { - pub(crate) environments: EnvironmentStack, pub(crate) stack: Vec, pub(crate) call_frame: Option, - pub(crate) realm: Realm, } impl GeneratorContext { /// Creates a new `GeneratorContext` from the raw `Context` state components. - pub(crate) fn new( - environments: EnvironmentStack, - stack: Vec, - call_frame: CallFrame, - realm: Realm, - ) -> Self { + pub(crate) fn new(stack: Vec, call_frame: CallFrame) -> Self { Self { - environments, stack, call_frame: Some(call_frame), - realm, } } /// Creates a new `GeneratorContext` from the current `Context` state. pub(crate) fn from_current(context: &mut Context<'_>) -> Self { + let mut frame = context.vm.frame().clone(); + frame.environments = context.vm.environments.clone(); + frame.realm = context.realm().clone(); let fp = context.vm.frame().fp as usize; let this = Self { - environments: context.vm.environments.clone(), - call_frame: Some(context.vm.frame().clone()), + call_frame: Some(frame), stack: context.vm.stack[fp..].to_vec(), - realm: context.realm().clone(), }; context.vm.stack.truncate(fp); @@ -104,14 +95,13 @@ impl GeneratorContext { resume_kind: GeneratorResumeKind, context: &mut Context<'_>, ) -> CompletionRecord { - std::mem::swap(&mut context.vm.environments, &mut self.environments); std::mem::swap(&mut context.vm.stack, &mut self.stack); - context.swap_realm(&mut self.realm); context .vm .push_frame(self.call_frame.take().expect("should have a call frame")); context.vm.frame_mut().fp = 0; + context.vm.frame_mut().exit_early = true; if let Some(value) = value { context.vm.push(value); @@ -120,9 +110,7 @@ impl GeneratorContext { let result = context.run(); - std::mem::swap(&mut context.vm.environments, &mut self.environments); std::mem::swap(&mut context.vm.stack, &mut self.stack); - context.swap_realm(&mut self.realm); self.call_frame = context.vm.pop_frame(); assert!(self.call_frame.is_some()); result diff --git a/boa_engine/src/builtins/json/mod.rs b/boa_engine/src/builtins/json/mod.rs index 6b94df63d64..f2dafd2d1b9 100644 --- a/boa_engine/src/builtins/json/mod.rs +++ b/boa_engine/src/builtins/json/mod.rs @@ -129,10 +129,17 @@ impl Json { Gc::new(compiler.finish()) }; + let realm = context.realm().clone(); + let env_fp = context.vm.environments.len() as u32; - context - .vm - .push_frame(CallFrame::new(code_block, None, None).with_env_fp(env_fp)); + context.vm.push_frame( + CallFrame::new(code_block, None, context.vm.environments.clone(), realm) + .with_env_fp(env_fp), + ); + + context.vm.push(JsValue::undefined()); // Push `this` value. + context.vm.push(JsValue::undefined()); // No function object, so push undefined. + context.realm().resize_global_env(); let record = context.run(); context.vm.pop_frame(); diff --git a/boa_engine/src/bytecompiler/class.rs b/boa_engine/src/bytecompiler/class.rs index 7ee875ad16c..c75f997fa37 100644 --- a/boa_engine/src/bytecompiler/class.rs +++ b/boa_engine/src/bytecompiler/class.rs @@ -69,8 +69,8 @@ impl ByteCompiler<'_, '_> { compiler.emit_opcode(Opcode::PushUndefined); } else if class.super_ref().is_some() { compiler.emit_opcode(Opcode::SuperCallDerived); + compiler.emit_opcode(Opcode::BindThisValue); } else { - compiler.emit_opcode(Opcode::RestParameterPop); compiler.emit_opcode(Opcode::PushUndefined); } compiler.emit_opcode(Opcode::SetReturnValue); diff --git a/boa_engine/src/bytecompiler/declarations.rs b/boa_engine/src/bytecompiler/declarations.rs index 5e403923b00..158a6bb4c0e 100644 --- a/boa_engine/src/bytecompiler/declarations.rs +++ b/boa_engine/src/bytecompiler/declarations.rs @@ -1007,9 +1007,11 @@ impl ByteCompiler<'_, '_> { // a. Perform ? IteratorBindingInitialization of formals with arguments iteratorRecord and undefined. // 26. Else, // a. Perform ? IteratorBindingInitialization of formals with arguments iteratorRecord and env. - for parameter in formals.as_ref() { + for (i, parameter) in formals.as_ref().iter().enumerate() { if parameter.is_rest_param() { self.emit_opcode(Opcode::RestParameterInit); + } else { + self.emit_with_varying_operand(Opcode::GetArgument, i as u32); } match parameter.variable().binding() { Binding::Identifier(ident) => { @@ -1030,9 +1032,6 @@ impl ByteCompiler<'_, '_> { } } } - if !formals.has_rest_parameter() { - self.emit_opcode(Opcode::RestParameterPop); - } if generator { self.emit(Opcode::Generator, &[Operand::U8(self.in_async().into())]); diff --git a/boa_engine/src/bytecompiler/expression/mod.rs b/boa_engine/src/bytecompiler/expression/mod.rs index a9ae59ce2ed..1808a5900c5 100644 --- a/boa_engine/src/bytecompiler/expression/mod.rs +++ b/boa_engine/src/bytecompiler/expression/mod.rs @@ -326,6 +326,7 @@ impl ByteCompiler<'_, '_> { super_call.arguments().len() as u32, ); } + self.emit_opcode(Opcode::BindThisValue); if !use_expr { self.emit_opcode(Opcode::Pop); diff --git a/boa_engine/src/bytecompiler/jump_control.rs b/boa_engine/src/bytecompiler/jump_control.rs index 85708c90a98..40845ea0c5c 100644 --- a/boa_engine/src/bytecompiler/jump_control.rs +++ b/boa_engine/src/bytecompiler/jump_control.rs @@ -141,7 +141,12 @@ impl JumpRecord { // // Note: If there is promise capability resolve or reject it based on pending exception. (true, false) => compiler.emit_opcode(Opcode::CompletePromiseCapability), - (_, _) => {} + (false, false) => { + // TODO: We can omit checking for return, when constructing for functions, + // that cannot be constructed, like arrow functions. + compiler.emit_opcode(Opcode::CheckReturn); + } + (false, true) => {} } compiler.emit_opcode(Opcode::Return); diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index b61caf514db..491a7bc1f0b 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -324,7 +324,8 @@ impl<'ctx, 'host> ByteCompiler<'ctx, 'host> { compile_environments: Vec::default(), current_open_environments_count: 0, - current_stack_value_count: 0, + // This starts at two because the first value is the `this` value, then function object. + current_stack_value_count: 2, code_block_flags, handlers: ThinVec::default(), diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 32dd3008f71..37ce1e97170 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -82,9 +82,6 @@ use self::intrinsics::StandardConstructor; /// assert_eq!(value.as_number(), Some(12.0)) /// ``` pub struct Context<'host> { - /// realm holds both the global object and the environment - realm: Realm, - /// String interner in the context. interner: Interner, @@ -121,7 +118,7 @@ impl std::fmt::Debug for Context<'_> { let mut debug = f.debug_struct("Context"); debug - .field("realm", &self.realm) + .field("realm", &self.vm.realm) .field("interner", &self.interner) .field("vm", &self.vm) .field("strict", &self.strict) @@ -268,7 +265,7 @@ impl<'host> Context<'host> { length: usize, body: NativeFunction, ) -> JsResult<()> { - let function = FunctionObjectBuilder::new(&self.realm, body) + let function = FunctionObjectBuilder::new(self.realm(), body) .name(name.clone()) .length(length) .constructor(true) @@ -301,7 +298,7 @@ impl<'host> Context<'host> { length: usize, body: NativeFunction, ) -> JsResult<()> { - let function = FunctionObjectBuilder::new(&self.realm, body) + let function = FunctionObjectBuilder::new(self.realm(), body) .name(name.clone()) .length(length) .constructor(false) @@ -335,7 +332,7 @@ impl<'host> Context<'host> { /// context.register_global_class::()?; /// ``` pub fn register_global_class(&mut self) -> JsResult<()> { - if self.realm.has_class::() { + if self.realm().has_class::() { return Err(JsNativeError::typ() .with_message("cannot register a class twice") .into()); @@ -353,7 +350,7 @@ impl<'host> Context<'host> { self.global_object() .define_property_or_throw(js_string!(C::NAME), property, self)?; - self.realm.register_class::(class); + self.realm().register_class::(class); Ok(()) } @@ -384,20 +381,20 @@ impl<'host> Context<'host> { pub fn unregister_global_class(&mut self) -> JsResult> { self.global_object() .delete_property_or_throw(js_string!(C::NAME), self)?; - Ok(self.realm.unregister_class::()) + Ok(self.realm().unregister_class::()) } /// Checks if the currently active realm has the global class `C` registered. #[must_use] pub fn has_global_class(&self) -> bool { - self.realm.has_class::() + self.realm().has_class::() } /// Gets the constructor and prototype of the global class `C` if the currently active realm has /// that class registered. #[must_use] pub fn get_global_class(&self) -> Option { - self.realm.get_class::() + self.realm().get_class::() } /// Gets the string interner. @@ -417,21 +414,21 @@ impl<'host> Context<'host> { #[inline] #[must_use] pub fn global_object(&self) -> JsObject { - self.realm.global_object().clone() + self.vm.realm.global_object().clone() } /// Returns the currently active intrinsic constructors and objects. #[inline] #[must_use] pub fn intrinsics(&self) -> &Intrinsics { - self.realm.intrinsics() + self.vm.realm.intrinsics() } /// Returns the currently active realm. #[inline] #[must_use] pub const fn realm(&self) -> &Realm { - &self.realm + &self.vm.realm } /// Set the value of trace on the context @@ -510,7 +507,7 @@ impl<'host> Context<'host> { self.vm .environments .replace_global(realm.environment().clone()); - std::mem::replace(&mut self.realm, realm) + std::mem::replace(&mut self.vm.realm, realm) } /// Get the [`RootShape`]. @@ -565,7 +562,7 @@ impl<'host> Context<'host> { impl Context<'_> { /// Swaps the currently active realm with `realm`. pub(crate) fn swap_realm(&mut self, realm: &mut Realm) { - std::mem::swap(&mut self.realm, realm); + std::mem::swap(&mut self.vm.realm, realm); } /// Increment and get the parser identifier. @@ -794,7 +791,7 @@ impl Context<'_> { } if let Some(frame) = self.vm.frames.last() { - return frame.active_function.clone(); + return frame.function(&self.vm); } None @@ -987,7 +984,7 @@ impl<'icu, 'hooks, 'queue, 'module> ContextBuilder<'icu, 'hooks, 'queue, 'module hooks.into() }); let realm = Realm::create(&*host_hooks, &root_shape); - let vm = Vm::new(realm.environment().clone()); + let vm = Vm::new(realm); let module_loader = if let Some(loader) = self.module_loader { loader @@ -1005,7 +1002,6 @@ impl<'icu, 'hooks, 'queue, 'module> ContextBuilder<'icu, 'hooks, 'queue, 'module }; let mut context = Context { - realm, interner: self.interner.unwrap_or_default(), vm, strict: false, diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index 3c0714d5682..6b263aea553 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -1404,7 +1404,7 @@ impl SourceTextModule { // 2. Assert: All named exports from module are resolvable. // 3. Let realm be module.[[Realm]]. // 4. Assert: realm is not undefined. - let mut realm = parent.realm().clone(); + let realm = parent.realm().clone(); // 5. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]). // 6. Set module.[[Environment]] to env. @@ -1602,22 +1602,23 @@ impl SourceTextModule { envs.push_module(env); // 9. Set the Function of moduleContext to null. + // 10. Assert: module.[[Realm]] is not undefined. + // 11. Set the Realm of moduleContext to module.[[Realm]]. // 12. Set the ScriptOrModule of moduleContext to module. + // 13. Set the VariableEnvironment of moduleContext to module.[[Environment]]. + // 14. Set the LexicalEnvironment of moduleContext to module.[[Environment]]. + // 15. Set the PrivateEnvironment of moduleContext to null. let call_frame = CallFrame::new( codeblock.clone(), Some(ActiveRunnable::Module(parent.clone())), - None, + envs, + realm.clone(), ); context.vm.push_frame(call_frame); - // 13. Set the VariableEnvironment of moduleContext to module.[[Environment]]. - // 14. Set the LexicalEnvironment of moduleContext to module.[[Environment]]. - // 15. Set the PrivateEnvironment of moduleContext to null. - std::mem::swap(&mut context.vm.environments, &mut envs); + context.vm.push(JsValue::undefined()); // Push `this` value. + context.vm.push(JsValue::undefined()); // No function object, so push undefined. - // 10. Assert: module.[[Realm]] is not undefined. - // 11. Set the Realm of moduleContext to module.[[Realm]]. - context.swap_realm(&mut realm); // 17. Push moduleContext onto the execution context stack; moduleContext is now the running execution context. // deferred initialization of import bindings @@ -1675,15 +1676,14 @@ impl SourceTextModule { } // 25. Remove moduleContext from the execution context stack. - context + let frame = context .vm .pop_frame() .expect("There should be a call frame"); - std::mem::swap(&mut context.vm.environments, &mut envs); - context.swap_realm(&mut realm); - debug_assert!(envs.current().as_declarative().is_some()); - *parent.inner.environment.borrow_mut() = envs.current().as_declarative().cloned(); + debug_assert!(frame.environments.current().as_declarative().is_some()); + *parent.inner.environment.borrow_mut() = + frame.environments.current().as_declarative().cloned(); // 16. Set module.[[Context]] to moduleContext. self.inner @@ -1694,7 +1694,7 @@ impl SourceTextModule { info, context: SourceTextContext { codeblock, - environments: envs, + environments: frame.environments.clone(), realm, }, }, @@ -1718,8 +1718,8 @@ impl SourceTextModule { // 1. Let moduleContext be a new ECMAScript code execution context. let SourceTextContext { codeblock, - mut environments, - mut realm, + environments, + realm, } = match &*self.inner.status.borrow() { Status::Evaluating { context, .. } | Status::EvaluatingAsync { context, .. } => { context.clone() @@ -1728,22 +1728,27 @@ impl SourceTextModule { }; // 2. Set the Function of moduleContext to null. + // 3. Set the Realm of moduleContext to module.[[Realm]]. // 4. Set the ScriptOrModule of moduleContext to module. - let env_fp = environments.len() as u32; - let mut callframe = - CallFrame::new(codeblock, Some(ActiveRunnable::Module(self.parent())), None) - .with_env_fp(env_fp); - callframe.promise_capability = capability; - // 5. Assert: module has been linked and declarations in its module environment have been instantiated. // 6. Set the VariableEnvironment of moduleContext to module.[[Environment]]. // 7. Set the LexicalEnvironment of moduleContext to module.[[Environment]]. - std::mem::swap(&mut context.vm.environments, &mut environments); - // 3. Set the Realm of moduleContext to module.[[Realm]]. - context.swap_realm(&mut realm); + let env_fp = environments.len() as u32; + let mut callframe = CallFrame::new( + codeblock, + Some(ActiveRunnable::Module(self.parent())), + environments, + realm, + ) + .with_env_fp(env_fp); + callframe.promise_capability = capability; + // 8. Suspend the running execution context. context.vm.push_frame(callframe); + context.vm.push(JsValue::undefined()); // Push `this` value. + context.vm.push(JsValue::undefined()); // No function object, so push undefined. + // 9. If module.[[HasTLA]] is false, then // a. Assert: capability is not present. // b. Push moduleContext onto the execution context stack; moduleContext is now the running execution context. @@ -1755,8 +1760,6 @@ impl SourceTextModule { // b. Perform AsyncBlockStart(capability, module.[[ECMAScriptCode]], moduleContext). let result = context.run(); - std::mem::swap(&mut context.vm.environments, &mut environments); - context.swap_realm(&mut realm); context.vm.pop_frame(); // f. If result is an abrupt completion, then diff --git a/boa_engine/src/object/internal_methods/bound_function.rs b/boa_engine/src/object/internal_methods/bound_function.rs index cd39eac0cdd..8ff1da7dcf2 100644 --- a/boa_engine/src/object/internal_methods/bound_function.rs +++ b/boa_engine/src/object/internal_methods/bound_function.rs @@ -1,6 +1,6 @@ use crate::{object::JsObject, Context, JsResult, JsValue}; -use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; +use super::{CallValue, InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; /// Definitions of the internal object methods for function objects. /// @@ -27,33 +27,37 @@ pub(crate) static BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS: InternalObjectMetho /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-bound-function-exotic-objects-call-thisargument-argumentslist -#[track_caller] +#[allow(clippy::unnecessary_wraps)] fn bound_function_exotic_call( obj: &JsObject, - _: &JsValue, - arguments_list: &[JsValue], + argument_count: usize, context: &mut Context<'_>, -) -> JsResult { +) -> 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"); + let arguments_start_index = context.vm.stack.len() - argument_count; + // 1. Let target be F.[[BoundTargetFunction]]. let target = bound_function.target_function(); + context.vm.stack[arguments_start_index - 1] = target.clone().into(); // 2. Let boundThis be F.[[BoundThis]]. let bound_this = bound_function.this(); + context.vm.stack[arguments_start_index - 2] = bound_this.clone(); // 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); + context + .vm + .insert_values_at(bound_args, arguments_start_index); // 5. Return ? Call(target, boundThis, args). - target.call(bound_this, &args, context) + Ok(target.__call__(bound_args.len() + argument_count)) } /// Internal method `[[Construct]]` for Bound Function Exotic Objects @@ -62,14 +66,17 @@ fn bound_function_exotic_call( /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-bound-function-exotic-objects-construct-argumentslist-newtarget -#[track_caller] +#[allow(clippy::unnecessary_wraps)] fn bound_function_exotic_construct( - obj: &JsObject, - arguments_list: &[JsValue], - new_target: &JsObject, + function_object: &JsObject, + argument_count: usize, context: &mut Context<'_>, -) -> JsResult { - let object = obj.borrow(); +) -> JsResult { + let new_target = context.vm.pop(); + + debug_assert!(new_target.is_object(), "new.target should be an object"); + + let object = function_object.borrow(); let bound_function = object .as_bound_function() .expect("bound function exotic method should only be callable from bound function objects"); @@ -83,16 +90,20 @@ fn bound_function_exotic_construct( 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); + let arguments_start_index = context.vm.stack.len() - argument_count; + context + .vm + .insert_values_at(bound_args, arguments_start_index); // 5. If SameValue(F, newTarget) is true, set newTarget to target. - let new_target = if JsObject::equals(obj, new_target) { - target + let function_object: JsValue = function_object.clone().into(); + let new_target = if JsValue::same_value(&function_object, &new_target) { + target.clone().into() } else { new_target }; // 6. Return ? Construct(target, args, newTarget). - target.construct(&args, Some(new_target), context) + context.vm.push(new_target); + Ok(target.__construct__(bound_args.len() + argument_count)) } diff --git a/boa_engine/src/object/internal_methods/function.rs b/boa_engine/src/object/internal_methods/function.rs index 92ceb2afdec..fd44ca071a9 100644 --- a/boa_engine/src/object/internal_methods/function.rs +++ b/boa_engine/src/object/internal_methods/function.rs @@ -1,13 +1,16 @@ use crate::{ + builtins::function::{arguments::Arguments, FunctionKind, ThisMode}, context::intrinsics::StandardConstructors, + environments::{FunctionSlots, ThisBindingStatus}, object::{ internal_methods::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}, JsObject, ObjectData, ObjectKind, }, + vm::CallFrame, Context, JsNativeError, JsResult, JsValue, }; -use super::get_prototype_from_constructor; +use super::{get_prototype_from_constructor, CallValue}; /// Definitions of the internal object methods for function objects. /// @@ -33,13 +36,125 @@ pub(crate) static CONSTRUCTOR_INTERNAL_METHODS: InternalObjectMethods = Internal /// Panics if the object is currently mutably borrowed. // // -fn function_call( - obj: &JsObject, - this: &JsValue, - args: &[JsValue], +pub(crate) fn function_call( + function_object: &JsObject, + argument_count: usize, context: &mut Context<'_>, -) -> JsResult { - obj.call_internal(this, args, context) +) -> JsResult { + context.check_runtime_limits()?; + + let object = function_object.borrow(); + let function = object.as_function().expect("not a function"); + let realm = function.realm().clone(); + + if let FunctionKind::Ordinary { .. } = function.kind() { + if function.code.is_class_constructor() { + return Err(JsNativeError::typ() + .with_message("class constructor cannot be invoked without 'new'") + .with_realm(realm) + .into()); + } + } + + let code = function.code.clone(); + let environments = function.environments.clone(); + let script_or_module = function.script_or_module.clone(); + + drop(object); + + let env_fp = environments.len() as u32; + + let mut frame = CallFrame::new(code.clone(), script_or_module, environments, realm) + .with_argument_count(argument_count as u32) + .with_env_fp(env_fp); + + frame.exit_early = false; + + context.vm.push_frame(frame); + + let at = context.vm.stack.len() - argument_count; + + context.vm.frame_mut().fp = at as u32 - 2; + + let this = context.vm.stack[at - 2].clone(); + + let lexical_this_mode = code.this_mode == ThisMode::Lexical; + + let this = if lexical_this_mode { + ThisBindingStatus::Lexical + } else if code.strict() { + ThisBindingStatus::Initialized(this.clone()) + } else if this.is_null_or_undefined() { + ThisBindingStatus::Initialized(context.realm().global_this().clone().into()) + } else { + ThisBindingStatus::Initialized( + this.to_object(context) + .expect("conversion cannot fail") + .into(), + ) + }; + + let mut last_env = 0; + + if code.has_binding_identifier() { + let index = context + .vm + .environments + .push_lexical(code.compile_environments[last_env].clone()); + context + .vm + .environments + .put_lexical_value(index, 0, function_object.clone().into()); + last_env += 1; + } + + context.vm.environments.push_function( + code.compile_environments[last_env].clone(), + FunctionSlots::new(this, function_object.clone(), None), + ); + + if code.has_parameters_env_bindings() { + last_env += 1; + context + .vm + .environments + .push_lexical(code.compile_environments[last_env].clone()); + } + + // Taken from: `FunctionDeclarationInstantiation` abstract function. + // + // Spec: https://tc39.es/ecma262/#sec-functiondeclarationinstantiation + // + // 22. If argumentsObjectNeeded is true, then + if code.needs_arguments_object() { + // a. If strict is true or simpleParameterList is false, then + // i. Let ao be CreateUnmappedArgumentsObject(argumentsList). + // b. Else, + // i. NOTE: A mapped argument object is only provided for non-strict functions + // that don't have a rest parameter, any parameter + // default value initializers, or any destructured parameters. + // ii. Let ao be CreateMappedArgumentsObject(func, formals, argumentsList, env). + let args = context.vm.stack[at..].to_vec(); + let arguments_obj = if code.strict() || !code.params.is_simple() { + Arguments::create_unmapped_arguments_object(&args, context) + } else { + let env = context.vm.environments.current(); + Arguments::create_mapped_arguments_object( + function_object, + &code.params, + &args, + env.declarative_expect(), + context, + ) + }; + let env_index = context.vm.environments.len() as u32 - 1; + context + .vm + .environments + .put_lexical_value(env_index, 0, arguments_obj.into()); + } + + Ok(CallValue::Ready) } /// Construct an instance of this object with the specified arguments. @@ -49,12 +164,144 @@ fn function_call( /// Panics if the object is currently mutably borrowed. // fn function_construct( - obj: &JsObject, - args: &[JsValue], - new_target: &JsObject, + this_function_object: &JsObject, + argument_count: usize, context: &mut Context<'_>, -) -> JsResult { - obj.construct_internal(args, &new_target.clone().into(), context) +) -> JsResult { + context.check_runtime_limits()?; + + let object = this_function_object.borrow(); + let function = object.as_function().expect("not a function"); + let realm = function.realm().clone(); + + let FunctionKind::Ordinary { + constructor_kind, .. + } = function.kind() + else { + unreachable!("not a constructor") + }; + + let code = function.code.clone(); + let environments = function.environments.clone(); + let script_or_module = function.script_or_module.clone(); + let constructor_kind = *constructor_kind; + drop(object); + + let env_fp = environments.len() as u32; + + let new_target = context.vm.pop(); + + let at = context.vm.stack.len() - argument_count; + + let this = if constructor_kind.is_base() { + // 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 prototype = + get_prototype_from_constructor(&new_target, StandardConstructors::object, context)?; + let this = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + ObjectData::ordinary(), + ); + + this.initialize_instance_elements(this_function_object, context)?; + + Some(this) + } else { + None + }; + + let mut frame = CallFrame::new(code.clone(), script_or_module, environments, realm) + .with_argument_count(argument_count as u32) + .with_env_fp(env_fp); + + frame.exit_early = false; + frame.construct = true; + + context.vm.push_frame(frame); + + context.vm.frame_mut().fp = at as u32 - 1; + + let mut last_env = 0; + + if code.has_binding_identifier() { + let index = context + .vm + .environments + .push_lexical(code.compile_environments[last_env].clone()); + context + .vm + .environments + .put_lexical_value(index, 0, this_function_object.clone().into()); + last_env += 1; + } + + context.vm.environments.push_function( + code.compile_environments[last_env].clone(), + FunctionSlots::new( + this.clone().map_or(ThisBindingStatus::Uninitialized, |o| { + ThisBindingStatus::Initialized(o.into()) + }), + this_function_object.clone(), + Some( + new_target + .as_object() + .expect("new.target should be an object") + .clone(), + ), + ), + ); + + if code.has_parameters_env_bindings() { + last_env += 1; + context + .vm + .environments + .push_lexical(code.compile_environments[last_env].clone()); + } + + // Taken from: `FunctionDeclarationInstantiation` abstract function. + // + // Spec: https://tc39.es/ecma262/#sec-functiondeclarationinstantiation + // + // 22. If argumentsObjectNeeded is true, then + if code.needs_arguments_object() { + // a. If strict is true or simpleParameterList is false, then + // i. Let ao be CreateUnmappedArgumentsObject(argumentsList). + // b. Else, + // i. NOTE: A mapped argument object is only provided for non-strict functions + // that don't have a rest parameter, any parameter + // default value initializers, or any destructured parameters. + // ii. Let ao be CreateMappedArgumentsObject(func, formals, argumentsList, env). + let args = context.vm.stack[at..].to_vec(); + let arguments_obj = if code.strict() || !code.params.is_simple() { + Arguments::create_unmapped_arguments_object(&args, context) + } else { + let env = context.vm.environments.current(); + Arguments::create_mapped_arguments_object( + this_function_object, + &code.params, + &args, + env.declarative_expect(), + context, + ) + }; + let env_index = context.vm.environments.len() as u32 - 1; + context + .vm + .environments + .put_lexical_value(env_index, 0, arguments_obj.into()); + } + + // Insert `this` value + context + .vm + .stack + .insert(at - 1, this.map(JsValue::new).unwrap_or_default()); + + Ok(CallValue::Ready) } /// Definitions of the internal object methods for native function objects. @@ -82,13 +329,15 @@ pub(crate) static NATIVE_CONSTRUCTOR_INTERNAL_METHODS: InternalObjectMethods = /// Panics if the object is currently mutably borrowed. /// // -#[track_caller] pub(crate) fn native_function_call( obj: &JsObject, - this: &JsValue, - args: &[JsValue], + argument_count: usize, context: &mut Context<'_>, -) -> JsResult { +) -> JsResult { + let args = context.vm.pop_n_values(argument_count); + let _func = context.vm.pop(); + let this = context.vm.pop(); + // We technically don't need this since native functions don't push any new frames to the // vm, but we'll eventually have to combine the native stack with the vm stack. context.check_runtime_limits()?; @@ -113,16 +362,18 @@ pub(crate) fn native_function_call( context.vm.native_active_function = Some(this_function_object); let result = if constructor.is_some() { - function.call(&JsValue::undefined(), args, context) + function.call(&JsValue::undefined(), &args, context) } else { - function.call(this, args, context) + 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 + context.vm.push(result?); + + Ok(CallValue::Complete) } /// Construct an instance of this object with the specified arguments. @@ -131,13 +382,11 @@ pub(crate) fn native_function_call( /// /// Panics if the object is currently mutably borrowed. // -#[track_caller] fn native_function_construct( obj: &JsObject, - args: &[JsValue], - new_target: &JsObject, + argument_count: usize, context: &mut Context<'_>, -) -> JsResult { +) -> JsResult { // We technically don't need this since native functions don't push any new frames to the // vm, but we'll eventually have to combine the native stack with the vm stack. context.check_runtime_limits()?; @@ -161,9 +410,12 @@ fn native_function_construct( context.swap_realm(&mut realm); context.vm.native_active_function = Some(this_function_object); - let new_target = new_target.clone().into(); + let new_target = context.vm.pop(); + let args = context.vm.pop_n_values(argument_count); + let _func = context.vm.pop(); + let result = function - .call(&new_target, args, context) + .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()), @@ -190,5 +442,7 @@ fn native_function_construct( context.vm.native_active_function = None; context.swap_realm(&mut realm); - result + context.vm.push(result?); + + Ok(CallValue::Complete) } diff --git a/boa_engine/src/object/internal_methods/mod.rs b/boa_engine/src/object/internal_methods/mod.rs index 4fdb576ab6b..e4590d2e743 100644 --- a/boa_engine/src/object/internal_methods/mod.rs +++ b/boa_engine/src/object/internal_methods/mod.rs @@ -211,40 +211,38 @@ impl JsObject { /// Internal method `[[Call]]` /// - /// Call this object if it has a `[[Call]]` internal method. + /// The caller must ensure that the following values are pushed on the stack. + /// + /// Stack: `this, function, arg0, arg1, ..., argN` /// /// More information: /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects-call-thisargument-argumentslist - #[track_caller] - pub(crate) fn __call__( - &self, - this: &JsValue, - args: &[JsValue], - context: &mut Context<'_>, - ) -> JsResult { - let _timer = Profiler::global().start_event("Object::__call__", "object"); - (self.vtable().__call__)(self, this, args, context) + pub(crate) fn __call__(&self, argument_count: usize) -> CallValue { + CallValue::Pending { + func: self.vtable().__call__, + object: self.clone(), + argument_count, + } } /// Internal method `[[Construct]]` /// - /// Construct a new instance of this object if this object has a `[[Construct]]` internal method. + /// The caller must ensure that the following values are pushed on the stack. + /// + /// Stack: `function, arg0, arg1, ..., argN, new.target` /// /// More information: /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects-construct-argumentslist-newtarget - #[track_caller] - pub(crate) fn __construct__( - &self, - args: &[JsValue], - new_target: &Self, - context: &mut Context<'_>, - ) -> JsResult { - let _timer = Profiler::global().start_event("Object::__construct__", "object"); - (self.vtable().__construct__)(self, args, new_target, context) + pub(crate) fn __construct__(&self, argument_count: usize) -> CallValue { + CallValue::Pending { + func: self.vtable().__construct__, + object: self.clone(), + argument_count, + } } } @@ -299,9 +297,46 @@ 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__: fn(&JsObject, &JsValue, &[JsValue], &mut Context<'_>) -> JsResult, + pub(crate) __call__: + fn(&JsObject, argument_count: usize, &mut Context<'_>) -> JsResult, pub(crate) __construct__: - fn(&JsObject, &[JsValue], &JsObject, &mut Context<'_>) -> JsResult, + fn(&JsObject, argument_count: usize, &mut Context<'_>) -> JsResult, +} + +/// The return value of an internal method (`[[Call]]` or `[[Construct]]`). +/// +/// This is done to avoid recusion. +pub(crate) enum CallValue { + /// Calling is ready, the frames have been setup. + /// + /// Requires calling [`Context::run()`]. + Ready, + + /// Further processing is needed. + Pending { + func: fn(&JsObject, argument_count: usize, &mut Context<'_>) -> JsResult, + object: JsObject, + argument_count: usize, + }, + + /// The value has been computed and is the first element on the stack. + Complete, +} + +impl CallValue { + /// Resolves the [`CallValue`], and return if the value is complete. + pub(crate) fn resolve(mut self, context: &mut Context<'_>) -> JsResult { + while let Self::Pending { + func, + object, + argument_count, + } = self + { + self = func(&object, argument_count, context)?; + } + + Ok(matches!(self, Self::Complete)) + } } /// Abstract operation `OrdinaryGetPrototypeOf`. @@ -913,10 +948,9 @@ where pub(crate) fn non_existant_call( _obj: &JsObject, - _this: &JsValue, - _args: &[JsValue], + _argument_count: usize, context: &mut Context<'_>, -) -> JsResult { +) -> JsResult { Err(JsNativeError::typ() .with_message("not a callable function") .with_realm(context.realm().clone()) @@ -925,12 +959,11 @@ pub(crate) fn non_existant_call( pub(crate) fn non_existant_construct( _obj: &JsObject, - _args: &[JsValue], - _new_target: &JsObject, + _argument_count: usize, context: &mut Context<'_>, -) -> JsResult { +) -> JsResult { Err(JsNativeError::typ() - .with_message("object is not constructable") + .with_message("not a constructor") .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 4ebe9554dce..5e9ac2cb590 100644 --- a/boa_engine/src/object/internal_methods/proxy.rs +++ b/boa_engine/src/object/internal_methods/proxy.rs @@ -9,7 +9,7 @@ use crate::{ }; use rustc_hash::FxHashSet; -use super::ORDINARY_INTERNAL_METHODS; +use super::{CallValue, ORDINARY_INTERNAL_METHODS}; /// Definitions of the internal object methods for array exotic objects. /// @@ -922,10 +922,9 @@ pub(crate) fn proxy_exotic_own_property_keys( /// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-call-thisargument-argumentslist fn proxy_exotic_call( obj: &JsObject, - this: &JsValue, - args: &[JsValue], + argument_count: usize, context: &mut Context<'_>, -) -> JsResult { +) -> JsResult { // 1. Let handler be O.[[ProxyHandler]]. // 2. If handler is null, throw a TypeError exception. // 3. Assert: Type(handler) is Object. @@ -940,18 +939,25 @@ fn proxy_exotic_call( let Some(trap) = handler.get_method(utf16!("apply"), context)? else { // 6. If trap is undefined, then // a. Return ? Call(target, thisArgument, argumentsList). - return target.call(this, args, context); + return Ok(target.__call__(argument_count)); }; + let args = context.vm.pop_n_values(argument_count); + // 7. Let argArray be ! CreateArrayFromList(argumentsList). - let arg_array = array::Array::create_array_from_list(args.to_vec(), context); + let arg_array = array::Array::create_array_from_list(args, context); // 8. Return ? Call(trap, handler, « target, thisArgument, argArray »). - trap.call( - &handler.into(), - &[target.clone().into(), this.clone(), arg_array.into()], - context, - ) + let _func = context.vm.pop(); + let this = context.vm.pop(); + + context.vm.push(handler); // This + context.vm.push(trap.clone()); // Function + + context.vm.push(target); + context.vm.push(this); + context.vm.push(arg_array); + Ok(trap.__call__(3)) } /// `[[Construct]] ( argumentsList, newTarget )` @@ -962,10 +968,9 @@ fn proxy_exotic_call( /// [spec]: https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-construct-argumentslist-newtarget fn proxy_exotic_construct( obj: &JsObject, - args: &[JsValue], - new_target: &JsObject, + argument_count: usize, context: &mut Context<'_>, -) -> JsResult { +) -> JsResult { // 1. Let handler be O.[[ProxyHandler]]. // 2. If handler is null, throw a TypeError exception. // 3. Assert: Type(handler) is Object. @@ -983,20 +988,20 @@ fn proxy_exotic_construct( let Some(trap) = handler.get_method(utf16!("construct"), context)? else { // 7. If trap is undefined, then // a. Return ? Construct(target, argumentsList, newTarget). - return target.construct(args, Some(new_target), context); + return Ok(target.__construct__(argument_count)); }; + let new_target = context.vm.pop(); + let args = context.vm.pop_n_values(argument_count); + let _func = context.vm.pop(); + // 8. Let argArray be ! CreateArrayFromList(argumentsList). - let arg_array = array::Array::create_array_from_list(args.to_vec(), context); + let arg_array = array::Array::create_array_from_list(args, context); // 9. Let newObj be ? Call(trap, handler, « target, argArray, newTarget »). let new_obj = trap.call( &handler.into(), - &[ - target.clone().into(), - arg_array.into(), - new_target.clone().into(), - ], + &[target.into(), arg_array.into(), new_target], context, )?; @@ -1006,5 +1011,6 @@ fn proxy_exotic_construct( })?; // 11. Return newObj. - Ok(new_obj) + context.vm.push(new_obj); + Ok(CallValue::Complete) } diff --git a/boa_engine/src/object/operations.rs b/boa_engine/src/object/operations.rs index 9dc56276590..c4f0c811fcf 100644 --- a/boa_engine/src/object/operations.rs +++ b/boa_engine/src/object/operations.rs @@ -319,12 +319,29 @@ impl JsObject { ) -> JsResult { // 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. + context.vm.push(this.clone()); // this + context.vm.push(self.clone()); // func + let argument_count = args.len(); + context.vm.push_values(args); + // 3. Return ? F.[[Call]](V, argumentsList). - self.__call__(this, args, context) + let frame_index = context.vm.frames.len(); + let is_complete = self.__call__(argument_count).resolve(context)?; + + if is_complete { + return Ok(context.vm.pop()); + } + + context.vm.frames[frame_index].exit_early = true; + + let result = context.run().consume(); + + context.vm.pop_frame().expect("frame must exist"); + + result } /// `Construct ( F [ , argumentsList [ , newTarget ] ] )` @@ -349,9 +366,32 @@ impl JsObject { ) -> JsResult { // 1. If newTarget is not present, set newTarget to F. let new_target = new_target.unwrap_or(self); + + context.vm.push(self.clone()); // func + let argument_count = args.len(); + context.vm.push_values(args); + context.vm.push(new_target.clone()); + // 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) + let frame_index = context.vm.frames.len(); + let is_complete = self.__construct__(argument_count).resolve(context)?; + + if is_complete { + let result = context.vm.pop(); + return Ok(result + .as_object() + .expect("construct value should be an object") + .clone()); + } + + context.vm.frames[frame_index].exit_early = true; + + let result = context.run().consume(); + + context.vm.pop_frame().expect("frame must exist"); + + Ok(result?.as_object().expect("should be an object").clone()) } /// Make the object [`sealed`][IntegrityLevel::Sealed] or [`frozen`][IntegrityLevel::Frozen]. @@ -1168,7 +1208,7 @@ impl JsValue { .into()); }; - object.__call__(this, args, context) + object.call(this, args, context) } /// Abstract operation `( V, P [ , argumentsList ] )` diff --git a/boa_engine/src/script.rs b/boa_engine/src/script.rs index e69b22713eb..b367d3ce9c2 100644 --- a/boa_engine/src/script.rs +++ b/boa_engine/src/script.rs @@ -141,21 +141,26 @@ impl Script { let codeblock = self.codeblock(context)?; - let old_realm = context.enter_realm(self.inner.realm.clone()); let env_fp = context.vm.environments.len() as u32; context.vm.push_frame( - CallFrame::new(codeblock, Some(ActiveRunnable::Script(self.clone())), None) - .with_env_fp(env_fp), + CallFrame::new( + codeblock, + Some(ActiveRunnable::Script(self.clone())), + context.vm.environments.clone(), + self.inner.realm.clone(), + ) + .with_env_fp(env_fp), ); + context.vm.push(JsValue::undefined()); // Push `this` value. + context.vm.push(JsValue::undefined()); // No function object, so push undefined. + // TODO: Here should be https://tc39.es/ecma262/#sec-globaldeclarationinstantiation self.realm().resize_global_env(); let record = context.run(); context.vm.pop_frame(); - context.enter_realm(old_realm); - context.clear_kept_objects(); record.consume() diff --git a/boa_engine/src/vm/call_frame/mod.rs b/boa_engine/src/vm/call_frame/mod.rs index 3caa44a990c..c53cd3fd7f7 100644 --- a/boa_engine/src/vm/call_frame/mod.rs +++ b/boa_engine/src/vm/call_frame/mod.rs @@ -4,15 +4,16 @@ use crate::{ builtins::{iterable::IteratorRecord, promise::PromiseCapability}, - environments::BindingLocator, + environments::{BindingLocator, EnvironmentStack}, object::JsObject, + realm::Realm, vm::CodeBlock, JsValue, }; use boa_gc::{Finalize, Gc, Trace}; use thin_vec::ThinVec; -use super::ActiveRunnable; +use super::{ActiveRunnable, Vm}; /// A `CallFrame` holds the state of a function call. #[derive(Clone, Debug, Finalize, Trace)] @@ -42,7 +43,12 @@ pub struct CallFrame { /// \[\[ScriptOrModule\]\] pub(crate) active_runnable: Option, - pub(crate) active_function: Option, + pub(crate) environments: EnvironmentStack, + + pub(crate) realm: Realm, + + pub(crate) exit_early: bool, + pub(crate) construct: bool, } /// ---- `CallFrame` public API ---- @@ -57,11 +63,15 @@ impl CallFrame { /// ---- `CallFrame` creation methods ---- impl CallFrame { + pub(crate) const THIS_POSITION: u32 = 0; + pub(crate) const FUNCTION_POSITION: u32 = 1; + /// Creates a new `CallFrame` with the provided `CodeBlock`. pub(crate) fn new( code_block: Gc, active_runnable: Option, - active_function: Option, + environments: EnvironmentStack, + realm: Realm, ) -> Self { Self { code_block, @@ -75,7 +85,10 @@ impl CallFrame { binding_stack: Vec::new(), loop_iteration_count: 0, active_runnable, - active_function, + environments, + realm, + exit_early: true, + construct: false, } } @@ -90,6 +103,20 @@ impl CallFrame { self.env_fp = env_fp; self } + + pub(crate) fn this(&self, vm: &Vm) -> JsValue { + let this_index = self.fp + Self::THIS_POSITION; + vm.stack[this_index as usize].clone() + } + + pub(crate) fn function(&self, vm: &Vm) -> Option { + let function_index = self.fp + Self::FUNCTION_POSITION; + if let Some(object) = vm.stack[function_index as usize].as_object() { + return Some(object.clone()); + } + + None + } } /// ---- `CallFrame` stack methods ---- diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index df17b441e38..0cef1c11c52 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -3,17 +3,12 @@ //! This module is for the `CodeBlock` which implements a function representation in the VM use crate::{ - builtins::function::{ - arguments::Arguments, ConstructorKind, FunctionKind, OrdinaryFunction, ThisMode, - }, - context::intrinsics::StandardConstructors, - environments::{BindingLocator, CompileTimeEnvironment, FunctionSlots, ThisBindingStatus}, - error::JsNativeError, - object::{internal_methods::get_prototype_from_constructor, JsObject, ObjectData, PROTOTYPE}, + builtins::function::{ConstructorKind, FunctionKind, OrdinaryFunction, ThisMode}, + environments::{BindingLocator, CompileTimeEnvironment}, + object::{JsObject, ObjectData, PROTOTYPE}, property::PropertyDescriptor, string::utf16, - vm::CallFrame, - Context, JsError, JsResult, JsString, JsValue, + Context, JsString, JsValue, }; use bitflags::bitflags; use boa_ast::function::FormalParameterList; @@ -360,7 +355,8 @@ impl CodeBlock { | Instruction::SuperCall { argument_count: value, } - | Instruction::ConcatToString { value_count: value } => value.value().to_string(), + | Instruction::ConcatToString { value_count: value } + | Instruction::GetArgument { index: value } => value.value().to_string(), Instruction::PushDeclarativeEnvironment { compile_environments_index, } => compile_environments_index.value().to_string(), @@ -533,6 +529,7 @@ impl CodeBlock { | Instruction::MaybeException | Instruction::This | Instruction::Super + | Instruction::CheckReturn | Instruction::Return | Instruction::AsyncGeneratorClose | Instruction::CreatePromiseCapability @@ -556,7 +553,6 @@ impl CodeBlock { | Instruction::RequireObjectCoercible | Instruction::ValueNotNullOrUndefined | Instruction::RestParameterInit - | Instruction::RestParameterPop | Instruction::PushValueToArray | Instruction::PushElisionToArray | Instruction::PushIteratorToArray @@ -582,6 +578,7 @@ impl CodeBlock { | Instruction::ImportCall | Instruction::GetReturnValue | Instruction::SetReturnValue + | Instruction::BindThisValue | Instruction::Nop => String::new(), Instruction::U16Operands @@ -641,9 +638,7 @@ impl CodeBlock { | Instruction::Reserved53 | Instruction::Reserved54 | Instruction::Reserved55 - | Instruction::Reserved56 - | Instruction::Reserved57 - | Instruction::Reserved58 => unreachable!("Reserved opcodes are unrechable"), + | Instruction::Reserved56 => unreachable!("Reserved opcodes are unrechable"), } } } @@ -1006,320 +1001,3 @@ pub(crate) fn create_generator_function_object( constructor } - -impl JsObject { - pub(crate) fn call_internal( - &self, - this: &JsValue, - args: &[JsValue], - context: &mut Context<'_>, - ) -> JsResult { - context.check_runtime_limits()?; - let old_realm = context.realm().clone(); - - let context = &mut context.guard(move |ctx| { - ctx.enter_realm(old_realm); - }); - - let this_function_object = self.clone(); - let object = self.borrow(); - let function = object.as_function().expect("not a function"); - let realm = function.realm().clone(); - - if let FunctionKind::Ordinary { .. } = function.kind() { - if function.code.is_class_constructor() { - return Err(JsNativeError::typ() - .with_message("class constructor cannot be invoked without 'new'") - .with_realm(realm) - .into()); - } - } - context.enter_realm(realm); - - let code = function.code.clone(); - let mut environments = function.environments.clone(); - let script_or_module = function.script_or_module.clone(); - - drop(object); - - std::mem::swap(&mut environments, &mut context.vm.environments); - - let lexical_this_mode = code.this_mode == ThisMode::Lexical; - - let this = if lexical_this_mode { - ThisBindingStatus::Lexical - } else if code.strict() { - ThisBindingStatus::Initialized(this.clone()) - } else if this.is_null_or_undefined() { - ThisBindingStatus::Initialized(context.realm().global_this().clone().into()) - } else { - ThisBindingStatus::Initialized( - this.to_object(context) - .expect("conversion cannot fail") - .into(), - ) - }; - - let env_fp = context.vm.environments.len() as u32; - - let mut last_env = 0; - - if code.has_binding_identifier() { - let index = context - .vm - .environments - .push_lexical(code.compile_environments[last_env].clone()); - context - .vm - .environments - .put_lexical_value(index, 0, self.clone().into()); - last_env += 1; - } - - context.vm.environments.push_function( - code.compile_environments[last_env].clone(), - FunctionSlots::new(this, self.clone(), None), - ); - - if code.has_parameters_env_bindings() { - last_env += 1; - context - .vm - .environments - .push_lexical(code.compile_environments[last_env].clone()); - } - - // Taken from: `FunctionDeclarationInstantiation` abstract function. - // - // Spec: https://tc39.es/ecma262/#sec-functiondeclarationinstantiation - // - // 22. If argumentsObjectNeeded is true, then - if code.needs_arguments_object() { - // a. If strict is true or simpleParameterList is false, then - // i. Let ao be CreateUnmappedArgumentsObject(argumentsList). - // b. Else, - // i. NOTE: A mapped argument object is only provided for non-strict functions - // that don't have a rest parameter, any parameter - // default value initializers, or any destructured parameters. - // ii. Let ao be CreateMappedArgumentsObject(func, formals, argumentsList, env). - let arguments_obj = if code.strict() || !code.params.is_simple() { - Arguments::create_unmapped_arguments_object(args, context) - } else { - let env = context.vm.environments.current(); - Arguments::create_mapped_arguments_object( - &this_function_object, - &code.params, - args, - env.declarative_expect(), - context, - ) - }; - let env_index = context.vm.environments.len() as u32 - 1; - context - .vm - .environments - .put_lexical_value(env_index, 0, arguments_obj.into()); - } - - let argument_count = args.len(); - let parameters_count = code.params.as_ref().len(); - - let frame = CallFrame::new(code, script_or_module, Some(self.clone())) - .with_argument_count(argument_count as u32) - .with_env_fp(env_fp); - - context.vm.push_frame(frame); - - // Push function arguments to the stack. - for _ in argument_count..parameters_count { - context.vm.push(JsValue::undefined()); - } - context.vm.stack.extend(args.iter().rev().cloned()); - - let result = context - .run() - .consume() - .map_err(|err| err.inject_realm(context.realm().clone())); - - context.vm.pop_frame().expect("frame must exist"); - std::mem::swap(&mut environments, &mut context.vm.environments); - - result - } - - pub(crate) fn construct_internal( - &self, - args: &[JsValue], - this_target: &JsValue, - context: &mut Context<'_>, - ) -> JsResult { - context.check_runtime_limits()?; - let old_realm = context.realm().clone(); - let context = &mut context.guard(move |ctx| { - ctx.enter_realm(old_realm); - }); - - let this_function_object = self.clone(); - let object = self.borrow(); - let function = object.as_function().expect("not a function"); - let realm = function.realm().clone(); - - context.enter_realm(realm); - - let FunctionKind::Ordinary { - constructor_kind, .. - } = function.kind() - else { - unreachable!("not a constructor") - }; - - let code = function.code.clone(); - let mut environments = function.environments.clone(); - let script_or_module = function.script_or_module.clone(); - let constructor_kind = *constructor_kind; - drop(object); - - let this = if constructor_kind.is_base() { - // 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 prototype = - get_prototype_from_constructor(this_target, StandardConstructors::object, context)?; - let this = Self::from_proto_and_data_with_shared_shape( - context.root_shape(), - prototype, - ObjectData::ordinary(), - ); - - this.initialize_instance_elements(self, context)?; - - Some(this) - } else { - None - }; - - let environments_len = environments.len(); - std::mem::swap(&mut environments, &mut context.vm.environments); - - let new_target = this_target.as_object().expect("must be object"); - - let mut last_env = 0; - - if code.has_binding_identifier() { - let index = context - .vm - .environments - .push_lexical(code.compile_environments[last_env].clone()); - context - .vm - .environments - .put_lexical_value(index, 0, self.clone().into()); - last_env += 1; - } - - context.vm.environments.push_function( - code.compile_environments[last_env].clone(), - FunctionSlots::new( - this.clone().map_or(ThisBindingStatus::Uninitialized, |o| { - ThisBindingStatus::Initialized(o.into()) - }), - self.clone(), - Some(new_target.clone()), - ), - ); - - let environment = context.vm.environments.current(); - - if code.has_parameters_env_bindings() { - last_env += 1; - context - .vm - .environments - .push_lexical(code.compile_environments[last_env].clone()); - } - - // Taken from: `FunctionDeclarationInstantiation` abstract function. - // - // Spec: https://tc39.es/ecma262/#sec-functiondeclarationinstantiation - // - // 22. If argumentsObjectNeeded is true, then - if code.needs_arguments_object() { - // a. If strict is true or simpleParameterList is false, then - // i. Let ao be CreateUnmappedArgumentsObject(argumentsList). - // b. Else, - // i. NOTE: A mapped argument object is only provided for non-strict functions - // that don't have a rest parameter, any parameter - // default value initializers, or any destructured parameters. - // ii. Let ao be CreateMappedArgumentsObject(func, formals, argumentsList, env). - let arguments_obj = if code.strict() || !code.params.is_simple() { - Arguments::create_unmapped_arguments_object(args, context) - } else { - let env = context.vm.environments.current(); - Arguments::create_mapped_arguments_object( - &this_function_object, - &code.params, - args, - env.declarative_expect(), - context, - ) - }; - - let env_index = context.vm.environments.len() as u32 - 1; - context - .vm - .environments - .put_lexical_value(env_index, 0, arguments_obj.into()); - } - - let argument_count = args.len(); - let parameters_count = code.params.as_ref().len(); - - context.vm.push_frame( - CallFrame::new(code, script_or_module, Some(self.clone())) - .with_argument_count(argument_count as u32) - .with_env_fp(environments_len as u32), - ); - - // Push function arguments to the stack. - for _ in argument_count..parameters_count { - context.vm.push(JsValue::undefined()); - } - context.vm.stack.extend(args.iter().rev().cloned()); - - let record = context.run(); - - context.vm.pop_frame(); - - std::mem::swap(&mut environments, &mut context.vm.environments); - - let result = record - .consume() - .map_err(|err| err.inject_realm(context.realm().clone()))?; - - if let Some(result) = result.as_object() { - Ok(result.clone()) - } else if let Some(this) = this { - Ok(this) - } else if !result.is_undefined() { - Err(JsNativeError::typ() - .with_message("derived constructor can only return an Object or undefined") - .into()) - } else { - let function_env = environment - .declarative_expect() - .kind() - .as_function() - .expect("must be function environment"); - function_env - .get_this_binding() - .map(|v| { - v.expect("constructors cannot be arrow functions") - .as_object() - .expect("this binding must be object") - .clone() - }) - .map_err(JsError::from) - } - } -} diff --git a/boa_engine/src/vm/flowgraph/mod.rs b/boa_engine/src/vm/flowgraph/mod.rs index 5152f1b200d..e5495cafed0 100644 --- a/boa_engine/src/vm/flowgraph/mod.rs +++ b/boa_engine/src/vm/flowgraph/mod.rs @@ -207,7 +207,8 @@ impl CodeBlock { | Instruction::Call { .. } | Instruction::New { .. } | Instruction::SuperCall { .. } - | Instruction::ConcatToString { .. } => { + | Instruction::ConcatToString { .. } + | Instruction::GetArgument { .. } => { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } @@ -425,7 +426,6 @@ impl CodeBlock { | Instruction::RequireObjectCoercible | Instruction::ValueNotNullOrUndefined | Instruction::RestParameterInit - | Instruction::RestParameterPop | Instruction::PushValueToArray | Instruction::PushElisionToArray | Instruction::PushIteratorToArray @@ -456,6 +456,8 @@ impl CodeBlock { | Instruction::SetReturnValue | Instruction::Exception | Instruction::MaybeException + | Instruction::CheckReturn + | Instruction::BindThisValue | Instruction::Nop => { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); @@ -520,9 +522,7 @@ impl CodeBlock { | Instruction::Reserved53 | Instruction::Reserved54 | Instruction::Reserved55 - | Instruction::Reserved56 - | Instruction::Reserved57 - | Instruction::Reserved58 => unreachable!("Reserved opcodes are unrechable"), + | Instruction::Reserved56 => unreachable!("Reserved opcodes are unrechable"), } } diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 7267fd61b5e..e36b3d84584 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -5,13 +5,11 @@ //! plus an interpreter to execute those instructions use crate::{ - environments::{DeclarativeEnvironment, EnvironmentStack}, - script::Script, - vm::code_block::Readable, + environments::EnvironmentStack, realm::Realm, script::Script, vm::code_block::Readable, Context, JsError, JsNativeError, JsNativeErrorKind, JsObject, JsResult, JsValue, Module, }; -use boa_gc::{custom_trace, Finalize, Gc, Trace}; +use boa_gc::{custom_trace, Finalize, Trace}; use boa_profiler::Profiler; use std::mem::size_of; @@ -74,6 +72,9 @@ pub struct Vm { /// because we don't push a frame for them. pub(crate) native_active_function: Option, + /// realm holds both the global object and the environment + pub(crate) realm: Realm, + #[cfg(feature = "trace")] pub(crate) trace: bool, } @@ -96,15 +97,16 @@ unsafe impl Trace for ActiveRunnable { impl Vm { /// Creates a new virtual machine. - pub(crate) fn new(global: Gc) -> Self { + pub(crate) fn new(realm: Realm) -> Self { Self { frames: Vec::with_capacity(16), stack: Vec::with_capacity(1024), return_value: JsValue::undefined(), - environments: EnvironmentStack::new(global), + environments: EnvironmentStack::new(realm.environment().clone()), pending_exception: None, runtime_limits: RuntimeLimits::default(), native_active_function: None, + realm, #[cfg(feature = "trace")] trace: false, } @@ -158,11 +160,20 @@ impl Vm { pub(crate) fn push_frame(&mut self, mut frame: CallFrame) { let current_stack_length = self.stack.len(); frame.set_frame_pointer(current_stack_length as u32); + std::mem::swap(&mut self.environments, &mut frame.environments); + std::mem::swap(&mut self.realm, &mut frame.realm); + self.frames.push(frame); } pub(crate) fn pop_frame(&mut self) -> Option { - self.frames.pop() + let mut frame = self.frames.pop(); + if let Some(frame) = &mut frame { + std::mem::swap(&mut self.environments, &mut frame.environments); + std::mem::swap(&mut self.realm, &mut frame.realm); + } + + frame } /// Handles an exception thrown at position `pc`. @@ -195,6 +206,23 @@ impl Vm { pub(crate) fn set_return_value(&mut self, value: JsValue) { self.return_value = value; } + + pub(crate) fn take_return_value(&mut self) -> JsValue { + std::mem::take(&mut self.return_value) + } + + pub(crate) fn pop_n_values(&mut self, n: usize) -> Vec { + let at = self.stack.len() - n; + self.stack.split_off(at) + } + + pub(crate) fn push_values(&mut self, values: &[JsValue]) { + self.stack.extend_from_slice(values); + } + + pub(crate) fn insert_values_at(&mut self, values: &[JsValue], at: usize) { + self.stack.splice(at..at, values.iter().cloned()); + } } #[derive(Debug, Clone, Copy, PartialEq)] @@ -213,7 +241,7 @@ impl Context<'_> { const OPERAND_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH; const NUMBER_OF_COLUMNS: usize = 4; - fn trace_call_frame(&self) { + pub(crate) fn trace_call_frame(&self) { let msg = if self.vm.frames.last().is_some() { format!( " Call Frame -- {} ", @@ -257,21 +285,42 @@ impl Context<'_> { .code_block .instruction_operands(&instruction, self.interner()); + let opcode = instruction.opcode(); + match opcode { + Opcode::Call + | Opcode::CallSpread + | Opcode::CallEval + | Opcode::CallEvalSpread + | Opcode::New + | Opcode::NewSpread + | Opcode::Return + | Opcode::SuperCall + | Opcode::SuperCallSpread + | Opcode::SuperCallDerived => { + println!(); + } + _ => {} + } + let instant = Instant::now(); let result = self.execute_instruction(); - let duration = instant.elapsed(); + let fp = self.vm.frames.last().map(|frame| frame.fp as usize); + let stack = { let mut stack = String::from("[ "); - for (i, value) in self.vm.stack.iter().rev().enumerate() { + for (i, (j, value)) in self.vm.stack.iter().enumerate().rev().enumerate() { match value { value if value.is_callable() => stack.push_str("[function]"), value if value.is_object() => stack.push_str("[object]"), value => stack.push_str(&value.display().to_string()), } - if i + 1 != self.vm.stack.len() { + if fp == Some(j) { + let frame_index = self.vm.frames.len() - 1; + stack.push_str(&format!(" |{frame_index}|")); + } else if i + 1 != self.vm.stack.len() { stack.push(','); } @@ -289,10 +338,9 @@ impl Context<'_> { }; println!( - "{: { self.trace_call_frame(); } - loop { + 'instruction: loop { #[cfg(feature = "fuzz")] { if self.instructions_remaining == 0 { @@ -353,22 +401,60 @@ impl Context<'_> { Ok(CompletionType::Normal) => {} Ok(CompletionType::Return) => { self.vm.stack.truncate(self.vm.frame().fp as usize); - let execution_result = std::mem::take(&mut self.vm.return_value); - return CompletionRecord::Normal(execution_result); + + let result = self.vm.take_return_value(); + if self.vm.frame().exit_early { + return CompletionRecord::Normal(result); + } + + self.vm.push(result); + self.vm.pop_frame(); } Ok(CompletionType::Throw) => { self.vm.stack.truncate(self.vm.frame().fp as usize); - return CompletionRecord::Throw( - self.vm - .pending_exception - .take() - .expect("Err must exist for a CompletionType::Throw"), - ); + + if self.vm.frame().exit_early { + return CompletionRecord::Throw( + self.vm + .pending_exception + .take() + .expect("Err must exist for a CompletionType::Throw"), + ); + } + + self.vm.pop_frame(); + + while let Some(frame) = self.vm.frames.last_mut() { + let pc = frame.pc; + let fp = frame.fp; + let exit_early = frame.exit_early; + + if self.vm.handle_exception_at(pc) { + continue 'instruction; + } + + if exit_early { + return CompletionRecord::Throw( + self.vm + .pending_exception + .take() + .expect("Err must exist for a CompletionType::Throw"), + ); + } + + self.vm.stack.truncate(fp as usize); + self.vm.pop_frame(); + } } // Early return immediately. Ok(CompletionType::Yield) => { - let result = self.vm.pop(); - return CompletionRecord::Return(result); + let result = self.vm.take_return_value(); + if self.vm.frame().exit_early { + return CompletionRecord::Return(result); + } + + self.vm.push(result); + self.vm.pop_frame(); } Err(err) => { if let Some(native_error) = err.as_native() { @@ -384,10 +470,19 @@ impl Context<'_> { return CompletionRecord::Throw(err); } JsNativeErrorKind::RuntimeLimit => { - self.vm - .environments - .truncate(self.vm.frame().env_fp as usize); - self.vm.stack.truncate(self.vm.frame().fp as usize); + let mut fp = self.vm.stack.len(); + let mut env_fp = self.vm.environments.len(); + while let Some(frame) = self.vm.frames.last() { + if frame.exit_early { + break; + } + + fp = frame.fp as usize; + env_fp = frame.env_fp as usize; + self.vm.pop_frame(); + } + self.vm.environments.truncate(env_fp); + self.vm.stack.truncate(fp); return CompletionRecord::Throw(err); } _ => {} @@ -401,11 +496,38 @@ impl Context<'_> { continue; } - self.vm - .environments - .truncate(self.vm.frame().env_fp as usize); - self.vm.stack.truncate(self.vm.frame().fp as usize); - return CompletionRecord::Throw(err); + if self.vm.frame().exit_early { + self.vm + .environments + .truncate(self.vm.frame().env_fp as usize); + self.vm.stack.truncate(self.vm.frame().fp as usize); + return CompletionRecord::Throw(err); + } + + // Inject realm before crossing the function boundry + let err = err.inject_realm(self.realm().clone()); + + self.vm.pop_frame(); + + while let Some(frame) = self.vm.frames.last_mut() { + let pc = frame.pc; + let fp = frame.fp; + let env_fp = frame.env_fp; + let exit_early = frame.exit_early; + + if self.vm.handle_exception_at(pc) { + self.vm.pending_exception = Some(err); + continue 'instruction; + } + + if exit_early { + return CompletionRecord::Throw(err); + } + + self.vm.environments.truncate(env_fp as usize); + self.vm.stack.truncate(fp as usize); + self.vm.pop_frame(); + } } } } diff --git a/boa_engine/src/vm/opcode/await/mod.rs b/boa_engine/src/vm/opcode/await/mod.rs index d865077f256..1123f8f9d30 100644 --- a/boa_engine/src/vm/opcode/await/mod.rs +++ b/boa_engine/src/vm/opcode/await/mod.rs @@ -1,7 +1,7 @@ use boa_gc::{Gc, GcRefCell}; use crate::{ - builtins::{generator::GeneratorContext, Promise}, + builtins::{generator::GeneratorContext, promise::PromiseCapability, Promise}, native_function::NativeFunction, object::FunctionObjectBuilder, vm::{opcode::Operation, CompletionType, GeneratorResumeKind}, @@ -125,11 +125,17 @@ impl Operation for Await { context, ); - if let Some(promise_capabality) = context.vm.frame().promise_capability.clone() { - context.vm.push(promise_capabality.promise().clone()); - } else { - context.vm.push(JsValue::undefined()); - } + let return_value = context + .vm + .frame() + .promise_capability + .as_ref() + .map(PromiseCapability::promise) + .cloned() + .map(JsValue::from) + .unwrap_or_default(); + + context.vm.set_return_value(return_value); Ok(CompletionType::Yield) } } diff --git a/boa_engine/src/vm/opcode/call/mod.rs b/boa_engine/src/vm/opcode/call/mod.rs index 5d3547be195..0c678f12da3 100644 --- a/boa_engine/src/vm/opcode/call/mod.rs +++ b/boa_engine/src/vm/opcode/call/mod.rs @@ -16,14 +16,8 @@ pub(crate) struct CallEval; impl CallEval { fn operation(context: &mut Context<'_>, argument_count: usize) -> JsResult { - let mut arguments = Vec::with_capacity(argument_count); - for _ in 0..argument_count { - arguments.push(context.vm.pop()); - } - arguments.reverse(); - - let func = context.vm.pop(); - let this = context.vm.pop(); + let at = context.vm.stack.len() - argument_count; + let func = &context.vm.stack[at - 1]; let Some(object) = func.as_object() else { return Err(JsNativeError::typ() @@ -35,6 +29,9 @@ impl CallEval { let eval = object.borrow().is_native_function(); if eval { + let arguments = context.vm.pop_n_values(argument_count); + let _func = context.vm.pop(); + let _this = context.vm.pop(); if let Some(x) = arguments.get(0) { let strict = context.vm.frame().code_block.strict(); let result = crate::builtins::eval::Eval::perform_eval(x, true, strict, context)?; @@ -42,10 +39,11 @@ impl CallEval { } else { context.vm.push(JsValue::Undefined); } - } else { - let result = object.__call__(&this, &arguments, context)?; - context.vm.push(result); + + return Ok(CompletionType::Normal); } + + object.__call__(argument_count).resolve(context)?; Ok(CompletionType::Normal) } } @@ -94,8 +92,9 @@ impl Operation for CallEvalSpread { .expect("arguments array in call spread function must be dense") .clone(); - let func = context.vm.pop(); - let this = context.vm.pop(); + // let func = context.vm.pop(); + let at = context.vm.stack.len(); + let func = &context.vm.stack[at - 1]; let Some(object) = func.as_object() else { return Err(JsNativeError::typ() @@ -103,10 +102,14 @@ impl Operation for CallEvalSpread { .into()); }; + let object = object.clone(); + // A native function with the name "eval" implies, that is this the built-in eval function. let eval = object.borrow().is_native_function(); if eval { + let _func = context.vm.pop(); + let _this = context.vm.pop(); if let Some(x) = arguments.get(0) { let strict = context.vm.frame().code_block.strict(); let result = crate::builtins::eval::Eval::perform_eval(x, true, strict, context)?; @@ -114,10 +117,14 @@ impl Operation for CallEvalSpread { } else { context.vm.push(JsValue::Undefined); } - } else { - let result = object.__call__(&this, &arguments, context)?; - context.vm.push(result); + + return Ok(CompletionType::Normal); } + + let argument_count = arguments.len(); + context.vm.push_values(&arguments); + + object.__call__(argument_count).resolve(context)?; Ok(CompletionType::Normal) } } @@ -131,14 +138,8 @@ pub(crate) struct Call; impl Call { fn operation(context: &mut Context<'_>, argument_count: usize) -> JsResult { - let mut arguments = Vec::with_capacity(argument_count); - for _ in 0..argument_count { - arguments.push(context.vm.pop()); - } - arguments.reverse(); - - let func = context.vm.pop(); - let this = context.vm.pop(); + let at = context.vm.stack.len() - argument_count; + let func = &context.vm.stack[at - 1]; let Some(object) = func.as_object() else { return Err(JsNativeError::typ() @@ -146,9 +147,7 @@ impl Call { .into()); }; - let result = object.__call__(&this, &arguments, context)?; - - context.vm.push(result); + object.__call__(argument_count).resolve(context)?; Ok(CompletionType::Normal) } } @@ -193,8 +192,11 @@ impl Operation for CallSpread { .expect("arguments array in call spread function must be dense") .clone(); - let func = context.vm.pop(); - let this = context.vm.pop(); + let argument_count = arguments.len(); + context.vm.push_values(&arguments); + + let at = context.vm.stack.len() - argument_count; + let func = &context.vm.stack[at - 1]; let Some(object) = func.as_object() else { return Err(JsNativeError::typ() @@ -202,9 +204,7 @@ impl Operation for CallSpread { .into()); }; - let result = object.__call__(&this, &arguments, context)?; - - context.vm.push(result); + object.__call__(argument_count).resolve(context)?; Ok(CompletionType::Normal) } } diff --git a/boa_engine/src/vm/opcode/control_flow/return.rs b/boa_engine/src/vm/opcode/control_flow/return.rs index 8db9cab8707..c173d213d86 100644 --- a/boa_engine/src/vm/opcode/control_flow/return.rs +++ b/boa_engine/src/vm/opcode/control_flow/return.rs @@ -1,6 +1,6 @@ use crate::{ vm::{opcode::Operation, CompletionType}, - Context, JsResult, + Context, JsNativeError, JsResult, }; /// `Return` implements the Opcode Operation for `Opcode::Return` @@ -19,6 +19,57 @@ impl Operation for Return { } } +/// `CheckReturn` implements the Opcode Operation for `Opcode::CheckReturn` +/// +/// Operation: +/// - Check return from a function. +#[derive(Debug, Clone, Copy)] +pub(crate) struct CheckReturn; + +impl Operation for CheckReturn { + const NAME: &'static str = "CheckReturn"; + const INSTRUCTION: &'static str = "INST - CheckReturn"; + + fn execute(context: &mut Context<'_>) -> JsResult { + if !context.vm.frame().construct { + return Ok(CompletionType::Normal); + } + let frame = context.vm.frame(); + let this = frame.this(&context.vm); + let result = context.vm.take_return_value(); + + let result = if result.is_object() { + result + } else if !this.is_undefined() { + this + } else if !result.is_undefined() { + let realm = context.vm.frame().realm.clone(); + context.vm.pending_exception = Some( + JsNativeError::typ() + .with_message("derived constructor can only return an Object or undefined") + .with_realm(realm) + .into(), + ); + return Ok(CompletionType::Throw); + } else { + let realm = context.vm.frame().realm.clone(); + let this = context.vm.environments.get_this_binding(); + + match this { + Err(err) => { + let err = err.inject_realm(realm); + context.vm.pending_exception = Some(err); + return Ok(CompletionType::Throw); + } + Ok(this) => this, + } + }; + + context.vm.set_return_value(result); + Ok(CompletionType::Normal) + } +} + /// `GetReturnValue` implements the Opcode Operation for `Opcode::GetReturnValue` /// /// Operation: diff --git a/boa_engine/src/vm/opcode/environment/mod.rs b/boa_engine/src/vm/opcode/environment/mod.rs index bd04b9db40d..1aecd2e03d1 100644 --- a/boa_engine/src/vm/opcode/environment/mod.rs +++ b/boa_engine/src/vm/opcode/environment/mod.rs @@ -80,23 +80,14 @@ impl Operation for SuperCallPrepare { .get_this_environment() .as_function() .expect("super call must be in function environment"); - let new_target = this_env - .slots() - .new_target() - .expect("must have new target") - .clone(); let active_function = this_env.slots().function_object().clone(); let super_constructor = active_function .__get_prototype_of__(context) .expect("function object must have prototype"); - if let Some(constructor) = super_constructor { - context.vm.push(constructor); - } else { - context.vm.push(JsValue::Null); - } - context.vm.push(new_target); - + context + .vm + .push(super_constructor.map_or_else(JsValue::null, JsValue::from)); Ok(CompletionType::Normal) } } @@ -110,27 +101,14 @@ pub(crate) struct SuperCall; impl SuperCall { fn operation(context: &mut Context<'_>, argument_count: usize) -> JsResult { - let mut arguments = Vec::with_capacity(argument_count); - for _ in 0..argument_count { - arguments.push(context.vm.pop()); - } - arguments.reverse(); - - let new_target_value = context.vm.pop(); - let super_constructor = context.vm.pop(); - - let new_target = new_target_value - .as_object() - .expect("new target must be object"); - + let super_constructor_index = context.vm.stack.len() - argument_count - 1; + let super_constructor = context.vm.stack[super_constructor_index].clone(); let Some(super_constructor) = super_constructor.as_constructor() else { return Err(JsNativeError::typ() .with_message("super constructor object must be constructor") .into()); }; - let result = super_constructor.__construct__(&arguments, new_target, context)?; - let this_env = context .vm .environments @@ -138,12 +116,17 @@ impl SuperCall { .as_function() .expect("super call must be in function environment"); - this_env.bind_this_value(result.clone())?; - let function_object = this_env.slots().function_object().clone(); + let new_target = this_env + .slots() + .new_target() + .expect("must have new.target") + .clone(); - result.initialize_instance_elements(&function_object, context)?; + context.vm.push(new_target); - context.vm.push(result); + super_constructor + .__construct__(argument_count) + .resolve(context)?; Ok(CompletionType::Normal) } } @@ -176,8 +159,8 @@ impl Operation for SuperCall { pub(crate) struct SuperCallSpread; impl Operation for SuperCallSpread { - const NAME: &'static str = "SuperCallWithRest"; - const INSTRUCTION: &'static str = "INST - SuperCallWithRest"; + const NAME: &'static str = "SuperCallSpread"; + const INSTRUCTION: &'static str = "INST - SuperCallSpread"; fn execute(context: &mut Context<'_>) -> JsResult { // Get the arguments that are stored as an array object on the stack. @@ -192,20 +175,17 @@ impl Operation for SuperCallSpread { .expect("arguments array in call spread function must be dense") .clone(); - let new_target_value = context.vm.pop(); let super_constructor = context.vm.pop(); - let new_target = new_target_value - .as_object() - .expect("new target must be object"); - let Some(super_constructor) = super_constructor.as_constructor() else { return Err(JsNativeError::typ() .with_message("super constructor object must be constructor") .into()); }; - let result = super_constructor.__construct__(&arguments, new_target, context)?; + context.vm.push(super_constructor.clone()); + + context.vm.push_values(&arguments); let this_env = context .vm @@ -214,12 +194,17 @@ impl Operation for SuperCallSpread { .as_function() .expect("super call must be in function environment"); - this_env.bind_this_value(result.clone())?; - let function_object = this_env.slots().function_object().clone(); + let new_target = this_env + .slots() + .new_target() + .expect("must have new.target") + .clone(); - result.initialize_instance_elements(&function_object, context)?; + context.vm.push(new_target); - context.vm.push(result); + super_constructor + .__construct__(arguments.len()) + .resolve(context)?; Ok(CompletionType::Normal) } } @@ -236,11 +221,7 @@ impl Operation for SuperCallDerived { const INSTRUCTION: &'static str = "INST - SuperCallDerived"; fn execute(context: &mut Context<'_>) -> JsResult { - let argument_count = context.vm.frame().argument_count; - let mut arguments = Vec::with_capacity(argument_count as usize); - for _ in 0..argument_count { - arguments.push(context.vm.pop()); - } + let argument_count = context.vm.frame().argument_count as usize; let this_env = context .vm @@ -265,8 +246,45 @@ impl Operation for SuperCallDerived { .into()); } - let result = super_constructor.__construct__(&arguments, &new_target, context)?; + let arguments_start_index = context.vm.stack.len() - argument_count; + context + .vm + .stack + .insert(arguments_start_index, super_constructor.clone().into()); + + context.vm.push(new_target); + + super_constructor + .__construct__(argument_count) + .resolve(context)?; + Ok(CompletionType::Normal) + } +} +/// `BindThisValue` implements the Opcode Operation for `Opcode::BindThisValue` +/// +/// Operation: +/// - Binds `this` value and initializes the instance elements. +#[derive(Debug, Clone, Copy)] +pub(crate) struct BindThisValue; + +impl Operation for BindThisValue { + const NAME: &'static str = "BindThisValue"; + const INSTRUCTION: &'static str = "INST - BindThisValue"; + + fn execute(context: &mut Context<'_>) -> JsResult { + // Taken from `SuperCall : super Arguments` steps 7-12. + // + // + + let result = context + .vm + .pop() + .as_object() + .expect("construct result should be an object") + .clone(); + + // 7. Let thisER be GetThisEnvironment(). let this_env = context .vm .environments @@ -274,10 +292,17 @@ impl Operation for SuperCallDerived { .as_function() .expect("super call must be in function environment"); + // 8. Perform ? thisER.BindThisValue(result). this_env.bind_this_value(result.clone())?; + // 9. Let F be thisER.[[FunctionObject]]. + // SKIP: 10. Assert: F is an ECMAScript function object. + let active_function = this_env.slots().function_object().clone(); + + // 11. Perform ? InitializeInstanceElements(result, F). result.initialize_instance_elements(&active_function, context)?; + // 12. Return result. context.vm.push(result); Ok(CompletionType::Normal) } diff --git a/boa_engine/src/vm/opcode/generator/mod.rs b/boa_engine/src/vm/opcode/generator/mod.rs index c3b5bc7e574..243ed6c412d 100644 --- a/boa_engine/src/vm/opcode/generator/mod.rs +++ b/boa_engine/src/vm/opcode/generator/mod.rs @@ -7,7 +7,6 @@ use crate::{ async_generator::{AsyncGenerator, AsyncGeneratorState}, generator::{GeneratorContext, GeneratorState}, }, - environments::EnvironmentStack, error::JsNativeError, object::{ObjectData, PROTOTYPE}, string::utf16, @@ -37,15 +36,23 @@ impl Operation for Generator { fn execute(context: &mut Context<'_>) -> JsResult { let r#async = context.vm.read::() != 0; - let code_block = context.vm.frame().code_block().clone(); - let active_runnable = context.vm.frame().active_runnable.clone(); - let active_function = context.vm.frame().active_function.clone(); - let pc = context.vm.frame().pc; - let mut dummy_call_frame = - CallFrame::new(code_block, active_runnable, active_function.clone()); + let frame = context.vm.frame(); + let code_block = frame.code_block().clone(); + let active_runnable = frame.active_runnable.clone(); + let active_function = frame.function(&context.vm); + let environments = frame.environments.clone(); + let realm = frame.realm.clone(); + let pc = frame.pc; + + let mut dummy_call_frame = CallFrame::new(code_block, active_runnable, environments, realm); dummy_call_frame.pc = pc; let mut call_frame = std::mem::replace(context.vm.frame_mut(), dummy_call_frame); + context.vm.frame_mut().exit_early = call_frame.exit_early; + + call_frame.environments = context.vm.environments.clone(); + call_frame.realm = context.realm().clone(); + let fp = call_frame.fp as usize; let stack = context.vm.stack[fp..].to_vec(); @@ -71,32 +78,16 @@ impl Operation for Generator { Clone::clone, ); - let global_environement = context.vm.environments.global(); - let environments = std::mem::replace( - &mut context.vm.environments, - EnvironmentStack::new(global_environement), - ); - let data = if r#async { ObjectData::async_generator(AsyncGenerator { state: AsyncGeneratorState::SuspendedStart, - context: Some(GeneratorContext::new( - environments, - stack, - call_frame, - context.realm().clone(), - )), + context: Some(GeneratorContext::new(stack, call_frame)), queue: VecDeque::new(), }) } else { ObjectData::generator(crate::builtins::generator::Generator { state: GeneratorState::SuspendedStart { - context: GeneratorContext::new( - environments, - stack, - call_frame, - context.realm().clone(), - ), + context: GeneratorContext::new(stack, call_frame), }, }) }; @@ -119,7 +110,7 @@ impl Operation for Generator { .async_generator = Some(gen_clone); } - context.vm.push(generator); + context.vm.set_return_value(generator.into()); Ok(CompletionType::Yield) } } diff --git a/boa_engine/src/vm/opcode/generator/yield_stm.rs b/boa_engine/src/vm/opcode/generator/yield_stm.rs index 237fbdca8f4..477a5cf6427 100644 --- a/boa_engine/src/vm/opcode/generator/yield_stm.rs +++ b/boa_engine/src/vm/opcode/generator/yield_stm.rs @@ -15,7 +15,9 @@ impl Operation for GeneratorYield { const NAME: &'static str = "GeneratorYield"; const INSTRUCTION: &'static str = "INST - GeneratorYield"; - fn execute(_context: &mut Context<'_>) -> JsResult { + fn execute(context: &mut Context<'_>) -> JsResult { + let value = context.vm.pop(); + context.vm.set_return_value(value); Ok(CompletionType::Yield) } } @@ -76,11 +78,11 @@ impl Operation for AsyncGeneratorYield { context.vm.push(resume_kind); - Ok(CompletionType::Normal) - } else { - gen.state = AsyncGeneratorState::SuspendedYield; - context.vm.push(JsValue::undefined()); - GeneratorYield::execute(context) + return Ok(CompletionType::Normal); } + + gen.state = AsyncGeneratorState::SuspendedYield; + context.vm.set_return_value(JsValue::undefined()); + Ok(CompletionType::Yield) } } diff --git a/boa_engine/src/vm/opcode/get/argument.rs b/boa_engine/src/vm/opcode/get/argument.rs new file mode 100644 index 00000000000..fc0e507a54d --- /dev/null +++ b/boa_engine/src/vm/opcode/get/argument.rs @@ -0,0 +1,47 @@ +use crate::{ + vm::{opcode::Operation, CompletionType}, + Context, JsResult, +}; + +/// `GetArgument` implements the Opcode Operation for `Opcode::GetArgument` +/// +/// Operation: +/// - Get i-th argument of the current frame. +#[derive(Debug, Clone, Copy)] +pub(crate) struct GetArgument; + +impl GetArgument { + #[allow(clippy::unnecessary_wraps)] + fn operation(context: &mut Context<'_>, index: usize) -> JsResult { + let fp = context.vm.frame().fp as usize; + let argument_index = fp + 2; + let argument_count = context.vm.frame().argument_count as usize; + + let value = context.vm.stack[argument_index..(argument_index + argument_count)] + .get(index) + .cloned() + .unwrap_or_default(); + context.vm.push(value); + Ok(CompletionType::Normal) + } +} + +impl Operation for GetArgument { + const NAME: &'static str = "GetArgument"; + const INSTRUCTION: &'static str = "INST - GetArgument"; + + fn execute(context: &mut Context<'_>) -> JsResult { + let index = context.vm.read::() as usize; + Self::operation(context, index) + } + + fn execute_with_u16_operands(context: &mut Context<'_>) -> JsResult { + let index = context.vm.read::() as usize; + Self::operation(context, index) + } + + fn execute_with_u32_operands(context: &mut Context<'_>) -> JsResult { + let index = context.vm.read::() as usize; + Self::operation(context, index) + } +} diff --git a/boa_engine/src/vm/opcode/get/mod.rs b/boa_engine/src/vm/opcode/get/mod.rs index a3f09232e5f..69a67d7a58d 100644 --- a/boa_engine/src/vm/opcode/get/mod.rs +++ b/boa_engine/src/vm/opcode/get/mod.rs @@ -1,9 +1,11 @@ +pub(crate) mod argument; pub(crate) mod function; pub(crate) mod generator; pub(crate) mod name; pub(crate) mod private; pub(crate) mod property; +pub(crate) use argument::*; pub(crate) use function::*; pub(crate) use generator::*; pub(crate) use name::*; diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index e6633663334..0ce4d92873e 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -1058,6 +1058,15 @@ generate_opcodes! { /// Stack: **=>** ThrowMutateImmutable { index: VaryingOperand }, + /// Get i-th argument of the current frame. + /// + /// Returns `undefined` if `arguments.len()` < `index`. + /// + /// Operands: index: `u32` + /// + /// Stack: **=>** value + GetArgument { index: VaryingOperand }, + /// Find a binding on the environment chain and push its value. /// /// Operands: index: `u32` @@ -1571,21 +1580,21 @@ generate_opcodes! { /// /// Operands: /// - /// Stack: **=>** super_constructor, new_target + /// Stack: **=>** super_constructor SuperCallPrepare, /// Execute the `super()` method. /// /// Operands: argument_count: `u32` /// - /// Stack: super_constructor, new_target, argument_1, ... argument_n **=>** + /// Stack: super_constructor, argument_1, ... argument_n **=>** SuperCall { argument_count: VaryingOperand }, /// Execute the `super()` method where the arguments contain spreads. /// /// Operands: /// - /// Stack: super_constructor, new_target, arguments_array **=>** + /// Stack: super_constructor, arguments_array **=>** SuperCallSpread, /// Execute the `super()` method when no constructor of the class is defined. @@ -1595,6 +1604,17 @@ generate_opcodes! { /// Stack: argument_n, ... argument_1 **=>** SuperCallDerived, + /// Binds `this` value and initializes the instance elements. + /// + /// Performs steps 7-12 of [`SuperCall: super Arguments`][spec] + /// + /// Operands: + /// + /// Stack: result **=>** result + /// + /// [spec]: https://tc39.es/ecma262/#sec-super-keyword-runtime-semantics-evaluation + BindThisValue, + /// Dynamically import a module. /// /// Operands: @@ -1701,6 +1721,13 @@ generate_opcodes! { /// Stack: arguments_array, func **=>** result NewSpread, + /// Check return from a function. + /// + /// Operands: + /// + /// Stack: **=>** + CheckReturn, + /// Return from a function. /// /// Operands: @@ -1912,18 +1939,11 @@ generate_opcodes! { /// Stack: `argument_1` .. `argument_n` **=>** `array` RestParameterInit, - /// Pop the remaining arguments of a function. - /// - /// Operands: - /// - /// Stack: `argument_1` .. `argument_n` **=>** - RestParameterPop, - /// Yields from the current generator execution. /// /// Operands: /// - /// Stack: **=>** resume_kind, received + /// Stack: value **=>** resume_kind, received GeneratorYield, /// Resumes the current generator function. @@ -2169,10 +2189,6 @@ generate_opcodes! { Reserved55 => Reserved, /// Reserved [`Opcode`]. Reserved56 => Reserved, - /// Reserved [`Opcode`]. - Reserved57 => Reserved, - /// Reserved [`Opcode`]. - Reserved58 => Reserved, } /// Specific opcodes for bindings. diff --git a/boa_engine/src/vm/opcode/new/mod.rs b/boa_engine/src/vm/opcode/new/mod.rs index 00b3192831c..c1dca0965de 100644 --- a/boa_engine/src/vm/opcode/new/mod.rs +++ b/boa_engine/src/vm/opcode/new/mod.rs @@ -13,23 +13,17 @@ pub(crate) struct New; impl New { fn operation(context: &mut Context<'_>, argument_count: usize) -> JsResult { - let mut arguments = Vec::with_capacity(argument_count); - for _ in 0..argument_count { - arguments.push(context.vm.pop()); - } - arguments.reverse(); - let func = context.vm.pop(); + let at = context.vm.stack.len() - argument_count; + let func = &context.vm.stack[at - 1]; + + let cons = func + .as_object() + .ok_or_else(|| JsNativeError::typ().with_message("not a constructor"))? + .clone(); - let result = func - .as_constructor() - .ok_or_else(|| { - JsNativeError::typ() - .with_message("not a constructor") - .into() - }) - .and_then(|cons| cons.__construct__(&arguments, cons, context))?; + context.vm.push(cons.clone()); // Push new.target - context.vm.push(result); + cons.__construct__(argument_count).resolve(context)?; Ok(CompletionType::Normal) } } @@ -80,16 +74,17 @@ impl Operation for NewSpread { let func = context.vm.pop(); - let result = func - .as_constructor() - .ok_or_else(|| { - JsNativeError::typ() - .with_message("not a constructor") - .into() - }) - .and_then(|cons| cons.__construct__(&arguments, cons, context))?; + let cons = func + .as_object() + .ok_or_else(|| JsNativeError::typ().with_message("not a constructor"))? + .clone(); + + let argument_count = arguments.len(); + context.vm.push(func); + context.vm.push_values(&arguments); + context.vm.push(cons.clone()); // Push new.target - context.vm.push(result); + cons.__construct__(argument_count).resolve(context)?; Ok(CompletionType::Normal) } } diff --git a/boa_engine/src/vm/opcode/rest_parameter/mod.rs b/boa_engine/src/vm/opcode/rest_parameter/mod.rs index 79a809b01b5..2855b52a689 100644 --- a/boa_engine/src/vm/opcode/rest_parameter/mod.rs +++ b/boa_engine/src/vm/opcode/rest_parameter/mod.rs @@ -18,45 +18,16 @@ impl Operation for RestParameterInit { fn execute(context: &mut Context<'_>) -> JsResult { let arg_count = context.vm.frame().argument_count as usize; let param_count = context.vm.frame().code_block().params.as_ref().len(); - if arg_count >= param_count { - let rest_count = arg_count - param_count + 1; - let mut args = Vec::with_capacity(rest_count); - for _ in 0..rest_count { - args.push(context.vm.pop()); - } - let array = Array::create_array_from_list(args, context); - context.vm.push(array); + let array = if arg_count >= param_count { + let rest_count = arg_count - param_count + 1; + let args = context.vm.pop_n_values(rest_count); + Array::create_array_from_list(args, context) } else { - context.vm.pop(); - - let array = - Array::array_create(0, None, context).expect("could not create an empty array"); - context.vm.push(array); - } - Ok(CompletionType::Normal) - } -} + Array::array_create(0, None, context).expect("could not create an empty array") + }; -/// `RestParameterPop` implements the Opcode Operation for `Opcode::RestParameterPop` -/// -/// Operation: -/// - Pop the remaining arguments of a function. -#[derive(Debug, Clone, Copy)] -pub(crate) struct RestParameterPop; - -impl Operation for RestParameterPop { - const NAME: &'static str = "RestParameterPop"; - const INSTRUCTION: &'static str = "INST - RestParameterPop"; - - fn execute(context: &mut Context<'_>) -> JsResult { - let arg_count = context.vm.frame().argument_count; - let param_count = context.vm.frame().code_block().params.as_ref().len() as u32; - if arg_count > param_count { - for _ in 0..(arg_count - param_count) { - context.vm.pop(); - } - } + context.vm.push(array); Ok(CompletionType::Normal) } }