ferrite-lithic811 tests · 21 designs

ferrite-lithic-irphase A1116 tests

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

  1. 01

    An arena, so node identity is an integer

    Circuit stores nodes in insertion order and refers to them by NodeId. 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);
  2. 02

    A wire is a node that something drives

    wire allocates, drive connects a driver to it, and driver_of reads 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 as Ok(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));
  3. 03

    Pure constructors for every combinational primitive

    add, mul, the four divisions, the eight comparisons, select, cat, replicate, ite and case_ all live on Circuit directly. Each is infallible in shape and returns Result only where a width rule can be violated — select past 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)?;
  4. 04

    Node kinds are an enum, so an emitter cannot forget one

    Node is a Node enum with a variant per primitive — Constant, Wire, Not, Select, BitAnd, Add, UDiv, SRem, the comparisons, and the rest. An exhaustive match in 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.

  5. 05

    Names are optional and checked

    name and names attach 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 max with 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 incomplete case in 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.