diff --git a/DESIGN.md b/DESIGN.md
index 6ab08b1ba46b3c31e38739a0742f66223caddf9f..9d5d7ddd9c5e41c20c1a9cc2c526c946960ce4ff 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -32,6 +32,8 @@ The IR of the Hercules compiler is similar to the sea of nodes IR presented in "
 
 A key design consideration of Hercules IR is the absence of a concept of memory. A downside of this approach is that any language targetting Hecules IR must also be very restrictive regarding memory - in practice, this means tightly controlling or eliminating first-class references. The upside is that the compiler has complete freedom to layout data however it likes in memory when performing code generation. This includes deciding which data resides in which address spaces, which is a necessary ability for a compiler striving to have fine-grained control over what operations are computed on what devices.
 
+In addition to not having a generalized memory, Hercules IR has no functionality for calling functions with side-effects, or doing IO. In other words, Hercules is a pure IR (it's not functional, as functions aren't first class values). This may be changed in the future - we could support effectful programs by giving call operators a control input and output edge. However, at least for now, we need to work with the simplest IR possible.
+
 ### Optimizations
 
 TODO: @rarbore2
diff --git a/hercules_ir/src/dot.rs b/hercules_ir/src/dot.rs
index b953c219359d1e5e37245df281fe0db98dc74472..e72f118df0d0bca613758e55a77a6285c1194693 100644
--- a/hercules_ir/src/dot.rs
+++ b/hercules_ir/src/dot.rs
@@ -6,26 +6,29 @@ pub fn write_dot<W: std::fmt::Write>(module: &Module, w: &mut W) -> std::fmt::Re
     write!(w, "digraph \"Module\" {{\n")?;
     write!(w, "compound=true\n")?;
     for i in 0..module.functions.len() {
-        write_function(i, module, &module.constants, w)?;
+        write_function(i, module, w)?;
     }
     write!(w, "}}\n")?;
     Ok(())
 }
 
