Emitting Calyx from Python
The calyx
builder library can be used to generate Calyx code in Python.
Installation
To install the library, run the following from the repository root (requires flit installation):
cd calyx-py && flit install -s
Using the calyx
builder
The calyx
library provides a builder to generate Calyx code. The library reference documents most builder methods and constructs.
We will also walk through the file builder_example.py
to demonstrate how the builder library is used. This Calyx program initializes two registers with the numbers 1 and 41, adds them together, and stores the result in a register.
The add_main_component(prog)
method will, as the name suggests, add a main component to our program. We can define components for our Calyx program prog
with prog.component
. Here's a defininition of a main
component with a 32-bit input in
and output out
:
def add_main_component(prog):
main = prog.component("main")
main.input("in", 32)
main.output("out", 32)
Technically, we didn't need to assign prog.component("main")
to a variable; the component main
would have been added to prog
regardless. However, it will often prove useful to store handles to components, registers, or other objects you'd like to use later.
We then instantiate our cells: three 32-bit registers and one 32-bit adder.
lhs = main.reg("lhs", 32)
rhs = main.reg("rhs", 32)
sum = main.reg("sum", 32)
add = main.add(32, "add")
As with adding components to a program, we don't need to assign main.reg(...)
to a variable, but it'll be useful to be able to quickly refer to these cells.
Next, we'll define our groups of assignments. The syntax for defining a group looks like with {component}.group("group_name") as group_variable
, as we do below:
with main.group("update_operands") as update_operands:
Now, we'll initialize our registers. You can access cell ports using dot notation. Notably, port names that are also reserved keywords in Python such as in
are followed by an underscore.
# Directly index cell ports using dot notation
lhs.write_en = 1
# Builder attempts to infer the bitwidth of the constant
rhs.write_en = 1
# `in` is a reserved keyword, so we use `in_` instead
lhs.in_ = 1
As mentioned in the comments above, the Calyx builder will try to infer the bitwidth of constants. In case this doesn't work and you run into problems with this, you can provide the constant's size like so:
# Explicilty sized constants when bitwidth inference may not work
rhs.in_ = const(32, 41)
Calyx groups use a latency-insensitive go/done interface. When the done
signal of a component is 1
, it signals that the component has finished executing. Oftentimes, computing this signal is conditional. We use guarded assignements to a group's done signal in order to express this. Writing a group's done condition with the builder is pretty similar to doing so in Calyx, except that the ?
used for guarded assignments is now @
(due to conflicting usage of ?
in Python).
# Guards are specified using the `@` syntax
update_operands.done = (lhs.done & rhs.done) @ 1
In order to use the ports of cells in our main
component within the code for our component, we'll expose the adder's output port by explicitly constructing it using the calyx-py
AST.
# Bare name of the cell
add_out = ast.CompPort(ast.CompVar("add"), "out")
Now, when we want to use the output port of our adder, we can do so easily:
sum.in_ = add_out
In order to add continuous assignments to your program, use the construct with {component}.continuous:
.
with main.continuous:
this.out = sum.out
To access a component's ports while defining it, like we did above, we use the method this()
.
# Use `this()` method to access the ports on the current component
this = main.this()
Lastly, we'll construct the control portion of this Calyx program. It's pretty simple; we're running two groups in sequence. Sequences of groups are just Python lists:
main.control += [
update_operands,
compute_sum,
]
You can also use the builder to generate parallel control blocks. To do this, use the par
keyword. For instance, the above code with some parallel groups in it might look like
main.control += [
update_operands,
compute_sum,
par(A, B, C)
]
After making our modifications to the main
component, we'll build the program using the build()
method. We use the Builder
object to construct prog
, and then return the generated program.
def build():
prog = Builder()
add_main_component(prog)
return prog.program
Finally, we can emit the program we built.
if __name__ == "__main__":
build().emit()
That's about it for getting started with the calyx-py
builder library! You can inspect the generated Calyx code yourself by running:
python calyx-py/test/builder_example.py
Other examples using the builder can also be found in the calyx-py
test directory. All of our frontends were also written using this library, in case you'd like even more examples!