5.2 Interfaces with ppx_hardcaml
August 8, 2025 ยท View on GitHub
Interfaces with ppx_hardcaml
module Simple_interface = struct
type 'a t =
{ foo : 'a [@bits 32]
; bar : 'a (* Where a bit width is not specified, it defaults to 1. *)
}
[@@deriving hardcaml]
end
# print_s [%message (Simple_interface.port_names_and_widths : (string * int) Simple_interface.t)]
(Simple_interface.port_names_and_widths ((foo (foo 32)) (bar (bar 1))))
- : unit = ()
By default the ppx uses the field name for the string name, and [@bits N] attribute to
specify the field width - if not provided it defaults to 1.
The ppx supports nesting, arrays, lists, and various tools to manage port naming.
Nesting
Interfaces can contain other interfaces:
module Nested_interfaces = struct
type 'a t =
{ clock : 'a
; clear : 'a
; hello : 'a Simple_interface.t
; world : 'a Simple_interface.t
}
[@@deriving hardcaml]
end
Nesting can go as deep as required.
Arrays and lists
For arrays and lists, the lengths must be specified with an attribute.
module Array_and_list_interfaces = struct
type 'a t =
{ my_array : 'a array [@length 2]
; my_list : 'a list [@length 10] [@bits 10]
}
[@@deriving hardcaml]
end
Arrays of nested interfaces are also supported.
Options
Fields in an interface may be optional. Simple and nested fields are supported.
module Interface_with_option = struct
type 'a t =
{ maybe_x : 'a option[@exists true][@bits 3]
; maybe_y : 'a Simple_interface.t option [@exists false]
}
[@@deriving hardcaml]
end
The attribute exists controls if they are included in the interface or not and must be
specified. Such fields can arise when describing a circuit generator where some
configuration option may require extra inputs or outputs.
Naming
Naming simple fields
By default, the field name is used to derive a port name. Port names will be used during RTL generation. This can be modified with attributes.
rtlnameoverrides the fields name with the given string.rtlprefix/rtlsuffixapply the given prefix and/or suffix to the name
module Unmodified_port_names = struct
type 'a t =
{ a : 'a
; b : 'a
; c : 'a
}
[@@deriving hardcaml]
end
module Modified_port_names = struct
type 'a t =
{ a : 'a[@rtlname "aaa"]
; b : 'a[@rtlprefix "x_"]
; c : 'a[@rtlsuffix "_x"]
}
[@@deriving hardcaml]
end
# Unmodified_port_names.port_names;;
- : string Unmodified_port_names.t =
{Unmodified_port_names.a = "a"; b = "b"; c = "c"}
# Modified_port_names.port_names;;
- : string Modified_port_names.t =
{Modified_port_names.a = "aaa"; b = "x_b"; c = "c_x"}
rtlprefix, rtlsuffix and rtlmangle attributes may also be used with nested
interfaces, though rtlname will not work.
Naming nested interfaces
rtlprefix and rtlsuffix can be applied to nested interfaces and will apply the given
prefix or suffix to each field within the nested interface.
By default, we automatically mangle nested interface names by prefixing them with the
field name and a $ separator, to avoid name clashes.
# Nested_interfaces.port_names;;
- : string Nested_interfaces.t =
{Nested_interfaces.clock = "clock"; clear = "clear";
hello = {Simple_interface.foo = "hello$foo"; bar = "hello$bar"};
world = {Simple_interface.foo = "world$foo"; bar = "world$bar"}}
This functionality can be disabled with ~rtlmangle:false, or customized by specifying a
different separator.
module Nested_interfaces_mangled = struct
type 'a t =
{ clock : 'a
; clear : 'a
; hello : 'a Simple_interface.t
; world : 'a Simple_interface.t
}
[@@deriving hardcaml ~rtlmangle:"_"]
end
# Nested_interfaces_mangled.port_names;;
- : string Nested_interfaces_mangled.t =
{Nested_interfaces_mangled.clock = "clock"; clear = "clear";
hello = {Simple_interface.foo = "hello_foo"; bar = "hello_bar"};
world = {Simple_interface.foo = "world_foo"; bar = "world_bar"}}
Global attributes
The naming attributes can be specified for all fields as follows:
module Global_suffix = struct
type 'a t =
{ a : 'a
; b : 'a
; c : 'a
}
[@@deriving hardcaml ~rtlsuffix:"_i"]
end
rtlprefix, rtlsuffix and rtlmangle are supported.