ferrite-lithic811 tests · 21 designs

ferrite-lithic-bitsphase A196 tests

Fixed-width bitvectors

Runtime-width integers over u64 words, with the width rules that make Verilog agree with you.

This is the bottom of the stack and the only crate with no dependency on anything else in the workspace. It is a fixed-width bitvector: the width is a runtime value, not a type parameter, because the DSL builds graphs whose widths are only known once the design is.

It looks like a u64 with a width attached and it is not. Every operation here has to answer the same question — what happens when two operands disagree — and the answer has to be the one Verilog gives, because the whole project rests on the emitted Verilog and the simulator agreeing.

Step by step

  1. 01

    Store words, not bits

    A value is a Vec<u64> plus a width in bits. words_for_width is a const fn, so the word count is computed rather than searched for. Storing words rather than a bit per bit is the difference between a graph traversal that is memory-bound and one that is not.

    64bits per wordWORD_BITS — one shift, not a bit vector
    use ferrite_lithic_bits::{Bits, words_for_width, WORD_BITS};
    
    assert_eq!(WORD_BITS, 64);
    // The word count is arithmetic, not a loop.
    assert_eq!(words_for_width(1), 1);
    assert_eq!(words_for_width(65), 2);
  2. 02

    Construct from the four shapes hardware actually has

    zeros and ones are the two reset values, constant takes a host integer, and from_bytes_le is what a memory port or a byte-wide bus hands you. There is no From<u64> on purpose: a conversion that cannot fail is a conversion that will silently truncate, and the width has to be stated.

    use ferrite_lithic_bits::Bits;
    
    assert_eq!(Bits::zeros(8)?.to_u64()?, 0x00);
    assert_eq!(Bits::ones(8)?.to_u64()?, 0xff);
    assert_eq!(Bits::constant(0x5a, 8)?.to_u64()?, 0x5a);
    
    // A memory read, little-endian, exactly as the simulator hands it over.
    let bytes = [0x01, 0x02];
    assert_eq!(Bits::from_bytes_le(16, &bytes)?.to_u64()?, 0x0201);
  3. 03

    Extend and truncate explicitly

    zero_extend, sign_extend and truncate are three separate functions because they are three different pieces of hardware. Collapsing them into one with a flag makes every call site carry a boolean that says nothing at the call site.

    use ferrite_lithic_bits::Bits;
    
    let eight = Bits::constant(0xff, 8)?;
    assert_eq!(eight.zero_extend(16)?.to_u64()?, 0x00ff);
    
    let high = Bits::constant(0xff, 8)?;
    assert_eq!(high.sign_extend(16)?.to_u64()?, 0xffff);
    
    assert_eq!(high.truncate(4)?.to_u64()?, 0xf);
  4. 04

    Arithmetic that refuses rather than guesses

    Every arithmetic method returns Result. udiv by zero is an error, not a zero and not a panic — a division that cannot be built is a division the caller has to decide about, and the alternative is a design that means something the author did not write. Signed division and remainder are genuine primitives here rather than being expressed through unsigned ones, because Verilog's / and % are signed for signed operands and the distinction changes the gate count enormously.

    use ferrite_lithic_bits::Bits;
    
    let a = Bits::constant(7, 8)?;
    let b = Bits::constant(2, 8)?;
    assert_eq!(a.add(&b)?.to_u64()?, 9);
    assert_eq!(a.udiv(&b)?.to_u64()?, 3);
    assert_eq!(a.urem(&b)?.to_u64()?, 1);
    
    // Refused, not guessed.
    let zero = Bits::zeros(8)?;
    assert!(a.udiv(&zero).is_err());
  5. 05

    Expose the predicates the hardware needs

    is_negative, is_min_signed and magnitude_is_zero exist because a signed comparison in hardware is not a comparison on the raw word. A design that needs to know whether an adder overflowed needs is_min_signed on the result, and there is no way to derive that from is_negative alone.

What bites

  • A binary operation returns the wider of the two widths

    The rule throughout is max(left, right), with the narrower operand zero-extended. This is Verilog's rule, and it is not Rust's. A design that assumes Rust's behaviour silently gains width on every comparison and every and.

  • Width zero is an error, not an empty value

    Bits::zeros(0) fails rather than producing an empty vector. A zero-width signal is a modelling mistake, and letting it exist means the emitter has to decide what to print for it.

Notes

  • decision

    Runtime widths, not const generics

    A const generic width would make every width a distinct type and every arithmetic operator a trait to be implemented. The width is data here, so Design::add does not need to know it at compile time and the graph stays one type.

  • gap

    No arbitrary-precision arithmetic

    The host-side representation is u64 words, so a design wider than the host can hold bits in is not buildable. Nothing in the corpus needs it, and the limit is recorded rather than hidden.