Browse Source

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 <jwilliams720@bloomberg.net>
Co-authored-by: Halid Odat <halidodat@gmail.com>
pull/1031/head
Jason Williams 4 years ago committed by GitHub
parent
commit
1052ccdd0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      .vscode/tasks.json
  2. 3
      boa/Cargo.toml
  3. 51
      boa/src/context.rs
  4. 2
      boa/src/lib.rs
  5. 2
      boa/src/profiler.rs
  6. 56
      boa/src/syntax/ast/node/operator/bin_op/mod.rs
  7. 27
      boa/src/syntax/ast/node/operator/unary_op/mod.rs
  8. 14
      boa/src/syntax/ast/node/statement_list/mod.rs
  9. 64
      boa/src/vm/compilation.rs
  10. 105
      boa/src/vm/instructions.rs
  11. 279
      boa/src/vm/mod.rs
  12. 3
      boa_cli/Cargo.toml

25
.vscode/tasks.json vendored

@ -12,14 +12,35 @@
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true
}, },
"presentation": {
"clear": true
},
"options": { "options": {
"env": { "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": { "presentation": {
"clear": true "clear": true
} },
"options": {
"cwd": "${workspaceFolder}/boa_cli",
"env": {
"RUST_BACKTRACE": "1"
}
},
"problemMatcher": []
}, },
{ {
"type": "process", "type": "process",

3
boa/Cargo.toml

@ -14,6 +14,9 @@ edition = "2018"
profiler = ["measureme", "once_cell"] profiler = ["measureme", "once_cell"]
deser = [] deser = []
# Enable Bytecode generation & execution instead of tree walking
vm = []
# Enable Boa's WHATWG console object implementation. # Enable Boa's WHATWG console object implementation.
console = [] console = []

51
boa/src/context.rs

@ -30,6 +30,12 @@ use std::result::Result as StdResult;
#[cfg(feature = "console")] #[cfg(feature = "console")]
use crate::builtins::console::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. /// Store a builtin constructor (such as `Object`) and its corresponding prototype.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StandardConstructor { pub struct StandardConstructor {
@ -225,7 +231,7 @@ pub struct Context {
/// Cached iterator prototypes. /// Cached iterator prototypes.
iterator_prototypes: IteratorPrototypes, iterator_prototypes: IteratorPrototypes,
/// Cached standard objects and their prototypes /// Cached standard objects and their prototypes.
standard_objects: StandardObjects, standard_objects: StandardObjects,
} }
@ -700,6 +706,7 @@ impl Context {
/// assert!(value.is_number()); /// assert!(value.is_number());
/// assert_eq!(value.as_number().unwrap(), 4.0); /// assert_eq!(value.as_number().unwrap(), 4.0);
/// ``` /// ```
#[cfg(not(feature = "vm"))]
#[allow(clippy::unit_arg, clippy::drop_copy)] #[allow(clippy::unit_arg, clippy::drop_copy)]
#[inline] #[inline]
pub fn eval<T: AsRef<[u8]>>(&mut self, src: T) -> Result<Value> { pub fn eval<T: AsRef<[u8]>>(&mut self, src: T) -> Result<Value> {
@ -722,6 +729,48 @@ impl Context {
execution_result 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<T: AsRef<[u8]>>(&mut self, src: T) -> Result<Value> {
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. /// Returns a structure that contains the JavaScript well known symbols.
/// ///
/// # Examples /// # Examples

2
boa/src/lib.rs

@ -53,6 +53,8 @@ pub mod property;
pub mod realm; pub mod realm;
pub mod syntax; pub mod syntax;
pub mod value; pub mod value;
#[cfg(feature = "vm")]
pub mod vm;
pub mod context; pub mod context;

2
boa/src/profiler.rs

@ -11,8 +11,6 @@ use std::{
thread::{current, ThreadId}, thread::{current, ThreadId},
}; };
#[cfg(feature = "profiler")]
type SerializationSink = measureme::SerializationSink;
#[cfg(feature = "profiler")] #[cfg(feature = "profiler")]
pub struct BoaProfiler { pub struct BoaProfiler {
profiler: Profiler, profiler: Profiler,

56
boa/src/syntax/ast/node/operator/bin_op/mod.rs

@ -12,6 +12,12 @@ use std::fmt;
#[cfg(feature = "deser")] #[cfg(feature = "deser")]
use serde::{Deserialize, Serialize}; 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. /// Binary operators requires two operands, one before the operator and one after the operator.
/// ///
/// More information: /// 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 { impl fmt::Display for BinOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {}", self.lhs, self.op, self.rhs) write!(f, "{} {} {}", self.lhs, self.op, self.rhs)

27
boa/src/syntax/ast/node/operator/unary_op/mod.rs

@ -9,6 +9,12 @@ use std::fmt;
#[cfg(feature = "deser")] #[cfg(feature = "deser")]
use serde::{Deserialize, Serialize}; 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. /// A unary operation is an operation with only one operand.
/// ///
/// More information: /// More information:
@ -128,3 +134,24 @@ impl From<UnaryOp> for Node {
Self::UnaryOp(op) 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 => {}
}
}
}

14
boa/src/syntax/ast/node/statement_list/mod.rs

@ -11,6 +11,9 @@ use std::{fmt, ops::Deref, rc::Rc};
#[cfg(feature = "deser")] #[cfg(feature = "deser")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "vm")]
use crate::vm::{compilation::CodeGen, Compiler};
/// List of statements. /// List of statements.
/// ///
/// Similar to `Node::Block` but without the braces. /// 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<T> From<T> for StatementList impl<T> From<T> for StatementList
where where
T: Into<Box<[Node]>>, T: Into<Box<[Node]>>,

64
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<Instruction>,
pub(super) pool: Vec<Value>,
}
impl Compiler {
// Add a new instruction.
pub fn add_instruction(&mut self, instr: Instruction) {
self.instructions.push(instr);
}
pub fn add_string_instruction<S>(&mut self, string: S)
where
S: Into<RcString>,
{
let index = self.pool.len();
self.add_instruction(Instruction::String(index));
self.pool.push(string.into().into());
}
pub fn add_bigint_instruction<B>(&mut self, bigint: B)
where
B: Into<RcBigInt>,
{
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!(),
}
}
}

105
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"),
}
}
}

279
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<Instruction>,
pool: Vec<Value>,
stack: Vec<Value>,
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<Value> {
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)
}
}

3
boa_cli/Cargo.toml

@ -21,6 +21,9 @@ colored = "2.0.0"
regex = "1.4.2" regex = "1.4.2"
lazy_static = "1.4.0" lazy_static = "1.4.0"
[features]
vm = ["Boa/vm"]
[target.x86_64-unknown-linux-gnu.dependencies] [target.x86_64-unknown-linux-gnu.dependencies]
jemallocator = "0.3.2" jemallocator = "0.3.2"

Loading…
Cancel
Save