From d6d3e13bf15eca7924fbb5fa817246a88a2fe481 Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Wed, 17 May 2023 04:26:06 +0100
Subject: [PATCH] Add 'typeof' support, fix up grammar order, add more tests

---
 forge-script-lang/src/lexer/keywords.rs       | 12 +++
 forge-script-lang/src/lexer/mod.rs            |  9 +-
 forge-script-lang/src/lexer/tokens.rs         |  8 ++
 forge-script-lang/src/parser/ast.rs           | 19 ++++
 forge-script-lang/src/parser/grammar.rs       | 93 ++++++++++---------
 forge-script-lang/src/parser/test_suite.rs    |  7 ++
 .../src/runtime/executor/printer.rs           |  4 +
 .../src/runtime/executor/simple.rs            | 77 +++++++++++++--
 forge-script-lang/src/runtime/value.rs        | 10 ++
 9 files changed, 179 insertions(+), 60 deletions(-)

diff --git a/forge-script-lang/src/lexer/keywords.rs b/forge-script-lang/src/lexer/keywords.rs
index 7d91ae5..b919a22 100644
--- a/forge-script-lang/src/lexer/keywords.rs
+++ b/forge-script-lang/src/lexer/keywords.rs
@@ -195,3 +195,15 @@ pub fn token_from(input: Span) -> IResult<Span, ScriptToken> {
 		},
 	))
 }
+
+pub fn token_typeof(input: Span) -> IResult<Span, ScriptToken> {
+	let (input, pos) = position(input)?;
+	let (input, _) = tag_ws("typeof", input)?;
+	Ok((
+		input,
+		ScriptToken {
+			position: pos,
+			token_type: ScriptTokenType::Typeof,
+		},
+	))
+}
diff --git a/forge-script-lang/src/lexer/mod.rs b/forge-script-lang/src/lexer/mod.rs
index 3eaf5a1..76e85ed 100644
--- a/forge-script-lang/src/lexer/mod.rs
+++ b/forge-script-lang/src/lexer/mod.rs
@@ -8,7 +8,7 @@ mod tokens;
 use keywords::{
 	token_alias, token_else, token_export, token_for, token_from, token_function, token_if,
 	token_import, token_let, token_null, token_print, token_return, token_struct, token_super,
-	token_this, token_while,
+	token_this, token_typeof, token_while,
 };
 use operators::{
 	token_asterisk, token_bang, token_bang_equal, token_caret, token_comma, token_dot,
@@ -52,6 +52,7 @@ mod _lex {
 				token_struct,
 				token_super,
 				token_from,
+				token_typeof,
 			)),
 			alt((
 				token_plus,
@@ -72,13 +73,13 @@ mod _lex {
 				token_semicolon,
 			)),
 			alt((
-				token_less,
-				token_greater,
-				token_equal,
 				token_equal_equal,
 				token_bang_equal,
 				token_less_equal,
 				token_greater_equal,
+				token_less,
+				token_greater,
+				token_equal,
 			)),
 			alt((
 				token_float,
diff --git a/forge-script-lang/src/lexer/tokens.rs b/forge-script-lang/src/lexer/tokens.rs
index 33031ae..bb2731c 100644
--- a/forge-script-lang/src/lexer/tokens.rs
+++ b/forge-script-lang/src/lexer/tokens.rs
@@ -58,6 +58,7 @@ pub enum ScriptTokenType<'a> {
 	Import,
 	Alias,
 	From,
+	Typeof,
 
 	// Misc
 	Eof,
@@ -118,6 +119,7 @@ impl<'a> ScriptTokenType<'a> {
 			ScriptTokenType::Alias => 2,
 			ScriptTokenType::From => 4,
 			ScriptTokenType::Eof => 0,
+			ScriptTokenType::Typeof => 6,
 		}
 	}
 }
@@ -171,6 +173,7 @@ impl<'a> Display for ScriptTokenType<'a> {
 			ScriptTokenType::Alias => write!(f, "as"),
 			ScriptTokenType::From => write!(f, "from"),
 			ScriptTokenType::Eof => write!(f, ""),
+			ScriptTokenType::Typeof => write!(f, "typeof"),
 		}
 	}
 }
@@ -230,6 +233,7 @@ impl<'a> TryFrom<&'a str> for ScriptTokenType<'a> {
 			"import" => Ok(ScriptTokenType::Import),
 			"as" => Ok(ScriptTokenType::Alias),
 			"from" => Ok(ScriptTokenType::From),
