Spire — a modern Python HDL that compiles concise, composable hardware descriptions to synthesizable Verilog and AIG netlists, with synthesis optimization and a cycle-accurate simulator built in.
- Built for humans and agents alike: a small surface that stays readable as a design grows
- Reduces area and delay vs. a traditional Verilog flow: optimization is part of the compile
- Integrated with ABC and mockturtle: modern synthesis optimization wired directly into the compilation pipeline
- Arithmetic library with automated replacement: swap adders, multipliers, and FP cores driven by an objective
- Cycle-accurate Python simulator: drive inputs, tick clocks, inspect expressions/outputs without leaving Python
- Content-addressed optimization cache: instant re-runs via the
@abc_optimizeddecorator
Spire supports source-level optimization intent: the designer marks what to optimize (e.g. a module, FSM, or arithmetic block) directly in the HDL source, and the compiler realizes it through synthesis-aware passes.
Because these passes run as part of the compile, the emitted Verilog is already small and fast before external tools see it. Each feature's guide below includes measured results against a plain Yosys flow on the same RTL.
Drops in topology-tuned adders, multipliers, and MAC fusions against an area / delay / adp objective. MAC patterns (a*b + c) are fused into single column-reduction units, eliminating a full adder stage. See README_arithmetic_optimization.md.
One decorator stacks modern AIG synthesis (resyn2, &deepsyn) onto any Component or function, with a content-addressed cache for instant re-runs. It stacks with @arithmetic_optimized for compounding wins — ABC cleans up after the arithmetic rewriter. See README_optimization_decorators.md.
Hopcroft state minimisation and bit-assignment search as two composable context managers — nest both for compounding cell-count reductions, with no hand-tuned encoding tables. See README_fsm_optimization.md.
Chained conditionals and reduction loops both lower to linear, O(N)-deep mux cascades. selection_topology re-emits switch_/if_ blocks and hand-built mux chains in log-depth form (onehot, bittree, priority-preserving tournament, or auto), with the one-hot modes validated against provably-disjoint case labels. spire.reduce builds balanced trees for max_ / min_ / argmax_ / sum_ / reduce_tree, plus prefix_scan for running results. Rewrites are eager, so Verilog, AIG export, and the simulator all see the same structure. Details: README_control_structures.md and README_reductions.md.
Beyond the automatic passes above, the unified arithmetic generator lets you hand-pick the exact micro-architecture of an adder, multiplier, MAC, or matmul (partial-product generation, compression-tree topology, and final-stage adder), then emit Verilog/AIG, simulate, and collect Yosys metrics for direct comparison. See README_arithmetic_generator.md.
A content-addressed, verification-gated library of subcircuit implementations. Decorating a
function registers its golden spec as a slot; producers (tools, agents, humans) insert
alternative implementations through a verification gate, where each candidate is proven
equivalent to the golden (CEC or golden-simulated sim) before admission. Every later compile
splices in the best admitted design for the chosen objective (area / delay / adp).
See README_design_db.md.
In its simplest form, Spire only needs these core files. This is intentional — the HDL is kept to a minimal, self-contained core, and higher-level features are layered on top:
spire/expr.py– the expression DSL. It provides bit-precise types such asBool,UInt, andSInt, shared-expression caching, and the overloaded arithmetic / bitwise operators that make the Python syntax feel like an HDL.spire/component.py– theComponentbase class: author reusable designs, declare IO withIORecord/Input/Output, emit Verilog/AIG, analyze, and import/embed sub-designs. The flat netlist IR it lowers to lives inspire/ir.py(Netlist).spire/simulator.py– a lightweight simulator that can drive inputs, tick clocks, inspect outputs or internal expressions, and capture probes for debugging—all without leaving Python.
Deeper guides for specific features:
- Type system — values, the
BitSerializable/Assignabletraits, theExprvsHDLCompositehierarchy, and the…Likecoercion aliases (class diagram + philosophy) - Hints & common mistakes — core API in a page, width-inference pitfalls, synthesis-quality tips
- Composite data types — structured bit-packable values (
Array,CompositeRecord,FixedPoint,FloatingPoint,CompositeRegister) withto_bits/<<=/@= - Interfaces — reusable IO bundles (
Stream,Flow,MemPort) withFlipped/connect/view_as_flippedand on-interface behaviour - State machines — declaration with the
State/EncodingAPI andswitch_/case_bodies - Control structures —
if_/elif_/else_andswitch_/case_/defaultcontext managers - Reductions — balanced log-depth trees:
max_/argmax_/sum_/reduce_tree - Memories — RAM / ROM / FIFO primitives, port wiring with
<<=, simulation, and reading state - Arithmetic optimization — automatic replacement with optimized versions (adders, multipliers, MAC, etc)
- Optimization decorators —
@abc_optimizedcircuit optimization - FSM optimization —
optimized_fsmandoptimized_encoding(state minimisation + encoding search) - Arithmetic generators — evaluation scripts and extra tooling notes
- Design DB — verification-gated library of subcircuit implementations with deterministic selection and splice (
@from_design_db) - Tiled matmul accelerator — parametrizable signed output-stationary matmul over a
T×T×Tcore, with input/output RAMs and amode+ RAM-access interface - Custom Verilog — emit a raw Verilog block from a
CustomVerilogComponent, with or without a Python sim model (blackbox) - AIG / AAG export & import — lower a
Componentto an AIGER netlist and read AIG/AAG back in as aComponent - Simulation & debugging — driving the Python
Simulator, live inspection (peek/watch, register and memory state), VCD waveforms from a Python run, side-by-side cross-checks against the emitted Verilog - Verilog testbench — turn a
Simulatorrun into a self-checking, synthesizable Verilog testbench, and run it under Verilator/Icarus - Examples — example designs exercising Spire features
Install the latest release from PyPI:
pip install spire-hdlFor development, install from source in editable mode:
git clone https://github.com/huawei-csl/spire-hdl.git
cd spire-hdl
pip install -e .The library relies on the packages listed in requirements.txt.
A Component is the one abstraction you author. Declare its IO once with IORecord
(field names become signal names; Input/Output set direction), put the logic in
elaborate(), and emit Verilog directly — without ever touching the IR:
from spire import Component, IORecord, Input, Output, Bool, UInt
from spire.expr import mux, cat
class LogicDemo(Component):
def __init__(self):
self.io = IORecord(
a=Input(UInt(8)),
b=Input(UInt(8)),
sel=Input(Bool()),
sum=Output(UInt(9)),
mask=Output(UInt(2)),
out=Output(UInt(8)),
)
self.elaborate()
def elaborate(self):
io = self.io
io.sum <<= io.a + io.b # automatic width growth
io.mask <<= cat(io.a[7], io.b[7]) # concatenate slices
io.out <<= mux(io.sel, io.a & io.b, io.a | io.b)
demo = LogicDemo()
print(demo.to_verilog(name="LogicDemo"))to_verilog() checks that every output has a driver (and every register a next-state assignment) before emitting Verilog (see component.py).
Registers are created via the standalone Register class, which takes a typ and an optional reset value via the init= keyword (note: the keyword is init, not reset_value / reset). Assign the next-state expression with <<=, and pass with_clock/with_reset when emitting a sequential component:
from spire import Component, IORecord, Output
from spire.expr import Register, UInt
class Counter(Component):
def __init__(self):
self.io = IORecord(q=Output(UInt(8)))
self.elaborate()
def elaborate(self):
cnt = Register(UInt(8), init=0) # reset value via init=
cnt <<= cnt + 1 # next-state = cnt + 1
self.io.q <<= cnt
print(Counter().to_verilog(name="Counter", with_clock=True, with_reset=True))from spire import Simulator
sim = Simulator(demo) # Simulator lowers the Component internally
sim.set("a", 0xC3)
sim.set("b", 0x99)
sim.set("sel", 1)
sim.eval() # recompute combinational logic
print(sim.peek_outputs()) # {'sum': 0x15c, 'mask': 0x3, 'out': 0x81}The simulator keeps track of inputs, wires, outputs, and registers, supports eval() for combinational updates, step() for clocked designs, and exposes helpers such as peek, peek_next, and signal watching for deeper inspection (simulator.py).
A Component converts to Verilog, AIGER, or the flat netlist IR — and re-imports the same formats — so you can hand a design to synthesis, optimize it externally, and check the result is still equivalent:
from spire import Component
from spire.aig.aig_aigerverse import conv_aag_into_aig
from aigverse import Aig, equivalence_checking
verilog = demo.to_verilog(name="LogicDemo") # synthesizable Verilog (str)
aag = demo.to_aag(name="LogicDemo") # AIGER ASCII lines (list[str])
net = demo.to_netlist(name="LogicDemo") # flat netlist IR (Netlist)
demo2 = Component.from_netlist(net) # re-import the IR
# Import Verilog source (yosys-backed; ports map onto the declared IO) — here a round-trip
# of the text we just emitted; from_verilog_file(path) reads external .v files the same way.
# Sequential designs import too: their registers land on the surrounding design's global clock.
from spire.component import ImportedComponent
shell = ImportedComponent(IORecord(a=Input(UInt(8)), b=Input(UInt(8)), sel=Input(Bool()),
sum=Output(UInt(9)), mask=Output(UInt(2)), out=Output(UInt(8))))
demo3 = shell.from_verilog(verilog) # also: .from_verilog_file / .from_aag_lines / .from_aig_file
# equivalence-check the original against both re-imported designs (aigverse, on their AIGs)
a1 = conv_aag_into_aig(demo.to_aag(name="LogicDemo"), Aig())
a2 = conv_aag_into_aig(demo2.to_aag(name="LogicDemo"), Aig())
a3 = conv_aag_into_aig(demo3.to_aag(name="LogicDemo"), Aig())
assert equivalence_checking(a1, a2)
assert equivalence_checking(a1, a3) # the Verilog round-trip is logic-equivalent tooComponentis the one abstraction you author. It builds Verilog/AIG directly (to_verilog,to_aag), analyzes timing (analyze), imports designs from Verilog or AIG formats (from_verilog,from_aag_lines,from_netlist), and embeds reusable sub-designs into the surrounding logic automatically when their IO is wired. Components exposeget_ios()/get_spec()as the single IO normalization point — also what drives port regrouping when you import flattened designs (seecomponent.py).Netlist(inspire.ir) is the flat, lowered netlist that every backend consumes — Spire's internal IR. Power users can build one directly: it offers constructors for inputs, outputs, wires, and registers; signal enumeration; Verilog emission with automatic width fitting; and ananalyze()routine reporting combinational depth and node counts. The quick start never needs it.- Minimal end-to-end component example:
testing/examples/simple_component.py.
Short component + hierarchy usage example:
from spire import Component, IORecord, Input, Output, UInt
class SimpleAdder(Component):
def __init__(self, width=8):
self.width = width
self.io = IORecord(
a=Input(UInt(width)),
b=Input(UInt(width)),
sum=Output(UInt(width + 1)),
)
self.elaborate()
def elaborate(self):
self.io.sum <<= self.io.a + self.io.b
class Sum3Hierarchical(Component):
def __init__(self):
self.io = IORecord(
a=Input(UInt(8)),
b=Input(UInt(8)),
c=Input(UInt(8)),
sum=Output(UInt(10)),
)
self.elaborate()
def elaborate(self):
add_ab = SimpleAdder(width=8) # first sub-component
add_abc = SimpleAdder(width=9) # second sub-component
add_ab.io.a <<= self.io.a
add_ab.io.b <<= self.io.b
add_abc.io.a <<= add_ab.io.sum
add_abc.io.b <<= self.io.c
self.io.sum <<= add_abc.io.sum
print(Sum3Hierarchical().to_verilog(name="Sum3Hier")) # one flat module, built from embedded componentsComponents are how to build hierarchy: instantiate one inside another, adapt its IO, or drop in a pre-synthesized netlist — all without leaving Python. A common pattern wraps a reusable Spire block (see multipliers_ext.py). It is also possible to import an external AIG module, turn it into a Component, and use it directly as a native Spire block inside a larger generator (see multipliers_ext_optimized.py). The same approach covers Verilog imports, so Spire code and external IP mix freely.
Spire includes structured, bit-packable composites for cleaner interfaces and bulk assignments (composite/). See README_composite_types.md for the full reference with an example for every type:
HDLCompositedefines the base "pack to bits" API that powers all composites (base.py).Arrayoffers N-dimensional indexing, packed assignment (<<=), and element-wise assignment (@=) for nested vectors or composites (array.py).CompositeRecordis the bundle of named fields that stays packable to a flat bitvector — build it inline (CompositeRecord(a=Input(...), ...)), via a subclass__init__callingsuper().__init__(...), or as a@dataclass; declare fields as annotations for IDE autocomplete (record.py).FixedPointwraps aWireor view with explicit total/frac widths and quantization helpers, keeping arithmetic readable while staying hardware-friendly (fixed_point.py).FloatingPointprovides an IEEE-style view withadd/mulhelpers parameterized by exponent / fraction widths (floating_point.py).CompositeRegisterstores any composite in a single register while preserving a structured view via.value(register.py).
Example:
from spire.composite.array import Array
from spire.composite.record import CompositeRecord
from spire.composite.fixed_point import FixedPoint, FixedPointType
from spire.composite.register import CompositeRegister
from spire.expr import UInt, Wire
class Bus(CompositeRecord):
def __init__(self):
super().__init__(data=Wire(UInt(8)), valid=Wire(UInt(1)))
payload = Array([Bus(), Bus()])
acc = FixedPoint(FixedPointType(width_total=16, width_frac=8))
acc_reg = CompositeRegister(FixedPoint, acc.ftype, name="acc_reg")
acc_reg <<= acc # packed register write
payload[1] @= payload[0] # element-wise copy between bundlesThe simulator supports both combinational and sequential designs:
eval()recomputes combinational logic and captures registered probes.set()andget()let you drive or inspect signals by name.step()advances the clock, committing register next-state expressions while honoring asynchronous resets.watch()andpeek_next()provide scope-style visibility for debugging complex pipelines.
For waveforms, set sim.trace_enabled = True before driving the design, then dump the captured trace to a VCD file:
from spire.various.vcd_writer import write_vcd
sim.trace_enabled = True
sim.eval()
for _ in range(5):
sim.step()
write_vcd(trace_by_names=sim.get_trace_by_names(), filename="run.vcd", top_module="LogicDemo", timescale="1ns")Spire signals follow Python's indexing convention. For example, sig[4:7] creates a new expression made up of bits 4, 5, and 6 (counted from the LSB) of the original expression sig.
Check out the testing/examples/ directory for practical examples:
simple_component.py– A minimal example showing how to define a Component with IO ports and generate Verilogcomponent_example.py– Comprehensive examples including hierarchical design and simulationcomposing_components.py– Shows how to compose a largerComponentfrom smaller automatically embedded componentsdirect_expression_basics.py– Minimal direct expression examples (y = a + b) plus+,-, unary-,Const(..., SInt(...)), typed/plainFalse, and a recursive Horner polynomial buildertesting/riscv/rv32i.py– Minimal RV32I core example; seetesting/riscv/test_rv32i.pyfor simulation-based checks.
See the examples README for detailed documentation and key concepts.
