Calyx Language Reference

Top-Level Constructs

Calyx programs are a sequence of import statements followed by a sequence of extern statements or component definitions.

import statements

import "<path>" has almost exactly the same semantics to that of #include in the C preprocessor: it copies the code from the file at path into the current file.

extern definitions

extern definitions allow Calyx programs to link against arbitrary RTL code. An extern definition looks like this:

extern "<path>" {
  <primitives>...
}

<path> should be a valid file system path that points to a Verilog module that defines the same names as the primitives defined in the extern block. When run with the -b verilog flag, the Calyx compiler will copy the contents of every such Verilog file into the generated output.

primitive definitions

The primitive construct allows specification of the signature of an external Verilog module that the Calyx program uses. It has the following syntax:

[comb] primitive name<attributes>[PARAMETERS](ports) -> (ports);

The syntax for primitives resembles that for components, with some additional pieces:

  • The comb keyword signals that the primitive definition wraps purely combinational RTL code. This is useful for certain optimizations.
  • Attributes specify useful metadata for optimization.
  • PARAMETERS are named compile-time (metaprogramming) parameters to pass to the Verilog module definition. The primitive definition lists the names of integer-valued parameters; the corresponding Verilog module definition should have identical parameter declarations. Calyx code provides values for these parameters when instantiating a primitive as a cell.
  • The ports section contain sized port definitions that can either be positive number or one of the parameter names.

For example, the following is the signature of the std_reg primitive from the Calyx standard library:

primitive std_reg<"state_share"=1>[WIDTH](
  @write_together(1) @data in: WIDTH,
  @write_together(1) @interval(1) @go write_en: 1,
  @clk clk: 1,
  @reset reset: 1
) -> (
  @stable out: WIDTH,
  @done done: 1
)

The primitive defines one parameter called WIDTH, which describes the sizes for the in and the out ports.

Inlined Primitives

Inlined primitives do not have a corresponding Verilog file, and are defined within Calyx. The Calyx backend then converts these definitions into Verilog.

For example, the std_unsyn_mult primitive is inlined:

comb primitive std_unsyn_mult<"share"=1>[WIDTH](left: WIDTH, right: WIDTH) -> (out: WIDTH) {
  assign out = left * right;
};

This can be useful when a frontend needs to generate both Calyx and Verilog code at the same time. The backend ensures that the generated Verilog module has the correct signature.

Calyx Components

Components are the primary encapsulation unit of a Calyx program. They look like this:

component name<attributes>(ports) -> (ports) {
  cells { ... }
  wires { ... }
  control { ... }
}

Like primitive definitions, component signatures consist of a name, an optional list of attributes, and input/output ports. Unlike primitives, component definitions do not have parameters; ports must have a concrete (integer) width. A component encapsulates the control and the hardware structure that implements a hardware module.

Well-formedness: The control program of a component must take at least one cycle to finish executing.

Combinational Components

Using the comb keyword before a component definition marks it as a purely combinational component:

comb component add(left: 32, right: 32) -> (out: 32) {
  cells {
    a = std_add(32);
  }
  wires {
    a.left = left;
    a.right = right;
    out = a.out;
  }
}

A combinational component does not have a control section, can only use other comb components or primitives, and performs its computation combinationally.

Ports

A port definition looks like this:

[@<attr>...] <name>: <width>

Ports have a bit width but are otherwise untyped. They can also include optional attributes. For example, this component definition:

component counter(left: 32, right: 32) -> (@stable out0: 32, out1: 32) { .. }

defines two input ports, left and right, and two output ports, out0 and out1. All four ports are 32-bit signals. Additionally, the out0 port has the attribute @stable.

cells

A component's cells section instantiates a set of sub-components. It contains a list of declarations with this syntax:

[ref]? <name> = <comp>(<param...>);

Here, <comp> is the name of an existing primitive or component definition, and <name> is the fresh, local name of the instance. The optional ref parameter turns the cell into a by-reference cell. Parameters are only allowed when instantiating primitives, not Calyx-defined components.

