Skip to content

Commit

Permalink
Update the Rust's brainfuck implementation (#325)
Browse files Browse the repository at this point in the history
* Update the Rust's brainfuck implementation

More idiomatic code, and formatting with `rustfmt`. The most important change was in the `Tape` impl:

* `mov()` reallocs only once per call;
* `get()` and `inc()` uses unchecked indexing because the check is already done in `mov()`

* Pass String as reference

* Use more specific enum variants

Because of the strong typing, the `mov()` was doing many lossy
casts which the C and C++ versions don't; the D version uses
specific variants too.

Besides, the value was always -1 or 1, so it is more idiomatic
this way.
  • Loading branch information
ricvelozo authored Apr 18, 2021
1 parent 9dd8635 commit 7e2fc03
Showing 1 changed file with 96 additions and 43 deletions.
139 changes: 96 additions & 43 deletions brainfuck/bf.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,91 @@
use std::io;
use std::fs;
use std::env;
use std::fs;
use std::io::{self, prelude::*};
use std::process;

enum Op {
Inc(i32),
Move(isize),
Dec,
Inc,
Prev,
Next,
Loop(Box<[Op]>),
Print
Print,
}
use Op::*;

struct Tape {
pos: usize,
tape: Vec<i32>
tape: Vec<i32>,
}

