hdl-cat 0.9.1

Umbrella crate re-exporting the hdl-cat workspace
Documentation

hdl-cat

A Rust hardware description library re-architected around comp-cat-rs — every abstraction (composition, state, effects, simulation, codegen) is a morphism, a Kleisli arrow, or a catamorphism over a comp-cat-rs effect type.

Conceptually parallel to RHDL: bit-precise integer types, a typed hardware description IR, a cycle-accurate simulator, and a Verilog backend. Implementation is independent and categorical.

Table of contents

  1. Quick start
  2. Architecture
  3. Core design
  4. Building circuits
  5. Stateful machines
  6. Simulation
  7. Verilog emission
  8. VCD traces
  9. The #[kernel] macro
  10. Per-crate tour
  11. Conventions
  12. License

Quick start

Build, simulate, and emit Verilog for a 4-bit counter:

use hdl_cat::prelude::*;

fn main() -> Result<(), hdl_cat_error::Error> {
    // 1. Build an 8-bit counter (state + 1 each cycle).
    let counter = std_lib::counter::<8>()?;

    // 2. Simulate 4 cycles with empty input (counter takes no data input).
    let inputs = vec![
        hdl_cat_kind::BitSeq::new(),
        hdl_cat_kind::BitSeq::new(),
        hdl_cat_kind::BitSeq::new(),
        hdl_cat_kind::BitSeq::new(),
    ];
    let samples = Testbench::new(counter).run(inputs).run()?;

    // 3. Read back: the first 4 counts.
    let counts: Vec<u128> = samples
        .iter()
        .map(|s| Bits::<8>::from_bits_seq(s.value()).map(Bits::to_u128))
        .collect::<Result<Vec<_>, _>>()?;
    assert_eq!(counts, vec![0, 1, 2, 3]);

    // 4. Emit synthesizable SystemVerilog.
    let counter_for_verilog = std_lib::counter::<8>()?;
    let module = verilog::emit_sync_graph(
        counter_for_verilog.graph(),
        "counter8",
        counter_for_verilog.state_wire_count(),
        counter_for_verilog.input_wires(),
        counter_for_verilog.output_wires(),
        counter_for_verilog.initial_state(),
    ).run()?;
    let verilog_text = module.render().run()?;
    assert!(verilog_text.contains("always_ff @(posedge clk)"));
    Ok(())
}

Architecture

hdl-cat-error/    Shared Error enum (hand-rolled Display/From)
hdl-cat-bits/     Bits<N>, SignedBits<N> via const generics
hdl-cat-kind/     Hw trait + TypeDesc (runtime type witness)
hdl-cat-signal/   Signal<D, T> over comp_cat_rs::Stream
hdl-cat-ir/       Free-category IR (comp_cat_rs::Graph instance)
hdl-cat-circuit/  Circuit: Category + MonoidalCategory + Symmetric
hdl-cat-sync/     Mealy machines as IR + initial state
hdl-cat-sim/      Stream-based testbench / simulator / VCD tracer
hdl-cat-verilog/  Verilog AST + stateful emitter
hdl-cat-std/      Component library (counter, accumulator, FIFO, ...)
hdl-cat-macros/   #[kernel] proc-macro frontend
hdl-cat/          Umbrella crate + examples

Core design

  • Signals are Streams. Signal<D, T> wraps Stream<Error, T>; clock domains are zero-sized phantom types; domain-mixing is a type error.
  • Circuits form a symmetric monoidal category. Sequential composition is Category::comp; parallel composition is MonoidalCategory::tensor_map.
  • IR is a free category. HdlGraph implements comp_cat_rs::collapse::free_category::Graph. Compiled circuits are sequences of typed Instructions. Simulation and codegen walk the graph.
  • State is Kleisli. Sync<S, I, O> holds an IR graph whose inputs and outputs pair (state, data), threaded cycle to cycle. No closures, no interior mutability.
  • run at the boundary. Simulation and codegen build Io/Stream pipelines internally, calling .run() only at public entry points.

Building circuits

hdl-cat-circuit provides primitive gates and a categorical calculus for composing them. Each gate is a CircuitArrow<A, B> where A and B are typed objects (single Obj<T> or nested CircuitTensor).

use comp_cat_rs::foundation::category::Category;
use hdl_cat_circuit::{gates, Circuit};

# fn main() -> Result<(), hdl_cat_error::Error> {
// Two 4-bit inverters in series — the identity on 4-bit values.
let inv_a = gates::not_bits::<4>()?;
let inv_b = gates::not_bits::<4>()?;
let roundtrip = Circuit::comp(inv_a, inv_b);
assert_eq!(roundtrip.graph().instructions().len(), 2);
# Ok(()) }

Parallel composition places two arrows side-by-side:

use comp_cat_rs::foundation::monoidal::MonoidalCategory;
use hdl_cat_circuit::{gates, Circuit};

# fn main() -> Result<(), hdl_cat_error::Error> {
let a_inv = gates::not_bits::<4>()?;
let b_inv = gates::not_bits::<4>()?;
let paired = Circuit::tensor_map(a_inv, b_inv);
assert_eq!(paired.inputs().len(), 2);
assert_eq!(paired.outputs().len(), 2);
# Ok(()) }

Wire permutations (braid, associator, unitors) come from the monoidal coherence arrows in hdl_cat_circuit::coherence.

Stateful machines

A Sync<S, I, O> wraps an IR graph whose inputs are state ⊗ input and outputs are next_state ⊗ output. The simulator threads next_state into the following cycle's state.

