brainfuck-exe 0.1.7

A brainfuck interpreter implemented in rust
Documentation
//! # Brainfuck-exe
//!
//! a simple [`brainfuck`](https://esolangs.org/wiki/Brainfuck) interpreter crate implemented in rust 🦀
//! with many available customizations for flexibility
//!
//! see the [`Brainfuck`] struct for more information on usage
//!
//! ## Usage
//!
//! In your `Cargo.toml`:
//! ```toml
//! brainfuck-exe = "*"
//! ```
//!
//! If you are only using it as a library, and the CLI is not needed,
//! disable the `cli` (included by default) feature to remove unecessary dependencies:
//! ```toml
//! brainfuck-exe = { version = "*", default-features = false }
//! ```
//!
//! ## Example
//! Below is a basic example on how to use the crate
//!
//! ```rust
//!
//! use std::fs::File;
//! // import Result typealias and interpreter struct
//! use brainfuck_exe::{Result, Brainfuck};
//!
//! fn main() -> Result<()> {
//!     // brainfuck code to print "Hello, World!"
//!     let code = ">++++++++[<+++++++++>-]<.>++++[<+++++++>-]<+.+++++++..+++.>>++++++[<+++++++>-]<+
//!     +.------------.>++++++[<+++++++++>-]<+.<.+++.------.--------.>>>++++[<++++++++>-]<+.";
//!     // instantiate a new interpreter instance with the code
//!     Brainfuck::new(code)
//!         // optional builder method to write the output into a file not STDOUT
//!         .with_output(
//!             File::options()
//!                 .write(true)
//!                 .open("tests/output.txt")
//!                 .unwrap()
//!         )
//!         // executes the code
//!         .execute()?;
//!
//!     // alternatively use this to retrieve the code from an existing source file
//!     Brainfuck::from_file("tests/hello_world.bf")?
//!         .execute()?;
//!
//!     Ok(())
//! }
//! ```
//!
//! ## CLI
//! You can also use this crate as a CLI program
//!
//! ```bash
//! # installation
//! $ cargo install brainfuck-exe
//! # usage
//! $ brainfuck --help
//! $ brainfuck [CODE] [-f FILE] [OPTIONS]
//! ```

use std::{
    fs::File,
    path::Path,
    io::{Read, Write},
    ops::{Deref, DerefMut},
};
pub use error::{Error, Result};

pub mod error;

/// default max value a cell can have
///
/// it is `255`, the same as [`std::u8::MAX`]
pub const DEFAULT_MAX_CELL_VALUE: u32 = 255;


/// a helper wrapper enum that is used for storing the input stream
/// this allows for it to be passed by value OR reference
pub enum Reader<'a> {
    /// used when passing in the input stream by value
    Value(Box<dyn Read>),
    /// used when passing in the input stream as a mutable reference
    Ref(&'a mut dyn Read),
}

/// a helper wrapper enum that is used for storing the output stream
/// this allows for it to be passed by value OR reference
pub enum Writer<'a> {
    /// used when passing in the output stream by value
    Value(Box<dyn Write>),
    /// used when passing in the output stream as a mutable reference
    Ref(&'a mut dyn Write),
}

impl<'a> Deref for Reader<'a> {
    type Target = dyn Read + 'a;

    fn deref(&self) -> &Self::Target {
        match self {
            Self::Value(v) => &**v,
            Self::Ref(r) => &**r,
        }
    }
}

impl<'a> DerefMut for Reader<'a> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        match self {
            Self::Value(v) => &mut **v,
            Self::Ref(r) => &mut **r,
        }
    }
}

impl<'a> Deref for Writer<'a> {
    type Target = dyn Write + 'a;

    fn deref(&self) -> &Self::Target {
        match self {
            Self::Value(v) => &**v,
            Self::Ref(r) => &**r,
        }
    }
}

impl<'a> DerefMut for Writer<'a> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        match self {
            Self::Value(v) => &mut **v,
            Self::Ref(r) => &mut **r,
        }
    }
}


