rust_bf/
writer.rs

1//! A tiny Brainfuck generator library.
2//!
3//! This crate provides a minimal Brainfuck generator that operates on
4//! user input to generate an appropriate Brainfuck string.
5//!
6//! Features and behaviors:
7//! - Properly handles nested loops `[]`; unmatched brackets are reported as errors.
8//! - Any non-Brainfuck character causes an error.
9//!
10//! Quick start:
11//!
12//! ```no_run
13//! use rust_bf::{BrainfuckWriter, WriterOptions};
14//!
15//! // Classic "Hello World!" in Brainfuck
16//! let input = "Hello World!".as_bytes();
17//! let bf = BrainfuckWriter::new(input);
18//! let output = bf.generate().unwrap();
19//! println!("{}", output);
20//! println!(); // ensure a trailing newline for readability
21//! ```
22
23use std::cmp::Ordering;
24
25/// Errors that can occur while generating Brainfuck code.
26#[derive(Debug)]
27pub enum BrainfuckWriterError {
28    /// Encountered a character outside the Brainfuck instruction set `><+-.,[]`.
29    InvalidCharacter,
30    /// Loops were not balanced; a matching `[` or `]` was not found.
31    UnmatchedBrackets,
32    /// An underlying I/O error occurred when reading from stdin.
33    IoError(std::io::Error),
34}
35
36pub struct WriterOptions {
37    pub use_loops: bool, // Use loop-based multiplication when building from zero
38    pub max_loop_factor: u8, // Maximum outer loop counter to consider (e.g., 16..32 is fine)
39    pub assume_wrapping_u8: bool, // Assume BF cells wrap (most interpreters do)
40}
41
42impl Default for WriterOptions {
43    fn default() -> Self {
44        Self {
45            use_loops: true,
46            max_loop_factor: 16,
47            assume_wrapping_u8: true,
48        }
49    }
50}
51
52pub struct BrainfuckWriter<'writer> {
53    input: Box<&'writer [u8]>,
54    options: WriterOptions,
55}
56
57impl<'writer> BrainfuckWriter<'writer> {
58    pub fn new(input: &'writer [u8]) -> Self {
59        let options = WriterOptions::default();
60        Self { input: Box::new(input), options }
61    }
62    pub fn with_options(input: &'writer [u8], options: WriterOptions) -> Self {
63        Self { input: Box::new(input), options }
64    }
65
66    pub fn generate(&self) -> Result<String, BrainfuckWriterError> {
67        let mut output = String::new();
68        let mut cursor = 0u8;
69
70        for b in self.input.iter() {
71            // Option A: delta encodes from cursor -> b using wrapping arithmetic
72            let delta_sequence = self.encode_delta(cursor, *b);
73
74            // Option B: clear and rebuild from zero (no reliance on wrapping)
75            let from_zero_sequence = self.encode_from_zero(*b);
76
77            // Choose the shorter option for this byte
78            let best_sequence = if delta_sequence.len() <= from_zero_sequence.len() {
79                delta_sequence
80            } else {
81                from_zero_sequence
82            };
83
84            output.push_str(&best_sequence);
85            output.push('.');
86
87            cursor = *b;
88        }
89
90        Ok(output)
91    }
92
93    /// Encode the shortest delta from cursor to target.
94    /// If `assume_wrapping` is true, it computes the shortest path on a ring of 256.
95    /// If false, we avoid wrap and prefer clear+build, but still produce a non-wrapping delta.
96    fn encode_delta(&self, cursor: u8, target: u8) -> String {
97        if cursor == target {
98            return String::new();
99        }
100
101        let mut output = String::new();
102        if self.options.assume_wrapping_u8 {
103            // Compute the shortest path on a ring of 256
104            let forward = (target.wrapping_sub(cursor)) as u8; // `+` count
105            let backward = (cursor.wrapping_sub(target)) as u8; // `-` count
106            if forward <= backward {
107                for _ in 0..forward { output.push('+'); }
108            } else {
109                for _ in 0..backward { output.push('-'); }
110            }
111        } else {
112            match target.cmp(&cursor) {
113                Ordering::Greater => for _ in 0..(target - cursor) { output.push('+'); },
114                Ordering::Less => for _ in 0..(cursor - target) { output.push('-'); },
115                Ordering::Equal => {}
116            }
117        }
118
119        output
120    }
121
122    /// Build exact value `target` in the current cell starting from an unknown prior value.
123    fn encode_from_zero(&self, target: u8) -> String {
124        // Always start by clearing the current cell
125        let mut best = String::from("[-]");
126        best.push_str(&"+".repeat(target as usize));
127
128        if !self.options.use_loops || target == 0 {
129            return best;
130        }
131
132        // Try loop-based constructions of the form:
133        // Ensure temp cell (>) is zero: >[-]<
134        // Set current to 'a': '+' * a
135        // [ > '+' * b < - ] ; multiply a*b into temp, clear current
136        // > adjust remainder r = cursor - a*b with '+' or '-'
137        // [<+>-] ; move result back to current, clear temp, pointer at temp
138        // < ; return to current
139        //
140        // This leaves current == cursor, temp == 0, pointer back at current.
141        //
142        // We search a in [1..max_factor], b ~ round(cursor / a), clamp b to [1..=255],
143        // and adjust the small remainder with +/-.
144        let mut best_len = best.len();
145
146        for a in 1..=self.options.max_loop_factor {
147            // choose b as nearest integer to cursor / a, but at least 1
148            let b_f = (target as f32) / (a as f32);
149            let mut b = b_f.round() as i32;
150            if b < 1 { b = 1; }
151            if b > 255 { b = 255; }
152
153            let prod = (a as i32) * b;
154            let mut seq = String::new();
155            seq.push_str("[-]"); // clear current cell
156            seq.push_str(">[-]<"); // ensure temp cell is zero
157
158            seq.push_str(&"+".repeat(a as usize));
159            seq.push('[');
160            seq.push('>');
161            seq.push_str(&"+".repeat(b as usize));
162            seq.push('<');
163            seq.push('-');
164            seq.push(']');
165
166            // Move to temp and adjust remainder
167            seq.push('>');
168            let r = (target as i32) - prod;
169            if r > 0 {
170                seq.push_str(&"+".repeat(r as usize));
171            } else if r < 0 {
172                seq.push_str(&"-".repeat((-r) as usize));
173            }
174
175            // Move value back to current cell and return pointer
176            seq.push_str("[<+>-");
177            seq.push(']');
178            seq.push('<'); // return to current cell
179
180            if seq.len() < best_len {
181                best_len = seq.len();
182                best = seq;
183            }
184        }
185
186        best
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    
194    #[test]
195    fn simple_hello() {
196        let input = "Hello World!".as_bytes();
197        let writer = BrainfuckWriter::new(input);
198        let output = writer.generate().unwrap();
199        assert!(output.contains('.'));
200        assert!(output.len() > 0);
201    }
202    
203    #[test]
204    fn zero_and_repeat() {
205        let options = WriterOptions {
206            use_loops: true,
207            max_loop_factor: 16,
208            assume_wrapping_u8: true,
209        };
210        let input = &[0u8, 0u8, 0u8];
211        let writer = BrainfuckWriter::with_options(&*input, options);
212        let output = writer.generate().unwrap();
213        assert_eq!(output, "...");
214        assert_eq!(output.matches('.').count(), 3);
215    }
216}