use std::cmp::Ordering;
#[derive(Debug)]
pub enum BrainfuckWriterError {
InvalidCharacter,
UnmatchedBrackets,
IoError(std::io::Error),
}
pub struct WriterOptions {
pub use_loops: bool, pub max_loop_factor: u8, pub assume_wrapping_u8: bool, }
impl Default for WriterOptions {
fn default() -> Self {
Self {
use_loops: true,
max_loop_factor: 16,
assume_wrapping_u8: true,
}
}
}
pub struct BrainfuckWriter<'writer> {
input: Box<&'writer [u8]>,
options: WriterOptions,
}
impl<'writer> BrainfuckWriter<'writer> {
pub fn new(input: &'writer [u8]) -> Self {
let options = WriterOptions::default();
Self { input: Box::new(input), options }
}
pub fn with_options(input: &'writer [u8], options: WriterOptions) -> Self {
Self { input: Box::new(input), options }
}
pub fn generate(&self) -> Result<String, BrainfuckWriterError> {
let mut output = String::new();
let mut cursor = 0u8;
for b in self.input.iter() {
let delta_sequence = self.encode_delta(cursor, *b);
let from_zero_sequence = self.encode_from_zero(*b);
let best_sequence = if delta_sequence.len() <= from_zero_sequence.len() {
delta_sequence
} else {
from_zero_sequence
};
output.push_str(&best_sequence);
output.push('.');
cursor = *b;
}
Ok(output)
}
fn encode_delta(&self, cursor: u8, target: u8) -> String {
if cursor == target {
return String::new();
}
let mut output = String::new();
if self.options.assume_wrapping_u8 {
let forward = (target.wrapping_sub(cursor)) as u8; let backward = (cursor.wrapping_sub(target)) as u8; if forward <= backward {
for _ in 0..forward { output.push('+'); }
} else {
for _ in 0..backward { output.push('-'); }
}
} else {
match target.cmp(&cursor) {
Ordering::Greater => for _ in 0..(target - cursor) { output.push('+'); },
Ordering::Less => for _ in 0..(cursor - target) { output.push('-'); },
Ordering::Equal => {}
}
}
output
}
fn encode_from_zero(&self, target: u8) -> String {
let mut best = String::from("[-]");
best.push_str(&"+".repeat(target as usize));
if !self.options.use_loops || target == 0 {
return best;
}
let mut best_len = best.len();
for a in 1..=self.options.max_loop_factor {
let b_f = (target as f32) / (a as f32);
let mut b = b_f.round() as i32;
if b < 1 { b = 1; }
if b > 255 { b = 255; }
let prod = (a as i32) * b;
let mut seq = String::new();
seq.push_str("[-]"); seq.push_str(">[-]<");
seq.push_str(&"+".repeat(a as usize));
seq.push('[');
seq.push('>');
seq.push_str(&"+".repeat(b as usize));
seq.push('<');
seq.push('-');
seq.push(']');
seq.push('>');
let r = (target as i32) - prod;
if r > 0 {
seq.push_str(&"+".repeat(r as usize));
} else if r < 0 {
seq.push_str(&"-".repeat((-r) as usize));
}
seq.push_str("[<+>-");
seq.push(']');
seq.push('<');
if seq.len() < best_len {
best_len = seq.len();
best = seq;
}
}
best
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_hello() {
let input = "Hello World!".as_bytes();
let writer = BrainfuckWriter::new(input);
let output = writer.generate().unwrap();
assert!(output.contains('.'));
assert!(output.len() > 0);
}
#[test]
fn zero_and_repeat() {
let options = WriterOptions {
use_loops: true,
max_loop_factor: 16,
assume_wrapping_u8: true,
};
let input = &[0u8, 0u8, 0u8];
let writer = BrainfuckWriter::with_options(&*input, options);
let output = writer.generate().unwrap();
assert_eq!(output, "...");
assert_eq!(output.matches('.').count(), 3);
}
}