Hi, welcome back to our Hardcaml MIPS project! Now that we've finished our instruction fetch stage and become a bit more comfortable with Hardcaml, we'll clean up our code a bit and adopt some conventions to keep future development organized.
The Small Stuff
There's a few things we changed for consistency / cleanup across our codebase:
One Hardcaml convention is to name each circuit's implementation function
create. However, there's a lot of things called
create in Hardcaml, so we're renaming all our implementation functions to
We fixed a bug where instruction memory repeats the last instruction in the program forever once finished executing. Instead, it'll just output
noops to avoid unexpected behavior.
We cleaned up our explicit type hinting a bit, so that only
circuit_impl functions have typehinted arguments. This provides some visual documentation without unnecessary clutter.
One of my biggest frustrations with Verilog is that you can connect a 5-bit wire to an 8-bit input, and Verilog won't complain. Hardcaml already does a lot to prevent issues like this: for instance, all options in a mux must be the same width. That being said, one place that's missing a check is module composition: Hardcaml won't error if we pass an X-bit input to a subcircuit that expects Y bits.
O modules we use in each circuit to define input/output structure have an
assert_widths function (automatically available through through a ppx rewriter), which we can run on the provided input and generated output. Problem is, we don't want to put duplicate logic for this in every single circuit.
To get around this, we can use a functor to abstract away these checks for any given
module With_interface (I : Interface.S) (O : Interface.S) =
let check_widths circuit_impl i =
let out = circuit_impl i in
Now, we can use this to wrap our
let circuit_impl_exn program scope input =
let module W = Width_check.With_interface (I) (O) in
W.check_widths (circuit_impl program scope) input
_exn naming is a convention that indicates that the function may throw exceptions. This we do have to put in every circuit, but by using a functor, we've managed to centralize the actual logic in one place.
Accordingly, we changed all any code using
circuit_impl to use
Our source code for each circuit will follow a simple convention:
- First, we define the
O modules that represent the structure of the input/output of our circuit.
- Then, we have the actual implementation code of our circuit. This includes the
circuit_impl function, as well as any logic we've factored out into functions.
- Finally, we put any util functions wrapping
circuit_impl, such as
hierarchical for subcircuits.
For an example, see instruction_fetch.ml.
In our codebase, we'll have a directory for each stage. Files/modules used across stages go directly in
We haven't talked about Hardcaml's testing tools much in these posts, largely because Andrew Ray, the creator of Hardcaml, already has an excellent blog post about it.
That being said, just because we haven't been writing about it doesn't mean we haven't been doing it! Our project's
test directory has automated tests for every circuit we write. This makes it a lot easier to be confident that our design is right. We're also using GitHub Actions to run all our tests every time we push code to GitHub. Hardcaml's testing/simulation tools are one of it's strongest advantages over Verilog, so if you're making a Hardcaml project, it's definitely worth doing.