Indexing
Kathryn has one Karray index type, and every access — read, write, and
karray-to-karray copy — uses it. Each [] hop selects one dimension, and
every kind collapses that dimension to a single element: a selection always
names exactly one element, and every dimension must be indexed.
| Index | Kind | Meaning |
|---|---|---|
d[3] | static | picks one element of the dimension at build time |
d[sig] | dynamic | a runtime binary-encoded address |
d[fn] | custom fn | a user function; splits by direction — see below |
The custom-fn kind is the same syntax on both sides of an assignment, but it means different hardware depending on which side it lands on:
flowchart TB
FN["d[fn] — a callable index"] --> Q{"which side of the statement?"}
Q -->|"write destination"| WE["fn(i) -> 1-bit enable<br/>called once per index<br/>write lands where the enable is high"]
Q -->|"read / source"| RD["fn(a, b, level) -> pick-a<br/>called per 2:1 node<br/>the dim folds through a REDUCE tree"]
Write enables are covered in Dynamic Writes; the read-side fold has its own page, Reduce. This page covers static indexing, the dynamic binary address, and how the kinds mix.
Static indexing
Section titled “Static indexing”Plain int keys pin an element at build time. This is the form
Karray Basics uses throughout, and it costs no
hardware at all — the selection resolves straight to the backing components:
self.grid = Cell(HwComponentType.REG, (3, 4), "grid")
self.grid[2][1].d # element (2, 1)'s d field — a plain registerself.grid[2][1] |= {"v": self.a, "d": self.b}Static indices are bounds-checked against the shape; grid[3][0] on a 3-row
array is a ValueError. A bool is never accepted as an index, even though
Python treats it as an int.
Dynamic binary read
Section titled “Dynamic binary read”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].dataThe 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, and each
layer switches on one bit of the address — the inverted bit picks the left
child:
reg [7:0] WIRE_KARRAY_rf_6389_D0L0N0F0_DMUX_6454;
always @(*) begin WIRE_KARRAY_rf_6389_D0L0N0F0_DMUX_6454[7:0] <= VAL_..._DEFAULT_ZERO_6776[7:0]; if (EXPR_VAL_bsel0_6396_B0_N_6453) begin // address bit 0 low WIRE_KARRAY_rf_6389_D0L0N0F0_DMUX_6454[7:0] <= REG_rf_E0_data_6382[7:0]; end else begin WIRE_KARRAY_rf_6389_D0L0N0F0_DMUX_6454[7:0] <= REG_rf_E1_data_6384[7:0]; endendThe mux wires are named WIRE_KARRAY_<array>_D<dim>L<level>N<node>F<field>_DMUX_<id>
— dimension, tree level, node within the level, and field index — so a tree is
easy to follow in a waveform.
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)"]
Mixing kinds across dimensions
Section titled “Mixing kinds across dimensions”Every dimension gets its own kind, and they combine freely. Pinning a dimension with an int shrinks the tree to just that row or 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 muxThe result is a single 6-bit wire selecting among grid[2][0..3].d only.
A single statement may mix all three kinds, on both sides — the destination’s runtime dimensions become write enables while the source’s become mux and reduce trees:
# a: shape (2, 3, 2, 3); b: shape (2, 3, 2); element {data: 8}self.a[1][en_fn][1][self.aw] |= self.b[max_fn][self.br][1]# ^ ^ ^ ^ ^ ^ ^# static | static | reduce | static# custom enable dyn (binary) dyn (binary)This is tc35_karray_mixed_k2k, which verifies exactly one destination element
takes the value and that the guarded neighbours keep theirs.
A dynamic read must land on a field
Section titled “A dynamic read must land on a field”A dynamic read resolves to one field’s hardware:
self.rf[self.sel].data # OK — a field of the selected elementself.rf[self.sel] # not a readable value on its ownReading a whole element raises a TypeError (“read a specific field
(d[i][j].field), not a whole Karray element”). The one place a whole element
is a legal source is a karray-to-karray assignment, where the destination
supplies the field names — see
Conversion & Resize.
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.
Worked example: tc30
Section titled “Worked example: tc30”tc30_karray_dynamic_index fills a 4-entry register file with known data, then
reads it back through both runtime forms:
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
# read-side custom fn -> reduce fold: the max element's data self.o_max |= self.rf[lambda a, b, l: a.fields["data"] >= b.fields["data"]].dataEvery address, including bsel = 2 (a non-zero MSB), is verified end to end in
simulation. See the Examples Gallery for the
full file.
Writing through a runtime index is covered next, in Dynamic Writes.
© 2026 Tanawin Devaveja. All rights reserved. The canonical version of this documentation is published at kathryn-tools.org.