Builder Library Reference

Top-Level Program Structure

Here's the general structure of a program that uses the builder to generate Calyx code.

# import the builder library
import calyx.builder as cb


# define `second_comp`
def add_second_comp(prog):
    # `second_comp` definition here


# method for defining `my_component` and adding it to a program
def add_my_component(prog, second_comp): 
    # add the component to the program
    my_component = prog.component("my_component")

    # Adding an instance of `second_comp` as a cell of `my_component`
    my_second_comp = my_component.cell("my_second_comp", second_comp)

    # adding a register cell (or other cells) to the component 
    my_reg = my_component.reg("my_reg", 32)

    # define a `my_component` group
    with my_component.group("my_group") as my_group:
      # assignments here 
      my_reg.write_en = 1

    # add the group to `my_component`'s control program
    my_component.control += my_group


# assemble the program
def build():
    prog = cb.Builder()
    my_second_comp = add_second_comp(prog)
    add_my_component(prog, my_second_comp)

    # return the generated program
    return prog.program


# emit the program
if __name__ == "__main__":
    build().emit()

Components

Defining Components

To define a component, call the Builder().component() method.

prog = cb.Builder()
prog.component("my_component")

Retrieving Components

To reference a component without an existing handle to it, use the Builder().get_component() method.

prog = cb.Builder()
prog.component("my_component")
# a few lines later 
my_component = prog.get_component("my_component")

Defining Component Inputs and Outputs

Components can be given input and output ports. Just specify the name of the port and its size.

my_component.input("my_input", 32)
my_component.output("my_output", 32)

To access the input and output ports of a component within the definition of a component, use the syntax my_component.this().port.

def add_my_component(prog):
    my_component = prog.component("my_component")
    my_component.output("my_output", 32)

    with my_component.group("my_group"):
        my_component.this().my_output = const(32, 1)

Note that it's possible to create a handle to input and output ports.

Defining Component Attributes

Components can be given attributes. Similar to ports, just specify the name of the attribute and its value. Note that attribute(name, value) does not return a handle to the attribute.

my_component.attribute("my_attribute", 1)

Will create a component that looks like:

