From 86f40956835c2834c0e006efbefbc824ec46d90d Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Fri, 30 Jun 2023 22:35:24 -0600 Subject: [PATCH] Use virtual stack for JS to JS calls --- boa_engine/src/script.rs | 5 +- boa_engine/src/vm/mod.rs | 396 ++++++++++++++++++++++++++- boa_engine/src/vm/opcode/call/mod.rs | 36 +++ boa_engine/src/vm/opcode/mod.rs | 23 +- 4 files changed, 451 insertions(+), 9 deletions(-) diff --git a/boa_engine/src/script.rs b/boa_engine/src/script.rs index 737337bfeae..9d2ecfe8946 100644 --- a/boa_engine/src/script.rs +++ b/boa_engine/src/script.rs @@ -148,8 +148,7 @@ impl Script { // TODO: Here should be https://tc39.es/ecma262/#sec-globaldeclarationinstantiation self.realm().resize_global_env(); - let record = context.run(); - context.vm.pop_frame(); + let record = context.run2(); context.vm.stack = stack; context.vm.active_function = active_function; @@ -158,6 +157,6 @@ impl Script { context.clear_kept_objects(); - record.consume() + record } } diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index f9bca1d87ac..4a4194ecda5 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -4,14 +4,16 @@ //! This module will provide an instruction set for the AST to use, various traits, //! plus an interpreter to execute those instructions -#[cfg(feature = "fuzz")] -use crate::JsNativeError; use crate::{ - builtins::async_generator::{AsyncGenerator, AsyncGeneratorState}, - environments::{DeclarativeEnvironment, EnvironmentStack}, + builtins::{ + async_generator::{AsyncGenerator, AsyncGeneratorState}, + function::{arguments::Arguments, FunctionKind, ThisMode}, + }, + environments::{DeclarativeEnvironment, EnvironmentStack, FunctionSlots, ThisBindingStatus}, + realm::Realm, script::Script, vm::code_block::Readable, - Context, JsError, JsObject, JsResult, JsValue, Module, + Context, JsError, JsNativeError, JsObject, JsResult, JsValue, Module, }; use boa_gc::{custom_trace, Finalize, Gc, Trace}; @@ -34,6 +36,8 @@ mod runtime_limits; pub mod flowgraph; pub use runtime_limits::RuntimeLimits; + +use self::opcode::ExecutionResult; pub use {call_frame::CallFrame, code_block::CodeBlock, opcode::Opcode}; pub(crate) use { @@ -49,6 +53,14 @@ pub(crate) use { #[cfg(test)] mod tests; +struct CallerState { + realm: Realm, + active_function: Option, + environments: EnvironmentStack, + stack: Vec, + active_runnable: Option, +} + /// Virtual Machine. #[derive(Debug)] pub struct Vm { @@ -140,6 +152,11 @@ impl Vm { self.frames.last_mut().expect("no frame found") } + // TODO: Rename `frame` to `frame_expect` and make this `frame`. + pub(crate) fn frame_opt(&self) -> Option<&CallFrame> { + self.frames.last() + } + pub(crate) fn push_frame(&mut self, frame: CallFrame) { self.frames.push(frame); } @@ -174,6 +191,23 @@ impl Context<'_> { opcode.execute(self) } + fn execute_instruction2(&mut self) -> JsResult { + let opcode: Opcode = { + let _timer = Profiler::global().start_event("Opcode retrieval", "vm"); + + let frame = self.vm.frame_mut(); + + let pc = frame.pc; + let opcode = Opcode::from(frame.code_block.bytecode[pc as usize]); + frame.pc += 1; + opcode + }; + + let _timer = Profiler::global().start_event(opcode.as_instruction_str(), "vm"); + + opcode.execute2(self) + } + pub(crate) fn run(&mut self) -> CompletionRecord { #[cfg(feature = "trace")] const COLUMN_WIDTH: usize = 26; @@ -447,4 +481,356 @@ impl Context<'_> { } CompletionRecord::Normal(execution_result) } + + pub(crate) fn run2(&mut self) -> JsResult { + #[cfg(feature = "trace")] + const COLUMN_WIDTH: usize = 26; + #[cfg(feature = "trace")] + const TIME_COLUMN_WIDTH: usize = COLUMN_WIDTH / 2; + #[cfg(feature = "trace")] + const OPCODE_COLUMN_WIDTH: usize = COLUMN_WIDTH; + #[cfg(feature = "trace")] + const OPERAND_COLUMN_WIDTH: usize = COLUMN_WIDTH; + #[cfg(feature = "trace")] + const NUMBER_OF_COLUMNS: usize = 4; + + let _timer = Profiler::global().start_event("run2", "vm"); + + #[cfg(feature = "trace")] + if self.vm.trace { + let msg = if self.vm.frames.last().is_some() { + " Call Frame " + } else { + " VM Start " + }; + + println!( + "{}\n", + self.vm + .frame() + .code_block + .to_interned_string(self.interner()) + ); + println!( + "{msg:-^width$}", + width = COLUMN_WIDTH * NUMBER_OF_COLUMNS - 10 + ); + println!( + "{: = Vec::new(); + + let result = loop { + // Exit the execution loop if there aren't any more callers. + let Some(frame) = self.vm.frame_opt() else { + break self + .vm + .err + .take() + .map_or_else(|| Ok(self.vm.pop()), Err); + }; + + if frame.code_block.bytecode.len() <= frame.pc as usize { + let push_undef = self.vm.stack.len() <= self.vm.frame().fp as usize; + + // TODO: cleanup this hack. + if let Some(last) = caller_stack.pop() { + self.restore_caller(last); + } + + if push_undef { + self.vm.push(JsValue::undefined()); + } + + self.vm.pop_frame(); + continue; + } + + let result = if let Some(err) = self.vm.err.take() { + Err(err) + } else { + #[cfg(feature = "trace")] + if self.vm.trace || self.vm.frame().code_block.traceable() { + let mut pc = self.vm.frame().pc as usize; + let opcode: Opcode = self + .vm + .frame() + .code_block + .read::(pc) + .try_into() + .expect("invalid opcode"); + let operands = self + .vm + .frame() + .code_block + .instruction_operands(&mut pc, self.interner()); + + let instant = Instant::now(); + let result = self.execute_instruction2(); + + let duration = instant.elapsed(); + println!( + "{: "[function]".to_string(), + Some(value) if value.is_object() => "[object]".to_string(), + Some(value) => value.display().to_string(), + None => "".to_string(), + }, + ); + + result + } else { + self.execute_instruction2() + } + + #[cfg(not(feature = "trace"))] + self.execute_instruction2() + }; + + let completion = match result { + Ok(ExecutionResult::Call { f, this, args }) => { + prepare_call(f, this, args, &mut caller_stack, self); + continue; + } + Ok(ExecutionResult::Completion(completion)) => completion, + Err(err) => { + self.vm.err = Some(err); + + // If this frame has not evaluated the throw as an AbruptCompletion, then evaluate it + let evaluation = Opcode::Throw + .execute(self) + .expect("Opcode::Throw cannot return Err"); + + if evaluation == CompletionType::Normal { + continue; + } + + CompletionType::Throw + } + }; + + if completion == CompletionType::Throw { + // TODO: cleanup this hack. + if !caller_stack.is_empty() { + self.restore_caller( + caller_stack + .pop() + .expect("already checked that the stack is not empty"), + ); + } + self.vm.pop_frame(); + } else if completion == CompletionType::Return { + let result = self.vm.pop(); + // TODO: cleanup this hack. + if let Some(last) = caller_stack.pop() { + self.restore_caller(last); + } + self.vm.push(result); + self.vm.pop_frame(); + } + }; + + result + } + + fn restore_caller(&mut self, state: CallerState) { + self.vm.environments = state.environments; + self.vm.stack = state.stack; + self.vm.active_function = state.active_function; + self.vm.active_runnable = state.active_runnable; + self.enter_realm(state.realm); + } +} + +fn prepare_call( + f: JsObject, + this: JsValue, + args: Vec, + caller_stack: &mut Vec, + context: &mut Context<'_>, +) { + let object = f.borrow(); + let function_object = object.as_function().expect("not a function"); + let realm = function_object.realm().clone(); + + let old_realm = context.enter_realm(realm); + + let old_active_function = context.vm.active_function.replace(f.clone()); + + let (code, mut environments, class_object, mut script_or_module) = match function_object.kind() + { + FunctionKind::Ordinary { + code, + environments, + class_object, + script_or_module, + .. + } => { + let code = code.clone(); + if code.is_class_constructor() { + context.vm.err = Some( + JsNativeError::typ() + .with_message("class constructor cannot be invoked without 'new'") + .with_realm(context.realm().clone()) + .into(), + ); + return; + } + ( + code, + environments.clone(), + class_object.clone(), + script_or_module.clone(), + ) + } + _ => { + drop(object); + match f.call_internal(&this, &args, context) { + Ok(val) => context.vm.push(val), + Err(err) => { + context.vm.err = Some(err); + } + } + context.enter_realm(old_realm); + context.vm.active_function = old_active_function; + return; + } + }; + + 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 mut last_env = code.compile_environments.len() - 1; + + if let Some(class_object) = class_object { + let index = context + .vm + .environments + .push_lexical(code.compile_environments[last_env].clone()); + context + .vm + .environments + .put_lexical_value(index, 0, class_object.into()); + last_env -= 1; + } + + 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, f.clone().into()); + last_env -= 1; + } + + context.vm.environments.push_function( + code.compile_environments[last_env].clone(), + FunctionSlots::new(this, f.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( + &f, + &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(); + + // Push function arguments to the stack. + let mut args = if code.params.as_ref().len() > args.len() { + let mut v = args.to_vec(); + v.extend(vec![ + JsValue::Undefined; + code.params.as_ref().len() - args.len() + ]); + v + } else { + args.to_vec() + }; + args.reverse(); + let mut stack = args; + + std::mem::swap(&mut context.vm.stack, &mut stack); + + let frame = CallFrame::new(code).with_argument_count(argument_count as u32); + + std::mem::swap(&mut context.vm.active_runnable, &mut script_or_module); + + context.vm.push_frame(frame); + + caller_stack.push(CallerState { + realm: old_realm, + active_function: old_active_function, + environments, + stack, + active_runnable: script_or_module, + }); } diff --git a/boa_engine/src/vm/opcode/call/mod.rs b/boa_engine/src/vm/opcode/call/mod.rs index 4bdf52d7d94..f09335f1e6c 100644 --- a/boa_engine/src/vm/opcode/call/mod.rs +++ b/boa_engine/src/vm/opcode/call/mod.rs @@ -7,6 +7,8 @@ use crate::{ Context, JsResult, JsValue, NativeFunction, }; +use super::ExecutionResult; + /// `CallEval` implements the Opcode Operation for `Opcode::CallEval` /// /// Operation: @@ -198,6 +200,40 @@ impl Operation for Call { context.vm.push(result); Ok(CompletionType::Normal) } + + fn execute2(context: &mut Context<'_>) -> JsResult { + let argument_count = context.vm.read::(); + let mut arguments = Vec::with_capacity(argument_count as usize); + for _ in 0..argument_count { + arguments.push(context.vm.pop()); + } + arguments.reverse(); + + let func = context.vm.pop(); + let this = context.vm.pop(); + + let object = match func { + JsValue::Object(ref object) if object.is_callable() => object.clone(), + _ => { + return Err(JsNativeError::typ() + .with_message("not a callable function") + .into()); + } + }; + + if object.is_function() { + Ok(ExecutionResult::Call { + f: object, + this, + args: arguments, + }) + } else { + let result = object.__call__(&this, &arguments, context)?; + + context.vm.push(result); + Ok(ExecutionResult::Completion(CompletionType::Normal)) + } + } } #[derive(Debug, Clone, Copy)] diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index 59dfe9194e9..e09ce7b9685 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -1,5 +1,5 @@ /// The opcodes of the vm. -use crate::{vm::CompletionType, Context, JsResult}; +use crate::{vm::CompletionType, Context, JsObject, JsResult, JsValue}; // Operation modules mod await_stm; @@ -155,10 +155,27 @@ macro_rules! generate_impl { pub(super) fn execute(self, context: &mut Context<'_>) -> JsResult { Self::EXECUTE_FNS[self as usize](context) } + + const EXECUTE2_FNS: [fn(&mut Context<'_>) -> JsResult; Self::MAX] = [ + $( $mapping)?)>::execute2),* + ]; + + pub(super) fn execute2(self, context: &mut Context<'_>) -> JsResult { + Self::EXECUTE2_FNS[self as usize](context) + } } }; } +pub(crate) enum ExecutionResult { + Completion(CompletionType), + Call { + f: JsObject, + this: JsValue, + args: Vec, + }, +} + /// The `Operation` trait implements the execution code along with the /// identifying Name and Instruction value for an Boa Opcode. /// @@ -170,6 +187,10 @@ pub(crate) trait Operation { const INSTRUCTION: &'static str; fn execute(context: &mut Context<'_>) -> JsResult; + + fn execute2(context: &mut Context<'_>) -> JsResult { + Self::execute(context).map(ExecutionResult::Completion) + } } generate_impl! {