diff --git a/Cargo.lock b/Cargo.lock index 9324f86111c273b9acd1687cc1f553f89a9585cb..0f0b70eb557d84243aafe4d8ebe940aea5d8a181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,9 @@ dependencies = [ [[package]] name = "forge-script" version = "0.1.0" +dependencies = [ + "forge-script-lang", +] [[package]] name = "forge-script-lang" diff --git a/forge-script-lang/src/runtime/mod.rs b/forge-script-lang/src/runtime/mod.rs index 20065f855b55e297e2c00c99d3135ef2dbea2cb3..aab2ebbc07bed0cb8fbaa886a22f9be8a76fa8a6 100644 --- a/forge-script-lang/src/runtime/mod.rs +++ b/forge-script-lang/src/runtime/mod.rs @@ -1,4 +1,4 @@ pub mod executor; pub mod numbers; pub mod value; -mod vm; +pub mod vm; diff --git a/forge-script-lang/src/runtime/value.rs b/forge-script-lang/src/runtime/value.rs index 39e80302675d8f438674f245c615257eb970fd05..f91cf6109c936338a64e4eec66ff5c1dc5c72b52 100644 --- a/forge-script-lang/src/runtime/value.rs +++ b/forge-script-lang/src/runtime/value.rs @@ -3,6 +3,7 @@ use crate::parser::ast::LiteralNode; use crate::runtime::numbers::Number; use std::fmt::{Display, Formatter}; use std::ops::{Add, Div, Mul, Not, Rem, Sub}; +use std::process::{ExitCode, Termination}; #[derive(Clone, Debug, Default)] #[cfg_attr( @@ -123,6 +124,12 @@ impl ForgeValue { } } +impl Termination for ForgeValue { + fn report(self) -> ExitCode { + ExitCode::from(0) + } +} + impl Display for ForgeValue { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -270,9 +277,9 @@ impl Add for ForgeValue { #[derive(Copy, Clone, Debug, PartialEq)] pub struct UnsupportedOperation { - operation: BinaryOp, - left_type: &'static str, - right_type: &'static str, + pub operation: BinaryOp, + pub left_type: &'static str, + pub right_type: &'static str, } impl UnsupportedOperation { diff --git a/forge-script-lang/src/runtime/vm/chunks.rs b/forge-script-lang/src/runtime/vm/chunks.rs index a16a061de8367a850d059f4c4dbb007d948126c3..e7d9d5290e985175cd240502216a91198b15c909 100644 --- a/forge-script-lang/src/runtime/vm/chunks.rs +++ b/forge-script-lang/src/runtime/vm/chunks.rs @@ -1,6 +1,7 @@ use crate::runtime::value::ForgeValue; use crate::runtime::vm::const_data::{ConstData, ConstDataRef}; use crate::runtime::vm::opcode::{OpCode, OpCodeError}; +use crate::utilities::Clown; use std::fmt::{Display, Formatter}; use std::ops::{Deref, DerefMut}; @@ -53,18 +54,32 @@ impl Chunk { } } - pub fn op_return(&mut self) { - self.push_op(OpCode::Return); - } - pub fn op_constant(&mut self, data: ForgeValue) { let idx = self.push_const_data(data); self.push_op(OpCode::Constant(idx)); } } +impl<'scope> Clown<Chunk> for ChunkRef<'scope> { + fn clown(&self) -> Chunk { + Chunk { + consts: self.consts.clown(), + code: Vec::from(self.code), + } + } +} + pub trait ChunkOps { fn as_slice(&self) -> &[OpCode]; + fn as_const_slice(&self) -> &[ForgeValue]; + + fn as_chunk(&self) -> Chunk { + ChunkRef { + code: self.as_slice(), + consts: self.as_const_slice().into(), + } + .clown() + } fn write_chunk_data(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut offset = 0; @@ -80,6 +95,14 @@ pub trait ChunkOps { write!(f, "[{:08x}] {} ", offset, opcode)?; opcode.format_operand(f)?; + + match opcode { + OpCode::Constant(idx) => { + write!(f, " {}", self.as_const_slice()[*idx])?; + } + _ => {} + } + writeln!(f, "")?; offset = opcode.next_offset(offset); @@ -103,6 +126,9 @@ impl ChunkOps for Chunk { fn as_slice(&self) -> &[OpCode] { self.code.as_slice() } + fn as_const_slice(&self) -> &[ForgeValue] { + self.consts.as_slice() + } chunk_ops_generic!(); } @@ -110,6 +136,10 @@ impl<'scope> ChunkOps for ChunkRef<'scope> { fn as_slice(&self) -> &[OpCode] { self.code } + fn as_const_slice(&self) -> &[ForgeValue] { + *self.consts + } + chunk_ops_generic!(); } @@ -141,10 +171,10 @@ mod _chunks { fn const_chunk() { let mut chunk = Chunk::default(); chunk.op_constant(ForgeValue::Null); - chunk.op_return(); + chunk.push_op(OpCode::Return); let expected = format!( - "== test ==\n[00000000] op_constant {:016x}\n[00000001] op_return \n", + "== test ==\n[00000000] op_constant {:016x} null\n[00000001] op_return \n", 0 ); diff --git a/forge-script-lang/src/runtime/vm/const_data.rs b/forge-script-lang/src/runtime/vm/const_data.rs index 94f770db9fe033a60ee74315d62688f4acc256d1..7bd3a1d0af077ea57a03fc4133758522172d53cd 100644 --- a/forge-script-lang/src/runtime/vm/const_data.rs +++ b/forge-script-lang/src/runtime/vm/const_data.rs @@ -1,10 +1,17 @@ use crate::runtime::value::ForgeValue; +use crate::utilities::Clown; use std::ops::{Deref, DerefMut}; #[derive(Default)] pub struct ConstData(Vec<ForgeValue>); pub struct ConstDataRef<'scope>(&'scope [ForgeValue]); +impl<'scope> Clown<ConstData> for ConstDataRef<'scope> { + fn clown(&self) -> ConstData { + ConstData(self.0.to_vec()) + } +} + impl From<Vec<ForgeValue>> for ConstData { fn from(value: Vec<ForgeValue>) -> Self { ConstData(value) diff --git a/forge-script-lang/src/runtime/vm/machine.rs b/forge-script-lang/src/runtime/vm/machine.rs new file mode 100644 index 0000000000000000000000000000000000000000..5159ba38466001a82663eb68f52a5a789f77086b --- /dev/null +++ b/forge-script-lang/src/runtime/vm/machine.rs @@ -0,0 +1,228 @@ +use crate::parser::ast::BinaryOp; +use crate::runtime::value::{ForgeValue, UnsupportedOperation}; +use crate::runtime::vm::chunks::Chunk; +use crate::runtime::vm::{ChunkOps, OpCode}; +use std::collections::VecDeque; +use std::fmt::{Display, Formatter}; + +const INITIAL_STACK: usize = 512; + +pub struct Forge { + ip: usize, + sp: usize, + chunk: Chunk, + value_stack: VecDeque<ForgeValue>, +} + +macro_rules! impl_binary_opcode { + ($vm: expr, $op: expr) => {{ + let (lhs, rhs) = $vm.pop_stack_binary_or_err()?; + $vm.push_stack($op(lhs, rhs)?); + }}; +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum RuntimeErrorKind { + MissingEof, + InvalidConstData, + InvalidStackData, + UnsupportedOperation(BinaryOp), +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum VmError { + RuntimeError(RuntimeErrorKind), + CompilerError, +} + +impl From<UnsupportedOperation> for VmError { + fn from(value: UnsupportedOperation) -> Self { + VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(value.operation)) + } +} + +impl Display for VmError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::RuntimeError(kind) => write!(f, "{:?}", kind), + Self::CompilerError => write!(f, "Compiler Error"), + } + } +} + +pub type VmResult = Result<ForgeValue, VmError>; + +impl Forge { + fn next(&mut self) -> Option<OpCode> { + let code = self.chunk.as_slice().get(self.ip); + self.ip = self.ip.saturating_add(1); + code.copied() + } + + /// Get a constant data value from the target chunk's constant section + fn get_const(&self, idx: usize) -> Option<&ForgeValue> { + self.chunk.as_const_slice().get(idx) + } + + /// Adds a new value to the end of the value stack and returns the new + /// stack length + fn push_stack(&mut self, value: ForgeValue) -> usize { + self.value_stack.push_back(value); + self.value_stack.len() + } + + /// Takes the value off of the end of the value stack and returns it, if + /// one exists + fn pop_stack(&mut self) -> Option<ForgeValue> { + self.value_stack.pop_back() + } + + /// Examine the value at the end of the value stack without modifying the + /// stack + fn peek_stack(&self) -> Option<&ForgeValue> { + self.value_stack.back() + } + + fn pop_stack_or_err(&mut self) -> VmResult { + self.pop_stack() + .ok_or(VmError::RuntimeError(RuntimeErrorKind::InvalidStackData)) + } + + fn pop_stack_binary_or_err(&mut self) -> Result<(ForgeValue, ForgeValue), VmError> { + let first = match self.pop_stack() { + Some(value) => value, + None => return Err(VmError::RuntimeError(RuntimeErrorKind::InvalidStackData)), + }; + + let second = match self.pop_stack() { + Some(value) => value, + None => { + self.push_stack(first); + return Err(VmError::RuntimeError(RuntimeErrorKind::InvalidStackData)); + } + }; + + Ok((second, first)) + } + + /// Execute a chunk of code in a new VM instance + pub fn exec(chunk: impl ChunkOps) -> VmResult { + let mut vm = Forge::from_chunk(chunk.as_chunk()); + vm.run() + } + + /// Create a new VM instance with the given chunk data. No execution is + /// performed + pub fn from_chunk(chunk: Chunk) -> Self { + Forge { + chunk, + ip: 0, + sp: 0, + value_stack: VecDeque::with_capacity(INITIAL_STACK), + } + } + + pub fn run(&mut self) -> VmResult { + while let Some(opcode) = self.next() { + match opcode { + OpCode::Return => return Ok(self.peek_stack().cloned().unwrap_or_default()), + OpCode::Constant(idx) => { + if let Some(value) = self.get_const(idx) { + println!("Load Const {:?}", &value); + self.push_stack(value.clone()); + } else { + return Err(VmError::RuntimeError(RuntimeErrorKind::InvalidConstData)); + } + } + OpCode::Invert => { + if let Some(value) = self.pop_stack() { + self.push_stack(value.invert()); + } + } + OpCode::Add => { + let (lhs, rhs) = self.pop_stack_binary_or_err()?; + self.push_stack(lhs + rhs); + } + OpCode::Multiply => impl_binary_opcode!(self, std::ops::Mul::mul), + OpCode::Divide => impl_binary_opcode!(self, std::ops::Div::div), + OpCode::Subtract => impl_binary_opcode!(self, std::ops::Sub::sub), + } + } + + Err(VmError::RuntimeError(RuntimeErrorKind::MissingEof)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime::value::ForgeValue; + use crate::runtime::vm::{Chunk, Forge, OpCode, VmResult}; + use test_case::test_case; + + #[test] + fn op_negate() { + let mut chunk = Chunk::default(); + chunk.op_constant(ForgeValue::from(100)); + chunk.push_op(OpCode::Invert); + chunk.push_op(OpCode::Return); + + assert_eq!(Forge::exec(chunk), Ok(ForgeValue::from(-100))); + } + + #[test_case(ForgeValue::from(100), ForgeValue::from(100) => Ok(ForgeValue::from(200)) ; "Adding Numbers")] + #[test_case(ForgeValue::from("foo "), ForgeValue::from("bar") => Ok(ForgeValue::from("foo bar")) ; "Adding strings")] + #[test_case(ForgeValue::from("foo "), ForgeValue::from(100) => Ok(ForgeValue::from("foo 100")) ; "Adding string to number")] + #[test_case(ForgeValue::from(true), ForgeValue::from(false) => Ok(ForgeValue::from(false)) ; "Adding bools")] + fn op_add(lhs: ForgeValue, rhs: ForgeValue) -> VmResult { + let mut chunk = Chunk::default(); + chunk.op_constant(lhs); + chunk.op_constant(rhs); + chunk.push_op(OpCode::Add); + chunk.push_op(OpCode::Return); + + Forge::exec(chunk) + } + + #[test_case(ForgeValue::from(100), ForgeValue::from(100) => Ok(ForgeValue::from(0)) ; "Subtract Numbers")] + #[test_case(ForgeValue::from("foo "), ForgeValue::from("bar") => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Subtract))) ; "Subtract strings")] + #[test_case(ForgeValue::from("foo "), ForgeValue::from(100) => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Subtract))) ; "Subtract string to number")] + #[test_case(ForgeValue::from(true), ForgeValue::from(false) => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Subtract))) ; "Subtract bools")] + fn op_sub(lhs: ForgeValue, rhs: ForgeValue) -> VmResult { + let mut chunk = Chunk::default(); + chunk.op_constant(lhs); + chunk.op_constant(rhs); + chunk.push_op(OpCode::Subtract); + chunk.push_op(OpCode::Return); + + Forge::exec(chunk) + } + + #[test_case(ForgeValue::from(100), ForgeValue::from(100) => Ok(ForgeValue::from(1_00_00)) ; "Multiply Numbers")] + #[test_case(ForgeValue::from("foo "), ForgeValue::from("bar") => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Multiply))) ; "Multiply strings")] + #[test_case(ForgeValue::from("foo "), ForgeValue::from(100) => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Multiply))) ; "Multiply string to number")] + #[test_case(ForgeValue::from(true), ForgeValue::from(false) => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Multiply))) ; "Multiply bools")] + fn op_mul(lhs: ForgeValue, rhs: ForgeValue) -> VmResult { + let mut chunk = Chunk::default(); + chunk.op_constant(lhs); + chunk.op_constant(rhs); + chunk.push_op(OpCode::Multiply); + chunk.push_op(OpCode::Return); + + Forge::exec(chunk) + } + + #[test_case(ForgeValue::from(100), ForgeValue::from(100) => Ok(ForgeValue::from(1)) ; "Divide Numbers")] + #[test_case(ForgeValue::from("foo "), ForgeValue::from("bar") => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Divide))) ; "Divide strings")] + #[test_case(ForgeValue::from("foo "), ForgeValue::from(100) => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Divide))) ; "Divide string to number")] + #[test_case(ForgeValue::from(true), ForgeValue::from(false) => Err(VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(BinaryOp::Divide))) ; "Divide bools")] + fn op_div(lhs: ForgeValue, rhs: ForgeValue) -> VmResult { + let mut chunk = Chunk::default(); + chunk.op_constant(lhs); + chunk.op_constant(rhs); + chunk.push_op(OpCode::Divide); + chunk.push_op(OpCode::Return); + + Forge::exec(chunk) + } +} diff --git a/forge-script-lang/src/runtime/vm/mod.rs b/forge-script-lang/src/runtime/vm/mod.rs index bd30ae0c30f8d255caad3655e842ed2c56ae91b2..d1d1b4336caa5632865ccc4c73061803c3cda1c3 100644 --- a/forge-script-lang/src/runtime/vm/mod.rs +++ b/forge-script-lang/src/runtime/vm/mod.rs @@ -1,3 +1,9 @@ mod chunks; mod const_data; +mod machine; mod opcode; + +pub use chunks::{Chunk, ChunkOps, ChunkRef}; +pub use const_data::{ConstData, ConstDataRef}; +pub use machine::{Forge, VmError, VmResult}; +pub use opcode::{OpCode, OpCodeError}; diff --git a/forge-script-lang/src/runtime/vm/opcode.rs b/forge-script-lang/src/runtime/vm/opcode.rs index 9809dd3efe2c1ce3a3bf64056182f8d2c816371f..9b300dd9b7f4d5adc63fedbd567e4f4b5cbb3c0c 100644 --- a/forge-script-lang/src/runtime/vm/opcode.rs +++ b/forge-script-lang/src/runtime/vm/opcode.rs @@ -6,6 +6,11 @@ pub enum OpCode { Return, /// Load a constant value from the chunk's data pool. Data starts at the included offset Constant(usize), + Invert, + Add, + Multiply, + Divide, + Subtract, } impl OpCode { @@ -19,13 +24,18 @@ impl OpCode { match self { OpCode::Return => 1, OpCode::Constant(_) => 2, + OpCode::Invert => 3, + OpCode::Add => 4, + OpCode::Multiply => 5, + OpCode::Divide => 6, + OpCode::Subtract => 7, } } pub fn operand_bytes(&self) -> Vec<u8> { match self { - OpCode::Return => vec![], OpCode::Constant(value) => value.to_le_bytes().to_vec(), + _ => vec![], } } @@ -61,6 +71,11 @@ impl Display for OpCode { match self { OpCode::Return => "op_return", OpCode::Constant(_) => "op_constant", + OpCode::Invert => "op_negate", + OpCode::Add => "op_add", + OpCode::Multiply => "op_multiply", + OpCode::Divide => "op_divide", + OpCode::Subtract => "op_subtract", } ) } diff --git a/forge-script-lang/src/utilities.rs b/forge-script-lang/src/utilities.rs index a354bfcaf1ae5ad8e049d13dfcd743ab014ebb4c..6c5c1c25a627fa1e7e62ff9723d5be3a658cf609 100644 --- a/forge-script-lang/src/utilities.rs +++ b/forge-script-lang/src/utilities.rs @@ -35,3 +35,10 @@ macro_rules! deref_as { } }; } + +/// "Clone-owned", clone a struct into an owned version of that struct, which will typically be +/// a different type to the implementor. If the struct is already an owned struct then the impl +/// _should_ perform a simple clone, though the impl may choose to perform otherwise +pub trait Clown<Target> { + fn clown(&self) -> Target; +} diff --git a/forge-script/Cargo.toml b/forge-script/Cargo.toml index cbea55cdc1b79ae6d5ee2b05e23a1d87b7a86fe2..4212c0f9803ed9cac146a26e1899017c22cfaa01 100644 --- a/forge-script/Cargo.toml +++ b/forge-script/Cargo.toml @@ -10,3 +10,4 @@ authors = [ repository = "https://lab.lcr.gr/microhacks/forge-script.git" [dependencies] +forge-script-lang = { path = "../forge-script-lang" } \ No newline at end of file diff --git a/forge-script/src/main.rs b/forge-script/src/main.rs index a30eb952c15de83ff8946bb604d4f215fc87e151..ca6c7356087a842bcc27dccbe3555d089cca19c5 100644 --- a/forge-script/src/main.rs +++ b/forge-script/src/main.rs @@ -1,3 +1,21 @@ -fn main() { - println!("Hello, world!"); +use crate::repl::Repl; +use forge_script_lang::runtime::value::ForgeValue; +use forge_script_lang::runtime::vm::{Chunk, ChunkOps, Forge, OpCode, VmResult}; + +mod repl; + +fn main() -> VmResult { + let mut repl = Repl::new(); + repl.run(); + + Ok(ForgeValue::Null) + + // let mut chunk = Chunk::default(); + // chunk.op_constant(ForgeValue::String(String::from("foo"))); + // chunk.push_op(OpCode::Invert); + // chunk.push_op(OpCode::Return); + // + // let value = Forge::exec(chunk.as_ref()); + // println!("{:?}", &value); + // value } diff --git a/forge-script/src/repl.rs b/forge-script/src/repl.rs new file mode 100644 index 0000000000000000000000000000000000000000..e23a11c4ab0102a6bc2d35fc8b0264c7f8253cc5 --- /dev/null +++ b/forge-script/src/repl.rs @@ -0,0 +1,73 @@ +use forge_script_lang::runtime::vm::{Chunk, Forge}; +use std::io::Write; +use std::num::ParseIntError; + +pub struct Repl { + buffer: Vec<String>, + vm: Forge, +} + +fn parse_hex(string: &str) -> Result<Vec<u8>, ParseIntError> { + let parts = string.split(" ").collect::<Vec<&str>>(); + let mut out = Vec::with_capacity(parts.len()); + for part in parts { + out.push(u8::from_str_radix(part, 16)?); + } + Ok(out) +} + +impl Repl { + pub fn new() -> Self { + Repl { + buffer: Vec::new(), + vm: Forge::from_chunk(Chunk::default()), + } + } + + pub fn run(&mut self) { + println!("\n] =========== ["); + println!("] Forgescript ["); + println!("] =========== ["); + println!("\nRepl v{}\n", env!("CARGO_PKG_VERSION")); + + 'repl_loop: loop { + print!("> "); + match std::io::stdout().flush() { + Ok(_) => {} + Err(e) => { + eprintln!("{}", e); + break 'repl_loop; + } + } + + let mut buffer = String::new(); + let stdin = std::io::stdin(); + + match stdin.read_line(&mut buffer) { + Ok(_) => {} + Err(e) => { + eprintln!("\n{}", e); + break 'repl_loop; + } + } + + let buffer = buffer.trim(); + match buffer { + ".quit" => { + break 'repl_loop; + } + ".history" => { + println!("\n== History =="); + for line in self.buffer.iter() { + println!("{}", line); + } + println!("=============\n"); + } + other => { + println!("{}", other); + self.buffer.push(String::from(other)); + } + } + } + } +}