diff --git a/Cargo.lock b/Cargo.lock index 8a0b2db..2bb7450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "anstream" version = "0.6.18" @@ -52,6 +67,27 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.5.31" @@ -92,30 +128,101 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", +] + [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + [[package]] name = "proc-macro2" version = "1.0.94" @@ -134,11 +241,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rusty_man_computer" version = "0.5.0" dependencies = [ "clap", + "color-eyre", + "thiserror", ] [[package]] @@ -158,6 +273,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.17" @@ -170,6 +305,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 76f57f7..42f570e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2024" [dependencies] clap = { version = "4.5.30", features = ["derive"] } +color-eyre = { version = "0.6", default-features = false } +thiserror = "2.0.17" [[bin]] name = "bin_creator" @@ -14,4 +16,4 @@ path = "src/bin_creator.rs" [[bin]] name = "rmc_assemble" -path = "src/rmc_assemble.rs" +path = "src/assembler.rs" diff --git a/src/rmc_assemble.rs b/src/assembler.rs similarity index 95% rename from src/rmc_assemble.rs rename to src/assembler.rs index 314c50d..401c54b 100644 --- a/src/rmc_assemble.rs +++ b/src/assembler.rs @@ -1,5 +1,6 @@ use clap::Parser; use std::{collections::HashMap, fmt, fs, io, path::PathBuf}; +use thiserror::Error; use rusty_man_computer::value::Value; @@ -55,7 +56,7 @@ impl fmt::Display for ParseErrorType { } #[derive(Debug)] -struct ParseError { +pub struct ParseError { error: ParseErrorType, line: usize, } @@ -206,10 +207,15 @@ fn generate_machine_code(lines: Vec) -> Result, &'static str> { Ok(output) } -enum AssemblerError { +#[derive(Error)] +pub enum AssemblerError { + #[error("{0}")] ParseError(ParseError), + #[error("Machine code error: {0}")] MachineCodeError(&'static str), + #[error("Failed to read input file: {0}")] ReadError(io::Error), + #[error("Failed to write to output file: {0}")] WriteError(io::Error), } @@ -224,7 +230,7 @@ impl fmt::Debug for AssemblerError { } } -fn assemble(program: &str) -> Result, AssemblerError> { +pub fn assemble(program: &str) -> Result, AssemblerError> { let parsed = parse_assembly(program); let mut valid_lines: Vec = Vec::new(); // Only go forward with non-empty lines, and raise an error if we encounter an invalid line @@ -254,8 +260,7 @@ pub struct Args { output: PathBuf, } -fn main() -> Result<(), AssemblerError> { - let args = Args::parse(); +fn assemble_from_file(args: Args) -> Result<(), AssemblerError> { let program = std::fs::read_to_string(args.program).map_err(|e| AssemblerError::ReadError(e))?; let assembler_result = assemble(&program); @@ -269,6 +274,11 @@ fn main() -> Result<(), AssemblerError> { } } +fn main() -> Result<(), AssemblerError> { + let args = Args::parse(); + assemble_from_file(args) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 0d40d8f..064be40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use clap::Parser; +use clap::{Parser, Subcommand}; use std::{error::Error, fs, io::Write, path::PathBuf}; use value::Value; @@ -236,13 +236,13 @@ pub struct Computer { ram: RAM, registers: Registers, pub output: Output, - config: Config, + config: ComputerConfig, } impl Computer { - pub fn new(config: Config) -> Computer { + pub fn new(config: ComputerConfig) -> Computer { Computer { - ram: [Value::zero(); 100], + ram: config.ram, registers: Registers { program_counter: 0, instruction_register: 0, @@ -292,6 +292,10 @@ impl Computer { touched_addresses } + // pub fn load_values_to_ram(&mut self, values: Vec) { + + // } + pub fn clock_cycle(&mut self) -> bool { // Stage 1: Fetch let ram_index = self.registers.program_counter; @@ -500,8 +504,11 @@ fn read_input_until_valid(prompt: &str) -> Result { } } -pub struct Config { +pub struct ComputerConfig { + /// TODO remove pub load_ram_file_path: Option, + /// The initial contents of RAM (i.e. initial values for the letterboxes) + pub ram: [Value; 100], /// If the register values, output buffer, RAM values, and branch messages should be printed after every clock cycle pub print_computer_state: bool, /// If output should be directly and immediately printed when a OUT/OTC instruction is executed @@ -514,20 +521,21 @@ pub struct Config { pub input: Option>, } -impl Config { - pub fn from_args(args: Args) -> Config { +impl ComputerConfig { + pub fn from_args(args: ExecuteArgs) -> ComputerConfig { if args.ram_legacy.is_some() && args.ram.is_some() { print_error("Warning: Ignoring positional argument and using --ram argument instead."); print_error("Specifying a RAM file without --ram is no longer recommended."); } - Config { + ComputerConfig { load_ram_file_path: args.ram.or_else(|| { eprintln!( "Note: It is recommended to use the --ram argument to specify a RAM file." ); args.ram_legacy }), + ram: [Value::zero(); 100], print_computer_state: !args.output_only, print_raw_output: args.output_only, input: None, @@ -535,10 +543,11 @@ impl Config { } } -impl Default for Config { +impl Default for ComputerConfig { fn default() -> Self { - Config { + ComputerConfig { load_ram_file_path: None, + ram: [Value::zero(); 100], print_computer_state: true, print_raw_output: false, input: None, @@ -549,6 +558,22 @@ impl Default for Config { #[derive(Parser)] #[command(version)] pub struct Args { + #[command(subcommand)] + pub command: Command, +} + +#[derive(Subcommand, Clone)] +pub enum Command { + /// executes the provided Rusty-Man machine code + Execute(ExecuteArgs), + Run { + #[arg()] + file: PathBuf, + }, +} + +#[derive(Parser, Clone)] +pub struct ExecuteArgs { // Positional arg for memory file (kept for backwards compatibility) #[arg(hide = true)] ram_legacy: Option, @@ -560,7 +585,7 @@ pub struct Args { output_only: bool, } -pub fn run(config: Config) -> Result<(), Box> { +pub fn run(config: ComputerConfig) -> Result<(), Box> { let mut computer = Computer::new(config); computer.initialize_ram_from_file()?; computer.run(); @@ -573,7 +598,7 @@ mod tests { #[test] fn hlt_instruction_works() { - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.ram[0] = 000.into(); let has_halted = !computer.clock_cycle(); // It should halt after the first clock cycle @@ -583,7 +608,7 @@ mod tests { #[test] fn add_instruction_works() { // Test 40 + 2 = 42 - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = 40.into(); computer.ram[99] = 2.into(); // Operand computer.ram[0] = Value::new(199).unwrap(); // Add address 99 to ACC @@ -594,7 +619,7 @@ mod tests { #[test] fn sub_instruction_works() { // Test 42 - 2 = 40 - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = 42.into(); computer.ram[99] = 2.into(); // Operand computer.ram[0] = Value::new(299).unwrap(); // Subtract address 99 from ACC @@ -605,7 +630,7 @@ mod tests { #[test] fn store_instruction_works() { // Test storing 42 in address 99 - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = 42.into(); computer.ram[0] = Value::new(399).unwrap(); // Store ACC to address 99 computer.clock_cycle(); @@ -615,7 +640,7 @@ mod tests { #[test] fn load_instruction_works() { // Test loading 42 from address 99 - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.ram[99] = 42.into(); computer.ram[0] = Value::new(599).unwrap(); // Load ACC from address 99 computer.clock_cycle(); @@ -625,7 +650,7 @@ mod tests { #[test] fn branch_instruction_works() { // Test branching/jumping to address 42 - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.ram[0] = Value::new(642).unwrap(); // Branch to address 42 computer.clock_cycle(); assert_eq!(computer.registers.program_counter, 42); @@ -634,7 +659,7 @@ mod tests { #[test] fn branch_zero_instruction_when_zero() { // Test BRZ when the accumulator is zero (so it should branch) - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = 0.into(); computer.ram[0] = Value::new(742).unwrap(); // Branch to address 42 if ACC is zero computer.clock_cycle(); @@ -644,7 +669,7 @@ mod tests { #[test] fn branch_zero_instruction_when_non_zero() { // Test BRZ when the accumulator is non-zero (so it should not branch) - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = (-5).into(); computer.ram[0] = Value::new(742).unwrap(); // Branch to address 42 if ACC is zero computer.clock_cycle(); @@ -654,7 +679,7 @@ mod tests { #[test] fn branch_positive_instruction_when_positive() { // Test BRP when the accumulator is positive (so it should branch) - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = 5.into(); computer.ram[0] = Value::new(842).unwrap(); // Branch to address 42 if ACC is positive computer.clock_cycle(); @@ -665,7 +690,7 @@ mod tests { fn branch_positive_instruction_when_zero() { // Test BRP when the accumulator is zero (so it should branch) // (boundary data) - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = 0.into(); computer.ram[0] = Value::new(842).unwrap(); // Branch to address 42 if ACC is positive computer.clock_cycle(); @@ -675,7 +700,7 @@ mod tests { #[test] fn branch_positive_instruction_when_negative() { // Test BRP when the accumulator is negative (so it should not branch) - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = (-5).into(); computer.ram[0] = Value::new(842).unwrap(); // Branch to address 42 if ACC is positive computer.clock_cycle(); @@ -685,9 +710,9 @@ mod tests { #[test] fn input_instruction_works() { // Test inputting 21 - let mut computer = Computer::new(Config { + let mut computer = Computer::new(ComputerConfig { input: Some(vec![21.into()]), - ..Config::default() + ..ComputerConfig::default() }); computer.ram[0] = Value::new(901).unwrap(); computer.clock_cycle(); @@ -697,7 +722,7 @@ mod tests { #[test] fn output_instruction_works() { // Test outputting 21 - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = 21.into(); computer.ram[0] = Value::new(902).unwrap(); computer.clock_cycle(); @@ -707,7 +732,7 @@ mod tests { #[test] fn output_character_instruction_works() { // Test outputting ASCII value 104 (h) - let mut computer = Computer::new(Config::default()); + let mut computer = Computer::new(ComputerConfig::default()); computer.registers.accumulator = 104.into(); computer.ram[0] = Value::new(922).unwrap(); computer.clock_cycle(); diff --git a/src/main.rs b/src/main.rs index a3ff4bd..7f56d79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,38 @@ use clap::Parser; -use rusty_man_computer::{Args, Config, print_error}; +use color_eyre::eyre::eyre; +use rusty_man_computer::{Args, Command, Computer, ComputerConfig, print_error, value::Value}; +mod assembler; -fn main() -> () { - let config = Config::from_args(Args::parse()); +fn main() -> Result<(), color_eyre::Report> { + let args = Args::parse(); - println!("Little Man Computer implemented in Rust!"); - if let Err(e) = rusty_man_computer::run(config) { - print_error(&format!("Application error: {}", e)); + match args.command { + Command::Execute(execute) => { + let config = ComputerConfig::from_args(execute); + if let Err(e) = rusty_man_computer::run(config) { + print_error(&format!("Application error: {}", e)); + }; + Ok(()) + } + Command::Run { file } => { + let program = std::fs::read_to_string(file)?; + let machine_code = assembler::assemble(&program)?; + + // Initialize memory with the machine code + let mut machine_code_array: [Value; 100] = [Value::zero(); 100]; + if &machine_code.len() > &machine_code_array.len() { + return Err(eyre!("Program too large to fit in memory")); + } + for i in 0..machine_code.len() { + machine_code_array[i] = machine_code[i]; + } + + let mut computer = Computer::new(ComputerConfig { + ram: machine_code_array, + ..ComputerConfig::default() + }); + computer.run(); + Ok(()) + } } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 0b6d338..6c0428f 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,12 +1,12 @@ use std::path::PathBuf; -use rusty_man_computer::{Computer, Config}; +use rusty_man_computer::{Computer, ComputerConfig}; #[test] fn test_ascii_program() { - let mut computer = Computer::new(Config { + let mut computer = Computer::new(ComputerConfig { load_ram_file_path: Some(PathBuf::from("demos/ascii.bin")), - ..Config::default() + ..ComputerConfig::default() }); computer .initialize_ram_from_file() @@ -24,10 +24,10 @@ fn test_add_program() { let number_1 = 3; let number_2 = -5; let expected_output = number_1 + number_2; - let mut computer = Computer::new(Config { + let mut computer = Computer::new(ComputerConfig { load_ram_file_path: Some(PathBuf::from("demos/add.bin")), input: Some(vec![number_1.into(), number_2.into()]), - ..Config::default() + ..ComputerConfig::default() }); computer .initialize_ram_from_file() @@ -43,10 +43,10 @@ fn test_add_subtract_program() { let expected_sum = number_1 + number_2; let number_3 = 100; let expected_difference = number_3 - number_1; - let mut computer = Computer::new(Config { + let mut computer = Computer::new(ComputerConfig { load_ram_file_path: Some(PathBuf::from("demos/add-subtract.bin")), input: Some(vec![number_1.into(), number_2.into(), number_3.into()]), - ..Config::default() + ..ComputerConfig::default() }); computer .initialize_ram_from_file() @@ -61,10 +61,10 @@ fn test_factorial_program() { let input = 6; // 6! = 720 let expected_output = 720; - let mut computer = Computer::new(Config { + let mut computer = Computer::new(ComputerConfig { load_ram_file_path: Some(PathBuf::from("demos/factorial.bin")), input: Some(vec![input.into()]), - ..Config::default() + ..ComputerConfig::default() }); computer .initialize_ram_from_file()