From ff05c13844791ed9537d64d3c8c5496886eb04bf Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Sun, 28 May 2023 05:02:09 +0100
Subject: [PATCH] Replace peg parse_ fns with lalrpop parse_ fns

---
 .gitignore                                    |   2 +-
 forge-script-lang/src/build.rs                |   3 +-
 forge-script-lang/src/error/mod.rs            | 296 ++++++++++++++----
 forge-script-lang/src/lexer/keywords.rs       |   2 +-
 forge-script-lang/src/lexer/mod.rs            |  18 ++
 forge-script-lang/src/parser/ast.rs           |  64 ++--
 ...e_script.lalrpop => forge_grammar.lalrpop} | 123 +++++++-
 forge-script-lang/src/parser/forge_script.rs  |  71 +++++
 forge-script-lang/src/parser/lr_grammar.rs    |  45 ---
 forge-script-lang/src/parser/mod.rs           |  85 ++---
 forge-script-lang/src/utilities.rs            |  20 ++
 11 files changed, 548 insertions(+), 181 deletions(-)
 rename forge-script-lang/src/parser/{forge_script.lalrpop => forge_grammar.lalrpop} (51%)
 create mode 100644 forge-script-lang/src/parser/forge_script.rs
 delete mode 100644 forge-script-lang/src/parser/lr_grammar.rs

diff --git a/.gitignore b/.gitignore
index ff2439c..e28572c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,4 +130,4 @@ target/
 
 # Generated Parser
 
-forge-script-lang/src/parser/forge_script.rs
\ No newline at end of file
+forge-script-lang/src/parser/forge_grammar.rs
\ No newline at end of file
diff --git a/forge-script-lang/src/build.rs b/forge-script-lang/src/build.rs
index 3094868..194123d 100644
--- a/forge-script-lang/src/build.rs
+++ b/forge-script-lang/src/build.rs
@@ -1,5 +1,6 @@
 pub fn main() {
 	lalrpop::Configuration::new()
-		.process_file("src/parser/forge_script.lalrpop")
+		.generate_in_source_tree()
+		.process_file("src/parser/forge_grammar.lalrpop")
 		.expect("Failed to parse language grammar");
 }
diff --git a/forge-script-lang/src/error/mod.rs b/forge-script-lang/src/error/mod.rs
index b3986af..f741161 100644
--- a/forge-script-lang/src/error/mod.rs
+++ b/forge-script-lang/src/error/mod.rs
@@ -1,8 +1,12 @@
-use crate::lexer::Span;
+use crate::lexer::{ScriptTokenType, Span};
 use crate::parse::ScriptToken;
-use peg::error::ExpectedSet;
+use crate::utilities::offset_to_line_column;
+use lalrpop_util::ParseError as BaseLalrError;
 use std::error::Error;
 use std::fmt::{Display, Formatter};
+use std::process::{ExitCode, Termination};
+
+pub type LalrError<'a> = BaseLalrError<usize, ScriptTokenType<'a>, TokenError<'a>>;
 
 #[derive(Debug)]
 pub enum TokenErrorKind<'a> {
@@ -61,13 +65,53 @@ pub enum ForgeErrorKind<'a> {
 		found: ScriptToken<'a>,
 		expected: peg::error::ExpectedSet,
 	},
+	InvalidToken {
+		location: TokenIndex,
+	},
+	UnexpectedEof {
+		expected: Vec<String>,
+	},
+	UnrecognizedToken {
+		token: ScriptTokenType<'a>,
+		span: ErrorSpan,
+		expected: Vec<String>,
+	},
+	ExpectedEof {
+		token: ScriptTokenType<'a>,
+		span: ErrorSpan,
+	},
+	Custom(String),
 }
 
+pub type TokenIndex = usize;
+pub type ErrorSpan = (TokenIndex, TokenIndex);
+
 #[derive(Debug, PartialEq)]
 pub struct ForgeError<'a> {
 	pub kind: ForgeErrorKind<'a>,
 }
 
