Welcome to Ceramic Hacker

Hi, I'm Alexander (Sasha) Skvortsov. I'm a computer science and math major at Penn State. I'll be using this blog to share some of my projects and thoughts, mainly focused around software engineering and pottery.
Hardcaml MIPSBlog

[8/x] Design Patterns, Conventions, and Testing

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 circuit_impl.

  • 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.

Width Safety

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.

Luckily, the I and 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 I and O:

width_check.ml

open Hardcaml

module With_interface (I : Interface.S) (O : Interface.S) =
struct
  let check_widths circuit_impl i =
    I.Of_signal.assert_widths i;
    let out = circuit_impl i in
    O.Of_signal.assert_widths out;
    out
end

Now, we can use this to wrap our circuit_impl functions:

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

The _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 usecircuit_impl_exn instead.

File Organization

Our source code for each circuit will follow a simple convention:

  1. First, we define the I and O modules that represent the structure of the input/output of our circuit.
  2. 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.
  3. Finally, we put any util functions wrapping circuit_impl, such as circuit_impl_exn, and 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 lib.

Testing

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.

Previous articleNext article

Comments (2)

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 circuit_impl.

  • 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.

Width Safety

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.

Luckily, the I and 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 I and O:

width_check.ml

open Hardcaml

module With_interface (I : Interface.S) (O : Interface.S) =
struct
  let check_widths circuit_impl i =
    I.Of_signal.assert_widths i;
    let out = circuit_impl i in
    O.Of_signal.assert_widths out;
    out
end

Now, we can use this to wrap our circuit_impl functions:

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

The _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 usecircuit_impl_exn instead.

File Organization

Our source code for each circuit will follow a simple convention:

  1. First, we define the I and O modules that represent the structure of the input/output of our circuit.
  2. 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.
  3. Finally, we put any util functions wrapping circuit_impl, such as circuit_impl_exn, and 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 lib.

Testing

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.

10 days later

We do actually implement the port widths check across hierarchy, but until recently had it disabled (don't ask why …).

See https://github.com/janestreet/hardcaml/blob/master/src/circuit.ml#L73

We turned it back on properly (and forever) a couple of months ago.

I am not sure exactly what version of Hardcaml you are working with, but you might be able to enable these checks yourself as well (or just use your functor).

But fundamentally your point stands - Hardcaml had this hole for a while and it's embarrassing.

    AndyRay We turned it back on properly (and forever) a couple of months ago.

    Hmm, now that I try it again, output port widths are checked properly. I'm still getting issues with inputs though.

    For example, see the as/test-builtin-hierarchy-width-checks branch of our project repo. We're providing a 5 bit input to a circuit that expects one bit, but tests pass. I've tried explicitly specifying a bit width for that clock input, and that didn't help. However, if you change the output of instruction_fetch (for example, "32'h0000000F" => "10'h0000000F"), it errors as expected. The GitHub action is set up to use the latest bleeding release (hardcaml.v0.15~preview.124.35+330).

    Write a Reply...