testbench-gen · git:20261014.972668c · 2026-10-14 · sha256 a3e54ebe02c32a60

testbench-gen git:20261014.972668cA

Immutable. This exact content never changes and is served at /api/v1/blob/a3e54ebe02c32a60.

---
name: testbench-gen
description: "Generate a PROFESSIONAL, high-coverage testbench for a design directly from its Phase-1 L-docs — the Phase-2 'Simulation' step (canonical flow Step 4). Use whenever a design needs functional verification: derive the interface (L1/L9), clock/reset (L8/L9), a reference model, a scoreboard, functional coverage, and assertions, then run cocotb. Program-first: the deterministic generator `professional_tb_gen.py` (MCP `eda_professional_tb`) does the derivation; this skill is the judgment layer for filling a non-closed-form reference model and reviewing coverage. Triggers on: 'write a testbench', 'generate testbench', 'verify this design', 'functional verification', 'coverage', '幫我寫 testbench', '產生 testbench', after Phase-2 RTL is authored and before Phase-3."
---

# testbench-gen — deterministic professional testbench generation

The canonical flow's **Step 4 (Simulation)** derives a professional, high-coverage
testbench from the Phase-1 layers — never a hand-waved skeleton. The load-bearing
work is a PROGRAM: `programs/professional_tb_gen.py` (exposed as MCP tool
`eda_professional_tb`). This skill is the judgment layer around it.

## What the program does (program-first)

`professional_tb_gen.py <project>` reads the project's `phase1/generated_docs/L*.json`
and emits, under `phase2/stage1/sim_professional/<top>/`:

- `tb_<top>.py` — a **cocotb** testbench: clock/reset from L8/L9, a reference
  model, a SCOREBOARD, functional coverage (cocotb-coverage covergroups), and a
  directed-corner + constrained-random stimulus loop.
- `<top>_coverage_model.json` (an **L28** coverage model), `<top>_assertions.sva`
  (an **L29** assertion set), a cocotb `Makefile`, and `verification_plan.json`.

It **combines open-source** frameworks rather than re-inventing them: **cocotb**
(TB base) + **cocotb-coverage** (functional coverage + constrained-random) +
**Verilator/Icarus** (sim) + an **SVA bind** for Verilator/SymbiYosys. Add
`cocotb-coverage` to the runtime (see mcp-eda/INSTALL_GUIDE.md).

## The reference-model strategy — 3 tiers (honest, never a vacuous pass)

1. **Closed-form** (arithmetic primitives `c = a OP b`): reuses
   `arith_oracle_tb_gen.compute_golden` — the single source of truth.
2. **Bounded-latency streaming scoreboard** (serial datapaths — one parallel
   operand + a 1-bit serial operand + a 1-bit serial result, e.g. a bit-serial
   multiplier): the scoreboard AUTO-DERIVES the output `(latency, bit_order)`
   from a calibration vector, then checks every vector against the closed-form
   reference. **This closes the serial-datapath DEFER** that `arith_oracle_tb_gen`
   punts on entirely (proven: spm bit-serial multiplier, 208/208 vs
   `(x*y) mod 2^N`, `order=lsb latency=1` auto-derived).
3. **Reference hook** (classes with no closed form — CPU/SoC/DSP): the generated
   TB emits a clearly-marked `reference_model` hook that **`TestSkip`s until
   filled** — NEVER a silent vacuous pass. The judgment layer (this skill /
   spec-to-refmodel) fills it from L10 vectors or a spec-derived model, with the
   program-vs-AI cross-check (dual-track convergence).

## When YOU (the judgment layer) act

- **Fill a Tier-3 reference model**: read the L-docs (L2 function, L3 protocol,
  L10 vectors) and author the Python reference the hook needs — then re-run so
  the scoreboard is real. This is the only place an LLM is needed; the interface,
  clock/reset, coverage bins, and (for Tier-1/2) the reference are all derived by
  the program.

  **WRITE IT TO `expert_reference_tb.py`, NOT TO `tb_<top>.py`.**

  ```
  phase2/stage1/sim_professional/<top>/expert_reference_tb.py
  ```

  `tb_<top>.py` is REGENERATED by `professional_tb_gen` on every Step-4 run, so
  an oracle written there is deleted by the very re-run the gate asks for —
  `cpu_functional_oracle_waiver_check` says "fill … with the testbench-gen
  expert fallback and re-run Step 4", and that loop has no fixed point if you
  edit the generated file. The sibling `expert_reference_tb.py` is deliberately
  stored apart so regeneration cannot erase your work;
  `professional_tb_gen._load_expert_reference_tb` reads it and promotes the run
  to `dut_kind=expert_reference`, which is what makes the Phase-2 runner
  actually launch cocotb for an otherwise-generic class.

  For the loader to ACCEPT it (it rejects placeholder shapes before a simulator
  is launched), the file must:

  | requirement | why |
  |---|---|
  | a `@cocotb.test()` coroutine that OBSERVES `dut` and carries a real `assert` | a testbench that never reads the DUT is not an oracle |
  | print `PROFESSIONAL_TB PASS` from a passing check | the marker the Phase-2 runner greps for |
  | contain no `TestSkip` | a skipped hook is the unfilled state |

  The JUnit is still the authority: the run is credited only on
  `tests>0, passed>0, failures==0, errors==0`. The marker never credits a case
  on its own.

  *Measured (2026-09-02, a subservient × gf180mcuD acceptance run): the whole
  convention was named in exactly two files — `professional_tb_gen.py` and its
  own test. No gate message, no skill and no doc mentioned it, so an agent
  following the gate's own instruction wrote a full 7-oracle testbench into
  `tb_<top>.py` and watched the next Step-4 run delete it.*