+impl<'a> Termination for ForgeError<'a> {
+	fn report(self) -> ExitCode {
+		match self.kind {
+			ForgeErrorKind::IncompleteInput => ExitCode::from(101),
+			ForgeErrorKind::LexerError(_) => ExitCode::from(102),
+			ForgeErrorKind::UnexpectedToken { .. } => ExitCode::from(103),
+			ForgeErrorKind::InvalidToken { .. } => ExitCode::from(104),
+			ForgeErrorKind::UnexpectedEof { .. } => ExitCode::from(105),
+			ForgeErrorKind::UnrecognizedToken { .. } => ExitCode::from(106),
+			ForgeErrorKind::ExpectedEof { .. } => ExitCode::from(107),
+			ForgeErrorKind::Custom(_) => ExitCode::from(1),
+		}
+	}
+}
+
+impl<'a> From<ForgeErrorKind<'a>> for ForgeError<'a> {
+	fn from(value: ForgeErrorKind<'a>) -> Self {
+		Self { kind: value }
+	}
+}
+
 impl<'a> From<ParseError<'a>> for ForgeError<'a> {
 	fn from(value: ParseError<'a>) -> Self {
 		match value.kind {
@@ -91,6 +135,31 @@ impl<'a> From<TokenError<'a>> for ForgeError<'a> {
 	}
 }
 
+impl<'a> From<LalrError<'a>> for ForgeError<'a> {
+	fn from(value: LalrError<'a>) -> Self {
+		match value {
+			LalrError::InvalidToken { location } => {
+				ForgeErrorKind::InvalidToken { location }.into()
+			}
+			LalrError::UnrecognizedEof { expected, .. } => {
+				ForgeErrorKind::UnexpectedEof { expected }.into()
+			}
+			LalrError::UnrecognizedToken { token, expected } => ForgeErrorKind::UnrecognizedToken {
+				expected,
+				token: token.1,
+				span: (token.0, token.2),
+			}
+			.into(),
+			LalrError::ExtraToken { token } => ForgeErrorKind::ExpectedEof {
+				token: token.1,
+				span: (token.0, token.2),
+			}
+			.into(),
+			LalrError::User { error } => ForgeErrorKind::Custom(format!("{}", error)).into(),
+		}
+	}
+}
+
 pub type ForgeResult<'a, T> = Result<T, ForgeError<'a>>;
 
 pub fn print_unexpected_token<'a>(
@@ -134,79 +203,178 @@ 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);
+pub fn print_forge_error<'a>(source: &'a str, fe: &'a ForgeError) {
+	eprintln!("{}", format_forge_error(source, fe));
+}
+
+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!(
+				"{}",
+				SourcePrinter(
+					source,
+					format!("{}", found.token_type),
+					HighlightConfig {
+						line: found.position.location_line() as usize,
+						column: found.position.get_column(),
+						highlight_len: found.token_type.len(),
+					},
+					Some(format!(
+						"| Found {}, expected one of {}",
+						found.token_type,
+						expected
+							.tokens()
+							.collect::<Vec<&str>>()
+							.as_slice()
+							.join(", ")
+					))
+				),
+			)
+		}
+		ForgeErrorKind::InvalidToken { location } => {
+			let (line, column) = offset_to_line_column(source, *location);
+			format!(
+				"{}",
+				SourcePrinter(
+					source,
+					String::from("invalid token"),
+					HighlightConfig {
+						line,
+						column,
+						highlight_len: 1,
+					},
+					None,
+				)
+			)
+		}
+		ForgeErrorKind::UnexpectedEof { expected } => {
+			format!(
+				"| Unexpected end of file, expected one of {}",
+				expected.as_slice().join(", ")
+			)
+		}
+		ForgeErrorKind::UnrecognizedToken {
+			token,
+			span,
+			expected,
+		} => {
+			let (line, column) = offset_to_line_column(source, span.0);
+			format!(
+				"{}",
+				SourcePrinter(
+					source,
+					format!("{}", token),
+					HighlightConfig {
+						line,
+						column,
+						highlight_len: token.len(),
+					},
+					Some(format!(
+						"| Found {}, expected one of {}",
+						token,
+						expected.as_slice().join(", ")
+					))
+				),
+			)
+		}
+		ForgeErrorKind::ExpectedEof { token, span } => {
+			let (line, column) = offset_to_line_column(source, span.0);
+			format!(
+				"{}",
+				SourcePrinter(
+					source,
+					format!("{}", token),
+					HighlightConfig {
+						line,
+						column,
+						highlight_len: token.len(),
+					},
+					Some(format!("| Expected EOF, found {}", token))
+				),
+			)
+		}
+		ForgeErrorKind::Custom(msg) => format!("| {}", msg),
+	}
+}
+
+#[derive(Clone, Copy)]
+pub struct HighlightConfig {
+	line: usize,
+	column: usize,
+	highlight_len: usize,
+}
 