/// The struct representing a brainfuck interpreter instance
pub struct Brainfuck<'a> {
    /// the brainfuck source code to execute
    pub code: String,
    /// the input stream used for `,` operations
    pub input: Option<Reader<'a>>,
    /// the output stream used for `.` operations
    pub output: Option<Writer<'a>>,
    /// sets the maximum value of a cell, defaults to `255`
    pub max_cell_value: u32,
    /// sets the maximum length of the memory array
    ///
    /// defaults to [`None`], which is "infinite"
    pub memory_size: Option<usize>,
    /// indicates whether or not to manually flush the output buffer every write
    ///
    /// if set to `false` it will let the process automatically flush (end of program or at every newline),
    /// defaults to `true`
    pub flush_output: bool,
    /// this field is only of use if the input stream used is [`std::io::stdin`]
    ///
    /// it specifies whether or not to retrieve all the input data needed in one prompt the first time
    /// or rather prompt the user every time for a character,
    /// defaults to `false`
    pub prompt_stdin_once: bool,
    /// sets the limit on the amount of instructions we can process in one program
    ///
    /// defaults to [`None`], which is *no* limit
    /// (for safety and debugging usage)
    pub instructions_limit: Option<usize>,
    /// an instructions counter to count the number of instructions executed thus far
    instructions_ctn: usize,
}

impl<'a> Default for Brainfuck<'a> {
    fn default() -> Self {
        Self::new(String::new())
    }
}

impl<'a> Brainfuck<'a> {
    /// creates a new instance of a brainfuck interpeter with the provided `code`
    ///
    /// - input and output streams default to [`std::io::stdin`] and [`std::io::stdout`] respectively
    /// - the maximum value a cell can have is `255` (8 bits / 1 byte)
    /// - the program's memory array can grow indefinitely
    #[must_use]
    pub fn new<S: AsRef<str>>(code: S) -> Self {
        Self {
            code: code
                .as_ref()
                .to_string(),
            input: None,
            output: None,
            max_cell_value: DEFAULT_MAX_CELL_VALUE,
            memory_size: None,
            flush_output: true,
            prompt_stdin_once: false,
            instructions_limit: None,
            instructions_ctn: 0,
        }
    }

    /// an alternative to `Self::new`,
    /// used when the code is in a source file instead of being directly accessible as a string in the code
    ///
    /// # Errors
    /// - [`Error::FileReadError`]: propogated from [`std::io::Error`]
    ///   when opening or reading the source file
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let mut buf = String::new();
        let mut file = File::open(path)
            .map_err(Error::FileReadError)?;

