Skip to content

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

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 — a HwComponentType member: REG, WIRE, or MEM_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.

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 component

Each 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.

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.

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.

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.

  • 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.