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}