How to add an operator tile¶
This guide takes a new streaming operator from an idea to a registered core that a composition can instantiate. It follows the project’s order, golden, HDL, bench, registry entry, envelope, because each stage is the acceptance test for the next, and skipping one moves the failure later, where it costs more.
You will need verilator for the bench, and access to a synthesis host for
the final envelope step.
1. Write the golden first¶
The golden is a pure function in dau_core/operators.py (or
dau_core/temporal.py for time-series shapes) that computes what the tile
computes. It is the definition of the tile’s meaning, so write it before
any HDL exists and settle the semantics there.
Decide these explicitly, since they are the parts prose gets wrong:
The domain. Fields are fixed-width, null-free wires, and the width is a tile parameter, not part of the tile’s identity. Wrap at the declared width so
-1and the all-ones pattern are the same key.Do not put a width in the name. Part of the existing palette is still called
int32-*because it was written before the width was parameterized, and the name becomes wrong as soon as the tile gains the parameter. The row predicate filter’sFIELD_WIDTHaccepts 16, 32 and 64, soint32-would have been false for two of its three legal settings, which is why it is nowrow-predicate-filter.WIDTH_NAMED_TILESindau_core/tests/test_cores.pylists the tiles that still have a width in their name, each with a comment saying why and what would remove it. A few keep the width on purpose because for them it is part of the contract. A new tile is not one of them: give it a width in its name and that guard fails.Emission order. If your tile holds state, say exactly what order results leave in, and pick something implementation-independent.
Error codes. Add an
IntEnumwhose values are the tile’sstatus_error_codewire encoding, and say so in its docstring.Capacity behavior. A bounded table that fills is an error, never an eviction. State what the caller gets back.
Return (results, error_code) when the tile can fail.
2. Prove the golden against itself¶
Add dau_core/tests/test_<name>.py with property tests, not just examples:
permutation invariance if order should not matter, agreement with an
existing golden where the two overlap, boundary values, and the declared-width wire
identity. If your tile has a host-side counterpart (a merge, a split), prove
the algebra: merge(split(rows)) == one_shot(rows).
Run it:
python -m pytest dau_core/tests/test_<name>.py -q
3. Write the HDL¶
Put dau_<name>.sv in dau_core/hdl/ and export it from
dau_core/hdl/__init__.py. Name it for what it computes. Declare a
FIELD_WIDTH (or equivalent) parameter for the width and let the
composition choose it. Follow the palette’s conventions:
input_valid/input_ready/input_data/input_lastin,output_*out, plus thestatus_*close-out channel.Exactly one close-out per batch; silent on success mid-chain, terminal closes out.
Latch configuration on a batch’s first beat.
If you hold an array in BRAM, access it from a dedicated reset-free process and keep valid bits in a separate narrow array. An array touched by an FSM process with a reset infers distributed RAM instead.
Mirror the golden’s error-code values as localparams.
Lint before simulating:
verilator --lint-only -Wall --timing dau_core/hdl/dau_<name>.sv
4. Write the cocotb bench¶
Add dau_core/tests/cocotb_benches/dau_<name>_tb.py. It drives the HDL
and compares its output with the golden’s over the same inputs. It uses no
hand-written expected values.
Cover, at minimum: the nominal case, backpressure (a stalled beat must stay asserted and byte-stable), reset, repeated batches back to back, malformed input, and every error path. If the tile is stateful, add the randomized-delay patience harness: inject random delays on every channel and assert the emitted sequence is identical across seeds.
If the tile is parameterized, run the bench at its boundaries: the declared minimum, a small value that forces collisions or short scans, and the default. Cases whose point is holding several items at once should skip at parameter values too small for them rather than being weakened to fit.
Wire it into dau_core/tests/test_hdl_cocotb.py with one runner per
parameter value, then:
python -m pytest dau_core/tests/test_hdl_cocotb.py -q -k <name>
5. Add the registry entry¶
Append a CoreDefinition to dau_core/config/dau-core/cores.yaml; the
registry is that yaml file, with no Python list beside it. Declare:
name, kind: operator, module, source, a description that says what
the tile does and what its contracts are, golden (as
module:function), bench, count_port, contract_kind, record_words,
success_close_out, and parameters mirroring your RTL elaboration guards.
If the tile advertises capabilities, add opcodes, opcode_input_types,
and unit_kind. A composition built only from tiles that declare none
reports an empty opcode bitmap, and the planner cannot see it.
Declare ingest as the rate the tile sustains: state the assumption your rate depends on and
name the adversarial case rather than padding the number. If the tile owns
on-chip storage, declare storage_intent.
Check it loads:
python -c "from dau_core.registry import core; print(core('<name>'))"
6. Measure the envelope¶
Resource numbers come from the dau-build synthesize-cores task, never
from a hand-run synthesis:
dau-build task=tasks/build/synthesize-cores \
'model.cores=[/dau-core/<name>]' \
model.output_root=./ooc model.part=<part> \
'+model.parameters={<name>: {PARAM: <value>}}' \
model.execute=true
Run it for every parameter value the catalog can request, on every
supported part, and add the results as resources: measurement points
keyed by part, clock_ns, and params. Set the entry’s measured_from
to the digest the task reports for the core. A guard fails on a
synthesizable core with no envelope, and another on an envelope with no
measured_from, so the suite tells you while either is still missing. See
how to measure a resource envelope for the
full procedure.
Read a thin margin as a warning. An out-of-context margin under about half a nanosecond is a risk for the composed build, not headroom, so budget a retime before composing the tile.
7. Confirm the whole suite¶
python -m pytest dau_core -q
ruff check dau_core/ && ruff format dau_core/
The tile is now instantiable by any ScanCompositionSpec that names it.