Fix interpreter repl errors and add some tsests and create the base

readme file 🥰💙
This commit is contained in:
Anas Elgarhy 2022-10-08 22:11:13 +02:00
parent 353bdec211
commit 3ee4f37354
16 changed files with 366 additions and 33 deletions

View File

@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run bf-interpreter" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --package bf-interpreter --bin bf-interpreter " />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="emulateTerminal" value="true" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="FULL" />
<envs>
<env name="RUST_LOG" value="trace" />
</envs>
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

View File

@ -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"

88
README.md Normal file
View File

@ -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 <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 <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)

BIN
assets/cover.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
assets/cover.xcf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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");
}
}

View File

@ -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<u8>,
pub pointer: usize,
pub array_size: usize,
pub bf_code: String,
pub bf_code: Vec<char>,
brackets: Vec<BfCommand>,
pub features: Vec<arguments::Feature>,
mode: mode::RunMode,
}
impl Interpreter {
@ -17,49 +18,59 @@ impl Interpreter {
array_size: usize,
bf_code: Option<String>,
features: Vec<arguments::Feature>,
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<String>) -> Result<i32, InterpreterError> {
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<char>) -> 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<char>, 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<BfCommand> {
fn from_char(c: &char, index: usize) -> Option<BfCommand> {
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::<char>::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);
}
}

View File

@ -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 {

5
src/mode.rs Normal file
View File

@ -0,0 +1,5 @@
#[derive(PartialEq, Debug)]
pub enum RunMode {
Execute,
Repl,
}

View File

@ -6,6 +6,10 @@ struct Repl {
history: Vec<String>,
}
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();
}

View File

@ -3,7 +3,7 @@ pub(crate) fn read_brainfuck_code_if_any(source: &Option<String>) -> Option<Stri
Some(source) => {
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<String>) -> Option<Stri
None => None,
}
}
fn clean(source: String) -> String {
source
.chars()
.filter(|c| match c {
'+' | '-' | '<' | '>' | '[' | ']' | '.' | ',' => true,
_ => false,
})
.collect()
}

3
test_code/hello_world.bf Normal file
View File

@ -0,0 +1,3 @@
>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]
>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++
.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++.

1
test_code/like_cat.bf Normal file
View File

@ -0,0 +1 @@
+[>,.]

View File

@ -1 +1 @@
>+++++++++[<++++ ++++>-]<.>++++++++[<++++>-]<+.>+++++++++[<-------->-]<.
>+++++++++[ <++++ +++ + > - ] < . > + + ++++++[<++++>-]<+.>+++++++++[<-------->-]<.