-fn write_function<W: std::fmt::Write>(
-    i: usize,
-    module: &Module,
-    constants: &Vec<Constant>,
-    w: &mut W,
-) -> std::fmt::Result {
+fn write_function<W: std::fmt::Write>(i: usize, module: &Module, w: &mut W) -> std::fmt::Result {
     write!(w, "subgraph {} {{\n", module.functions[i].name)?;
-    write!(w, "label=\"{}\"\n", module.functions[i].name)?;
+    if module.functions[i].num_dynamic_constants > 0 {
+        write!(
+            w,
+            "label=\"{}<{}>\"\n",
+            module.functions[i].name, module.functions[i].num_dynamic_constants
+        )?;
+    } else {
+        write!(w, "label=\"{}\"\n", module.functions[i].name)?;
+    }
     write!(w, "bgcolor=ivory4\n")?;
     write!(w, "cluster=true\n")?;
     let mut visited = HashMap::default();
     let function = &module.functions[i];
     for j in 0..function.nodes.len() {
-        visited = write_node(i, j, module, constants, visited, w)?.1;
+        visited = write_node(i, j, module, visited, w)?.1;
     }
     write!(w, "}}\n")?;
     Ok(())
@@ -35,7 +38,6 @@ fn write_node<W: std::fmt::Write>(
     i: usize,
     j: usize,
     module: &Module,
-    constants: &Vec<Constant>,
     mut visited: HashMap<NodeID, String>,
     w: &mut W,
 ) -> Result<(String, HashMap<NodeID, String>), std::fmt::Error> {
@@ -51,32 +53,119 @@ fn write_node<W: std::fmt::Write>(
                 write!(w, "{} [label=\"start\"];\n", name)?;
                 visited
             }
+            Node::Region { preds } => {
+                write!(w, "{} [label=\"region\"];\n", name)?;
+                for (idx, pred) in preds.iter().enumerate() {
+                    let (pred_name, tmp_visited) = write_node(i, pred.idx(), module, visited, w)?;
+                    visited = tmp_visited;
+                    write!(
+                        w,
+                        "{} -> {} [label=\"pred {}\", style=\"dashed\"];\n",
+                        pred_name, name, idx
+                    )?;
+                }
+                visited
+            }
+            Node::If { control, cond } => {
+                write!(w, "{} [label=\"if\"];\n", name)?;
+                let (control_name, visited) = write_node(i, control.idx(), module, visited, w)?;
+                let (cond_name, visited) = write_node(i, cond.idx(), module, visited, w)?;
+                write!(
+                    w,
+                    "{} -> {} [label=\"control\", style=\"dashed\"];\n",
+                    control_name, name
+                )?;
+                write!(w, "{} -> {} [label=\"cond\"];\n", cond_name, name)?;
+                visited
+            }
+            Node::Fork { control, factor } => {
+                write!(
+                    w,
+                    "{} [label=\"fork<{:?}>\"];\n",
+                    name,
+                    module.dynamic_constants[factor.idx()]
+                )?;
+                let (control_name, visited) = write_node(i, control.idx(), module, visited, w)?;
+                write!(
+                    w,
+                    "{} -> {} [label=\"control\", style=\"dashed\"];\n",
+                    control_name, name
+                )?;
+                visited
+            }
+            Node::Join { control, data } => {
+                write!(w, "{} [label=\"join\"];\n", name,)?;
+                let (control_name, visited) = write_node(i, control.idx(), module, visited, w)?;
+                let (data_name, visited) = write_node(i, data.idx(), module, visited, w)?;
+                write!(
+                    w,
+                    "{} -> {} [label=\"control\", style=\"dashed\"];\n",
+                    control_name, name
+                )?;
+                write!(w, "{} -> {} [label=\"data\"];\n", data_name, name)?;
+                visited
+            }
+            Node::Phi { control, data } => {
+                write!(w, "{} [label=\"phi\"];\n", name)?;
+                let (control_name, mut visited) = write_node(i, control.idx(), module, visited, w)?;
+                write!(
+                    w,
+                    "{} -> {} [label=\"control\", style=\"dashed\"];\n",
+                    control_name, name
+                )?;
+                for (idx, data) in data.iter().enumerate() {
+                    let (data_name, tmp_visited) = write_node(i, data.idx(), module, visited, w)?;
+                    visited = tmp_visited;
+                    write!(w, "{} -> {} [label=\"data {}\"];\n", data_name, name, idx)?;
+                }
+                visited
+            }
             Node::Return { control, value } => {
-                let (control_name, visited) =
-                    write_node(i, control.idx(), module, constants, visited, w)?;
-                let (value_name, visited) =
-                    write_node(i, value.idx(), module, constants, visited, w)?;
+                let (control_name, visited) = write_node(i, control.idx(), module, visited, w)?;
+                let (value_name, visited) = write_node(i, value.idx(), module, visited, w)?;
                 write!(w, "{} [label=\"return\"];\n", name)?;
-                write!(w, "{} -> {} [style=\"dashed\"];\n", control_name, name)?;
-                write!(w, "{} -> {};\n", value_name, name)?;
+                write!(
+                    w,
+                    "{} -> {} [label=\"control\", style=\"dashed\"];\n",
+                    control_name, name
+                )?;
+                write!(w, "{} -> {} [label=\"value\"];\n", value_name, name)?;
                 visited
             }
             Node::Parameter { index } => {
-                write!(w, "{} [label=\"param #{}\"];\n", name, index)?;
+                write!(w, "{} [label=\"param #{}\"];\n", name, index + 1)?;
                 visited
             }
             Node::Constant { id } => {
-                write!(w, "{} [label=\"{:?}\"];\n", name, constants[id.idx()])?;
+                write!(
+                    w,
+                    "{} [label=\"{:?}\"];\n",
+                    name,
+                    module.constants[id.idx()]
+                )?;
+                visited
+            }
+            Node::DynamicConstant { id } => {
+                write!(
+                    w,
+                    "{} [label=\"dynamic_constant({:?})\"];\n",
+                    name,
+                    module.dynamic_constants[id.idx()]
+                )?;
                 visited
             }
-            Node::Add { left, right } => {
-                let (left_name, visited) =
-                    write_node(i, left.idx(), module, constants, visited, w)?;
-                let (right_name, visited) =
-                    write_node(i, right.idx(), module, constants, visited, w)?;
-                write!(w, "{} [label=\"add\"];\n", name)?;
-                write!(w, "{} -> {};\n", left_name, name)?;
-                write!(w, "{} -> {};\n", right_name, name)?;
+            Node::Unary { input, op } => {
+                write!(w, "{} [label=\"{}\"];\n", name, get_string_uop_kind(*op))?;
+                let (input_name, visited) = write_node(i, input.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"input\"];\n", input_name, name)?;
+                visited
+            }
+            Node::Binary { left, right, op } => {
+                write!(w, "{} [label=\"{}\"];\n", name, get_string_bop_kind(*op))?;
+                let (left_name, visited) = write_node(i, left.idx(), module, visited, w)?;
+                let (right_name, visited) = write_node(i, right.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"left\"];\n", left_name, name)?;
+                write!(w, "{} -> {} [label=\"right\"];\n", right_name, name)?;
                 visited
             }
             Node::Call {
@@ -84,28 +173,90 @@ fn write_node<W: std::fmt::Write>(
                 dynamic_constants,
                 args,
             } => {
-                for arg in args.iter() {
-                    let (arg_name, tmp_visited) =
-                        write_node(i, arg.idx(), module, constants, visited, w)?;
+                write!(w, "{} [label=\"call<", name,)?;
+                for (idx, id) in dynamic_constants.iter().enumerate() {
+                    let dc = &module.dynamic_constants[id.idx()];
+                    if idx == 0 {
+                        write!(w, "{:?}", dc)?;
+                    } else {
+                        write!(w, ", {:?}", dc)?;
+                    }
+                }
+                write!(w, ">({})\"];\n", module.functions[function.idx()].name)?;
+                for (idx, arg) in args.iter().enumerate() {
+                    let (arg_name, tmp_visited) = write_node(i, arg.idx(), module, visited, w)?;
                     visited = tmp_visited;
-                    write!(w, "{} -> {};\n", arg_name, name)?;
+                    write!(w, "{} -> {} [label=\"arg {}\"];\n", arg_name, name, idx)?;
                 }
                 write!(
                     w,
-                    "{} [label=\"call({})\"];\n",
+                    "{} -> start_{}_0 [label=\"call\", lhead={}];\n",
                     name,
+                    function.idx(),
                     module.functions[function.idx()].name
                 )?;
+                visited
+            }
+            Node::ReadProd { prod, index } => {
+                write!(w, "{} [label=\"read_prod({})\"];\n", name, index)?;
+                let (prod_name, visited) = write_node(i, prod.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"prod\"];\n", prod_name, name)?;
+                visited
+            }
+            Node::WriteProd { prod, data, index } => {
+                write!(w, "{} [label=\"write_prod({})\"];\n", name, index)?;
+                let (prod_name, visited) = write_node(i, prod.idx(), module, visited, w)?;
+                let (data_name, visited) = write_node(i, data.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"prod\"];\n", prod_name, name)?;
+                write!(w, "{} -> {} [label=\"data\"];\n", data_name, name)?;
+                visited
+            }
+            Node::ReadArray { array, index } => {
+                write!(w, "{} [label=\"read_array\"];\n", name)?;
+                let (array_name, visited) = write_node(i, array.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"array\"];\n", array_name, name)?;
+                let (index_name, visited) = write_node(i, index.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"index\"];\n", index_name, name)?;
+                visited
+            }
+            Node::WriteArray { array, data, index } => {
+                write!(w, "{} [label=\"write_array\"];\n", name)?;
+                let (array_name, visited) = write_node(i, array.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"array\"];\n", array_name, name)?;
+                let (data_name, visited) = write_node(i, data.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"data\"];\n", data_name, name)?;
+                let (index_name, visited) = write_node(i, index.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"index\"];\n", index_name, name)?;
+                visited
+            }
+            Node::Match { control, sum } => {
+                write!(w, "{} [label=\"match\"];\n", name)?;
+                let (control_name, visited) = write_node(i, control.idx(), module, visited, w)?;
+                write!(
+                    w,
+                    "{} -> {} [label=\"control\", style=\"dashed\"];\n",
+                    control_name, name
+                )?;
+                let (sum_name, visited) = write_node(i, sum.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"sum\"];\n", sum_name, name)?;
+                visited
+            }
+            Node::BuildSum {
+                data,
+                sum_ty,
+                variant,
+            } => {
                 write!(
                     w,
-                    "{} -> start_{}_0 [lhead={}];\n",
+                    "{} [label=\"build_sum({:?}, {})\"];\n",
                     name,
-                    function.idx(),
-                    module.functions[function.idx()].name
+                    module.types[sum_ty.idx()],
+                    variant
                 )?;
+                let (data_name, visited) = write_node(i, data.idx(), module, visited, w)?;
+                write!(w, "{} -> {} [label=\"data\"];\n", data_name, name)?;
                 visited
             }
-            _ => todo!(),
         };
         Ok((visited.get(&id).unwrap().clone(), visited))
     }
@@ -125,7 +276,7 @@ fn get_string_node_kind(node: &Node) -> &'static str {
         } => "fork",
         Node::Join {
             control: _,
-            factor: _,
+            data: _,
         } => "join",
         Node::Phi {
             control: _,
@@ -138,14 +289,57 @@ fn get_string_node_kind(node: &Node) -> &'static str {
         Node::Parameter { index: _ } => "parameter",
         Node::DynamicConstant { id: _ } => "dynamic_constant",
         Node::Constant { id: _ } => "constant",
-        Node::Add { left: _, right: _ } => "add",
-        Node::Sub { left: _, right: _ } => "sub",
-        Node::Mul { left: _, right: _ } => "mul",
-        Node::Div { left: _, right: _ } => "div",
+        Node::Unary { input: _, op } => get_string_uop_kind(*op),
+        Node::Binary {
+            left: _,
+            right: _,
+            op,
+        } => get_string_bop_kind(*op),
         Node::Call {
             function: _,
             dynamic_constants: _,
             args: _,
         } => "call",
+        Node::ReadProd { prod: _, index: _ } => "read_prod",
+        Node::WriteProd {
+            prod: _,
+            data: _,
+            index: _,
+        } => "write_prod ",
+        Node::ReadArray { array: _, index: _ } => "read_array",
+        Node::WriteArray {
+            array: _,
+            data: _,
+            index: _,
+        } => "write_array",
+        Node::Match { control: _, sum: _ } => "match",
+        Node::BuildSum {
+            data: _,
+            sum_ty: _,
+            variant: _,
+        } => "build_sum",
+    }
+}
+
+fn get_string_uop_kind(uop: UnaryOperator) -> &'static str {
+    match uop {
+        UnaryOperator::Not => "not",
+        UnaryOperator::Neg => "neg",
+    }
+}
+
+fn get_string_bop_kind(bop: BinaryOperator) -> &'static str {
+    match bop {
+        BinaryOperator::Add => "add",
+        BinaryOperator::Sub => "sub",
+        BinaryOperator::Mul => "mul",
+        BinaryOperator::Div => "div",
+        BinaryOperator::Rem => "rem",
+        BinaryOperator::LT => "lt",
+        BinaryOperator::LTE => "lte",
+        BinaryOperator::GT => "gt",
+        BinaryOperator::GTE => "gte",
+        BinaryOperator::EQ => "eq",
+        BinaryOperator::NE => "ne",
     }
 }
