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!