4.2 Always DSL
March 30, 2026 ยท View on GitHub
The Always DSL
in Hardcaml lets one describe circuits
in a similar style to a Verilog always block. Always blocks allow
hardware multiplexer structures to be described in a somewhat
imperative manner using variable assignments, if-else conditions,
switches and a simple procedural macro construct.
This programming model often makes complicated logic (such as state machines) easier to reason about.
The Always DSL
open Base
open Hardcaml
There are two key components when using the Always DSL:
1. Variable Declarations
There are two kinds of variable declarations, namely wires and registers.
A reg is one whose value is sequentially updated on the edge of the
clock signal provided within a Reg_spec.t type.
# (* Creates a register variable. *)
Always.Variable.reg
- : here:[%call_pos] ->
(width:int -> Always.Variable.t) Signal.with_register_spec
= <fun>
A wire is one whose value is updated combinationally, meaning that
the new value is visible at the same clock cycle as when it is
assigned. The new value will not persist to the next cycle. If no
such assignment exists, the variable will possess the default value.
# (* Creates a wire register, that is, the value of the wire. *)
Always.Variable.wire
- : here:[%call_pos] -> default:Signal.t -> unit -> Always.Variable.t = <fun>
Both kinds of variable will return the same type, namely an Always.Variable.t
# let foo = Always.Variable.wire ~default:Signal.gnd () ;;
val foo : Always.Variable.t =
{Hardcaml.Always.Variable.value = (wire (width 1)); internal = <abstr>}
Variables may be assigned within an Always block. To read the value
of a variable, access the value field, which provides a Signal.t
that can be used to form expressions.
# foo.value;;
- : Signal.t = (wire (width 1))
2. Writing an Always Program
Having declared the variables, we can now write the actual 'procedural' blocks.
An Always program comprises assignments to variables optionally
guarded by if_, when or switch conditional statements. For example:
let something =
let open Signal in
let a = input "a" 1 in
let b = input "b" 1 in
let c = Always.Variable.wire ~default:gnd () in
let d = Always.Variable.wire ~default:gnd () in
let e = Always.Variable.wire ~default:gnd () in
Always.(compile [
(* Assignments. *)
c <-- (a ^: b );
(* [if_] statements. *)
if_ (a ==: b) [
d <-- vdd;
] [
d <-- gnd;
];
(* [when_] is like [if_], with an empty [else]. *)
when_ c.value [
e <--. 1;
];
]);
;;
Notice the call to the compile function surrounding the procedural
block.
# Always.compile
- : Always.t list -> unit = <fun>
Two things going on here:
- Notice that
compilereturns a unit. This is because variable declarations create unassigned wires, and the compile function assigns them to the appropriate multiplexers. - Notice that the Always DSL is just a list of
Always.t. This means we can play with various meta-programming and abstraction tricks within these program blocks.
Example
The following is an example of these pieces put together.
let clock = Signal.input "clock" 1
let clear = Signal.input "clear" 1
let a = Signal.input "a" 8
let b = Signal.input "b" 8
let r_sync = Signal.Reg_spec.create ~clock ~clear ()
let create =
let open Signal in
(* [wire] and [register] variable declarations. *)
let c_wire = Always.Variable.wire ~default:(Signal.zero 8) () in
let c_reg = Always.Variable.reg ~enable:Signal.vdd r_sync ~width:8 in
(* The program block with a call to [compile]. *)
Always.(compile [
if_ (a ==: b) [
c_wire <-- (sll a ~by:1);
c_reg <-- (sll a ~by:1)
] [
c_wire <-- (a +: b);
c_reg <-- (a +: b);
]
]);
(* The [c_wire.value] are assigned appropriately by the Always
compiler. *)
output "c_wire" c_wire.value, output "c_reg" c_reg.value
;;
Differences to Verilog Always blocks
The semantics are similar to Verilog, with a few caveats:
- Assignments are non-blocking. Hardcaml does not support blocking assignments.
- In Verilog, an Always block can describe either registers or combinational logic, but not both. With the Always DSL, both can be defined in a single block.
- Hardcaml Always blocks do not require a sensitivity list - the clocks driving the registers are bound to the register variable themselves.
How values update
The reader might be surprised by the following example.
let counter_thing enable =
let open Signal in
let a = Always.Variable.reg ~enable:vdd r_sync ~width:8 in
Always.(compile [
(* Here we might expect [a] to take the values [10] or [11], depending on
[enable]. But that is not the case. While [enable] is high, [a] will
increment by one every clock cycle. When [enable] is low [a] is set to
10. *)
a <--. 10;
when_ (enable) [
a <-- a.value +:. 1;
]
]);
;;
The rule is: the last assignment executed will set the next value.