impl Tape {
fn new() -> Tape { Tape { pos: 0, tape: vec![0] } }
fn get(&self) -> i32 { self.tape[self.pos] }
fn inc(&mut self, x: i32) { self.tape[self.pos] += x; }
fn mov(&mut self, x: isize) {
self.pos = (self.pos as isize + x) as usize;
while self.pos >= self.tape.len() { self.tape.push(0); }
fn new() -> Self {
Default::default()
}

fn get(&self) -> i32 {
// Always safe
unsafe { *self.tape.get_unchecked(self.pos) }
}

fn dec(&mut self) {
// Always safe
unsafe {
*self.tape.get_unchecked_mut(self.pos) -= 1;
}
}

fn inc(&mut self) {
// Always safe
unsafe {
*self.tape.get_unchecked_mut(self.pos) += 1;
}
}

fn prev(&mut self) {
self.pos -= 1;
}

fn next(&mut self) {
self.pos += 1;
if self.pos >= self.tape.len() {
self.tape.resize(self.pos << 1, 0);
}
}
}

impl Default for Tape {
fn default() -> Self {
Self {
pos: 0,
tape: vec![0],
}
}
}

#[derive(Default)]
struct Printer {
sum1: i32,
sum2: i32,
quiet: bool
quiet: bool,
}

impl Printer {
fn new(quiet: bool) -> Printer { Printer { sum1: 0, sum2: 0, quiet: quiet} }
fn new(quiet: bool) -> Self {
Self {
quiet,
..Default::default()
}
}

fn print(&mut self, n: i32) {
if self.quiet {
self.sum1 = (self.sum1 + n) % 255;
self.sum2 = (self.sum2 + self.sum1) % 255;
} else {
print!("{}", n as u8 as char);
io::Write::flush(&mut io::stdout()).unwrap();
let mut stdout = io::stdout();
stdout.lock().write_all(&[n as u8]).ok();
stdout.flush().ok();
}
}

Expand All @@ -49,61 +94,68 @@ impl Printer {
}
}

fn _run(program: &[Op], tape: &mut Tape, p: &mut Printer) {
fn run(program: &[Op], tape: &mut Tape, p: &mut Printer) {
for op in program {
match *op {
Inc(x) => tape.inc(x),
Move(x) => tape.mov(x),
Loop(ref program) => while tape.get() > 0 {
_run(program, tape, p);
},
Dec => tape.dec(),
Inc => tape.inc(),
Prev => tape.prev(),
Next => tape.next(),
Loop(ref program) => {
while tape.get() > 0 {
run(program, tape, p);
}
}
Print => {
p.print(tape.get());
}
}
}
}

fn parse<I: Iterator<Item=char>>(it: &mut I) -> Box<[Op]> {
let mut buf = Vec::new();
fn parse<I: Iterator<Item = char>>(it: &mut I) -> Box<[Op]> {
let mut buf = vec![];
while let Some(c) = it.next() {
buf.push( match c {
'+' => Inc(1),
'-' => Inc(-1),
'>' => Move(1),
'<' => Move(-1),
buf.push(match c {
'-' => Dec,
'+' => Inc,
'<' => Prev,
'>' => Next,
'.' => Print,
'[' => Loop(parse(it)),
']' => break,
_ => continue,
} );
});
}
buf.into_boxed_slice()
}

struct Program {
ops: Box<[Op]>
ops: Box<[Op]>,
}

impl Program {
fn new(code: String) -> Program { Program { ops: parse(&mut code.chars()) } }
fn new(code: &str) -> Self {
Self {
ops: parse(&mut code.chars()),
}
}

fn run(&self, p: &mut Printer) {
let mut tape = Tape::new();
_run(&self.ops, &mut tape, p);
run(&self.ops, &mut tape, p);
}
}

fn notify(msg: &str) {
use std::io::Write;

if let Ok(mut stream) = std::net::TcpStream::connect("localhost:9001") {
stream.write_all(msg.as_bytes()).unwrap();
stream.write_all(msg.as_bytes()).ok();
}
}

fn verify() {
let s = String::from("++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>
---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.");
let s = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>
---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.";
let mut p_left = Printer::new(true);
Program::new(s).run(&mut p_left);
let left = p_left.get_checksum();
Expand All @@ -114,8 +166,8 @@ fn verify() {
}
let right = p_right.get_checksum();
if left != right {
eprintln!("{:?} != {:?}", left, right);
std::process::exit(-1);
eprintln!("{} != {}", left, right);
process::exit(-1);
}
}

Expand All @@ -124,13 +176,14 @@ fn main() {

let arg1 = env::args().nth(1).unwrap();
let s = fs::read_to_string(arg1).unwrap();
let mut p = Printer::new(std::env::var("QUIET").is_ok());
let mut p = Printer::new(env::var("QUIET").is_ok());

notify(&format!("Rust\t{}", std::process::id()));
Program::new(s).run(&mut p);
notify(&format!("Rust\t{}", process::id()));
Program::new(&s).run(&mut p);
notify("stop");

if p.quiet {
println!("Output checksum: {}", p.get_checksum());
}
}

3 comments on commit 7e2fc03

@jcmoyer
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit introduces a memory safety issue that wasn't present before. Consider the program <.:

With the old version, < would overflow the tape position to usize::MAX, then attempt to resize the tape in a loop until the new size is usize::MAX. This is actually a non-terminating loop because the tape length is bounded by usize too, so it can never be greater than the tape position. This is bad, but at least it's memory-safe.

With the new version up to 856b1a1 (HEAD as of writing) , < overflows the tape position to usize::MAX without any subsequent resize/bounds check, then . can be used to print the contents of arbitrary memory beyond the tape array.

This issue is also present with e.g. the current C++ implementation which checks nothing, and I would argue it is an incorrect implementation too. Since the goal of Rust is to prevent this kind of issue, removing bounds checks is pretty misleading from a benchmarking perspective. (not saying it's intentional here, it seems like either a legitimate oversight or optimizing for the test input, but in the latter case, why bother mentioning safety in recent commits at all?)

@nuald
Copy link
Collaborator

@nuald nuald commented on 7e2fc03 Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit introduces a memory safety issue that wasn't present before. Consider the program <.:

Good catch, thank you! I've looked into the original implementation (mirror in https://gitlab.com/nuald-grp/bf-2), and they have the range check. As the check could be a bottleneck, some tests (including Rust and C++) may have the unfair advantage. I've created a ticket #473 and will try to address it in my spare time. It could be a little bit a challenge for functional programming languages as it introduces the side effect, but I guess it's unavoidable as exceptional situations should be properly handled.

@ricvelozo
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the debug build an underflow will trigger a panic, that the right thing to do in this case. However, because the reference impl (C++) doesn't do bound checks, I didn't do that in release builds, or it would be unfair...

If the C++ impl change that, the Rust version can easily check using usize::checked_sub.

Please sign in to comment.