use crate::{
Context, JsError, JsNativeError, JsObject, JsResult, JsString, JsValue, Module,
builtins::promise::{PromiseCapability, ResolvingFunctions},
environments::EnvironmentStack,
object::JsFunction,
realm::Realm,
script::Script,
};
use boa_gc::{Finalize, Gc, Trace, custom_trace};
use shadow_stack::ShadowStack;
use std::{future::Future, ops::ControlFlow, pin::Pin, task};
#[cfg(feature = "trace")]
use crate::sys::time::Instant;
#[cfg(feature = "trace")]
use std::fmt::Write as _;
#[allow(unused_imports)]
pub(crate) use opcode::{Instruction, InstructionIterator, Opcode};
pub(crate) use {
call_frame::CallFrameFlags,
code_block::{
CodeBlockFlags, Constant, Handler, create_function_object, create_function_object_fast,
},
completion_record::CompletionRecord,
inline_cache::InlineCache,
};
pub use runtime_limits::RuntimeLimits;
pub use {
call_frame::{CallFrame, GeneratorResumeKind},
code_block::CodeBlock,
source_info::{NativeSourceInfo, SourcePath},
};
mod call_frame;
mod code_block;
mod completion_record;
mod inline_cache;
mod runtime_limits;
pub(crate) mod opcode;
pub(crate) mod shadow_stack;
pub(crate) mod source_info;
#[cfg(feature = "flowgraph")]
pub mod flowgraph;
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub struct Vm {
pub(crate) frame: CallFrame,
pub(crate) frames: Vec<CallFrame>,
pub(crate) stack: Stack,
pub(crate) return_value: JsValue,
pub(crate) pending_exception: Option<JsError>,
pub(crate) environments: EnvironmentStack,
pub(crate) runtime_limits: RuntimeLimits,
pub(crate) native_active_function: Option<JsObject>,
pub(crate) realm: Realm,
pub(crate) shadow_stack: ShadowStack,
#[cfg(feature = "trace")]
pub(crate) trace: bool,
}
#[derive(Clone, Debug, Trace, Finalize)]
pub(crate) struct Stack {
stack: Vec<JsValue>,
}
impl Stack {
fn new(capacity: usize) -> Self {
Self {
stack: Vec::with_capacity(capacity),
}
}
pub(crate) fn truncate_to_frame(&mut self, frame: &CallFrame) {
self.stack.truncate(frame.frame_pointer());
}
pub(crate) fn split_off_frame(&mut self, frame: &CallFrame) -> Self {
let frame_pointer = frame.frame_pointer();
Self {
stack: self.stack.split_off(frame_pointer),
}
}
pub(crate) fn get_this(&self, frame: &CallFrame) -> JsValue {
self.stack[frame.this_index()].clone()
}
pub(crate) fn set_this(&mut self, frame: &CallFrame, this: JsValue) {
self.stack[frame.this_index()] = this;
}
pub(crate) fn get_function(&self, frame: &CallFrame) -> Option<JsObject> {
if let Some(object) = self.stack[frame.function_index()].as_object() {
return Some(object.clone());
}
None
}
pub(crate) fn get_arguments(&self, frame: &CallFrame) -> &[JsValue] {
&self.stack[frame.arguments_range()]
}
pub(crate) fn get_argument(&self, frame: &CallFrame, index: usize) -> Option<&JsValue> {
self.get_arguments(frame).get(index)
}
pub(crate) fn pop_rest_arguments(&mut self, frame: &CallFrame) -> Option<Vec<JsValue>> {
let argument_count = frame.argument_count as usize;
let param_count = frame.code_block().parameter_length as usize;
if argument_count < param_count {
return None;
}
let rp = frame.rp as usize;
let rest_count = argument_count - param_count + 1;
Some(self.stack.drain((rp - rest_count)..rp).collect())
}
#[track_caller]
pub(crate) fn set_promise_capability(
&mut self,
frame: &CallFrame,
promise_capability: Option<&PromiseCapability>,
) {
debug_assert!(
frame.code_block().is_async(),
"Only async functions have a promise capability"
);
self.stack[frame.promise_capability_promise_register_index()] = promise_capability
.map(PromiseCapability::promise)
.cloned()
.map_or_else(JsValue::undefined, Into::into);
self.stack[frame.promise_capability_resolve_register_index()] = promise_capability
.map(PromiseCapability::resolve)
.cloned()
.map_or_else(JsValue::undefined, Into::into);
self.stack[frame.promise_capability_reject_register_index()] = promise_capability
.map(PromiseCapability::reject)
.cloned()
.map_or_else(JsValue::undefined, Into::into);
}
#[track_caller]
pub(crate) fn get_promise_capability(&self, frame: &CallFrame) -> Option<PromiseCapability> {
if !frame.code_block().is_async() {
return None;
}
let promise = self
.stack
.get(frame.promise_capability_promise_register_index())
.expect("stack must have a promise capability")
.as_object()?;
let resolve = self
.stack
.get(frame.promise_capability_resolve_register_index())
.expect("stack must have a resolve function")
.as_object()
.and_then(JsFunction::from_object)?;
let reject = self
.stack
.get(frame.promise_capability_reject_register_index())
.expect("stack must have a reject function")
.as_object()
.and_then(JsFunction::from_object)?;
Some(PromiseCapability {
promise,
functions: ResolvingFunctions { resolve, reject },
})
}
#[track_caller]
pub(crate) fn set_async_generator_object(&mut self, frame: &CallFrame, object: JsObject) {
self.stack[frame.async_generator_object_register_index()] = object.into();
}
#[track_caller]
pub(crate) fn async_generator_object(&self, frame: &CallFrame) -> Option<JsObject> {
if !frame.code_block().is_async_generator() {
return None;
}
self.stack
.get(frame.async_generator_object_register_index())
.expect("stack must have an async generator object")
.as_object()
}
pub(crate) fn push<T>(&mut self, value: T)
where
T: Into<JsValue>,
{
self.stack.push(value.into());
}
#[track_caller]
pub(crate) fn pop(&mut self) -> JsValue {
self.stack.pop().expect("stack was empty")
}
pub(crate) fn calling_convention_pop_arguments(
&mut self,
argument_count: usize,
) -> Vec<JsValue> {
let index = self.stack.len() - argument_count;
self.stack.split_off(index)
}
pub(crate) fn calling_convention_push_arguments(&mut self, values: &[JsValue]) {
self.stack.extend_from_slice(values);
}
#[track_caller]
pub(crate) fn calling_convention_get_function(&self, argument_count: usize) -> &JsValue {
let index = self.stack.len() - 1 - argument_count;
self.stack
.get(index)
.expect("invalid calling convention function index")
}
#[track_caller]
pub(crate) fn calling_convention_set_function(
&mut self,
argument_count: usize,
function: JsValue,
) {
let index = self.stack.len() - 1 - argument_count;
self.stack[index] = function;
}
#[track_caller]
pub(crate) fn calling_convention_set_this(&mut self, argument_count: usize, function: JsValue) {
let index = self.stack.len() - 2 - argument_count;
self.stack[index] = function;
}
pub(crate) fn calling_convention_insert_arguments(
&mut self,
existing_argument_count: usize,
arguments: &[JsValue],
) {
let index = self.stack.len() - existing_argument_count;
self.stack.splice(index..index, arguments.iter().cloned());
}
#[cfg(feature = "trace")]
fn display_trace(&self, frame: &CallFrame, frame_count: usize) -> String {
let mut string = String::from("[ ");
for (i, (j, value)) in self.stack.iter().enumerate().rev().enumerate() {
match value {
value if value.is_callable() => string.push_str("[function]"),
value if value.is_object() => string.push_str("[object]"),
value => string.push_str(&value.display().to_string()),
}
if frame.frame_pointer() == j {
let _ = write!(string, " |{frame_count}|");
} else if i + 1 != self.stack.len() {
string.push(',');
}
string.push(' ');
}
string.push(']');
string
}
}
#[derive(Debug, Clone, Finalize)]
pub(crate) enum ActiveRunnable {
Script(Script),
Module(Module),
}
unsafe impl Trace for ActiveRunnable {
custom_trace!(this, mark, {
match this {
Self::Script(script) => mark(script),
Self::Module(module) => mark(module),
}
});
}
impl Vm {
pub(crate) fn new(realm: Realm) -> Self {
Self {
frames: Vec::with_capacity(16),
frame: CallFrame::new(
Gc::new(CodeBlock::new(JsString::default(), 0, true)),
None,
EnvironmentStack::new(realm.environment().clone()),
realm.clone(),
),
stack: Stack::new(1024),
return_value: JsValue::undefined(),
environments: EnvironmentStack::new(realm.environment().clone()),
pending_exception: None,
runtime_limits: RuntimeLimits::default(),
native_active_function: None,
realm,
shadow_stack: ShadowStack::default(),
#[cfg(feature = "trace")]
trace: false,
}
}
#[track_caller]
pub(crate) fn set_register(&mut self, index: usize, value: JsValue) {
self.stack.stack[self.frame.rp as usize + index] = value;
}
#[track_caller]
pub(crate) fn get_register(&self, index: usize) -> &JsValue {
self.stack
.stack
.get(self.frame.rp as usize + index)
.expect("registers must be initialized")
}
#[track_caller]
pub(crate) fn frame(&self) -> &CallFrame {
&self.frame
}
#[track_caller]
pub(crate) fn frame_mut(&mut self) -> &mut CallFrame {
&mut self.frame
}
pub(crate) fn push_frame(&mut self, mut frame: CallFrame) {
let current_stack_length = self.stack.stack.len();
frame.set_register_pointer(current_stack_length as u32);
std::mem::swap(&mut self.environments, &mut frame.environments);
std::mem::swap(&mut self.realm, &mut frame.realm);
if !frame.registers_already_pushed() {
self.stack.stack.resize_with(
current_stack_length + frame.code_block.register_count as usize,
JsValue::undefined,
);
}
if frame.active_runnable.is_none() {
frame
.active_runnable
.clone_from(&self.frame.active_runnable);
}
self.shadow_stack
.push_bytecode(self.frame.pc, frame.code_block().source_info.clone());
std::mem::swap(&mut self.frame, &mut frame);
self.frames.push(frame);
}
pub(crate) fn push_frame_with_stack(
&mut self,
frame: CallFrame,
this: JsValue,
function: JsValue,
) {
self.stack.push(this);
self.stack.push(function);
self.push_frame(frame);
}
pub(crate) fn pop_frame(&mut self) -> Option<CallFrame> {
if let Some(mut frame) = self.frames.pop() {
self.shadow_stack.pop();
std::mem::swap(&mut self.frame, &mut frame);
std::mem::swap(&mut self.environments, &mut frame.environments);
std::mem::swap(&mut self.realm, &mut frame.realm);
Some(frame)
} else {
None
}
}
#[inline]
pub(crate) fn handle_exception_at(&mut self, pc: u32) -> bool {
let frame = self.frame_mut();
let Some((_, handler)) = frame.code_block().find_handler(pc) else {
return false;
};
let catch_address = handler.handler();
let environment_sp = frame.env_fp + handler.environment_count;
frame.pc = catch_address;
self.environments.truncate(environment_sp as usize);
true
}
pub(crate) fn get_return_value(&self) -> JsValue {
self.return_value.clone()
}
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)
}
}
#[allow(clippy::print_stdout)]
#[cfg(feature = "trace")]
impl Context {
const COLUMN_WIDTH: usize = 26;
const TIME_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH / 2;
const OPCODE_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH;
const OPERAND_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH;
const NUMBER_OF_COLUMNS: usize = 4;
pub(crate) fn trace_call_frame(&self) {
let frame = self.vm.frame();
let msg = if self.vm.frames.is_empty() {
" VM Start ".to_string()
} else {
format!(
" Call Frame -- {} ",
frame.code_block().name().to_std_string_escaped()
)
};
println!("{}", frame.code_block);
println!(
"{msg:-^width$}",
width = Self::COLUMN_WIDTH * Self::NUMBER_OF_COLUMNS - 10
);
println!(
"{:<TIME_COLUMN_WIDTH$} {:<OPCODE_COLUMN_WIDTH$} {:<OPERAND_COLUMN_WIDTH$} Stack\n",
"Time",
"Opcode",
"Operands",
TIME_COLUMN_WIDTH = Self::TIME_COLUMN_WIDTH,
OPCODE_COLUMN_WIDTH = Self::OPCODE_COLUMN_WIDTH,
OPERAND_COLUMN_WIDTH = Self::OPERAND_COLUMN_WIDTH,
);
}
fn trace_execute_instruction<F>(
&mut self,
f: F,
opcode: Opcode,
) -> ControlFlow<CompletionRecord>
where
F: FnOnce(&mut Context, Opcode) -> ControlFlow<CompletionRecord>,
{
let frame = self.vm.frame();
let (instruction, _) = frame
.code_block
.bytecode
.next_instruction(frame.pc as usize);
let operands = self
.vm
.frame()
.code_block()
.instruction_operands(&instruction);
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(f, opcode);
let duration = instant.elapsed();
let stack = self
.vm
.stack
.display_trace(self.vm.frame(), self.vm.frames.len() - 1);
println!(
"{:<TIME_COLUMN_WIDTH$} {:<OPCODE_COLUMN_WIDTH$} {operands:<OPERAND_COLUMN_WIDTH$} {stack}",
format!("{}μs", duration.as_micros()),
format!("{}", opcode.as_str()),
TIME_COLUMN_WIDTH = Self::TIME_COLUMN_WIDTH,
OPCODE_COLUMN_WIDTH = Self::OPCODE_COLUMN_WIDTH,
OPERAND_COLUMN_WIDTH = Self::OPERAND_COLUMN_WIDTH,
);
result
}
}
impl Context {
fn execute_instruction<F>(&mut self, f: F, opcode: Opcode) -> ControlFlow<CompletionRecord>
where
F: FnOnce(&mut Context, Opcode) -> ControlFlow<CompletionRecord>,
{
f(self, opcode)
}
fn execute_one<F>(&mut self, f: F, opcode: Opcode) -> ControlFlow<CompletionRecord>
where
F: FnOnce(&mut Context, Opcode) -> ControlFlow<CompletionRecord>,
{
#[cfg(feature = "fuzz")]
{
if self.instructions_remaining == 0 {
return ControlFlow::Break(CompletionRecord::Throw(JsError::from_native(
JsNativeError::no_instructions_remain(),
)));
}
self.instructions_remaining -= 1;
}
#[cfg(feature = "trace")]
if self.vm.trace || self.vm.frame().code_block.traceable() {
self.trace_execute_instruction(f, opcode)
} else {
self.execute_instruction(f, opcode)
}
#[cfg(not(feature = "trace"))]
self.execute_instruction(f, opcode)
}
fn handle_error(&mut self, mut err: JsError) -> ControlFlow<CompletionRecord> {
if !err.is_catchable() {
if err.backtrace.is_none() {
err.backtrace = Some(
self.vm
.shadow_stack
.take(self.vm.runtime_limits.backtrace_limit(), self.vm.frame.pc),
);
}
let mut frame = None;
let mut env_fp = self.vm.environments.len();
loop {
if self.vm.frame.exit_early() {
break;
}
env_fp = self.vm.frame.env_fp as usize;
let Some(f) = self.vm.pop_frame() else {
break;
};
frame = Some(f);
}
self.vm.environments.truncate(env_fp);
if let Some(frame) = frame {
self.vm.stack.truncate_to_frame(&frame);
}
return ControlFlow::Break(CompletionRecord::Throw(err));
}
let pc = self.vm.frame().pc.saturating_sub(1);
if self.vm.handle_exception_at(pc) {
self.vm.pending_exception = Some(err);
return ControlFlow::Continue(());
}
let err = err.inject_realm(self.realm().clone());
self.vm.pending_exception = Some(err);
self.handle_throw()
}
fn handle_return(&mut self) -> ControlFlow<CompletionRecord> {
let exit_early = self.vm.frame().exit_early();
self.vm.stack.truncate_to_frame(&self.vm.frame);
let result = self.vm.take_return_value();
if exit_early {
return ControlFlow::Break(CompletionRecord::Normal(result));
}
self.vm.stack.push(result);
self.vm.pop_frame().expect("frame must exist");
ControlFlow::Continue(())
}
fn handle_yield(&mut self) -> ControlFlow<CompletionRecord> {
let result = self.vm.take_return_value();
if self.vm.frame().exit_early() {
return ControlFlow::Break(CompletionRecord::Return(result));
}
self.vm.stack.push(result);
self.vm.pop_frame().expect("frame must exist");
ControlFlow::Continue(())
}
fn handle_throw(&mut self) -> ControlFlow<CompletionRecord> {
if let Some(err) = &mut self.vm.pending_exception
&& err.backtrace.is_none()
{
err.backtrace = Some(
self.vm
.shadow_stack
.take(self.vm.runtime_limits.backtrace_limit(), self.vm.frame.pc),
);
}
let mut env_fp = self.vm.frame().env_fp;
if self.vm.frame().exit_early() {
self.vm.environments.truncate(env_fp as usize);
self.vm.stack.truncate_to_frame(&self.vm.frame);
return ControlFlow::Break(CompletionRecord::Throw(
self.vm
.pending_exception
.take()
.expect("Err must exist for a CompletionType::Throw"),
));
}
let mut frame = self.vm.pop_frame().expect("frame must exist");
loop {
env_fp = self.vm.frame.env_fp;
let pc = self.vm.frame.pc;
let exit_early = self.vm.frame.exit_early();
if self.vm.handle_exception_at(pc) {
return ControlFlow::Continue(());
}
if exit_early {
return ControlFlow::Break(CompletionRecord::Throw(
self.vm
.pending_exception
.take()
.expect("Err must exist for a CompletionType::Throw"),
));
}
let Some(f) = self.vm.pop_frame() else {
break;
};
frame = f;
}
self.vm.environments.truncate(env_fp as usize);
self.vm.stack.truncate_to_frame(&frame);
ControlFlow::Continue(())
}
#[allow(clippy::future_not_send)]
pub(crate) async fn run_async_with_budget(&mut self, budget: u32) -> CompletionRecord {
#[cfg(feature = "trace")]
if self.vm.trace {
self.trace_call_frame();
}
let mut runtime_budget: u32 = budget;
while let Some(byte) = self
.vm
.frame
.code_block
.bytecode
.bytecode
.get(self.vm.frame.pc as usize)
{
let opcode = Opcode::decode(*byte);
match self.execute_one(
|context, opcode| {
context.execute_bytecode_instruction_with_budget(&mut runtime_budget, opcode)
},
opcode,
) {
ControlFlow::Continue(()) => {}
ControlFlow::Break(value) => return value,
}
if runtime_budget == 0 {
runtime_budget = budget;
yield_now().await;
}
}
CompletionRecord::Throw(JsError::from_native(JsNativeError::error()))
}
pub(crate) fn run(&mut self) -> CompletionRecord {
#[cfg(feature = "trace")]
if self.vm.trace {
self.trace_call_frame();
}
while let Some(byte) = self
.vm
.frame
.code_block
.bytecode
.bytecode
.get(self.vm.frame.pc as usize)
{
let opcode = Opcode::decode(*byte);
match self.execute_one(Self::execute_bytecode_instruction, opcode) {
ControlFlow::Continue(()) => {}
ControlFlow::Break(value) => return value,
}
}
CompletionRecord::Throw(JsError::from_native(JsNativeError::error()))
}
pub(crate) fn check_runtime_limits(&self) -> JsResult<()> {
if self.vm.runtime_limits.recursion_limit() <= self.vm.frames.len() {
return Err(JsNativeError::runtime_limit()
.with_message("exceeded maximum number of recursive calls")
.into());
}
if self.vm.runtime_limits.stack_size_limit() <= self.vm.stack.stack.len() {
return Err(JsNativeError::runtime_limit()
.with_message("exceeded maximum call stack length")
.into());
}
Ok(())
}
}
fn yield_now() -> impl Future<Output = ()> {
struct YieldNow(bool);
impl Future for YieldNow {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
if self.0 {
task::Poll::Ready(())
} else {
self.0 = true;
cx.waker().wake_by_ref();
task::Poll::Pending
}
}
}
YieldNow(false)
}