From a9de8dbf81e8dbc08ee46e3a0fc05f5812e95c1c Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Tue, 16 May 2023 17:23:02 +0100
Subject: [PATCH] Visitors need return and error types, start basic expressions
 in simple interpreter

---
 forge-script-lang/src/parser/ast.rs           |   6 +-
 forge-script-lang/src/parser/grammar.rs       |   8 +-
 forge-script-lang/src/parser/test_suite.rs    |   4 +
 forge-script-lang/src/runtime/executor/mod.rs |  22 +-
 .../src/runtime/executor/printer.rs           |  58 +++-
 .../src/runtime/executor/simple.rs            | 157 +++++++++
 forge-script-lang/src/runtime/value.rs        | 303 ++++++++++++++++++
 7 files changed, 534 insertions(+), 24 deletions(-)
 create mode 100644 forge-script-lang/src/runtime/executor/simple.rs

diff --git a/forge-script-lang/src/parser/ast.rs b/forge-script-lang/src/parser/ast.rs
index 644913e..131a146 100644
--- a/forge-script-lang/src/parser/ast.rs
+++ b/forge-script-lang/src/parser/ast.rs
@@ -64,8 +64,7 @@ pub enum Expression {
 	Void(VoidExpression),
 }
 
-#[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[derive(Clone, Eq, Copy, PartialEq, Debug)]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
@@ -89,8 +88,7 @@ impl Display for UnaryOp {
 	}
 }
 
