From f8b051c15b3197a8514895d37a3bd6f0ff159a32 Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Tue, 16 May 2023 18:30:20 +0100
Subject: [PATCH] Support variable assignment, state, basic environment

---
 .../src/runtime/executor/simple.rs            | 107 ++++++++++++++++--
 forge-script-lang/src/runtime/value.rs        |   3 +-
 2 files changed, 102 insertions(+), 8 deletions(-)

diff --git a/forge-script-lang/src/runtime/executor/simple.rs b/forge-script-lang/src/runtime/executor/simple.rs
index 13cad9a..f4f99ac 100644
--- a/forge-script-lang/src/runtime/executor/simple.rs
+++ b/forge-script-lang/src/runtime/executor/simple.rs
@@ -1,22 +1,31 @@
 use crate::parse::ast::{
-	BinaryOp, DeclareFunction, ExpressionList, Print, Program, UnaryOp, ValueExpression,
-	VoidExpression,
+	BinaryOp, DeclareFunction, DeclareIdent, ExpressionList, Print, Program, UnaryOp,
+	ValueExpression, VoidExpression,
 };
+use crate::parser::ast::Assignment;
 use crate::runtime::executor::Visitor;
 use crate::runtime::value::{ForgeValue, UnsupportedOperation};
+use std::collections::hash_map::Entry;
 use std::collections::HashMap;
 use std::error::Error;
 use std::fmt::{Debug, Display, Formatter};
 
 #[derive(Clone, Default)]
-pub struct SimpleExecutor {
+pub struct SimpleScope {
 	data: HashMap<String, ForgeValue>,
 	vtable: HashMap<String, DeclareFunction>,
 }
 
+#[derive(Clone, Default)]
+pub struct SimpleExecutor {
+	scope: SimpleScope,
+}
+
 #[derive(Clone, Debug, PartialEq)]
 pub enum RuntimeError {
 	Unsupported(&'static str),
+	UndefinedVariable(String),
+	VariableAlreadyDefined(String),
 	BadOperands(UnsupportedOperation),
 }
 
@@ -37,13 +46,63 @@ impl Display for RuntimeError {
 				)
 			}
 			RuntimeError::BadOperands(unsupported) => write!(f, "[Runtime] {}", unsupported),
+			RuntimeError::UndefinedVariable(var) => {
+				write!(f, "[Runtime] Attempt to access undefined variable {}", var)
+			}
+			RuntimeError::VariableAlreadyDefined(var) => write!(
+				f,
+				"[Runtime] Attempt to redefine already defined variable {}",
+				var
+			),
 		}
 	}
 }
 
 impl Error for RuntimeError {}
 
+pub type RuntimeResult<T> = Result<T, RuntimeError>;
+
 impl SimpleExecutor {
+	pub fn get_variable(&self, identifier: impl ToString) -> Option<&ForgeValue> {
+		self.scope.data.get(&identifier.to_string())
+	}
+
+	pub fn set_variable(
+		&mut self,
+		identifier: impl ToString,
+		value: ForgeValue,
+	) -> RuntimeResult<()> {
+		let entry = self.scope.data.entry(identifier.to_string());
+		match entry {
+			Entry::Occupied(mut e) => {
+				e.insert(value);
+				Ok(())
+			}
+			Entry::Vacant(_) => Err(RuntimeError::UndefinedVariable(identifier.to_string())),
+		}
+	}
+
+	pub fn define_variable(
+		&mut self,
+		identifier: impl ToString,
+		value: ForgeValue,
+	) -> RuntimeResult<()> {
+		let entry = self.scope.data.entry(identifier.to_string());
+		match entry {
+			Entry::Vacant(e) => {
+				e.insert(value);
+				Ok(())
+			}
+			Entry::Occupied(_) => Err(RuntimeError::VariableAlreadyDefined(identifier.to_string())),
+		}
+	}
+
+	fn process_assignment(&mut self, assignment: &Assignment) -> RuntimeResult<ForgeValue> {
+		let value = self.evaluate_value_expression(assignment.value.as_ref())?;
+		self.set_variable(&assignment.ident, value.clone())?;
+		Ok(value)
+	}
+
 	fn evaluate_expression_list(
 		&mut self,
 		list: &ExpressionList,
@@ -92,14 +151,24 @@ impl Visitor for SimpleExecutor {
 			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::DeclareIdentifier(decl) => match decl {
+				DeclareIdent::WithValue(assignment) => {
+					self.define_variable(&assignment.ident, ForgeValue::Null)?;
+					self.process_assignment(assignment)
+				}
+				DeclareIdent::WithoutValue(identifier) => {
+					self.define_variable(identifier, ForgeValue::Null)?;
+					Ok(ForgeValue::Null)
+				}
+			},
 			ValueExpression::Assignment(_) => Err(RuntimeError::Unsupported("Assignment")),
 			ValueExpression::ConditionalBlock(_) => {
 				Err(RuntimeError::Unsupported("ConditionalBlock"))
 			}
-			ValueExpression::Identifier(_) => Err(RuntimeError::Unsupported("Identifier")),
+			ValueExpression::Identifier(ident) => self
+				.get_variable(ident)
+				.cloned()
+				.ok_or_else(|| RuntimeError::UndefinedVariable(ident.to_string())),
 			ValueExpression::FunctionCall(_) => Err(RuntimeError::Unsupported("FunctionCall")),
 			ValueExpression::DeclareFunction(_) => {
 				Err(RuntimeError::Unsupported("DeclareFunction"))
@@ -174,4 +243,28 @@ mod interpreter_test {
 		let mut vm = SimpleExecutor::default();
 		assert_eq!(vm.evaluate_program(&print_4), Ok(ForgeValue::Null));
 	}
+
+	#[test]
+	fn define_variables() {
+		let simple_define = parse_program("let foo").expect("Failed to parse");
+		let assignment = parse_program("let bar = 123").expect("Failed to parse");
+		let redecl_err = parse_program("let foo = 123").expect("Failed to parse");
+		let missing_err = parse_program("print missing").expect("Failed to parse");
+		let use_vars = parse_program("let splop = bar * 2").expect("Failed to parse");
+
+		let mut vm = SimpleExecutor::default();
+
+		assert_eq!(vm.evaluate_program(&simple_define), Ok(ForgeValue::Null));
+		assert_eq!(vm.evaluate_program(&assignment), Ok(ForgeValue::from(123)));
+		assert_eq!(
+			vm.evaluate_program(&redecl_err),
+			Err(RuntimeError::VariableAlreadyDefined(String::from("foo")))
+		);
+		assert_eq!(
+			vm.evaluate_program(&missing_err),
+			Err(RuntimeError::UndefinedVariable(String::from("missing")))
+		);
+		assert_eq!(vm.evaluate_program(&use_vars), Ok(ForgeValue::from(246)));
+		assert_eq!(vm.get_variable("splop"), Some(&ForgeValue::from(246)));
+	}
 }
diff --git a/forge-script-lang/src/runtime/value.rs b/forge-script-lang/src/runtime/value.rs
index 1078730..ad6e04f 100644
--- a/forge-script-lang/src/runtime/value.rs
+++ b/forge-script-lang/src/runtime/value.rs
@@ -4,7 +4,7 @@ use crate::runtime::numbers::Number;
 use std::fmt::{Display, Formatter};
 use std::ops::{Add, Div, Mul, Not, Rem, Sub};
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Default)]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
@@ -23,6 +23,7 @@ pub enum ForgeValue {
 	Boolean(bool),
 	String(String),
 	List(Vec<ForgeValue>),
+	#[default]
 	Null,
 }
 
-- 
GitLab