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
- 01
Store words, not bits
A value is a
Vec<u64>plus a width in bits.words_for_widthis aconst 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 vectoruse 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); - 02
Construct from the four shapes hardware actually has
zerosandonesare the two reset values,constanttakes a host integer, andfrom_bytes_leis what a memory port or a byte-wide bus hands you. There is noFrom<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);
- 03
Extend and truncate explicitly
zero_extend,sign_extendandtruncateare 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);
- 04
Arithmetic that refuses rather than guesses
Every arithmetic method returns
Result.udivby 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());
- 05
Expose the predicates the hardware needs
is_negative,is_min_signedandmagnitude_is_zeroexist because a signed comparison in hardware is not a comparison on the raw word. A design that needs to know whether an adder overflowed needsis_min_signedon the result, and there is no way to derive that fromis_negativealone.
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 everyand.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
constgeneric width would make every width a distinct type and every arithmetic operator a trait to be implemented. The width is data here, soDesign::adddoes not need to know it at compile time and the graph stays one type. - gap
No arbitrary-precision arithmetic
The host-side representation is
u64words, 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.