Skip to content
Snippets Groups Projects
machine.rs 9.35 KiB
Newer Older
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::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))
	}
}

Louis's avatar
Louis committed
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)
	}
}