diff --git a/hercules_ir/src/ir.rs b/hercules_ir/src/ir.rs
index fbb9db0365b97b0bcedd3b7feee26d968471be2c..639437a6763c42bcf5db65ce8f522b75739d027e 100644
--- a/hercules_ir/src/ir.rs
+++ b/hercules_ir/src/ir.rs
@@ -1,5 +1,11 @@
 extern crate ordered_float;
 
+/*
+ * A module is a list of functions. Functions contain types, constants, and
+ * dynamic constants, which are interned at the module level. Thus, if one
+ * wants to run an intraprocedural pass in parallel, it is advised to first
+ * destruct the module, then reconstruct it once finished.
+ */
 #[derive(Debug, Clone)]
 pub struct Module {
     pub functions: Vec<Function>,
@@ -8,6 +14,14 @@ pub struct Module {
     pub dynamic_constants: Vec<DynamicConstant>,
 }
 
+/*
+ * A function has a name, a list of types for its parameters, a single return
+ * type, a list of nodes in its sea-of-nodes style IR, and a number of dynamic
+ * constants. When calling a function, arguments matching the parameter types
+ * are required, as well as the correct number of dynamic constants. All
+ * dynamic constants are 64-bit unsigned integers (usize / u64), so it is
+ * sufficient to merely store how many of them the function takes as arguments.
+ */
 #[derive(Debug, Clone)]
 pub struct Function {
     pub name: String,
@@ -17,9 +31,22 @@ pub struct Function {
     pub num_dynamic_constants: u32,
 }
 
+/*
+ * Hercules IR has a fairly standard type system, with the exception of the
+ * control type. Hercules IR is based off of the sea-of-nodes IR, the main
+ * feature of which being a merged control and data flow graph. Thus, control
+ * is a type of value, just like any other type. However, the type system is
+ * very restrictive over what can be done with control values. A novel addition
+ * in Hercules IR is that a control type is parameterized by a list of thread
+ * spawning factors. This is the mechanism in Hercules IR for representing
+ * parallelism. Summation types are an IR equivalent of Rust's enum types.
+ * These are lowered into tagged unions during scheduling. Array types are one-
+ * dimensional. Multi-dimensional arrays are represented by nesting array types.
+ * An array extent is represented with a dynamic constant.
+ */
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum Type {
-    Control(DynamicConstantID),
+    Control(Box<[DynamicConstantID]>),
     Integer8,
     Integer16,
     Integer32,
@@ -32,9 +59,17 @@ pub enum Type {
     Float64,
     Product(Box<[TypeID]>),
     Summation(Box<[TypeID]>),
-    Array(TypeID, Box<[DynamicConstantID]>),
+    Array(TypeID, DynamicConstantID),
 }
 
+/*
+ * Constants are pretty standard in Hercules IR. Float constants used the
+ * ordered_float crate so that constants can be keys in maps (used for
+ * interning constants during IR construction). Product, summation, and array
+ * constants all contain their own type. This is only strictly necessary for
+ * summation types, but provides a nice mechanism for sanity checking for
+ * product and array types as well.
+ */
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum Constant {
     Integer8(i8),
@@ -52,12 +87,34 @@ pub enum Constant {
     Array(TypeID, Box<[ConstantID]>),
 }
 
+/*
+ * Dynamic constants are unsigned 64-bit integers passed to a Hercules function
+ * at runtime using the Hercules runtime API. They cannot be the result of
+ * computations in Hercules IR. For a single execution of a Hercules function,
+ * dynamic constants are constant throughout execution. This provides a
+ * mechanism by which Hercules functions can operate on arrays with variable
+ * length, while not needing Hercules functions to perform dynamic memory
+ * allocation - by providing dynamic constants to the runtime API, the runtime
+ * can allocate memory as necessary.
+ */
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum DynamicConstant {
     Constant(usize),
     Parameter(usize),
 }
 
+/*
+ * Hercules IR is a combination of a possibly cylic control flow graph, and
+ * many acyclic data flow graphs. Each node represents some operation on input
+ * values (including control), and produces some output value. Operations that
+ * conceptually produce multiple outputs (such as an if node) produce a product
+ * type instead. For example, the if node produces prod(control(N),
+ * control(N)), where the first control token represents the false branch, and
+ * the second control token represents the true branch. Another example is the
+ * fork node, which produces prod(control(N*k), u64), where the u64 is the
+ * thread ID. Functions are devoid of side effects, so call nodes don't take as
+ * input or output control tokens. There is also no global memory - use arrays.
+ */
 #[derive(Debug, Clone)]
 pub enum Node {
     Start,
@@ -74,7 +131,7 @@ pub enum Node {
     },
     Join {
         control: NodeID,
-        factor: DynamicConstantID,
+        data: NodeID,
     },
     Phi {
         control: NodeID,
@@ -93,29 +150,73 @@ pub enum Node {
     DynamicConstant {
         id: DynamicConstantID,
     },
-    Add {
-        left: NodeID,
-        right: NodeID,
-    },
-    Sub {
-        left: NodeID,
-        right: NodeID,
-    },
-    Mul {
-        left: NodeID,
-        right: NodeID,
+    Unary {
+        input: NodeID,
+        op: UnaryOperator,
     },
-    Div {
+    Binary {
         left: NodeID,
         right: NodeID,
+        op: BinaryOperator,
     },
     Call {
         function: FunctionID,
         dynamic_constants: Box<[DynamicConstantID]>,
         args: Box<[NodeID]>,
     },
+    ReadProd {
+        prod: NodeID,
+        index: usize,
+    },
+    WriteProd {
+        prod: NodeID,
+        data: NodeID,
+        index: usize,
+    },
+    ReadArray {
+        array: NodeID,
+        index: NodeID,
+    },
+    WriteArray {
+        array: NodeID,
+        data: NodeID,
+        index: NodeID,
+    },
+    Match {
+        control: NodeID,
+        sum: NodeID,
+    },
+    BuildSum {
+        data: NodeID,
+        sum_ty: TypeID,
+        variant: usize,
+    },
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum UnaryOperator {
+    Not,
+    Neg,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum BinaryOperator {
+    Add,
+    Sub,
+    Mul,
+    Div,
+    Rem,
+    LT,
+    LTE,
+    GT,
+    GTE,
+    EQ,
+    NE,
 }
 
+/*
+ * Rust things to make newtyped IDs usable.
+ */
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub struct FunctionID(u32);
 
diff --git a/hercules_ir/src/parse.rs b/hercules_ir/src/parse.rs
index 76956e84ed26b4a0edbf71bf961087e40e53edb7..50613f5c82190aa61b0617ef82775fba52c0a22d 100644
--- a/hercules_ir/src/parse.rs
+++ b/hercules_ir/src/parse.rs
@@ -6,10 +6,21 @@ use std::str::FromStr;
 
 use crate::*;
 
+/*
+ * Top level parse function.
+ */
 pub fn parse(ir_test: &str) -> Module {
     parse_module(ir_test, Context::default()).unwrap().1
 }
 
+/*
+ * This is a context sensitive parser. We parse directly into the graph data
+ * structure inside ir::Module, so this is where we perform interning.
+ * We intern function names, node names, types, constants, and dynamic
+ * constants. Sometimes, types and dynamic constants need to be looked up, so
+ * we also maintain reverse intern maps for that purpose. IDs are assigned
+ * in increasing order, based on the intern map's size.
+ */
 #[derive(Default)]
 struct Context<'a> {
     function_ids: HashMap<&'a str, FunctionID>,
@@ -21,6 +32,10 @@ struct Context<'a> {
     reverse_dynamic_constant_map: HashMap<DynamicConstantID, DynamicConstant>,
 }
 
+/*
+ * Interning functions. In general, all modifications to intern maps should be
+ * done through these functions.
+ */
 impl<'a> Context<'a> {
     fn get_function_id(&mut self, name: &'a str) -> FunctionID {
         if let Some(id) = self.function_ids.get(name) {
@@ -77,13 +92,22 @@ impl<'a> Context<'a> {
     }
 }
 
+/*
+ * A module is just a file with a list of functions.
+ */
 fn parse_module<'a>(ir_text: &'a str, context: Context<'a>) -> nom::IResult<&'a str, Module> {
     let context = RefCell::new(context);
+
+    // If there is any text left after successfully parsing some functions,
+    // treat that as an error.
     let (rest, functions) =
         nom::combinator::all_consuming(nom::multi::many0(|x| parse_function(x, &context)))(
             ir_text,
         )?;
     let mut context = context.into_inner();
+
+    // functions, as returned by parsing, is in parse order, which may differ
+    // from the order dictated by FunctionIDs in the function name intern map.
     let mut fixed_functions = vec![
         Function {
             name: String::from(""),
@@ -96,10 +120,16 @@ fn parse_module<'a>(ir_text: &'a str, context: Context<'a>) -> nom::IResult<&'a
     ];
     for function in functions {
         let function_name = function.name.clone();
+
+        // We can remove items from context now, as it's going to be destroyed
+        // anyway.
         let function_id = context.function_ids.remove(function_name.as_str()).unwrap();
         fixed_functions[function_id.idx()] = function;
     }
-    let mut types = vec![Type::Control(DynamicConstantID::new(0)); context.interned_types.len()];
+
+    // Assemble flat lists of interned goodies, now that we've figured out
+    // everyones' IDs.
+    let mut types = vec![Type::Control(Box::new([])); context.interned_types.len()];
     for (ty, id) in context.interned_types {
         types[id.idx()] = ty;
     }
@@ -121,11 +151,20 @@ fn parse_module<'a>(ir_text: &'a str, context: Context<'a>) -> nom::IResult<&'a
     Ok((rest, module))
 }
 
+/*
+ * A function is a function declaration, followed by a list of node statements.
+ */
 fn parse_function<'a>(
     ir_text: &'a str,
     context: &RefCell<Context<'a>>,
 ) -> nom::IResult<&'a str, Function> {
+    // Each function contains its own list of interned nodes, so we need to
+    // clear the node name intern map.
     context.borrow_mut().node_ids.clear();
+
+    // This parser isn't split into lexing and parsing steps. So, we very
+    // frequently need to eat whitespace. Is this ugly? Yes. Does it work? Also
+    // yes.
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let ir_text = nom::bytes::complete::tag("fn")(ir_text)?.0;
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
@@ -141,6 +180,8 @@ fn parse_function<'a>(
     };
     let (ir_text, num_dynamic_constants) =
         nom::combinator::opt(parse_num_dynamic_constants)(ir_text)?;
+
+    // If unspecified, assumed function has no dynamic constant arguments.
     let num_dynamic_constants = num_dynamic_constants.unwrap_or(0);
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let ir_text = nom::character::complete::char('(')(ir_text)?.0;
@@ -156,29 +197,49 @@ fn parse_function<'a>(
             nom::character::complete::multispace0,
         )),
     )(ir_text)?;
-    context
-        .borrow_mut()
-        .node_ids
-        .insert("start", NodeID::new(0));
+
+    // The start node is not explicitly specified in the textual IR, so create
+    // it manually.
+    context.borrow_mut().get_node_id("start");
+
+    // Insert nodes for each parameter.
     for param in params.iter() {
-        let id = NodeID::new(context.borrow().node_ids.len());
-        context.borrow_mut().node_ids.insert(param.1, id);
+        context.borrow_mut().get_node_id(param.1);
     }
     let ir_text = nom::character::complete::char(')')(ir_text)?.0;
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let ir_text = nom::bytes::complete::tag("->")(ir_text)?.0;
     let (ir_text, return_type) = parse_type_id(ir_text, context)?;
     let (ir_text, nodes) = nom::multi::many1(|x| parse_node(x, context))(ir_text)?;
+
+    // nodes, as returned by parsing, is in parse order, which may differ from
+    // the order dictated by NodeIDs in the node name intern map.
     let mut fixed_nodes = vec![Node::Start; context.borrow().node_ids.len()];
     for (name, node) in nodes {
+        // We can remove items from the node name intern map now, as the map
+        // will be cleared during the next iteration of parse_function.
         fixed_nodes[context.borrow_mut().node_ids.remove(name).unwrap().idx()] = node;
     }
+
+    // The nodes removed from node_ids in the previous step are nodes that are
+    // defined in statements parsed by parse_node. There are 2 kinds of nodes
+    // that aren't defined in statements inside the function body: the start
+    // node, and the parameter nodes. The node at ID 0 is already a start node,
+    // by the initialization of fixed_nodes. Here, we set the other nodes to
+    // parameter nodes. The node id in node_ids corresponds to the parameter
+    // index + 1, because in parse_function, we add the parameter names to
+    // node_ids (a.k.a. the node name intern map) in order, after adding the
+    // start node.
     for (_, id) in context.borrow().node_ids.iter() {
         if id.idx() != 0 {
-            fixed_nodes[id.idx()] = Node::Parameter { index: id.idx() }
+            fixed_nodes[id.idx()] = Node::Parameter {
+                index: id.idx() - 1,
+            }
         }
     }
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
+
+    // Intern function name.
     context.borrow_mut().get_function_id(function_name);
     Ok((
         ir_text,
@@ -192,6 +253,10 @@ fn parse_function<'a>(
     ))
 }
 
+/*
+ * A node is a statement of the form a = b(c), where a is the name of the output
+ * of the node, b is the node type, and c is a list of arguments.
+ */
 fn parse_node<'a>(
     ir_text: &'a str,
     context: &RefCell<Context<'a>>,
@@ -207,15 +272,39 @@ fn parse_node<'a>(
         "if" => parse_if(ir_text, context)?,
         "fork" => parse_fork(ir_text, context)?,
         "join" => parse_join(ir_text, context)?,
+        "phi" => parse_phi(ir_text, context)?,
         "return" => parse_return(ir_text, context)?,
         "constant" => parse_constant_node(ir_text, context)?,
-        "add" => parse_add(ir_text, context)?,
+        "dynamic_constant" => parse_dynamic_constant_node(ir_text, context)?,
+        // Unary and binary ops are spelled out in the textual format, but we
+        // parse them into Unary or Binary node kinds.
+        "not" => parse_unary(ir_text, context, UnaryOperator::Not)?,
+        "neg" => parse_unary(ir_text, context, UnaryOperator::Neg)?,
+        "add" => parse_binary(ir_text, context, BinaryOperator::Add)?,
+        "sub" => parse_binary(ir_text, context, BinaryOperator::Sub)?,
+        "mul" => parse_binary(ir_text, context, BinaryOperator::Mul)?,
+        "div" => parse_binary(ir_text, context, BinaryOperator::Div)?,
+        "rem" => parse_binary(ir_text, context, BinaryOperator::Rem)?,
+        "lt" => parse_binary(ir_text, context, BinaryOperator::LT)?,
+        "lte" => parse_binary(ir_text, context, BinaryOperator::LTE)?,
+        "gt" => parse_binary(ir_text, context, BinaryOperator::GT)?,
+        "gte" => parse_binary(ir_text, context, BinaryOperator::GTE)?,
+        "eq" => parse_binary(ir_text, context, BinaryOperator::EQ)?,
+        "ne" => parse_binary(ir_text, context, BinaryOperator::NE)?,
         "call" => parse_call(ir_text, context)?,
+        "read_prod" => parse_read_prod(ir_text, context)?,
+        "write_prod" => parse_write_prod(ir_text, context)?,
+        "read_array" => parse_read_array(ir_text, context)?,
+        "write_array" => parse_write_array(ir_text, context)?,
+        "match" => parse_match(ir_text, context)?,
+        "build_sum" => parse_build_sum(ir_text, context)?,
         _ => Err(nom::Err::Error(nom::error::Error {
             input: ir_text,
             code: nom::error::ErrorKind::IsNot,
         }))?,
     };
+
+    // Intern node name.
     context.borrow_mut().get_node_id(node_name);
     Ok((ir_text, (node_name, node)))
 }
@@ -224,6 +313,13 @@ fn parse_region<'a>(
     ir_text: &'a str,
     context: &RefCell<Context<'a>>,
 ) -> nom::IResult<&'a str, Node> {
+    // Each of these parse node functions are very similar. The node name and
+    // type have already been parsed, so here we just parse the node's
+    // arguments. These are always in between parantheses and separated by
+    // commas, so there are parse_tupleN utility functions that do this. If
+    // there is a variable amount of arguments, then we need to represent that
+    // explicitly using nom's separated list functionality. This example here
+    // is a bit of an abuse of what parse_tupleN functions are meant for.
     let (ir_text, (preds,)) = parse_tuple1(nom::multi::separated_list1(
         nom::sequence::tuple((
             nom::character::complete::multispace0,
@@ -232,6 +328,9 @@ fn parse_region<'a>(
         )),
         parse_identifier,
     ))(ir_text)?;
+
+    // When the parsed arguments are node names, we need to look up their ID in
+    // the node name intern map.
     let preds = preds
         .into_iter()
         .map(|x| context.borrow_mut().get_node_id(x))
@@ -250,14 +349,42 @@ fn parse_fork<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IRes
     let (ir_text, (control, factor)) =
         parse_tuple2(parse_identifier, |x| parse_dynamic_constant_id(x, context))(ir_text)?;
     let control = context.borrow_mut().get_node_id(control);
+
+    // Because parse_dynamic_constant_id returned a DynamicConstantID directly,
+    // we don't need to manually convert it here.
     Ok((ir_text, Node::Fork { control, factor }))
 }
 
 fn parse_join<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IResult<&'a str, Node> {
-    let (ir_text, (control, factor)) =
-        parse_tuple2(parse_identifier, |x| parse_dynamic_constant_id(x, context))(ir_text)?;
+    let (ir_text, (control, data)) = parse_tuple2(parse_identifier, parse_identifier)(ir_text)?;
     let control = context.borrow_mut().get_node_id(control);
-    Ok((ir_text, Node::Join { control, factor }))
+    let data = context.borrow_mut().get_node_id(data);
+
+    // A join node doesn't need to explicitly store a join factor. The join
+    // factor is implicitly stored at the tail of the control token's type
+    // level list of thread spawn factors. Intuitively, fork pushes to the end
+    // of this list, while join just pops from the end of this list.
+    Ok((ir_text, Node::Join { control, data }))
+}
+
+fn parse_phi<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (control, data)) = parse_tuple2(
+        parse_identifier,
+        nom::multi::separated_list1(
+            nom::sequence::tuple((
+                nom::character::complete::multispace0,
+                nom::character::complete::char(','),
+                nom::character::complete::multispace0,
+            )),
+            parse_identifier,
+        ),
+    )(ir_text)?;
+    let control = context.borrow_mut().get_node_id(control);
+    let data = data
+        .into_iter()
+        .map(|x| context.borrow_mut().get_node_id(x))
+        .collect();
+    Ok((ir_text, Node::Phi { control, data }))
 }
 
 fn parse_return<'a>(
