From 3c163eabc78ddbd26bb250ef5ad6da28cd61adc6 Mon Sep 17 00:00:00 2001 From: Sophie Forrest Date: Fri, 30 Aug 2024 23:35:45 +1200 Subject: feat: split engine into crates --- crates/brainf_lexer/Cargo.toml | 8 ++ crates/brainf_lexer/src/lexer.rs | 75 ++++++++++++++ crates/brainf_lexer/src/lib.rs | 105 ++++++++++++++++++++ crates/brainf_rs/Cargo.toml | 22 +++++ crates/brainf_rs/src/engine.rs | 98 ++++++++++++++++++ crates/brainf_rs/src/executor.rs | 131 +++++++++++++++++++++++++ crates/brainf_rs/src/lib.rs | 207 +++++++++++++++++++++++++++++++++++++++ crates/brainf_rs/src/main.rs | 128 ++++++++++++++++++++++++ crates/brainf_rs/src/parser.rs | 169 ++++++++++++++++++++++++++++++++ crates/brainf_rs/src/utility.rs | 50 ++++++++++ 10 files changed, 993 insertions(+) create mode 100644 crates/brainf_lexer/Cargo.toml create mode 100644 crates/brainf_lexer/src/lexer.rs create mode 100644 crates/brainf_lexer/src/lib.rs create mode 100644 crates/brainf_rs/Cargo.toml create mode 100644 crates/brainf_rs/src/engine.rs create mode 100644 crates/brainf_rs/src/executor.rs create mode 100644 crates/brainf_rs/src/lib.rs create mode 100644 crates/brainf_rs/src/main.rs create mode 100644 crates/brainf_rs/src/parser.rs create mode 100644 crates/brainf_rs/src/utility.rs (limited to 'crates') 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) -> (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, 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; + + /// 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 { + 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 { + Ok(std::io::stdin().read_u16::()?) + } + + 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 { + Ok(std::io::stdin().read_u32::()?) + } + + 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 { + /// 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 Executor for E +where + E: Engine, +{ + #[inline] + fn execute( + instructions: &[Instruction], + tape: &mut [::TapeInner], + data_pointer: &mut usize, + ) -> Result<(), Error> { + execute::(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( + 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::(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 = vec![0; 1024]; + + utility::execute_from_file::("./test_programs/hello_world.bf", &mut tape)?; + + Ok(()) + } + + #[test] + fn hello_world_u16() -> Result<(), Error> { + let mut tape: Vec = vec![0; 1024]; + + utility::execute_from_file::("./test_programs/hello_world.bf", &mut tape)?; + + Ok(()) + } + + #[test] + fn hello_world_from_hell() -> Result<(), Error> { + let mut tape: Vec = vec![0; 1024]; + + utility::execute_from_file::( + "./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 = vec![0; 1024]; + + #[allow(clippy::expect_used)] + utility::execute_from_file::( + "./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 = vec![0; 1024]; + + #[allow(clippy::expect_used)] + utility::execute_from_file::( + "./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 = vec![0; 1024]; + + #[allow(clippy::expect_used)] + utility::execute_from_file::( + "./test_programs/hello_world_from_hell.bf", + &mut tape, + ) + .expect("failed to run"); + }); + } + + #[test] + fn hello_world_short() -> Result<(), Error> { + let mut tape: Vec = vec![0; 1024]; + + utility::execute_from_str::( + "--[+++++++<---->>-->+>+>+<<<<]<.>++++[-<++++>>->--<<]>>-.>--..>+.<<<.<<-.>>+>->>.\ + +++[.<]", + &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::(path, &mut tape) + .wrap_err("when executing from file")?; + } + Command::Text { ref input } => execute_from_str::(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), +} + +/// 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), +} + +/// 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, Error> { + let mut program: Vec = 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( + path: impl AsRef, + 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::(&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(input: &str, tape: &mut [E::TapeInner]) -> Result<(), Error> { + let tokens = lex(input)?; + + let instructions = parse(input, &tokens)?; + + let mut data_pointer = 0; + + execute::(&instructions, tape, &mut data_pointer)?; + + Ok(()) +} -- cgit 1.4.1