Skip to content

Bit Utilities

kathryn.lib is Kathryn’s small standard library: generic, reusable hardware helpers built purely on the public DSL — nothing in it is magic, and everything it builds you could write by hand with wires, zif/zelse, and slices. This page covers the bit-manipulation helpers; interface helpers live in Bundles & Handshake.

from kathryn.lib import width_of, zext, sext, cat, replicate, or_reduce, and_reduce, mux

The helpers split into two families, and the distinction matters for where you may call them:

  • Expression-onlywidth_of, zext, or_reduce, and_reduce build (or inspect) expressions and are usable anywhere a signal is.
  • Wire-declaringsext, cat, replicate, mux declare a fresh wire and/or open zero-cycle blocks, so they must run inside an open module scope — in practice, call them from a @flow method.
flowchart TB
    subgraph EXPR["expression-only: use anywhere"]
        WO["width_of"] ~~~ ZX["zext"] ~~~ OR["or_reduce / and_reduce"]
    end
    subgraph WIRE["declare a fresh wire: call from @flow"]
        SX["sext"] ~~~ CT["cat / replicate"] ~~~ MX["mux"]
    end

Everything on this page is combinational: outputs reflect their inputs the same cycle (worked example: tc34, which drives all inputs and checks every derived output in the same cycle).

Returns the width of a signal as referenced — a slice view reports the slice width, not the underlying signal’s width. Plain Python int, usable in any host-side computation.

Zero-extend to width bits. Wraps the native extend op, so it returns a plain expression; if the signal is already at the target width it passes through unchanged. A source wider than the target raises ValueError.

self.o_zext *= zext(self.b, 8) # 4-bit b, high 4 bits padded with 0
assign EXPR_expr0 = {{4{1'b0}}, WIRE_b[3:0]};

Sign-extend to width bits: the low bits pass through and the high bits mirror the source’s sign bit (its MSB). Unlike zext this declares a fresh wire and drives it through a full-coverage zif/zelse pair, one branch per sign value:

self.o_sext *= sext(self.b, 8) # b = 0x9 → 0xF9; b = 0x5 → 0x05

Concatenate MSB-first — the same order as Verilog {a, b, c}, so the last argument lands in the least-significant bits. The result is a fresh wire whose width is the sum of the parts, each part driven onto its slice:

self.o_cat *= cat(self.b, self.a[3, 0]) # {b, a[3:0]} — b in the high half

n copies of a 1-bit signal (Verilog {n{bit}}); a wider source raises ValueError. It is literally cat applied to the same bit n times.

self.o_repl *= replicate(self.c, 4) # 0b1111 when c is high, else 0b0000

Fold a Python list of same-width signals (typically 1-bit flags) into one result. Rather than a linear a | b | c | d … chain, the fold is a balanced binary tree, so the hardware is log-depth:

flowchart TB
    A0["a[0]"] --> O1["or"]
    A1["a[1]"] --> O1
    A2["a[2]"] --> O2["or"]
    A3["a[3]"] --> O2
    O1 --> O3["or"]
    O2 --> O3
    O3 --> R["result"]
self.o_orr *= or_reduce([self.a[i] for i in range(8)]) # any bit set?
self.o_andr *= and_reduce([self.a[i] for i in range(8)]) # all bits set?

An odd element is carried up and paired at the next layer — any list length works. An empty list raises ValueError.

A 2:1 mux: cond ? a : b as a fresh wire, driven by a full-coverage zif/zelse with no default. a and b may be signals or plain ints (ints auto-wrap as usual — see Expressions), but at least one must be a signal so the output width can be inferred (max(width(a), width(b))); two ints raise ValueError.

self.o_mux *= mux(self.c, self.a, zext(self.b, 8)) # c ? a : zext(b)

All eight helpers are exercised end-to-end in tc34_lib_bits — see the Examples Gallery.