Ports by derive
A port list is a struct, and the derive reads the shape rather than a list of strings.
A design's interface is a struct with one field per port. #[derive(PortList)] reads that struct and produces the names, the widths and the clock designation, so the front end can build the ports and the emitter can name them without either side maintaining a parallel list.
The alternative is a macro taking ("clk", 1), ("rst", 1), ("count", 16) — and then a width that appears in one place and not the other, which is a bug that compiles.
Step by step
- 01
Declare the interface as a struct
Fields are one bit wide unless
#[bits(n)]says otherwise, and#[clock]marks the one port that is a clock. Field names become port names.use ferrite_lithic::Signal; use ferrite_lithic_derive::PortList; #[derive(Clone, Debug, PortList)] pub struct Inputs { #[clock] pub clk: Signal, pub rst: Signal, #[bits(16)] pub count: Signal, } - 02
Hand the struct to the front end
inputs::<Inputs>(design)declares every port as a named wire in one call and returns a struct of the signals to drive. The alternative is a loop over a name list, which cannot produce a struct.let inputs = ferrite_lithic::inputs::<Inputs>(&design)?; inputs.count; // a Signal, already wired to the port
- 03
RTL names are separate on purpose
PortList::rtl_names()applies the#[rtlname("...")]attributes. Keeping the display name separate from the field name means a Rust field can be renamed without silently renaming a port in generated Verilog that a testbench refers to.
What bites
A port list cannot be generic
The derive rejects generic structs outright. A port list is a fixed interface, and a generic one would mean the width is not known at the point the ports are built.
Default width is one bit
A field with no
#[bits]is a single bit. This is the right default — most control signals are one bit — but it means a forgotten attribute produces a one-bit port rather than a compile error.