@@ -274,6 +401,8 @@ fn parse_constant_node<'a>(
     ir_text: &'a str,
     context: &RefCell<Context<'a>>,
 ) -> nom::IResult<&'a str, Node> {
+    // Here, we don't use parse_tuple2 because there is a dependency between
+    // the parse functions of the 2 arguments.
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let ir_text = nom::character::complete::char('(')(ir_text)?.0;
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
@@ -287,14 +416,40 @@ fn parse_constant_node<'a>(
     Ok((ir_text, Node::Constant { id }))
 }
 
-fn parse_add<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IResult<&'a str, Node> {
+fn parse_dynamic_constant_node<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (id,)) = parse_tuple1(|x| parse_dynamic_constant_id(x, context))(ir_text)?;
+    Ok((ir_text, Node::DynamicConstant { id }))
+}
+
+fn parse_unary<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+    op: UnaryOperator,
+) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (input,)) = parse_tuple1(parse_identifier)(ir_text)?;
+    let input = context.borrow_mut().get_node_id(input);
+    Ok((ir_text, Node::Unary { input, op }))
+}
+
+fn parse_binary<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+    op: BinaryOperator,
+) -> nom::IResult<&'a str, Node> {
     let (ir_text, (left, right)) = parse_tuple2(parse_identifier, parse_identifier)(ir_text)?;
     let left = context.borrow_mut().get_node_id(left);
     let right = context.borrow_mut().get_node_id(right);
