brainfuck/brainfuck.rs
1//! Brainfuck interpreter written in Rust, using Logos.
2//!
3//! Usage:
4//! cargo run --example brainfuck <path/to/file>
5//!
6//! Example:
7//! cargo run --example brainfuck examples/hello_word.bf
8//!
9//! Brainfuck is an esoteric programming language that only
10//! uses 8 single-character commands:
11//! - '>';
12//! - '<';
13//! - '+';
14//! - '-';
15//! - '.';
16//! - ',';
17//! - '[';
18//! - and ']'.
19//!
20//! Despite being very hard to use in practice, this makes
21//! this language very simple to interpret. The following code
22//! defines an [`execute`] function that runs Brainfuck code.
23//!
24//! Logos is used here to directly transform the code stream
25//! into meaningful `Op` operations (or commands).
26//! Errors, i.e., unknown tokens, are discarded using `filter_map`.
27//!
28//! More details can be found on Wikipedia:
29//! <https://en.wikipedia.org/wiki/Brainfuck>.
30//!
31//! or on <http://brainfuck.org/>.
32
33/* ANCHOR: all */
34use logos::Logos;
35use std::collections::HashMap;
36use std::env;
37use std::fs;
38use std::io::{self, Read};
39
40/* ANCHOR: tokens */
41/// Each [`Op`] variant is a single character.
42#[derive(Debug, Logos)]
43// skip all non-op characters
44#[logos(skip(".|\n", priority = 0))]
45enum Op {
46 /// Increment pointer.
47 #[token(">")]
48 IncPointer,
49 /// Decrement pointer.
50 #[token("<")]
51 DecPointer,
52 /// Increment data at pointer.
53 #[token("+")]
54 IncData,
55 /// Decrement data at pointer.
56 #[token("-")]
57 DecData,
58 /// Output data at pointer.
59 #[token(".")]
60 OutData,
61 /// Input (read) to data at pointer.
62 #[token(",")]
63 InpData,
64 /// Conditionally jump to matching `']'`.
65 #[token("[")]
66 CondJumpForward,
67 /// Conditionally jump to matching `'['`.
68 #[token("]")]
69 CondJumpBackward,
70}
71/* ANCHOR_END: tokens */
72
73/// Print one byte to the terminal.
74#[inline(always)]
75fn print_byte(byte: u8) {
76 print!("{}", byte as char);
77}
78
79/// Read one byte from the terminal.
80#[inline(always)]
81fn read_byte() -> u8 {
82 let mut input = [0u8; 1];
83 io::stdin()
84 .read_exact(&mut input)
85 .expect("An error occurred while reading byte!");
86 input[0]
87}
88
89/// Execute Brainfuck code from a string slice.
90pub fn execute(code: &str) {
91 let operations: Vec<_> = Op::lexer(code).collect::<Result<_, _>>().unwrap();
92 let mut data = [0u8; 30_000]; // Minimum recommended size
93 let mut pointer: usize = 0;
94 let len = operations.len();
95
96 // We pre-process matching jump commands, and we create
97 // a mapping between them.
98 let mut queue = Vec::new();
99 let mut pairs = HashMap::new();
100 let mut pairs_reverse = HashMap::new();
101
102 for (i, op) in operations.iter().enumerate() {
103 match op {
104 Op::CondJumpForward => queue.push(i),
105 Op::CondJumpBackward => {
106 if let Some(start) = queue.pop() {
107 pairs.insert(start, i);
108 pairs_reverse.insert(i, start);
109 } else {
110 panic!(
111 "Unexpected conditional backward jump at position {}, does not match any '['",
112 i
113 );
114 }
115 }
116 _ => (),
117 }
118 }
119
120 if !queue.is_empty() {
121 panic!("Unmatched conditional forward jump at positions {:?}, expecting a closing ']' for each of them", queue);
122 }
123
124 /* ANCHOR: fsm */
125 let mut i: usize = 0;
126 // True program execution.
127 loop {
128 match operations[i] {
129 Op::IncPointer => pointer += 1,
130 Op::DecPointer => pointer -= 1,
131 Op::IncData => data[pointer] = data[pointer].wrapping_add(1),
132 Op::DecData => data[pointer] = data[pointer].wrapping_sub(1),
133 Op::OutData => print_byte(data[pointer]),
134 Op::InpData => data[pointer] = read_byte(),
135 Op::CondJumpForward => {
136 if data[pointer] == 0 {
137 // Skip until matching end.
138 i = *pairs.get(&i).unwrap();
139 }
140 }
141 Op::CondJumpBackward => {
142 if data[pointer] != 0 {
143 // Go back to matching start.
144 i = *pairs_reverse.get(&i).unwrap();
145 }
146 }
147 }
148 i += 1;
149
150 if i >= len {
151 break;
152 }
153 }
154 /* ANCHOR_END: fsm */
155}
156
157fn main() {
158 let src = fs::read_to_string(env::args().nth(1).expect("Expected file argument"))
159 .expect("Failed to read file");
160
161 execute(src.as_str());
162}
163/* ANCHOR_END: all */