From 8811bc0b3a689f8c864f18e73a52d63d219ae3ad Mon Sep 17 00:00:00 2001 From: Emil Englesson Date: Tue, 1 Apr 2025 18:18:17 +0200 Subject: [PATCH 1/2] Implemented a profiling interpreter Implemented a profiling interpreter as an alternative to the regular interpreter. It is currently pretty flawed and barebones, and ridiculously slow. It would be desirable to speed it up but this is a good baseline for now. --- Cargo.lock | 203 ++++++++++++++++++++++++++++++++++-- Cargo.toml | 5 +- src/cli/mod.rs | 52 ++++----- src/cli/run.rs | 108 +++++++++++-------- src/cli/util.rs | 80 +++++++++++++- src/interpreter/basic.rs | 39 +++++++ src/interpreter/mod.rs | 117 ++------------------- src/interpreter/profiler.rs | 71 +++++++++++++ src/interpreter/tape.rs | 88 ++++++++++++++++ src/main.rs | 4 +- tests/integration_test.rs | 6 +- 11 files changed, 577 insertions(+), 196 deletions(-) create mode 100644 src/interpreter/basic.rs create mode 100644 src/interpreter/profiler.rs create mode 100644 src/interpreter/tape.rs diff --git a/Cargo.lock b/Cargo.lock index d1bedc0..63bfa0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,25 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ansi-str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060de1453b69f46304b28274f382132f4e72c55637cf362920926a70d090890d" +dependencies = [ + "ansitok", +] + +[[package]] +name = "ansitok" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a8acea8c2f1c60f0a92a8cd26bf96ca97db56f10bbcab238bbe0cceba659ee" +dependencies = [ + "nom", + "vte", +] + [[package]] name = "anstream" version = "0.6.18" @@ -52,6 +71,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "bitflags" version = "2.9.0" @@ -63,23 +88,31 @@ name = "brainrust" version = "0.1.4" dependencies = [ "clap", + "colored", "paste", + "tabled", ] +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + [[package]] name = "clap" -version = "4.5.32" +version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" dependencies = [ "anstream", "anstyle", @@ -100,6 +133,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys", +] + [[package]] name = "errno" version = "0.3.10" @@ -110,6 +152,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -128,11 +182,46 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "papergrid" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b915f831b85d984193fdc3d3611505871dc139b2534530fa01c1a6a6707b6723" +dependencies = [ + "ansi-str", + "ansitok", + "bytecount", + "fnv", + "unicode-width", +] [[package]] name = "paste" @@ -140,11 +229,51 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rustix" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags", "errno", @@ -159,6 +288,42 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tabled" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121d8171ee5687a4978d1b244f7d99c43e7385a272185a2f1e1fa4dc0979d444" +dependencies = [ + "ansi-str", + "ansitok", + "papergrid", + "tabled_derive", +] + +[[package]] +name = "tabled_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d9946811baad81710ec921809e2af67ad77719418673b2a3794932d57b7538" +dependencies = [ + "heck", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "terminal_size" version = "0.4.2" @@ -169,12 +334,34 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "arrayvec", + "memchr", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 614336a..0d82d83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,14 @@ name = "brainrust" version = "0.1.4" authors = ["Emil Englesson "] edition = "2024" +description = "Brainfuck interpreter" +repository = "https://github.com/LimeEng/brainrust/" publish = false [dependencies] clap = { version = "4.5", features = ["cargo", "color", "suggestions", "wrap_help"] } +colored = "3.0" +tabled = { version = "0.18", features = ["ansi"] } [dev-dependencies] paste = "1.0" - diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8173970..31832de 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,46 +1,46 @@ use crate::{interpreter, program}; use clap::{Command, crate_name, crate_version}; -use std::env; +use std::{env, io}; mod run; mod util; +pub fn run() -> Result<(), Error> { + 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!(), + } +} + #[derive(Debug)] -pub enum CliError { - IoError(std::io::Error), - ParsingError(program::Error), +pub enum Error { + Io(std::io::Error), + Parsing(program::Error), Interpreter(interpreter::Error), } -impl From for CliError { - fn from(error: std::io::Error) -> Self { - CliError::IoError(error) +impl From for Error { + fn from(error: io::Error) -> Self { + Error::Io(error) } } -impl From for CliError { +impl From for Error { fn from(error: program::Error) -> Self { - CliError::ParsingError(error) + Error::Parsing(error) } } -impl From for CliError { +impl From for Error { 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!(), + Error::Interpreter(error) } } diff --git a/src/cli/run.rs b/src/cli/run.rs index 0121429..a5f9339 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,77 +1,99 @@ -use super::CliError; -use crate::{cli::util, interpreter, program::Program}; +use crate::{ + cli::util, + interpreter::{self, Analytics}, + program::Program, +}; use clap::{Arg, ArgAction, ArgMatches, Command, value_parser}; use std::{fs, io, time::Instant}; -const DEFAULT_MEMORY_SIZE: usize = 32768; +const DEFAULT_MEMORY_SIZE: &str = "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"]; +const ARG_MEMORY_SIZE: &str = "memory"; +const ARG_TIME: &str = "time"; +const ARG_PROFILE: &str = "profile"; pub fn build_command() -> Command { Command::new("run") - .about("Parses Brainfuck in the specified file and interprets it") + .about("Parse and execute a Brainfuck program from a file") .arg( Arg::new(ARG_INPUT_FILE) - .help("The file to parse") + .help("Path to the Brainfuck source file") .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::new(ARG_MEMORY_SIZE) + .help("Number of memory cells") + .long(ARG_MEMORY_SIZE) + .action(ArgAction::Set) + .default_value(DEFAULT_MEMORY_SIZE) + .value_parser(value_parser!(usize)), ) .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), + Arg::new(ARG_PROFILE) + .help("Collect and print program metrics") + .long_help("Collect and print program metrics. Substantially increases execution time and memory usage.") + .long(ARG_PROFILE) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(ARG_TIME) + .help("Print parsing and execution time") + .long(ARG_TIME) + .action(ArgAction::SetTrue), ) } -pub fn execute(matches: &ArgMatches) -> Result<(), CliError> { +pub fn execute(matches: &ArgMatches) -> Result<(), crate::cli::Error> { 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 memory_size = *matches + .get_one(ARG_MEMORY_SIZE) + .expect("Memory size should have a default value"); + let should_profile = *matches.get_one::(ARG_PROFILE).unwrap_or(&false); + let print_timings = *matches.get_one::(ARG_TIME).unwrap_or(&false); - let total_start = Instant::now(); + let 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 parse_elapsed = util::format_duration(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(); + let (exec_elapsed, analytics) = if should_profile { + let start = Instant::now(); + let analytics = interpreter::profile(&program, &mut input, &mut output, memory_size)?; + (util::format_duration(start.elapsed()), Some(analytics)) + } else { + let start = Instant::now(); + interpreter::execute(&program, &mut input, &mut output, memory_size)?; + (util::format_duration(start.elapsed()), None) + }; + + if let Some(analytics) = analytics { + print_analytics(&analytics); + } - if !time_metrics.is_empty() { + if print_timings { 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)); - } + println!("Parsing time: {parse_elapsed}"); + println!("Execution time: {exec_elapsed}"); } Ok(()) } + +fn print_analytics(analytics: &Analytics) { + let freq_table = util::build_frequency_table(analytics); + let loop_table = util::build_loop_patterns_table(analytics); + let misc_table = util::build_misc_table(analytics); + + println!(); + println!("{freq_table}"); + println!(); + println!("{loop_table}"); + println!(); + println!("{misc_table}"); +} diff --git a/src/cli/util.rs b/src/cli/util.rs index 1d101bf..02bf8c8 100644 --- a/src/cli/util.rs +++ b/src/cli/util.rs @@ -1,4 +1,10 @@ -use std::time::Duration; +use crate::{interpreter::Analytics, program::Instruction}; +use colored::Colorize; +use std::{collections::HashMap, time::Duration}; +use tabled::{ + builder::Builder, + settings::{Style, Width, peaker::Priority, style::BorderSpanCorrection}, +}; pub fn format_duration(d: Duration) -> String { let nanos = d.as_nanos(); @@ -13,3 +19,75 @@ pub fn format_duration(d: Duration) -> String { .map(|&(factor, unit)| format!("{:.1}{unit}", nanos as f64 / factor as f64)) .unwrap_or_else(|| "pretty quick".to_string()) } + +fn table_builder(columns: &[&str]) -> Builder { + let mut builder = Builder::default(); + + let col_header: Vec<_> = columns.iter().map(|t| t.bold().to_string()).collect(); + builder.push_record(col_header); + builder +} + +fn build_table(builder: Builder) -> String { + builder + .build() + .with(Style::modern()) + .with(BorderSpanCorrection) + .with(Width::wrap(80).priority(Priority::max(true))) + .to_string() +} + +pub fn build_frequency_table(analytics: &Analytics) -> String { + let mut builder = table_builder(&["Instruction", "Count"]); + let mut merged = HashMap::new(); + + // Merge instructions by type + for (key, count) in &analytics.frequency { + let (instr, multiplier) = match key { + Instruction::MoveRight(value) => ("MoveRight", *value as u64), + Instruction::MoveLeft(value) => ("MoveLeft", *value as u64), + Instruction::Add(value) => ("Add", *value as u64), + Instruction::Sub(value) => ("Sub", *value as u64), + Instruction::Loop { .. } => todo!("Needs to accurately track [ and ]"), + Instruction::Print => ("Print", 1u64), + Instruction::Read => ("Read", 1u64), + Instruction::Set(_) => ("Set", 1u64), + }; + *merged.entry(instr).or_insert(0) += count * multiplier; + } + + let mut entries: Vec<_> = merged.iter().collect(); + entries.sort_by(|(_, v1), (_, v2)| v2.cmp(v1)); + + for (key, value) in entries { + builder.push_record([(*key).to_string(), value.to_string()]); + } + + build_table(builder) +} + +pub fn build_loop_patterns_table(analytics: &Analytics) -> String { + let mut builder = table_builder(&["Loop Patterns", "Count"]); + let mut entries: Vec<_> = analytics.loop_patterns.iter().collect(); + entries.sort_by(|(_, v1), (_, v2)| v2.cmp(v1)); + + let top_entries = &entries[..entries.len().min(10)]; + for (pattern, count) in top_entries { + let pattern = (**pattern) + .iter() + .map(|instr| format!("{instr:?}")) + .collect::>() + .join(" "); + builder.push_record([pattern.to_string(), count.to_string()]); + } + + build_table(builder) +} + +pub fn build_misc_table(analytics: &Analytics) -> String { + let mut builder = table_builder(&["Metric", "Value"]); + // Plus 1 since the memory is zero-indexed + let highest_memory_access = analytics.highest_memory_access + 1; + builder.push_record(["Highest Memory Access", &format!("{highest_memory_access}")]); + build_table(builder) +} diff --git a/src/interpreter/basic.rs b/src/interpreter/basic.rs new file mode 100644 index 0000000..c775f14 --- /dev/null +++ b/src/interpreter/basic.rs @@ -0,0 +1,39 @@ +use super::{Error, tape::Tape}; +use crate::program::{Instruction, Program}; +use std::io; + +pub fn execute( + program: &Program, + input: &mut dyn io::Read, + output: &mut dyn io::Write, + memory_size: usize, +) -> Result<(), Error> { + let mut tape = Tape::new(input, output, memory_size); + execute_instructions(&mut tape, program.instructions())?; + Ok(()) +} + +fn execute_instructions(tape: &mut Tape, instructions: &[Instruction]) -> Result<(), Error> { + for instruction in instructions { + execute_instruction(tape, instruction)?; + } + Ok(()) +} + +fn execute_instruction(tape: &mut Tape, instruction: &Instruction) -> Result<(), Error> { + match instruction { + Instruction::MoveRight(value) => tape.move_pointer_right(*value)?, + Instruction::MoveLeft(value) => tape.move_pointer_left(*value)?, + Instruction::Add(value) => tape.increment_current_cell(*value as u8), + Instruction::Sub(value) => tape.decrement_current_cell(*value as u8), + Instruction::Loop { body } => { + while tape.read_current_cell() != 0 { + execute_instructions(tape, body)?; + } + } + Instruction::Print => tape.print()?, + Instruction::Read => tape.read()?, + Instruction::Set(value) => tape.write_current_cell(*value as u8), + } + Ok(()) +} diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 8d4315e..a9574f7 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -1,112 +1,7 @@ -use crate::program::{Instruction, Program}; -use std::io; +mod basic; +mod profiler; +mod tape; -#[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(()) - } -} +pub use basic::execute; +pub use profiler::{Analytics, profile}; +pub use tape::Error; diff --git a/src/interpreter/profiler.rs b/src/interpreter/profiler.rs new file mode 100644 index 0000000..5292bf6 --- /dev/null +++ b/src/interpreter/profiler.rs @@ -0,0 +1,71 @@ +use super::{Error, tape::Tape}; +use crate::program::{Instruction, Program}; +use std::{collections::HashMap, io}; + +#[derive(Clone, Debug, Default)] +pub struct Analytics { + pub frequency: HashMap, + pub loop_patterns: HashMap, u64>, + pub highest_memory_access: usize, +} + +pub fn profile( + program: &Program, + input: &mut dyn io::Read, + output: &mut dyn io::Write, + memory_size: usize, +) -> Result { + let mut tape = Tape::new(input, output, memory_size); + let mut analytics = Analytics::default(); + + execute_instructions(&mut tape, program.instructions(), &mut analytics)?; + Ok(analytics) +} + +fn contains_loop(instructions: &[Instruction]) -> bool { + instructions + .iter() + .any(|instruction| matches!(instruction, Instruction::Loop { .. })) +} + +fn execute_instructions( + tape: &mut Tape, + instructions: &[Instruction], + analytics: &mut Analytics, +) -> Result<(), Error> { + for instruction in instructions { + execute_instruction(tape, instruction, analytics)?; + + if let Instruction::Loop { body } = instruction { + if !contains_loop(body) { + *analytics.loop_patterns.entry(body.clone()).or_insert(0) += 1; + } + } else { + *analytics.frequency.entry(instruction.clone()).or_insert(0) += 1; + } + analytics.highest_memory_access = analytics.highest_memory_access.max(tape.pointer()); + } + Ok(()) +} + +fn execute_instruction( + tape: &mut Tape, + instruction: &Instruction, + analytics: &mut Analytics, +) -> Result<(), Error> { + match instruction { + Instruction::MoveRight(value) => tape.move_pointer_right(*value)?, + Instruction::MoveLeft(value) => tape.move_pointer_left(*value)?, + Instruction::Add(value) => tape.increment_current_cell(*value as u8), + Instruction::Sub(value) => tape.decrement_current_cell(*value as u8), + Instruction::Loop { body } => { + while tape.read_current_cell() != 0 { + execute_instructions(tape, body, analytics)?; + } + } + Instruction::Print => tape.print()?, + Instruction::Read => tape.read()?, + Instruction::Set(value) => tape.write_current_cell(*value as u8), + } + Ok(()) +} diff --git a/src/interpreter/tape.rs b/src/interpreter/tape.rs new file mode 100644 index 0000000..1b17dc1 --- /dev/null +++ b/src/interpreter/tape.rs @@ -0,0 +1,88 @@ +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 read_current_cell(&self) -> u8 { + self.memory[self.pointer] + } + + pub fn write_current_cell(&mut self, value: u8) { + self.memory[self.pointer] = value; + } + + pub fn increment_current_cell(&mut self, value: u8) { + let value = self.read_current_cell().wrapping_add(value); + self.write_current_cell(value); + } + + pub fn decrement_current_cell(&mut self, value: u8) { + let value = self.read_current_cell().wrapping_sub(value); + self.write_current_cell(value); + } + + pub 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(()) + } + + pub fn move_pointer_left(&mut self, steps: usize) -> Result<(), Error> { + if self.pointer < steps { + return Err(Error::PointerUnderflow); + } + self.pointer -= steps; + Ok(()) + } + + pub fn print(&mut self) -> Result<(), Error> { + self.output.write_all(&[self.read_current_cell()])?; + Ok(()) + } + + pub 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(()) + } + + pub fn pointer(&self) -> usize { + self.pointer + } +} diff --git a/src/main.rs b/src/main.rs index ca473b8..a19abad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -use brainrust::cli::{self, CliError}; +use brainrust::cli; -fn main() -> Result<(), CliError> { +fn main() -> Result<(), cli::Error> { cli::run() } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 07752c3..c09f1d2 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -3,8 +3,6 @@ use brainrust::{ program::{self, Program}, }; -const MEMORY_SIZE: usize = 32768; - macro_rules! file_path { ($program:ident, $ext:literal) => { concat!( @@ -50,14 +48,14 @@ test_programs! { } fn run_program(file: &str, input: &str) -> Result, TestError> { + const MEMORY_SIZE: usize = 32768; 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::execute(&program, &mut input, &mut output, MEMORY_SIZE)?; Ok(output) } From 2d7694b0af67f3987345e2c393852a0c0737aad8 Mon Sep 17 00:00:00 2001 From: Emil Englesson Date: Tue, 1 Apr 2025 18:25:10 +0200 Subject: [PATCH 2/2] Bumped version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63bfa0f..10fb7ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "brainrust" -version = "0.1.4" +version = "0.1.5" dependencies = [ "clap", "colored", diff --git a/Cargo.toml b/Cargo.toml index 0d82d83..84e1ac7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "brainrust" -version = "0.1.4" +version = "0.1.5" authors = ["Emil Englesson "] edition = "2024" description = "Brainfuck interpreter"