From 850e7e891984b5044fd8e2ed10ab6ad4300fec61 Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Mon, 15 May 2023 23:20:18 +0100
Subject: [PATCH] Start parsing test suite, propogate correct error in wasm

---
 forge-script-lang/src/error/mod.rs         | 68 ++++++++++++++++++++++
 forge-script-lang/src/lib.rs               |  5 +-
 forge-script-lang/src/parser/grammar.rs    |  1 +
 forge-script-lang/src/parser/mod.rs        |  2 +
 forge-script-lang/src/parser/test_suite.rs | 30 ++++++++++
 forge-script-web/src/lib.rs                | 20 ++++++-
 6 files changed, 122 insertions(+), 4 deletions(-)
 create mode 100644 forge-script-lang/src/parser/test_suite.rs

diff --git a/forge-script-lang/src/error/mod.rs b/forge-script-lang/src/error/mod.rs
index 46b8e80..7019a93 100644
--- a/forge-script-lang/src/error/mod.rs
+++ b/forge-script-lang/src/error/mod.rs
@@ -1,5 +1,6 @@
 use crate::lexer::Span;
 use crate::parse::ScriptToken;
+use peg::error::ExpectedSet;
 use std::error::Error;
 use std::fmt::{Display, Formatter};
 
@@ -133,6 +134,63 @@ pub fn print_unexpected_token<'a>(
 	eprintln!("|\n| Failed To Parse: expected {}", expected);
 }
 
+pub struct ErrorFormat<'a>(pub &'a str, pub &'a ScriptToken<'a>, pub &'a ExpectedSet);
+
+impl<'a> Display for ErrorFormat<'a> {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		let ErrorFormat(source, token, expected) = self;
+
+		let line = token.position.location_line() as usize;
+		let column = token.position.get_column();
+
+		let previous_line = if line > 1 {
+			source.lines().nth(line - 2)
+		} else {
+			None
+		};
+		let source_line = source.lines().nth(line - 1).expect("Missing line");
+		let next_line = source.lines().nth(line);
+
+		let largest_line_num = line.max(line.saturating_sub(1)).max(line.saturating_add(1));
+		let number_length = format!("{}", largest_line_num).len();
+
+		writeln!(f, "| Script error on line {} at \"{}\"\n|", line, token)?;
+		if let Some(prev) = previous_line {
+			writeln!(
+				f,
+				"| [{:>width$}] {}",
+				line - 1,
+				prev,
+				width = number_length
+			)?;
+		}
+		writeln!(
+			f,
+			"| [{:>width$}] {}",
+			line,
+			source_line,
+			width = number_length
+		)?;
+		writeln!(
+			f,
+			"| {} {}{}",
+			vec![" "; number_length + 2].join(""),
+			vec![" "; column - 1].join(""),
+			vec!["^"; token.token_type.len()].join(""),
+		)?;
+		if let Some(next) = next_line {
+			writeln!(
+				f,
+				"| [{:>width$}] {}",
+				line + 1,
+				next,
+				width = number_length
+			)?;
+		}
+		writeln!(f, "|\n| Failed To Parse: expected {}", expected)
+	}
+}
+
 pub fn print_forge_error<'a>(source: &'a str, fe: &'a ForgeError) {
 	match &fe.kind {
 		ForgeErrorKind::IncompleteInput => eprintln!("| Unexpected end of file"),
@@ -142,3 +200,13 @@ pub fn print_forge_error<'a>(source: &'a str, fe: &'a ForgeError) {
 		}
 	}
 }
+
+pub fn format_forge_error<'a>(source: &'a str, fe: &'a ForgeError) -> String {
+	match &fe.kind {
+		ForgeErrorKind::IncompleteInput => String::from("| Unexpected end of file"),
+		ForgeErrorKind::LexerError(err) => format!("| {}", err),
+		ForgeErrorKind::UnexpectedToken { found, expected } => {
+			format!("{}", ErrorFormat(source, found, expected))
+		}
+	}
+}
diff --git a/forge-script-lang/src/lib.rs b/forge-script-lang/src/lib.rs
index 91ddc9b..24498c6 100644
--- a/forge-script-lang/src/lib.rs
+++ b/forge-script-lang/src/lib.rs
@@ -3,7 +3,10 @@ mod lexer;
 mod parser;
 pub mod runtime;
 
