diff --git a/.idea/runConfigurations/Run_bf_interpreter.xml b/.idea/runConfigurations/Run_bf_interpreter.xml new file mode 100644 index 0000000..f3c0cc7 --- /dev/null +++ b/.idea/runConfigurations/Run_bf_interpreter.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 5c7d1a5..1951a52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,6 @@ exclude = [ clap = { version = "4.0.10", features = ["derive", "color", "cargo"] } log = "0.4.17" pretty_env_logger = "0.4.0" + +[dev-dependencies] +pretty_assertions = "1.3.0" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c331c7 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +![brainfuc*k interpreter](./assets/cover.png) +# brainfuc*k interpreter: a simple brainfuc*k interpreter and REPL writen in rust 🦀 + +## Install + +- from crates.io + ```bash + crago install bf-bf_interpreter + ``` +- From aur: + ```shell + yay -S bf-interpreter + ``` + +## Options and arguments + +```bash +bf-bf_interpreter --help +``` +```text +Brainfu*k interpreter and REPL written in Rust + +Usage: bf-interpreter [OPTIONS] [SOURCE] + +Arguments: + [SOURCE] + The brainfuck source code file to run (if not will be entered in REPL mode) + +Options: + -f, --features + Possible values: + - reverse-counter: + If the value is you want decrement the value and the value is 0, set the value to 255, otherwise decrement the value. If the value is you want increment the value and the value is 255, set the value to 0, otherwise increment the value + - reverse-pointer: + If the pointer at the end of the array, set the pointer to 0, otherwise increment the pointer. If the pointer at the beginning of the array, set the pointer to the end of the array, otherwise decrement the pointer + + -a, --array-size + The brainfuck array size + + [default: 30000] + + -w, --without-tiles + Dont print the tiles (e.g. exit code, file name, etc) + + -h, --help + Print help information (use `-h` for a summary) + + -V, --version + Print version information +``` + +### Examples + +```bash +bf-bf_interpreter test_code/hello_world.bf +``` +```text +Hello world! +Successfully ran brainfuck source code from file: test_code/hello_world.bf +Exiting with code: 0 +``` + +```bash +bf-bf_interpreter -w test_code/hello_world.bf +``` +```text +Hello world! +``` + +```bash +bf-bf_interpreter test_code/print_hi_yooo.bf +``` +```text +Hi yoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!Successfully ran brainfuck source code from file: test_code/print_hi_yooo.bf +Exiting with code: 0 +``` + +```bash +bf-bf_interpreter -w test_code/print_hi_yooo.bf +``` +```text +Hi yoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! +``` + +```bash +bf-bf_interpreter test_code/like_cat.bf +``` +![output](./screenshots/like_cat_output.png) diff --git a/assets/cover.jpg b/assets/cover.jpg new file mode 100644 index 0000000..bfc3234 Binary files /dev/null and b/assets/cover.jpg differ diff --git a/assets/cover.png b/assets/cover.png new file mode 100644 index 0000000..7aa1d24 Binary files /dev/null and b/assets/cover.png differ diff --git a/assets/cover.xcf b/assets/cover.xcf new file mode 100644 index 0000000..b83cedf Binary files /dev/null and b/assets/cover.xcf differ diff --git a/screenshots/like_cat_output.png b/screenshots/like_cat_output.png new file mode 100644 index 0000000..9ca3c34 Binary files /dev/null and b/screenshots/like_cat_output.png differ diff --git a/src/bf_interpreter/error.rs b/src/bf_interpreter/error.rs index 034e0c4..103a16c 100644 --- a/src/bf_interpreter/error.rs +++ b/src/bf_interpreter/error.rs @@ -1,5 +1,6 @@ use std::fmt::{Debug, Formatter, Display}; +#[derive(PartialEq)] pub struct InterpreterError { message: String, pub code: i32, @@ -33,8 +34,7 @@ impl std::error::Error for InterpreterError { } pub enum InterpreterErrorKind { - PointerOutOfBounds(usize), - // takes pointer value + PointerOutOfBounds(usize), // takes pointer value ValueOutOfBounds, ByteReadError(std::io::Error), ReadError, @@ -69,3 +69,45 @@ impl Display for InterpreterErrorKind { } } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; // for testing only + + #[test] + fn test_error_kind_display() { + let error = InterpreterErrorKind::PointerOutOfBounds(10).to_error(); + assert_eq!(error.to_string(), "Pointer out of bounds 10"); + assert_eq!(error.code, 11); + + let error = InterpreterErrorKind::ValueOutOfBounds.to_error(); + assert_eq!(error.to_string(), "Value out of bounds"); + assert_eq!(error.code, 12); + + let error = InterpreterErrorKind::ByteReadError(std::io::Error::new(std::io::ErrorKind::Other, "test")).to_error(); + assert_eq!(error.to_string(), "Failed to read byte from stdin: no bytes available: test"); + assert_eq!(error.code, 13); + + let error = InterpreterErrorKind::ReadError.to_error(); + assert_eq!(error.to_string(), "Failed to read byte from stdin: no bytes available"); + assert_eq!(error.code, 14); + + let error = InterpreterErrorKind::UnmatchedClosingBracket(10).to_error(); + assert_eq!(error.to_string(), "Unmatched closing bracket at position 10"); + assert_eq!(error.code, 15); + } + + #[test] + fn test_error_display() { + let error = InterpreterError::new("test".to_string(), 10); + assert_eq!(error.to_string(), "test"); + assert_eq!(error.code, 10); + } + + #[test] + fn test_error_debug() { + let error = InterpreterError::new("test".to_string(), 10); + assert_eq!(format!("{:?}", error), "test, code: 10"); + } +} diff --git a/src/bf_interpreter/interpreter.rs b/src/bf_interpreter/interpreter.rs index abbb493..52b890f 100644 --- a/src/bf_interpreter/interpreter.rs +++ b/src/bf_interpreter/interpreter.rs @@ -1,15 +1,16 @@ -use crate::arguments; +use crate::{arguments, mode}; use crate::bf_interpreter::error::{InterpreterError, InterpreterErrorKind}; use std::io::{Read, Write}; -use std::usize; +use std::{char, usize, vec}; pub struct Interpreter { pub cells: Vec, pub pointer: usize, pub array_size: usize, - pub bf_code: String, + pub bf_code: Vec, brackets: Vec, pub features: Vec, + mode: mode::RunMode, } impl Interpreter { @@ -17,49 +18,59 @@ impl Interpreter { array_size: usize, bf_code: Option, features: Vec, + run_mode: mode::RunMode ) -> Self { + trace!("Run mode{run_mode:?}"); Self { cells: vec![0; array_size], pointer: 0, array_size, - bf_code: bf_code.unwrap_or_else(|| String::new()), + bf_code: bf_code.unwrap_or_else(|| String::new()).chars().collect(), brackets: Vec::new(), features, + mode: run_mode, } } pub fn run(&mut self, bf_code: Option) -> Result { let bf_code = match bf_code { Some(bf_code) => { - self.bf_code.push_str(&*bf_code); - bf_code + bf_code.chars().collect() } None => self.bf_code.clone(), }; - match self.run_brainfuck_code(&bf_code) { + match self.run_brainfuck_code(bf_code, false) { Ok(_) => Ok(0), Err(e) => Err(e), } } // +[>++<-] - fn iterate(&mut self, code: String) -> Result<(), InterpreterError> { + fn iterate(&mut self, code: Vec) -> Result<(), InterpreterError> { + trace!("Iterate: {:?}", code); while self.cells[self.pointer] != 0 { - self.run_brainfuck_code(&code)?; + self.run_brainfuck_code(code.clone(), true)?; } Ok(()) } - fn run_brainfuck_code(&mut self, bf_code: &str) -> Result<(), InterpreterError> { - for (i, ch) in bf_code.chars().enumerate() { - match BfCommand::from_char(ch, i) { + fn run_brainfuck_code(&mut self, bf_code: Vec, from_loop: bool) -> Result<(), InterpreterError> { + let mut removed_num = 0_usize; + for (i, ch) in bf_code.iter().enumerate() { + match BfCommand::from_char(ch, i - removed_num) { Some(cmd) => { trace!("Executing command: {:?}", cmd); - self.execute(cmd)? + self.execute(cmd)?; + + // Push the char to the bf_code vector if isn't from loop and we run in REPL mode + if !from_loop && self.mode == mode::RunMode::Repl { + self.bf_code.push(ch.clone()); + } } None => { - trace!("Skipping character: {}", ch); + trace!("Skipping character: \'{}\'", ch); + removed_num += 1; } } } @@ -134,15 +145,36 @@ impl Interpreter { match open_bracket { Some(BfCommand::LoopStart(j)) => { if self.cells[self.pointer] != 0 { - let code = self.bf_code[j..i].to_string(); + let start = match &self.mode { + mode::RunMode::Repl if self.bf_code.len() - j >= i => + self.bf_code.len() - j - i + 1, + _ => j + 1 + }; + debug!("bf_code array len: {}", self.bf_code.len()); + debug!("start index {}", start); + debug!("bf_code at start: {}", self.bf_code[start]); + debug!("i: {i}, j: {j}"); + // debug!("{}", self.bf_code[i]); + let end = match &self.mode { + mode::RunMode::Repl => { + let mut s = i + start - 2; + + if s >= self.bf_code.len() { + s = s - (self.bf_code.len() - start) + 1; + } + + s + }, + mode::RunMode::Execute => i - 1, + }; + let range = start..=end; + debug!("{range:?}"); + let code = self.bf_code[range].to_vec(); self.iterate(code)?; } } _ => { - return Err(InterpreterError::new( - format!("Unmatched closing bracket at position {}", i), - 15, - )); + return Err(InterpreterErrorKind::UnmatchedClosingBracket(i).to_error()); } } } @@ -154,6 +186,7 @@ impl Interpreter { self.cells = vec![0; self.array_size]; self.pointer = 0; self.brackets = Vec::new(); + self.bf_code = Vec::new(); } } @@ -170,7 +203,7 @@ enum BfCommand { } impl BfCommand { - fn from_char(c: char, index: usize) -> Option { + fn from_char(c: &char, index: usize) -> Option { match c { '>' => Some(BfCommand::IncPtr), '<' => Some(BfCommand::DecPtr), @@ -185,3 +218,116 @@ impl BfCommand { } } +#[cfg(test)] +mod tests { + use super::*; + use crate::mode::RunMode; + use pretty_assertions::assert_eq; + use crate::utils; // for testing only + + #[test] + fn print_h_combine_repl() { + let mut interpreter = Interpreter::new( + 30000, + None, + vec![], + RunMode::Repl + ); + + assert_eq!(interpreter.run(None), Ok(0)); + + assert_eq!(interpreter.run(Some(String::from(">+++++++++[<++++ ++++>-]<."))), Ok(0)); + } + #[test] + fn print_h_repl() { + let mut interpreter = Interpreter::new( + 30000, + None, + vec![], + RunMode::Repl + ); + + assert_eq!(interpreter.run(None), Ok(0)); + + assert_eq!(interpreter.run(Some(String::from(">+++++++++"))), Ok(0)); + assert_eq!(interpreter.run(Some(String::from("[<++++ ++++>-]<."))), Ok(0)); + } + + #[test] + fn execute_hello_world_from_file() { + let mut interpreter = Interpreter::new( + 30000, + utils::read_brainfuck_code_if_any(&Some(String::from("test_code/hello_world.bf"))), + vec![], + RunMode::Execute + ); + + assert_eq!(interpreter.run(None), Ok(0)); + } + + #[test] + fn execute_print_hi_from_file() { + let mut interpreter = Interpreter::new( + 30000, + utils::read_brainfuck_code_if_any(&Some(String::from("test_code/print_hi.bf"))), + vec![], + RunMode::Execute + ); + + assert_eq!(interpreter.run(None), Ok(0)); + } + + #[test] + fn execute_print_hi_yooo_from_file() { + let mut interpreter = Interpreter::new( + 30000, + utils::read_brainfuck_code_if_any(&Some(String::from("test_code/print_hi_yooo.bf"))), + vec![], + RunMode::Execute + ); + + assert_eq!(interpreter.run(None), Ok(0)); + } + + #[test] + fn reset() { + let mut interpreter = Interpreter::new( + 30000, + None, + vec![], + RunMode::Repl + ); + + assert_eq!(interpreter.run(None), Ok(0)); + + assert_eq!(interpreter.run(Some(String::from(">++++"))), Ok(0)); + + assert_eq!(interpreter.pointer, 1); + assert_eq!(interpreter.cells[0], 0); + assert_eq!(interpreter.cells[1], 4); + assert_eq!(interpreter.bf_code, vec!['>', '+', '+', '+' , '+']); + + // reset + interpreter.reset(); + + assert_eq!(interpreter.pointer, 0); + assert_eq!(interpreter.cells[0], 0); + assert_eq!(interpreter.cells[1], 0); + assert_eq!(interpreter.bf_code, Vec::::new()); + + assert_eq!(interpreter.run(None), Ok(0)); + } + + #[test] + fn test_from_char() { + assert_eq!(BfCommand::from_char(&'>', 0), Some(BfCommand::IncPtr)); + assert_eq!(BfCommand::from_char(&'<', 0), Some(BfCommand::DecPtr)); + assert_eq!(BfCommand::from_char(&'+', 0), Some(BfCommand::IncVal)); + assert_eq!(BfCommand::from_char(&'-', 0), Some(BfCommand::DecVal)); + assert_eq!(BfCommand::from_char(&'.', 0), Some(BfCommand::Print)); + assert_eq!(BfCommand::from_char(&',', 0), Some(BfCommand::Read)); + assert_eq!(BfCommand::from_char(&'[', 0), Some(BfCommand::LoopStart(0))); + assert_eq!(BfCommand::from_char(&']', 0), Some(BfCommand::LoopEnd(0))); + assert_eq!(BfCommand::from_char(&' ', 0), None); + } +} diff --git a/src/main.rs b/src/main.rs index ba8dffb..a64ed54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,9 @@ mod arguments; mod repl; mod utils; mod bf_interpreter; +mod mode; use clap::Parser; - extern crate pretty_env_logger; #[macro_use] extern crate log; @@ -24,6 +24,10 @@ fn main() { args.array_size, utils::read_brainfuck_code_if_any(&args.source), args.features.unwrap_or_else(|| vec![]), + match args.source { + Some(_) => mode::RunMode::Execute, + None => mode::RunMode::Repl + }, ); match args.source { diff --git a/src/mode.rs b/src/mode.rs new file mode 100644 index 0000000..b64f225 --- /dev/null +++ b/src/mode.rs @@ -0,0 +1,5 @@ +#[derive(PartialEq, Debug)] +pub enum RunMode { + Execute, + Repl, +} \ No newline at end of file diff --git a/src/repl.rs b/src/repl.rs index cf011fc..a74c225 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -6,6 +6,10 @@ struct Repl { history: Vec, } +const PROMPT: &str = "bf-interpreter> "; +const HISTORY_FILE: &str = "bf-interpreter-history.bfr"; +const COMMAND_PREFIX: &str = "!"; + impl Repl { pub fn new(interpreter: Interpreter) -> Self { Self { @@ -16,8 +20,11 @@ impl Repl { pub fn run(mut self) { loop { - print!("\n> "); - std::io::stdout().flush().unwrap(); + print!("\n {}", PROMPT); + std::io::stdout().flush().unwrap_or_else(|_| { + error!("Failed to flush stdout"); + std::process::exit(1); + }); let mut input = String::new(); match std::io::stdin().read_line(&mut input) { @@ -31,7 +38,7 @@ impl Repl { self.history.push(input.clone()); // Save input to history - if input.starts_with("!") { + if input.starts_with(COMMAND_PREFIX) { self.run_repl_cmd(input); } else { match self.interpreter.run(Some(input)) { @@ -50,7 +57,7 @@ impl Repl { let mut cmd = input.split_whitespace(); match cmd.next() { Some(repl_cmd) => { - match repl_cmd.get(1..).unwrap_or("") { + match repl_cmd.get(COMMAND_PREFIX.len()..).unwrap_or("") { "fuck" => { println!("Bye bye :D"); std::process::exit(0); @@ -78,7 +85,7 @@ impl Repl { } } "save" | "s" => { - let file_name = cmd.next().unwrap_or("brainfuck_repl_history.bfr"); + let file_name = cmd.next().unwrap_or(HISTORY_FILE); println!("Saving history to file: {file_name}"); match std::fs::write(file_name, self.history.join("\n")) { @@ -91,7 +98,7 @@ impl Repl { } } "load" | "l" => { - let file_name = cmd.next().unwrap_or("brainfuck_repl_history.bfr"); + let file_name = cmd.next().unwrap_or(HISTORY_FILE); println!("Loading history from file: {file_name}"); match std::fs::read_to_string(file_name) { @@ -133,7 +140,7 @@ impl Repl { println!("!history, !h: print the history of the commands"); println!("!save, !s: save the history to a file"); println!("!load, !l: load the history from a file"); - println!("!reset, !r: reset the bf_interpreter"); + println!("!reset, !r: reset the REPL"); println!("!help: show this fu*king help message"); println!("!fuck: exit the REPL mode"); } @@ -145,6 +152,9 @@ impl Repl { } } +/// Run the REPL +/// # Arguments +/// * `interpreter` - The interpreter to use pub fn start(interpreter: Interpreter) { info!("Entering REPL mode"); println!("Welcome to the brainfuck REPL mode! :)"); @@ -155,7 +165,7 @@ pub fn start(interpreter: Interpreter) { ); println!("Enter your brainfuck code and press enter to run it."); println!("Enter !fuck to exit :D"); - println!("Enter !help fuck to get more help"); + println!("Enter !help to get more fu*king help"); Repl::new(interpreter).run(); } diff --git a/src/utils.rs b/src/utils.rs index 16ab5e4..dac20a3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,7 +3,7 @@ pub(crate) fn read_brainfuck_code_if_any(source: &Option) -> Option { info!("Reading brainfuck source code from file: {}", source); match std::fs::read_to_string(source) { - Ok(source) => Some(source), + Ok(source) => Some(clean(source)), Err(e) => { error!("Failed to read source code file: {}", e); eprintln!("Failed to read source code file: {}", e); @@ -14,3 +14,13 @@ pub(crate) fn read_brainfuck_code_if_any(source: &Option) -> Option None, } } + +fn clean(source: String) -> String { + source + .chars() + .filter(|c| match c { + '+' | '-' | '<' | '>' | '[' | ']' | '.' | ',' => true, + _ => false, + }) + .collect() +} diff --git a/test_code/hello_world.bf b/test_code/hello_world.bf new file mode 100644 index 0000000..273b209 --- /dev/null +++ b/test_code/hello_world.bf @@ -0,0 +1,3 @@ +>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-] +>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++ +.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++. diff --git a/test_code/like_cat.bf b/test_code/like_cat.bf new file mode 100644 index 0000000..2f0f0c4 --- /dev/null +++ b/test_code/like_cat.bf @@ -0,0 +1 @@ ++[>,.] diff --git a/test_code/print_hi.bf b/test_code/print_hi.bf index 59f99f4..41ec69a 100644 --- a/test_code/print_hi.bf +++ b/test_code/print_hi.bf @@ -1 +1 @@ ->+++++++++[<++++ ++++>-]<.>++++++++[<++++>-]<+.>+++++++++[<-------->-]<. +>+++++++++[ <++++ +++ + > - ] < . > + + ++++++[<++++>-]<+.>+++++++++[<-------->-]<.