component my_component<"my_attribute"=1>(...) -> (...) {

Multi-Component Designs

Calyx supports multi-component designs. The top-level example demonstrates how to construct multi-component designs using the library.

Defining Common Calyx Cells

Here's a snippet of code that adds a few common kinds of cells to a component:

my_component = prog.component("my_component")

# Registers: reg(name, bitwidth)
my_component.reg("my_reg", 32)

# Constants: const(name, bitwidth, value)
my_component.const("my_reg", 32, 42)

# Adders/Subtractors: [add|sub](name, size, signed=False)
# a signed adder 
my_component.add("my_add", 32, signed=True)
# a subtractor
my_component.sub("my_sub", 32)


# Comparators: [gt|lt|eq|neq|ge|le](name, size, signed=False)
my_component.gt("my_gt", 32)
# a signed le comparison
my_component.lt("my_lt", 32, signed=True)
my_component.eq("my_eq", 32)
my_component.neq("my_neq", 32)
my_component.ge("my_ge", 32)
my_component.le("my_le", 32)

# 1-D memory: 
# mem_d1(name, bitwidth, len, idx_size, is_external=False, is_ref=False)
my_component.mem_d1("my_mem", 32, 4, 32)
# An external memory
my_component.mem_d1("my_mem", 32, 4, 32, is_external=True)
# A memory by reference
my_component.mem_d1("my_mem", 32, 4, 32, is_ref=True)

If you're curious, you can read more about external memories or memories by reference.

Retrieving Cells

In order to reference a cell without a handle, use the Builder().get_cell() method.

# defining a register cell
my_component.reg("my_reg", 32)

# a few lines later 
my_reg = prog.get_cell("my_reg")

Wires

Guarded Assignments

Guarded assignments in the builder are syntactically similar to those in Calyx.

my_component = prog.component("my_component")

my_add = comp.add("my_add", 32)
my_reg = comp.reg("my_reg", 32)

with my_component.group("my_group"):
    # unconditional assignments
    add.left = const(32, 1)
    add.right = const(32, 41)
    my_reg.write_en = 1

    # a guarded assignment using @
    # in Calyx, this line would be:
    # my_reg.in = (add.left < add.right) ? add.out;
    my_reg.in = (add.left < add.right) @ add.out

Groups

Defining Groups

Groups are defined using the group() method for a component. To make a handle for a group, use the as syntax. Group handles are necessary in order to set done ports for groups.

It's possible to define a static delay for a group using the optional static_delay argument.

my_component = prog.component("my_component")

# a group definition + handle for the group
with my_component.group("my_group") as my_group:
    # assignments here

# a group with a static delay
with my_component.group("my_static_group", static_delay=1): 

Defining Combinational Groups

Combinational groups are defined similarly to groups, except with the comb_group method.

my_component = prog.component("my_component")

with my_component.comb_group("my_comb_group"):
    # assignments here

Retrieving Groups

If a group doesn't have a handle, it can be retrieved later with the Builder().get_group() method. It's possible to retrieve combinational groups as well as regular groups with this method.

prog = cb.Builder()
my_component = prog.component("my_component")

with my_component.group("my_group"):
    # group definition here

# a few lines later
my_group = prog.get_group("my_group")

with my_component.comb_group("my_comb_group"):
    # comb group definition here

my_comb_group = prog.get_group("my_comb_group")

Continuous Assignments

Continuous assignments are generated by using the syntax with comp.continuous.

my_component = prog.component("my_component")

my_output = my_component.output("my_output", 32)
my_reg = comp.reg("my_reg", 32)

with my_component.continuous:
    my_component.this().my_output = my_reg.out

Control Operators and Programs

A component's control program is defined by augmenting the list my_component.control. Control programs are constructed with control operators.

Group Enables

To enable a group, include it in a component's control program.

my_component.control += my_group

# using `get_group`
my_component.control += my_component.get_group("my_group")

seq

Control statements are sequenced in the order that they appear in a component's control program, represented by a Python list. Let's say we want to sequence the control statements A, B, and C.

my_component.control += [A, B, C]

par

For parallel compositions of control programs, use the par() function. Here's how to compose control programs A and B in parallel, and then sequence their composition with the control program C.

my_component.control += [par(A, B), C]

if

See the language reference for if.

# `if_(port, cond, body, else_body=None)`
my_if = if_(my_port, my_comb_group, my_true_group)

# with a nested if
my_other_if = if_(my_port, my_if)

# with a comb group to compute the value of my_port
my_if_comb = if_(my_port, my_comb_group, my_true_group)

# with an else body
my_if_else = if_(my_port, my_comb_group, my_true_group, my_false_group)

my_component.control += [my_if, my_other_if, my_if_comb, my_if_else]

while

See the language reference for while.

# while_(port, cond, body)
my_while = while_(my_port, my_body)

# with a comb group to compute the value of my_port
my_while = while_(my_port, my_comb_group, my_body)

invoke

See the language reference for invoke.

# invoke(cell, **kwargs)
my_invoke = invoke(my_cell, in_arg1=my_cell_arg1_reg.out, in_arg2=1)

Miscellaneous Tips + Tricks

Creating Handles

Handles allow components, groups, cells, control operators, and input/output ports to be referenced after their definition.

def add_my_component(prog):
    # Creating a handle to a component
    my_component = prog.component("my_component")

    # using the component handle
    my_component.reg("my_reg", 32)

    # Creating a handle to an input/output port
    my_input = component.input("my_input", 32)
    my_output = component.output("my_output", 32)

    # Creating a handle to a cell
    my_second_comp = my_component.cell("my_cell", my_second_comp)

    # Creating a handle to a group
    with my_component.group("my_group") as my_group:
        # assignments

    # Creating a handle to a control operator
    my_if = if_(my_second_comp.out_port, body=my_group)

    # using the group handle + control operator handle
    my_component.control += [my_group, my_if]

Importing Calyx Libraries

To generate imports for Calyx libraries, use the Builder.import_() method.

prog = cb.Builder()
prog.import_("primitives/binary_operators.futil")

Explictly Stating Widths with const

Usually, the builder library can automatically infer the width of a port. In cases where it can't, use the const(width, value) expression:

my_cell.my_port = const(32, 1)

High and Low Signals

The calyx.builder.HI or calyx.builder.LO are shorthand for one-bit high and low signals.

"""A one-bit low signal"""
LO = const(1, 0)
"""A one-bit high signal"""
HI = const(1, 1)