From 1052ccdd0c791092eb73dfd0d0b13dc3d9eb6ef0 Mon Sep 17 00:00:00 2001 From: Jason Williams <936006+jasonwilliams@users.noreply.github.com> Date: Sat, 2 Jan 2021 18:01:37 +0000 Subject: [PATCH] Bytecode Interpreter (new branch) (#860) Nodes implement CodeGen which generates instructions onto a stack held in Context. The VM will interpret the instructions from Context. There are some issues: - Only basic instructions are added, but I'm working off https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Internals/Bytecode for now it should be easy to add more in. - The Stack is a Vec, this isn't ideal (we may be able to live with it for now) but the stack should really be a fixed sized array. This isn't possible because Value can't be copied in there as it holds Rc and Gc values. Can we have fixed-sized Values that hold a pointer? Something like the "stackvec" crate should help - put all VM related code behind "vm" feature flag Co-authored-by: Jason Williams Co-authored-by: Halid Odat --- .vscode/tasks.json | 25 +- boa/Cargo.toml | 3 + boa/src/context.rs | 51 +++- boa/src/lib.rs | 2 + boa/src/profiler.rs | 2 - .../syntax/ast/node/operator/bin_op/mod.rs | 56 ++++ .../syntax/ast/node/operator/unary_op/mod.rs | 27 ++ boa/src/syntax/ast/node/statement_list/mod.rs | 14 + boa/src/vm/compilation.rs | 64 ++++ boa/src/vm/instructions.rs | 105 +++++++ boa/src/vm/mod.rs | 279 ++++++++++++++++++ boa_cli/Cargo.toml | 3 + 12 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 boa/src/vm/compilation.rs create mode 100644 boa/src/vm/instructions.rs create mode 100644 boa/src/vm/mod.rs diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7592103972..1667f00566 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,14 +12,35 @@ "kind": "build", "isDefault": true }, + "presentation": { + "clear": true + }, "options": { "env": { - "RUST_BACKTRACE": "full" + "RUST_BACKTRACE": "1" } }, + "problemMatcher": [] + }, + { + "type": "process", + "label": "Cargo Run (VM)", + "command": "cargo", + "args": ["run", "--features", "vm", "../tests/js/test.js"], + "group": { + "kind": "build", + "isDefault": true + }, "presentation": { "clear": true - } + }, + "options": { + "cwd": "${workspaceFolder}/boa_cli", + "env": { + "RUST_BACKTRACE": "1" + } + }, + "problemMatcher": [] }, { "type": "process", diff --git a/boa/Cargo.toml b/boa/Cargo.toml index 0473e033db..61272035c7 100644 --- a/boa/Cargo.toml +++ b/boa/Cargo.toml @@ -14,6 +14,9 @@ edition = "2018" profiler = ["measureme", "once_cell"] deser = [] +# Enable Bytecode generation & execution instead of tree walking +vm = [] + # Enable Boa's WHATWG console object implementation. console = [] diff --git a/boa/src/context.rs b/boa/src/context.rs index c56e7170d7..0c835132d8 100644 --- a/boa/src/context.rs +++ b/boa/src/context.rs @@ -30,6 +30,12 @@ use std::result::Result as StdResult; #[cfg(feature = "console")] use crate::builtins::console::Console; +#[cfg(feature = "vm")] +use crate::vm::{ + compilation::{CodeGen, Compiler}, + VM, +}; + /// Store a builtin constructor (such as `Object`) and its corresponding prototype. #[derive(Debug, Clone)] pub struct StandardConstructor { @@ -225,7 +231,7 @@ pub struct Context { /// Cached iterator prototypes. iterator_prototypes: IteratorPrototypes, - /// Cached standard objects and their prototypes + /// Cached standard objects and their prototypes. standard_objects: StandardObjects, } @@ -700,6 +706,7 @@ impl Context { /// assert!(value.is_number()); /// assert_eq!(value.as_number().unwrap(), 4.0); /// ``` + #[cfg(not(feature = "vm"))] #[allow(clippy::unit_arg, clippy::drop_copy)] #[inline] pub fn eval>(&mut self, src: T) -> Result { @@ -722,6 +729,48 @@ impl Context { execution_result } + /// Evaluates the given code by compiling down to bytecode, then interpreting the bytecode into a value + /// + /// # Examples + /// ``` + ///# use boa::Context; + /// let mut context = Context::new(); + /// + /// let value = context.eval("1 + 3").unwrap(); + /// + /// assert!(value.is_number()); + /// assert_eq!(value.as_number().unwrap(), 4.0); + /// ``` + #[cfg(feature = "vm")] + #[allow(clippy::unit_arg, clippy::drop_copy)] + pub fn eval>(&mut self, src: T) -> Result { + let main_timer = BoaProfiler::global().start_event("Main", "Main"); + let src_bytes: &[u8] = src.as_ref(); + + let parsing_result = Parser::new(src_bytes, false) + .parse_all() + .map_err(|e| e.to_string()); + + let statement_list = match parsing_result { + Ok(statement_list) => statement_list, + Err(e) => return self.throw_syntax_error(e), + }; + + let mut compiler = Compiler::default(); + statement_list.compile(&mut compiler); + dbg!(&compiler); + + let mut vm = VM::new(compiler, self); + // Generate Bytecode and place it into instruction_stack + // Interpret the Bytecode + let result = vm.run(); + // The main_timer needs to be dropped before the BoaProfiler is. + drop(main_timer); + BoaProfiler::global().drop(); + + result + } + /// Returns a structure that contains the JavaScript well known symbols. /// /// # Examples diff --git a/boa/src/lib.rs b/boa/src/lib.rs index 33ed6ee2cc..0540130fc3 100644 --- a/boa/src/lib.rs +++ b/boa/src/lib.rs @@ -53,6 +53,8 @@ pub mod property; pub mod realm; pub mod syntax; pub mod value; +#[cfg(feature = "vm")] +pub mod vm; pub mod context; diff --git a/boa/src/profiler.rs b/boa/src/profiler.rs index a372fe18f9..8458c70e52 100644 --- a/boa/src/profiler.rs +++ b/boa/src/profiler.rs @@ -11,8 +11,6 @@ use std::{ thread::{current, ThreadId}, }; -#[cfg(feature = "profiler")] -type SerializationSink = measureme::SerializationSink; #[cfg(feature = "profiler")] pub struct BoaProfiler { profiler: Profiler, diff --git a/boa/src/syntax/ast/node/operator/bin_op/mod.rs b/boa/src/syntax/ast/node/operator/bin_op/mod.rs index ba0edecba2..f99e3ed28b 100644 --- a/boa/src/syntax/ast/node/operator/bin_op/mod.rs +++ b/boa/src/syntax/ast/node/operator/bin_op/mod.rs @@ -12,6 +12,12 @@ use std::fmt; #[cfg(feature = "deser")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "vm")] +use crate::{ + profiler::BoaProfiler, + vm::{compilation::CodeGen, Compiler, Instruction}, +}; + /// Binary operators requires two operands, one before the operator and one after the operator. /// /// More information: @@ -230,6 +236,56 @@ impl Executable for BinOp { } } +#[cfg(feature = "vm")] +impl CodeGen for BinOp { + fn compile(&self, compiler: &mut Compiler) { + let _timer = BoaProfiler::global().start_event("binOp", "codeGen"); + match self.op() { + op::BinOp::Num(op) => { + self.lhs().compile(compiler); + self.rhs().compile(compiler); + match op { + NumOp::Add => compiler.add_instruction(Instruction::Add), + NumOp::Sub => compiler.add_instruction(Instruction::Sub), + NumOp::Mul => compiler.add_instruction(Instruction::Mul), + NumOp::Div => compiler.add_instruction(Instruction::Div), + NumOp::Exp => compiler.add_instruction(Instruction::Pow), + NumOp::Mod => compiler.add_instruction(Instruction::Mod), + } + } + op::BinOp::Bit(op) => { + self.lhs().compile(compiler); + self.rhs().compile(compiler); + match op { + BitOp::And => compiler.add_instruction(Instruction::BitAnd), + BitOp::Or => compiler.add_instruction(Instruction::BitOr), + BitOp::Xor => compiler.add_instruction(Instruction::BitXor), + BitOp::Shl => compiler.add_instruction(Instruction::Shl), + BitOp::Shr => compiler.add_instruction(Instruction::Shr), + BitOp::UShr => compiler.add_instruction(Instruction::UShr), + } + } + op::BinOp::Comp(op) => { + self.lhs().compile(compiler); + self.rhs().compile(compiler); + match op { + CompOp::Equal => compiler.add_instruction(Instruction::Eq), + CompOp::NotEqual => compiler.add_instruction(Instruction::NotEq), + CompOp::StrictEqual => compiler.add_instruction(Instruction::StrictEq), + CompOp::StrictNotEqual => compiler.add_instruction(Instruction::StrictNotEq), + CompOp::GreaterThan => compiler.add_instruction(Instruction::Gt), + CompOp::GreaterThanOrEqual => compiler.add_instruction(Instruction::Ge), + CompOp::LessThan => compiler.add_instruction(Instruction::Lt), + CompOp::LessThanOrEqual => compiler.add_instruction(Instruction::Le), + CompOp::In => compiler.add_instruction(Instruction::In), + CompOp::InstanceOf => compiler.add_instruction(Instruction::InstanceOf), + } + } + _ => unimplemented!(), + } + } +} + impl fmt::Display for BinOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} {} {}", self.lhs, self.op, self.rhs) diff --git a/boa/src/syntax/ast/node/operator/unary_op/mod.rs b/boa/src/syntax/ast/node/operator/unary_op/mod.rs index 7b3966d474..03afa2c755 100644 --- a/boa/src/syntax/ast/node/operator/unary_op/mod.rs +++ b/boa/src/syntax/ast/node/operator/unary_op/mod.rs @@ -9,6 +9,12 @@ use std::fmt; #[cfg(feature = "deser")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "vm")] +use crate::{ + profiler::BoaProfiler, + vm::{compilation::CodeGen, Compiler, Instruction}, +}; + /// A unary operation is an operation with only one operand. /// /// More information: @@ -128,3 +134,24 @@ impl From for Node { Self::UnaryOp(op) } } + +#[cfg(feature = "vm")] +impl CodeGen for UnaryOp { + fn compile(&self, compiler: &mut Compiler) { + let _timer = BoaProfiler::global().start_event("UnaryOp", "codeGen"); + self.target().compile(compiler); + match self.op { + op::UnaryOp::Void => compiler.add_instruction(Instruction::Void), + op::UnaryOp::Plus => compiler.add_instruction(Instruction::Pos), + op::UnaryOp::Minus => compiler.add_instruction(Instruction::Neg), + op::UnaryOp::TypeOf => compiler.add_instruction(Instruction::TypeOf), + op::UnaryOp::Not => compiler.add_instruction(Instruction::Not), + op::UnaryOp::Tilde => compiler.add_instruction(Instruction::BitNot), + op::UnaryOp::IncrementPost => {} + op::UnaryOp::IncrementPre => {} + op::UnaryOp::DecrementPost => {} + op::UnaryOp::DecrementPre => {} + op::UnaryOp::Delete => {} + } + } +} diff --git a/boa/src/syntax/ast/node/statement_list/mod.rs b/boa/src/syntax/ast/node/statement_list/mod.rs index 17e299f899..563eafbda8 100644 --- a/boa/src/syntax/ast/node/statement_list/mod.rs +++ b/boa/src/syntax/ast/node/statement_list/mod.rs @@ -11,6 +11,9 @@ use std::{fmt, ops::Deref, rc::Rc}; #[cfg(feature = "deser")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "vm")] +use crate::vm::{compilation::CodeGen, Compiler}; + /// List of statements. /// /// Similar to `Node::Block` but without the braces. @@ -92,6 +95,17 @@ impl Executable for StatementList { } } +#[cfg(feature = "vm")] +impl CodeGen for StatementList { + fn compile(&self, compiler: &mut Compiler) { + let _timer = BoaProfiler::global().start_event("StatementList - Code Gen", "codeGen"); + + for item in self.items().iter() { + item.compile(compiler); + } + } +} + impl From for StatementList where T: Into>, diff --git a/boa/src/vm/compilation.rs b/boa/src/vm/compilation.rs new file mode 100644 index 0000000000..b4f51b453b --- /dev/null +++ b/boa/src/vm/compilation.rs @@ -0,0 +1,64 @@ +use super::*; +use crate::{syntax::ast::Const, syntax::ast::Node, value::RcBigInt, value::RcString}; + +#[derive(Debug, Default)] +pub struct Compiler { + pub(super) instructions: Vec, + pub(super) pool: Vec, +} + +impl Compiler { + // Add a new instruction. + pub fn add_instruction(&mut self, instr: Instruction) { + self.instructions.push(instr); + } + + pub fn add_string_instruction(&mut self, string: S) + where + S: Into, + { + let index = self.pool.len(); + self.add_instruction(Instruction::String(index)); + self.pool.push(string.into().into()); + } + + pub fn add_bigint_instruction(&mut self, bigint: B) + where + B: Into, + { + let index = self.pool.len(); + self.add_instruction(Instruction::BigInt(index)); + self.pool.push(bigint.into().into()); + } +} + +pub(crate) trait CodeGen { + fn compile(&self, compiler: &mut Compiler); +} + +impl CodeGen for Node { + fn compile(&self, compiler: &mut Compiler) { + let _timer = BoaProfiler::global().start_event(&format!("Node ({})", &self), "codeGen"); + match *self { + Node::Const(Const::Undefined) => compiler.add_instruction(Instruction::Undefined), + Node::Const(Const::Null) => compiler.add_instruction(Instruction::Null), + Node::Const(Const::Bool(true)) => compiler.add_instruction(Instruction::True), + Node::Const(Const::Bool(false)) => compiler.add_instruction(Instruction::False), + Node::Const(Const::Num(num)) => compiler.add_instruction(Instruction::Rational(num)), + Node::Const(Const::Int(num)) => match num { + 0 => compiler.add_instruction(Instruction::Zero), + 1 => compiler.add_instruction(Instruction::One), + _ => compiler.add_instruction(Instruction::Int32(num)), + }, + Node::Const(Const::String(ref string)) => { + compiler.add_string_instruction(string.clone()) + } + Node::Const(Const::BigInt(ref bigint)) => { + compiler.add_bigint_instruction(bigint.clone()) + } + Node::BinOp(ref op) => op.compile(compiler), + Node::UnaryOp(ref op) => op.compile(compiler), + _ => unimplemented!(), + } + } +} diff --git a/boa/src/vm/instructions.rs b/boa/src/vm/instructions.rs new file mode 100644 index 0000000000..86fc972dff --- /dev/null +++ b/boa/src/vm/instructions.rs @@ -0,0 +1,105 @@ +#[derive(Debug, Clone, Copy)] +pub enum Instruction { + Undefined, + Null, + True, + False, + Zero, + One, + String(usize), + BigInt(usize), + + /// Loads an i32 onto the stack + Int32(i32), + + /// Loads an f32 onto the stack + Rational(f64), + + /// Adds the values from destination and source and stores the result in destination + Add, + + /// subtracts the values from destination and source and stores the result in destination + Sub, + + /// Multiplies the values from destination and source and stores the result in destination + Mul, + + /// Divides the values from destination and source and stores the result in destination + Div, + + Pow, + + Mod, + + BitAnd, + BitOr, + BitXor, + Shl, + Shr, + UShr, + + Eq, + NotEq, + StrictEq, + StrictNotEq, + + Gt, + Ge, + Lt, + Le, + + In, + InstanceOf, + + Void, + TypeOf, + Pos, + Neg, + BitNot, + Not, +} + +impl std::fmt::Display for Instruction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Self::Undefined => write!(f, "Undefined"), + Self::Null => write!(f, "Null"), + Self::True => write!(f, "True"), + Self::False => write!(f, "False"), + Self::Zero => write!(f, "Zero"), + Self::One => write!(f, "One"), + Self::String(usize) => write!(f, "String({})", usize), + Self::BigInt(usize) => write!(f, "BigInt({})", usize), + Self::Int32(i32) => write!(f, "Int32({})", i32), + Self::Rational(f64) => write!(f, "Rational({})", f64), + Self::Add => write!(f, "Add"), + Self::Sub => write!(f, "Sub"), + Self::Mul => write!(f, "Mul"), + Self::Div => write!(f, "Div"), + Self::Pow => write!(f, "Pow"), + Self::Mod => write!(f, "Mod"), + Self::BitAnd => write!(f, "BitAnd"), + Self::BitOr => write!(f, "BitOr"), + Self::BitXor => write!(f, "BitXor"), + Self::Shl => write!(f, "Shl"), + Self::Shr => write!(f, "Shr"), + Self::UShr => write!(f, "UShr"), + Self::Eq => write!(f, "Eq"), + Self::NotEq => write!(f, "NotEq"), + Self::StrictEq => write!(f, "StrictEq"), + Self::StrictNotEq => write!(f, "StrictNotEq"), + Self::Gt => write!(f, "Gt"), + Self::Ge => write!(f, "Ge"), + Self::Lt => write!(f, "Lt"), + Self::Le => write!(f, "Le"), + Self::In => write!(f, "In"), + Self::InstanceOf => write!(f, "InstanceOf"), + Self::Void => write!(f, "Void"), + Self::TypeOf => write!(f, "TypeOf"), + Self::Pos => write!(f, "Pos"), + Self::Neg => write!(f, "Neg"), + Self::BitNot => write!(f, "BitNot"), + Self::Not => write!(f, "Not"), + } + } +} diff --git a/boa/src/vm/mod.rs b/boa/src/vm/mod.rs new file mode 100644 index 0000000000..ac73339acf --- /dev/null +++ b/boa/src/vm/mod.rs @@ -0,0 +1,279 @@ +use crate::{Context, Result, Value}; + +pub(crate) mod compilation; +pub(crate) mod instructions; + +use crate::BoaProfiler; +pub use compilation::Compiler; +pub use instructions::Instruction; + +// Virtual Machine. +#[derive(Debug)] +pub struct VM<'a> { + ctx: &'a mut Context, + instructions: Vec, + pool: Vec, + stack: Vec, + stack_pointer: usize, +} + +impl<'a> VM<'a> { + pub fn new(compiler: Compiler, ctx: &'a mut Context) -> Self { + Self { + ctx, + instructions: compiler.instructions, + pool: compiler.pool, + stack: vec![], + stack_pointer: 0, + } + } + + /// Push a value on the stack. + #[inline] + pub fn push(&mut self, value: Value) { + self.stack.push(value); + } + + /// Pop a value off the stack. + /// + /// # Panics + /// + /// If there is nothing to pop, then this will panic. + #[inline] + pub fn pop(&mut self) -> Value { + self.stack.pop().unwrap() + } + + pub fn run(&mut self) -> Result { + let _timer = BoaProfiler::global().start_event("runVM", "vm"); + let mut idx = 0; + + while idx < self.instructions.len() { + let _timer = + BoaProfiler::global().start_event(&self.instructions[idx].to_string(), "vm"); + match self.instructions[idx] { + Instruction::Undefined => self.push(Value::undefined()), + Instruction::Null => self.push(Value::null()), + Instruction::True => self.push(Value::boolean(true)), + Instruction::False => self.push(Value::boolean(false)), + Instruction::Zero => self.push(Value::integer(0)), + Instruction::One => self.push(Value::integer(1)), + Instruction::Int32(i) => self.push(Value::integer(i)), + Instruction::Rational(r) => self.push(Value::rational(r)), + Instruction::String(index) => { + let value = self.pool[index].clone(); + self.push(value) + } + Instruction::BigInt(index) => { + let value = self.pool[index].clone(); + self.push(value) + } + Instruction::Add => { + let r = self.pop(); + let l = self.pop(); + let val = l.add(&r, self.ctx)?; + + self.push(val); + } + Instruction::Sub => { + let r = self.pop(); + let l = self.pop(); + let val = l.sub(&r, self.ctx)?; + + self.push(val); + } + Instruction::Mul => { + let r = self.pop(); + let l = self.pop(); + let val = l.mul(&r, self.ctx)?; + + self.push(val); + } + Instruction::Div => { + let r = self.pop(); + let l = self.pop(); + let val = l.div(&r, self.ctx)?; + + self.push(val); + } + Instruction::Pow => { + let r = self.pop(); + let l = self.pop(); + let val = l.pow(&r, self.ctx)?; + + self.push(val); + } + Instruction::Mod => { + let r = self.pop(); + let l = self.pop(); + let val = l.rem(&r, self.ctx)?; + + self.push(val); + } + Instruction::BitAnd => { + let r = self.pop(); + let l = self.pop(); + let val = l.bitand(&r, self.ctx)?; + + self.push(val); + } + Instruction::BitOr => { + let r = self.pop(); + let l = self.pop(); + let val = l.bitor(&r, self.ctx)?; + + self.push(val); + } + Instruction::BitXor => { + let r = self.pop(); + let l = self.pop(); + let val = l.bitxor(&r, self.ctx)?; + + self.push(val); + } + Instruction::Shl => { + let r = self.pop(); + let l = self.pop(); + let val = l.shl(&r, self.ctx)?; + + self.push(val); + } + Instruction::Shr => { + let r = self.pop(); + let l = self.pop(); + let val = l.shr(&r, self.ctx)?; + + self.push(val); + } + Instruction::UShr => { + let r = self.pop(); + let l = self.pop(); + let val = l.ushr(&r, self.ctx)?; + + self.push(val); + } + Instruction::Eq => { + let r = self.pop(); + let l = self.pop(); + let val = l.equals(&r, self.ctx)?; + + self.push(val.into()); + } + Instruction::NotEq => { + let r = self.pop(); + let l = self.pop(); + let val = !l.equals(&r, self.ctx)?; + + self.push(val.into()); + } + Instruction::StrictEq => { + let r = self.pop(); + let l = self.pop(); + let val = l.strict_equals(&r); + + self.push(val.into()); + } + Instruction::StrictNotEq => { + let r = self.pop(); + let l = self.pop(); + let val = !l.strict_equals(&r); + + self.push(val.into()); + } + Instruction::Gt => { + let r = self.pop(); + let l = self.pop(); + let val = l.ge(&r, self.ctx)?; + + self.push(val.into()); + } + Instruction::Ge => { + let r = self.pop(); + let l = self.pop(); + let val = l.ge(&r, self.ctx)?; + + self.push(val.into()); + } + Instruction::Lt => { + let r = self.pop(); + let l = self.pop(); + let val = l.lt(&r, self.ctx)?; + + self.push(val.into()); + } + Instruction::Le => { + let r = self.pop(); + let l = self.pop(); + let val = l.le(&r, self.ctx)?; + + self.push(val.into()); + } + Instruction::In => { + let r = self.pop(); + let l = self.pop(); + + if !r.is_object() { + return self.ctx.throw_type_error(format!( + "right-hand side of 'in' should be an object, got {}", + r.get_type().as_str() + )); + } + let key = l.to_property_key(self.ctx)?; + let val = self.ctx.has_property(&r, &key); + + self.push(val.into()); + } + Instruction::InstanceOf => { + let r = self.pop(); + let _l = self.pop(); + if !r.is_object() { + return self.ctx.throw_type_error(format!( + "right-hand side of 'instanceof' should be an object, got {}", + r.get_type().as_str() + )); + } + + // spec: https://tc39.es/ecma262/#sec-instanceofoperator + todo!("instanceof operator") + } + Instruction::Void => { + let _value = self.pop(); + self.push(Value::undefined()); + } + Instruction::TypeOf => { + let value = self.pop(); + self.push(value.get_type().as_str().into()); + } + Instruction::Pos => { + let value = self.pop(); + let value = value.to_number(self.ctx)?; + self.push(value.into()); + } + Instruction::Neg => { + let value = self.pop(); + self.push(Value::from(!value.to_boolean())); + } + Instruction::Not => { + let value = self.pop(); + self.push((!value.to_boolean()).into()); + } + Instruction::BitNot => { + let target = self.pop(); + let num = target.to_number(self.ctx)?; + let value = if num.is_nan() { + -1 + } else { + // TODO: this is not spec compliant. + !(num as i32) + }; + self.push(value.into()); + } + } + + idx += 1; + } + + let res = self.pop(); + Ok(res) + } +} diff --git a/boa_cli/Cargo.toml b/boa_cli/Cargo.toml index c87930a186..233eeafdf0 100644 --- a/boa_cli/Cargo.toml +++ b/boa_cli/Cargo.toml @@ -21,6 +21,9 @@ colored = "2.0.0" regex = "1.4.2" lazy_static = "1.4.0" +[features] +vm = ["Boa/vm"] + [target.x86_64-unknown-linux-gnu.dependencies] jemallocator = "0.3.2"