The graph
Nodes, widths and dependencies. No behaviour, no types, no opinions about what hardware is.
The IR is a circuit: a flat arena of nodes, each with a width, connected by edges. That is the whole data structure. It has no notion of a clock, no notion of a process, and no notion of what a register is beyond being a node with a feedback edge.
Keeping it this small is the point. Both backends — the cycle simulator and the Verilog emitter — read this and nothing else, so anything they can both express is expressible, and anything one of them needs that the other cannot is a gap you find immediately rather than at integration.
Step by step
- 01
An arena, so node identity is an integer
Circuitstores nodes in insertion order and refers to them byNodeId. Nothing holds a reference into the arena, which means the graph is cheap to traverse, cheap to clone, and impossible to invalidate.use ferrite_lithic_ir::Circuit; let mut circuit = Circuit::new(); let a = circuit.wire(8)?; let b = circuit.wire(8)?; let sum = circuit.add(a, b)?; assert_eq!(circuit.width_of(sum), 8); assert_eq!(circuit.len(), 3);
- 02
A wire is a node that something drives
wireallocates,driveconnects a driver to it, anddriver_ofreads the connection back. A wire with no driver is a legitimate state — it is what a register looks like between elaboration and the clock edge — so the IR reports it asOk(None)rather than as an error.let mut circuit = Circuit::new(); let w = circuit.wire(4)?; assert_eq!(circuit.driver_of(w)?, None); let v = circuit.constant(ferrite_lithic_bits::Bits::constant(3, 4)?); circuit.drive(w, v)?; assert_eq!(circuit.driver_of(w)?, Some(v));
- 03
Pure constructors for every combinational primitive
add,mul, the four divisions, the eight comparisons,select,cat,replicate,iteandcase_all live onCircuitdirectly. Each is infallible in shape and returnsResultonly where a width rule can be violated —selectpast the end of a value, for instance.let mut c = Circuit::new(); let a = c.wire(8)?; let b = c.wire(8)?; let wide = c.add(a, b)?; // max(8, 8) = 8 let cmp = c.ult(a, b)?; // 1 bit let mux = c.ite(cmp, a, b)?;
- 04
Node kinds are an enum, so an emitter cannot forget one
Nodeis aNodeenum with a variant per primitive —Constant,Wire,Not,Select,BitAnd,Add,UDiv,SRem, the comparisons, and the rest. An exhaustivematchin each backend means adding a primitive is a compile error in every backend that has not been taught it, rather than a silently dropped node. - 05
Names are optional and checked
nameandnamesattach identifiers for waveforms and generated Verilog. They are optional, because a design that has not been named yet is still a valid design and forcing names at IR-construction time would mean naming things before knowing what they are.
What bites
Two nodes of different widths in one operator
Allowed, and resolved as
maxwith zero-extension — the Verilog rule. It is the single most common source of a design that is a byte wider than its author intended.`case_` with no default arm
case_requires a default. An incompletecasein generated Verilog is a latch, and a latch inferred silently by a synthesis tool is the hardest class of bug to find from a waveform.
Notes
- decision
Flat arena, no tree
A tree-shaped IR would make construction easy and every consumer awkward. The flat arena makes construction slightly more explicit and traversal trivial, and traversal is what both backends do constantly.