For example, the following definition of the counter component instantiates a std_add and std_reg primitive as well as a foo Calyx component

component foo() -> () { ... }
component counter() -> () {
  cells {
    r = std_reg(32);
    a = std_add(32);
    f = foo();
  }
  wires { ... }
  control { ... }
}

When instantiating a primitive definition, the parameters are passed within the parenthesis. For example, we pass 32 for the WIDTH parameter of the std_reg in the above instantiation. Since an instantiation of a Calyx component does not take any parameters, the parameters are always empty.

The wires Section

A component's wires section contains guarded assignments that connect ports together. The assignments can either appear at the top level, making them continuous assignments, or be organized into named group and comb group definitions.

Guarded Assignments

Assignments connect ports between two cells together, with this syntax:

<cell>.<port> = [<guard> ?] <cell>.<port>;

The left-hand and right-hand side are both port references, which name a specific input or output port within a cell declared within the same component. The optional guard condition is a logical expression that determines whether the connection is active.

For example, this assignment:

r.in = add.out;

unconditionally transfers the value from a port named out in the add cell to r's in port.

Assignments are simultaneous and non-blocking. When a block of assignments runs, they all first read their right-hand sides and then write into their left-hand sides; they are not processed in order. The result is that the order of assignments does not matter. For example, this block of assignments:

r.in = add.out;
add.left = y.out;
add.right = z.out;

is a valid way to take the values from registers y and z and put the sum into r. Any permutation of these assignments is equivalent.

Guards

An assignment's optional guard expression is a logical expression that produces a 1-bit value, as in these examples:

r.in = cond.out ? add.out;
r.in = !cond.out ? 32'd0;

Using guards, Calyx programs can assign multiple different values to the same port. Omitting a guard expression is equivalent to using 1'd1 (a constant "true") as the guard.

Guards can use the following constructs:

  • port: A port access on a defined cell
  • port op port: A comparison between values on two ports. Valid op are: >, <, >=, <=, ==
  • !guard: Logical negation of a guard value
  • guard || guard: Disjunction between two guards
  • guard && guard: Conjunction of two guards

