Karray Basics
A Karray is Kathryn’s typed multi-dimensional array. Think of it as an array of records: you declare the element layout once (a set of named, sized fields), give the array a shape and a backing, and Kathryn materializes the hardware for you. It is the natural building block for register files, reorder buffers, scoreboards, and any other “table of structs” you would otherwise wire by hand.
The defining property of a Karray is that each field of each element is its own
hardware component. A 4-entry array whose element is {valid: 1, data: 7}
materializes eight independent registers — not four packed 8-bit vectors that get
bit-sliced. A field reference drives (or reads) exactly that field’s hardware,
full-width, with no bit-level splitting.
This is the Hardware Aggregator — a Table of Slots, where every slot (an element’s field) is its own independent component:
flowchart TB
subgraph E0["element 0"]
V0["valid (1b reg)"]
D0["data (7b reg)"]
end
subgraph E1["element 1"]
V1["valid (1b reg)"]
D1["data (7b reg)"]
end
subgraph E2["element 2"]
V2["valid (1b reg)"]
D2["data (7b reg)"]
end
subgraph E3["element 3"]
V3["valid (1b reg)"]
D3["data (7b reg)"]
end
Declaring a Karray
Section titled “Declaring a Karray”Declare the element layout by subclassing Karray and listing fields with
kaf(width) (short for Karray field). Then construct the subclass with a
backing, a shape, and an optional name:
from kathryn import *
class RfEntry(Karray): valid = kaf(1) data = kaf(7)
class my_module(Module): @init def com_declare(self): # 4 entries, each a {valid:1, data:7} record, register-backed self.rf = RfEntry(HwComponentType.REG, (4,), "rf")The constructor signature is Karray(backing, shape=(1,), name=None):
backing— aHwComponentTypemember:REG,WIRE, orMEM_BLOCK. This decides what hardware each field becomes and which assignment operator you must use (see Backings).shape— an iterable of dimension extents.(4,)is a 1-D array of 4 elements;(5, 3)is a 5×3 grid.name— optional; auto-generated if omitted. The name shows up in the emitted Verilog as a prefix (e.g.rf_E0_valid).
The field name defaults to the attribute name; kaf(width, name) lets you
override it. Duplicate field names raise a TypeError at class-definition time.
Field declarations are inherited: a subclass of a Karray subclass collects the base class’s fields first, then adds its own. This lets you build up element layouts incrementally.
Static access: rf[i][j].field
Section titled “Static access: rf[i][j].field”Indexing a Karray with plain integers selects an element; a trailing attribute selects a field of that element:
self.rob = RobEntry(HwComponentType.REG, (5, 3), "rob")
self.rob[2][1].valid # the valid field of element (2, 1)self.rob[2][1].reg_idx # the reg_idx field — a DIFFERENT hardware componentEach hop of [] indexes one more dimension; the .field suffix narrows the
reference to a single field. A field reference behaves like a signal: use it as
an assignment source or destination.
Because every field is its own component, rob[2][1].valid resolves to a 1-bit
register and rob[2][1].reg_idx to a 5-bit register — two distinct pieces of
hardware, not two slices of one packed word. Referencing a field name that was
never declared raises a ValueError when the reference is resolved.
Writing elements
Section titled “Writing elements”There are two write styles (both shown here on a reg backing, so |=):
with seq(): # field-wise: drive each field's own component self.rf[0].valid |= self.c_valid self.rf[0].data |= self.c_data
# whole-element: a {field_name: source} map; each named source # is connected to the field of that name (full-width, no bit split) self.rf[1] |= {"valid": self.c_pvalid, "data": self.c_pdata}A whole-element assignment takes a dict keyed by field name — never a packed
bit-vector. Reading, on the other hand, always goes through a specific field
(rf[0].data); you cannot read a whole element as one value.
What gets emitted
Section titled “What gets emitted”For the 4-entry {valid:1, data:7} register file above, the Verilog backend
emits one register per (element, field):
reg [0:0] REG_rf_E0_valid_5159;reg [6:0] REG_rf_E0_data_5160;reg [0:0] REG_rf_E1_valid_5161;reg [6:0] REG_rf_E1_data_5162;// ... E2, E3 ...and each write becomes an ordinary clocked assignment on that field’s register.
Slices and regions
Section titled “Slices and regions”Integer slices select a region of a Karray — useful for copying between arrays
(dst[0:2] |= src[1:3]). A sliced region cannot be read as a scalar or narrowed
to a field; it only makes sense as a source or destination of a
karray-to-karray assignment. See
Conversion & Resize.
Where to next
Section titled “Where to next”- Backings — reg vs wire vs mem_blk, and which assignment operator each allows.
- Indexing — runtime-signal (dynamic) indexing, binary and one-hot.
- Dynamic Writes — runtime-selected writes and custom write-enable logic.
- Reduce — fold an array down to a single winner.