Skip to content

Indexing

A Karray index can be more than a plain integer. Each [] hop accepts:

IndexMeaning
intstatic — selects one element of the dimension at build time
slice (d[0:2])static region — for karray-to-karray assignment only
signaldynamic binary — a runtime binary-encoded address
oh(signal)dynamic one-hot — one runtime select bit per element

Static indexing was covered in Karray Basics. This page is about the dynamic forms, where which element is decided by hardware at runtime.

Index with a bare signal and Kathryn treats it as a binary-encoded address. Reading a field of the dynamically-selected element builds a balanced 2:1 mux tree over all candidate elements and returns a fresh wire of the field width:

class RfEntry(Karray):
valid = kaf(1)
data = kaf(8)
class worker(Module):
@init
def decl(self):
self.rf = RfEntry(HwComponentType.REG, (4,), "rf")
self.sel = reg(2) # 2-bit binary address for 4 elements
self.out = reg(8)
@flow
def f(self):
with seq():
self.out |= self.rf[self.sel].data # mux over rf[0..3].data

The read result is an 8-bit combinational wire (the mux output), regardless of the array’s backing. Every element’s data is wired into the tree; each mux layer switches on one bit of the address:

always @(*) begin
WIRE_REG_rf_E0_data_DMUX[7:0] <= 8'h0; // default
if (EXPR_VAL_bsel0_B0_N) begin // address bit 0 low
WIRE_REG_rf_E0_data_DMUX[7:0] <= REG_rf_E0_data[7:0];
end else begin
WIRE_REG_rf_E0_data_DMUX[7:0] <= REG_rf_E1_data[7:0];
end
end

Every element’s field feeds a balanced 2:1 mux tree, one address bit per layer:

flowchart TB
    E0["rf[0].data"] --> M0["2:1 mux<br/>addr bit 0"]
    E1["rf[1].data"] --> M0
    E2["rf[2].data"] --> M1["2:1 mux<br/>addr bit 0"]
    E3["rf[3].data"] --> M1
    M0 --> T["2:1 mux<br/>addr bit 1"]
    M1 --> T
    T --> OUT["out (8b wire)"]

Wrap the selector in oh(...) to mark it as one-hot: one select bit per element of the dimension, instead of a binary-encoded address.

self.rf = RfEntry(HwComponentType.REG, (4,), "rf")
self.osel = reg(4) # 4-bit one-hot for 4 elements
got = self.rf[oh(self.osel)].data # element i selected when osel[i] is high

oh(sig) is just a marker (OneHot) — it does not build hardware by itself. Use it when your selector is already one-hot (grant vectors, match lines) and you want to skip the binary decode.

The two selector forms address the same 4 elements differently:

flowchart LR
    subgraph BIN["binary: sel = reg(2)"]
      BA["2-bit address"] --> BD["decode into<br/>2:1 mux tree"]
    end
    subgraph OH["one-hot: osel = reg(4)"]
      OA["4-bit select<br/>one bit per element"] --> OD["direct select,<br/>no binary decode"]
    end

Static and dynamic indices combine freely across dimensions. Pinning a dimension with an int shrinks the mux to just that row/column:

class Cell(Karray):
v = kaf(1)
d = kaf(6)
self.grid = Cell(HwComponentType.REG, (3, 4), "grid")
self.sel = reg(2)
got = self.grid[2][self.sel].d # row 2 static, column dynamic -> 4:1 mux

The result is a single 6-bit wire selecting among grid[2][0..3].d only.

Two restrictions:

  • A range slice cannot mix with dynamic indexingd[0:2][sel] is a TypeError. Slices exist only for static karray-to-karray regions.
  • A dynamic read must land on a field (...[sel].data), never a whole element.

Dynamic reads build hardware — do them in scope

Section titled “Dynamic reads build hardware — do them in scope”

A dynamic read materializes mux hardware the moment the reference is resolved, so it must happen inside a module scope — an @init or @flow method — like any other hardware construction. Using the result as an assignment source inside a flow block (as in the examples above) is the normal pattern.

The example tc29_karray_dynamic_index fills a 4-entry register file with known data, then reads it back through both decodes:

with seq():
# fill every element with its known (valid, data)
for i in range(4):
self.rf[i].valid |= self.c_v
self.rf[i].data |= self.c_d[i]
# binary dynamic read of each address (rf[bsel_i].data == DATA[i])
for i in range(4):
self.o_d[i] |= self.rf[self.bsel[i]].data
# one-hot dynamic read (osel = 1 << 2 -> element 2)
self.o_od |= self.rf[oh(self.osel)].data

Every address, plus a one-hot select with a non-trivial bit set, is verified end-to-end in simulation. See the Examples Gallery for the full file.

Writing through a dynamic index is covered next, in Dynamic Writes.