From 1ded907f47ad8568d2bb38962ef9031ea6e3fde4 Mon Sep 17 00:00:00 2001 From: Artemis Rosman <73006620+rozukke@users.noreply.github.com> Date: Sun, 15 Sep 2024 22:35:26 +1000 Subject: [PATCH 1/2] Add runtime --- src/air.rs | 26 +++++ src/lexer/mod.rs | 1 + src/lib.rs | 3 +- src/main.rs | 56 ++++++--- src/parser.rs | 10 ++ src/runtime.rs | 292 ++++++++++++++++++++++++++++++++++++++++++++++- src/symbol.rs | 1 + 7 files changed, 368 insertions(+), 21 deletions(-) diff --git a/src/air.rs b/src/air.rs index 049eb8a..3f16553 100644 --- a/src/air.rs +++ b/src/air.rs @@ -55,6 +55,15 @@ impl Air { } } +impl IntoIterator for Air { + type Item = AsmLine; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.ast.into_iter() + } +} + /// Single LC3 statement. Has optional labels. #[derive(PartialEq, Eq, Debug)] pub enum AirStmt { @@ -106,6 +115,12 @@ pub enum AirStmt { src_reg: Register, dest_label: Label, }, + /// Store value in src_reg at dest_label + offset + StoreOffs { + src_reg: Register, + dest_reg: Register, + offset: u8, + }, /// A raw value created during preprocessing RawWord { val: RawWord }, /// Jump to address at index trap_vect of the trap table @@ -283,6 +298,17 @@ impl AsmLine { raw |= self.bit_offs(dest_label, 9)?; Ok(raw) } + AirStmt::StoreOffs { + src_reg, + dest_reg, + offset, + } => { + let mut raw = 0x7000; + raw |= (*src_reg as u16) << 9; + raw |= (*dest_reg as u16) << 6; + raw |= *offset as u16; + Ok(raw) + } AirStmt::RawWord { val: bytes } => Ok(bytes.0), AirStmt::Trap { trap_vect } => Ok(0xF000 | *trap_vect as u16), } diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 9d9b1ce..bf08134 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -330,6 +330,7 @@ impl Cursor<'_> { "rti" => Instr(Rti), "st" => Instr(St), "sti" => Instr(Sti), + "str" => Instr(Str), _ => TokenKind::Label, } } diff --git a/src/lib.rs b/src/lib.rs index 71e6d77..a505ed4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,9 @@ pub use parser::AsmParser; pub mod air; pub use air::Air; +mod runtime; +pub use runtime::RunState; mod error; mod lexer; -mod runtime; mod symbol; diff --git a/src/main.rs b/src/main.rs index 78ce2b9..4f7b2e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use clap::{Parser, Subcommand}; use colored::Colorize; use miette::{GraphicalTheme, IntoDiagnostic, MietteHandlerOpts, Result}; -use lace::AsmParser; +use lace::{AsmParser, RunState}; /// Lace is a complete & convenient assembler toolchain for the LC3 assembly language. #[derive(Parser)] @@ -19,18 +19,14 @@ struct Args { #[command(subcommand)] command: Option, - /// Run commands in debug mode - #[arg(long)] - debug: bool, + /// Quickly provide a `.asm` file to run + path: Option, } #[derive(Subcommand)] enum Command { /// Run text `.asm` or binary `.lc3` file directly and output to terminal Run { - /// Map the LC3 operating system ROM to memory to handle traps and interrupts - #[arg(short, long)] - os: bool, /// .asm file to run name: PathBuf, }, @@ -63,7 +59,33 @@ fn main() -> miette::Result<()> { if let Some(command) = args.command { match command { - Command::Run { os, name } => todo!(), + Command::Run { name } => { + let contents: &'static str = + Box::leak(Box::new(fs::read_to_string(&name).into_diagnostic()?)); + println!( + "{:>12} {}", + "Assembling".green().bold(), + name.to_str().unwrap() + ); + // Process asm + let parser = lace::AsmParser::new(&contents)?; + let mut air = parser.parse()?; + air.backpatch()?; + // Run file + println!( + "{:>12} {}", + "Running".green().bold(), + name.to_str().unwrap() + ); + let mut program = RunState::try_from(air)?; + program.run(); + println!( + "{:>12} {}", + "Finished".green().bold(), + name.to_str().unwrap() + ); + Ok(()) + } Command::Compile { name, dest } => { // Available until end of program let contents: &'static str = @@ -109,10 +131,14 @@ fn main() -> miette::Result<()> { Command::Fmt { name } => todo!(), } } else { - println!("\n~ lace v{VERSION} - Copyright (c) 2024 Artemis Rosman ~"); - println!("{}", LOGO.truecolor(255, 183, 197).bold()); - println!("{SHORT_INFO}"); - std::process::exit(0); + if let Some(path) = args.path { + todo!("Should allow for running files with no subcommand") + } else { + println!("\n~ lace v{VERSION} - Copyright (c) 2024 Artemis Rosman ~"); + println!("{}", LOGO.truecolor(255, 183, 197).bold()); + println!("{SHORT_INFO}"); + std::process::exit(0); + } } } @@ -131,9 +157,9 @@ x .d88" "% ^Y" ^Y' "P' "YP'"#; const SHORT_INFO: &str = r" -Welcome to lace (from LAIS - LC3 Assembler & Interpreter System), an all-in-one toolchain -for working with LC3 assembly code. Please use `-h` or `--help` to access -the usage instructions and documentation. +Welcome to lace (from LAIS - LC3 Assembler & Interpreter System), +an all-in-one toolchain for working with LC3 assembly code. +Please use `-h` or `--help` to access the usage instructions and documentation. "; const SHORT_HELP: &str = r" diff --git a/src/parser.rs b/src/parser.rs index 5808002..0cd0aa1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -290,6 +290,16 @@ impl AsmParser { dest_label, }) } + InstrKind::Str => { + let src_reg = self.expect_reg()?; + let dest_reg = self.expect_reg()?; + let offset = self.expect_lit(Bits::Signed(6))? as u8; + Ok(AirStmt::StoreOffs { + src_reg, + dest_reg, + offset, + }) + } } } diff --git a/src/runtime.rs b/src/runtime.rs index 4941050..f609f24 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,22 +1,304 @@ #![allow(unused)] -use crate::symbol::Flag; +use core::panic; +use std::{cmp::Ordering, i16, u16, u32, u8, usize}; + +use crate::{symbol::Flag, Air}; +use colored::Colorize; +use miette::Result; /// LC3 can address 128KB of memory. const MEMORY_MAX: usize = 0x10000; /// Represents complete program state during runtime. -pub struct State { +pub struct RunState { /// System memory - 128KB in size. /// Need to figure out if this would cause problems with the stack. mem: [u16; MEMORY_MAX], - /// Address provided by program to indicate where to load user code - init_addr: u16, /// Program counter pc: u16, /// 8x 16-bit registers reg: [u16; 8], /// Condition code - cc: Flag, + flag: RunFlag, /// Processor status register psr: u16, } + +#[derive(Clone, Copy)] +enum RunFlag { + N = 0b100, + Z = 0b010, + P = 0b001, + Uninit = 0b000, +} + +impl RunState { + // Not generic because of miette error + pub fn try_from(air: Air) -> Result { + let orig: usize = air.orig().unwrap_or(0x3000).into(); + let mut mem = [0; MEMORY_MAX]; + let mut air_array = Vec::with_capacity(air.len()); + + for stmt in air { + air_array.push(stmt.emit()?); + } + + // Sanity check + if orig + air_array.len() > MEMORY_MAX { + panic!("Assembly file is too long and cannot fit in memory."); + } + + mem[orig..orig + air_array.len()].clone_from_slice(&air_array); + + Ok(RunState { + mem, + pc: orig as u16, + reg: [0; 8], + flag: RunFlag::Uninit, + psr: 0, + }) + } + + const OP_TABLE: [fn(&mut RunState, u16); 16] = [ + Self::br, // 0x0 + Self::add, // 0x1 + Self::ld, // 0x2 + Self::st, // 0x3 + Self::jsr, // 0x4 + Self::and, // 0x5 + Self::ldr, // 0x6 + Self::str, // 0x7 + Self::rti, // 0x8 + Self::not, // 0x9 + Self::ldi, // 0xA + Self::sti, // 0xB + Self::jmp, // 0xC + Self::nul, // 0xD + Self::lea, // 0xE + Self::trap, // 0xF + ]; + + /// Run with preset memory + pub fn run(&mut self) { + loop { + if self.pc >= 0xFE00 { + // Entering device address space + break; + } + let instr = self.mem[self.pc as usize]; + let opcode = (instr >> 12) as usize; + // PC incremented before instruction is performed + self.pc += 1; + Self::OP_TABLE[opcode](self, instr); + } + } + + #[inline] + fn reg(&mut self, reg: u16) -> &mut u16 { + // SAFETY: Should only be indexed with values that are & 0b111 + unsafe { self.reg.get_unchecked_mut(reg as usize) } + } + + #[inline] + fn mem(&mut self, addr: u16) -> &mut u16 { + // SAFETY: memory fits any u16 index + unsafe { self.mem.get_unchecked_mut(addr as usize) } + } + + #[inline] + fn s_ext(val: u16, bits: u32) -> u16 { + let val = val & (2u16.pow(bits) - 1); + if val & 2u16.pow(bits - 1) == 0 { + // positive + val + } else { + // negative + val | !(2u16.pow(bits) - 1) + } + } + + #[inline] + fn set_flags(&mut self, val: u16) { + self.flag = match (val as i16).cmp(&0) { + Ordering::Less => RunFlag::N, + Ordering::Equal => RunFlag::Z, + Ordering::Greater => RunFlag::P, + } + } + + fn nul(&mut self, _instr: u16) { + panic!("You called a reserved instruction. Halting...") + } + + fn add(&mut self, instr: u16) { + let dr = (instr >> 9) & 0b111; + let sr = (instr >> 6) & 0b111; + + let val1 = *self.reg(dr); + // Check if imm + let val2 = if instr & 0b100000 == 0 { + // reg + *self.reg((instr & 0b111)) + } else { + // imm + Self::s_ext(instr, 5) + }; + let res = val1 + val2; + self.set_flags(res); + *self.reg(dr) = res; + } + + fn and(&mut self, instr: u16) { + let dr = (instr >> 9) & 0b111; + let sr = (instr >> 6) & 0b111; + + let val1 = *self.reg(dr); + // Check if imm + let val2 = if instr & 0b100000 == 0 { + // reg + *self.reg((instr & 0b111)) + } else { + // imm + Self::s_ext(instr, 5) + }; + let res = val1 + val2; + self.set_flags(res); + *self.reg(dr) = res; + } + + fn br(&mut self, instr: u16) { + let flag = (instr >> 9) & 0b111; + if self.flag as u16 & flag != 0 { + self.pc = self.pc.wrapping_add(Self::s_ext(instr, 9)) + } + } + + fn jmp(&mut self, instr: u16) { + let br = (instr >> 6) & 0b111; + self.pc = *self.reg(br) + } + + fn jsr(&mut self, instr: u16) { + *self.reg(7) = self.pc; + if instr & 0x800 == 0 { + // reg + let br = (instr >> 6) & 0b111; + self.pc = *self.reg(br) + } else { + // offs + self.pc = self.pc.wrapping_add(Self::s_ext(instr, 11)) + } + } + + fn ld(&mut self, instr: u16) { + let dr = (instr >> 9) & 0b111; + let val = *self.mem(self.pc + Self::s_ext(instr, 9)); + *self.reg(dr) = val; + self.set_flags(val); + } + + fn ldi(&mut self, instr: u16) { + let dr = (instr >> 9) & 0b111; + let ptr = *self.mem(self.pc + Self::s_ext(instr, 9)); + let val = *self.mem(ptr); + *self.reg(dr) = val; + self.set_flags(val); + } + + fn ldr(&mut self, instr: u16) { + let dr = (instr >> 9) & 0b111; + let br = (instr >> 6) & 0b111; + let ptr = *self.reg(br); + let val = *self.mem(ptr + Self::s_ext(instr, 6)); + *self.reg(dr) = val; + self.set_flags(val); + } + + fn lea(&mut self, instr: u16) { + let dr = (instr >> 9) & 0b111; + let val = self.pc + Self::s_ext(instr, 9); + *self.reg(dr) = val; + self.set_flags(val); + } + + fn not(&mut self, instr: u16) { + let dr = (instr >> 9) & 0b111; + let sr = (instr >> 6) & 0b111; + let val = !*self.reg(sr); + *self.reg(dr) = val; + self.set_flags(val); + } + + fn rti(&mut self, instr: u16) { + todo!("Please open an issue and I'll get RTI implemented in a jiffy :)") + } + + fn st(&mut self, instr: u16) { + let sr = (instr >> 9) & 0b111; + let val = *self.reg(sr); + *self.mem(self.pc + Self::s_ext(instr, 9)) = val; + } + + fn sti(&mut self, instr: u16) { + let sr = (instr >> 9) & 0b111; + let val = *self.reg(sr); + let ptr = *self.mem(self.pc + Self::s_ext(instr, 9)); + *self.mem(ptr) = val; + } + + fn str(&mut self, instr: u16) { + let sr = (instr >> 9) & 0b111; + let br = (instr >> 6) & 0b111; + let ptr = *self.reg(br); + *self.mem(ptr + Self::s_ext(instr, 6)); + } + + fn trap(&mut self, instr: u16) { + let trap_vect = instr & 0xFF; + match trap_vect { + // getc + 0x20 => { + // TODO: impl getc + todo!("TODO: Some dependencies are required for immediate response.") + } + // out + 0x21 => unsafe { + let chr = (*self.reg(0) & 0xFF) as u8 as char; + print!("{chr}") + }, + // puts + 0x22 => { + // could probably rewrite with iterators but idk if worth + let mut addr = *self.reg(0); + let mut string = String::new(); + loop { + let chr_raw = *self.mem(addr); + let chr_ascii = (chr_raw & 0xFF) as u8 as char; + if chr_ascii == '\0' { + break; + } + string.push(chr_ascii); + addr += 1; + } + print!("{string}"); + } + // in + 0x23 => { + // TODO: impl in + todo!("TODO: Some dependencies are required for immediate response.") + } + // putsp + 0x24 => { + // TODO: impl putsp + todo!("TODO: putsp can be put off until someone needs it") + } + // halt + 0x25 => { + self.pc = u16::MAX; + println!("\n{:>12}", "Halted".cyan()); + } + // unknown + _ => panic!("You called a trap with an unknown vector of {}", trap_vect), + } + } +} diff --git a/src/symbol.rs b/src/symbol.rs index 5235344..f5d616b 100644 --- a/src/symbol.rs +++ b/src/symbol.rs @@ -227,6 +227,7 @@ pub enum InstrKind { Rti, St, Sti, + Str, } #[derive(Clone, Copy, PartialEq, Eq, Debug)] From 8279ea1bb7b0932d6afe498b80e602c2acccc09a Mon Sep 17 00:00:00 2001 From: Artemis Rosman <73006620+rozukke@users.noreply.github.com> Date: Sun, 15 Sep 2024 22:35:52 +1000 Subject: [PATCH 2/2] Add runtime tests --- Cargo.lock | 38 ++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + tests/files/hw.asm | 4 ++++ tests/integration_tests.rs | 12 ++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/files/hw.asm diff --git a/Cargo.lock b/Cargo.lock index dde14b2..c95fbef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "backtrace" version = "0.3.73" @@ -233,6 +239,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -300,6 +315,7 @@ dependencies = [ "indexmap", "lazy_static", "miette", + "predicates", "regex", ] @@ -367,6 +383,21 @@ dependencies = [ "adler", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.3" @@ -384,13 +415,16 @@ checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" [[package]] name = "predicates" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ "anstyle", "difflib", + "float-cmp", + "normalize-line-endings", "predicates-core", + "regex", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c0d0490..e5b82d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ fxhash = "0.2.1" [dev-dependencies] assert_cmd = "2.0.14" +predicates = "3.1.2" [[bin]] name = "lace" diff --git a/tests/files/hw.asm b/tests/files/hw.asm new file mode 100644 index 0000000..9551c33 --- /dev/null +++ b/tests/files/hw.asm @@ -0,0 +1,4 @@ +lea r0 hw +puts +halt +hw .stringz "Hello, world!" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index f4b7a2f..fc208e2 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,4 +1,5 @@ use assert_cmd::prelude::*; +use predicates::str::contains; use std::process::Command; #[test] @@ -6,3 +7,14 @@ fn runs_without_arguments() { let mut cmd = Command::cargo_bin("lace").unwrap(); cmd.assert().success(); } + +#[test] +fn runs_hello_world() { + let mut cmd = Command::cargo_bin("lace").unwrap(); + cmd.arg("run").arg("tests/files/hw.asm"); + + cmd.assert() + .success() + .stdout(contains("Hello, world!")) + .stdout(contains("Halted")); +}