Metaprogramming Overview

When building hardware, it is often useful to design it parameterically so that we can use the same code to generate different hardware designs. For example, it is extremely common to design modules for numerical operations, such as adders, to be parameter over the bitwidth of the operands.

module Add #(
    parameter WIDTH = 32
) (
    input [WIDTH-1:0] a,
    input [WIDTH-1:0] b,
    output [WIDTH-1:0] c
);
    assign c = a + b;
endmodule

The Verilog module above specifies a parameter WIDTH that can be used to specify the bitwidth of the operands. User code can simply instantiate the module with the desired bitwidth:

Add #(.WIDTH(32)) adder_32(...);
Add #(.WIDTH(64)) adder_64(...);

Of course, this example hides the fact that metaprogramming ends up generating hardware. This is because the + operation needs to be implemented differently for different bitwidths; a 32-bit adder needs a lot more circuitry than a 1-bit adder. HDLs like Verilog provide abstractions like generate blocks to allow users to generate hardware at compile time. Languages like Chisel go one step further to generate hardware by writing Scala programs.

The challenge with generative programming is ensuring that the generated code is correct. This is harder than it seems because we don't have to ensure that one piece of code is correct; we have to ensure that all possible code generate-able from a module definition is correct. Scala's strong types are useful in ensuring that code generated by Chisel is free of some bugs, such as missing ports connections, but it misses crucial properties like correct pipelining and timing.

Filament's promise

Our goal with Filament is to provide an expressive metaprogramming model parameteric modules that typecheck are guaranteed to generate correctly pipelined hardware. Read on to see how we do this.