-pub use error::{print_forge_error, ParseError, ParseErrorKind, TokenError, TokenErrorKind};
+pub use error::{
+	format_forge_error, print_forge_error, ForgeError, ForgeErrorKind, ParseError, ParseErrorKind,
+	TokenError, TokenErrorKind,
+};
 pub mod parse {
 	pub use super::lexer::{script_to_tokens, ScriptToken, ScriptTokenType};
 	pub use super::parser::ast;
diff --git a/forge-script-lang/src/parser/grammar.rs b/forge-script-lang/src/parser/grammar.rs
index e292608..9e5fdba 100644
--- a/forge-script-lang/src/parser/grammar.rs
+++ b/forge-script-lang/src/parser/grammar.rs
@@ -62,6 +62,7 @@ peg::parser! {
 			/ "-" { BinaryOp::Subtract }
 			/ "*" { BinaryOp::Multiply }
 			/ "/" { BinaryOp::Divide }
+			/ "%" { BinaryOp::Modulo }
 
 		rule unary_operator() -> UnaryOp
 			= "!" { UnaryOp::Not }
diff --git a/forge-script-lang/src/parser/mod.rs b/forge-script-lang/src/parser/mod.rs
index 454b676..acbf4a7 100644
--- a/forge-script-lang/src/parser/mod.rs
+++ b/forge-script-lang/src/parser/mod.rs
@@ -1,6 +1,8 @@
 pub mod ast;
 mod atoms;
 mod grammar;
+#[cfg(test)]
+mod test_suite;
 
 use crate::error::{ForgeError, ForgeErrorKind, ForgeResult};
 use crate::print_forge_error;
diff --git a/forge-script-lang/src/parser/test_suite.rs b/forge-script-lang/src/parser/test_suite.rs
new file mode 100644
index 0000000..19f8c1a
--- /dev/null
+++ b/forge-script-lang/src/parser/test_suite.rs
@@ -0,0 +1,30 @@
+use crate::parse::parse_program;
+
+#[test]
+fn binary_ops() {
+	parse_program("1+1").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");
+	parse_program("1%1").expect("Failed binary mod");
+}
+
+#[test]
+fn unary_ops() {
+	parse_program("-1").expect("Failed binary inv");
+	parse_program("!1").expect("Failed binary not");
+}
+
+#[test]
+fn indentifiers() {
+	parse_program("let my_ident").expect("Failed declare");
+	parse_program("let my_ident = 123").expect("Failed declare/assign");
+}
+
+#[test]
+fn conditional() {
+	parse_program("if false { }").expect("Failed conditional with literal");
+	parse_program("if some_ident { }").expect("Failed conditional with identifier");
+	parse_program("if 1 + 1 { }").expect("Failed conditional with expr");
+	parse_program("if (let ident = false) { }").expect("Failed conditional with decl");
+}
diff --git a/forge-script-web/src/lib.rs b/forge-script-web/src/lib.rs
index 11e449e..7d8985d 100644
--- a/forge-script-web/src/lib.rs
+++ b/forge-script-web/src/lib.rs
@@ -1,3 +1,4 @@
+use forge_script_lang::format_forge_error;
 use forge_script_lang::parse::parse_program;
 use forge_script_lang::runtime::executor::{TreePrinter, Visitor};
 use wasm_bindgen::prelude::*;
@@ -9,13 +10,26 @@ pub fn init() {
 
 #[wasm_bindgen]
 pub fn compile_ast(program: &str) -> Result<JsValue, serde_wasm_bindgen::Error> {
-	serde_wasm_bindgen::to_value(&parse_program(program).expect("Failed to parse"))
+	match parse_program(program) {
+		Ok(val) => serde_wasm_bindgen::to_value(&val),
+		Err(e) => Err(serde_wasm_bindgen::Error::new(format_forge_error(
+			program, &e,
+		))),
+	}
 }
 
 #[wasm_bindgen]
 pub fn format_script(program: &str) -> Result<String, serde_wasm_bindgen::Error> {
-	let ast = &parse_program(program).expect("Failed to parse");
+	let ast = match parse_program(program) {
+		Ok(val) => val,
+		Err(e) => {
+			return Err(serde_wasm_bindgen::Error::new(format_forge_error(
+				program, &e,
+			)));
+		}
+	};
+
 	let mut writer = TreePrinter::new();
-	writer.evaluate_program(ast);
+	writer.evaluate_program(&ast);
 	Ok(writer.take_value())
 }
-- 
GitLab