cpr_bf/
lib.rs

1//! A simple Brainfuck interpretation library
2//!
3//! The library exposes the [`BrainfuckVM`] trait, representing an object
4//! that is able to run Brainfuck programs either from source code represented
5//! as a string, or from a Brainfuck source file.
6//!
7//! In addition to this general trait, it also provides the [`VMBuilder`] struct,
8//! that can be used to create a Brainfuck VM that is customizable through various
9//! means.
10//!
11//! # Examples
12//!
13//! To simply create a basic spec-compliant Brainfuck runner, and run some Brainfuck code:
14//! ```
15//! let code = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.";
16//!
17//! let vm = cpr_bf::VMBuilder::new().build();
18//! vm.run_string(code);
19//! ```
20
21pub mod allocators;
22
23use allocators::DynamicAllocator;
24use num::{
25    traits::{WrappingAdd, WrappingSub},
26    Unsigned,
27};
28use std::{
29    any::type_name,
30    convert::{TryFrom, TryInto},
31    fmt::Display,
32    fs::File,
33    io::{self, stdin, stdout, Read, Stdin, Stdout, Write},
34    iter::repeat,
35    marker::PhantomData,
36    path::Path,
37};
38
39/// Represents a single Brainfuck instruction
40#[derive(Clone, Copy, Debug)]
41pub enum Instruction {
42    /// Increment the current data pointer by one
43    IncrDP,
44
45    /// Decrement the current data pointer by one
46    DecrDP,
47
48    /// Increment the value in the cell that the data pointer currently points to by one
49    Incr,
50
51    /// Decrements the value in the cell that the data pointer currently points to by one
52    Decr,
53
54    /// Writes the value in the cell that the data pointer currently points to, to the VM writer
55    Output,
56
57    /// Reads one byte from the VM reader and stores it in the cell that the data pointer currently points to
58    Input,
59
60    /// If the value in the currently pointed-to cell is zero, jumps forwards to the next matching [`Instruction::JumpBack`] instruction.
61    JumpFwd,
62
63    /// If the value in the currently pointer-to cell is not zero, jumps backwards to the previous matching [`Instruction::JumpFwd`] instruction.
64    JumpBack,
65}
66
67impl TryFrom<char> for Instruction {
68    type Error = ();
69
70    fn try_from(value: char) -> Result<Self, Self::Error> {
71        match value {
72            '>' => Ok(Instruction::IncrDP),
73            '<' => Ok(Instruction::DecrDP),
74            '+' => Ok(Instruction::Incr),
75            '-' => Ok(Instruction::Decr),
76            '.' => Ok(Instruction::Output),
77            ',' => Ok(Instruction::Input),
78            '[' => Ok(Instruction::JumpFwd),
79            ']' => Ok(Instruction::JumpBack),
80            _ => Err(()),
81        }
82    }
83}
84
85/// Struct representing a complete Brainfuck program.
86/// The program does not need to be constructed directly,
87/// and is instead constructed automatically through the various `run_*` methods
88/// defined on the [`BrainfuckVM`] trait.
89///
90/// If desired, however, one can be constructed through the [`From<&str>`] trait
91/// implementation defined for [`Program`]
92pub struct Program {
93    instructions: Vec<Instruction>,
94}
95
96impl From<&str> for Program {
97    fn from(input: &str) -> Self {
98        let instructions = input
99            .chars()
100            .filter_map(|c| Instruction::try_from(c).ok())
101            .collect();
102
103        Program { instructions }
104    }
105}
106
107/// This trait defines types that can be used as the datatype for a single cell of
108/// a Brainfuck VM. Can be implemented manually (although not recommended), but is
109/// already implemented for the default unsigned int types ([`u8`], [`u16`], etc.)
110pub trait BrainfuckCell:
111    Unsigned + Copy + Default + TryInto<u32> + From<u8> + WrappingAdd + WrappingSub + std::fmt::Debug
112{
113}
114
115impl<
116        T: Unsigned
117            + Copy
118            + Default
119            + TryInto<u32>
120            + From<u8>
121            + WrappingAdd
122            + WrappingSub
123            + std::fmt::Debug,
124    > BrainfuckCell for T
125{
126}
127
128/// An out-of-bounds access error returned by the
129/// Brainfuck VM if an access is attempted outside the
130/// allocated memory region, without dynamic allocation being enabled
131#[derive(Debug)]
132pub struct OutOfBoundsAccess {
133    /// The current maximum capacity of the VM, in number of cells
134    pub capacity: usize,
135
136    /// The index of the attempted access
137    pub access: usize,
138}
139
140/// A general memory error encountered during runtime by the VM
141#[derive(Debug)]
142pub enum VMMemoryError {
143    /// An out-of-bounds access
144    OutOfBounds(OutOfBoundsAccess),
145}
146
147impl From<VMMemoryError> for BrainfuckExecutionError {
148    fn from(value: VMMemoryError) -> Self {
149        BrainfuckExecutionError::MemoryError(value)
150    }
151}
152
153/// A trait representing an object that is capable of
154/// allocating memory for a Brainfuck VM
155pub trait BrainfuckAllocator {
156    /// Ensures that `data` has at least `min_size` cells available for
157    /// both reading and writing. If this function returns [`Result::Ok`],
158    /// `data[min_size - 1]` can be safely read from and written to.
159    ///
160    /// Any new cells created by this function shall be initialized
161    /// to the default value of `T`
162    fn ensure_capacity<T: BrainfuckCell>(
163        data: &mut Vec<T>,
164        min_size: usize,
165    ) -> Result<(), VMMemoryError>;
166}
167
168struct VirtualMachine<T: BrainfuckCell, A: BrainfuckAllocator, R: Read, W: Write> {
169    data_ptr: usize,
170    data: Vec<T>,
171    alloc: PhantomData<A>,
172    reader: R,
173    writer: W,
174}
175
176/// A builder struct for the default implementation of [`BrainfuckVM`]
177/// Create the default configuration with [`VMBuilder::new()`] or [`VMBuilder::default()`],
178/// customize with the member functions, and build the final VM with [`VMBuilder::build()`]
179pub struct VMBuilder<
180    T: BrainfuckCell = u8,
181    A: BrainfuckAllocator = DynamicAllocator,
182    R: Read = Stdin,
183    W: Write = Stdout,
184> {
185    initial_size: usize,
186    celltype: PhantomData<T>,
187    allocator: PhantomData<A>,
188    reader: R,
189    writer: W,
190}
191
192impl VMBuilder {
193    /// Construct a new VMBuilder with the default initial configuration
194    pub fn new() -> VMBuilder {
195        VMBuilder::default()
196    }
197}
198
199impl Default for VMBuilder {
200    /// Construct a new VMBuilder with the default initial configuration
201    fn default() -> Self {
202        VMBuilder {
203            initial_size: 0,
204            celltype: PhantomData,
205            allocator: PhantomData,
206            reader: stdin(),
207            writer: stdout(),
208        }
209    }
210}
211
212impl<T: BrainfuckCell, A: BrainfuckAllocator, R: Read, W: Write> Display for VMBuilder<T, A, R, W> {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        write!(
215            f,
216            "VMBuilder<{}, {}, {}, {}> with initial size {}",
217            type_name::<T>(),
218            type_name::<A>(),
219            type_name::<R>(),
220            type_name::<W>(),
221            self.initial_size
222        )?;
223
224        Ok(())
225    }
226}
227
228impl<T: BrainfuckCell + 'static, A: BrainfuckAllocator + 'static, R: Read + 'static, W: Write + 'static>
229    VMBuilder<T, A, R, W>
230{
231    /// Changes the type of the memory cells to `U`
232    pub fn with_cell_type<U: BrainfuckCell>(self) -> VMBuilder<U, A, R, W> {
233        VMBuilder {
234            initial_size: self.initial_size,
235            celltype: PhantomData::<U>,
236            allocator: self.allocator,
237            reader: self.reader,
238            writer: self.writer,
239        }
240    }
241
242    /// Changes the used allocator to `U`
243    pub fn with_allocator<U: BrainfuckAllocator>(self) -> VMBuilder<T, U, R, W> {
244        VMBuilder {
245            initial_size: self.initial_size,
246            celltype: self.celltype,
247            allocator: PhantomData::<U>,
248            reader: self.reader,
249            writer: self.writer,
250        }
251    }
252
253    /// Changes the amount of pre-allocated cells to `num_preallocated`
254    pub fn with_preallocated_cells(self, num_preallocated: usize) -> VMBuilder<T, A, R, W> {
255        VMBuilder {
256            initial_size: num_preallocated,
257            ..self
258        }
259    }
260
261    /// Changes the reader used by the VM as input for the running Brainfuck
262    /// programs to `reader`
263    pub fn with_reader<U: Read>(self, reader: U) -> VMBuilder<T, A, U, W> {
264        VMBuilder {
265            initial_size: self.initial_size,
266            celltype: self.celltype,
267            allocator: self.allocator,
268            reader,
269            writer: self.writer,
270        }
271    }
272
273    /// Changes the writer used by the VM as output for the running Brainfuck programs
274    /// to `writer`
275    pub fn with_writer<U: Write>(self, writer: U) -> VMBuilder<T, A, R, U> {
276        VMBuilder {
277            initial_size: self.initial_size,
278            celltype: self.celltype,
279            allocator: self.allocator,
280            reader: self.reader,
281            writer,
282        }
283    }
284
285    /// Builds the [`BrainfuckVM`] with the currently
286    /// stored configuration of this builder
287    pub fn build(self) -> Box<dyn BrainfuckVM> {
288        log::info!("Building Brainfuck VM with configuration: {}", self);
289
290        Box::new(VirtualMachine::<T, A, R, W>::new(
291            self.initial_size,
292            self.reader,
293            self.writer,
294        ))
295    }
296}
297
298/// The kind of missing jump instruction
299#[derive(Debug)]
300pub enum MissingKind {
301    JumpFwd,
302    JumpBack,
303}
304
305/// A fatal error encountered by the Brainfuck VM during program execution.
306#[derive(Debug)]
307pub enum BrainfuckExecutionError {
308    /// An unknown error
309    UnknownError,
310
311    /// An error during input or output
312    IOError(io::Error),
313
314    /// Mismatched jump instructions
315    JumpMismatchError(MissingKind),
316
317    /// An error during memory allocation or access
318    MemoryError(VMMemoryError),
319
320    /// Overflow in the data pointer
321    DataPointerOverflow,
322
323    /// Underflow in the data pointer
324    DataPointerUnderflow,
325}
326
327impl Display for BrainfuckExecutionError {
328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329        match self {
330            BrainfuckExecutionError::UnknownError => write!(f, "Unknown error"),
331            BrainfuckExecutionError::IOError(e) => write!(f, "I/O Error: {}", e),
332            BrainfuckExecutionError::JumpMismatchError(MissingKind::JumpBack) => {
333                write!(f, "Too few closing brackets")
334            }
335            BrainfuckExecutionError::JumpMismatchError(MissingKind::JumpFwd) => {
336                write!(f, "Too few opening brackets")
337            }
338            BrainfuckExecutionError::MemoryError(VMMemoryError::OutOfBounds(a)) => write!(
339                f,
340                "Out of bounds memory access at index {} (max size {})",
341                a.access, a.capacity
342            ),
343            BrainfuckExecutionError::DataPointerOverflow => write!(f, "Data pointer overflow!"),
344            BrainfuckExecutionError::DataPointerUnderflow => write!(f, "Data pointer underflow!"),
345        }
346    }
347}
348
349impl std::error::Error for BrainfuckExecutionError {
350    fn cause(&self) -> Option<&dyn std::error::Error> {
351        match self {
352            BrainfuckExecutionError::IOError(e) => Some(e),
353            _ => None,
354        }
355    }
356}
357
358impl From<()> for BrainfuckExecutionError {
359    fn from(_: ()) -> Self {
360        BrainfuckExecutionError::UnknownError
361    }
362}
363
364impl From<io::Error> for BrainfuckExecutionError {
365    fn from(value: io::Error) -> Self {
366        BrainfuckExecutionError::IOError(value)
367    }
368}
369
370impl<T: BrainfuckCell, Alloc: BrainfuckAllocator, R: Read, W: Write>
371    VirtualMachine<T, Alloc, R, W>
372{
373    fn new(init_size: usize, reader: R, writer: W) -> Self {
374        VirtualMachine {
375            data_ptr: 0,
376            data: repeat(T::default()).take(init_size).collect(),
377            alloc: PhantomData,
378            reader,
379            writer,
380        }
381    }
382
383    fn exec(
384        &mut self,
385        instrs: &[Instruction],
386        instr_ptr: usize,
387    ) -> Result<usize, BrainfuckExecutionError> {
388        let instr = instrs[instr_ptr];
389
390        log::debug!("Executing instruction {}: {:?}", instr_ptr, instr);
391
392        match instr {
393            Instruction::IncrDP => {
394                log::trace!("Old data pointer: {}", self.data_ptr);
395
396                self.data_ptr = self
397                    .data_ptr
398                    .checked_add(1)
399                    .ok_or(BrainfuckExecutionError::DataPointerOverflow)?;
400
401                log::trace!("New data pointer: {}", self.data_ptr);
402
403                Ok(instr_ptr + 1)
404            }
405            Instruction::DecrDP => {
406                log::trace!("Old data pointer: {}", self.data_ptr);
407
408                self.data_ptr = self
409                    .data_ptr
410                    .checked_sub(1)
411                    .ok_or(BrainfuckExecutionError::DataPointerUnderflow)?;
412
413                log::trace!("New data pointer: {}", self.data_ptr);
414
415                Ok(instr_ptr + 1)
416            }
417            Instruction::Incr => {
418                log::trace!("Incrementing cell {}", self.data_ptr);
419
420                Alloc::ensure_capacity(&mut self.data, self.data_ptr + 1)?;
421
422                log::trace!("Previous value: {:?}", self.data[self.data_ptr]);
423                self.data[self.data_ptr] = self.data[self.data_ptr].wrapping_add(&T::one());
424                log::trace!("New value: {:?}", self.data[self.data_ptr]);
425
426                Ok(instr_ptr + 1)
427            }
428            Instruction::Decr => {
429                log::trace!("Decrementing cell {}", self.data_ptr);
430
431                Alloc::ensure_capacity(&mut self.data, self.data_ptr + 1)?;
432
433                log::trace!("Previous value: {:?}", self.data[self.data_ptr]);
434                self.data[self.data_ptr] = self.data[self.data_ptr].wrapping_sub(&T::one());
435                log::trace!("New value: {:?}", self.data[self.data_ptr]);
436
437                Ok(instr_ptr + 1)
438            }
439            Instruction::Output => {
440                log::trace!("Outputting value at cell {}", self.data_ptr);
441
442                let val = self.data.get(self.data_ptr).cloned().unwrap_or_default();
443                let as_char: char = val
444                    .try_into()
445                    .ok()
446                    .and_then(char::from_u32)
447                    .unwrap_or(char::REPLACEMENT_CHARACTER);
448
449                log::trace!("Found value: {:?}, as char: {}", val, as_char);
450
451                write!(self.writer, "{}", as_char)?;
452
453                Ok(instr_ptr + 1)
454            }
455            Instruction::Input => {
456                log::trace!("Reading input into cell {}", self.data_ptr);
457
458                let mut buf = [0_u8; 1];
459                let num_read = self.reader.read(&mut buf)?;
460
461                if num_read == 1 {
462                    log::trace!("Read byte: {}", buf[0]);
463
464                    Alloc::ensure_capacity(&mut self.data, self.data_ptr + 1)?;
465
466                    let conv_buf: T = buf[0].into();
467
468                    log::trace!("Converted to cell type: {:?}", conv_buf);
469
470                    self.data[self.data_ptr] = conv_buf;
471                } else {
472                    log::debug!("Attempted to read input, but no input was available");
473                }
474
475                Ok(instr_ptr + 1)
476            }
477            Instruction::JumpFwd => {
478                let val = self.data.get(self.data_ptr).cloned().unwrap_or_default();
479
480                if val != T::zero() {
481                    log::trace!(
482                        "Value at cell {} is not zero, not jumping forward",
483                        self.data_ptr
484                    );
485                    return Ok(instr_ptr + 1);
486                }
487
488                log::trace!("Value at cell {} is zero, jumping forward", self.data_ptr);
489
490                let mut closing_tag = instr_ptr + 1;
491                let mut tag_stack: usize = 1;
492
493                while closing_tag < instrs.len() {
494                    match instrs[closing_tag] {
495                        Instruction::JumpFwd => {
496                            log::trace!(
497                                "Encountered additional JumpFwd, increasing tag stack {}=>{}",
498                                tag_stack,
499                                tag_stack + 1
500                            );
501                            tag_stack += 1
502                        }
503                        Instruction::JumpBack => {
504                            log::trace!(
505                                "Encountered JumpBack, decreasing tag stack {}=>{}",
506                                tag_stack,
507                                tag_stack - 1
508                            );
509                            tag_stack -= 1;
510                            if tag_stack == 0 {
511                                log::trace!("Found matching JumpBack at {}", closing_tag);
512                                return Ok(closing_tag);
513                            }
514                        }
515                        _ => {}
516                    }
517
518                    closing_tag += 1;
519                }
520
521                log::error!("No matching JumpBack found for JumpFwd at {}", instr_ptr);
522
523                Err(BrainfuckExecutionError::JumpMismatchError(
524                    MissingKind::JumpBack,
525                ))
526            }
527            Instruction::JumpBack => {
528                let val = self.data.get(self.data_ptr).cloned().unwrap_or_default();
529
530                if val == T::zero() {
531                    log::trace!("Value at cell {} is zero, not jumping back", self.data_ptr);
532                    return Ok(instr_ptr + 1);
533                }
534
535                if instr_ptr == 0 {
536                    log::error!("Instruction pointer is already 0, no matching opening bracket can be found");
537
538                    return Err(BrainfuckExecutionError::JumpMismatchError(
539                        MissingKind::JumpFwd,
540                    ));
541                }
542
543                let mut opening_tag = instr_ptr - 1;
544                let mut tag_stack: usize = 1;
545
546                while opening_tag > 0 {
547                    match instrs[opening_tag] {
548                        Instruction::JumpFwd => {
549                            log::trace!(
550                                "Encountered JumpFwd, decreasing tag stack {}=>{}",
551                                tag_stack,
552                                tag_stack - 1
553                            );
554                            tag_stack -= 1;
555                            if tag_stack == 0 {
556                                log::trace!("Found matching JumpFwd at {}", opening_tag);
557                                return Ok(opening_tag);
558                            }
559                        }
560                        Instruction::JumpBack => {
561                            log::trace!(
562                                "Encountered additional JumpBack, increasing tag stack {}=>{}",
563                                tag_stack,
564                                tag_stack + 1
565                            );
566                            tag_stack += 1
567                        }
568                        _ => {}
569                    }
570
571                    opening_tag -= 1;
572                }
573
574                log::error!("No matching JumpFwd found for JumpBack at {}", instr_ptr);
575
576                Err(BrainfuckExecutionError::JumpMismatchError(
577                    MissingKind::JumpFwd,
578                ))
579            }
580        }
581    }
582}
583
584/// The result of the execution of a Brainfuck program
585pub type BfResult = Result<(), BrainfuckExecutionError>;
586
587/// This trait represents an object that is able to
588/// run Brainfuck programs, either from a string
589/// of Brainfuck source code or by reading a Brainfuck source file
590///
591/// A default implementation can be constructed using [`VMBuilder`]
592pub trait BrainfuckVM {
593    /// Runs the given Brainfuck program on this VM.
594    /// After the program has been run, the memory of the VM
595    /// is *not* automatically reset back to zero. (see [`BrainfuckVM::reset_memory`])
596    ///
597    /// Note that the VM might not be new, so the VM must take
598    /// care of resetting the data pointer back to zero before
599    /// running the program
600    fn run_program(&mut self, program: &Program) -> BfResult;
601
602    /// Resets all currently allocated memory cells back to their default
603    /// value, as if no program has been run on the VM before.
604    /// This does not free any cells that were allocated during the execution
605    /// of any previous Brainfuck programs.
606    fn reset_memory(&mut self);
607
608    /// Compiles and runs the given string of Brainfuck source code.
609    /// See [`BrainfuckVM::run_program`]
610    fn run_string(&mut self, bf_str: &str) -> BfResult {
611        log::info!("Running string of {} bytes", bf_str.len());
612
613        let program: Program = bf_str.into();
614
615        self.run_program(&program)
616    }
617
618    /// Reads the given file into a string, and
619    /// runs the string on this VM.
620    ///
621    /// See [`BrainfuckVM::run_string`]
622    fn run_file(&mut self, file: &mut File) -> BfResult {
623        log::info!(
624            "Running file of size {}",
625            file.metadata()
626                .ok()
627                .map(|meta| meta.len().to_string())
628                .unwrap_or("{unknown size}".to_owned())
629        );
630
631        let mut program_str = String::new();
632        file.read_to_string(&mut program_str)?;
633
634        self.run_string(&program_str)
635    }
636
637    /// Opens the file pointed to by the given path,
638    /// and attempts to run its contents on this VM.
639    ///
640    /// See [`BrainfuckVM::run_file`]
641    fn run_from_path(&mut self, path: &Path) -> BfResult {
642        log::info!("Running program at path {:?}", path);
643
644        let mut file = File::open(path)?;
645
646        self.run_file(&mut file)
647    }
648}
649
650impl<T: BrainfuckCell, A: BrainfuckAllocator, R: Read, W: Write> BrainfuckVM
651    for VirtualMachine<T, A, R, W>
652{
653    fn reset_memory(&mut self) {
654        log::info!("Resetting VM memory cells");
655
656        self.data.iter_mut().for_each(|cell| *cell = T::default());
657    }
658
659    fn run_program(&mut self, program: &Program) -> Result<(), BrainfuckExecutionError> {
660        log::info!("Running program");
661
662        if program.instructions.is_empty() {
663            log::info!("Program empty, returning");
664            return Ok(());
665        }
666
667        self.data_ptr = 0;
668        let mut instr_ptr = 0;
669
670        while instr_ptr < program.instructions.len() {
671            instr_ptr = self.exec(&program.instructions, instr_ptr)?;
672        }
673
674        log::debug!("Flushing writer");
675        self.writer.flush()?;
676
677        Ok(())
678    }
679}