Emitting Calyx from Python
The calyx
builder library provisions an embedded domain-specific language (eDSL) that can be used to generate Calyx code.
The DSL is embedded in Python.
Installation
To install the library, run the following from the repository root.
The command requires flit, which you can install with pip install flit
.
cd calyx-py && flit install -s
Hello, Calyx World!
We will start by using the calyx
library to generate a simple Calyx program.
Glance through the Python code below, which is also available at helloworld.py
.
import calyx.builder as cb
def insert_adder_component(prog):
comp = prog.component("adder")
val1 = comp.input("val1", 32)
val2 = comp.input("val2", 32)
comp.output("out", 32)
sum = comp.reg(32)
add = comp.add(32)
with comp.group("compute_sum") as compute_sum:
add.left = val1
add.right = val2
sum.write_en = cb.HI
sum.in_ = add.out
compute_sum.done = sum.done
with comp.continuous:
comp.this().out = sum.out
comp.control += compute_sum
if __name__ == "__main__":
prog = cb.Builder()
insert_adder_component(prog)
prog.program.emit()
Running this Python code, with
python calyx-py/test/helloworld.py
will generate the following Calyx code. As you may have inferred, we are have simply created a 32-bit adder in a contrived manner.
import "primitives/core.futil";
import "primitives/binary_operators.futil";
component adder(val1: 32, val2: 32) -> (out: 32) {
cells {
reg_1 = std_reg(32);
add_2 = std_add(32);
}
wires {
group compute_sum {
add_2.left = val1;
add_2.right = val2;
reg_1.write_en = 1'd1;
reg_1.in = add_2.out;
compute_sum[done] = reg_1.done;
}
out = reg_1.out;
}
control {
compute_sum;
}
}
Walkthrough
So far, it does not look like using our eDSL has bought us much. We have essentially written Calyx, line by line, in Python. However, it is useful to go through the process of generating a simple program to understand the syntax and semantics of the builder library.
For each item discussed below, we encourage you to refer to both the Python code and the generated Calyx code.
We add the component adder
to our program with the following line:
comp = prog.component("adder")
We then specify the names and bitwidths of any ports that we want the component to have.
val1 = comp.input("val1", 32)
val2 = comp.input("val2", 32)
comp.output("out", 32)
We also add two cells to the component: a 32-bit adder and a 32-bit register.
sum = comp.reg(32)
add = comp.add(32)
The heart of the component is a group of assignments. We begin the group with:
with comp.group("compute_sum") as compute_sum:
We know that we have instantiated a std_add
cell, and that such a cell has input ports left
and right
.
We need to assign values to these ports, and we do so using straightforward dot-notated access.
The values val1
and val2
exactly the inputs of the component.
add.left = val1
add.right = val2
Now we would like to write the output of the adder to a register.
We know that registers have input ports write_en
and in
.
We assert the high signal on write_en
with:
sum.write_en = cb.HI
We specify the value to be written to the register with:
sum.in_ = add.out
Although the port is named in
, we must use in_
to avoid a clash with Python's in
keyword.
Observe that we have used dot-notated access to both read the out
port of the adder and write to the in
port of the register.
Since compute_sum
is a group of assignments, we must specify when it is done. We do this with:
compute_sum.done = sum.done
That is, the group is done when the register we are writing into asserts its done
signal.
In order to add a continuous assignment to our program, we use the construct with {component}.continuous:
.
To access the ports of a component while defining it, we use the this()
method.
with comp.continuous:
comp.this().out = sum.out
That is, we want this component's out
port to be continuously assigned the value of the sum
's out
port.
Finally, we construct the control portion of this Calyx program:
comp.control += compute_sum
We have some boilerplate code that creates an instance of the builder, adds to it the component that we have just studied, and emits Calyx code.
if __name__ == "__main__":
prog = cb.Builder()
insert_adder_component(prog)
prog.program.emit()
Further, the builder library is able to infer which Calyx libraries are needed in order to support the generated Calyx code, and adds the necessary import
directives to the generated code.
Further Reading
The builder library walkthrough contains a detailed description of the constructs available in the builder library.
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!