diff --git a/.gitignore b/.gitignore
index ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba..507684b6bfd3372427f56fbe2cea93961333b2a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 /target
+*.dot
diff --git a/Cargo.lock b/Cargo.lock
index ace37b6a49a23baec0d94db39d85290696a2d58e..bfb5320c6fffa6fdedf2475f23c8c6436f9a9739 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,12 +2,112 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "anstream"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "clap"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
 [[package]]
 name = "hercules_ir"
 version = "0.1.0"
@@ -16,6 +116,14 @@ dependencies = [
  "ordered-float",
 ]
 
+[[package]]
+name = "hercules_tools"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "hercules_ir",
+]
+
 [[package]]
 name = "memchr"
 version = "2.6.3"
@@ -55,3 +163,116 @@ checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06"
 dependencies = [
  "num-traits",
 ]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "2.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
diff --git a/Cargo.toml b/Cargo.toml
index c93ca7055db1b397d0e103312a52772af7c4386a..a5227e311467bcf857ec0d14c6e36b2d337b95b1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,6 @@
 [workspace]
 
 members = [
-	"hercules_ir"
+	"hercules_ir",
+	"hercules_tools"
 ]
diff --git a/hercules_ir/src/parse.rs b/hercules_ir/src/parse.rs
index 9f401ea99dfbf417d7fc630711f73ea531a9b7a4..ff3bfa4d2eba9b48feac2d3d6fd014bdb4444ec8 100644
--- a/hercules_ir/src/parse.rs
+++ b/hercules_ir/src/parse.rs
@@ -4,7 +4,7 @@ use std::collections::HashMap;
 
 use crate::*;
 
-fn parse(ir_test: &str) -> Module {
+pub fn parse(ir_test: &str) -> Module {
     parse_module(ir_test, Context::default()).unwrap().1
 }
 
@@ -347,9 +347,5 @@ fn add(x: i32, y: i32) -> i32
   z = add(start, x, y)
 ",
         );
-        println!("{:?}", module);
-        let mut dot = String::new();
-        write_dot(&module, &mut dot).unwrap();
-        println!("{}", dot);
     }
 }
diff --git a/hercules_tools/Cargo.toml b/hercules_tools/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..260db105d3af6ecdceadcc7a1c1d37e64be4f1e7
--- /dev/null
+++ b/hercules_tools/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "hercules_tools"
+version = "0.1.0"
+authors = ["Russel Arbore <rarbore2@illinois.edu>"]
+
+[[bin]]
+name = "hercules_dot"
+path = "src/hercules_dot/main.rs"
+
+[dependencies]
+clap = { version = "*", features = ["derive"] }
+hercules_ir = { path = "../hercules_ir" }
diff --git a/hercules_tools/src/hercules_dot/main.rs b/hercules_tools/src/hercules_dot/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a9153689434d50bb7cb288d2068acb9cffd35372
--- /dev/null
+++ b/hercules_tools/src/hercules_dot/main.rs
@@ -0,0 +1,51 @@
+extern crate clap;
+
+use std::env::temp_dir;
+use std::fs::File;
+use std::io::prelude::*;
+use std::process::Command;
+
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    hir_file: String,
+
+    #[arg(short, long, default_value_t = String::new())]
+    output: String,
+}
+
+fn main() {
+    let args = Args::parse();
+    if !args.hir_file.ends_with(".hir") {
+        eprintln!("WARNING: Running hercules_dot on a file without a .hir extension - interpreting as a textual Hercules IR file.");
+    }
+
+    let mut file = File::open(args.hir_file).expect("PANIC: Unable to open input file.");
+    let mut contents = String::new();
+    file.read_to_string(&mut contents)
+        .expect("PANIC: Unable to read input file contents.");
+    let module = hercules_ir::parse::parse(&contents);
+    if args.output.is_empty() {
+        let mut tmp_path = temp_dir();
+        tmp_path.push("hercules_dot.dot");
+        let mut file = File::create(tmp_path.clone()).expect("PANIC: Unable to open output file.");
+        let mut contents = String::new();
+        hercules_ir::dot::write_dot(&module, &mut contents)
+            .expect("PANIC: Unable to generate output file contents.");
+        file.write_all(contents.as_bytes())
+            .expect("PANIC: Unable to write output file contents.");
+        Command::new("xdot")
+            .args([tmp_path])
+            .output()
+            .expect("PANIC: Couldn't execute xdot.");
+    } else {
+        let mut file = File::create(args.output).expect("PANIC: Unable to open output file.");
+        let mut contents = String::new();
+        hercules_ir::dot::write_dot(&module, &mut contents)
+            .expect("PANIC: Unable to generate output file contents.");
+        file.write_all(contents.as_bytes())
+            .expect("PANIC: Unable to write output file contents.");
+    }
+}
diff --git a/samples/simple1.dot b/samples/simple1.dot
new file mode 100644
index 0000000000000000000000000000000000000000..0b74f0cf7507d8c25baf20fd32887a78613425d4
--- /dev/null
+++ b/samples/simple1.dot
@@ -0,0 +1,38 @@
+digraph "Module" {
+compound=true
+subgraph add {
+label="add"
+bgcolor=ivory4
+cluster=true
+start_0_0 [label="start"];
+parameter_0_1 [label="param #1"];
+parameter_0_2 [label="param #2"];
+constant_0_3 [label="Integer8(5)"];
+add_0_6 [label="add"];
+start_0_0 -> add_0_6 [style="dashed"];
+parameter_0_1 -> add_0_6;
+parameter_0_2 -> add_0_6;
+add_0_4 [label="add"];
+start_0_0 -> add_0_4 [style="dashed"];
+add_0_6 -> add_0_4;
+constant_0_3 -> add_0_4;
+return_0_5 [label="return"];
+start_0_0 -> return_0_5 [style="dashed"];
+add_0_4 -> return_0_5;
+}
+subgraph myfunc {
+label="myfunc"
+bgcolor=ivory4
+cluster=true
+start_1_0 [label="start"];
+parameter_1_1 [label="param #1"];
+parameter_1_1 -> call_1_2;
+parameter_1_1 -> call_1_2;
+call_1_2 [label="call(add)"];
+start_1_0 -> call_1_2 [style="dashed"];
+call_1_2 -> start_0_0 [lhead=add];
+return_1_3 [label="return"];
+start_1_0 -> return_1_3 [style="dashed"];
+call_1_2 -> return_1_3;
+}
+}
diff --git a/samples/simple1.hir b/samples/simple1.hir
new file mode 100644
index 0000000000000000000000000000000000000000..23e4d3b32456a16abdb4ef7a0321c62c5579a909
--- /dev/null
+++ b/samples/simple1.hir
@@ -0,0 +1,10 @@
+fn myfunc(x: i32) -> i32
+  y = call(start, add, x, x)
+  r = return(start, y)
+
+fn add(x: i32, y: i32) -> i32
+  c = constant(i8, 5)
+  r = return(start, w)
+  w = add(start, z, c)
+  z = add(start, x, y)
+