-    Ok((ir_text, Node::Add { left, right }))
+    Ok((ir_text, Node::Binary { left, right, op }))
 }
 
 fn parse_call<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IResult<&'a str, Node> {
+    // Call nodes are a bit complicated because they 1. optionally take dynamic
+    // constants as "arguments" (though these are specified between <>), 2.
+    // take a function name as an argument, and 3. take a variable number of
+    // normal arguments.
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let parse_dynamic_constants =
         |ir_text: &'a str| -> nom::IResult<&'a str, Vec<DynamicConstantID>> {
@@ -342,6 +497,50 @@ fn parse_call<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IRes
     ))
 }
 
+fn parse_read_prod<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (prod, index)) =
+        parse_tuple2(parse_identifier, |x| parse_prim::<usize>(x, "1234567890"))(ir_text)?;
+    let prod = context.borrow_mut().get_node_id(prod);
+    Ok((ir_text, Node::ReadProd { prod, index }))
+}
+
+fn parse_write_prod<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (prod, data, index)) = parse_tuple3(parse_identifier, parse_identifier, |x| {
+        parse_prim::<usize>(x, "1234567890")
+    })(ir_text)?;
+    let prod = context.borrow_mut().get_node_id(prod);
+    let data = context.borrow_mut().get_node_id(data);
+    Ok((ir_text, Node::WriteProd { prod, data, index }))
+}
+
+fn parse_read_array<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (array, index)) = parse_tuple2(parse_identifier, parse_identifier)(ir_text)?;
+    let array = context.borrow_mut().get_node_id(array);
+    let index = context.borrow_mut().get_node_id(index);
+    Ok((ir_text, Node::ReadArray { array, index }))
+}
+
+fn parse_write_array<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (array, data, index)) =
+        parse_tuple3(parse_identifier, parse_identifier, parse_identifier)(ir_text)?;
+    let array = context.borrow_mut().get_node_id(array);
+    let data = context.borrow_mut().get_node_id(data);
+    let index = context.borrow_mut().get_node_id(index);
+    Ok((ir_text, Node::WriteArray { array, data, index }))
+}
+
 fn parse_type_id<'a>(
     ir_text: &'a str,
     context: &RefCell<Context<'a>>,