+			"typeof" => Ok(ScriptTokenType::Typeof),
 			"false" => Ok(ScriptTokenType::Boolean(false)),
 			"true" => Ok(ScriptTokenType::Boolean(true)),
 			_ => Err(TokenFromStringError { source: value }),
@@ -366,5 +370,9 @@ mod token_tests {
 		);
 		assert_eq!(ScriptTokenType::try_from("as"), Ok(ScriptTokenType::Alias));
 		assert_eq!(ScriptTokenType::try_from("from"), Ok(ScriptTokenType::From));
+		assert_eq!(
+			ScriptTokenType::try_from("typeof"),
+			Ok(ScriptTokenType::Typeof)
+		);
 	}
 }
diff --git a/forge-script-lang/src/parser/ast.rs b/forge-script-lang/src/parser/ast.rs
index 131a146..042841a 100644
--- a/forge-script-lang/src/parser/ast.rs
+++ b/forge-script-lang/src/parser/ast.rs
@@ -101,6 +101,17 @@ pub enum BinaryOp {
 	Divide,
 	Modulo,
 	Equals,
+	BoolAnd,
+	BoolOr,
+}
+
+impl BinaryOp {
+	pub fn short_circuits(&self) -> bool {
+		match self {
+			Self::BoolAnd | Self::BoolOr => true,
+			_ => false,
+		}
+	}
 }
 
 impl Display for BinaryOp {
@@ -115,6 +126,8 @@ impl Display for BinaryOp {
 				BinaryOp::Divide => "/",
 				BinaryOp::Modulo => "%",
 				BinaryOp::Equals => "==",
+				BinaryOp::BoolAnd => "&&",
+				BinaryOp::BoolOr => "||",
 			}
 		)
 	}
@@ -167,8 +180,14 @@ pub enum ValueExpression {
 	Identifier(Identifier),
 	FunctionCall(FunctionCall),
 	DeclareFunction(DeclareFunction),
+	Typeof(TypeofValue),
 }
 
+#[derive(Clone)]
+#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct TypeofValue(pub Box<ValueExpression>);
+
 #[derive(Clone)]
 #[cfg_attr(feature = "debug-ast", derive(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 1f1a12d..d3af36b 100644