        file.read_to_string(&mut buf)
            .map_err(Error::FileReadError)?;
        Ok(Self::new(buf))
    }

    /// builder method to specify the brainfuck code for the interpreter
    #[must_use]
    pub fn with_code<S: AsRef<str>>(mut self, code: S) -> Self {
        self.code = code
            .as_ref()
            .to_string();
        self
    }

    /// builder method to specify the input stream **passing by value**, for the `,` operation
    #[must_use]
    pub fn with_input<I>(mut self, input: I) -> Self
    where
        I: Read + 'static
    {
        self.input = Some(
            Reader::Value(Box::new(input))
        );
        self
    }

    /// builder method to specify the output stream **passing by value**, for the `.` operation
    #[must_use]
    pub fn with_output<O>(mut self, output: O) -> Self
    where
        O: Write + 'static
    {
        self.output = Some(
            Writer::Value(Box::new(output))
        );
        self
    }

    /// builder method to specify the input stream **passing by reference**, for the `,` operation
    #[must_use]
    pub fn with_input_ref<I>(mut self, input: &'a mut I) -> Self
    where
        I: Read + 'static
    {
        self.input = Some(
            Reader::Ref(input)
        );
        self
    }

    /// builder method to specify the output stream **passing by reference**, for the `.` operation
    #[must_use]
    pub fn with_output_ref<O>(mut self, output: &'a mut O) -> Self
    where
        O: Write + 'static
    {
        self.output = Some(
            Writer::Ref(output)
        );
        self
    }

    /// builder method to specify the max value of a cell
    #[must_use]
    pub const fn with_max_value(mut self, cell_value: u32) -> Self {
        self.max_cell_value = cell_value;
        self
    }

    /// builder method to specify the maximum memory array length
    #[must_use]
    pub const fn with_mem_size(mut self, mem_size: usize) -> Self {
        self.memory_size = Some(mem_size);
        self
    }

    /// builder method to indicate whether or not to flush the output stream on every write
    #[must_use]
    pub const fn with_flush(mut self, flush: bool) -> Self {
        self.flush_output = flush;
        self
    }

    /// builder method to indicate whether or not to only prompt [`std::io::stdin`] once
    #[must_use]
    pub const fn prompt_stdin_once(mut self, once: bool) -> Self {
        self.prompt_stdin_once = once;
        self
    }

    /// builder method to set the maximum amount of instructions we can process in one program
    #[must_use]
    pub const fn with_instructions_limit(mut self, limit: usize) -> Self {
        self.instructions_limit = Some(limit);
        self
    }

    /// a getter that returns the number of instructions executed thus far
    #[must_use]
    pub const fn instructions_count(&self) -> usize {
        self.instructions_ctn
    }

    /// consumes itself and returns the input stream in an [`Option`]
    #[must_use]
    #[allow(clippy::missing_const_for_fn)]
    pub fn into_input(self) -> Option<Reader<'a>> {
        self.input
    }

    /// consumes itself and returns the output stream in an [`Option`]
    #[must_use]
    #[allow(clippy::missing_const_for_fn)]
    pub fn into_output(self) -> Option<Writer<'a>> {
        self.output
    }

    /// helper method to read from [`std::io::stdin`]
    ///
    /// it accomplishes such in one prompt, retrieving all the data at once
    /// as a fallback to if no other input stream is specified for the `,` operation
    #[must_use]
    fn read_from_stdin_once() -> u32 {
        let mut buffer = [0];
        match std::io::stdin()
            .read_exact(&mut buffer[0..1])
        {
            Ok(_) => u32::from(buffer[0]),
            Err(_) => 0,
        }
    }

    /// helper method to read from [`std::io::stdin`]
    ///
    /// it prompts every time this function is called however
    /// as a fallback to if no other input stream is specified for the `,` operation
    #[must_use]
    fn read_from_stdin() -> u32 {
        let mut buffer = String::new();
        match std::io::stdin()
            .read_line(&mut buffer)
        {
            Ok(_) => buffer
                .chars()
                .next()
                .map_or(0, |c| c as u32),
            Err(_) => 0,
        }
    }

    /// executes the provided brainfuck code
    /// which is stored in the struct field: `code`
    ///
    /// brainfuck supports 8 operations which are as following:
    /// `+ - < > . , [ ]`
    ///
    /// different implementations vary on wraparound rules
    ///
    /// # Operations
    /// - `+`: increments the current cell by `1`
    ///   if the value exceeds `self.max_cell_value`, it gets wrapped back to `0`
    /// - `-`: decrements the current cell by `1`
    ///   if the value goes below `0`, it gets wrapped back to `self.max_cell_value`
    /// - `>`: moves the pointer up 1 cell
    ///   if the the pointer exceeds `self.memory_size`, it gets wrapped back to `0`;
    ///   however, if `self.memory_size` is [`None`], it will grow the array by 1 additional cell
    /// - `<`: moves the pointer down 1 cell
    ///   if the value goes below `0`, it gets wrapped back to the end of the memory array
    /// - `.`: writes the value of the current cell as ASCII into the provided output stream, `self.output`
    ///   defaulting to [`std::io::stdout`]
    /// - `,`: reads 1 byte from the provided input stream, `self.input`
    ///   defaulting to [`std::io::stdin`]
    ///   if reading fails (e.g. there were no bytes to read (EOF) or other error), the current cell gets set back to `0`
    /// - `[`: always should be paired with a `]`, acts as a "loop" in brainfuck
    ///   the code that is enclosed within a pair of `[ ]` gets looped over until the current cell != 0
    /// - `]`: the closing bracket for a loop, paired with `[`
    ///   if the current cell != 0, jump back to corresponding `[`
    ///
    /// returns the used memory array of the program which is a [`Vec<u32>`]
    ///
    /// # Errors
    /// - [`Error::MismatchedBrackets`]: the amount of `[` in the code does not equal the amount of `]`
    /// - [`Error::IoError`]: Propogated from [`std::io::Error`] in the `.` operation
    ///
    #[allow(clippy::too_many_lines)]
    pub fn execute(&mut self) -> Result<Vec<u32>> {
        let (opening, closing) = (
            self.code.chars()
                .filter(|c| *c == '[')
                .count(),
            self.code.chars()
                .filter(|c| *c == ']')
                .count()
        );

        if opening != closing {
            return Err(Error::MismatchedBrackets {
                opening, closing
            });
        }

        let mut cells =
            self.memory_size
                .map_or_else(
                    || vec![0],
                    |mem_size| vec![0; mem_size],
                );

        self.instructions_ctn = 0;
        let mut code_idx = 0usize;
        let mut ptr = 0usize;

        while code_idx < self.code
            .chars()
            .count()
        {
            match self.code
                .chars()
                .nth(code_idx)
            {
                Some('+') =>
                    if cells[ptr] >= self.max_cell_value {
                        cells[ptr] = 0;
                    } else {
                        cells[ptr] += 1;
                    },
                Some('-') =>
                    if cells[ptr] == 0 {
                        cells[ptr] = self.max_cell_value;
                    } else {
                        cells[ptr] -= 1;
                    },
                Some('<') =>
                    if ptr == 0 {
                        ptr = cells.len() - 1;
                    } else {
                        ptr -= 1;
                    },
                Some('>') => {
                    ptr += 1;
                    if let Some(mem_size) = self.memory_size {
                        if ptr >= mem_size {
                            ptr = 0;
                        }
                    } else if ptr >= cells.len() {
                        cells.push(0);
                    }
                },
                Some('.') =>
                    if let Some(chr) =
                        std::char::from_u32(cells[ptr])
                    {
                        if let Some(ref mut writer) =
                            self.output
                        {
                            let mut buf = vec![0; chr.len_utf8()];
                            chr.encode_utf8(&mut buf);

                            writer.write_all(&buf)?;
                            if self.flush_output {
                                writer.flush()?;
                            }
                        } else {
                            print!("{chr}");
                            if self.flush_output {
                                std::io::stdout()
                                    .flush()?;
                            }
                        }
                    },
                #[allow(clippy::option_if_let_else)]
                Some(',') =>
                    cells[ptr] = if let Some(ref mut reader) =
                        self.input
                    {
                        let mut buffer = [0];
                        match reader
                            .read_exact(&mut buffer[0..1])
                        {
                            Ok(_) => u32::from(buffer[0]),
                            Err(_) => 0,
                        }
                    } else if self.prompt_stdin_once {
                        Self::read_from_stdin_once()
                    } else {
                        Self::read_from_stdin()
                    },
                Some('[') =>
                    if cells[ptr] == 0 {
                        let mut loop_ = 1;
                        while loop_ > 0 {
                            code_idx += 1;
                            match self.code
                                .chars()
                                .nth(code_idx)
                            {
                                Some('[') => loop_ += 1,
                                Some(']') => loop_ -= 1,
                                _ => (),
                            }
                        }
                    },
                Some(']') => {
                    let mut loop_ = 1;
                    while loop_ > 0 {
                        code_idx -= 1;
                        match self.code
                            .chars()
                            .nth(code_idx)
                        {
                            Some('[') => loop_ -= 1,
                            Some(']') => loop_ += 1,
                            _ => (),
                        }
                    }
                    code_idx -= 1;
                },
                _ => (),
            }
            code_idx += 1;
            self.instructions_ctn += 1;

            if let Some(cap) = self.instructions_limit {
                if self.instructions_ctn > cap {
                    return Err(Error::MaxInstructionsExceeded(cap));
                }
            }
        }

        Ok(cells)
    }
}