summary refs log tree commit diff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/brainf_lexer/Cargo.toml8
-rw-r--r--crates/brainf_lexer/src/lexer.rs75
-rw-r--r--crates/brainf_lexer/src/lib.rs105
-rw-r--r--crates/brainf_rs/Cargo.toml22
-rw-r--r--crates/brainf_rs/src/engine.rs98
-rw-r--r--crates/brainf_rs/src/executor.rs131
-rw-r--r--crates/brainf_rs/src/lib.rs207
-rw-r--r--crates/brainf_rs/src/main.rs128
-rw-r--r--crates/brainf_rs/src/parser.rs169
-rw-r--r--crates/brainf_rs/src/utility.rs50
10 files changed, 993 insertions, 0 deletions
diff --git a/crates/brainf_lexer/Cargo.toml b/crates/brainf_lexer/Cargo.toml
new file mode 100644
index 0000000..58be7a5
--- /dev/null
+++ b/crates/brainf_lexer/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "brainf_lexer"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+logos = "0.13.0"
+thiserror = "1.0.44"
diff --git a/crates/brainf_lexer/src/lexer.rs b/crates/brainf_lexer/src/lexer.rs
new file mode 100644
index 0000000..b95cd87
--- /dev/null
+++ b/crates/brainf_lexer/src/lexer.rs
@@ -0,0 +1,75 @@
+//! Lexer implementation using logos.
+
+#![expect(clippy::indexing_slicing)]
+
+use logos::{Lexer, Logos};
+
+/// List of operator codes for the lexer
+/// Note: Any input symbol that is not in this list is a comment
+
+fn loop_callback(lex: &Lexer<Token>) -> (usize, usize) {
+	let span = lex.span();
+
+	(span.start, span.len())
+}
+
+/// List of Tokens for the lexer
+/// Note: Any input symbol that is not in this list is a comment
+#[derive(Clone, Copy, Debug, Logos, PartialEq, Eq)]
+#[logos(skip r"[^<>+\-.,\[\]]+")]
+pub enum Token {
+	/// `>`
+	///
+	/// Increment the data pointer by one (to point to the next cell to the
+	/// right).
+	#[token(">")]
+	IncrementPointer,
+
+	/// `<`
+	///
+	/// Decrement the data pointer by one (to point to the next cell to the
+	/// left).
+	#[token("<")]
+	DecrementPointer,
+
+	/// `+`
+	///
+	/// Increment the byte at the data pointer by one.
+	#[token("+")]
+	IncrementByte,
+
+	/// `-`
+	///
+	/// Decrement the byte at the data pointer by one.
+	#[token("-")]
+	DecrementByte,
+
+	/// `.`
+	///
+	/// Output the byte at the data pointer.
+	#[token(".")]
+	OutputByte,
+
+	/// `,`
+	///
+	/// Accept one byte of input, storing its value in the byte at the data
+	/// pointer.
+	#[token(",")]
+	InputByte,
+
+	/// `[`
+	///
+	/// If the byte at the data pointer is zero, then instead of moving the
+	/// instruction pointer forward to the next command, jump it forward to the
+	/// command after the matching ] command.
+	#[token("[", loop_callback)]
+	StartLoop((usize, usize)),
+
+	/// `]`
+	///
+	/// If the byte at the data pointer is nonzero, then instead of moving the
+	/// instruction pointer forward to the next command, jump it back to the
+	/// command after the matching [ command.
+	#[token("]", loop_callback)]
+	EndLoop((usize, usize)),
+}
diff --git a/crates/brainf_lexer/src/lib.rs b/crates/brainf_lexer/src/lib.rs
new file mode 100644
index 0000000..7f6e5be
--- /dev/null
+++ b/crates/brainf_lexer/src/lib.rs
@@ -0,0 +1,105 @@
+#![feature(lint_reasons)]
+#![deny(clippy::complexity)]
+#![deny(clippy::nursery)]
+#![deny(clippy::pedantic)]
+#![deny(clippy::perf)]
+#![deny(clippy::suspicious)]
+#![deny(clippy::alloc_instead_of_core)]
+#![deny(clippy::as_underscore)]
+#![deny(clippy::clone_on_ref_ptr)]
+#![deny(clippy::create_dir)]
+#![warn(clippy::dbg_macro)]
+#![deny(clippy::default_numeric_fallback)]
+#![deny(clippy::default_union_representation)]
+#![deny(clippy::deref_by_slicing)]
+#![deny(clippy::empty_structs_with_brackets)]
+#![deny(clippy::exit)]
+#![deny(clippy::expect_used)]
+#![deny(clippy::filetype_is_file)]
+#![deny(clippy::fn_to_numeric_cast)]
+#![deny(clippy::format_push_string)]
+#![deny(clippy::get_unwrap)]
+#![deny(clippy::if_then_some_else_none)]
+#![allow(
+	clippy::implicit_return,
+	reason = "returns should be done implicitly, not explicitly"
+)]
+#![deny(clippy::indexing_slicing)]
+#![deny(clippy::large_include_file)]
+#![deny(clippy::let_underscore_must_use)]
+#![deny(clippy::lossy_float_literal)]
+#![deny(clippy::map_err_ignore)]
+#![deny(clippy::mem_forget)]
+#![deny(clippy::missing_docs_in_private_items)]
+#![deny(clippy::missing_trait_methods)]
+#![deny(clippy::mod_module_files)]
+#![deny(clippy::multiple_inherent_impl)]
+#![deny(clippy::mutex_atomic)]
+#![deny(clippy::needless_return)]
+#![deny(clippy::non_ascii_literal)]
+#![deny(clippy::panic_in_result_fn)]
+#![deny(clippy::pattern_type_mismatch)]
+#![deny(clippy::rc_buffer)]
+#![deny(clippy::rc_mutex)]
+#![deny(clippy::rest_pat_in_fully_bound_structs)]
+#![deny(clippy::same_name_method)]
+#![deny(clippy::separated_literal_suffix)]
+#![deny(clippy::str_to_string)]
+#![deny(clippy::string_add)]
+#![deny(clippy::string_slice)]
+#![deny(clippy::string_to_string)]
+#![allow(
+	clippy::tabs_in_doc_comments,
+	reason = "tabs are preferred for this project"
+)]
+#![deny(clippy::try_err)]
+#![deny(clippy::undocumented_unsafe_blocks)]
+#![deny(clippy::unnecessary_self_imports)]
+#![deny(clippy::unneeded_field_pattern)]
+#![deny(clippy::unwrap_in_result)]
+#![deny(clippy::unwrap_used)]
+#![warn(clippy::use_debug)]
+#![deny(clippy::verbose_file_reads)]
+#![deny(clippy::wildcard_dependencies)]
+#![deny(clippy::wildcard_enum_match_arm)]
+#![deny(missing_copy_implementations)]
+#![deny(missing_debug_implementations)]
+#![deny(missing_docs)]
+#![deny(single_use_lifetimes)]
+#![deny(unsafe_code)]
+#![deny(unused)]
+
+//! # `brainf_lexer`
+//!
+//! Implementation of a Brainfuck lexer in Rust.
+
+mod lexer;
+
+pub use lexer::Token;
+use logos::Logos;
+use thiserror::Error;
+
+/// Error type for lexer.
+#[derive(Clone, Copy, Debug, Error)]
+pub enum Error {
+	/// Logos was unable to lex part of the input.
+	#[error("lexer was unable to lex input")]
+	LexingError,
+}
+
+/// Lexes the Brainfuck input, returning a Vec of Tokens.
+///
+/// # Errors
+///
+/// This function will return an error if the lexer is unable to lex one or more
+/// of the input characters.
+pub fn lex(input: &str) -> Result<Vec<Token>, Error> {
+	lexer::Token::lexer(input).try_fold(Vec::new(), |mut arr, result| {
+		result
+			.map_or(Err(()), |token| {
+				arr.push(token);
+				Ok(arr)
+			})
+			.map_err(|_err| Error::LexingError)
+	})
+}
diff --git a/crates/brainf_rs/Cargo.toml b/crates/brainf_rs/Cargo.toml
new file mode 100644
index 0000000..5520652
--- /dev/null
+++ b/crates/brainf_rs/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "brainf_rs"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+brainf_lexer = { path = "../brainf_lexer" }
+byteorder = { optional = true, version = "1.4.3" }
+clap = { features = ["derive"], version = "4.3.21" }
+num-traits = "0.2.16"
+fs-err = "2.9.0"
+logos = "0.13.0"
+miette = { features = ["fancy"], version = "5.10.0" }
+thiserror = "1.0.44"
+widestring = { default-features = false, optional = true, version = "1.0.2" }
+
+[features]
+default = ["engine-u16", "engine-u32", "utilities"]
+bigint-engine = ["dep:byteorder", "dep:widestring"]
+engine-u16 = ["bigint-engine"]
+engine-u32 = ["bigint-engine"]
+utilities = []
diff --git a/crates/brainf_rs/src/engine.rs b/crates/brainf_rs/src/engine.rs
new file mode 100644
index 0000000..e60acaa
--- /dev/null
+++ b/crates/brainf_rs/src/engine.rs
@@ -0,0 +1,98 @@
+//! Executor engine implementation for Brainfuck interpreter.
+//!
+//! This predominantly allows implementation of a [`u16`] executor.
+
+#[cfg(feature = "bigint-engine")]
+use byteorder::{NativeEndian, ReadBytesExt};
+use num_traits::{One, Unsigned, WrappingAdd, WrappingSub, Zero};
+use thiserror::Error;
+
+use crate::executor;
+
+/// Error type for engine errors
+#[derive(Debug, Error)]
+pub enum Error {
+	/// Engine ran into an io Error
+	#[error(transparent)]
+	Io(#[from] std::io::Error),
+
+	/// Utf16 error from widestring
+	#[error("could not convert byte to Utf16Str")]
+	Utf16,
+
+	/// Utf32 error from widestring
+	#[error("could not convert byte to Utf32Str")]
+	Utf32,
+}
+
+/// Generic engine implementation for the Brainfuck interpreter.
+pub trait Engine {
+	/// Inner type of the Tape.
+	type TapeInner: Clone + Copy + Unsigned + WrappingAdd + WrappingSub + One + Zero;
+
+	/// Read one byte from stdin.
+	///
+	/// # Errors
+	///
+	/// This function will return an error if it is unable to read from stdin,
+	/// or if it indexes out of bounds.
+	fn read_byte() -> Result<Self::TapeInner, Error>;
+
+	/// Write the provided byte to stdout.
+	///
+	/// # Errors
+	///
+	/// This function will return an error if it is unable to write a byte to
+	/// stdout.
+	fn write_byte(byte: Self::TapeInner) -> Result<(), Error>;
+}
+
+impl Engine for executor::U8 {
+	type TapeInner = u8;
+
+	fn read_byte() -> Result<u8, Error> {
+		Ok(std::io::stdin().read_u8()?)
+	}
+
+	fn write_byte(byte: u8) -> Result<(), Error> {
+		print!("{}", char::from(byte));
+
+		Ok(())
+	}
+}
+
+#[cfg(feature = "engine-u16")]
+impl Engine for executor::U16 {
+	type TapeInner = u16;
+
+	fn read_byte() -> Result<u16, Error> {
+		Ok(std::io::stdin().read_u16::<NativeEndian>()?)
+	}
+
+	fn write_byte(byte: u16) -> Result<(), Error> {
+		print!(
+			"{}",
+			widestring::Utf16Str::from_slice(&[byte]).map_err(|_err| Error::Utf16)?
+		);
+
+		Ok(())
+	}
+}
+
+#[cfg(feature = "engine-u32")]
+impl Engine for executor::U32 {
+	type TapeInner = u32;
+
+	fn read_byte() -> Result<u32, Error> {
+		Ok(std::io::stdin().read_u32::<NativeEndian>()?)
+	}
+
+	fn write_byte(byte: u32) -> Result<(), Error> {
+		print!(
+			"{}",
+			widestring::Utf32Str::from_slice(&[byte]).map_err(|_err| Error::Utf32)?
+		);
+
+		Ok(())
+	}
+}
diff --git a/crates/brainf_rs/src/executor.rs b/crates/brainf_rs/src/executor.rs
new file mode 100644
index 0000000..c5fff93
--- /dev/null
+++ b/crates/brainf_rs/src/executor.rs
@@ -0,0 +1,131 @@
+//! Executor implementation for Brainfuck.
+
+use miette::Diagnostic;
+use num_traits::{One, WrappingAdd, WrappingSub, Zero};
+use thiserror::Error;
+
+use crate::{engine::Engine, parser::Instruction};
+
+/// Runtime errors that can occur in brainfuck executor.
+#[derive(Debug, Diagnostic, Error)]
+pub enum Error {
+	/// Brainfuck engine ran into an error at runtime.
+	#[error(transparent)]
+	Engine(#[from] crate::engine::Error),
+
+	/// Brainfuck code performed an out of bounds index on the tape during
+	/// runtime.
+	#[error("tape indexed out of bounds, attempted index at `{0}`")]
+	IndexOutOfBounds(usize),
+}
+
+/// Struct for executor implementation, allows u8 Engine to be implemented.
+#[derive(Clone, Copy, Debug)]
+pub struct U8;
+
+/// Struct for executor implementation, allows u16 Engine to be implemented.
+#[derive(Clone, Copy, Debug)]
+#[cfg(feature = "engine-u16")]
+pub struct U16;
+
+/// Struct for executor implementation, allows u32 Engine to be implemented.
+#[derive(Clone, Copy, Debug)]
+#[cfg(feature = "engine-u32")]
+pub struct U32;
+
+/// Trait that must be implemented by all executors.
+pub trait Executor<E: Engine> {
+	/// Executes the provided instruction set, utilising the provided tape.
+	///
+	/// # Errors
+	///
+	/// This function will return an error if the Brainfuck code indexes out of
+	/// bounds of the tape, or if the executor cannot read an input byte from
+	/// stdin.
+	fn execute(
+		instructions: &[Instruction],
+		tape: &mut [E::TapeInner],
+		data_pointer: &mut usize,
+	) -> Result<(), Error>;
+}
+
+impl<E> Executor<E> for E
+where
+	E: Engine,
+{
+	#[inline]
+	fn execute(
+		instructions: &[Instruction],
+		tape: &mut [<E as Engine>::TapeInner],
+		data_pointer: &mut usize,
+	) -> Result<(), Error> {
+		execute::<E>(instructions, tape, data_pointer)
+	}
+}
+
+/// Executes the provided instruction set, utilising the provided tape. This
+/// function allows specifying the Brainfuck engine implementation per call.
+///
+/// # Errors
+///
+/// This function will return an error if the Brainfuck code indexes out of
+/// bounds of the tape, or if the executor cannot read an input byte from
+/// stdin.
+pub fn execute<E: Engine>(
+	instructions: &[Instruction],
+	tape: &mut [E::TapeInner],
+	data_pointer: &mut usize,
+) -> Result<(), Error> {
+	for instruction in instructions {
+		match *instruction {
+			Instruction::IncrementPointer => {
+				let tape_len: usize = tape.len() - 1;
+
+				if *data_pointer == tape_len {
+					*data_pointer = 0;
+				} else {
+					*data_pointer += 1;
+				}
+			}
+			Instruction::DecrementPointer => {
+				*data_pointer = match *data_pointer {
+					0 => tape.len() - 1,
+					_ => *data_pointer - 1,
+				};
+			}
+			Instruction::IncrementByte => match tape.get_mut(*data_pointer) {
+				Some(value) => *value = value.wrapping_add(&E::TapeInner::one()),
+				None => return Err(Error::IndexOutOfBounds(*data_pointer)),
+			},
+			Instruction::DecrementByte => match tape.get_mut(*data_pointer) {
+				Some(value) => *value = value.wrapping_sub(&E::TapeInner::one()),
+				None => return Err(Error::IndexOutOfBounds(*data_pointer)),
+			},
+			Instruction::OutputByte => {
+				E::write_byte(match tape.get(*data_pointer) {
+					Some(value) => *value,
+					None => return Err(Error::IndexOutOfBounds(*data_pointer)),
+				})?;
+			}
+			Instruction::InputByte => {
+				let input = E::read_byte()?;
+
+				match tape.get_mut(*data_pointer) {
+					Some(value) => *value = input,
+					None => return Err(Error::IndexOutOfBounds(*data_pointer)),
+				};
+			}
+			Instruction::Loop(ref instructions) => {
+				while match tape.get(*data_pointer) {
+					Some(value) => value,
+					None => return Err(Error::IndexOutOfBounds(*data_pointer)),
+				} != &E::TapeInner::zero()
+				{
+					execute::<E>(instructions, tape, data_pointer)?;
+				}
+			}
+		}
+	}
+
+	Ok(())
+}
diff --git a/crates/brainf_rs/src/lib.rs b/crates/brainf_rs/src/lib.rs
new file mode 100644
index 0000000..f5c8987
--- /dev/null
+++ b/crates/brainf_rs/src/lib.rs
@@ -0,0 +1,207 @@
+#![allow(incomplete_features)]
+#![feature(async_fn_in_trait)]
+#![feature(custom_inner_attributes)]
+#![feature(lint_reasons)]
+#![feature(never_type)]
+#![feature(test)]
+#![deny(clippy::complexity)]
+#![deny(clippy::nursery)]
+#![deny(clippy::pedantic)]
+#![deny(clippy::perf)]
+#![deny(clippy::suspicious)]
+#![deny(clippy::alloc_instead_of_core)]
+#![deny(clippy::as_underscore)]
+#![deny(clippy::clone_on_ref_ptr)]
+#![deny(clippy::create_dir)]
+#![warn(clippy::dbg_macro)]
+#![deny(clippy::default_numeric_fallback)]
+#![deny(clippy::default_union_representation)]
+#![deny(clippy::deref_by_slicing)]
+#![deny(clippy::empty_structs_with_brackets)]
+#![deny(clippy::exit)]
+#![deny(clippy::expect_used)]
+#![deny(clippy::filetype_is_file)]
+#![deny(clippy::fn_to_numeric_cast)]
+#![deny(clippy::format_push_string)]
+#![deny(clippy::get_unwrap)]
+#![deny(clippy::if_then_some_else_none)]
+#![allow(
+	clippy::implicit_return,
+	reason = "returns should be done implicitly, not explicitly"
+)]
+#![deny(clippy::indexing_slicing)]
+#![deny(clippy::large_include_file)]
+#![deny(clippy::let_underscore_must_use)]
+#![deny(clippy::lossy_float_literal)]
+#![deny(clippy::map_err_ignore)]
+#![deny(clippy::mem_forget)]
+#![deny(clippy::missing_docs_in_private_items)]
+#![deny(clippy::missing_trait_methods)]
+#![deny(clippy::mod_module_files)]
+#![deny(clippy::multiple_inherent_impl)]
+#![deny(clippy::mutex_atomic)]
+#![deny(clippy::needless_return)]
+#![deny(clippy::non_ascii_literal)]
+#![deny(clippy::panic_in_result_fn)]
+#![deny(clippy::pattern_type_mismatch)]
+#![deny(clippy::rc_buffer)]
+#![deny(clippy::rc_mutex)]
+#![deny(clippy::rest_pat_in_fully_bound_structs)]
+#![deny(clippy::same_name_method)]
+#![deny(clippy::separated_literal_suffix)]
+#![deny(clippy::str_to_string)]
+#![deny(clippy::string_add)]
+#![deny(clippy::string_slice)]
+#![deny(clippy::string_to_string)]
+#![allow(
+	clippy::tabs_in_doc_comments,
+	reason = "tabs are preferred for this project"
+)]
+#![deny(clippy::try_err)]
+#![deny(clippy::undocumented_unsafe_blocks)]
+#![deny(clippy::unnecessary_self_imports)]
+#![deny(clippy::unneeded_field_pattern)]
+#![deny(clippy::unwrap_in_result)]
+#![deny(clippy::unwrap_used)]
+#![warn(clippy::use_debug)]
+#![deny(clippy::verbose_file_reads)]
+#![deny(clippy::wildcard_dependencies)]
+#![deny(clippy::wildcard_enum_match_arm)]
+#![deny(missing_copy_implementations)]
+#![deny(missing_debug_implementations)]
+#![deny(missing_docs)]
+#![deny(single_use_lifetimes)]
+#![deny(unsafe_code)]
+#![deny(unused)]
+
+//! Brainfuck RS
+//!
+//! Implementation of a Brainfuck interpreter written in Rust.
+
+#[cfg(test)]
+extern crate test;
+
+mod engine;
+pub mod executor;
+pub mod parser;
+#[cfg(feature = "utilities")]
+pub mod utility;
+
+pub use brainf_lexer::{lex, Token};
+pub use executor::{U16 as ExecutorU16, U32 as ExecutorU32, U8 as ExecutorU8};
+use miette::Diagnostic;
+pub use parser::{parse, Instruction};
+use thiserror::Error;
+
+/// Top-level error type for Brainfuck interpreter.
+#[derive(Debug, Diagnostic, Error)]
+pub enum Error {
+	/// Error occurred when reading input from a file.
+	#[error(transparent)]
+	Io(#[from] std::io::Error),
+
+	/// Error occurred while lexing the input.
+	#[error(transparent)]
+	Lexer(#[from] brainf_lexer::Error),
+
+	/// An error that occurred while parsing Brainfuck code.
+	#[diagnostic(transparent)]
+	#[error(transparent)]
+	Parser(#[from] parser::Error),
+
+	/// An error that occurred during runtime.
+	#[error(transparent)]
+	Runtime(#[from] executor::Error),
+}
+
+#[cfg(test)]
+mod tests {
+	use test::Bencher;
+
+	use super::*;
+
+	#[test]
+	fn hello_world() -> Result<(), Error> {
+		let mut tape: Vec<u8> = vec![0; 1024];
+
+		utility::execute_from_file::<executor::U8>("./test_programs/hello_world.bf", &mut tape)?;
+
+		Ok(())
+	}
+
+	#[test]
+	fn hello_world_u16() -> Result<(), Error> {
+		let mut tape: Vec<u16> = vec![0; 1024];
+
+		utility::execute_from_file::<executor::U16>("./test_programs/hello_world.bf", &mut tape)?;
+
+		Ok(())
+	}
+
+	#[test]
+	fn hello_world_from_hell() -> Result<(), Error> {
+		let mut tape: Vec<u16> = vec![0; 1024];
+
+		utility::execute_from_file::<executor::U16>(
+			"./test_programs/hello_world_from_hell.bf",
+			&mut tape,
+		)?;
+
+		Ok(())
+	}
+
+	#[bench]
+	fn hello_world_from_hell_bench_u8(b: &mut Bencher) {
+		b.iter(|| {
+			let mut tape: Vec<u8> = vec![0; 1024];
+
+			#[allow(clippy::expect_used)]
+			utility::execute_from_file::<executor::U8>(
+				"./test_programs/hello_world_from_hell.bf",
+				&mut tape,
+			)
+			.expect("failed to run");
+		});
+	}
+
+	#[bench]
+	fn hello_world_from_hell_bench_u16(b: &mut Bencher) {
+		b.iter(|| {
+			let mut tape: Vec<u16> = vec![0; 1024];
+
+			#[allow(clippy::expect_used)]
+			utility::execute_from_file::<executor::U16>(
+				"./test_programs/hello_world_from_hell.bf",
+				&mut tape,
+			)
+			.expect("failed to run");
+		});
+	}
+
+	#[bench]
+	fn hello_world_from_hell_bench_u32(b: &mut Bencher) {
+		b.iter(|| {
+			let mut tape: Vec<u32> = vec![0; 1024];
+
+			#[allow(clippy::expect_used)]
+			utility::execute_from_file::<executor::U32>(
+				"./test_programs/hello_world_from_hell.bf",
+				&mut tape,
+			)
+			.expect("failed to run");
+		});
+	}
+
+	#[test]
+	fn hello_world_short() -> Result<(), Error> {
+		let mut tape: Vec<u8> = vec![0; 1024];
+
+		utility::execute_from_str::<executor::U8>(
+			"--[+++++++<---->>-->+>+>+<<<<]<.>++++[-<++++>>->--<<]>>-.>--..>+.<<<.<<-.>>+>->>.\
+			 +++[.<]",
+			&mut tape,
+		)?;
+
+		Ok(())
+	}
+}
diff --git a/crates/brainf_rs/src/main.rs b/crates/brainf_rs/src/main.rs
new file mode 100644
index 0000000..13868a6
--- /dev/null
+++ b/crates/brainf_rs/src/main.rs
@@ -0,0 +1,128 @@
+#![allow(incomplete_features)]
+#![feature(async_fn_in_trait)]
+#![feature(custom_inner_attributes)]
+#![feature(lint_reasons)]
+#![feature(never_type)]
+#![deny(clippy::complexity)]
+#![deny(clippy::nursery)]
+#![deny(clippy::pedantic)]
+#![deny(clippy::perf)]
+#![deny(clippy::suspicious)]
+#![deny(clippy::alloc_instead_of_core)]
+#![deny(clippy::as_underscore)]
+#![deny(clippy::clone_on_ref_ptr)]
+#![deny(clippy::create_dir)]
+#![warn(clippy::dbg_macro)]
+#![deny(clippy::default_numeric_fallback)]
+#![deny(clippy::default_union_representation)]
+#![deny(clippy::deref_by_slicing)]
+#![deny(clippy::empty_structs_with_brackets)]
+#![deny(clippy::exit)]
+#![deny(clippy::filetype_is_file)]
+#![deny(clippy::fn_to_numeric_cast)]
+#![deny(clippy::format_push_string)]
+#![deny(clippy::get_unwrap)]
+#![deny(clippy::if_then_some_else_none)]
+#![allow(
+	clippy::implicit_return,
+	reason = "returns should be done implicitly, not explicitly"
+)]
+#![deny(clippy::indexing_slicing)]
+#![deny(clippy::large_include_file)]
+#![deny(clippy::let_underscore_must_use)]
+#![deny(clippy::lossy_float_literal)]
+#![deny(clippy::map_err_ignore)]
+#![deny(clippy::mem_forget)]
+#![deny(clippy::missing_docs_in_private_items)]
+#![deny(clippy::missing_trait_methods)]
+#![deny(clippy::mod_module_files)]
+#![deny(clippy::multiple_inherent_impl)]
+#![deny(clippy::mutex_atomic)]
+#![deny(clippy::needless_return)]
+#![deny(clippy::non_ascii_literal)]
+#![deny(clippy::panic_in_result_fn)]
+#![deny(clippy::pattern_type_mismatch)]
+#![deny(clippy::rc_buffer)]
+#![deny(clippy::rc_mutex)]
+#![deny(clippy::rest_pat_in_fully_bound_structs)]
+#![deny(clippy::same_name_method)]
+#![deny(clippy::separated_literal_suffix)]
+#![deny(clippy::str_to_string)]
+#![deny(clippy::string_add)]
+#![deny(clippy::string_slice)]
+#![deny(clippy::string_to_string)]
+#![allow(
+	clippy::tabs_in_doc_comments,
+	reason = "tabs are preferred for this project"
+)]
+#![deny(clippy::try_err)]
+#![deny(clippy::undocumented_unsafe_blocks)]
+#![deny(clippy::unnecessary_self_imports)]
+#![deny(clippy::unneeded_field_pattern)]
+#![deny(clippy::unwrap_in_result)]
+#![deny(clippy::unwrap_used)]
+#![warn(clippy::use_debug)]
+#![deny(clippy::verbose_file_reads)]
+#![deny(clippy::wildcard_dependencies)]
+#![deny(clippy::wildcard_enum_match_arm)]
+#![deny(missing_copy_implementations)]
+#![deny(missing_debug_implementations)]
+#![deny(missing_docs)]
+#![deny(single_use_lifetimes)]
+#![deny(unsafe_code)]
+#![deny(unused)]
+
+//! Brainfuck RS
+//!
+//! Brainfuck interpreter written in Rust
+
+use std::path::PathBuf;
+
+use brainf_rs::{
+	executor,
+	utility::{execute_from_file, execute_from_str},
+};
+use clap::{Parser, Subcommand};
+use miette::Context;
+
+/// Representation of command line application for the Brainfuck interpreter.
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+#[command(propagate_version = true)]
+struct App {
+	/// Subcommands supported by the Brainfuck interpreter
+	#[command(subcommand)]
+	command: Command,
+}
+
+/// Implementation of subcommands for the Brainfuck interpreter.
+#[derive(Subcommand)]
+enum Command {
+	/// Interprets Brainfuck code from a provided file.
+	File {
+		/// Path to the file to interpret with Brainfuck.
+		path: PathBuf,
+	},
+
+	/// Interprets Brainfuck code from a text input on the command line.
+	Text {
+		/// Text input to be interpreted as Brainfuck.
+		input: String,
+	},
+}
+
+fn main() -> miette::Result<()> {
+	let app = App::parse();
+
+	let mut tape = vec![0u8; 1024];
+
+	match app.command {
+		Command::File { ref path } => {
+			execute_from_file::<executor::U8>(path, &mut tape)
+				.wrap_err("when executing from file")?;
+		}
+		Command::Text { ref input } => execute_from_str::<executor::U8>(input, &mut tape)?,
+	};
+
+	Ok(())
+}
diff --git a/crates/brainf_rs/src/parser.rs b/crates/brainf_rs/src/parser.rs
new file mode 100644
index 0000000..c9b3246
--- /dev/null
+++ b/crates/brainf_rs/src/parser.rs
@@ -0,0 +1,169 @@
+//! Parser implementation for Brainfuck. Parses operator codes into instruction
+//! sets.
+
+use brainf_lexer::Token;
+use miette::{Diagnostic, SourceSpan};
+use thiserror::Error;
+
+/// Parsed instructions for Brainfuck.
+#[derive(Clone, Debug)]
+pub enum Instruction {
+	/// `>`
+	///
+	/// Increment the data pointer by one (to point to the next cell to the
+	/// right).
+	IncrementPointer,
+
+	/// `<`
+	///
+	/// Decrement the data pointer by one (to point to the next cell to the
+	/// left).
+	DecrementPointer,
+
+	/// `+`
+	///
+	/// Increment the byte at the data pointer by one.
+	IncrementByte,
+
+	/// `-`
+	///
+	/// Decrement the byte at the data pointer by one.
+	DecrementByte,
+
+	/// `.`
+	///
+	/// Output the byte at the data pointer.
+	OutputByte,
+
+	/// `,`
+	///
+	/// Accept one byte of input, storing its value in the byte at the data
+	/// pointer.
+	InputByte,
+
+	/// `[]`
+	///
+	/// Loops through the inner instructions.
+	Loop(Vec<Instruction>),
+}
+
+/// Error type for errors that occur during parsing from [`OperatorCode`]s to
+/// [`Instruction`]s.
+#[derive(Debug, Diagnostic, Error)]
+pub enum Error {
+	/// Parser encountered a loop with no beginning.
+	#[diagnostic(
+		code(brainf_rs::parser::loop_with_no_beginning),
+		help("try closing the loop")
+	)]
+	#[error("loop closed at {loop_src:?} has no beginning")]
+	LoopWithNoBeginning {
+		/// Source code associated with diagnostic
+		#[source_code]
+		input: String,
+
+		/// SourceSpan of the loop bracket.
+		#[label("loop ending")]
+		loop_src: SourceSpan,
+	},
+
+	/// Parser encountered a loop with no ending.
+	#[diagnostic(
+		code(brainf_rs::parser::loop_with_no_ending),
+		help("try closing the loop")
+	)]
+	#[error("loop beginning at {loop_src:?} has no ending")]
+	LoopWithNoEnding {
+		/// Source code associated with diagnostic
+		#[source_code]
+		input: String,
+
+		/// SourceSpan of the loop bracket.
+		#[label("loop beginning")]
+		loop_src: SourceSpan,
+	},
+
+	/// Parser sliced out of bounds.
+	#[error("parser sliced out of bounds")]
+	SliceOutOfBounds(std::ops::Range<usize>),
+}
+
+/// Parses the operator codes into instruction codes.
+///
+/// # Parameters
+///
+/// * `src` - The source the operator codes originate from. This is used for
+///   error reporting.
+/// * `operator_codes` - The operator codes reveiced from the lexer.
+///
+/// # Errors
+///
+/// This function will return an error if a loop is encountered with no
+/// beginning, a loop is encountered with no ending, or if the parser attempts
+/// to slice out of bounds.
+pub fn parse(src: &str, tokens: &[Token]) -> Result<Vec<Instruction>, Error> {
+	let mut program: Vec<Instruction> = Vec::new();
+	let mut loop_stack: i32 = 0;
+	let mut loop_start = 0;
+	let mut loop_span: (usize, usize) = (0, 0);
+
+	tokens
+		.iter()
+		.enumerate()
+		.try_for_each(|(i, operator_code)| -> Result<(), Error> {
+			match (loop_stack, *operator_code) {
+				(0i32, Token::StartLoop(span)) => {
+					loop_start = i;
+					loop_span = span;
+					loop_stack += 1i32;
+				}
+				(0i32, _) => {
+					if let Some(instruction) = match *operator_code {
+						Token::IncrementPointer => Some(Instruction::IncrementPointer),
+						Token::DecrementPointer => Some(Instruction::DecrementPointer),
+						Token::IncrementByte => Some(Instruction::IncrementByte),
+						Token::DecrementByte => Some(Instruction::DecrementByte),
+						Token::OutputByte => Some(Instruction::OutputByte),
+						Token::InputByte => Some(Instruction::InputByte),
+						Token::EndLoop(span) => {
+							return Err(Error::LoopWithNoBeginning {
+								input: src.to_owned(),
+								loop_src: span.into(),
+							})
+						}
+						// We don't care about this variant as it is handled in a subsequent arm
+						Token::StartLoop { .. } => None,
+					} {
+						program.push(instruction);
+					}
+				}
+				(_, Token::StartLoop { .. }) => loop_stack += 1i32,
+				(_, Token::EndLoop { .. }) => {
+					loop_stack -= 1i32;
+					if loop_stack == 0i32 {
+						let loop_program = parse(
+							src,
+							match tokens.get(loop_start + 1..i) {
+								Some(value) => value,
+								None => return Err(Error::SliceOutOfBounds(loop_start + 1..i)),
+							},
+						)?;
+
+						program.push(Instruction::Loop(loop_program));
+					}
+				}
+				_ => (),
+			};
+
+			Ok(())
+		})?;
+
+	if loop_stack == 0i32 {
+		Ok(program)
+	} else {
+		Err(Error::LoopWithNoEnding {
+			input: src.to_owned(),
+			loop_src: loop_span.into(),
+		})
+	}
+}
diff --git a/crates/brainf_rs/src/utility.rs b/crates/brainf_rs/src/utility.rs
new file mode 100644
index 0000000..c83dd42
--- /dev/null
+++ b/crates/brainf_rs/src/utility.rs
@@ -0,0 +1,50 @@
+//! Utility functions for working with the Brainfuck interpreter.
+
+use std::path::Path;
+
+use crate::{engine::Engine, executor::execute, lex, parse, Error};
+
+/// Utility function to execute a Brainfuck file. Lexes, parses and executes the
+/// input file.
+///
+/// # Errors
+///
+/// This function will return an error if reading the input file, parsing or
+/// execution fails. See documentation for [`crate::parser::parse`] and
+/// [`crate::executor::execute`].
+pub fn execute_from_file<E: Engine>(
+	path: impl AsRef<Path>,
+	tape: &mut [E::TapeInner],
+) -> Result<(), Error> {
+	let input = fs_err::read_to_string(path.as_ref())?;
+
+	let tokens = lex(&input)?;
+
+	let instructions = parse(&input, &tokens)?;
+
+	let mut data_pointer = 0;
+
+	execute::<E>(&instructions, tape, &mut data_pointer)?;
+
+	Ok(())
+}
+
+/// Utility function to execute Brainfuck code. Lexes, parses and executes the
+/// input.
+///
+/// # Errors
+///
+/// This function will return an error if parsing or
+/// execution fails. See documentation for [`crate::parser::parse`] and
+/// [`crate::executor::execute`].
+pub fn execute_from_str<E: Engine>(input: &str, tape: &mut [E::TapeInner]) -> Result<(), Error> {
+	let tokens = lex(input)?;
+
+	let instructions = parse(input, &tokens)?;
+
+	let mut data_pointer = 0;
+
+	execute::<E>(&instructions, tape, &mut data_pointer)?;
+
+	Ok(())
+}