--- a/forge-script-lang/src/parser/grammar.rs
+++ b/forge-script-lang/src/parser/grammar.rs
@@ -1,7 +1,7 @@
 use crate::parser::TokenSlice;
 
 peg::parser! {
-	grammar script_parser<'a>() for TokenSlice<'a> {
+	grammar forge_parser<'a>() for TokenSlice<'a> {
 		use crate::parser::ast::*;
 		use crate::runtime::numbers::Number;
 		use crate::lexer::{ScriptToken, ScriptTokenType};
@@ -9,38 +9,30 @@ peg::parser! {
 		pub rule program() -> Program
 			= ex:expression_list() eof() { Program(ex) }
 
-		rule expression_list() -> ExpressionList
-			= e:(expression() ++ ";") term:";"? { ExpressionList { expressions: e, is_void: term.is_some() } }
-
-		// rule statement() -> Expression
-		// 	Include conditional here separately from expression to allow "if" without semi
-			// = e:conditional() { Expression::Value(ValueExpression::ConditionalBlock(e)) }
-			// / e:condition_loop() { Expression::Void(VoidExpression::ConditionLoop(e)) }
-			// / e:expression() ";" { e }
-
 		pub rule expression() -> Expression
 			= ex:value_expression() { Expression::Value(ex) }
 			/ ex:void_expression() { Expression::Void(ex) }
 
 		rule void_expression() -> VoidExpression
 			= ex:print() { VoidExpression::Print(ex) }
-			/ "import" "{" items:identifier_list() "}" "from" source:raw_string() { VoidExpression::Import(Import { source, items }) }
+			/ "import" "{" items:identifier_list() "}" "from" source:string_value() { VoidExpression::Import(Import { source, items }) }
 			/ "export" "{" items:identifier_list() "}" { VoidExpression::Export(Export { items }) }
 			/ e:condition_loop() { VoidExpression::ConditionLoop(e) }
 
-		 #[cache_left_rec]
+		#[cache_left_rec]
 		rule value_expression() -> ValueExpression
-			= co:conditional() { ValueExpression::ConditionalBlock(co) }
+			= co:conditional_statement() { ValueExpression::ConditionalBlock(co) }
+			/ t:type_of() { ValueExpression::Typeof(t) }
 			/ decl:declare_variable() { ValueExpression::DeclareIdentifier(decl) }
 			/ decl:declare_function() { ValueExpression::DeclareFunction(decl) }
-			/ name:bare_identifier() "(" params:param_list()? ")"
+			/ name:simple_identifier() "(" params:param_list()? ")"
 				{ ValueExpression::FunctionCall(FunctionCall { name, params: params.unwrap_or_default() }) }
 			/ left:value_expression() op:binary_operator() right:value_expression()
 				{ 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) }
+			/ ident:simple_identifier() !"(" { ValueExpression::Identifier(ident) }
 			/ li:literal() { ValueExpression::Literal(li) }
 
 		rule grouped() -> ValueExpression
@@ -50,39 +42,45 @@ peg::parser! {
 		rule print() -> Print
 			= "print" ex:value_expression() { ex.into() }
 
-		rule declare_variable() -> DeclareIdent
-			= "let" ex:declare_identifier() { ex }
+		rule type_of() -> TypeofValue
+			= "typeof" ex:value_expression() { TypeofValue(Box::new(ex)) }
 
-		rule declare_identifier() -> DeclareIdent
+		rule declare_function() -> DeclareFunction
+			= "fn" ident:simple_identifier() "(" params:(function_param() ** ",") ")" body:block()
+				{ DeclareFunction { ident, params, body } }
+
+		rule function_param() -> DeclareIdent
 			= assign:assignment() { DeclareIdent::WithValue(assign) }
-			/ ident:bare_identifier() { DeclareIdent::WithoutValue(ident) }
+			/ ident:simple_identifier() { DeclareIdent::WithoutValue(ident) }
 
-		rule declare_function() -> DeclareFunction
-			= "fn" ident:bare_identifier() "(" params:(declare_identifier() ** ",") ")" "{" block:expression_list()? "}"
-				{ DeclareFunction { ident, params, body: block.unwrap_or_default() } }
+		rule assignment() -> Assignment
+			= ident:simple_identifier() "=" ex:value_expression() { Assignment { ident, value: Box::new(ex) } }
+
+		rule declare_variable() -> DeclareIdent
+			= "let" assign:assignment() { DeclareIdent::WithValue(assign) }
+			/ "let" ident:simple_identifier() { DeclareIdent::WithoutValue(ident) }
 
 		rule condition_loop() -> ConditionalLoop
-			= "while" guard:value_expression() "{" block:expression_list()? "}"
-				{ ConditionalLoop { block: GuardedBlock { guard: Box::new(guard), block: block.unwrap_or_default() }, fallback: None } }
-			/ "while" guard:value_expression() "{" block:expression_list()? "}" "else" "{" fallback:expression_list()? "}"
-				{ ConditionalLoop { block: GuardedBlock { guard: Box::new(guard), block: block.unwrap_or_default() }, fallback } }
-
-		rule conditional() -> Conditional
-			// = bl:guarded_block() { Conditional { fallback: None, blocks: vec![bl] } }
-			= blocks:(guarded_block() ++ "else") "else" "{" fallback:expression_list() "}" {
-				Conditional {
-					blocks,
-					fallback: Some(fallback)
-				}
-			}
-			/ blocks:(guarded_block() ++ "else") { Conditional { fallback: None, blocks, } }
-
-		rule guarded_block() -> GuardedBlock
+			= "while" guard:value_expression() block:block()
+				{ ConditionalLoop { block: GuardedBlock { guard: Box::new(guard), block }, fallback: None } }
+			/ "while" guard:value_expression() block:block() "else" fallback:block()
+				{ ConditionalLoop { block: GuardedBlock { guard: Box::new(guard), block }, fallback: Some(fallback) } }
+
+		rule conditional_statement() -> Conditional
+			= blocks:(conditional_block() ++ "else") "else" fallback:block()
+				{ Conditional { blocks, fallback: Some(fallback) } }
+			/ blocks:(conditional_block() ++ "else")
+				{ Conditional { blocks, fallback: None } }
+
+		rule conditional_block() -> GuardedBlock
 			= "if" guard:value_expression() "{" block:expression_list()? "}"
 				{ GuardedBlock { block: block.unwrap_or_default(), guard: Box::new(guard) } }
 
-		rule assignment() -> Assignment
-			= ident:bare_identifier() "=" expr:value_expression() { Assignment { ident, value: Box::new(expr) } }
+		rule block() -> ExpressionList
+			= "{" ex:expression_list() "}" { ex }
+
+		rule expression_list() -> ExpressionList
+			= ex:(expression() ** ";") term:";"? { ExpressionList { expressions: ex, is_void: term.is_some() } }
 
 		rule binary_operator() -> BinaryOp
 			= "+" { BinaryOp::Add }
@@ -90,6 +88,9 @@ peg::parser! {
 			/ "*" { BinaryOp::Multiply }
 			/ "/" { BinaryOp::Divide }
 			/ "%" { BinaryOp::Modulo }
+			/ "==" { BinaryOp::Equals }
+			/ "&&" { BinaryOp::BoolAnd }
+			/ "||" { BinaryOp::BoolOr }
 
 		rule unary_operator() -> UnaryOp
 			= "!" { UnaryOp::Not }
@@ -99,16 +100,16 @@ peg::parser! {
 			= identifier() ++ ","
 
 		rule param_list() -> ParameterList
-			= ids:value_expression() ++ ","
+			= value_expression() ++ ","
 
 		rule identifier() -> IdentifierNode
 			= id:alias_identifier() { IdentifierNode::Alias(id) }
-			/ id:bare_identifier() { IdentifierNode::Direct(id) }
+			/ id:simple_identifier() { IdentifierNode::Direct(id) }
 
 		rule alias_identifier() -> IdentifierAlias
-			= base:bare_identifier() "as" alias:bare_identifier() { IdentifierAlias(base.0, alias.0) }
+			= base:simple_identifier() "as" alias:simple_identifier() { IdentifierAlias(base.0, alias.0) }
 
-		rule bare_identifier() -> Identifier
+		rule simple_identifier() -> Identifier
 			= [ScriptToken { token_type: ScriptTokenType::Identifier(vl), .. }] { Identifier(String::from(*vl)) }
 
 		rule literal() -> LiteralNode
@@ -120,7 +121,7 @@ peg::parser! {
 			/ [ScriptToken { token_type: ScriptTokenType::Integer(vl), .. }] { LiteralNode::Number(Number::Integer(*vl)) }
 			/ [ScriptToken { token_type: ScriptTokenType::Float(vl), .. }] { LiteralNode::Number(Number::Float(*vl)) }
 
-		rule raw_string() -> String
+		rule string_value() -> String
 			= [ScriptToken { token_type: ScriptTokenType::String(vl), .. }] { String::from(*vl) }
 			/ [ScriptToken { token_type: ScriptTokenType::OwnedString(vl), .. }] { vl.clone() }
 
@@ -128,4 +129,4 @@ peg::parser! {
 	}
 }
 
-pub use script_parser::{expression, program};
+pub use forge_parser::{expression, program};
diff --git a/forge-script-lang/src/parser/test_suite.rs b/forge-script-lang/src/parser/test_suite.rs
index fc217e2..9a7f497 100644
--- a/forge-script-lang/src/parser/test_suite.rs
+++ b/forge-script-lang/src/parser/test_suite.rs
@@ -73,3 +73,10 @@ fn declare_function() {
 	parse_program("fn add(first, second) { first + second }")
 		.expect("Failed basic function with body");
 }
+
+#[test]
+fn type_check() {
+	parse_program("typeof 123").expect("Failed to parse typeof literal");
+	parse_program("typeof foo").expect("Failed to parse typeof identifier");
+	parse_program("typeof (\"Str \" + 123)").expect("Failed to parse typeof grouped expression");
+}
diff --git a/forge-script-lang/src/runtime/executor/printer.rs b/forge-script-lang/src/runtime/executor/printer.rs
index 31b5b3b..2d8d996 100644
--- a/forge-script-lang/src/runtime/executor/printer.rs
+++ b/forge-script-lang/src/runtime/executor/printer.rs
@@ -219,6 +219,10 @@ impl Visitor for TreePrinter {
 				self.decrement();
 				self.writeln("}");
 			}
+			ValueExpression::Typeof(type_of) => {
+				self.write("typeof ");
+				self.evaluate_value_expression(type_of.0.as_ref());
+			}
 		}
 
 		Ok(())
diff --git a/forge-script-lang/src/runtime/executor/simple.rs b/forge-script-lang/src/runtime/executor/simple.rs
index 12a04b3..46fb554 100644
--- a/forge-script-lang/src/runtime/executor/simple.rs
+++ b/forge-script-lang/src/runtime/executor/simple.rs
@@ -136,16 +136,29 @@ impl Visitor for SimpleExecutor {
 				}
 			}
 			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()),
+				if operator.short_circuits() {
+					let lhs = self.evaluate_value_expression(lhs.as_ref())?;
+					match (operator, lhs.as_bool()) {
+						(BinaryOp::BoolOr, true) => Ok(true.into()),
+						(BinaryOp::BoolAnd, false) => Ok(false.into()),
+						(BinaryOp::BoolOr, false) | (BinaryOp::BoolAnd, true) => Ok(self
+							.evaluate_value_expression(rhs.as_ref())?
+							.as_bool()
+							.into()),
+						_ => Ok(ForgeValue::Null),
+					}
+				} else {
+					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()),
+						BinaryOp::BoolOr | BinaryOp::BoolAnd => Ok(ForgeValue::Null),
+					}
 				}
 			}
 			ValueExpression::Grouped(group) => self.evaluate_value_expression(group.inner.as_ref()),