@@ -352,20 +551,66 @@ fn parse_type_id<'a>(
     Ok((ir_text, id))
 }
 
+fn parse_match<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (control, sum)) = parse_tuple2(parse_identifier, parse_identifier)(ir_text)?;
+    let control = context.borrow_mut().get_node_id(control);
+    let sum = context.borrow_mut().get_node_id(sum);
+    Ok((ir_text, Node::Match { control, sum }))
+}
+
+fn parse_build_sum<'a>(
+    ir_text: &'a str,
+    context: &RefCell<Context<'a>>,
+) -> nom::IResult<&'a str, Node> {
+    let (ir_text, (data, sum_ty, variant)) = parse_tuple3(
+        parse_identifier,
+        |x| parse_type_id(x, context),
+        |x| parse_prim::<usize>(x, "1234567890"),
+    )(ir_text)?;
+    let data = context.borrow_mut().get_node_id(data);
+    Ok((
+        ir_text,
+        Node::BuildSum {
+            data,
+            sum_ty,
+            variant,
+        },
+    ))
+}
+
 fn parse_type<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IResult<&'a str, Type> {
+    // Parser combinators are very convenient, if a bit hard to read.
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let (ir_text, ty) = nom::branch::alt((
+        // Control tokens are parameterized by a list of dynamic constants
+        // representing their thread spawn factors.
         nom::combinator::map(
             nom::sequence::tuple((
                 nom::bytes::complete::tag("ctrl"),
                 nom::character::complete::multispace0,
                 nom::character::complete::char('('),
-                |x| parse_dynamic_constant_id(x, context),
+                nom::character::complete::multispace0,
+                nom::multi::separated_list1(
+                    nom::sequence::tuple((
+                        nom::character::complete::multispace0,
+                        nom::character::complete::char(','),
+                        nom::character::complete::multispace0,
+                    )),
+                    |x| parse_dynamic_constant_id(x, context),
+                ),
                 nom::character::complete::multispace0,
                 nom::character::complete::char(')'),
             )),
-            |(_, _, _, id, _, _)| Type::Control(id),
+            |(_, _, _, _, id, _, _)| Type::Control(id.into_boxed_slice()),
         ),
+        // If no arguments are provided, assumed that no forks have occurred.
+        nom::combinator::map(nom::bytes::complete::tag("ctrl"), |_| {
+            Type::Control(Box::new([]))
+        }),
+        // Primitive types are written in Rust style.
         nom::combinator::map(nom::bytes::complete::tag("i8"), |_| Type::Integer8),
         nom::combinator::map(nom::bytes::complete::tag("i16"), |_| Type::Integer16),
         nom::combinator::map(nom::bytes::complete::tag("i32"), |_| Type::Integer32),
@@ -382,6 +627,7 @@ fn parse_type<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IRes
         }),
         nom::combinator::map(nom::bytes::complete::tag("f32"), |_| Type::Float32),
         nom::combinator::map(nom::bytes::complete::tag("f64"), |_| Type::Float64),
