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 primitive
s, 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, such ascond.out
, or a literal, such as3'd2
.port op port
: A comparison between values on two ports. Valid instances ofop
are:>
,<
,>=
,<=
,==
!guard
: Logical negation of a guard valueguard | guard
: Disjunction between two guardsguard & guard
: Conjunction of two guards
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
andoutputs
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 theinvoke
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
orfalse_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 thego
signal before thedone
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.
In the first invocation, we pass in the concrete cell m1
while in the second we pass
in m2
.
See the tutorial for longer example on how to use this feature.
Subtyping for ref
cells
When providing bindings for ref
cells, one must provide a concrete cell that is a
subtype of the ref
cell. A cell a
is a subtype of cell b
if the component
defining a
has at least the same ports as the component defining b.
Consider the following component definitions:
component b(in_1 : 1) -> (out_1 : 1) {
// cells, wires, and control blocks
}
component a(in_1 : 1, in_2 : 1) -> (out_1 : 1, out_2 : 1){
// cells, wires, and control blocks
}
Because the component definition of a
has the ports in_1
and out_1
, a concrete cell of a
can be bound to a ref
cell of component b
:
//Expects a `ref` cell of component `b`
component c() -> () {
cells{
ref b1 = b();
}
wires{...}
control{...}
}
component main() -> () {
cells {
c_cell = c();
b_cell = b();
a_cell = a(); //recall `a` is a subtype of `b`
}
wires { ... }
control {
seq {
// Pass `b_cell` by reference. Both are `b1` and `b_cell` are defined by the component `b`
invoke c[b1=b_cell]()();
// Pass `a_cell` by reference. The `ref` cell and concrete cell are defined by different components,
// but this is allowed because `a` is a subtype of `b`.
invoke c[b1=a_cell]()();
}
}
}
Ports are considered to be equal with respect to subtyping if they have the same name, width, direction, and attributes.
Note: The notion of subtyping described above, that only checks for port equivalence between components, is incomplete. A complete, correct definition of subtyping would require that for
a
to be a subtype ofb
, for everyref
cell expected ina
, componentb
must expect aref
cell that is a subtype of the associatedref
cell ina
(note that the relationship between these nestedref
cells is opposite the relationship ofa
andb
).Because nested
ref
cells are not currently allowed in Calyx, this is not a problem in practice.