@@ -191,6 +204,10 @@ impl Visitor for SimpleExecutor {
 			ValueExpression::DeclareFunction(_) => {
 				Err(RuntimeError::Unsupported("DeclareFunction"))
 			}
+			ValueExpression::Typeof(type_of) => Ok(ForgeValue::from(
+				self.evaluate_value_expression(type_of.0.as_ref())?
+					.type_name(),
+			)),
 		}
 	}
 
@@ -258,6 +275,7 @@ mod interpreter_test {
 	#[test]
 	fn print() {
 		let print_4 = parse_program("print 2 + 2").expect("Failed to parse");
+
 		let mut vm = SimpleExecutor::default();
 		assert_eq!(vm.evaluate_program(&print_4), Ok(ForgeValue::Null));
 	}
@@ -285,4 +303,43 @@ mod interpreter_test {
 		assert_eq!(vm.evaluate_program(&use_vars), Ok(ForgeValue::from(246)));
 		assert_eq!(vm.get_variable("splop"), Some(&ForgeValue::from(246)));
 	}
+
+	#[test]
+	fn conditional_blocks() {
+		let tests = [
+			("let value = true; if value { 123 } else { 456 }", Ok(ForgeValue::from(123))),
+			("let value = true; if !value { 123 } else { 456 }", Ok(ForgeValue::from(456))),
+			("let value = 2; if value == 1 { 123 } else if value == 2 { 456 } else { \"Nothing\" }", Ok(ForgeValue::from(456))),
+		];
+
+		for (prog, expected) in tests {
+			let ast = parse_program(prog).expect("Failed to parse");
+			let mut vm = SimpleExecutor::default();
+			assert_eq!(vm.evaluate_program(&ast), expected);
+		}
+	}
+
+	#[test]
+	/// When the first term in a binary op has already decided the outcome, don't evaluate the following terms
+	fn short_circuit() {
+		let prog = parse_program("if true || let bar = 123 { 9001 }").expect("Failed to parse");
+		let mut vm = SimpleExecutor::default();
+		assert_eq!(vm.evaluate_program(&prog), Ok(ForgeValue::from(9001)));
+		assert!(vm.get_variable("bar").is_none());
+	}
+
+	#[test]
+	fn variable_type() {
+		let tests = [
+			("typeof 123", Ok(ForgeValue::from("number"))),
+			(r#"typeof "some string""#, Ok(ForgeValue::from("string"))),
+			("typeof false", Ok(ForgeValue::from("boolean"))),
+		];
+
+		for (prog, expected) in tests {
+			let ast = parse_program(prog).expect("Failed to parse");
+			let mut vm = SimpleExecutor::default();
+			assert_eq!(vm.evaluate_program(&ast), expected);
+		}
+	}
 }
diff --git a/forge-script-lang/src/runtime/value.rs b/forge-script-lang/src/runtime/value.rs
index 697567f..3619e10 100644
--- a/forge-script-lang/src/runtime/value.rs
+++ b/forge-script-lang/src/runtime/value.rs
@@ -32,6 +32,16 @@ impl ForgeValue {
 		ForgeValue::Null
 	}
 
+	pub fn type_name(&self) -> &'static str {
+		match self {
+			ForgeValue::Number(_) => "number",
+			ForgeValue::Boolean(_) => "boolean",
+			ForgeValue::String(_) => "string",
+			ForgeValue::List(_) => "list",
+			ForgeValue::Null => "null",
+		}
+	}
+
 	pub fn invert(&self) -> Self {
 		match self {
 			ForgeValue::Number(num) => match num {
-- 
GitLab