commit 2b4e352f88e5d5bccc4b05989b5c19792a371dcd
parent 8cdbd4e268ad77d7166cd5160ffaaa837432d833
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Wed, 22 Jul 2026 18:03:26 -0400
Add test subcommand and load_file method to simulator
Diffstat:
4 files changed, 206 insertions(+), 9 deletions(-)
diff --git a/sim/Cargo.lock b/sim/Cargo.lock
@@ -33,6 +33,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -160,6 +210,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
+name = "clap"
+version = "4.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
name = "color-eyre"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -187,6 +277,12 @@ dependencies = [
]
[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
name = "compact_str"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -585,6 +681,12 @@ dependencies = [
]
[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -821,6 +923,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
name = "ordered-float"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1307,6 +1415,7 @@ version = "0.1.0"
dependencies = [
"bounded-integer",
"cc",
+ "clap",
"color-eyre",
"crossterm",
"ratatui",
diff --git a/sim/Cargo.toml b/sim/Cargo.toml
@@ -8,6 +8,7 @@ cc = "1.2.60"
[dependencies]
bounded-integer = "0.6.1"
+clap = { version = "4.6.2", features = ["derive"] }
color-eyre = "0.6.5"
crossterm = "0.29.0"
ratatui = "0.30.0"
diff --git a/sim/src/main.rs b/sim/src/main.rs
@@ -1,5 +1,6 @@
-use std::{fmt::Display, time::Duration};
+use std::{error::Error, fmt::Display, path::PathBuf, time::Duration};
+use clap::{Parser, Subcommand};
use crossterm::event::{self, Event, KeyCode, KeyEvent};
use ratatui::{
DefaultTerminal, Frame,
@@ -13,6 +14,23 @@ use crate::perilus::{MemoryIndex, Perilus, Register};
mod perilus;
+#[derive(Parser)]
+struct Cli {
+ #[command(subcommand)]
+ command: Option<Command>,
+}
+
+#[derive(Subcommand)]
+enum Command {
+ Interactive {
+ /// File to load verbatim into memory starting at 0x0
+ file: Option<PathBuf>,
+ },
+ Test {
+ file: PathBuf,
+ },
+}
+
enum Mode {
Normal,
EditPc,
@@ -389,12 +407,63 @@ impl Widget for &App {
}
}
-fn main() -> color_eyre::Result<()> {
- let perilus = Perilus::init();
- let mut app = App::new(perilus);
-
+fn run_interactive(mut app: App) -> color_eyre::Result<()> {
color_eyre::install()?;
ratatui::run(|terminal| app.run(terminal))?;
-
Ok(())
}
+
+const EBREAK_INSTRUCTION: u32 = 0x00100073;
+const TEST_MAX_CYCLES: u32 = 1_000_000;
+const TEST_PASS_VALUE: u32 = 0xebbb3689;
+const TEST_FAIL_VALUE: u32 = 0xd02a545a;
+
+fn main() -> Result<(), Box<dyn Error>> {
+ let cli = Cli::parse();
+ let perilus = Perilus::init();
+
+ match cli.command {
+ Some(Command::Interactive { file }) => {
+ if let Some(file) = file {
+ perilus.load_file(file)?;
+ }
+ let app = App::new(perilus);
+ run_interactive(app).map_err(|e| -> Box<dyn Error> { e.to_string().into() })?;
+ Ok(())
+ }
+ Some(Command::Test { file }) => {
+ perilus.load_file(file)?;
+ let mut cycles = 0;
+ loop {
+ perilus.pulse_clock();
+ cycles += 1;
+ if cycles > TEST_MAX_CYCLES {
+ return Err("Test execution hit maximum cycle count limit".into());
+ }
+ if perilus.get_control_unit_state() == 0 {
+ let pc = perilus.get_pc();
+ let instr = perilus.get_memory().get((pc / 4) as usize);
+ if instr.is_none() {
+ return Err(format!(
+ "Program counter diverged to an address outside memory: 0x{pc:08x}"
+ )
+ .into());
+ }
+ if let Some(&EBREAK_INSTRUCTION) = instr {
+ let result = perilus.get_registers()[1];
+ match result {
+ TEST_PASS_VALUE => return Ok(()),
+ TEST_FAIL_VALUE => return Err("Test failed".into()),
+ v => return Err(format!("Got unexpected test result 0x{v:08x}").into()),
+ }
+ }
+ }
+ }
+ }
+ None => {
+ let app = App::new(perilus);
+ run_interactive(app).map_err(|e| -> Box<dyn Error> { e.to_string().into() })?;
+ Ok(())
+ }
+ }
+}
diff --git a/sim/src/perilus.rs b/sim/src/perilus.rs
@@ -1,5 +1,5 @@
use bounded_integer::BoundedU32;
-use std::slice;
+use std::{io, path::PathBuf, slice};
unsafe extern "C" {
fn perilus_init();
@@ -18,13 +18,13 @@ unsafe extern "C" {
}
pub(crate) type Register = BoundedU32<0, 31>;
-pub(crate) type MemoryIndex = BoundedU32<0, 1024>;
+pub(crate) type MemoryIndex = BoundedU32<0, { Perilus::MEMORY_SIZE_WORDS as u32 }>;
pub(crate) struct Perilus;
impl Perilus {
const NUM_REGISTERS: usize = 32;
- const MEMORY_SIZE_WORDS: usize = 1024;
+ const MEMORY_SIZE_WORDS: usize = 1_048_576;
pub(crate) fn init() -> Perilus {
unsafe { perilus_init() };
@@ -68,6 +68,24 @@ impl Perilus {
pub(crate) fn set_memory(&self, address: u32, value: u32) {
unsafe { perilus_set_memory(address, value) };
}
+ pub(crate) fn load_file(&self, file: PathBuf) -> io::Result<()> {
+ let bytes = std::fs::read(file)?;
+ if bytes.len() > Self::MEMORY_SIZE_WORDS * 4 {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "Provided file is too large to fit in simulated memory",
+ ));
+ }
+ for (address_bytes, value) in bytes.iter().enumerate() {
+ let address_words = address_bytes / 4;
+ let shift = (address_bytes % 4) * 8;
+ let current = self.get_memory()[address_words];
+ let mask = !(0xffu32 << shift);
+ let new = (current & mask) | ((*value as u32) << shift);
+ self.set_memory(address_words as u32, new);
+ }
+ Ok(())
+ }
}
impl Drop for Perilus {