4
\$\begingroup\$

I'm learning Rust at the moment by reading the book (currently on chapter 17). I wrote this simple Brainfuck interpreter, because I think that it's always a fun exercise.

The program consists of three files: main.rs, tape.rs, and errors.rs. It expects a Brainfuck source file as a command line argument, and can be run with the command:

cargo run --release bf/mandelbrot.bf

Here's my GitHub repo for it (it also contains some example .bf files to run): https://github.com/ruohola/brainfuck-interpreter-rust

Would be great to get some feedback on this. Especially looking for ways to write some parts more idiomatically, using some of Rust's nicer features I've missed.

main.rs:

use std::collections::HashMap;
use std::env;
use std::fs;
use std::process;

use crate::errors::{BraceError, TapeError};
use crate::tape::Tape;

mod errors;
mod tape;


const TOKENS: &str = "<>+-[],.";


fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        eprintln!("{}{}",
                  "Expected a brainfuck source file as a command line argument.\n",
                  "Terminating the program.");
        process::exit(1);
    }

    let source = read_source(&args[1]);

    let brace_pairs = match pair_braces(&source) {
        Ok(pairs) => pairs,
        Err(e) => {
            eprintln!("{}", e);
            process::exit(1);
        }
    };

    let source = Source {
        source,
        brace_pairs,
    };

    if let Err(e) = interpret(source, 30000) {
        eprintln!("{}", e);
        process::exit(1);
    }
}


struct Source {
    source: Vec<char>,
    brace_pairs: HashMap<usize, usize>,
}

impl Source {
    fn get_idx_of_matching(&self, i: &usize) -> usize {
        *self.brace_pairs.get(i).unwrap()
    }
}


/// Read the source file into a char vector, and return the vector.
/// This will ignore all the comment chars in the file.
fn read_source(file_path: &str) -> Vec<char> {
    fs::read_to_string(file_path)
        .expect("Error reading the source file")
        .chars()
        .filter(|c| TOKENS.contains(c.to_owned()))
        .collect()
}


/// Pair up the braces in a HashMap so that for every brace with index `i`,
/// the index `j` of its matching brace can be found with `j = brace_pairs.get(i)`.
fn pair_braces(source: &Vec<char>) -> Result<HashMap<usize, usize>, BraceError> {
    let mut brace_pairs = HashMap::new();

    for (i, chr) in source.iter().enumerate() {
        if *chr == '[' {
            let mut j = i;
            let mut brace_count = 1;
            while source[j] != ']' || brace_count != 0 {
                if j == source.len() {
                    return Err(BraceError { brace_index: i });
                }
                j += 1;
                match source[j] {
                    '[' => brace_count += 1,
                    ']' => brace_count -= 1,
                    _ => ()
                }
            }
            brace_pairs.insert(i, j);
            brace_pairs.insert(j, i);
        }
    }
    Ok(brace_pairs)
}


/// The actual interpreter. Goes through the source char by char and
/// acts according to Brainfuck's rules.
fn interpret(source: Source, tape_size: i32) -> Result<(), TapeError> {
    let mut tape = Tape::new(tape_size);

    // the index to read the source
    let mut i = 0usize;
    while i < source.source.len() {
        match source.source[i] {
            '+' => tape.incr_cell(),
            '-' => tape.decr_cell(),
            '>' => tape.incr_head()?,
            '<' => tape.decr_head()?,
            '.' => tape.print_current(),
            ',' => tape.read_to_current(),
            '[' => {
                if tape.get_current() == 0 {
                    i = source.get_idx_of_matching(&i);
                }
            }
            ']' => {
                if tape.get_current() != 0 {
                    i = source.get_idx_of_matching(&i);
                }
            }
            _ => ()
        }
        i += 1;
    }

    Ok(())
}

tape.rs:

use std::fmt::{self, Display, Formatter};
use std::io::{self, Read, Write};
use std::num::Wrapping;

use crate::errors::TapeError;


/// The memory tape of the program.
pub struct Tape {
    size: i32,
    head: i32,
    tape: Vec<Wrapping<u8>>,
}

impl Tape {
    pub fn new(size: i32) -> Tape {
        Tape {
            size,
            head: 0,
            tape: vec![Wrapping(0u8); size as usize],
        }
    }

    pub fn incr_cell(&mut self) {
        self.tape[self.head as usize] += Wrapping(1);
    }

    pub fn decr_cell(&mut self) {
        self.tape[self.head as usize] -= Wrapping(1);
    }

    pub fn incr_head(&mut self) -> Result<(), TapeError> {
        self.head += 1;
        self.err_if_not_in_bounds()
    }

    pub fn decr_head(&mut self) -> Result<(), TapeError> {
        self.head -= 1;
        self.err_if_not_in_bounds()
    }

    pub fn print_current(&self) {
        print!("{}", self.tape[self.head as usize].0 as char);
        io::stdout().flush().unwrap();
    }

    pub fn read_to_current(&mut self) {
        self.tape[self.head as usize].0 = io::stdin()
            .bytes()
            .next()
            .and_then(|result| result.ok())
            .map(|byte| byte as u8)
            .unwrap();
    }

    pub fn get_current(&self) -> u8 {
        self.tape[self.head as usize].0
    }

    fn err_if_not_in_bounds(&self) -> Result<(), TapeError> {
        if self.head < 0 || self.head >= self.size {
            Err(TapeError { tape_index: self.head })
        } else {
            Ok(())
        }
    }
}

impl Display for Tape {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        let sample = 40;
        let mut s = String::with_capacity(4 * sample);

        for cell in self.tape.iter().take(sample) {
            s.push_str(format!("[{}] ", cell.0).as_str());
        }
        write!(f, "{}", s)
    }
}

errors.rs:

use std::fmt::{self, Display, Formatter};


#[derive(Debug, Clone)]
pub struct BraceError {
    pub brace_index: usize,
}

impl Display for BraceError {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "Mismatched braces in the source file, no pair for the brace in index: {}", self.brace_index)
    }
}


#[derive(Debug, Clone)]
pub struct TapeError {
    pub tape_index: i32,
}

impl Display for TapeError {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "Tape head over tape bounds at index: {}", self.tape_index)
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.