use hdl_cat_bits::Bits;
use hdl_cat_circuit::{CircuitUnit, Obj};
use hdl_cat_sync::Sync;

# fn main() -> Result<(), hdl_cat_error::Error> {
// A stateless inverter lifted to a Sync machine (state = CircuitUnit).
let inv = hdl_cat_circuit::gates::not_bits::<4>()?;
let m: Sync<CircuitUnit, Obj<Bits<4>>, Obj<Bits<4>>> = Sync::lift_comb(inv);
assert_eq!(m.state_wire_count(), 0);
# Ok(()) }

Sync machines compose via compose_sync (state becomes (S1, S2)), par_sync (parallel, state (S1, S2)), and feedback_sync (one-cycle loop-back).

Simulation

use hdl_cat::prelude::*;
use hdl_cat_kind::BitSeq;

# fn main() -> Result<(), hdl_cat_error::Error> {
let counter = std_lib::counter::<4>()?;
let empty = BitSeq::new();
let inputs = vec![empty.clone(), empty.clone(), empty.clone(), empty.clone()];
let samples = Testbench::new(counter).run(inputs).run()?;
let values: Vec<u128> = samples
    .iter()
    .map(|s| Bits::<4>::from_bits_seq(s.value()).map(Bits::to_u128))
    .collect::<Result<Vec<_>, _>>()?;
assert_eq!(values, vec![0, 1, 2, 3]);
# Ok(()) }

Testbench::run returns Io<Error, Vec<TimedSample<BitSeq>>>; .run() collects the samples.

Verilog emission

Two emitters:

  • emit_graph — flat combinational view; treats every input wire as a module input port and every output wire as an output port. Useful for pure combinational circuits.
  • emit_sync_graph — stateful view; promotes state wires to reg declarations driven by always_ff @(posedge clk) blocks with synchronous reset from the machine's initial state.

The counter above emits as:

module counter4 (
    input clk,
    input rst,
    output reg [3:0] w0
);
    wire [3:0] w1;
    wire [3:0] w2;
    assign w1 = 4'd1;
    assign w2 = (w0 + w1);
    always_ff @(posedge clk) if (rst) w0 <= 4'd0; else w0 <= w2;
endmodule

VCD traces

hdl_cat_sim::trace_to_string runs a simulation and emits a VCD (Value Change Dump) string. Each graph wire becomes a $var wire declaration; one timestamp per cycle with per-wire values.

use hdl_cat::prelude::*;
use hdl_cat_kind::BitSeq;
use hdl_cat_sim::trace_to_string;

# fn main() -> Result<(), hdl_cat_error::Error> {
let c = std_lib::counter::<4>()?;
let inputs: Vec<BitSeq> = (0..8).map(|_| BitSeq::new()).collect();
let vcd = trace_to_string(&c, inputs)?;
assert!(vcd.contains("$timescale"));
assert!(vcd.contains("#0"));
# Ok(()) }

Redirect the string to counter.vcd and open in GTKWave or Surfer.

The #[kernel] macro

#[kernel] lifts a small Rust-subset function into a CircuitArrow builder. v1 supports: bool / Bits<N> / SignedBits<N> parameters, let bindings, binary arithmetic/bitwise ops, unary !.

use hdl_cat::prelude::*;
use hdl_cat::kernel;

#[kernel]
fn xor_then_add(a: Bits<8>, b: Bits<8>) -> Bits<8> {
    let x = a ^ b;
    x + a
}

# fn main() -> Result<(), hdl_cat_error::Error> {
let arrow = xor_then_add()?;
// Two instructions: Xor + Add.
assert_eq!(arrow.graph().instructions().len(), 2);
# Ok(()) }

After expansion, xor_then_add becomes a nullary function returning Result<CircuitArrow<CircuitTensor<Obj<Bits<8>>, Obj<Bits<8>>>, Obj<Bits<8>>>, Error>.

Per-crate tour

Crate Purpose
hdl-cat-error The workspace-wide Error enum with hand-rolled Display/From
hdl-cat-bits Bits<N>/SignedBits<N> const-generic integers, wrap arithmetic
hdl-cat-kind Hw trait, TypeDesc, BitSeq buffer — the hardware-typable-values layer
hdl-cat-signal Signal<D, T> over comp_cat_rs::Stream, clock-domain phantoms
hdl-cat-ir HdlGraph, Instruction, Op — the IR implementing comp_cat_rs::Graph
hdl-cat-circuit CircuitArrow, Obj, CircuitTensor, gates, Circuit: Category + MonoidalCategory + Braided + Symmetric
hdl-cat-sync Sync<S, I, O> Mealy machines, compose_sync / par_sync / feedback_sync
hdl-cat-sim Testbench, IR interpreter, TimedSample, VCD emission
hdl-cat-verilog Verilog AST, emit_graph, emit_sync_graph, renderer
hdl-cat-std Standard components: counter, accumulator, down_counter, toggle_ff, shift_register_left, half_adder, full_adder
hdl-cat-macros #[kernel] proc-macro
hdl-cat Umbrella crate re-exporting everything; use hdl_cat::prelude::*

Conventions

Every crate follows the same Rust discipline (functional, type-driven, domain-driven). See CLAUDE.md at the workspace root for the enforced rules: newtypes for domain primitives, hand-rolled error handling, combinators over pattern matching (on both Option and Result), static dispatch only, no mut/return/loop/for/unwrap/expect.

Verification gate for every change:

RUSTFLAGS="-D warnings" cargo clippy --all-targets --all-features
cargo test --all

License

Dual-licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.