+        // Product types are parsed as a list of their element types.
         nom::combinator::map(
             nom::sequence::tuple((
                 nom::bytes::complete::tag("prod"),
@@ -401,6 +647,7 @@ fn parse_type<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IRes
             )),
             |(_, _, _, _, ids, _, _)| Type::Product(ids.into_boxed_slice()),
         ),
+        // Sum types are parsed as a list of their variant types.
         nom::combinator::map(
             nom::sequence::tuple((
                 nom::bytes::complete::tag("sum"),
@@ -420,6 +667,8 @@ fn parse_type<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IRes
             )),
             |(_, _, _, _, ids, _, _)| Type::Summation(ids.into_boxed_slice()),
         ),
+        // Array types are just a pair between an element type and a dynamic
+        // constant representing its extent.
         nom::combinator::map(
             nom::sequence::tuple((
                 nom::bytes::complete::tag("array"),
@@ -430,25 +679,19 @@ fn parse_type<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IRes
                 nom::character::complete::multispace0,
                 nom::character::complete::char(','),
                 nom::character::complete::multispace0,
-                nom::multi::separated_list1(
-                    nom::sequence::tuple((
-                        nom::character::complete::multispace0,
-                        nom::character::complete::char(','),
-                        nom::character::complete::multispace0,
-                    )),
-                    |x| parse_dynamic_constant_id(x, context),
-                ),
+                |x| parse_dynamic_constant_id(x, context),
                 nom::character::complete::multispace0,
                 nom::character::complete::char(')'),
             )),
-            |(_, _, _, _, ty_id, _, _, _, dc_ids, _, _)| {
-                Type::Array(ty_id, dc_ids.into_boxed_slice())
-            },
+            |(_, _, _, _, ty_id, _, _, _, dc_id, _, _)| Type::Array(ty_id, dc_id),
         ),
     ))(ir_text)?;
     Ok((ir_text, ty))
 }
 
+// For types, constants, and dynamic constant parse functions, there is a
+// variant parsing the object itself, and a variant that parses the object and
+// returns the interned ID.
 fn parse_dynamic_constant_id<'a>(
     ir_text: &'a str,
     context: &RefCell<Context<'a>>,
@@ -467,6 +710,8 @@ fn parse_dynamic_constant<'a>(ir_text: &'a str) -> nom::IResult<&'a str, Dynamic
             |x| parse_prim::<usize>(x, "1234567890"),
             |x| DynamicConstant::Constant(x),
         ),
