From fc9636d2aba7ce408dc717b965749c135f034866 Mon Sep 17 00:00:00 2001 From: Emil Englesson Date: Sat, 29 Mar 2025 23:34:12 +0100 Subject: [PATCH 1/4] Reorganized into modules and added the Loop and Set instruction --- src/interpreter.rs | 108 ---------------- src/interpreter/mod.rs | 112 +++++++++++++++++ src/lib.rs | 9 +- src/main.rs | 37 +++--- src/optimizer.rs | 239 ------------------------------------ src/parser.rs | 146 ---------------------- src/{ => program}/lexer.rs | 42 +++---- src/program/mod.rs | 46 +++++++ src/program/optimizer.rs | 245 +++++++++++++++++++++++++++++++++++++ src/program/parser.rs | 106 ++++++++++++++++ src/program/util.rs | 61 +++++++++ tests/integration_test.rs | 17 +-- 12 files changed, 622 insertions(+), 546 deletions(-) delete mode 100644 src/interpreter.rs create mode 100644 src/interpreter/mod.rs delete mode 100644 src/optimizer.rs delete mode 100644 src/parser.rs rename src/{ => program}/lexer.rs (65%) create mode 100644 src/program/mod.rs create mode 100644 src/program/optimizer.rs create mode 100644 src/program/parser.rs create mode 100644 src/program/util.rs diff --git a/src/interpreter.rs b/src/interpreter.rs deleted file mode 100644 index 1e247d1..0000000 --- a/src/interpreter.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::io; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum Instruction { - MoveRight(usize), - MoveLeft(usize), - Add(usize), - Sub(usize), - JumpIfZero(usize), - JumpIfNotZero(usize), - Print, - Read, - Clear, -} - -#[derive(Debug)] -pub enum Error { - Io(io::Error), - PointerOverflow, - PointerUnderflow, -} - -impl From for Error { - fn from(error: io::Error) -> Self { - Error::Io(error) - } -} - -pub struct Interpreter { - memory: Vec, - pointer: usize, -} - -impl Interpreter { - #[must_use] - pub fn new(memory_size: usize) -> Self { - Interpreter { - memory: vec![0; memory_size], - pointer: 0, - } - } - - fn read_current_cell(&self) -> u8 { - self.memory[self.pointer] - } - - fn write_current_cell(&mut self, value: u8) { - self.memory[self.pointer] = value; - } - - pub fn run( - &mut self, - program: &[Instruction], - input: &mut dyn io::Read, - output: &mut dyn io::Write, - ) -> Result<(), Error> { - let mut next_instruction = 0; - - while next_instruction < program.len() { - match program[next_instruction] { - Instruction::MoveRight(steps) => { - if self.pointer + steps >= self.memory.len() { - return Err(Error::PointerOverflow); - } - self.pointer += steps; - } - Instruction::MoveLeft(steps) => { - if self.pointer < steps { - return Err(Error::PointerUnderflow); - } - self.pointer -= steps; - } - Instruction::Add(term) => { - let value = self.read_current_cell().wrapping_add(term as u8); - self.write_current_cell(value); - } - Instruction::Sub(term) => { - let value = self.read_current_cell().wrapping_sub(term as u8); - self.write_current_cell(value); - } - Instruction::JumpIfZero(index) => { - if self.read_current_cell() == 0 { - next_instruction = index; - } - } - Instruction::JumpIfNotZero(index) => { - if self.read_current_cell() != 0 { - next_instruction = index; - } - } - Instruction::Print => { - output.write_all(&[self.read_current_cell()])?; - } - Instruction::Read => { - let mut buffer = [0; 1]; - let bytes = input.read(&mut buffer)?; - let value = if bytes > 0 { buffer[0] } else { 0 }; - self.write_current_cell(value); - } - Instruction::Clear => { - self.write_current_cell(0); - } - } - next_instruction += 1; - } - Ok(()) - } -} diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs new file mode 100644 index 0000000..8d4315e --- /dev/null +++ b/src/interpreter/mod.rs @@ -0,0 +1,112 @@ +use crate::program::{Instruction, Program}; +use std::io; + +#[derive(Debug)] +pub enum Error { + Io(io::Error), + PointerOverflow, + PointerUnderflow, +} + +impl From for Error { + fn from(error: io::Error) -> Self { + Error::Io(error) + } +} + +pub struct Tape<'a> { + input: &'a mut dyn io::Read, + output: &'a mut dyn io::Write, + memory: Vec, + pointer: usize, +} + +impl<'a> Tape<'a> { + #[must_use] + pub fn new( + input: &'a mut dyn io::Read, + output: &'a mut dyn io::Write, + memory_size: usize, + ) -> Self { + Self { + input, + output, + memory: vec![0; memory_size], + pointer: 0, + } + } + + pub fn execute(&mut self, program: &Program) -> Result<(), Error> { + self.execute_instructions(program.instructions()) + } + + fn execute_instructions(&mut self, instructions: &[Instruction]) -> Result<(), Error> { + let mut next_instruction = 0; + + while next_instruction < instructions.len() { + match &instructions[next_instruction] { + Instruction::MoveRight(value) => self.move_pointer_right(*value)?, + Instruction::MoveLeft(value) => self.move_pointer_left(*value)?, + Instruction::Add(value) => self.increment_current_cell(*value as u8), + Instruction::Sub(value) => self.decrement_current_cell(*value as u8), + Instruction::Loop { body } => { + while self.read_current_cell() != 0 { + self.execute_instructions(body)?; + } + } + Instruction::Print => self.print()?, + Instruction::Read => self.read()?, + Instruction::Set(value) => self.write_current_cell(*value as u8), + } + next_instruction += 1; + } + Ok(()) + } + + fn read_current_cell(&self) -> u8 { + self.memory[self.pointer] + } + + fn write_current_cell(&mut self, value: u8) { + self.memory[self.pointer] = value; + } + + fn increment_current_cell(&mut self, value: u8) { + let value = self.read_current_cell().wrapping_add(value); + self.write_current_cell(value); + } + + fn decrement_current_cell(&mut self, value: u8) { + let value = self.read_current_cell().wrapping_sub(value); + self.write_current_cell(value); + } + + fn move_pointer_right(&mut self, steps: usize) -> Result<(), Error> { + if self.pointer + steps >= self.memory.len() { + return Err(Error::PointerOverflow); + } + self.pointer += steps; + Ok(()) + } + + fn move_pointer_left(&mut self, steps: usize) -> Result<(), Error> { + if self.pointer < steps { + return Err(Error::PointerUnderflow); + } + self.pointer -= steps; + Ok(()) + } + + fn print(&mut self) -> Result<(), Error> { + self.output.write_all(&[self.read_current_cell()])?; + Ok(()) + } + + fn read(&mut self) -> Result<(), Error> { + let mut buffer = [0; 1]; + let bytes = self.input.read(&mut buffer)?; + let value = if bytes > 0 { buffer[0] } else { 0 }; + self.write_current_cell(value); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index df1312f..22df862 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ +#![allow(clippy::missing_panics_doc)] +#![allow(clippy::missing_errors_doc)] +// TODO +#![allow(clippy::cast_possible_truncation)] + pub mod interpreter; -pub mod lexer; -pub mod optimizer; -pub mod parser; +pub mod program; diff --git a/src/main.rs b/src/main.rs index 79d2326..3331a11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use brainrust::{ - interpreter::{self, Interpreter}, - lexer, optimizer, parser, + interpreter, + program::{Program, parser}, }; use clap::{Arg, ArgAction, ArgMatches, Command, crate_name, crate_version, value_parser}; use std::{env, fs, io, time::Instant}; @@ -17,24 +17,23 @@ pub enum CliError { IoError(std::io::Error), ParsingError(parser::Error), InterpreterError(interpreter::Error), - ValidationError(String), } impl From for CliError { - fn from(io_error: std::io::Error) -> Self { - CliError::IoError(io_error) + fn from(error: std::io::Error) -> Self { + CliError::IoError(error) } } impl From for CliError { - fn from(parser_error: parser::Error) -> Self { - CliError::ParsingError(parser_error) + fn from(error: parser::Error) -> Self { + CliError::ParsingError(error) } } impl From for CliError { - fn from(interpreter_error: interpreter::Error) -> Self { - CliError::InterpreterError(interpreter_error) + fn from(error: interpreter::Error) -> Self { + CliError::InterpreterError(error) } } @@ -44,6 +43,7 @@ fn main() -> Result<(), CliError> { .long_version(crate_version!()) .about("Brainfuck interpreter") .arg_required_else_help(true) + .subcommand_required(true) .subcommand( Command::new(CLI_SUB_CMD_RUN) .about("Parses Brainfuck in the specified file and interprets it") @@ -73,9 +73,7 @@ fn main() -> Result<(), CliError> { match matches.subcommand() { Some((CLI_SUB_CMD_RUN, matches)) => handle_run(matches), - _ => Err(CliError::ValidationError(String::from( - "Invalid subcommand", - ))), + _ => unreachable!(), } } @@ -94,15 +92,16 @@ fn handle_run(matches: &ArgMatches) -> Result<(), CliError> { let total_start = Instant::now(); let contents = fs::read_to_string(input)?; - let tokens = lexer::lex(&contents); - let parsed = parser::parse(&tokens)?; - let optimized = optimizer::optimize(parsed); + let program = Program::parse(&contents)?; + let program = program.optimized(); let parse_elapsed = total_start.elapsed(); - let mut interpreter = Interpreter::new(memory); + let mut input = io::stdin(); + let mut output = io::stdout(); + let mut tape = interpreter::Tape::new(&mut input, &mut output, memory); let exec_start = Instant::now(); - interpreter.run(&optimized, &mut io::stdin(), &mut io::stdout())?; + tape.execute(&program)?; let exec_elapsed = exec_start.elapsed(); let total_elapsed = total_start.elapsed(); @@ -127,8 +126,6 @@ fn handle_run(matches: &ArgMatches) -> Result<(), CliError> { Ok(()) } - None => Err(CliError::ValidationError(String::from( - "Input file missing", - ))), + None => unreachable!(), } } diff --git a/src/optimizer.rs b/src/optimizer.rs deleted file mode 100644 index b279571..0000000 --- a/src/optimizer.rs +++ /dev/null @@ -1,239 +0,0 @@ -use crate::{ - interpreter::Instruction::{ - self, Add, Clear, JumpIfNotZero, JumpIfZero, MoveLeft, MoveRight, Sub, - }, - parser, -}; -use itertools::Itertools; -use std::ops::RangeInclusive; - -#[must_use] -pub fn optimize(instructions: Vec) -> Vec { - let iter = instructions.into_iter(); - let iter = combine_instructions(iter); - let iter = optimize_clear_loop(iter); - let mut optimized: Vec<_> = iter.collect(); - parser::link_loops(&mut optimized).unwrap() -} - -fn combine_instructions>( - instructions: It, -) -> impl Iterator { - instructions.coalesce(|previous, current| match (previous, current) { - (MoveRight(a), MoveRight(b)) => Ok(MoveRight(a + b)), - (MoveLeft(a), MoveLeft(b)) => Ok(MoveLeft(a + b)), - (Add(a), Add(b)) => Ok(Add(a + b)), - (Sub(a), Sub(b)) => Ok(Sub(a + b)), - (Clear, Clear) => Ok(Clear), - _ => Err((previous, current)), - }) -} - -fn optimize_clear_loop>( - instructions: It, -) -> impl Iterator { - fn find_clear_loop(instructions: &[Instruction]) -> Option> { - let mut run_started = false; - let mut start = 0; - for (i, item) in instructions.iter().enumerate() { - match item { - JumpIfZero(_a) => { - run_started = true; - start = i; - } - // Unless the argument is odd, the loop is actually infinite and should not - // be changed into a clear loop. It is probably possible to allow all odd - // arguments but for now an argument of 1 is the easiest to reason about. - Sub(1) | Add(1) => { /* noop */ } - JumpIfNotZero(_a) => { - if run_started { - // Check to make sure that only a single instruction is between the two brackets - if start + 2 == i { - return Some(start..=i); - } - // Reset run - run_started = false; - } - } - _ => { - run_started = false; - } - }; - } - None - } - - let mut instructions = instructions.collect::>(); - - while let Some(range) = find_clear_loop(&instructions) { - instructions.splice(range, vec![Clear].into_iter()); - } - - instructions.into_iter() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_basic_combine() { - use Instruction::*; - let input = vec![ - Add(1), - Add(2), - Add(3), - Add(4), - Sub(4), - Sub(3), - MoveLeft(2), - MoveLeft(1), - MoveRight(3), - MoveRight(1), - Clear, - Clear, - ]; - let expected = vec![Add(10), Sub(7), MoveLeft(3), MoveRight(4), Clear]; - let optimized: Vec = combine_instructions(input.into_iter()).collect(); - assert_eq!(optimized, expected); - } - - #[test] - fn test_no_combinations() { - use Instruction::*; - let input = vec![ - Add(1), - Sub(1), - Add(1), - Sub(1), - Add(1), - Clear, - Add(1), - Clear, - MoveLeft(1), - MoveRight(1), - MoveLeft(1), - MoveRight(1), - ]; - let optimized: Vec = combine_instructions(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - } - - #[test] - fn test_non_combinable_instructions() { - use Instruction::*; - let input = vec![ - JumpIfZero(1), - JumpIfNotZero(0), - JumpIfZero(1), - JumpIfZero(1), - JumpIfNotZero(1), - JumpIfNotZero(1), - Read, - Read, - Print, - Print, - ]; - let optimized: Vec = combine_instructions(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - } - - #[test] - fn test_combine_empty_input() { - let input = vec![]; - let optimized: Vec = combine_instructions(input.into_iter()).collect(); - assert_eq!(optimized, vec![]); - } - - #[test] - fn test_clear_loop_empty_input() { - let input = vec![]; - let optimized: Vec = optimize_clear_loop(input.into_iter()).collect(); - assert_eq!(optimized, vec![]); - } - - #[test] - fn test_basic_subtract_clear_loop() { - use Instruction::*; - let input = vec![JumpIfZero(2), Sub(1), JumpIfNotZero(0)]; - let optimized: Vec = optimize_clear_loop(input.into_iter()).collect(); - assert_eq!(optimized, vec![Clear]); - } - - #[test] - fn test_basic_add_clear_loop() { - use Instruction::*; - let input = vec![JumpIfZero(2), Add(1), JumpIfNotZero(0)]; - let optimized: Vec = optimize_clear_loop(input.into_iter()).collect(); - assert_eq!(optimized, vec![Clear]); - } - - #[test] - fn test_combined_subtract_clear_loop() { - use Instruction::*; - let input = vec![JumpIfZero(2), Sub(5), JumpIfNotZero(0)]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - } - - #[test] - fn test_combined_add_clear_loop() { - use Instruction::*; - let input = vec![JumpIfZero(2), Add(5), JumpIfNotZero(0)]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - } - - #[test] - fn test_clear_loop_invalid_input() { - use Instruction::*; - let input = vec![JumpIfZero(2), Sub(1), Sub(1), JumpIfNotZero(0)]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - - let input = vec![JumpIfZero(4), Sub(1), Sub(1), Sub(1), JumpIfNotZero(0)]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - - let input = vec![ - JumpIfZero(5), - Sub(1), - Sub(1), - Sub(1), - Sub(1), - JumpIfNotZero(0), - ]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - - let input = vec![ - JumpIfZero(6), - Sub(1), - Sub(1), - Sub(1), - Sub(1), - Sub(1), - JumpIfNotZero(0), - ]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - - let input = vec![ - JumpIfZero(4), - JumpIfZero(3), - MoveRight(1), - JumpIfNotZero(1), - JumpIfNotZero(0), - ]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - - let input = vec![JumpIfZero(2), Sub(1), Add(1), JumpIfNotZero(0)]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - - let input = vec![JumpIfZero(1), JumpIfNotZero(0)]; - let optimized: Vec = optimize_clear_loop(input.clone().into_iter()).collect(); - assert_eq!(optimized, input); - } -} diff --git a/src/parser.rs b/src/parser.rs deleted file mode 100644 index 173d5d9..0000000 --- a/src/parser.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::{interpreter::Instruction, lexer::Command}; - -pub fn parse(commands: &[Command]) -> Result, Error> { - let mut instructions: Vec = commands - .iter() - .map(|cmd| match cmd { - Command::MoveRight => Instruction::MoveRight(1), - Command::MoveLeft => Instruction::MoveLeft(1), - Command::Add => Instruction::Add(1), - Command::Sub => Instruction::Sub(1), - Command::JumpIfZero => Instruction::JumpIfZero(0), - Command::JumpIfNotZero => Instruction::JumpIfNotZero(0), - Command::Print => Instruction::Print, - Command::Read => Instruction::Read, - }) - .collect(); - - link_loops(&mut instructions) -} - -pub fn link_loops(program: &mut [Instruction]) -> Result, Error> { - let mut jump_stack = Vec::new(); - - for i in 0..program.len() { - match program[i] { - Instruction::JumpIfZero(_) => { - jump_stack.push(i); - } - Instruction::JumpIfNotZero(_) => { - let jump_index = jump_stack - .pop() - .ok_or_else(|| Error::Syntax(String::from("Unexpected closing bracket")))?; - - program[i] = Instruction::JumpIfNotZero(jump_index); - program[jump_index] = Instruction::JumpIfZero(i); - } - _ => {} - } - } - - if !jump_stack.is_empty() { - return Err(Error::Syntax(String::from( - "Opening bracket missing closing bracket", - ))); - } - Ok(program.to_vec()) -} - -#[derive(Debug)] -pub enum Error { - Syntax(String), -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_basic_parse() { - let input = vec![ - Command::MoveRight, - Command::MoveLeft, - Command::Add, - Command::Sub, - Command::JumpIfZero, - Command::JumpIfNotZero, - Command::Print, - Command::Read, - ]; - let expected = vec![ - Instruction::MoveRight(1), - Instruction::MoveLeft(1), - Instruction::Add(1), - Instruction::Sub(1), - Instruction::JumpIfZero(5), - Instruction::JumpIfNotZero(4), - Instruction::Print, - Instruction::Read, - ]; - assert_eq!(parse(&input).unwrap(), expected); - } - - #[test] - fn test_consecutive_instructions() { - let input = vec![ - Command::JumpIfZero, - Command::Add, - Command::Add, - Command::JumpIfNotZero, - Command::Sub, - Command::Sub, - Command::Sub, - Command::Read, - ]; - let expected = vec![ - Instruction::JumpIfZero(3), - Instruction::Add(1), - Instruction::Add(1), - Instruction::JumpIfNotZero(0), - Instruction::Sub(1), - Instruction::Sub(1), - Instruction::Sub(1), - Instruction::Read, - ]; - assert_eq!(parse(&input).unwrap(), expected); - } - - #[test] - fn test_balanced_brackets() { - let input = vec![ - Command::JumpIfZero, - Command::JumpIfZero, - Command::JumpIfZero, - Command::JumpIfZero, - Command::Add, - Command::JumpIfNotZero, - Command::JumpIfNotZero, - Command::JumpIfNotZero, - Command::JumpIfNotZero, - ]; - let expected = vec![ - Instruction::JumpIfZero(8), - Instruction::JumpIfZero(7), - Instruction::JumpIfZero(6), - Instruction::JumpIfZero(5), - Instruction::Add(1), - Instruction::JumpIfNotZero(3), - Instruction::JumpIfNotZero(2), - Instruction::JumpIfNotZero(1), - Instruction::JumpIfNotZero(0), - ]; - assert_eq!(parse(&input).unwrap(), expected); - } - - #[test] - fn test_missing_closing_bracket() { - let input = vec![Command::JumpIfZero, Command::Add]; - assert!(parse(&input).is_err()); - } - - #[test] - fn test_missing_opening_bracket() { - let input = vec![Command::Add, Command::JumpIfNotZero]; - assert!(parse(&input).is_err()); - } -} diff --git a/src/lexer.rs b/src/program/lexer.rs similarity index 65% rename from src/lexer.rs rename to src/program/lexer.rs index 3ec4fbd..f40f1f6 100644 --- a/src/lexer.rs +++ b/src/program/lexer.rs @@ -10,8 +10,8 @@ pub enum Command { Read, } -pub fn lex(input: &str) -> Vec { - input.chars().filter_map(lex_char).collect() +pub fn lex(text: &str) -> Vec { + text.chars().filter_map(lex_char).collect() } fn lex_char(chr: char) -> Option { @@ -34,46 +34,44 @@ mod tests { #[test] fn test_basic_lex() { - use Command::*; let input = "><+-[].,"; let expected = vec![ - MoveRight, - MoveLeft, - Add, - Sub, - JumpIfZero, - JumpIfNotZero, - Print, - Read, + Command::MoveRight, + Command::MoveLeft, + Command::Add, + Command::Sub, + Command::JumpIfZero, + Command::JumpIfNotZero, + Command::Print, + Command::Read, ]; assert_eq!(lex(input), expected); } #[test] fn test_lex_with_unicode() { - use Command::*; let input = "๐Ÿฆ€๐Ÿฆ€๐Ÿฆ€>๐Ÿš๐Ÿš<๐ŸŒด๐ŸŒด+๐ŸŒŠ-๐ŸŒŠ[๐ŸŒŠ]๐ŸŒŠ.๐ŸŒŠ,๐ŸŒŠ"; let expected = vec![ - MoveRight, - MoveLeft, - Add, - Sub, - JumpIfZero, - JumpIfNotZero, - Print, - Read, + Command::MoveRight, + Command::MoveLeft, + Command::Add, + Command::Sub, + Command::JumpIfZero, + Command::JumpIfNotZero, + Command::Print, + Command::Read, ]; assert_eq!(lex(input), expected); } #[test] fn test_with_empty_input() { - assert_eq!(lex(""), vec![]); + assert_eq!(lex(""), []); } #[test] fn test_with_invalid_input() { let input = "What a beautiful ๐Ÿฆ€๐Ÿฆ€๐Ÿฆ€๐Ÿš๐Ÿš๐ŸŒด๐ŸŒด๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ ocean landscape!"; - assert_eq!(lex(input), vec![]); + assert_eq!(lex(input), []); } } diff --git a/src/program/mod.rs b/src/program/mod.rs new file mode 100644 index 0000000..a56115c --- /dev/null +++ b/src/program/mod.rs @@ -0,0 +1,46 @@ +pub mod lexer; +pub mod optimizer; +pub mod parser; +mod util; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Instruction { + MoveRight(usize), + MoveLeft(usize), + Add(usize), + Sub(usize), + Loop { body: Vec }, + Print, + Read, + Set(usize), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Program { + instructions: Vec, +} + +impl From> for Program { + fn from(instructions: Vec) -> Self { + Self { instructions } + } +} + +impl Program { + pub fn parse(input: &str) -> Result { + let instructions = parser::parse(input)?; + Ok(Self { instructions }) + } + + #[must_use] + pub fn optimized(&self) -> Self { + let mut instructions = self.instructions.clone(); + optimizer::optimize(&mut instructions); + Self::from(instructions) + } + + #[must_use] + pub fn instructions(&self) -> &[Instruction] { + &self.instructions + } +} diff --git a/src/program/optimizer.rs b/src/program/optimizer.rs new file mode 100644 index 0000000..dc5ce9f --- /dev/null +++ b/src/program/optimizer.rs @@ -0,0 +1,245 @@ +use super::Instruction; +use crate::program::util; + +pub fn optimize(instructions: &mut Vec) { + combine_instructions(instructions); + optimize_clear_loop(instructions); +} + +fn combine_instructions(instructions: &mut Vec) { + use Instruction as Instr; + + // Coalesce the current block + util::coalesce_2(instructions, |current, next| match (current, next) { + (Instr::MoveRight(a), Instr::MoveRight(b)) => Some(Instr::MoveRight(a + b)), + (Instr::MoveLeft(a), Instr::MoveLeft(b)) => Some(Instr::MoveLeft(a + b)), + (Instr::Add(a), Instr::Add(b)) => Some(Instr::Add(a + b)), + (Instr::Sub(a), Instr::Sub(b)) => Some(Instr::Sub(a + b)), + (Instr::Set(_), Instr::Set(b)) => Some(Instr::Set(*b)), + _ => None, + }); + + // Recursively handle loops + for instruction in instructions.iter_mut() { + if let Instr::Loop { body } = instruction { + optimize(body); + } + } +} + +fn optimize_clear_loop(instructions: &mut Vec) { + for instruction in instructions { + if let Instruction::Loop { body } = instruction { + if matches!(body.as_slice(), [Instruction::Add(1) | Instruction::Sub(1)]) { + *instruction = Instruction::Set(0); + } else { + optimize_clear_loop(body); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_combine() { + let mut input = vec![ + Instruction::Add(1), + Instruction::Add(2), + Instruction::Add(3), + Instruction::Add(4), + Instruction::Sub(4), + Instruction::Sub(3), + Instruction::MoveLeft(2), + Instruction::MoveLeft(1), + Instruction::MoveRight(3), + Instruction::MoveRight(1), + Instruction::Set(0), + Instruction::Set(0), + ]; + let expected = vec![ + Instruction::Add(10), + Instruction::Sub(7), + Instruction::MoveLeft(3), + Instruction::MoveRight(4), + Instruction::Set(0), + ]; + combine_instructions(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_no_combinations() { + let mut input = vec![ + Instruction::Add(1), + Instruction::Sub(1), + Instruction::Add(1), + Instruction::Sub(1), + Instruction::Add(1), + Instruction::Set(0), + Instruction::Add(1), + Instruction::Set(0), + Instruction::MoveLeft(1), + Instruction::MoveRight(1), + Instruction::MoveLeft(1), + Instruction::MoveRight(1), + ]; + let expected = input.clone(); + combine_instructions(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_non_combinable_instructions() { + let mut input = vec![ + Instruction::Loop { body: vec![] }, + Instruction::Loop { + body: vec![Instruction::Read, Instruction::Read], + }, + Instruction::Loop { body: vec![] }, + Instruction::Read, + Instruction::Read, + Instruction::Print, + Instruction::Print, + ]; + let expected = input.clone(); + combine_instructions(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_combine_empty_input() { + let mut input = vec![]; + combine_instructions(&mut input); + assert_eq!(input, vec![]); + } + + #[test] + fn test_combine_multiple_nested_loops() { + let mut input = vec![Instruction::Loop { + body: vec![ + Instruction::Add(1), + Instruction::Add(2), + Instruction::Loop { + body: vec![ + Instruction::MoveRight(1), + Instruction::MoveRight(2), + Instruction::Loop { + body: vec![Instruction::Sub(1), Instruction::Sub(2)], + }, + ], + }, + ], + }]; + let expected = vec![Instruction::Loop { + body: vec![ + Instruction::Add(3), + Instruction::Loop { + body: vec![ + Instruction::MoveRight(3), + Instruction::Loop { + body: vec![Instruction::Sub(3)], + }, + ], + }, + ], + }]; + combine_instructions(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_clear_loop_empty_input() { + let mut input = vec![]; + optimize_clear_loop(&mut input); + assert_eq!(input, vec![]); + } + + #[test] + fn test_basic_subtract_clear_loop() { + let mut input = vec![Instruction::Loop { + body: vec![Instruction::Sub(1)], + }]; + optimize_clear_loop(&mut input); + assert_eq!(input, &[Instruction::Set(0)]); + } + + #[test] + fn test_basic_add_clear_loop() { + let mut input = vec![Instruction::Loop { + body: vec![Instruction::Add(1)], + }]; + optimize_clear_loop(&mut input); + assert_eq!(input, &[Instruction::Set(0)]); + } + + #[test] + fn test_combined_subtract_clear_loop() { + let mut input = vec![Instruction::Loop { + body: vec![Instruction::Sub(5)], + }]; + let expected = input.clone(); + optimize_clear_loop(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_combined_add_clear_loop() { + let mut input = vec![Instruction::Loop { + body: vec![Instruction::Add(5)], + }]; + let expected = input.clone(); + optimize_clear_loop(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_double_subtract_clear_loop() { + let mut input = vec![Instruction::Loop { + body: vec![Instruction::Sub(1), Instruction::Sub(1)], + }]; + let expected = input.clone(); + optimize_clear_loop(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_double_add_clear_loop() { + let mut input = vec![Instruction::Loop { + body: vec![Instruction::Add(1), Instruction::Add(1)], + }]; + let expected = input.clone(); + optimize_clear_loop(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_nested_subtract_clear_loop() { + let mut input = vec![Instruction::Loop { + body: vec![Instruction::Loop { + body: vec![Instruction::Sub(1)], + }], + }]; + let expected = &[Instruction::Loop { + body: vec![Instruction::Set(0)], + }]; + optimize_clear_loop(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_nested_add_clear_loop() { + let mut input = vec![Instruction::Loop { + body: vec![Instruction::Loop { + body: vec![Instruction::Add(1)], + }], + }]; + let expected = &[Instruction::Loop { + body: vec![Instruction::Set(0)], + }]; + optimize_clear_loop(&mut input); + assert_eq!(input, expected); + } +} diff --git a/src/program/parser.rs b/src/program/parser.rs new file mode 100644 index 0000000..5456f0e --- /dev/null +++ b/src/program/parser.rs @@ -0,0 +1,106 @@ +use super::{ + Instruction, + lexer::{Command, lex}, +}; + +#[derive(Debug)] +pub enum Error { + Syntax(String), +} + +pub fn parse(text: &str) -> Result, Error> { + let commands = lex(text); + // Instructions encountered in the current block + let mut instructions = vec![]; + let mut loop_stack = vec![]; + + for cmd in commands { + match cmd { + Command::MoveRight => instructions.push(Instruction::MoveRight(1)), + Command::MoveLeft => instructions.push(Instruction::MoveLeft(1)), + Command::Add => instructions.push(Instruction::Add(1)), + Command::Sub => instructions.push(Instruction::Sub(1)), + Command::Print => instructions.push(Instruction::Print), + Command::Read => instructions.push(Instruction::Read), + Command::JumpIfZero => { + loop_stack.push(instructions); + instructions = vec![]; + } + Command::JumpIfNotZero => match loop_stack.pop() { + Some(mut parent) => { + parent.push(Instruction::Loop { body: instructions }); + instructions = parent; + } + None => { + return Err(Error::Syntax("Missing opening bracket".into())); + } + }, + } + } + + if !loop_stack.is_empty() { + return Err(Error::Syntax("Missing closing bracket".into())); + } + + Ok(instructions) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_parse() { + let input = "><+-[].,"; + let expected = vec![ + Instruction::MoveRight(1), + Instruction::MoveLeft(1), + Instruction::Add(1), + Instruction::Sub(1), + Instruction::Loop { body: vec![] }, + Instruction::Print, + Instruction::Read, + ]; + assert_eq!(parse(input).unwrap(), expected); + } + + #[test] + fn test_consecutive_instructions() { + let input = "[++]---,"; + let expected = vec![ + Instruction::Loop { + body: vec![Instruction::Add(1), Instruction::Add(1)], + }, + Instruction::Sub(1), + Instruction::Sub(1), + Instruction::Sub(1), + Instruction::Read, + ]; + assert_eq!(parse(input).unwrap(), expected); + } + + #[test] + fn test_balanced_brackets() { + let input = "[[[[+]]]]"; + let expected = vec![Instruction::Loop { + body: vec![Instruction::Loop { + body: vec![Instruction::Loop { + body: vec![Instruction::Loop { + body: vec![Instruction::Add(1)], + }], + }], + }], + }]; + assert_eq!(parse(input).unwrap(), expected); + } + + #[test] + fn test_missing_closing_bracket() { + assert!(parse("[+").is_err()); + } + + #[test] + fn test_missing_opening_bracket() { + assert!(parse("+]").is_err()); + } +} diff --git a/src/program/util.rs b/src/program/util.rs new file mode 100644 index 0000000..608c504 --- /dev/null +++ b/src/program/util.rs @@ -0,0 +1,61 @@ +pub fn coalesce_2(elements: &mut Vec, merge: F) +where + T: Clone, + F: Fn(&T, &T) -> Option, +{ + if elements.is_empty() { + return; + } + + let mut write = 0; + for read in 1..elements.len() { + let current = &elements[write]; + let next = &elements[read]; + + if let Some(coalesced) = merge(current, next) { + elements[write] = coalesced; + } else { + write += 1; + elements[write] = elements[read].clone(); + } + } + + elements.truncate(write + 1); +} + +pub fn coalesce_3(elements: &mut Vec, merge: F) +where + T: Clone, + F: Fn(&T, &T, &T) -> Option, +{ + if elements.is_empty() { + return; + } + + let mut write = 0; + let mut read = 0; + + while read + 2 < elements.len() { + let current = &elements[read]; + let next = &elements[read + 1]; + let next_next = &elements[read + 2]; + + if let Some(coalesced) = merge(current, next, next_next) { + elements[write] = coalesced; + read += 3; + } else { + elements[write] = current.clone(); + read += 1; + } + write += 1; + } + + // Copy any remaining instructions + while read < elements.len() { + elements[write] = elements[read].clone(); + write += 1; + read += 1; + } + + elements.truncate(write); +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index e36c4a6..c19d5fe 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,6 +1,6 @@ use brainrust::{ - interpreter::{self, Interpreter}, - lexer, optimizer, parser, + interpreter, + program::{Program, parser}, }; use std::{fs, io, str}; @@ -39,14 +39,15 @@ test_programs! { } fn run_program(file: &str, input: &str) -> Result, TestError> { - let tokens = lexer::lex(file); - let parsed = parser::parse(&tokens)?; - let optimized = optimizer::optimize(parsed); - let mut interpreter = Interpreter::new(MEMORY_SIZE); - + let mut input = input.as_bytes(); let mut output: Vec = vec![]; + let mut tape = interpreter::Tape::new(&mut input, &mut output, MEMORY_SIZE); + + let program = Program::parse(file)?; + let program = program.optimized(); + + tape.execute(&program)?; - interpreter.run(&optimized, &mut input.as_bytes(), &mut output)?; Ok(output) } From 9aaaca32fafa7c71365e163c3ebbfd793527be3a Mon Sep 17 00:00:00 2001 From: Emil Englesson Date: Sat, 29 Mar 2025 23:44:01 +0100 Subject: [PATCH 2/4] Refactored the integration tests and removed itertools --- Cargo.lock | 23 ++++--------- Cargo.toml | 7 ++-- tests/integration_test.rs | 71 +++++++++++++++++++-------------------- 3 files changed, 46 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83568a7..d1bedc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,7 +63,7 @@ name = "brainrust" version = "0.1.4" dependencies = [ "clap", - "itertools", + "paste", ] [[package]] @@ -100,12 +100,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "errno" version = "0.3.10" @@ -122,15 +116,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "libc" version = "0.2.171" @@ -149,6 +134,12 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "rustix" version = "1.0.3" diff --git a/Cargo.toml b/Cargo.toml index e19db89..614336a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,8 @@ edition = "2024" publish = false [dependencies] -clap = { version = "4.5", features = ["cargo", "color", "wrap_help", "suggestions"] } -itertools = "0.14" +clap = { version = "4.5", features = ["cargo", "color", "suggestions", "wrap_help"] } + +[dev-dependencies] +paste = "1.0" + diff --git a/tests/integration_test.rs b/tests/integration_test.rs index c19d5fe..85e79f5 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -2,40 +2,51 @@ use brainrust::{ interpreter, program::{Program, parser}, }; -use std::{fs, io, str}; - -const PROGRAM_PREFIX: &str = "tests/programs/"; -const PROGRAM_EXTENSION: &str = ".b"; -const INPUT_EXTENSION: &str = ".input"; -const OUTPUT_EXTENSION: &str = ".output"; const MEMORY_SIZE: usize = 32768; -macro_rules! test_programs { - ($($test_name:ident: $program_name:expr,)*) => { - $( - #[test] - fn $test_name() -> Result<(), TestError> { - let program_name = $program_name; - let program = format!("{PROGRAM_PREFIX}{program_name}{PROGRAM_EXTENSION}"); - let input = format!("{PROGRAM_PREFIX}{program_name}{INPUT_EXTENSION}"); - let output = format!("{PROGRAM_PREFIX}{program_name}{OUTPUT_EXTENSION}"); - - let program = fs::read_to_string(program)?; - let input = fs::read_to_string(input)?; - let output = fs::read(output)?; +macro_rules! file_path { + ($program:ident, $ext:literal) => { + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/programs/", + stringify!($program), + $ext + ) + }; +} - let result = run_program(&program, &input)?; +macro_rules! include_file { + (string, $program:ident, $ext:literal) => { + include_str!(file_path!($program, $ext)) + }; + (bytes, $program:ident, $ext:literal) => { + include_bytes!(file_path!($program, $ext)) + }; +} - assert_eq!(result, output); - Ok(()) +macro_rules! test_programs { + ($($program:ident,)*) => { + $( + paste::item! { + #[test] + fn [< test_ $program >] () -> Result<(), TestError> { + let program = include_file!(string, $program, ".b"); + let input = include_file!(string, $program, ".input"); + let output = include_file!(bytes, $program, ".output"); + + let result = run_program(program, input)?; + + assert_eq!(result, output); + Ok(()) + } } )* } } test_programs! { - monty: "monty", + monty, } fn run_program(file: &str, input: &str) -> Result, TestError> { @@ -53,16 +64,8 @@ fn run_program(file: &str, input: &str) -> Result, TestError> { #[derive(Debug)] enum TestError { - Io, Parsing, Interpreter, - ConversationError, -} - -impl From for TestError { - fn from(_error: io::Error) -> Self { - TestError::Io - } } impl From for TestError { @@ -76,9 +79,3 @@ impl From for TestError { TestError::Interpreter } } - -impl From for TestError { - fn from(_error: str::Utf8Error) -> Self { - TestError::ConversationError - } -} From cfd612841eb9d6db57ac706429382eaa2454d9b4 Mon Sep 17 00:00:00 2001 From: Emil Englesson Date: Sat, 29 Mar 2025 23:50:51 +0100 Subject: [PATCH 3/4] Modularized the command line interface --- src/cli/mod.rs | 46 ++++++++++++++ src/cli/run.rs | 77 +++++++++++++++++++++++ src/cli/util.rs | 15 +++++ src/lib.rs | 3 + src/main.rs | 130 +-------------------------------------- src/program/optimizer.rs | 2 +- src/program/util.rs | 39 +----------- 7 files changed, 145 insertions(+), 167 deletions(-) create mode 100644 src/cli/mod.rs create mode 100644 src/cli/run.rs create mode 100644 src/cli/util.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..57a8dd9 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,46 @@ +use crate::{interpreter, program::parser}; +use clap::{Command, crate_name, crate_version}; +use std::env; + +mod run; +mod util; + +#[derive(Debug)] +pub enum CliError { + IoError(std::io::Error), + ParsingError(parser::Error), + Interpreter(interpreter::Error), +} + +impl From for CliError { + fn from(error: std::io::Error) -> Self { + CliError::IoError(error) + } +} + +impl From for CliError { + fn from(error: parser::Error) -> Self { + CliError::ParsingError(error) + } +} + +impl From for CliError { + fn from(error: interpreter::Error) -> Self { + CliError::Interpreter(error) + } +} + +pub fn run() -> Result<(), CliError> { + let matches = Command::new(crate_name!()) + .version(crate_version!()) + .about("Brainfuck interpreter") + .arg_required_else_help(true) + .subcommand_required(true) + .subcommand(run::build_command()) + .get_matches(); + + match matches.subcommand() { + Some(("run", matches)) => run::execute(matches), + _ => unreachable!(), + } +} diff --git a/src/cli/run.rs b/src/cli/run.rs new file mode 100644 index 0000000..0121429 --- /dev/null +++ b/src/cli/run.rs @@ -0,0 +1,77 @@ +use super::CliError; +use crate::{cli::util, interpreter, program::Program}; +use clap::{Arg, ArgAction, ArgMatches, Command, value_parser}; +use std::{fs, io, time::Instant}; + +const DEFAULT_MEMORY_SIZE: usize = 32768; +const ARG_INPUT_FILE: &str = "input"; +const ARG_MEMORY: &str = "memory"; +const ARG_RUN_TIME: &str = "time"; +const ARG_RUN_TIME_OPTIONS: [&str; 3] = ["total", "exec", "parse"]; + +pub fn build_command() -> Command { + Command::new("run") + .about("Parses Brainfuck in the specified file and interprets it") + .arg( + Arg::new(ARG_INPUT_FILE) + .help("The file to parse") + .index(1) + .required(true), + ) + .arg( + Arg::new(ARG_MEMORY) + .help(format!( + "Sets the number of memory cells, defaults to {DEFAULT_MEMORY_SIZE:?}" + )) + .long(ARG_MEMORY) + .value_parser(value_parser!(u32)), + ) + .arg( + Arg::new(ARG_RUN_TIME) + .help("Prints time of various metrics") + .long(ARG_RUN_TIME) + .value_parser(ARG_RUN_TIME_OPTIONS) + .action(ArgAction::Append), + ) +} + +pub fn execute(matches: &ArgMatches) -> Result<(), CliError> { + let input_file = matches + .get_one::(ARG_INPUT_FILE) + .expect("Input file is required"); + let memory = *matches.get_one(ARG_MEMORY).unwrap_or(&DEFAULT_MEMORY_SIZE); + let time_metrics: Vec<&str> = matches + .get_many::(ARG_RUN_TIME) + .unwrap_or_default() + .map(String::as_str) + .collect(); + + let total_start = Instant::now(); + let contents = fs::read_to_string(input_file)?; + let program = Program::parse(&contents)?; + let program = program.optimized(); + let parse_elapsed = total_start.elapsed(); + + let mut input = io::stdin(); + let mut output = io::stdout(); + let mut tape = interpreter::Tape::new(&mut input, &mut output, memory); + + let exec_start = Instant::now(); + tape.execute(&program)?; + let exec_elapsed = exec_start.elapsed(); + let total_elapsed = total_start.elapsed(); + + if !time_metrics.is_empty() { + println!(); + if time_metrics.contains(&"parse") { + println!("Parsing time: {}", util::format_duration(parse_elapsed)); + } + if time_metrics.contains(&"exec") { + println!("Execution time: {}", util::format_duration(exec_elapsed)); + } + if time_metrics.contains(&"total") { + println!("Total time: {}", util::format_duration(total_elapsed)); + } + } + Ok(()) +} diff --git a/src/cli/util.rs b/src/cli/util.rs new file mode 100644 index 0000000..1d101bf --- /dev/null +++ b/src/cli/util.rs @@ -0,0 +1,15 @@ +use std::time::Duration; + +pub fn format_duration(d: Duration) -> String { + let nanos = d.as_nanos(); + [ + (1_000_000_000, "s"), + (1_000_000, "ms"), + (1_000, "ยตs"), + (1, "ns"), + ] + .iter() + .find(|&&(factor, _)| nanos >= factor) + .map(|&(factor, unit)| format!("{:.1}{unit}", nanos as f64 / factor as f64)) + .unwrap_or_else(|| "pretty quick".to_string()) +} diff --git a/src/lib.rs b/src/lib.rs index 22df862..e6b7e3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,9 @@ #![allow(clippy::missing_errors_doc)] // TODO #![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_precision_loss)] +#![allow(clippy::map_unwrap_or)] +pub mod cli; pub mod interpreter; pub mod program; diff --git a/src/main.rs b/src/main.rs index 3331a11..8b34610 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,131 +1,5 @@ -use brainrust::{ - interpreter, - program::{Program, parser}, -}; -use clap::{Arg, ArgAction, ArgMatches, Command, crate_name, crate_version, value_parser}; -use std::{env, fs, io, time::Instant}; - -const DEFAULT_MEMORY_SIZE: usize = 32768; -const CLI_SUB_CMD_RUN: &str = "run"; -const CLI_ARG_INPUT_FILE: &str = "input"; -const CLI_SUB_CMD_RUN_MEMORY: &str = "memory"; -const CLI_SUB_CMD_RUN_TIME: &str = "time"; -const CLI_SUB_CMD_RUN_TIME_OPTIONS: [&str; 3] = ["total", "exec", "parse"]; - -#[derive(Debug)] -pub enum CliError { - IoError(std::io::Error), - ParsingError(parser::Error), - InterpreterError(interpreter::Error), -} - -impl From for CliError { - fn from(error: std::io::Error) -> Self { - CliError::IoError(error) - } -} - -impl From for CliError { - fn from(error: parser::Error) -> Self { - CliError::ParsingError(error) - } -} - -impl From for CliError { - fn from(error: interpreter::Error) -> Self { - CliError::InterpreterError(error) - } -} +use brainrust::cli::CliError; fn main() -> Result<(), CliError> { - let matches = Command::new(crate_name!()) - .version(crate_version!()) - .long_version(crate_version!()) - .about("Brainfuck interpreter") - .arg_required_else_help(true) - .subcommand_required(true) - .subcommand( - Command::new(CLI_SUB_CMD_RUN) - .about("Parses Brainfuck in the specified file and interprets it") - .arg( - Arg::new(CLI_ARG_INPUT_FILE) - .help("The file to parse") - .index(1) - .required(true), - ) - .arg( - Arg::new(CLI_SUB_CMD_RUN_MEMORY) - .help(format!( - "Sets the number of memory cells, defaults to {DEFAULT_MEMORY_SIZE:?}" - )) - .long(CLI_SUB_CMD_RUN_MEMORY) - .value_parser(value_parser!(u32)), - ) - .arg( - Arg::new(CLI_SUB_CMD_RUN_TIME) - .help("Prints time of various metrics") - .long(CLI_SUB_CMD_RUN_TIME) - .value_parser(CLI_SUB_CMD_RUN_TIME_OPTIONS) - .action(ArgAction::Append), - ), - ) - .get_matches(); - - match matches.subcommand() { - Some((CLI_SUB_CMD_RUN, matches)) => handle_run(matches), - _ => unreachable!(), - } -} - -fn handle_run(matches: &ArgMatches) -> Result<(), CliError> { - match matches.get_one::(CLI_ARG_INPUT_FILE) { - Some(input) => { - let memory = *matches - .get_one(CLI_SUB_CMD_RUN_MEMORY) - .unwrap_or(&DEFAULT_MEMORY_SIZE); - - let time_metrics: Vec<&str> = matches - .get_many::(CLI_SUB_CMD_RUN_TIME) - .unwrap_or_default() - .map(String::as_str) - .collect(); - - let total_start = Instant::now(); - let contents = fs::read_to_string(input)?; - let program = Program::parse(&contents)?; - let program = program.optimized(); - let parse_elapsed = total_start.elapsed(); - - let mut input = io::stdin(); - let mut output = io::stdout(); - let mut tape = interpreter::Tape::new(&mut input, &mut output, memory); - - let exec_start = Instant::now(); - tape.execute(&program)?; - let exec_elapsed = exec_start.elapsed(); - let total_elapsed = total_start.elapsed(); - - if !time_metrics.is_empty() { - println!(); - if time_metrics.contains(&"total") { - println!("Total time: {}s", total_elapsed.as_secs()); - println!(" {}ms", total_elapsed.as_millis()); - println!(" {}ยตs", total_elapsed.as_micros()); - } - if time_metrics.contains(&"exec") { - println!("Execution time: {}s", exec_elapsed.as_secs()); - println!(" {}ms", exec_elapsed.as_millis()); - println!(" {}ยตs", exec_elapsed.as_micros()); - } - if time_metrics.contains(&"parse") { - println!("Parsing time: {}s", parse_elapsed.as_secs()); - println!(" {}ms", parse_elapsed.as_millis()); - println!(" {}ยตs", parse_elapsed.as_micros()); - } - } - - Ok(()) - } - None => unreachable!(), - } + brainrust::cli::run() } diff --git a/src/program/optimizer.rs b/src/program/optimizer.rs index dc5ce9f..9c249ef 100644 --- a/src/program/optimizer.rs +++ b/src/program/optimizer.rs @@ -10,7 +10,7 @@ fn combine_instructions(instructions: &mut Vec) { use Instruction as Instr; // Coalesce the current block - util::coalesce_2(instructions, |current, next| match (current, next) { + util::coalesce(instructions, |current, next| match (current, next) { (Instr::MoveRight(a), Instr::MoveRight(b)) => Some(Instr::MoveRight(a + b)), (Instr::MoveLeft(a), Instr::MoveLeft(b)) => Some(Instr::MoveLeft(a + b)), (Instr::Add(a), Instr::Add(b)) => Some(Instr::Add(a + b)), diff --git a/src/program/util.rs b/src/program/util.rs index 608c504..5504991 100644 --- a/src/program/util.rs +++ b/src/program/util.rs @@ -1,4 +1,4 @@ -pub fn coalesce_2(elements: &mut Vec, merge: F) +pub fn coalesce(elements: &mut Vec, merge: F) where T: Clone, F: Fn(&T, &T) -> Option, @@ -22,40 +22,3 @@ where elements.truncate(write + 1); } - -pub fn coalesce_3(elements: &mut Vec, merge: F) -where - T: Clone, - F: Fn(&T, &T, &T) -> Option, -{ - if elements.is_empty() { - return; - } - - let mut write = 0; - let mut read = 0; - - while read + 2 < elements.len() { - let current = &elements[read]; - let next = &elements[read + 1]; - let next_next = &elements[read + 2]; - - if let Some(coalesced) = merge(current, next, next_next) { - elements[write] = coalesced; - read += 3; - } else { - elements[write] = current.clone(); - read += 1; - } - write += 1; - } - - // Copy any remaining instructions - while read < elements.len() { - elements[write] = elements[read].clone(); - write += 1; - read += 1; - } - - elements.truncate(write); -} From f39b4907333a63b43fd8e448773ec863411308ea Mon Sep 17 00:00:00 2001 From: Emil Englesson Date: Sat, 29 Mar 2025 23:58:33 +0100 Subject: [PATCH 4/4] Modularized the optimization techniques --- src/cli/mod.rs | 8 +- src/main.rs | 4 +- src/program/mod.rs | 11 +- src/program/optimizer.rs | 245 ------------------ src/program/optimizer/clear_loop.rs | 107 ++++++++ src/program/optimizer/combine_instructions.rs | 132 ++++++++++ src/program/optimizer/mod.rs | 35 +++ src/program/{ => optimizer}/util.rs | 0 tests/integration_test.rs | 6 +- 9 files changed, 289 insertions(+), 259 deletions(-) delete mode 100644 src/program/optimizer.rs create mode 100644 src/program/optimizer/clear_loop.rs create mode 100644 src/program/optimizer/combine_instructions.rs create mode 100644 src/program/optimizer/mod.rs rename src/program/{ => optimizer}/util.rs (100%) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 57a8dd9..8173970 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,4 @@ -use crate::{interpreter, program::parser}; +use crate::{interpreter, program}; use clap::{Command, crate_name, crate_version}; use std::env; @@ -8,7 +8,7 @@ mod util; #[derive(Debug)] pub enum CliError { IoError(std::io::Error), - ParsingError(parser::Error), + ParsingError(program::Error), Interpreter(interpreter::Error), } @@ -18,8 +18,8 @@ impl From for CliError { } } -impl From for CliError { - fn from(error: parser::Error) -> Self { +impl From for CliError { + fn from(error: program::Error) -> Self { CliError::ParsingError(error) } } diff --git a/src/main.rs b/src/main.rs index 8b34610..ca473b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -use brainrust::cli::CliError; +use brainrust::cli::{self, CliError}; fn main() -> Result<(), CliError> { - brainrust::cli::run() + cli::run() } diff --git a/src/program/mod.rs b/src/program/mod.rs index a56115c..d298911 100644 --- a/src/program/mod.rs +++ b/src/program/mod.rs @@ -1,7 +1,8 @@ -pub mod lexer; -pub mod optimizer; -pub mod parser; -mod util; +mod lexer; +mod optimizer; +mod parser; + +pub use parser::Error; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Instruction { @@ -27,7 +28,7 @@ impl From> for Program { } impl Program { - pub fn parse(input: &str) -> Result { + pub fn parse(input: &str) -> Result { let instructions = parser::parse(input)?; Ok(Self { instructions }) } diff --git a/src/program/optimizer.rs b/src/program/optimizer.rs deleted file mode 100644 index 9c249ef..0000000 --- a/src/program/optimizer.rs +++ /dev/null @@ -1,245 +0,0 @@ -use super::Instruction; -use crate::program::util; - -pub fn optimize(instructions: &mut Vec) { - combine_instructions(instructions); - optimize_clear_loop(instructions); -} - -fn combine_instructions(instructions: &mut Vec) { - use Instruction as Instr; - - // Coalesce the current block - util::coalesce(instructions, |current, next| match (current, next) { - (Instr::MoveRight(a), Instr::MoveRight(b)) => Some(Instr::MoveRight(a + b)), - (Instr::MoveLeft(a), Instr::MoveLeft(b)) => Some(Instr::MoveLeft(a + b)), - (Instr::Add(a), Instr::Add(b)) => Some(Instr::Add(a + b)), - (Instr::Sub(a), Instr::Sub(b)) => Some(Instr::Sub(a + b)), - (Instr::Set(_), Instr::Set(b)) => Some(Instr::Set(*b)), - _ => None, - }); - - // Recursively handle loops - for instruction in instructions.iter_mut() { - if let Instr::Loop { body } = instruction { - optimize(body); - } - } -} - -fn optimize_clear_loop(instructions: &mut Vec) { - for instruction in instructions { - if let Instruction::Loop { body } = instruction { - if matches!(body.as_slice(), [Instruction::Add(1) | Instruction::Sub(1)]) { - *instruction = Instruction::Set(0); - } else { - optimize_clear_loop(body); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_basic_combine() { - let mut input = vec![ - Instruction::Add(1), - Instruction::Add(2), - Instruction::Add(3), - Instruction::Add(4), - Instruction::Sub(4), - Instruction::Sub(3), - Instruction::MoveLeft(2), - Instruction::MoveLeft(1), - Instruction::MoveRight(3), - Instruction::MoveRight(1), - Instruction::Set(0), - Instruction::Set(0), - ]; - let expected = vec![ - Instruction::Add(10), - Instruction::Sub(7), - Instruction::MoveLeft(3), - Instruction::MoveRight(4), - Instruction::Set(0), - ]; - combine_instructions(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_no_combinations() { - let mut input = vec![ - Instruction::Add(1), - Instruction::Sub(1), - Instruction::Add(1), - Instruction::Sub(1), - Instruction::Add(1), - Instruction::Set(0), - Instruction::Add(1), - Instruction::Set(0), - Instruction::MoveLeft(1), - Instruction::MoveRight(1), - Instruction::MoveLeft(1), - Instruction::MoveRight(1), - ]; - let expected = input.clone(); - combine_instructions(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_non_combinable_instructions() { - let mut input = vec![ - Instruction::Loop { body: vec![] }, - Instruction::Loop { - body: vec![Instruction::Read, Instruction::Read], - }, - Instruction::Loop { body: vec![] }, - Instruction::Read, - Instruction::Read, - Instruction::Print, - Instruction::Print, - ]; - let expected = input.clone(); - combine_instructions(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_combine_empty_input() { - let mut input = vec![]; - combine_instructions(&mut input); - assert_eq!(input, vec![]); - } - - #[test] - fn test_combine_multiple_nested_loops() { - let mut input = vec![Instruction::Loop { - body: vec![ - Instruction::Add(1), - Instruction::Add(2), - Instruction::Loop { - body: vec![ - Instruction::MoveRight(1), - Instruction::MoveRight(2), - Instruction::Loop { - body: vec![Instruction::Sub(1), Instruction::Sub(2)], - }, - ], - }, - ], - }]; - let expected = vec![Instruction::Loop { - body: vec![ - Instruction::Add(3), - Instruction::Loop { - body: vec![ - Instruction::MoveRight(3), - Instruction::Loop { - body: vec![Instruction::Sub(3)], - }, - ], - }, - ], - }]; - combine_instructions(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_clear_loop_empty_input() { - let mut input = vec![]; - optimize_clear_loop(&mut input); - assert_eq!(input, vec![]); - } - - #[test] - fn test_basic_subtract_clear_loop() { - let mut input = vec![Instruction::Loop { - body: vec![Instruction::Sub(1)], - }]; - optimize_clear_loop(&mut input); - assert_eq!(input, &[Instruction::Set(0)]); - } - - #[test] - fn test_basic_add_clear_loop() { - let mut input = vec![Instruction::Loop { - body: vec![Instruction::Add(1)], - }]; - optimize_clear_loop(&mut input); - assert_eq!(input, &[Instruction::Set(0)]); - } - - #[test] - fn test_combined_subtract_clear_loop() { - let mut input = vec![Instruction::Loop { - body: vec![Instruction::Sub(5)], - }]; - let expected = input.clone(); - optimize_clear_loop(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_combined_add_clear_loop() { - let mut input = vec![Instruction::Loop { - body: vec![Instruction::Add(5)], - }]; - let expected = input.clone(); - optimize_clear_loop(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_double_subtract_clear_loop() { - let mut input = vec![Instruction::Loop { - body: vec![Instruction::Sub(1), Instruction::Sub(1)], - }]; - let expected = input.clone(); - optimize_clear_loop(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_double_add_clear_loop() { - let mut input = vec![Instruction::Loop { - body: vec![Instruction::Add(1), Instruction::Add(1)], - }]; - let expected = input.clone(); - optimize_clear_loop(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_nested_subtract_clear_loop() { - let mut input = vec![Instruction::Loop { - body: vec![Instruction::Loop { - body: vec![Instruction::Sub(1)], - }], - }]; - let expected = &[Instruction::Loop { - body: vec![Instruction::Set(0)], - }]; - optimize_clear_loop(&mut input); - assert_eq!(input, expected); - } - - #[test] - fn test_nested_add_clear_loop() { - let mut input = vec![Instruction::Loop { - body: vec![Instruction::Loop { - body: vec![Instruction::Add(1)], - }], - }]; - let expected = &[Instruction::Loop { - body: vec![Instruction::Set(0)], - }]; - optimize_clear_loop(&mut input); - assert_eq!(input, expected); - } -} diff --git a/src/program/optimizer/clear_loop.rs b/src/program/optimizer/clear_loop.rs new file mode 100644 index 0000000..96d3746 --- /dev/null +++ b/src/program/optimizer/clear_loop.rs @@ -0,0 +1,107 @@ +use crate::program::Instruction; + +pub fn optimize(instructions: &mut Vec) { + for instruction in instructions { + if let Instruction::Loop { body } = instruction { + if matches!(body.as_slice(), [Instruction::Add(1) | Instruction::Sub(1)]) { + *instruction = Instruction::Set(0); + } else { + optimize(body); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_optimizes_to(input: Vec, expected: &[Instruction]) { + let mut input = input; + optimize(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_clear_loop_empty_input() { + assert_optimizes_to(vec![], &[]); + } + + #[test] + fn test_basic_subtract_clear_loop() { + let input = vec![Instruction::Loop { + body: vec![Instruction::Sub(1)], + }]; + assert_optimizes_to(input, &[Instruction::Set(0)]); + } + + #[test] + fn test_basic_add_clear_loop() { + let input = vec![Instruction::Loop { + body: vec![Instruction::Add(1)], + }]; + assert_optimizes_to(input, &[Instruction::Set(0)]); + } + + #[test] + fn test_combined_subtract_clear_loop() { + let input = vec![Instruction::Loop { + body: vec![Instruction::Sub(5)], + }]; + assert_optimizes_to(input.clone(), &input.clone()); + } + + #[test] + fn test_combined_add_clear_loop() { + let input = vec![Instruction::Loop { + body: vec![Instruction::Add(5)], + }]; + assert_optimizes_to(input.clone(), &input.clone()); + } + + #[test] + fn test_double_subtract_clear_loop() { + let input = vec![Instruction::Loop { + body: vec![Instruction::Sub(1), Instruction::Sub(1)], + }]; + assert_optimizes_to(input.clone(), &input.clone()); + } + + #[test] + fn test_double_add_clear_loop() { + let input = vec![Instruction::Loop { + body: vec![Instruction::Add(1), Instruction::Add(1)], + }]; + assert_optimizes_to(input.clone(), &input.clone()); + } + + #[test] + fn test_nested_subtract_clear_loop() { + let input = vec![Instruction::Loop { + body: vec![Instruction::Loop { + body: vec![Instruction::Sub(1)], + }], + }]; + assert_optimizes_to( + input, + &[Instruction::Loop { + body: vec![Instruction::Set(0)], + }], + ); + } + + #[test] + fn test_nested_add_clear_loop() { + let input = vec![Instruction::Loop { + body: vec![Instruction::Loop { + body: vec![Instruction::Add(1)], + }], + }]; + assert_optimizes_to( + input, + &[Instruction::Loop { + body: vec![Instruction::Set(0)], + }], + ); + } +} diff --git a/src/program/optimizer/combine_instructions.rs b/src/program/optimizer/combine_instructions.rs new file mode 100644 index 0000000..c10c9d9 --- /dev/null +++ b/src/program/optimizer/combine_instructions.rs @@ -0,0 +1,132 @@ +use crate::program::{Instruction, optimizer::util}; + +pub fn optimize(instructions: &mut Vec) { + use Instruction as Instr; + + // Coalesce the current block + util::coalesce(instructions, |current, next| match (current, next) { + (Instr::MoveRight(a), Instr::MoveRight(b)) => Some(Instr::MoveRight(a + b)), + (Instr::MoveLeft(a), Instr::MoveLeft(b)) => Some(Instr::MoveLeft(a + b)), + (Instr::Add(a), Instr::Add(b)) => Some(Instr::Add(a + b)), + (Instr::Sub(a), Instr::Sub(b)) => Some(Instr::Sub(a + b)), + (Instr::Set(_), Instr::Set(b)) => Some(Instr::Set(*b)), + _ => None, + }); + + // Recursively handle loops + for instruction in instructions.iter_mut() { + if let Instr::Loop { body } = instruction { + optimize(body); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_optimizes_to(input: Vec, expected: &[Instruction]) { + let mut input = input; + optimize(&mut input); + assert_eq!(input, expected); + } + + #[test] + fn test_basic_combine() { + let input = vec![ + Instruction::Add(1), + Instruction::Add(2), + Instruction::Add(3), + Instruction::Add(4), + Instruction::Sub(4), + Instruction::Sub(3), + Instruction::MoveLeft(2), + Instruction::MoveLeft(1), + Instruction::MoveRight(3), + Instruction::MoveRight(1), + Instruction::Set(0), + Instruction::Set(0), + ]; + let expected = vec![ + Instruction::Add(10), + Instruction::Sub(7), + Instruction::MoveLeft(3), + Instruction::MoveRight(4), + Instruction::Set(0), + ]; + assert_optimizes_to(input, &expected); + } + + #[test] + fn test_no_combinations() { + let input = vec![ + Instruction::Add(1), + Instruction::Sub(1), + Instruction::Add(1), + Instruction::Sub(1), + Instruction::Add(1), + Instruction::Set(0), + Instruction::Add(1), + Instruction::Set(0), + Instruction::MoveLeft(1), + Instruction::MoveRight(1), + Instruction::MoveLeft(1), + Instruction::MoveRight(1), + ]; + assert_optimizes_to(input.clone(), &input.clone()); + } + + #[test] + fn test_non_combinable_instructions() { + let input = vec![ + Instruction::Loop { body: vec![] }, + Instruction::Loop { + body: vec![Instruction::Read, Instruction::Read], + }, + Instruction::Loop { body: vec![] }, + Instruction::Read, + Instruction::Read, + Instruction::Print, + Instruction::Print, + ]; + assert_optimizes_to(input.clone(), &input.clone()); + } + + #[test] + fn test_combine_empty_input() { + assert_optimizes_to(vec![], &[]); + } + + #[test] + fn test_combine_multiple_nested_loops() { + let input = vec![Instruction::Loop { + body: vec![ + Instruction::Add(1), + Instruction::Add(2), + Instruction::Loop { + body: vec![ + Instruction::MoveRight(1), + Instruction::MoveRight(2), + Instruction::Loop { + body: vec![Instruction::Sub(1), Instruction::Sub(2)], + }, + ], + }, + ], + }]; + let expected = vec![Instruction::Loop { + body: vec![ + Instruction::Add(3), + Instruction::Loop { + body: vec![ + Instruction::MoveRight(3), + Instruction::Loop { + body: vec![Instruction::Sub(3)], + }, + ], + }, + ], + }]; + assert_optimizes_to(input, &expected); + } +} diff --git a/src/program/optimizer/mod.rs b/src/program/optimizer/mod.rs new file mode 100644 index 0000000..c9f977b --- /dev/null +++ b/src/program/optimizer/mod.rs @@ -0,0 +1,35 @@ +use super::Instruction; +use std::hash::{DefaultHasher, Hash, Hasher}; + +mod clear_loop; +mod combine_instructions; +mod util; + +pub fn optimize(instructions: &mut Vec) { + const OPTIMIZATION_PASSES: usize = 32; + + for _current_pass in 0..OPTIMIZATION_PASSES { + let changed = optimize_once(instructions); + if !changed { + // println!("Reached fixed point: {current_pass} pass(es)"); + return; + } + } + // Failed to reach fixed point +} + +fn optimize_once(instructions: &mut Vec) -> bool { + // This is an elegant (?) alternative to cloning + // the instructions, but it might be better to + // simply do the clone anyway + let initial_hash = calculate_hash(instructions); + combine_instructions::optimize(instructions); + clear_loop::optimize(instructions); + calculate_hash(instructions) != initial_hash +} + +fn calculate_hash(instructions: &[Instruction]) -> u64 { + let mut hasher = DefaultHasher::new(); + instructions.hash(&mut hasher); + hasher.finish() +} diff --git a/src/program/util.rs b/src/program/optimizer/util.rs similarity index 100% rename from src/program/util.rs rename to src/program/optimizer/util.rs diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 85e79f5..07752c3 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,6 +1,6 @@ use brainrust::{ interpreter, - program::{Program, parser}, + program::{self, Program}, }; const MEMORY_SIZE: usize = 32768; @@ -68,8 +68,8 @@ enum TestError { Interpreter, } -impl From for TestError { - fn from(_error: parser::Error) -> Self { +impl From for TestError { + fn from(_error: program::Error) -> Self { TestError::Parsing } }