- **Review coverage**: after a run, read `coverage_<top>.xml` + the L28 model;
  if functional bins are unhit, add directed vectors or tighten the
  constrained-random constraints. Loop until the closure policy (functional bins
  100% + code coverage targets) is met.
- **Formalise assertions**: the L29 SVA carries reset→known-state + protocol
  handshake properties deterministically; L16 must/shall prose is emitted as
  `TODO(spec-to-assertion)` stubs for you to formalise (never fabricated as a
  passing SVA).

### Skill: Software-visible field profiles for reference models

**Pattern**: An instruction extension or interface selection is treated as proof of every software-readable or writable field, so a reference model confuses internal saved state with the selected software interface.

**When to apply**: Authoring a Tier-3 model around an implementation that exposes a documented subset of architectural or interface state.

**What to do**: First record the declared scope, selected IP revision and pinned public capability sources, then distinguish software read masks, write permissions and side effects from internal state. Compare only source-supported fields while preserving transaction widths, timing, full byte commits and unknown-value checks. Check an internal saved state through its specified observable effect when software readback is not promised. An unsupported or ambiguous requirement remains `NOT_MEASURED` and cannot be silently removed from required acceptance. If a newly reviewed profile narrows an earlier oracle, preserve the old oracle and counterexample and label the new scope explicitly; never choose masks or goldens from DUT traces. Retain positive, fault-specific negative and byte-identical reverse controls.

**Worked pattern** (anonymized): An interrupt implementation may retain a saved enable internally for return behavior while exposing only the current enable through software reads. Test the guaranteed read field and the specified return effect independently; do not invent read or write access to the saved field. A second eventual interrupt alone does not prove that pending state survived masking: use a discriminating countercontrol or keep that property unmeasured.

**Why this is GENERAL**: The same distinction applies to processor status fields, peripheral register views and protocol capability subsets. Instruction or interface membership, internal state and software access are separate claims; actual native/JUnit evidence remains scoped to the tested fields and effects.

_Captured by benchmark-enhancement-capture 2026-10-15._

## Optional reset-retention byte stimulus

When a retention test needs a designer-chosen memory stimulus, declare
`verification_stimuli.reset_retention.memory_preload` in the project's
`plugin_output/declaration.json` through `spec_declaration_emit --from-json`
or `--set`. This verification choice is separate from the SRAM timing contract.
It is a non-empty list of `{"address": 0, "byte": 165}` entries: exactly those
two keys, unique addresses within the selected memory depth, and integer byte
values 0..255. Boolean, string and floating-point values are refused.

`testbench_gen.emit_unit_tbs` applies the bytes after initializing every memory
location and before observing either reset arm. It preserves every rising-edge
write, the pending first-edge check, all-byte comparisons and the 64-cycle
activity budget. Absent preload keeps the stock seed; malformed preload cannot
credit actual-byte retention. A chosen stimulus alone is never execution
evidence: absent non-idempotent pending activity remains NOT_MEASURED.

## Declared serial arithmetic scenarios

The existing unit producer can generate operand-boundary, consecutive-transaction
and synchronous-reset scenarios from the case's own L10 scope. It requires the
selected serial function, bit order and settled observation offset, plus
`transaction_rearm`, `rearm_reset_cycles` and
`parallel_hold=through_final_product_bit`. Supported rearm is `clocked_reset`,
or `continuous` when the settled offset is zero. Unmodeled input roles, clock
domains, reset styles and ambiguous scopes remain unmeasured.

The scenario oracle uses independent modulo goldens at one declared window; it
does not search DUT alignment to choose an expected answer. It exercises all
selected corner pairs, the stated transaction population, early/middle/late
interruptions with single/multiple reset holds, and between-edge reset pulses.
A row listing multiple supported families executes each family in the same
native case, with separate actual populations and an aggregate comparison total.
Generated scenario credit requires the current native comparison population
and unchanged input, DUT, declaration, TB and transcript identities. Generic
generated arithmetic remains insufficient for scenario credit. Authored and
delivered testbenches retain their original priority.

`latency_cycles_origin=settled_edge_offset` is the normal CR-5 convention.
Explicit legacy `pre_nba_sample_offset` is normalized once. Native manifests
carry their origin; a bare measured integer is not origin evidence. Neither
convention relaxes an incorrect declared window.

## Capture

Any recovery here (a new reference-model pattern, a new coverage-derivation rule,
a streaming-alignment case) is captured per `benchmark-enhancement-capture`:
program-first into `professional_tb_gen.py` (a deterministic derivation rule) so
the next design gets it automatically — and, if the gap is in a forked sim tool
(Verilator/Icarus/cocotb), as a **Bucket-T** forked-tool item.

## Gaps to grow into (from the 2026-07-11 verification research)

- Fill the currently-empty **L22 verification plan** fields (coverage_goals /
  formal_properties / regression_matrix) and wire them to L28/L29.
- Add **L30 stimulus/constraints** (constrained-random weights, sequences).
- Widen code coverage to FSM-transition / expression / condition (Verilator
  `--coverage-user`) and gate sign-off on it.
- pyuvm structural env + riscv-dv for CPU-class constrained-random.

## Compliance gate (mandatory)

After producing your output, save it to a file and run:

```bash
python3 plugins/vibe-ic/_shared/skill_compliance_check.py \
    --requirements plugins/vibe-ic/skills/testbench-gen/compliance.yaml \
    <your_output_file>
```

Exit 0 = PASS, exit 1 = FAIL with specific missing elements listed.
`compliance.yaml` in the corresponding skill directory enumerates
every required element of your output: section headers, metadata fields,
handoff lines, tool invocations.

**Your task is not complete until the audit returns PASS.** Missing
elements are the single largest source of skill-execution non-determinism
across different agents.