rust_bf/
reader.rs

1//! A tiny Brainfuck interpreter library.
2//!
3//! This crate provides a minimal Brainfuck interpreter that operates on a
4//! memory tape (default 30,000 cells) with a single data pointer.
5//!
6//! Features and behaviors:
7//! - Memory tape initialized to 0.
8//! - Strict pointer bounds: moving left from cell 0 or right past the end
9//!   returns an error.
10//! - Input `,` reads a single byte from stdin; on EOF the current cell is set to 0.
11//! - Output `.` prints the byte at the current cell as a character (no newline).
12//! - Properly handles nested loops `[]`; unmatched brackets are reported as errors.
13//! - Any non-Brainfuck character causes an error.
14//!
15//! Quick start:
16//!
17//! ```no_run
18//! use rust_bf::BrainfuckReader;
19//!
20//! // Classic "Hello World!" in Brainfuck
21//! let code = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.";
22//! let mut bf = BrainfuckReader::new(code.to_string());
23//! bf.run().expect("program should run");
24//! println!(); // ensure a trailing newline for readability
25//! ```
26
27use std::fmt;
28use std::sync::{
29    atomic::{AtomicBool, Ordering},
30    Arc,
31};
32
33/// Errors that can occur while interpreting Brainfuck code.
34#[derive(Debug, thiserror::Error)]
35pub enum BrainfuckReaderError {
36    /// The data pointer attempted to move left of cell 0 or beyond the last cell.
37    #[error("Pointer out of bounds at instruction {ip} (ptr={ptr}, op='{op}')")]
38    PointerOutOfBounds { ip: usize, ptr: usize, op: char },
39    
40    /// Encountered a character outside the Brainfuck instruction set `><+-.,[]`.
41    #[error("Invalid character: '{ch}' at instruction {ip}")]
42    InvalidCharacter { ch: char, ip: usize },
43    
44    /// Loops were not balanced; a matching `[` or `]` was not found.
45    #[error("Unmatched bracket {kind} at instruction {ip}")]
46    UnmatchedBrackets{ ip: usize, kind: UnmatchedBracketKind },
47    
48    /// An underlying I/O error occurred when reading from stdin.
49    #[error("I/O error at instruction {ip}: {source}")]
50    IoError { ip: usize, #[source] source: std::io::Error },
51
52    /// Execution aborted due to step limit.
53    #[error("Execution aborted: step limit exceeded ({limit})")]
54    StepLimitExceeded { limit: usize },
55
56    /// Execution aborted due to cooperative cancellation (e.g., timeout)
57    #[error("Execution aborted: cancelled")]
58    Canceled,
59}
60
61/// Which side of the loop was unmatched.
62#[derive(Debug, Clone, Copy)]
63pub enum UnmatchedBracketKind {
64    Open,
65    Close,
66}
67
68impl fmt::Display for UnmatchedBracketKind {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            UnmatchedBracketKind::Open => write!(f, "'['"),
72            UnmatchedBracketKind::Close => write!(f, "']'"),
73        }
74    }
75}
76
77/// Controls for cooperative cancellation and step limiting.
78#[derive(Clone)]
79pub struct StepControl {
80    pub max_steps: Option<usize>,
81    pub cancel_flag: Arc<AtomicBool>,
82}
83
84impl StepControl {
85    pub fn new(max_steps: Option<usize>, cancel_flag: Arc<AtomicBool>) -> Self {
86        Self { max_steps, cancel_flag }
87    }
88}
89
90/// A simple Brainfuck interpreter.
91///
92/// The interpreter maintains:
93/// - the program codes as a `String`,
94/// - a customizable capacity memory tape initialized to zeros (30,000 cells by default),
95/// - a data pointer indexing into that tape.
96pub struct BrainfuckReader {
97    code: String,
98    memory: Vec<u8>,
99    pointer: usize,
100    // Optional hooks:
101    output_sink: Option<Box<dyn Fn(&[u8]) + Send + Sync>>,
102    input_provider: Option<Box<dyn Fn() -> Option<u8> + Send + Sync>>,
103    // (window_size, observer (ptr, base, window_slice))
104    tape_observer: Option<(usize, Box<dyn Fn(usize, usize, &[u8]) + Send + Sync>)>,
105}
106
107impl BrainfuckReader {
108    /// Create a new interpreter from Brainfuck `code`.
109    ///
110    /// The memory tape is initialized to 30,000 zeroed cells.
111    pub fn new(code: String) -> Self {
112        Self {
113            code,
114            memory: vec![0; 30000],
115            pointer: 0,
116            output_sink: None,
117            input_provider: None,
118            tape_observer: None,
119        }
120    }
121
122    /// Create a new interpreter from Brainfuck `code` but with a custom memory size.
123    pub fn new_with_memory(code: String, memory_size: usize) -> Self {
124        Self {
125            code,
126            memory: vec![0; memory_size],
127            pointer: 0,
128            output_sink: None,
129            input_provider: None,
130            tape_observer: None,
131        }
132    }
133
134    /// Provide an output sink. When set, '.' sends bytes to this sink instead of stdout.
135    /// The sink receives a slice of bytes; for Brainfuck, it will be a single-byte slice per '.'.
136    pub fn set_output_sink<F>(&mut self, sink: F)
137    where
138        F: Fn(&[u8]) + Send + Sync + 'static,
139    {
140        self.output_sink = Some(Box::new(sink));
141    }
142
143    /// Provide an input provider. When set, ',' reads from this provider instead of stdin.
144    /// Returning None indicates EOF (cell is set to 0).
145    pub fn set_input_provider<F>(&mut self, provider: F)
146    where
147        F: Fn() -> Option<u8> + Send + Sync + 'static,
148    {
149        self.input_provider = Some(Box::new(provider));
150    }
151
152    /// Provide a tape observer and desired window size.
153    pub fn set_tape_observer<F>(&mut self, window_size: usize, observer: F)
154    where
155        // ptr: absolute data pointer
156        // base: start index of the window slice (page-aligned)
157        // window: slice view of memory[base..base+window_size]
158        F: Fn(usize, usize, &[u8]) + Send + Sync + 'static,
159    {
160        self.tape_observer = Some((window_size.max(1), Box::new(observer)));
161    }
162
163    /// Internal executor shared by run and run_debug.
164    fn execute(&mut self, debug: bool, step_control: Option<&StepControl>) -> Result<(), BrainfuckReaderError> {
165        let mut code_ptr = 0;
166        let chars: Vec<char> = self.code.chars().collect();
167        let code_len = chars.len();
168
169        // Precompute matching bracket positions for O(1) jumps and early validation.
170        // jump_map[i] holds the matching index for '[' or ']' at index i.
171        // For non-bracket positions, it is None.
172        let mut jump_map: Vec<Option<usize>> = vec![None; code_len];
173        {
174            let mut stack: Vec<usize> = Vec::new();
175            for (i, &c) in chars.iter().enumerate() {
176                if c == '[' {
177                    stack.push(i);
178                } else if c == ']' {
179                    let Some(open_index) = stack.pop() else {
180                        return Err(BrainfuckReaderError::UnmatchedBrackets {
181                            ip: i,
182                            kind: UnmatchedBracketKind::Close,
183                        });
184                    };
185                    jump_map[open_index] = Some(i);
186                    jump_map[i] = Some(open_index);
187                }
188            }
189
190            if let Some(unmatched_open) = stack.last().copied() {
191                return Err(BrainfuckReaderError::UnmatchedBrackets {
192                    ip: unmatched_open,
193                    kind: UnmatchedBracketKind::Open,
194                })
195            }
196        }
197
198        let mut step: usize = 0;
199        if debug {
200            println!("STEP | IP  | PTR | CELL | INSTR | ACTION");
201            println!("-----+-----+-----+------+-------+------------------------------------------------");
202        }
203
204        while code_ptr < code_len {
205            // Cooperative cancellation check
206            if let Some(ctrl) = step_control {
207                if ctrl.cancel_flag.load(Ordering::Relaxed) {
208                    return Err(BrainfuckReaderError::Canceled);
209                }
210            }
211
212            // Step counting
213            if let Some(ctrl) = step_control {
214                if let Some(max) = ctrl.max_steps {
215                    if step >= max {
216                        return Err(BrainfuckReaderError::StepLimitExceeded { limit: max });
217                    }
218                }
219            }
220
221            let instr = chars[code_ptr];
222            let (ptr_before, cell_before) = (self.pointer, self.memory[self.pointer]);
223            let mut action: Option<String> = if debug { Some(String::new()) } else { None };
224
225            match instr {
226                '>' => {
227                    if self.pointer >= self.memory.len() - 1 {
228                        return Err(BrainfuckReaderError::PointerOutOfBounds {
229                            ip: code_ptr,
230                            ptr: self.pointer,
231                            op: instr,
232                        });
233                    }
234                    self.pointer += 1;
235                    if let Some(a) = action.as_mut() { *a = format!("Moved pointer head to index {}", self.pointer); }
236                }
237                '<' => {
238                    if self.pointer == 0 {
239                        return Err(BrainfuckReaderError::PointerOutOfBounds {
240                        ip: code_ptr,
241                            ptr: self.pointer,
242                            op: instr,
243                        });
244                    }
245                    self.pointer -= 1;
246                    if let Some(a) = action.as_mut() { *a = format!("Moved pointer head to index {}", self.pointer); }
247                }
248                '+' => {
249                    let after = self.memory[self.pointer].wrapping_add(1);
250                    self.memory[self.pointer] = after;
251                    if let Some(a) = action.as_mut() { *a = format!("Increment cell[{}] from {} to {}", ptr_before, cell_before, after); }
252                }
253                '-' => {
254                    let after = self.memory[self.pointer].wrapping_sub(1);
255                    self.memory[self.pointer] = after;
256                    if let Some(a) = action.as_mut() { *a = format!("Decrement cell[{}] from {} to {}", ptr_before, cell_before, after); }
257                }
258                '.' => {
259                    if debug {
260                        if let Some(a) = action.as_mut() { *a = format!("Output byte '{}' (suppressed in debug)", self.memory[self.pointer] as char); }
261                    } else {
262                        // Use output sink when provided; fallback to stdout.
263                        if let Some(sink) = self.output_sink.as_ref() {
264                            let b = [self.memory[self.pointer]];
265                            (sink)(&b);
266                        } else {
267                            print!("{}", self.memory[self.pointer] as char);
268                        }
269                    }
270                }
271                ',' => {
272                    if debug {
273                        self.memory[self.pointer] = 0; // simulate EOF
274                        if let Some(a) = action.as_mut() { *a = "Read byte from stdin -> simulated EOF (set cell to 0)".to_string(); }
275                    } else {
276                        // Prefer input provider when set; fall back to stdin.
277                        if let Some(provider) = self.input_provider.as_ref() {
278                            match (provider)() {
279                                Some(b) => { self.memory[self.pointer] = b; }
280                                None => { self.memory[self.pointer] = 0; } // EOF
281                            }
282                            if let Some(a) = action.as_mut() { *a = format!("Read byte from input provider -> {}", self.memory[self.pointer]); }
283                        } else {
284                            // Read exactly one byte from stdin into the current cell.
285                            // On EOF, set the current cell to 0.
286                            use std::io::Read;
287                            let mut buf = [0u8; 1];
288                            match std::io::stdin().read(&mut buf) {
289                                Ok(0) => {
290                                    // EOF: common BF behavior is to set cell to 0
291                                    self.memory[self.pointer] = 0;
292                                }
293                                Ok(_) => {
294                                    self.memory[self.pointer] = buf[0];
295                                }
296                                Err(e) => {
297                                    return Err(BrainfuckReaderError::IoError { ip: code_ptr, source: e });
298                                }
299                            }
300                            if let Some(a) = action.as_mut() { *a = format!("Read byte from stdin -> {}", self.memory[self.pointer]); }
301                        }
302                    }
303                }
304                '[' => {
305                    if self.memory[self.pointer] == 0 {
306                        let j = jump_map[code_ptr].expect("validated bracket");
307                        if let Some(a) = action.as_mut() { *a = format!("Cell is 0; jump forward to matching ']' at IP {}", j); }
308                        code_ptr = j;
309                    } else if let Some(a) = action.as_mut() {
310                        *a = "Enter loop (cell != 0)".to_string();
311                    }
312                }
313                ']' => {
314                    if self.memory[self.pointer] != 0 {
315                        let j = jump_map[code_ptr].expect("validated bracket");
316                        if let Some(a) = action.as_mut() { *a = format!("Cell != 0; jump back to matching '[' at IP {}", j); }
317                        code_ptr = j;
318                    } else if let Some(a) = action.as_mut() {
319                        *a = "Exit loop (cell is 0)".to_string();
320                    }
321                }
322                _ => {
323                    return Err(BrainfuckReaderError::InvalidCharacter { ch: instr, ip: code_ptr });
324                }
325            }
326
327            // Notify tape observer (if any) after applying the instruction's effect.
328            if let Some((win_size, observer)) = self.tape_observer.as_ref() {
329                let base = self.pointer.saturating_sub(self.pointer % *win_size);
330                let end = (base + *win_size).min(self.memory.len());
331                (observer)(self.pointer, base, &self.memory[base..end]);
332            }
333
334            if debug {
335                println!(
336                    "{:<4} | {:<3} | {:<3} | {:<4} |  {}    | {}",
337                    step,
338                    code_ptr,
339                    ptr_before,
340                    cell_before,
341                    instr,
342                    action.unwrap_or_default()
343                );
344                step += 1;
345            }
346
347            // Advance step counter
348            step += 1;
349            // Move to the next instruction
350            code_ptr += 1;
351        }
352
353        Ok(())
354    }
355
356    /// Execute the Brainfuck program until completion.
357    ///
358    /// Returns `Ok(())` on success or a [`BrainfuckReaderError`] on failure.
359    pub fn run(&mut self) -> Result<(), BrainfuckReaderError> {
360        self.execute(false, None)
361    }
362
363    /// Debug-run the Brainfuck program, printing a step-by-step table of operations
364    /// instead of producing I/O side effects. The interpreter state (pointer, memory)
365    /// advances exactly as it would during a real run, but:
366    /// - '.' does not print the character; we log the action instead
367    /// - ',' does not read from stdin; we simulate EOF and set the cell to 0 and log
368    pub fn run_debug(&mut self) -> Result<(), BrainfuckReaderError> { self.execute(true, None) }
369
370    /// Execute with cooperative cancellation and optional step limit.
371    pub fn run_with_control(&mut self, step_control: StepControl) -> Result<(), BrainfuckReaderError> {
372        self.execute(false, Some(&step_control))
373    }
374
375    /// Debug-run with cooperative cancellation and optional step limit.
376    pub fn run_debug_with_control(&mut self, step_control: StepControl) -> Result<(), BrainfuckReaderError> {
377        self.execute(true, Some(&step_control))
378    }
379}
380
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn invalid_character_returns_error() {
388        let mut bf = BrainfuckReader::new_with_memory("+a+".to_string(), 10);
389        let result = bf.run();
390        assert!(matches!(result, Err(BrainfuckReaderError::InvalidCharacter { ch: 'a', .. })));
391    }
392
393    #[test]
394    fn unmatched_open_bracket_returns_error() {
395        // The starting cell is zero, so encountering '[' with no matching ']' should error.
396        let mut bf = BrainfuckReader::new_with_memory("[+".to_string(), 10);
397        let result = bf.run();
398        assert!(matches!(result, Err(BrainfuckReaderError::UnmatchedBrackets { kind: UnmatchedBracketKind::Open, .. })));
399    }
400
401    #[test]
402    fn left_pointer_out_of_bounds_errors() {
403        let mut bf = BrainfuckReader::new_with_memory("<".to_string(), 10);
404        let result = bf.run();
405        assert!(matches!(result, Err(BrainfuckReaderError::PointerOutOfBounds { op: '<', .. })));
406    }
407
408    #[test]
409    fn right_pointer_out_of_bounds_errors() {
410        // Move right beyond the last cell. With 3 cells (0..=2), the 3rd '>' attempts to move beyond index 2.
411        let memory_size = 3;
412        let code = ">".repeat(memory_size);
413        let mut bf = BrainfuckReader::new_with_memory(code, memory_size);
414        let result = bf.run();
415        assert!(matches!(result, Err(BrainfuckReaderError::PointerOutOfBounds { op: '>', .. })));
416    }
417
418    #[test]
419    fn empty_loop_on_zero_cell_is_ok() {
420        let mut bf = BrainfuckReader::new_with_memory("[]".to_string(), 10);
421        let result = bf.run();
422        assert!(result.is_ok());
423    }
424
425    #[test]
426    fn simple_program_without_io_runs_ok() {
427        // Increment a few times and use a loop to zero the cell.
428        // This exercises '+', '-', '[', ']' without relying on I/O or stdout.
429        let mut bf = BrainfuckReader::new_with_memory("+++[-]".to_string(), 10);
430        let result = bf.run();
431        assert!(result.is_ok());
432    }
433
434    #[test]
435    fn wrapping_subtraction() {
436        let mut bf = BrainfuckReader::new_with_memory("-".to_string(), 1);
437        let result = bf.run();
438        assert!(result.is_ok());
439        assert_eq!(bf.memory[0], 255);
440    }
441
442    #[test]
443    fn wrapping_addition() {
444        let code = "+".repeat(256); // 256 increments should wrap around
445        let mut bf = BrainfuckReader::new_with_memory(code, 1);
446        let result = bf.run();
447        assert!(result.is_ok());
448        assert_eq!(bf.memory[0], 0);
449    }
450}