-impl<'a> Display for ErrorFormat<'a> {
+struct SourcePrinter<'a>(&'a str, String, HighlightConfig, Option<String>);
+impl<'a> Display for SourcePrinter<'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
-			)?;
-		}
+		highlight_source_snippet(self.0, &self.1, self.2, self.3.clone(), f)
+	}
+}
+
+pub fn highlight_source_snippet(
+	source: &str,
+	name: impl Display,
+	highlight: HighlightConfig,
+	additional: Option<String>,
+	f: &mut Formatter<'_>,
+) -> std::fmt::Result {
+	let HighlightConfig {
+		line,
+		column,
+		highlight_len,
+	} = highlight;
+
+	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, name)?;
+	if let Some(prev) = previous_line {
 		writeln!(
 			f,
 			"| [{:>width$}] {}",
-			line,
-			source_line,
+			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!["^"; highlight_len].join(""),
+	)?;
+	if let Some(next) = next_line {
 		writeln!(
 			f,
-			"| {} {}{}",
-			vec![" "; number_length + 2].join(""),
-			vec![" "; column - 1].join(""),
-			vec!["^"; token.token_type.len()].join(""),
+			"| [{:>width$}] {}",
+			line + 1,
+			next,
+			width = number_length
 		)?;
-		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"),
-		ForgeErrorKind::LexerError(err) => eprintln!("| {}", err),
-		ForgeErrorKind::UnexpectedToken { found, expected } => {
-			print_unexpected_token(source, found, expected)
-		}
 	}
-}
 
-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))
-		}
+	if let Some(extra) = additional {
+		writeln!(f, "|\n| {}", extra)
+	} else {
+		writeln!(f, "|")
 	}
 }