-#[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[derive(Clone, Eq, Copy, PartialEq, Debug)]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
diff --git a/forge-script-lang/src/parser/grammar.rs b/forge-script-lang/src/parser/grammar.rs
index 59fba41..1f1a12d 100644
--- a/forge-script-lang/src/parser/grammar.rs
+++ b/forge-script-lang/src/parser/grammar.rs
@@ -30,8 +30,7 @@ peg::parser! {
 
 		 #[cache_left_rec]
 		rule value_expression() -> ValueExpression
-			= "(" ex:value_expression() ")" { ValueExpression::Grouped(GroupedExpression { inner: Box::new(ex) }) }
-			/ co:conditional() { ValueExpression::ConditionalBlock(co) }
+			= co:conditional() { ValueExpression::ConditionalBlock(co) }
 			/ decl:declare_variable() { ValueExpression::DeclareIdentifier(decl) }
 			/ decl:declare_function() { ValueExpression::DeclareFunction(decl) }
 			/ name:bare_identifier() "(" params:param_list()? ")"
@@ -40,9 +39,14 @@ peg::parser! {
 				{ ValueExpression::Binary { lhs: Box::new(left), rhs: Box::new(right), operator: op } }
 			/ op:unary_operator() operand:value_expression()
 				{ ValueExpression::Unary { operator: op, operand: Box::new(operand) } }
+			/ grouped()
 			/ ident:bare_identifier() { ValueExpression::Identifier(ident) }
 			/ li:literal() { ValueExpression::Literal(li) }
 
+		rule grouped() -> ValueExpression
+			= "(" ex:value_expression() ")"
+				{ ValueExpression::Grouped(GroupedExpression { inner: Box::new(ex) }) }
+
 		rule print() -> Print
 			= "print" ex:value_expression() { ex.into() }
 
diff --git a/forge-script-lang/src/parser/test_suite.rs b/forge-script-lang/src/parser/test_suite.rs
index 0b00afa..fc217e2 100644
--- a/forge-script-lang/src/parser/test_suite.rs
+++ b/forge-script-lang/src/parser/test_suite.rs
@@ -3,6 +3,10 @@ use crate::parse::parse_program;
 #[test]
 fn binary_ops() {
 	parse_program("1+1").expect("Failed binary add");
+	parse_program("1 + 2 + 3 + 4").expect("Failed binary add");
+	parse_program("1 + (2 + 3) + 4").expect("Failed binary add");
+	parse_program("(1 + 2) + 3 + 4").expect("Failed binary add");
+
 	parse_program("1-1").expect("Failed binary sub");
 	parse_program("1*1").expect("Failed binary mul");
 	parse_program("1/1").expect("Failed binary div");
diff --git a/forge-script-lang/src/runtime/executor/mod.rs b/forge-script-lang/src/runtime/executor/mod.rs
index ab3ef43..7b0bb30 100644
--- a/forge-script-lang/src/runtime/executor/mod.rs
+++ b/forge-script-lang/src/runtime/executor/mod.rs
@@ -1,17 +1,31 @@
 mod printer;
+mod simple;
 
 use crate::parser::ast::{Expression, Program, ValueExpression, VoidExpression};
+use std::error::Error;
 
 pub trait Visitor {
-	fn evaluate_value_expression(&mut self, expression: &ValueExpression);
-	fn evaluate_void_expression(&mut self, expression: &VoidExpression);
-	fn evaluate_expression(&mut self, expression: &Expression) {
+	type Output;
+	type Error: Error;
+
+	fn evaluate_value_expression(
+		&mut self,
+		expression: &ValueExpression,
+	) -> Result<Self::Output, Self::Error>;
+	fn evaluate_void_expression(
+		&mut self,
+		expression: &VoidExpression,
+	) -> Result<Self::Output, Self::Error>;
+	fn evaluate_expression(
+		&mut self,
+		expression: &Expression,
+	) -> Result<Self::Output, Self::Error> {
 		match expression {
 			Expression::Value(expr) => self.evaluate_value_expression(expr),
 			Expression::Void(expr) => self.evaluate_void_expression(expr),
 		}
 	}
-	fn evaluate_program(&mut self, program: &Program);
+	fn evaluate_program(&mut self, program: &Program) -> Result<Self::Output, Self::Error>;
 }
 
 pub use printer::TreePrinter;
diff --git a/forge-script-lang/src/runtime/executor/printer.rs b/forge-script-lang/src/runtime/executor/printer.rs
index 4f95e61..31b5b3b 100644
--- a/forge-script-lang/src/runtime/executor/printer.rs
+++ b/forge-script-lang/src/runtime/executor/printer.rs
@@ -1,9 +1,12 @@
 use crate::parser::ast::*;
 use crate::runtime::executor::Visitor;
+use std::error::Error;
+use std::fmt::{Display, Formatter};
 
 pub struct TreePrinter {
 	indent: usize,
 	buffer: String,
+	should_indent: bool,
 }
 
 impl TreePrinter {
@@ -11,6 +14,15 @@ impl TreePrinter {
 		Self {
 			indent: 0,
 			buffer: String::new(),
+			should_indent: true,
+		}
+	}
+
+	pub fn with_indent(indent: usize) -> Self {
+		Self {
+			indent,
+			buffer: String::new(),
+			should_indent: true,
 		}
 	}
 
@@ -27,17 +39,25 @@ impl TreePrinter {
 	}
 
 	fn write(&mut self, value: impl ToString) {
+		if self.should_indent {
+			self.should_indent = false;
+			self.write_indent();
+		}
 		self.buffer.push_str(value.to_string().as_str());
 	}
+
 	fn writeln(&mut self, value: impl ToString) {
-		self.buffer.push_str(value.to_string().as_str());
-		self.buffer.push('\n');
+		self.write(value);
+		self.new_line();
 	}
+
 	fn write_indent(&mut self) {
 		self.write(self.get_indent());
 	}
+
 	fn new_line(&mut self) {
 		self.buffer.push('\n');
+		self.should_indent = true;
 	}
 
 	fn format_expression_list(&self, list: &ExpressionList) -> String {
@@ -72,8 +92,20 @@ impl TreePrinter {
 	}
 }
 
+#[derive(Debug, Clone, Copy)]
+pub struct TreeError;
+impl Display for TreeError {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		f.write_str("Tree Error")
+	}
+}
+impl Error for TreeError {}
+
 impl Visitor for TreePrinter {
-	fn evaluate_value_expression(&mut self, expression: &ValueExpression) {
+	type Output = ();
+	type Error = TreeError;
+
+	fn evaluate_value_expression(&mut self, expression: &ValueExpression) -> Result<(), TreeError> {
 		match expression {
 			ValueExpression::Unary { operand, operator } => {
 				self.write(operator);
@@ -150,10 +182,7 @@ impl Visitor for TreePrinter {
 					.params
 					.iter()
 					.map(|param| {
-						let mut inner = TreePrinter {
-							indent: self.indent,
-							buffer: String::new(),
-						};
+						let mut inner = TreePrinter::with_indent(self.indent);
 						inner.evaluate_value_expression(param);
 						inner.take_value()
 					})
@@ -169,11 +198,7 @@ impl Visitor for TreePrinter {
 					.params
 					.iter()
 					.map(|param| {
-						let mut inner = TreePrinter {
-							indent: self.indent,
-							buffer: String::new(),
-						};
-
+						let mut inner = TreePrinter::with_indent(self.indent);
 						match param {
 							DeclareIdent::WithValue(assign) => {
 								inner.write(&assign.ident);
@@ -195,9 +220,11 @@ impl Visitor for TreePrinter {
 				self.writeln("}");
 			}
 		}
+
+		Ok(())
 	}
 
-	fn evaluate_void_expression(&mut self, expression: &VoidExpression) {
+	fn evaluate_void_expression(&mut self, expression: &VoidExpression) -> Result<(), TreeError> {
 		match expression {
 			VoidExpression::ConditionLoop(cond) => {
 				self.write("while ");
@@ -237,9 +264,12 @@ impl Visitor for TreePrinter {
 				self.evaluate_value_expression(print.expr.as_ref());
 			}
 		}
+
+		Ok(())
 	}
 
-	fn evaluate_program(&mut self, program: &Program) {
+	fn evaluate_program(&mut self, program: &Program) -> Result<(), TreeError> {
 		self.writeln(self.format_expression_list(&program.0));
+		Ok(())
 	}
 }
diff --git a/forge-script-lang/src/runtime/executor/simple.rs b/forge-script-lang/src/runtime/executor/simple.rs
new file mode 100644
index 0000000..6dbd2c3
--- /dev/null
+++ b/forge-script-lang/src/runtime/executor/simple.rs
@@ -0,0 +1,157 @@
+use crate::parse::ast::{
+	BinaryOp, DeclareFunction, ExpressionList, Program, UnaryOp, ValueExpression, VoidExpression,
+};
+use crate::runtime::executor::Visitor;
+use crate::runtime::value::{ForgeValue, UnsupportedOperation};
+use std::collections::HashMap;
+use std::error::Error;
+use std::fmt::{Debug, Display, Formatter};
+
+#[derive(Clone, Default)]
+pub struct SimpleExecutor {
+	data: HashMap<String, ForgeValue>,
+	vtable: HashMap<String, DeclareFunction>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum RuntimeError {
+	Unsupported(&'static str),
+	BadOperands(UnsupportedOperation),
+}
+
+impl From<UnsupportedOperation> for RuntimeError {
+	fn from(value: UnsupportedOperation) -> Self {
+		Self::BadOperands(value)
+	}
+}
+
+impl Display for RuntimeError {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		match self {
+			RuntimeError::Unsupported(expr) => {
+				write!(
+					f,
+					"[Runtime] Encountered an unsupported expression: {}",
+					expr
+				)
+			}
+			RuntimeError::BadOperands(unsupported) => write!(f, "[Runtime] {}", unsupported),
+		}
+	}
+}
+
+impl Error for RuntimeError {}
+
+impl SimpleExecutor {
+	fn evaluate_expression_list(
+		&mut self,
+		list: &ExpressionList,
+	) -> Result<ForgeValue, RuntimeError> {
+		let mut last_val = Ok(ForgeValue::Null);
+		for expr in &list.expressions {
+			last_val = Ok(self.evaluate_expression(expr)?);
+		}
+		if list.is_void {
+			Ok(ForgeValue::Null)
+		} else {
+			last_val
+		}
+	}
+}
+
+impl Visitor for SimpleExecutor {
+	type Output = ForgeValue;
+	type Error = RuntimeError;
+
+	fn evaluate_value_expression(
+		&mut self,
+		expression: &ValueExpression,
+	) -> Result<Self::Output, Self::Error> {
+		match expression {
+			ValueExpression::Unary { operator, operand } => {
+				let value = self.evaluate_value_expression(operand.as_ref())?;
+				match operator {
+					UnaryOp::Negate => Ok(value.invert()),
+					UnaryOp::Not => Ok(!value),
+				}
+			}
+			ValueExpression::Binary { operator, lhs, rhs } => {
+				let lhs = self.evaluate_value_expression(lhs.as_ref())?;
+				let rhs = self.evaluate_value_expression(rhs.as_ref())?;
+
+				match operator {
+					BinaryOp::Add => Ok(lhs + rhs),
+					BinaryOp::Subtract => Ok((lhs - rhs)?),
+					BinaryOp::Divide => Ok((lhs / rhs)?),
+					BinaryOp::Multiply => Ok((lhs * rhs)?),
+					BinaryOp::Modulo => Ok((lhs % rhs)?),
+					BinaryOp::Equals => Ok((lhs == rhs).into()),
+				}
+			}
+			ValueExpression::Grouped(group) => self.evaluate_value_expression(group.inner.as_ref()),
+			ValueExpression::Block(_) => Err(RuntimeError::Unsupported("Block")),
+			ValueExpression::Literal(lit) => Ok(ForgeValue::from(lit.clone())),
+			ValueExpression::DeclareIdentifier(_) => {
+				Err(RuntimeError::Unsupported("DeclareIdentifier"))
+			}
+			ValueExpression::Assignment(_) => Err(RuntimeError::Unsupported("Assignment")),
+			ValueExpression::ConditionalBlock(_) => {
+				Err(RuntimeError::Unsupported("ConditionalBlock"))
+			}
+			ValueExpression::Identifier(_) => Err(RuntimeError::Unsupported("Identifier")),
+			ValueExpression::FunctionCall(_) => Err(RuntimeError::Unsupported("FunctionCall")),
+			ValueExpression::DeclareFunction(_) => {
+				Err(RuntimeError::Unsupported("DeclareFunction"))
+			}
+		}
+	}
+
+	fn evaluate_void_expression(
+		&mut self,
+		expression: &VoidExpression,
+	) -> Result<Self::Output, Self::Error> {
+		Err(RuntimeError::Unsupported("Void Expression"))
+	}
+
+	fn evaluate_program(&mut self, program: &Program) -> Result<Self::Output, Self::Error> {
+		self.evaluate_expression_list(&program.0)
+	}
+}
+
+#[cfg(test)]
+mod interpreter_test {
+	use crate::parse::parse_program;
+	use crate::runtime::executor::simple::{RuntimeError, SimpleExecutor};
+	use crate::runtime::executor::Visitor;
+	use crate::runtime::value::ForgeValue;
+
+	#[test]
+	fn the_basics() {
+		let add_numbers = parse_program("1 + 1").expect("Failed to parse");
+		let add_strings = parse_program("\"foo\" + \" \" + \"bar\"").expect("Failed to parse");
+		let concat_string_num = parse_program("\"#\" + 1").expect("Failed to parse");
+		let bad_mult = parse_program("false * 123").expect("Failed to parse");
+		let mut vm = SimpleExecutor::default();
+
+		assert_eq!(vm.evaluate_program(&add_numbers), Ok(ForgeValue::from(2)));
+		assert_eq!(
+			vm.evaluate_program(&add_strings),
+			Ok(ForgeValue::from("foo bar"))
+		);
+		assert_eq!(
+			vm.evaluate_program(&concat_string_num),
+			Ok(ForgeValue::from("#1"))
+		);
+		assert!(matches!(
+			vm.evaluate_program(&bad_mult),
+			Err(RuntimeError::BadOperands(_))
+		))
+	}
+
+	#[test]
+	fn combos() {
+		let add_numbers = parse_program("1 + -1").expect("Failed to parse");
+		let mut vm = SimpleExecutor::default();
+		assert_eq!(vm.evaluate_program(&add_numbers), Ok(ForgeValue::from(0)));
+	}
+}
diff --git a/forge-script-lang/src/runtime/value.rs b/forge-script-lang/src/runtime/value.rs
index 089a07f..1078730 100644
--- a/forge-script-lang/src/runtime/value.rs
+++ b/forge-script-lang/src/runtime/value.rs
@@ -1,6 +1,8 @@
+use crate::parse::ast::BinaryOp;
 use crate::parser::ast::LiteralNode;
 use crate::runtime::numbers::Number;
 use std::fmt::{Display, Formatter};
+use std::ops::{Add, Div, Mul, Not, Rem, Sub};
 
 #[derive(Clone, Debug)]
 #[cfg_attr(
@@ -25,6 +27,20 @@ pub enum ForgeValue {
 }
 
 impl ForgeValue {
+	pub fn null() -> Self {
+		ForgeValue::Null
+	}
+
+	pub fn invert(&self) -> Self {
+		match self {
+			ForgeValue::Number(num) => match num {
+				Number::Integer(val) => Number::Integer(-*val).into(),
+				Number::Float(val) => Number::Float(-*val).into(),
+			},
+			val => val.clone(),
+		}
+	}
+
 	/// Perform type coercion to force this value into a bool
 	///
 	/// ## True
@@ -140,3 +156,290 @@ impl From<LiteralNode> for ForgeValue {
 		}
 	}
 }
+
+impl From<String> for ForgeValue {
+	fn from(value: String) -> Self {
+		ForgeValue::String(value)
+	}
+}
+impl<'a> From<&'a str> for ForgeValue {
+	fn from(value: &'a str) -> Self {
+		ForgeValue::String(String::from(value))
+	}
+}
+
+impl From<bool> for ForgeValue {
+	fn from(value: bool) -> Self {
+		ForgeValue::Boolean(value)
+	}
+}
+
+impl From<Number> for ForgeValue {
+	fn from(value: Number) -> Self {
+		ForgeValue::Number(value)
+	}
+}
+
+macro_rules! from_number {
+    ($($typ:ty),+) => {
+		$(
+		impl From<$typ> for ForgeValue {
+			fn from(value: $typ) -> Self {
+				ForgeValue::Number(Number::from(value))
+			}
+		}
+		)+
+	};
+}
+
+from_number!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64);
+
+impl PartialEq for ForgeValue {
+	fn eq(&self, other: &Self) -> bool {
+		match (self, other) {
+			(ForgeValue::String(s1), ForgeValue::String(s2)) => s1.eq(s2),
+			(ForgeValue::Number(n1), ForgeValue::Number(n2)) => n1.eq(n2),
+			(ForgeValue::Boolean(b1), ForgeValue::Boolean(b2)) => b1.eq(b2),
+			(ForgeValue::Null, ForgeValue::Null) => true,
+			_ => false,
+		}
+	}
+}
+
+impl Add for ForgeValue {
+	type Output = ForgeValue;
+
+	fn add(self, rhs: Self) -> Self::Output {
+		match (self, rhs) {
+			(ForgeValue::String(s1), ForgeValue::String(s2)) => ForgeValue::String({
+				let mut buffer = String::with_capacity(s1.len() + s2.len());
+				buffer.push_str(s1.as_str());
+				buffer.push_str(s2.as_str());
+				buffer
+			}),
+			(ForgeValue::Number(n1), ForgeValue::Number(n2)) => ForgeValue::Number(n1 + n2),
+			(ForgeValue::String(val1), ForgeValue::Number(val2)) => {
+				ForgeValue::String(format!("{}{}", val1, val2))
+			}
+
+			(ForgeValue::Number(val1), ForgeValue::String(val2)) => {
+				ForgeValue::String(format!("{}{}", val1, val2))
+			}
+
+			(ForgeValue::List(first), ForgeValue::List(second)) => {
+				ForgeValue::List(first.iter().chain(second.iter()).cloned().collect())
+			}
+
+			(other_value, ForgeValue::List(mut list))
+			| (ForgeValue::List(mut list), other_value) => {
+				list.push(other_value);
+				ForgeValue::List(list)
+			}
+
+			// Boolean && null values are ignored
+			(ForgeValue::Number(val), ForgeValue::Null)
+			| (ForgeValue::Number(val), ForgeValue::Boolean(_))
+			| (ForgeValue::Null, ForgeValue::Number(val))
+			| (ForgeValue::Boolean(_), ForgeValue::Number(val)) => ForgeValue::Number(val),
+
+			(ForgeValue::Boolean(val), ForgeValue::Null)
+			| (ForgeValue::Null, ForgeValue::Boolean(val)) => ForgeValue::Boolean(val),
+
+			(ForgeValue::Null, ForgeValue::String(val))
+			| (ForgeValue::Boolean(_), ForgeValue::String(val))
+			| (ForgeValue::String(val), ForgeValue::Null)
+			| (ForgeValue::String(val), ForgeValue::Boolean(_)) => ForgeValue::String(val),
+
+			(ForgeValue::Null, ForgeValue::Null) => ForgeValue::Null,
+
+			(ForgeValue::Boolean(b1), ForgeValue::Boolean(b2)) => ForgeValue::Boolean(b1 && b2),
+		}
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct UnsupportedOperation {
+	operation: BinaryOp,
+	left_type: &'static str,
+	right_type: &'static str,
+}
+
+impl Display for UnsupportedOperation {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		write!(
+			f,
+			"Cannot perform {} with lhs type {} and rhs type {}",
+			self.operation, self.left_type, self.right_type
+		)
+	}
+}
+
+impl Sub for ForgeValue {
+	type Output = Result<ForgeValue, UnsupportedOperation>;
+	fn sub(self, rhs: Self) -> Self::Output {
+		match (self, rhs) {
+			(ForgeValue::Number(n1), ForgeValue::Number(n2)) => Ok(ForgeValue::Number(n1 - n2)),
+			(ForgeValue::Null, ForgeValue::Null) => Ok(ForgeValue::Null),
+			(ForgeValue::Number(_), _) => Err(UnsupportedOperation {
+				operation: BinaryOp::Subtract,
+				left_type: "number",
+				right_type: "non-number",
+			}),
+			(_, _) => Err(UnsupportedOperation {
+				operation: BinaryOp::Subtract,
+				left_type: "non-number",
+				right_type: "non-number",
+			}),
+		}
+	}
+}
+
+impl Div for ForgeValue {
+	type Output = Result<ForgeValue, UnsupportedOperation>;
+	fn div(self, rhs: Self) -> Self::Output {
+		match (self, rhs) {
+			(ForgeValue::Number(n1), ForgeValue::Number(n2)) => Ok(ForgeValue::Number(n1 / n2)),
+			(ForgeValue::Null, ForgeValue::Null) => Ok(ForgeValue::Null),
+			(ForgeValue::Number(_), _) => Err(UnsupportedOperation {
+				operation: BinaryOp::Divide,
+				left_type: "number",
+				right_type: "non-number",
+			}),
+			(_, _) => Err(UnsupportedOperation {
+				operation: BinaryOp::Divide,
+				left_type: "non-number",
+				right_type: "non-number",
+			}),
+		}
+	}
+}
+
+impl Mul for ForgeValue {
+	type Output = Result<ForgeValue, UnsupportedOperation>;
+	fn mul(self, rhs: Self) -> Self::Output {
+		match (self, rhs) {
+			(ForgeValue::Number(n1), ForgeValue::Number(n2)) => Ok(ForgeValue::Number(n1 * n2)),
+			(ForgeValue::Null, ForgeValue::Null) => Ok(ForgeValue::Null),
+			(ForgeValue::Number(_), _) => Err(UnsupportedOperation {
+				operation: BinaryOp::Multiply,
+				left_type: "number",
+				right_type: "non-number",
+			}),
+			(_, _) => Err(UnsupportedOperation {
+				operation: BinaryOp::Multiply,
+				left_type: "non-number",
+				right_type: "non-number",
+			}),
+		}
+	}
+}
+
+impl Rem for ForgeValue {
+	type Output = Result<ForgeValue, UnsupportedOperation>;
+	fn rem(self, rhs: Self) -> Self::Output {
+		match (self, rhs) {
+			(ForgeValue::Number(n1), ForgeValue::Number(n2)) => Ok(ForgeValue::Number(n1 % n2)),
+			(ForgeValue::Null, ForgeValue::Null) => Ok(ForgeValue::Null),
+			(ForgeValue::Number(_), _) => Err(UnsupportedOperation {
+				operation: BinaryOp::Modulo,
+				left_type: "number",
+				right_type: "non-number",
+			}),
+			(_, _) => Err(UnsupportedOperation {
+				operation: BinaryOp::Modulo,
+				left_type: "non-number",
+				right_type: "non-number",
+			}),
+		}
+	}
+}
+
+impl Not for ForgeValue {
+	type Output = ForgeValue;
+
+	fn not(self) -> Self::Output {
+		match self {
+			ForgeValue::Number(num) => (num != Number::Integer(0)).into(),
+			ForgeValue::Boolean(b) => (!b).into(),
+			ForgeValue::String(st) => (!st.is_empty()).into(),
+			ForgeValue::List(ls) => (!ls.is_empty()).into(),
+			ForgeValue::Null => ForgeValue::Null,
+		}
+	}
+}
+
+#[cfg(test)]
+mod value_tests {
+	use crate::runtime::value::ForgeValue;
+
+	#[test]
+	fn strict_type_equality() {
+		assert_ne!(ForgeValue::from(22), ForgeValue::from("22"));
+		assert_ne!(ForgeValue::from(true), ForgeValue::from("true"));
+		assert_ne!(ForgeValue::from(false), ForgeValue::from("false"));
+		assert_ne!(ForgeValue::from(true), ForgeValue::from(1));
+		assert_ne!(ForgeValue::from(false), ForgeValue::from(0));
+	}
+
+	#[test]
+	fn add_strings_concats() {
+		let first = ForgeValue::from("hello_");
+		let second = ForgeValue::from("world");
+
+		assert_eq!(
+			first + second,
+			ForgeValue::String(String::from("hello_world"))
+		);
+	}
+
+	#[test]
+	fn add_numbers() {
+		let first = ForgeValue::from(250);
+		let second = ForgeValue::from(250);
+		let third = ForgeValue::from(250);
+		let fourth = ForgeValue::from(250.0);
+
+		assert_eq!(first + second, ForgeValue::from(500));
+		assert_eq!(third + fourth, ForgeValue::from(500.0));
+	}
+
+	#[test]
+	fn add_bool_noop() {
+		assert_eq!(
+			ForgeValue::from("Something") + ForgeValue::from(true),
+			ForgeValue::from("Something")
+		);
+		assert_eq!(
+			ForgeValue::from("Something") + ForgeValue::from(false),
+			ForgeValue::from("Something")
+		);
+		assert_eq!(
+			ForgeValue::from(1000) + ForgeValue::from(true),
+			ForgeValue::from(1000)
+		);
+		assert_eq!(
+			ForgeValue::from(1000) + ForgeValue::from(false),
+			ForgeValue::from(1000)
+		);
+	}
+
+	#[test]
+	fn add_null_noop() {
+		assert_eq!(
+			ForgeValue::from("Something") + ForgeValue::null(),
+			ForgeValue::from("Something")
+		);
+		assert_eq!(
+			ForgeValue::from(1000) + ForgeValue::null(),
+			ForgeValue::from(1000)
+		);
+		assert_eq!(
+			ForgeValue::from(true) + ForgeValue::null(),
+			ForgeValue::from(true)
+		);
+		assert_eq!(
+			ForgeValue::from(false) + ForgeValue::null(),
+			ForgeValue::from(false)
+		);
+	}
+}
-- 
GitLab