In the context of guards, a port can also be a literal (i.e., counter.out == 3'd2 is a valid guard).

Well-formedness: For each input port on the LHS, only one guard should be active in any given cycle during the execution of a Calyx program.

Continuous Assignments

When an assignment appears directly inside a component's wires section, it is called a continuous assignment and is permanently active, even when the control program of the component is inactive.

group definitions

A group is a way to name a set of assignments that together represent some meaningful action:

group name<attributes> {
  assignments...
  name[done] = done_cond;
}

Assignments within a group can be reasoned about in isolation from assignments in other groups. Unlike continuous assignments, a group's encapsulated assignments only execute as dictated by the control program. This means that seemingly conflicting writes to the same ports are allowed:

group foo {
  r.in = 32'd10;
  foo[done] = ...;
}
group bar {
  r.in = 32'd22;
  bar[done] = ...;
}

However, group assignments must not conflict with continuous assignments defined in the component:

group foo {
  r.in = 32'd10; ... // Malformed because it conflicts with the write below.
  foo[done] = ...
}
r.in = 32'd50;

Groups can take any (nonzero) number of cycles to complete. To indicate to the outside world when their execution has completed, every group has a special done signal, which is a special port written as <group>[done]. The group should assign 1 to this port to indicate that its execution is complete.

Groups can have an optional list of attributes.

Well-formedness: All groups are required to run for at least one cycle. (Sub-cycle logic should use comb group instead.)

comb group definitions

Combinational groups are a restricted version of groups which perform their computation purely combinationally and therefore run for "less than one cycle":

comb group name<attributes> {
  assignments...
}

Because their computation is required to run for less than a cycle, comb group definitions do not specify a done condition.

Combinational groups cannot be used within normal control operators. Instead, they only occur after the with keyword in a control program.

The Control Operators

The control section of a component contains an imperative program that describes the component's behavior. The statements in the control program consist of the following operators:

Group Enable

Simply naming a group in a control statement, called a group enable, executes the group to completion. This is a leaf node in the control program.

invoke

invoke acts like the function call operator for Calyx and has the following syntax:

invoke instance[ref cells](inputs)(outputs) [with comb_group];
  • instance is the name of the cell instance that needs to be invoked.
  • inputs and outputs define connections for a subset of input and output ports of the instance.
  • The with comb_group section is optional and names a combinational group that is active during the execution of the invoke statement.
  • ref cells is a list of cell names to pass to the invoked component's required cell reference. (It can be omitted if the invoked component contains no cell references.)

Invoking an instance runs its control program to completion before returning. Any Calyx component or primitive that implements the go-done interface can be invoked. Like the group enable statement, invoke is a leaf node in the control program.

seq

The syntax for sequential composition is:

seq { c1; ...; cn; }

where each ci is a nested control statement. Sequences run the control programs c1...cn in sequence, guaranteeing that each program runs fully before the next one starts executing. seq does not provide any cycle-level guarantees on when a succeeding group starts executing after the previous one finishes.

par

The syntax for parallel composition is:

par { c1; ...; cn; }

The statement runs the nested control programs c1...cn in parallel, guaranteeing that each program only runs once. par does not provide any guarantees on how the execution of child programs is scheduled. It is therefore not safe to assume that all children begin execution at the same time.

Well-formedness: The assignments in the children c1...cn should never conflict with each other.

if

The syntax is:

if <port> [with comb_group] {
  true_c
} else {
  false_c
}

The conditional execution runs either true_c or false_c using the value of <port>. The optional with comb_group syntax allows running a combinational group that computes the value of the port.

Well-formedness: The combinational group is considered to be running during the entire execution of the control program and therefore should not have conflicting assignments with either true_c or false_c.

while

The syntax is:

while <port> [with comb_group] {
  body_c
}

Repeatedly executes body_c while the value on port is non-zero. The optional with comb_group enables a combinational group that computes the value of port.

Well-formedness: The combinational group is considered active during the execution of the while loop and therefore should not have conflicting assignments with body_c.

repeat

The syntax is:

repeat <num_repeats> {
  body_c
}

Repeatedly executes the control program body_c num_repeat times in a row.

The go-done Interface

By default, Calyx components implement a one-sided ready-valid interface called the go-done interface. During compilation, the Calyx compiler will add an input port marked with the attribute @go and an output port marked with the attribute @done to the interface of the component:

component counter(left: 32, right: 32, @go go: 1) -> (out: 32, @done done: 1)

The interface provides a way to trigger the control program of the counter using assignments. When the go signal of the component is set to 1, the control program starts executing. When the component sets the done signal to 1, its control program has finished executing.

Well-formedness: The go signal to the component must remain set to 1 while the done signal is not 1. Lowering the go signal before the done signal is set to 1 will lead to undefined behavior.

The clk and reset Ports

The compiler also adds special input ports marked with @clk and @reset to the interface. By default, Calyx components are not allowed to look at or use these signals. They are automatically threaded to any primitive that defines @clk or @reset ports.

Advanced Concepts

ref cells

Calyx components can specify that a cell needs to be passed "by reference":

// Component that performs mem[0] += 1;
component update_memory() -> () {
  cells {
    ref mem = comb_mem_d1(...)
  }
  wires { ... }
  control { ... }
}

When invoking such a component, the calling component must provide a binding for each defined cell:

component main() -> () {
  cells {
    upd = update_memory();
    m1 = comb_mem_d1(...);
    m2 = comb_mem_d2(...);
  }
  wires { ... }
  control {
    seq {
      invoke upd[mem=m1]()(); // Pass `m1` by reference
      invoke upd[mem=m2]()(); // Pass `m2` by reference
    }
  }
}

As the example shows, each invocation can take different bindings for each ref cell. See the tutorial for longer example on how to use this feature.