+        // Parameter dynamic constants of a function are written by preprending
+        // a '#' to the parameter's number.
         nom::combinator::map(
             nom::sequence::tuple((nom::character::complete::char('#'), |x| {
                 parse_prim::<usize>(x, "1234567890")
@@ -487,12 +732,20 @@ fn parse_constant_id<'a>(
     Ok((ir_text, id))
 }
 
+/*
+ * parse_constant requires a type argument so that we know what we're parsing
+ * upfront. Not having this would make parsing primitive constants much harder.
+ * This is a bad requirement to have for a source language, but for a verbose
+ * textual format for an IR, it's fine and simplifies the parser, typechecking,
+ * and the IR itself.
+ */
 fn parse_constant<'a>(
     ir_text: &'a str,
     ty: Type,
     context: &RefCell<Context<'a>>,
 ) -> nom::IResult<&'a str, Constant> {
     let (ir_text, constant) = match ty.clone() {
+        // There are not control constants.
         Type::Control(_) => Err(nom::Err::Error(nom::error::Error {
             input: ir_text,
             code: nom::error::ErrorKind::IsNot,
@@ -519,11 +772,10 @@ fn parse_constant<'a>(
             tys,
             context,
         )?,
-        Type::Array(elem_ty, dc_bounds) => parse_array_constant(
+        Type::Array(elem_ty, _) => parse_array_constant(
             ir_text,
             context.borrow_mut().get_type_id(ty.clone()),
             elem_ty,
-            dc_bounds,
             context,
         )?,
     };
@@ -531,6 +783,9 @@ fn parse_constant<'a>(
     Ok((ir_text, constant))
 }
 
+/*
+ * Utility for parsing types implementing FromStr.
+ */
 fn parse_prim<'a, T: FromStr>(ir_text: &'a str, chars: &'static str) -> nom::IResult<&'a str, T> {
     let (ir_text, x_text) = nom::bytes::complete::is_a(chars)(ir_text)?;
     let x = x_text.parse::<T>().map_err(|_| {
@@ -609,6 +864,8 @@ fn parse_product_constant<'a>(
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let mut ir_text = nom::character::complete::char('(')(ir_text)?.0;
     let mut subconstants = vec![];
+
+    // There should be one constant for each element type.
     for ty in tys.iter() {
         if !subconstants.is_empty() {
             ir_text = nom::character::complete::multispace0(ir_text)?.0;
@@ -642,6 +899,8 @@ fn parse_summation_constant<'a>(
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let ir_text = nom::character::complete::char('(')(ir_text)?.0;
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
+
+    // Sum constants need to specify their variant number.
     let (ir_text, variant) = parse_prim::<u32>(ir_text, "1234567890")?;
     let ir_text = nom::character::complete::multispace0(ir_text)?.0;
     let ir_text = nom::character::complete::char(',')(ir_text)?.0;
@@ -665,85 +924,43 @@ fn parse_array_constant<'a>(
     ir_text: &'a str,
     array_ty: TypeID,
     elem_ty: TypeID,
-    dc_bounds: Box<[DynamicConstantID]>,
     context: &RefCell<Context<'a>>,
 ) -> nom::IResult<&'a str, Constant> {
-    let mut bounds = vec![];
-    let borrow = context.borrow();
-    let mut total_elems = 1;
-    for dc in dc_bounds.iter() {
-        let dc = borrow.reverse_dynamic_constant_map.get(dc).unwrap();
-        match dc {
-            DynamicConstant::Constant(b) => {
-                if *b == 0 {
-                    Err(nom::Err::Error(nom::error::Error {
-                        input: ir_text,
-                        code: nom::error::ErrorKind::IsNot,
-                    }))?
-                }
-                total_elems *= b;
-                bounds.push(*b);
-            }
-            _ => Err(nom::Err::Error(nom::error::Error {
-                input: ir_text,
-                code: nom::error::ErrorKind::IsNot,
-            }))?,
-        }
-    }
-    let mut contents = vec![];
-    let ir_text =
-        parse_array_constant_helper(ir_text, elem_ty, bounds.as_slice(), &mut contents, context)?.0;
+    let ir_text = nom::character::complete::multispace0(ir_text)?.0;
+    let ir_text = nom::character::complete::char('[')(ir_text)?.0;
+    let ir_text = nom::character::complete::multispace0(ir_text)?.0;
+    let (ir_text, entries) = nom::multi::separated_list1(
+        nom::sequence::tuple((
+            nom::character::complete::multispace0,
+            nom::character::complete::char(','),
+            nom::character::complete::multispace0,
+        )),
+        |x| {
+            parse_constant_id(
+                x,
+                context
+                    .borrow()
+                    .reverse_type_map
+                    .get(&elem_ty)
+                    .unwrap()
+                    .clone(),
+                context,
+            )
+        },
+    )(ir_text)?;
+    let ir_text = nom::character::complete::multispace0(ir_text)?.0;
+    let ir_text = nom::character::complete::char(']')(ir_text)?.0;
+
+    // Will check that entries is the correct size during typechecking.
     Ok((
         ir_text,
-        Constant::Array(array_ty, contents.into_boxed_slice()),
+        Constant::Array(array_ty, entries.into_boxed_slice()),
     ))
 }
 
-fn parse_array_constant_helper<'a>(
-    ir_text: &'a str,
-    elem_ty: TypeID,
-    bounds: &[usize],
-    contents: &mut Vec<ConstantID>,
-    context: &RefCell<Context<'a>>,
-) -> nom::IResult<&'a str, ()> {
-    if bounds.len() > 0 {
-        let ir_text = nom::character::complete::multispace0(ir_text)?.0;
-        let ir_text = nom::character::complete::char('[')(ir_text)?.0;
-        let ir_text = nom::character::complete::multispace0(ir_text)?.0;
-        let (ir_text, empties) = nom::multi::separated_list1(
-            nom::sequence::tuple((
-                nom::character::complete::multispace0,
-                nom::character::complete::char(','),
-                nom::character::complete::multispace0,
-            )),
-            |x| parse_array_constant_helper(x, elem_ty, bounds, contents, context),
-        )(ir_text)?;
-        if empties.len() != bounds[0] {
-            Err(nom::Err::Error(nom::error::Error {
-                input: ir_text,
-                code: nom::error::ErrorKind::IsNot,
-            }))?
-        }
-        let ir_text = nom::character::complete::multispace0(ir_text)?.0;
-        let ir_text = nom::character::complete::char(']')(ir_text)?.0;
-        Ok((ir_text, ()))
-    } else {
-        let (ir_text, id) = parse_constant_id(
-            ir_text,
-            context
-                .borrow()
-                .reverse_type_map
-                .get(&elem_ty)
-                .unwrap()
-                .clone(),
-            context,
-        )?;
-        contents.push(id);
-        Ok((ir_text, ()))
-    }
-}
-
 fn parse_identifier<'a>(ir_text: &'a str) -> nom::IResult<&'a str, &'a str> {
+    // Here's the set of characters that can be in an identifier. Must be non-
+    // empty.
     nom::combinator::verify(
         nom::bytes::complete::is_a(
             "1234567890_@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
@@ -752,6 +969,9 @@ fn parse_identifier<'a>(ir_text: &'a str) -> nom::IResult<&'a str, &'a str> {
     )(ir_text)
 }
 
+/*
+ * Helper function for parsing tuples of arguments in the textual format.
+ */
 fn parse_tuple1<'a, A, AF>(mut parse_a: AF) -> impl FnMut(&'a str) -> nom::IResult<&'a str, (A,)>
 where
     AF: nom::Parser<&'a str, A, nom::error::Error<&'a str>>,
@@ -819,6 +1039,9 @@ where
     }
 }
 
+/*
+ * Some tests that demonstrate what the textual format looks like.
+ */
 mod tests {
     #[allow(unused_imports)]
     use super::*;
diff --git a/samples/matmul.hir b/samples/matmul.hir
new file mode 100644
index 0000000000000000000000000000000000000000..511bdfa8118194d31e0c21b12e83c62eba4318ed
--- /dev/null
+++ b/samples/matmul.hir
@@ -0,0 +1,32 @@
+fn matmul<3>(a: array(array(f32, #1), #0), b: array(array(f32, #2), #1)) -> array(array(f32, #2), #0)
+  i = fork(start, #0)
+  i_ctrl = read_prod(i, 0)
+  i_idx = read_prod(i, 1)
+  k = fork(i_ctrl, #2)
+  k_ctrl = read_prod(k, 0)
+  k_idx = read_prod(k, 1)
+  zero_idx = constant(u64, 0)
+  one_idx = constant(u64, 1)
+  zero_val = constant(f32, 0)
+  loop = region(k_ctrl, if_true)
+  j = phi(loop, zero_idx, j_inc)
+  sum = phi(loop, zero_val, sum_inc)
+  j_inc = add(j, one_idx)
+  fval1 = read_array(a, i_idx)
+  fval2 = read_array(b, j)
+  val1 = read_array(fval1, j)
+  val2 = read_array(fval2, k_idx)
+  mul = mul(val1, val2)
+  sum_inc = add(sum, mul)
+  j_size = dynamic_constant(#1)
+  less = lt(j_inc, j_size)
+  if = if(loop, less)
+  if_false = read_prod(if, 0)
+  if_true = read_prod(if, 1)
+  k_join = join(if_false, sum_inc)
+  k_join_ctrl = read_prod(k_join, 0)
+  k_join_data = read_prod(k_join, 1)
+  i_join = join(k_join_ctrl, k_join_data)
+  i_join_ctrl = read_prod(i_join, 0)
+  i_join_data = read_prod(i_join, 1)
+  r = return(i_join_ctrl, i_join_data)
diff --git a/samples/simple1.hir b/samples/simple1.hir
index acfc64163444e39c199b067be18b8532fdcd1a19..415b2bc3f9a710fceccd222d4ac47c348669a0d5 100644
--- a/samples/simple1.hir
+++ b/samples/simple1.hir
@@ -1,10 +1,11 @@
 fn myfunc(x: i32) -> i32
-  y = call(add, x, x)
+  y = call<5>(add, x, x)
   r = return(start, y)
 
-fn add(x: i32, y: i32) -> i32
+fn add<1>(x: i32, y: i32) -> i32
   c = constant(i8, 5)
-  r = return(start, w)
+  dc = dynamic_constant(#0)
+  r = return(start, s)
   w = add(z, c)
-  z = add(x, y)
-
+  s = add(w, dc)
+  z = add(x, y)
\ No newline at end of file