use crate::parser::ast::BinaryOp; use crate::parser::parse_program; use crate::runtime::value::{ForgeValue, UnsupportedOperation}; use crate::runtime::vm::chunk_builder::ChunkBuilder; use crate::runtime::vm::chunks::Chunk; use crate::runtime::vm::{ChunkOps, OpCode}; use crate::{format_forge_error, ForgeErrorKind}; use std::collections::VecDeque; use std::error::Error; 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(Clone, Eq, PartialEq, Debug)] pub enum CompilerErrorKind { AstParser(String), Chunkbuilder(String), } #[derive(Clone, Eq, PartialEq, Debug)] pub enum VmError { RuntimeError(RuntimeErrorKind), CompilerError(CompilerErrorKind), } impl VmError { pub fn ast_parser(reason: impl ToString) -> VmError { VmError::CompilerError(CompilerErrorKind::AstParser(reason.to_string())) } pub fn chunk_parser(reason: impl ToString) -> VmError { VmError::CompilerError(CompilerErrorKind::Chunkbuilder(reason.to_string())) } } impl From<UnsupportedOperation> for VmError { fn from(value: UnsupportedOperation) -> Self { VmError::RuntimeError(RuntimeErrorKind::UnsupportedOperation(value.operation)) } } impl From<String> for VmError { fn from(value: String) -> Self { VmError::CompilerError(CompilerErrorKind::Chunkbuilder(value)) } } impl Display for VmError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::RuntimeError(kind) => write!(f, "[Runtime Error] {}", kind), Self::CompilerError(kind) => write!(f, "[Compiler Error] {}", kind), } } } impl Display for RuntimeErrorKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { RuntimeErrorKind::MissingEof => write!(f, "Unexpected end of file"), RuntimeErrorKind::InvalidConstData => { write!(f, "Tried to access invalid constant data") } RuntimeErrorKind::InvalidStackData => write!(f, "Tried to access invalid stack data"), RuntimeErrorKind::UnsupportedOperation(op) => write!(f, "Unsupported op: {:?}", op), } } } impl Display for CompilerErrorKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { CompilerErrorKind::AstParser(err) => err.as_str(), CompilerErrorKind::Chunkbuilder(err) => err.as_str(), } ) } } impl Error for VmError {} 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)) } pub fn compile(value: impl ToString) -> Result<Chunk, VmError> { let value = value.to_string(); ChunkBuilder::parse(value.as_str()) } pub fn compile_and_run(value: impl ToString) -> VmResult { let chunk = Forge::compile(value)?; Forge::exec(chunk) } /// 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) { 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) } }