diff --git a/forge-script-lang/src/lexer/keywords.rs b/forge-script-lang/src/lexer/keywords.rs
index 892876e..62a92c9 100644
--- a/forge-script-lang/src/lexer/keywords.rs
+++ b/forge-script-lang/src/lexer/keywords.rs
@@ -66,7 +66,7 @@ pub fn token_if(input: Span) -> IResult<Span, ScriptToken> {
 
 pub fn token_null(input: Span) -> IResult<Span, ScriptToken> {
 	let (input, pos) = position(input)?;
-	let (input, _) = tag_ws("null", input)?;
+	let (input, _) = tag("null")(input)?;
 	Ok((
 		input,
 		ScriptToken {
diff --git a/forge-script-lang/src/lexer/mod.rs b/forge-script-lang/src/lexer/mod.rs
index 685a1bc..f0b0937 100644
--- a/forge-script-lang/src/lexer/mod.rs
+++ b/forge-script-lang/src/lexer/mod.rs
@@ -108,6 +108,24 @@ mod _lex {
 
 		Ok(tokens)
 	}
+
+	#[cfg(test)]
+	mod lex_test {
+		use super::*;
+		use test_case::test_case;
+
+		#[test_case("123" => matches Ok(ScriptTokenType::Integer(123)))]
+		#[test_case("0.123" => matches Ok(ScriptTokenType::Float(_)))]
+		#[test_case("null" => matches Ok(ScriptTokenType::Null))]
+		#[test_case("foobar" => matches Ok(ScriptTokenType::Identifier(_)))]
+		#[test_case("true" => matches Ok(ScriptTokenType::Boolean(true)))]
+		#[test_case("false" => matches Ok(ScriptTokenType::Boolean(false)))]
+		fn correct_lexing(inp: &str) -> Result<ScriptTokenType, ()> {
+			any_token(Span::new(inp))
+				.map_err(|_| ())
+				.map(|tok| tok.1.token_type)
+		}
+	}
 }
 
 use crate::parser::TokenSlice;
diff --git a/forge-script-lang/src/parser/ast.rs b/forge-script-lang/src/parser/ast.rs
index feab320..24690c2 100644
--- a/forge-script-lang/src/parser/ast.rs
+++ b/forge-script-lang/src/parser/ast.rs
@@ -6,13 +6,13 @@ use std::ops::Deref;
 pub trait AstNode {}
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct Program(pub ExpressionList);
 impl AstNode for Program {}
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct ExpressionList {
 	pub expressions: Vec<Expression>,
@@ -51,7 +51,7 @@ impl Deref for ExpressionList {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
@@ -144,7 +144,7 @@ impl Display for BinaryOp {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
@@ -158,14 +158,14 @@ pub enum VoidExpression {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct GroupedExpression {
 	pub inner: Box<ValueExpression>,
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
@@ -194,12 +194,12 @@ pub enum ValueExpression {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), 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(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct ConditionalLoop {
 	pub block: GuardedBlock,
@@ -222,7 +222,7 @@ impl ConditionalLoop {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct Conditional {
 	pub blocks: Vec<GuardedBlock>,
@@ -230,7 +230,7 @@ pub struct Conditional {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct GuardedBlock {
 	pub guard: Box<ValueExpression>,
@@ -238,7 +238,7 @@ pub struct GuardedBlock {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct Import {
 	pub source: String,
@@ -246,14 +246,14 @@ pub struct Import {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct Export {
 	pub items: IdentifierList,
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct Print {
 	pub expr: Box<ValueExpression>,
@@ -270,7 +270,7 @@ pub type IdentifierList = Vec<IdentifierNode>;
 pub type ParameterList = Vec<ValueExpression>;
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct Identifier(pub String);
 impl Display for Identifier {
@@ -282,7 +282,7 @@ impl Display for Identifier {
 /// Alias an identifier, to create a new way of referring to it
 /// IdentifierAlias(original, alias) => identifier "as" alias
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct IdentifierAlias(pub String, pub String);
 impl Display for IdentifierAlias {
@@ -292,7 +292,7 @@ impl Display for IdentifierAlias {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
@@ -328,7 +328,7 @@ impl IdentifierNode {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
@@ -353,7 +353,7 @@ impl Display for LiteralNode {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct Assignment {
 	pub ident: Identifier,
@@ -361,7 +361,7 @@ pub struct Assignment {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(
 	feature = "serde",
 	derive(serde::Serialize, serde::Deserialize),
@@ -373,7 +373,7 @@ pub enum DeclareIdent {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct DeclareFunction {
 	pub ident: Identifier,
@@ -382,14 +382,25 @@ pub struct DeclareFunction {
 }
 
 #[derive(Clone)]
-#[cfg_attr(feature = "debug-ast", derive(Debug))]
+#[cfg_attr(any(feature = "debug-ast", test), derive(Debug))]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct FunctionCall {
 	pub name: Identifier,
 	pub params: ParameterList,
 }
 
-macro_rules! impl_into_ast {
+macro_rules! impl_into_void_expr {
+	($($type: ty => $variant: expr),+) => {
+		$(
+		impl From<$type> for VoidExpression {
+			fn from(other: $type) -> Self {
+				$variant(other)
+			}
+		}
+		)+
+	};
+}
+macro_rules! impl_into_value_expr {
     ($($type: ty => $variant: expr),+) => {
 		$(
 		impl From<$type> for ValueExpression {
@@ -401,7 +412,7 @@ macro_rules! impl_into_ast {
 	};
 }
 
-impl_into_ast!(
+impl_into_value_expr!(
 	GroupedExpression => ValueExpression::Grouped,
 	ExpressionList => ValueExpression::Block,
 	LiteralNode => ValueExpression::Literal,
@@ -413,3 +424,10 @@ impl_into_ast!(
 	DeclareFunction => ValueExpression::DeclareFunction,
 	TypeofValue => ValueExpression::Typeof
 );
+
+impl_into_void_expr!(
+	Print => VoidExpression::Print,
+	ConditionalLoop => VoidExpression::ConditionLoop,
+	Import => VoidExpression::Import,
+	Export => VoidExpression::Export
+);
diff --git a/forge-script-lang/src/parser/forge_script.lalrpop b/forge-script-lang/src/parser/forge_grammar.lalrpop
similarity index 51%
rename from forge-script-lang/src/parser/forge_script.lalrpop
rename to forge-script-lang/src/parser/forge_grammar.lalrpop
index 06cb127..1066ca2 100644
--- a/forge-script-lang/src/parser/forge_script.lalrpop
+++ b/forge-script-lang/src/parser/forge_grammar.lalrpop
@@ -4,17 +4,78 @@ use crate::runtime::numbers::Number;
 
 grammar<'a>;
 
+pub Program: Program = {
+    ExpressionList => Program(<>)
+};
+
+Block: Option<ExpressionList> = {
+    "{" <ExpressionList?> "}" => <>,
+};
+
+ExpressionList: ExpressionList = {
+    <mut v:(<Expression> ";")+> <e:Expression?> => {
+        match e {
+            Some(val) => {
+                v.push(val);
+                ExpressionList {
+                    expressions: v,
+                    is_void: false,
+                }
+            },
+            None => {
+                ExpressionList {
+                    expressions: v,
+                    is_void: true,
+                }
+            },
+        }
+    }
+};
+
 pub Expression: Expression = {
     ValueExpression => Expression::Value(<>),
-}
+    VoidExpression => Expression::Void(<>),
+};
+
+VoidExpression: VoidExpression = {
+    "print" <expr:ValueExpression> => Print { expr: Box::new(expr) }.into(),
+    "import" "{" <items:IdentifierList> "}" "from" <source:StringValue> => Import { source, items }.into(),
+    "export" "{" <items:IdentifierList> "}" => Export { items }.into(),
+};
 
 ValueExpression: ValueExpression = {
     #[precedence(level="1")]
     Literal => ValueExpression::Literal(<>),
     #[precedence(level="1")]
+    "(" <expr:ValueExpression> ")" => GroupedExpression { inner: Box::new(expr) }.into(),
+
+    #[precedence(level="2")] #[assoc(side="right")]
     "typeof" <expr:ValueExpression> => TypeofValue(Box::new(expr)).into(),
+    #[precedence(level="2")] #[assoc(side="right")]
+    "-" <expr:ValueExpression> => ValueExpression::Unary { operand: Box::new(expr), operator: UnaryOp::Negate },
+    #[precedence(level="2")] #[assoc(side="right")]
+    "!" <expr:ValueExpression> => ValueExpression::Unary { operand: Box::new(expr), operator: UnaryOp::Not },
 
+    #[precedence(level = "3")]
+    IfStatement => <>.into(),
+
+    #[precedence(level="4")] #[assoc(side="left")]
+    <lhs:ValueExpression> "*" <rhs:ValueExpression> => {
+        ValueExpression::Binary {
+            lhs: Box::new(lhs),
+            rhs: Box::new(rhs),
+            operator: BinaryOp::Multiply,
+        }
+    },
     #[precedence(level="4")] #[assoc(side="left")]
+    <lhs:ValueExpression> "/" <rhs:ValueExpression> => {
+        ValueExpression::Binary {
+            lhs: Box::new(lhs),
+            rhs: Box::new(rhs),
+            operator: BinaryOp::Divide,
+        }
+    },
+    #[precedence(level="6")] #[assoc(side="left")]
     <lhs:ValueExpression> "+" <rhs:ValueExpression> => {
         ValueExpression::Binary {
             lhs: Box::new(lhs),
@@ -22,7 +83,7 @@ ValueExpression: ValueExpression = {
             operator: BinaryOp::Add,
         }
     },
-    #[precedence(level="4")] #[assoc(side="left")]
+    #[precedence(level="6")] #[assoc(side="left")]
     <lhs:ValueExpression> "-" <rhs:ValueExpression> => {
         ValueExpression::Binary {
             lhs: Box::new(lhs),
@@ -30,7 +91,49 @@ ValueExpression: ValueExpression = {
             operator: BinaryOp::Subtract,
         }
     },
-}
+
+    #[precedence(level="1")]
+    Identifier => <>.into()
+};
+
+IfStatement: Conditional = {
+    BareIfStatement => Conditional { blocks: vec![<>], fallback: None },
+    <fi: BareIfStatement> "else" <bl:Block> => Conditional { blocks: vec![fi], fallback: bl },
+    <fi:BareIfStatement> "else" <ls:IfStatement> => {
+        let mut ls = ls;
+        let mut new = vec![fi];
+        new.append(&mut ls.blocks);
+        Conditional {
+            blocks: new,
+            fallback: ls.fallback,
+        }
+    }
+};
+
+BareIfStatement: GuardedBlock = {
+    "if" <guard:ValueExpression> <bl:Block> => {
+        GuardedBlock {
+            block: bl.unwrap_or_default(),
+            guard: Box::new(guard),
+        }
+    }
+};
+
+ParameterList = ListOf<ValueExpression>;
+IdentifierList = ListOf<IdentifierNode>;
+
+IdentifierNode: IdentifierNode = {
+    IdentifierAlias => IdentifierNode::Alias(<>),
+    Identifier => IdentifierNode::Direct(<>),
+};
+
+IdentifierAlias: IdentifierAlias = {
+    <base:Identifier> "as" <alias:Identifier> => IdentifierAlias(base.0, alias.0),
+};
+
+Identifier: Identifier = {
+    "identifier" => Identifier(String::from(<>))
+};
 
 Literal: LiteralNode = {
     "boolean" => LiteralNode::Boolean(<>),
@@ -39,12 +142,22 @@ Literal: LiteralNode = {
     "integer" => LiteralNode::Number(Number::Integer(<>)),
     "string" => LiteralNode::String(String::from(<>)),
     "owned_string" => LiteralNode::String(<>),
-}
+};
 
 StringValue: String = {
     "string" => String::from(<>),
     "owned_string" => <>,
-}
+};
+
+ListOf<T>: Vec<T> = {
+    <mut v:(<T> ",")+> <e:T?> => match e {
+        None => v,
+        Some(e) => {
+            v.push(e);
+            v
+        }
+    }
+};
 
 extern {
     type Location = usize;
diff --git a/forge-script-lang/src/parser/forge_script.rs b/forge-script-lang/src/parser/forge_script.rs
new file mode 100644
index 0000000..dc9abcc
--- /dev/null
+++ b/forge-script-lang/src/parser/forge_script.rs
@@ -0,0 +1,71 @@
+// use lalrpop_util::lalrpop_mod;
+// lalrpop_mod!(pub forge_script);
+
+use crate::error::ForgeResult;
+use crate::lexer::{script_to_tokens, ScriptTokenType};
+use crate::parser::ast::{Expression, Program};
+use crate::TokenError;
+use peg::Parse;
+
+pub type InputSpan<'a, Loc, Tok> = Result<(Loc, Tok, Loc), TokenError<'a>>;
+type ExprSpan<'a> = InputSpan<'a, usize, ScriptTokenType<'a>>;
+
+macro_rules! export_grammar_fn {
+	($name:ident = $output:ty => $part: tt) => {
+		pub fn $name(source: &str) -> ForgeResult<$output> {
+			let tokens = script_to_tokens(source)?
+				.iter()
+				.map(|tok| {
+					Ok((
+						tok.position.start(),
+						tok.token_type.clone(),
+						tok.position.start() + tok.position.len(),
+					))
+				})
+				.collect::<Vec<ExprSpan>>();
+
+			let value =
+				super::forge_grammar::$part::new().parse::<ExprSpan, Vec<ExprSpan>>(tokens)?;
+
+			Ok(value)
+		}
+	};
+}
+
+export_grammar_fn!(parse_program = Program => ProgramParser);
+export_grammar_fn!(parse_expression = Expression => ExpressionParser);
+
+// pub fn parse_expression(source: &str) -> ForgeResult<Expression> {
+// 	let tokens = script_to_tokens(source)?
+// 		.iter()
+// 		.map(|tok| {
+// 			Ok((
+// 				tok.position.start(),
+// 				tok.token_type.clone(),
+// 				tok.position.start() + tok.position.len(),
+// 			))
+// 		})
+// 		.collect::<Vec<ExprSpan>>();
+//
+// 	let value =
+// 		super::forge_grammar::ExpressionParser::new().parse::<ExprSpan, Vec<ExprSpan>>(tokens)?;
+//
+// 	Ok(value)
+// }
+
+#[cfg(test)]
+mod grammar_test {
+	use super::parse_expression;
+	use crate::error::ForgeResult;
+	use crate::parse::ast::Expression;
+	use test_case::test_case;
+
+	#[test_case("123" => matches Ok(_) ; "Parse literal number")]
+	#[test_case(r#""Basic String""# => matches Ok(_) ; "Parse literal string")]
+	#[test_case("false" => matches Ok(_) ; "Parse literal false")]
+	#[test_case("true" => matches Ok(_) ; "Parse literal true")]
+	#[test_case("null" => matches Ok(_) ; "Parse literal null")]
+	fn expression_parsing<'a>(prog: &'a str) -> ForgeResult<'a, Expression> {
+		parse_expression(prog)
+	}
+}
diff --git a/forge-script-lang/src/parser/lr_grammar.rs b/forge-script-lang/src/parser/lr_grammar.rs
deleted file mode 100644
index c7d0e39..0000000
--- a/forge-script-lang/src/parser/lr_grammar.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-// use lalrpop_util::lalrpop_mod;
-// lalrpop_mod!(pub forge_script);
-
-use crate::lexer::{ScriptToken, ScriptTokenType};
-use crate::TokenError;
-use peg::Parse;
-use std::slice::Iter;
-
-pub struct TokenInput<'a>(std::slice::Iter<'a, ScriptToken<'a>>);
-
-impl<'a> From<Iter<'a, ScriptToken<'a>>> for TokenInput<'a> {
-	fn from(value: Iter<'a, ScriptToken<'a>>) -> Self {
-		TokenInput(value)
-	}
-}
-
-pub type InputSpan<'a, Loc, Tok> = Result<(Loc, Tok, Loc), TokenError<'a>>;
-
-impl<'a> Iterator for TokenInput<'a> {
-	type Item = InputSpan<'a, usize, ScriptTokenType<'a>>;
-
-	fn next(&mut self) -> Option<Self::Item> {
-		self.0.next().map(|tok| {
-			Ok((
-				tok.position.start(),
-				tok.token_type.clone(),
-				tok.position.start() + tok.position.len(),
-			))
-		})
-	}
-}
-
-#[cfg(test)]
-mod grammar_test {
-	use crate::lexer::{script_to_tokens, ScriptTokenType};
-	use crate::parser::lr_grammar::{InputSpan, TokenInput};
-
-	#[test]
-	fn basic_parsing() {
-		let code = script_to_tokens("4 + 4").expect("Failed to parse");
-		crate::parser::forge_script::ExpressionParser::new()
-			.parse::<InputSpan<usize, ScriptTokenType>, TokenInput>(code.iter().into())
-			.expect("Failed to boop");
-	}
-}
diff --git a/forge-script-lang/src/parser/mod.rs b/forge-script-lang/src/parser/mod.rs
index 2745c9f..03f85bf 100644
--- a/forge-script-lang/src/parser/mod.rs
+++ b/forge-script-lang/src/parser/mod.rs
@@ -1,11 +1,11 @@
 pub mod ast;
 mod atoms;
+mod forge_script;
 mod grammar;
-mod lr_grammar;
 #[cfg(test)]
 mod test_suite;
 
-mod forge_script;
+pub(crate) mod forge_grammar;
 
 use crate::error::{ForgeError, ForgeErrorKind, ForgeResult};
 use crate::print_forge_error;
@@ -15,44 +15,47 @@ pub fn slice<'a>(toks: &'a [crate::lexer::ScriptToken]) -> TokenSlice<'a> {
 	TokenSlice(toks)
 }
 
-pub fn parse_expression(expr: &str) -> ForgeResult<ast::Expression> {
-	let tokens = crate::lexer::script_to_tokens(expr)?;
-	let result = match grammar::expression(&TokenSlice(tokens.as_slice())) {
-		Ok(expr) => Ok(expr),
-		Err(parse_error) => {
-			let bad_token = &tokens[parse_error.location];
-			Err(ForgeError {
-				kind: ForgeErrorKind::UnexpectedToken {
-					found: bad_token.clone(),
-					expected: parse_error.expected,
-				},
-			})
-		}
-	};
+pub use forge_script::parse_expression;
+pub use forge_script::parse_program;
 
-	result.map_err(|e| {
-		print_forge_error(expr, &e);
-		e
-	})
-}
+// pub fn parse_expression(expr: &str) -> ForgeResult<ast::Expression> {
+// 	let tokens = crate::lexer::script_to_tokens(expr)?;
+// 	let result = match grammar::expression(&TokenSlice(tokens.as_slice())) {
+// 		Ok(expr) => Ok(expr),
+// 		Err(parse_error) => {
+// 			let bad_token = &tokens[parse_error.location];
+// 			Err(ForgeError {
+// 				kind: ForgeErrorKind::UnexpectedToken {
+// 					found: bad_token.clone(),
+// 					expected: parse_error.expected,
+// 				},
+// 			})
+// 		}
+// 	};
+//
+// 	result.map_err(|e| {
+// 		print_forge_error(expr, &e);
+// 		e
+// 	})
+// }
 
-pub fn parse_program(prog: &str) -> ForgeResult<ast::Program> {
-	let tokens = crate::lexer::script_to_tokens(prog)?;
-	let result = match grammar::program(&TokenSlice(tokens.as_slice())) {
-		Ok(prog) => Ok(prog.clone()),
-		Err(parse_error) => {
-			let bad_token = &tokens[parse_error.location];
-			Err(ForgeError {
-				kind: ForgeErrorKind::UnexpectedToken {
-					found: bad_token.clone(),
-					expected: parse_error.expected,
-				},
-			})
-		}
-	};
-
-	result.map_err(|e| {
-		print_forge_error(prog, &e);
-		e
-	})
-}
+// pub fn parse_program(prog: &str) -> ForgeResult<ast::Program> {
+// 	let tokens = crate::lexer::script_to_tokens(prog)?;
+// 	let result = match grammar::program(&TokenSlice(tokens.as_slice())) {
+// 		Ok(prog) => Ok(prog.clone()),
+// 		Err(parse_error) => {
+// 			let bad_token = &tokens[parse_error.location];
+// 			Err(ForgeError {
+// 				kind: ForgeErrorKind::UnexpectedToken {
+// 					found: bad_token.clone(),
+// 					expected: parse_error.expected,
+// 				},
+// 			})
+// 		}
+// 	};
+//
+// 	result.map_err(|e| {
+// 		print_forge_error(prog, &e);
+// 		e
+// 	})
+// }
diff --git a/forge-script-lang/src/utilities.rs b/forge-script-lang/src/utilities.rs
index 6c5c1c2..0233d1a 100644
--- a/forge-script-lang/src/utilities.rs
+++ b/forge-script-lang/src/utilities.rs
@@ -42,3 +42,23 @@ macro_rules! deref_as {
 pub trait Clown<Target> {
 	fn clown(&self) -> Target;
 }
+
+type LineNum = usize;
+type ColumnNum = usize;
+
+pub fn offset_to_line_column(source: &str, offset: usize) -> (LineNum, ColumnNum) {
+	let mut remaining = offset;
+	let mut current_line = 0;
+	for line in source.lines() {
+		current_line += 1;
+		if remaining < line.len() {
+			return (current_line, remaining + 1);
+		}
+		remaining -= line.len();
+	}
+
+	(
+		current_line,
+		source.lines().last().map(|l| l.len()).unwrap_or(0),
+	)
+}
-- 
GitLab