Skip to main content

logicaffeine_compile/codegen_sva/
qor_harness.rs

1//! qor_harness — the measurement harness of the comparative-synthesis bench.
2//!
3//! Every candidate implementation of a kernel (LOGOS's own RTL, an HLS tool's output, a third-party
4//! design) is measured inside the same generated wrapper, `qorbist_<candidate>(clk, rst, sig)`:
5//!
6//! * **Stimulus.** A 64-bit xorshift state (13, 7, 17) seeded with [`SEED`] advances once per
7//!   operation the candidate *accepts*. Each input field reads its own lane word
8//!   ([`lane_word`]: a rotation of the state XOR a per-lane constant) and shapes it into the
9//!   kernel's declared domain ([`Field`]). Values never depend on the candidate's timing, so every
10//!   candidate of a kernel sees the same operand sequence.
11//! * **Signature.** Each result the candidate *produces* folds into a 64-bit MISR
12//!   ([`misr_step`] over [`fold`]). The MISR advances on result-valid only, so the signature is
13//!   independent of latency, initiation interval and stalls: two candidates of one kernel are
14//!   functionally equal on the stimulus exactly when their signatures are, and both must equal the
15//!   golden [`signature`] the LOGOS VM (or the oracle the RTL generator is locked to) predicts.
16//! * **Boundary.** The candidate is instantiated under `(* keep_hierarchy *)` as `u_dut`, so
17//!   synthesis cannot fold the shaped stimulus into it and its area and power are reported for that
18//!   instance alone. An input its binding does not name is held at 0, never left floating. The wrapper exposes only `clk`, `rst` and a 16-bit `sig`, which is what the
19//!   gate-level power flow drives.
20//!
21//! A kernel is a [`Kernel`] contract (ports, widths, domains, golden); a candidate is a
22//! [`Candidate`] (top module, clock, reset, [`Protocol`], and a port binding). [`check_ports`]
23//! refuses a binding that does not fit the DUT's real ports before any harness is emitted.
24
25use crate::codegen_sva::decision_workload::{DECIDE_FN, DECIDE_NAME};
26use crate::codegen_sva::fabric_pipe::{
27    carry_save_lower_with, decision_ops, emit_fabric_pipelined_verilog, horner_seq_pipeline, schedule_pipeline, FinalAdd,
28    Inputs, PipelineSchedule, SumSharing, DECISION_CELLS_BUDGET, DECISION_CHAIN_BUDGET,
29};
30use crate::codegen_sva::select_hoist::hoist_selects;
31use crate::codegen_sva::fabric_rtl::{
32    decision_fabric_from_source, emit_decision_fabric_verilog, emit_fabric_verilog, emit_seq_datapath_verilog,
33    residual_from_source, DecisionFabric, FabricOp, POLY_BODY,
34};
35use crate::codegen_sva::fabric_rtl::AluOp;
36use crate::codegen_sva::gpu_simt_rtl::{emit_gpu_simt_verilog, load_slots, LaneArith, SimtEngine, Slot};
37use crate::codegen_sva::mul_tree::mul_tree_levels;
38use crate::codegen_sva::simt_ports::emit_simt_ports_verilog;
39use crate::codegen_sva::keccak_rtl::{
40    emit_keccak_core_half_verilog, emit_keccak_core_verilog, emit_ntt_butterfly_pipelined_verilog, emit_ntt_butterfly_verilog,
41    fqmul, NttCuts,
42};
43use crate::codegen_sva::systolic::{
44    emit_systolic_os_streaming_verilog, emit_systolic_pipelined_loadable_verilog, os_streaming_result_offset,
45    pipelined_latency, SystolicOs,
46};
47
48/// The stimulus state after reset.
49pub const SEED: u64 = 0x1234_5678_9ABC_DEF1;
50
51/// The per-lane constant multiplier: lane `k` XORs `k · LANE_MIX` into its rotated state.
52pub const LANE_MIX: u64 = 0x9E37_79B9_7F4A_7C15;
53
54/// One step of the stimulus generator: xorshift64 with shifts 13, 7, 17.
55pub fn next_state(s: u64) -> u64 {
56    let mut x = s;
57    x ^= x << 13;
58    x ^= x >> 7;
59    x ^= x << 17;
60    x
61}
62
63/// The rotation lane `k` applies to the state.
64fn lane_rotation(k: usize) -> u32 {
65    ((11 * k) % 64) as u32
66}
67
68/// The stimulus state `n` steps after `s`.
69pub fn advance(s: u64, n: u32) -> u64 {
70    (0..n).fold(s, |s, _| next_state(s))
71}
72
73/// The 64×64 GF(2) matrix of `n` stimulus steps as rows: bit `i` of [`advance`]`(s, n)` is the parity
74/// of `s & rows[i]` (xorshift is linear, so `n` steps are one matrix).
75pub fn jump_rows(n: u32) -> [u64; 64] {
76    let cols: Vec<u64> = (0..64).map(|k| advance(1u64 << k, n)).collect();
77    let mut rows = [0u64; 64];
78    for (i, row) in rows.iter_mut().enumerate() {
79        for (k, col) in cols.iter().enumerate() {
80            if (col >> i) & 1 == 1 {
81                *row |= 1u64 << k;
82            }
83        }
84    }
85    rows
86}
87
88/// A contract reference `port` or `port@j`: the port and the batch slot (operation `j` of the batch
89/// a batched candidate takes per call).
90pub fn contract_ref(r: &str) -> (&str, u32) {
91    match r.split_once('@') {
92        Some((p, j)) => (p, j.parse().unwrap_or(u32::MAX)),
93        None => (r, 0),
94    }
95}
96
97/// The 64-bit word input lane `k` reads from state `s`.
98pub fn lane_word(s: u64, k: usize) -> u64 {
99    s.rotate_left(lane_rotation(k)) ^ (k as u64).wrapping_mul(LANE_MIX)
100}
101
102/// Which values a field draws: uniformly over its domain, or only its two ends.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum Mode {
105    /// Every value of the domain (the slice of the lane word, offset by `base`).
106    Shaped,
107    /// Only the domain's minimum and maximum, chosen per sub-lane by one bit of the lane word.
108    Edge,
109}
110
111/// How one 64-bit chunk of an input port is drawn from its lane word: `lanes` packed sub-lanes of
112/// `width` bits each. A single-lane field is `base + slice` (sign-extended when `signed`) as a
113/// 64-bit value; a packed field (`lanes > 1`) carries no offset and packs each sub-lane's raw slice.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub struct Field {
116    pub base: i64,
117    pub width: u32,
118    pub signed: bool,
119    pub lanes: u32,
120}
121
122impl Field {
123    pub const fn unsigned(base: i64, width: u32) -> Field {
124        Field { base, width, signed: false, lanes: 1 }
125    }
126
127    pub const fn signed(width: u32) -> Field {
128        Field { base: 0, width, signed: true, lanes: 1 }
129    }
130
131    pub const fn full() -> Field {
132        Field { base: 0, width: 64, signed: false, lanes: 1 }
133    }
134
135    pub const fn packed(width: u32, lanes: u32, signed: bool) -> Field {
136        Field { base: 0, width, signed, lanes }
137    }
138
139    fn slice_bounds(&self) -> (i128, i128) {
140        let w = self.width;
141        if self.signed {
142            (-(1i128 << (w - 1)), (1i128 << (w - 1)) - 1)
143        } else {
144            (0, (1i128 << w) - 1)
145        }
146    }
147
148    /// The smallest and largest value one sub-lane takes.
149    pub fn range(&self) -> (i128, i128) {
150        let (lo, hi) = self.slice_bounds();
151        (self.base as i128 + lo, self.base as i128 + hi)
152    }
153
154    fn mask(&self) -> u64 {
155        if self.width >= 64 {
156            u64::MAX
157        } else {
158            (1u64 << self.width) - 1
159        }
160    }
161
162    fn slice(&self, word: u64, j: u32, mode: Mode) -> i128 {
163        let (lo, hi) = self.slice_bounds();
164        match mode {
165            Mode::Edge => {
166                if (word >> j) & 1 == 1 {
167                    hi
168                } else {
169                    lo
170                }
171            }
172            Mode::Shaped => {
173                let raw = if self.width >= 64 {
174                    word
175                } else {
176                    (word >> (j * self.width)) & self.mask()
177                };
178                if self.signed && self.width < 64 && (raw >> (self.width - 1)) & 1 == 1 {
179                    raw as i128 - (1i128 << self.width)
180                } else if self.signed && self.width == 64 {
181                    raw as i64 as i128
182                } else {
183                    raw as i128
184                }
185            }
186        }
187    }
188
189    /// The 64-bit chunk this field draws from `word`.
190    pub fn value(&self, word: u64, mode: Mode) -> u64 {
191        if self.lanes == 1 {
192            return (self.base as i128 + self.slice(word, 0, mode)) as u64;
193        }
194        (0..self.lanes).fold(0u64, |acc, j| {
195            acc | (((self.slice(word, j, mode) as u64) & self.mask()) << (j * self.width))
196        })
197    }
198}
199
200/// An input port of a kernel contract: `bits` wide, one [`Field`] per 64-bit chunk (least
201/// significant first). A port with `hold > 1` (a power of two) keeps the value drawn at its first
202/// operation for `hold` operations — a weight tile reused across rows.
203#[derive(Clone, Debug, PartialEq)]
204pub struct InPort {
205    pub name: &'static str,
206    pub bits: u32,
207    pub fields: Vec<Field>,
208    pub hold: u32,
209}
210
211/// An output port of a kernel contract.
212#[derive(Clone, Debug, PartialEq)]
213pub struct OutPort {
214    pub name: &'static str,
215    pub bits: u32,
216}
217
218/// Where a kernel's LOGOS candidate comes from: compiled from a LOGOS program by the compiler, or
219/// written by one of the RTL generators in this module tree.
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub enum SourceKind {
222    CompiledFromLg,
223    RtlGenerator,
224}
225
226impl SourceKind {
227    pub fn label(&self) -> &'static str {
228        match self {
229            SourceKind::CompiledFromLg => "compiled-from-lg",
230            SourceKind::RtlGenerator => "rtl-generator",
231        }
232    }
233}
234
235/// One operation's values: per port, per 64-bit chunk.
236pub type Words = Vec<Vec<u64>>;
237
238/// The reference a kernel's results are checked against.
239#[derive(Clone, Copy)]
240pub enum Golden {
241    /// The LOGOS function `func` declared by `program()`, run on the VM: every input port is one
242    /// `Int` argument and the single output port is its result.
243    Vm { program: fn() -> String, func: &'static str },
244    /// The VM computes each lane of a SIMT step with `func`; the engine's mask and cross-lane sum
245    /// are composed over those lane results.
246    VmLanes { program: fn() -> String, func: &'static str, lanes: usize },
247    /// The Rust oracle the kernel's RTL generator is locked against.
248    Oracle(fn(&Words) -> Words),
249}
250
251impl std::fmt::Debug for Golden {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        match self {
254            Golden::Vm { func, .. } => write!(f, "Vm({func})"),
255            Golden::VmLanes { func, lanes, .. } => write!(f, "VmLanes({func} × {lanes})"),
256            Golden::Oracle(_) => write!(f, "Oracle"),
257        }
258    }
259}
260
261/// A kernel of the bench: its contract and its golden.
262#[derive(Clone, Debug)]
263pub struct Kernel {
264    pub id: &'static str,
265    pub name: &'static str,
266    pub summary: &'static str,
267    pub source_kind: SourceKind,
268    pub inputs: Vec<InPort>,
269    pub outputs: Vec<OutPort>,
270    pub golden: Golden,
271}
272
273fn port(name: &'static str, bits: u32, field: Field) -> InPort {
274    InPort { name, bits, fields: vec![field], hold: 1 }
275}
276
277fn out(name: &'static str, bits: u32) -> OutPort {
278    OutPort { name, bits }
279}
280
281fn decide_program() -> String {
282    DECIDE_FN.to_string()
283}
284
285fn horner_program() -> String {
286    format!("## To poly (x: Int) -> Int:\n    Return {POLY_BODY}.\n")
287}
288
289fn shade_program() -> String {
290    "## To shade (a: Int, x: Int, b: Int) -> Int:\n    Return a * x + b.\n".to_string()
291}
292
293fn keccak_oracle(op: &Words) -> Words {
294    let mut st = [0u64; 25];
295    st.copy_from_slice(&op[0][..25]);
296    logicaffeine_system::keccak::keccak_f1600(&mut st);
297    vec![st.to_vec()]
298}
299
300fn butterfly_oracle(op: &Words) -> Words {
301    let (a, b, zeta) = (op[0][0] as i16, op[1][0] as i16, op[2][0] as i16);
302    let t = fqmul(zeta, b);
303    vec![
304        vec![a.wrapping_add(t) as u16 as u64],
305        vec![a.wrapping_sub(t) as u16 as u64],
306    ]
307}
308
309/// GEMM tile rows and columns, and the operand width.
310pub const GEMM_DIM: usize = 8;
311pub const GEMM_BITS: u32 = 8;
312/// The accumulator width every GEMM candidate's output carries.
313pub const GEMM_ACC_BITS: u32 = 32;
314/// The narrower accumulator Gemmini's default configuration gives its mesh (`spatialArrayOutputType`
315/// SInt(20)): wide enough for every sum of eight int8 products, so the matched rows compute the same
316/// results with less state.
317pub const GEMM_MATCHED_ACC_BITS: u32 = 20;
318
319fn acc_name(base: &str, acc_bits: u32) -> String {
320    if acc_bits == GEMM_ACC_BITS {
321        base.to_string()
322    } else {
323        format!("{base}_acc{acc_bits}")
324    }
325}
326
327fn gemm_oracle(op: &Words) -> Words {
328    let byte = |w: &[u64], i: usize| ((w[i / 8] >> (8 * (i % 8))) & 0xff) as u8 as i8 as i64;
329    let mut o = vec![0u64; GEMM_DIM * GEMM_ACC_BITS as usize / 64];
330    for c in 0..GEMM_DIM {
331        let acc: i64 = (0..GEMM_DIM)
332            .map(|r| byte(&op[0], r) * byte(&op[1], r * GEMM_DIM + c))
333            .sum();
334        o[c / 2] |= (acc as i32 as u32 as u64) << (32 * (c % 2));
335    }
336    vec![o]
337}
338
339/// SIMT lanes of the K6 engine.
340pub const SIMT_LANES: usize = 4;
341
342/// The kernels of the bench, in id order.
343pub fn kernels() -> Vec<Kernel> {
344    vec![
345        Kernel {
346            id: "K1",
347            name: "decide",
348            summary: "HFT decision: microprice market-making, fat-finger clamp, position risk gate; 8 Int in, 1 Int out",
349            source_kind: SourceKind::CompiledFromLg,
350            inputs: vec![
351                port("micro100", 64, Field::unsigned(10_000, 7)),
352                port("fair100", 64, Field::unsigned(10_000, 7)),
353                port("edge100", 64, Field::unsigned(0, 4)),
354                port("base_size", 64, Field::unsigned(1, 6)),
355                port("max_order", 64, Field::unsigned(10, 5)),
356                port("inventory", 64, Field::signed(8)),
357                port("pos_cap", 64, Field::unsigned(64, 7)),
358                port("book_ok", 64, Field::unsigned(0, 1)),
359            ],
360            outputs: vec![out("order", 64)],
361            golden: Golden::Vm { program: decide_program, func: DECIDE_NAME },
362        },
363        Kernel {
364            id: "K2",
365            name: "horner",
366            summary: "Degree-3 polynomial in Horner form, ((2x+5)x-3)x+100, over Int",
367            source_kind: SourceKind::CompiledFromLg,
368            inputs: vec![port("x", 64, Field::signed(21))],
369            outputs: vec![out("y", 64)],
370            golden: Golden::Vm { program: horner_program, func: "poly" },
371        },
372        Kernel {
373            id: "K3",
374            name: "gemm8",
375            summary: "8x8 int8 weight-stationary GEMM tile, int32 accumulation, weights reloaded every 8 rows",
376            source_kind: SourceKind::RtlGenerator,
377            inputs: vec![
378                port("a", 64, Field::packed(8, 8, true)),
379                InPort {
380                    name: "w",
381                    bits: (GEMM_DIM * GEMM_DIM) as u32 * GEMM_BITS,
382                    fields: vec![Field::packed(8, 8, true); GEMM_DIM],
383                    hold: GEMM_DIM as u32,
384                },
385            ],
386            outputs: vec![out("o", GEMM_DIM as u32 * GEMM_ACC_BITS)],
387            golden: Golden::Oracle(gemm_oracle),
388        },
389        Kernel {
390            id: "K4",
391            name: "keccak_f1600",
392            summary: "Keccak-f[1600] permutation (FIPS 202), 1600-bit state in and out",
393            source_kind: SourceKind::RtlGenerator,
394            inputs: vec![InPort {
395                name: "s_in",
396                bits: 1600,
397                fields: vec![Field::full(); 25],
398                hold: 1,
399            }],
400            outputs: vec![out("s_out", 1600)],
401            golden: Golden::Oracle(keccak_oracle),
402        },
403        Kernel {
404            id: "K5",
405            name: "ntt_bfly",
406            summary: "ML-KEM NTT butterfly: Montgomery fqmul (q = 3329) and Cooley-Tukey add/sub, 16-bit",
407            source_kind: SourceKind::RtlGenerator,
408            inputs: vec![
409                port("a", 16, Field::signed(16)),
410                port("b", 16, Field::signed(16)),
411                port("zeta", 16, Field::signed(16)),
412            ],
413            outputs: vec![out("hi", 16), out("lo", 16)],
414            golden: Golden::Oracle(butterfly_oracle),
415        },
416        Kernel {
417            id: "K6",
418            name: "simt_axpb_sum",
419            summary: "4-lane SIMT step: masked a*x+b per lane (uniform a, b; varying x) and the cross-lane sum",
420            source_kind: SourceKind::CompiledFromLg,
421            inputs: vec![
422                port("a", 64, Field::signed(31)),
423                port("b", 64, Field::signed(31)),
424                port("mask", SIMT_LANES as u32, Field::unsigned(0, SIMT_LANES as u32)),
425                port("x0", 64, Field::signed(31)),
426                port("x1", 64, Field::signed(31)),
427                port("x2", 64, Field::signed(31)),
428                port("x3", 64, Field::signed(31)),
429            ],
430            outputs: vec![out("y0", 64), out("y1", 64), out("y2", 64), out("y3", 64), out("sum", 64)],
431            golden: Golden::VmLanes { program: shade_program, func: "shade", lanes: SIMT_LANES },
432        },
433    ]
434}
435
436/// The kernel with id `id`.
437pub fn kernel(id: &str) -> Option<Kernel> {
438    kernels().into_iter().find(|k| k.id == id)
439}
440
441fn chunk_bits(bits: u32, c: usize) -> u32 {
442    bits.saturating_sub(64 * c as u32).min(64)
443}
444
445fn mask_bits(v: u64, bits: u32) -> u64 {
446    if bits >= 64 {
447        v
448    } else {
449        v & ((1u64 << bits) - 1)
450    }
451}
452
453/// The operand values of the first `ops` operations of `k` in `mode`.
454pub fn stimulus(k: &Kernel, mode: Mode, ops: usize) -> Vec<Words> {
455    let mut s = SEED;
456    let mut held: Vec<Vec<u64>> = vec![Vec::new(); k.inputs.len()];
457    let mut all = Vec::with_capacity(ops);
458    for op in 0..ops {
459        let mut lane = 0usize;
460        let mut words = Vec::with_capacity(k.inputs.len());
461        for (p, port) in k.inputs.iter().enumerate() {
462            let fresh: Vec<u64> = port
463                .fields
464                .iter()
465                .enumerate()
466                .map(|(c, f)| mask_bits(f.value(lane_word(s, lane + c), mode), chunk_bits(port.bits, c)))
467                .collect();
468            lane += port.fields.len();
469            if port.hold > 1 {
470                if op % port.hold as usize == 0 {
471                    held[p] = fresh;
472                }
473                words.push(held[p].clone());
474            } else {
475                words.push(fresh);
476            }
477        }
478        all.push(words);
479        s = next_state(s);
480    }
481    all
482}
483
484/// Why a harness, golden, or run was refused.
485#[derive(Clone, Debug, PartialEq)]
486pub enum QorError {
487    /// The binding names a DUT port the module does not have.
488    PortMissing { candidate: String, port: String },
489    /// A DUT port is narrower than the contract bits bound to it.
490    PortWidth { candidate: String, port: String, need: u32, have: u32 },
491    /// A DUT port runs the other way from its binding.
492    PortDirection { candidate: String, port: String },
493    /// Contract bits no binding drives or reads.
494    Unmapped { candidate: String, contract: String },
495    /// The protocol's control ports do not fit the DUT.
496    Protocol(String),
497    /// The golden could not be computed.
498    Golden(String),
499    /// A simulated signature went unknown (X/Z) at this operation.
500    Unknown { op: usize },
501    /// The testbench output carries no verdict.
502    Testbench(String),
503    /// The candidate cannot be built.
504    Build(String),
505}
506
507impl std::fmt::Display for QorError {
508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509        match self {
510            QorError::PortMissing { candidate, port } => {
511                write!(f, "{candidate}: the DUT has no port `{port}`")
512            }
513            QorError::PortWidth { candidate, port, need, have } => {
514                write!(f, "{candidate}: port `{port}` is {have} bits, the binding needs {need}")
515            }
516            QorError::PortDirection { candidate, port } => {
517                write!(f, "{candidate}: port `{port}` runs the wrong way for its binding")
518            }
519            QorError::Unmapped { candidate, contract } => {
520                write!(f, "{candidate}: contract port `{contract}` is not fully bound")
521            }
522            QorError::Protocol(m) | QorError::Golden(m) | QorError::Testbench(m) | QorError::Build(m) => {
523                write!(f, "{m}")
524            }
525            QorError::Unknown { op } => write!(f, "the signature went unknown at operation {op}"),
526        }
527    }
528}
529
530impl std::error::Error for QorError {}
531
532/// The VM's printed integer, reduced to its 64-bit two's-complement pattern.
533fn wrap_decimal(s: &str) -> Option<u64> {
534    let (neg, digits) = match s.strip_prefix('-') {
535        Some(d) => (true, d),
536        None => (false, s),
537    };
538    if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
539        return None;
540    }
541    let v = digits
542        .bytes()
543        .fold(0u64, |acc, b| acc.wrapping_mul(10).wrapping_add((b - b'0') as u64));
544    Some(if neg { v.wrapping_neg() } else { v })
545}
546
547/// A LOGOS integer literal argument; a negative one is written `(0 - n)`.
548fn logos_arg(v: i64) -> String {
549    if v < 0 {
550        format!("(0 - {})", (v as i128).abs())
551    } else {
552        v.to_string()
553    }
554}
555
556/// Run `func` of `program` on the VM over `calls` (each the argument list of one call), in
557/// batches of one program per 512 calls.
558fn vm_calls(program: &str, func: &str, calls: &[Vec<i64>]) -> Result<Vec<u64>, QorError> {
559    let mut out = Vec::with_capacity(calls.len());
560    for batch in calls.chunks(512) {
561        let mut src = format!("{program}\n## Main\n");
562        for args in batch {
563            let list: Vec<String> = args.iter().map(|a| logos_arg(*a)).collect();
564            src.push_str(&format!("    Show {func}({}).\n", list.join(", ")));
565        }
566        let run = crate::compile::vm_outcome(&src);
567        if let Some(e) = run.error {
568            return Err(QorError::Golden(format!("the VM refused `{func}`: {e}")));
569        }
570        let values: Vec<u64> = run
571            .output
572            .lines()
573            .map(str::trim)
574            .filter(|l| !l.is_empty())
575            .map(|l| wrap_decimal(l).ok_or_else(|| QorError::Golden(format!("`{l}` is not an Int"))))
576            .collect::<Result<_, _>>()?;
577        if values.len() != batch.len() {
578            return Err(QorError::Golden(format!(
579                "the VM printed {} results for {} calls of `{func}`",
580                values.len(),
581                batch.len()
582            )));
583        }
584        out.extend(values);
585    }
586    Ok(out)
587}
588
589/// The golden results of `k` over `stim`.
590pub fn golden(k: &Kernel, stim: &[Words]) -> Result<Vec<Words>, QorError> {
591    match k.golden {
592        Golden::Oracle(f) => Ok(stim.iter().map(f).collect()),
593        Golden::Vm { program, func } => {
594            let calls: Vec<Vec<i64>> = stim
595                .iter()
596                .map(|op| op.iter().map(|p| p[0] as i64).collect())
597                .collect();
598            let results = vm_calls(&program(), func, &calls)?;
599            Ok(results.into_iter().map(|r| vec![vec![r]]).collect())
600        }
601        Golden::VmLanes { program, func, lanes } => {
602            let mut calls = Vec::with_capacity(stim.len() * lanes);
603            for op in stim {
604                let (a, b) = (op[0][0] as i64, op[1][0] as i64);
605                for lane in 0..lanes {
606                    calls.push(vec![a, op[3 + lane][0] as i64, b]);
607                }
608            }
609            let results = vm_calls(&program(), func, &calls)?;
610            Ok(stim
611                .iter()
612                .zip(results.chunks(lanes))
613                .map(|(op, lane_out)| {
614                    let mask = op[2][0];
615                    let mut words: Words = Vec::with_capacity(lanes + 1);
616                    let mut sum = 0u64;
617                    for (lane, y) in lane_out.iter().enumerate() {
618                        let y = if (mask >> lane) & 1 == 1 { *y } else { 0 };
619                        sum = sum.wrapping_add(y);
620                        words.push(vec![y]);
621                    }
622                    words.push(vec![sum]);
623                    words
624                })
625                .collect())
626        }
627    }
628}
629
630/// One result folded to 64 bits: chunk `c` (counted across ports in order) rotated left by `c`.
631pub fn fold(k: &Kernel, result: &Words) -> u64 {
632    let mut c = 0usize;
633    let mut acc = 0u64;
634    for (p, port) in k.outputs.iter().enumerate() {
635        for (i, w) in result[p].iter().enumerate() {
636            acc ^= mask_bits(*w, chunk_bits(port.bits, i)).rotate_left((c % 64) as u32);
637            c += 1;
638        }
639    }
640    acc
641}
642
643/// The MISR update: shift left with the parity of the old state fed back, XOR the folded result.
644pub fn misr_step(misr: u64, folded: u64) -> u64 {
645    ((misr << 1) | (misr.count_ones() as u64 & 1)) ^ folded
646}
647
648/// The signature of a result sequence.
649pub fn signature(k: &Kernel, results: &[Words]) -> u64 {
650    results.iter().fold(0u64, |m, r| misr_step(m, fold(k, r)))
651}
652
653/// A DUT port's direction.
654#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
655pub enum Dir {
656    In,
657    Out,
658}
659
660/// A port of the DUT's top module, as its netlist declares it.
661#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
662pub struct DutPort {
663    pub name: String,
664    pub dir: Dir,
665    pub bits: u32,
666}
667
668/// A DUT reset input.
669#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
670pub struct Reset {
671    pub port: String,
672    pub active_high: bool,
673}
674
675/// One cycle of a register-file transfer: contract bits `[lsb, lsb + width)` of port `contract`.
676#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
677pub struct RegSlot {
678    pub contract: String,
679    pub lsb: u32,
680    pub width: u32,
681}
682
683/// What a beat schedule drives onto one DUT input port in one beat.
684#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
685pub enum DriveValue {
686    Const(u64),
687    /// Contract bits `[lsb, lsb + width)` of input reference `contract` (`port` or `port@j`).
688    Slice { contract: String, lsb: u32, width: u32 },
689    /// The same bits of the previous call's operands (zero before the first call): a design that
690    /// overlaps calls — loading one call's weights while it computes on the call before — takes part
691    /// of a call's operands a call later than the rest.
692    Previous { contract: String, lsb: u32, width: u32 },
693}
694
695/// DUT input `dut` takes `value` in beat `beat` of every call; a port is 0 in the beats that do not
696/// drive it.
697#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
698pub struct Drive {
699    pub beat: u32,
700    pub dut: String,
701    pub value: DriveValue,
702}
703
704/// A device's configuration-register port: `valid` with `rw` high writes `data` to register `addr`;
705/// with `rw` low it reads, and the device answers on `rsp_valid`.
706#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
707pub struct DcrBus {
708    pub valid: String,
709    pub rw: String,
710    pub addr: String,
711    pub data: String,
712    pub rsp_valid: String,
713}
714
715/// One configuration-register write.
716#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
717pub struct DcrWrite {
718    pub addr: u32,
719    pub data: u64,
720}
721
722/// One constant line of a device's memory (a program image): its line address and its bytes in hex,
723/// the byte at the highest address first.
724#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
725pub struct ImageLine {
726    pub line: u64,
727    pub hex: String,
728}
729
730/// Lines of a device's memory holding contract ports from line `base_line` on: the listed ports in
731/// order, each as `words_per_port` words, packed from the region's first byte. An input region answers
732/// reads with the call's operands; an output region keeps the device's writes.
733#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
734pub struct LineRegion {
735    pub base_line: u64,
736    pub contract: Vec<String>,
737    pub words_per_port: u32,
738    /// The bits each port occupies, as a structure of machine words lays them out (0: its own
739    /// width): an operand narrower than its stride is extended the way a bind extends one (by sign
740    /// for a signed field), a result is read from the low bits of its stride.
741    #[serde(default)]
742    pub stride_bits: u32,
743}
744
745impl LineRegion {
746    /// The bits port `bits` wide occupies in this region.
747    fn span(&self, bits: u32) -> u32 {
748        bits.max(self.stride_bits)
749    }
750}
751
752/// The memory a device reads and writes whole lines of, served by the harness over `ports` channels:
753/// each a request (`<prefix>_req_valid`, `_rw`, `_byteen`, `_addr` — a line address — `_data`, `_tag`,
754/// `_ready`) and a read response (`<prefix>_rsp_valid`, `_data`, `_tag`, `_ready`); a write has no
755/// response. Each signal carries every port's value side by side, port 0 in the most significant
756/// bits when `msb_first`. The harness accepts every request and answers each read in order on its
757/// port from the next cycle on, with the line as it is when answered: an ideal memory, never a
758/// candidate's bottleneck.
759#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
760pub struct LineBus {
761    pub ports: u32,
762    pub line_bytes: u32,
763    pub addr_bits: u32,
764    pub tag_bits: u32,
765    pub msb_first: bool,
766    pub prefix: String,
767    pub image: Vec<ImageLine>,
768    pub regions: Vec<LineRegion>,
769}
770
771impl LineBus {
772    fn line_bits(&self) -> u32 {
773        self.line_bytes * 8
774    }
775
776    fn port(&self, suffix: &str) -> String {
777        format!("{}_{suffix}", self.prefix)
778    }
779
780    /// Port `p`'s field of a signal carrying `width` bits per port.
781    fn slot(&self, signal: &str, p: u32, width: u32) -> String {
782        let at = if self.msb_first { self.ports - 1 - p } else { p };
783        format!("{signal}[{}:{}]", at * width + width - 1, at * width)
784    }
785
786    /// Every bus signal, its direction from the device, and its width.
787    fn signals(&self) -> Vec<(String, Dir, u32)> {
788        let (n, lb) = (self.ports, self.line_bits());
789        [
790            ("req_valid", Dir::Out, n),
791            ("req_rw", Dir::Out, n),
792            ("req_byteen", Dir::Out, n * self.line_bytes),
793            ("req_addr", Dir::Out, n * self.addr_bits),
794            ("req_data", Dir::Out, n * lb),
795            ("req_tag", Dir::Out, n * self.tag_bits),
796            ("req_ready", Dir::In, n),
797            ("rsp_valid", Dir::In, n),
798            ("rsp_data", Dir::In, n * lb),
799            ("rsp_tag", Dir::In, n * self.tag_bits),
800            ("rsp_ready", Dir::Out, n),
801        ]
802        .into_iter()
803        .map(|(s, d, w)| (self.port(s), d, w))
804        .collect()
805    }
806}
807
808/// How the harness hands operands to a candidate and collects its results.
809#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
810pub enum Protocol {
811    /// A new operation every cycle; its result is valid `latency` cycles later (0: combinational).
812    Registered { latency: u32 },
813    /// Pulse `start` with the operands; hold them until `done`, which marks the result valid.
814    /// One idle cycle separates operations.
815    StartDone { start: String, done: String },
816    /// Pulse `start` every `ii` cycles with a new operation; each `done` marks the next result.
817    Pipelined { start: String, done: String, ii: u32 },
818    /// Hold `go` high; each one-cycle `done` marks a result. A component restarts in the very cycle it
819    /// raises `done`, so from that cycle on the next operation's operands are presented (a read issued
820    /// then answers with them).
821    GoDone { go: String, done: String },
822    /// Hold `enable` high with the operands until `done`, which stays up once raised; then lower `enable` and
823    /// raise `clear` for one cycle, which takes `done` down, before the next operation — how Spatial's Fringe
824    /// drives an accelerator (its command and status registers, the accelerator's own reset between runs).
825    EnableDone { enable: String, done: String, clear: String },
826    /// A register-file engine: one `load` cycle per load slot puts the slot on `din`, a `go` pulse
827    /// starts it, `done` marks the results, and one cycle per read slot puts the slot's index on
828    /// `raddr` and takes it from `dout`. With `reset_each_op` the DUT's reset is pulsed before each
829    /// operation's loads (an engine whose load counter counts from reset).
830    LoadGoRead {
831        load: String,
832        din: String,
833        go: String,
834        done: String,
835        raddr: String,
836        dout: String,
837        reset_each_op: bool,
838        load_slots: Vec<RegSlot>,
839        read_slots: Vec<RegSlot>,
840    },
841    /// A fixed beat schedule: every `period` cycles a call begins; beat `t` of a call drives the
842    /// `drives` of beat `t`, and the result of the call's operation `j` is on the output binds of
843    /// slot `j` exactly `captures[j]` cycles after its beat 0. A call takes the candidate's whole batch.
844    Beats { period: u32, drives: Vec<Drive>, captures: Vec<u32> },
845    /// A device launched like a GPU kernel: after reset the `setup` writes go out on its DCR port, one
846    /// a cycle; then each call raises `start` for a cycle, waits for `busy` to rise and to fall, issues
847    /// the `flush` DCR read (if any) and waits for its answer, and takes the call's results from the
848    /// output regions of `bus`, which the device reads its operands from and writes its results to.
849    Launch { start: String, busy: String, dcr: DcrBus, setup: Vec<DcrWrite>, flush: Option<u32>, bus: LineBus },
850}
851
852impl Protocol {
853    pub fn label(&self) -> String {
854        match self {
855            Protocol::Registered { latency } => format!("registered, latency {latency}"),
856            Protocol::StartDone { .. } => "start/done".to_string(),
857            Protocol::Pipelined { ii, .. } => format!("pipelined start/done, II {ii}"),
858            Protocol::GoDone { .. } => "go/done".to_string(),
859            Protocol::EnableDone { .. } => "enable held until a sticky done, then cleared".to_string(),
860            Protocol::Beats { period, captures, .. } => {
861                format!("beat schedule: a call every {period} cycles, {} results", captures.len())
862            }
863            Protocol::LoadGoRead { load_slots, read_slots, reset_each_op, .. } => format!(
864                "register file: {} loads, go/done, {} reads{}",
865                load_slots.len(),
866                read_slots.len(),
867                if *reset_each_op { ", reset before each operation" } else { "" }
868            ),
869            Protocol::Launch { bus, flush, .. } => format!(
870                "kernel launch over a {}-port {}-byte-line memory bus{}",
871                bus.ports,
872                bus.line_bytes,
873                if flush.is_some() { ", flushed before the results are read" } else { "" }
874            ),
875        }
876    }
877}
878
879/// The memory regions of a launched device's bus (none for any other protocol).
880fn launch_regions(c: &Candidate) -> &[LineRegion] {
881    match &c.protocol {
882        Protocol::Launch { bus, .. } => &bus.regions,
883        _ => &[],
884    }
885}
886
887/// Contract bits `[lsb, lsb + width)` of port `contract` connect to DUT port `dut`. Several binds
888/// to one input DUT port concatenate, the first in the least significant bits; an input DUT port
889/// wider than what is bound to it is sign-extended when the topmost bound contract field is signed,
890/// else zero-extended. An output DUT port contributes its low `width` bits.
891#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
892pub struct Bind {
893    pub contract: String,
894    pub lsb: u32,
895    pub width: u32,
896    pub dut: String,
897    /// For an output: the DUT port bit the contract bits start at.
898    #[serde(default)]
899    pub dut_lsb: u32,
900    /// For an output: the DUT port whose one-cycle pulse marks a new value on `dut`. Values arrive in
901    /// operation order; the harness queues each port and folds a result once every port has one.
902    #[serde(default)]
903    pub valid: Option<String>,
904    /// For an output the DUT carries narrower than the contract field: the DUT supplies this many
905    /// bits from `dut_lsb`, a signed value sign-extended to the bind's width (0: the whole width).
906    #[serde(default, skip_serializing_if = "is_zero")]
907    pub dut_width: u32,
908}
909
910fn is_zero(v: &u32) -> bool {
911    *v == 0
912}
913
914impl Bind {
915    /// The whole contract port `contract` to DUT port `dut`; the width is the contract's.
916    pub fn whole(contract: &str, dut: &str) -> Bind {
917        Bind { contract: contract.to_string(), lsb: 0, width: 0, dut: dut.to_string(), dut_lsb: 0, valid: None, dut_width: 0 }
918    }
919
920    pub fn slice(contract: &str, lsb: u32, width: u32, dut: &str) -> Bind {
921        Bind { contract: contract.to_string(), lsb, width, dut: dut.to_string(), dut_lsb: 0, valid: None, dut_width: 0 }
922    }
923
924    /// This output bind taking only `dut_width` signed bits from the DUT, sign-extended to its width.
925    pub fn sign_extended(mut self, dut_width: u32) -> Bind {
926        self.dut_width = dut_width;
927        self
928    }
929
930    /// The DUT port bits this bind reads or drives.
931    fn dut_bits(&self, contract_bits: u32) -> u32 {
932        if self.dut_width == 0 {
933            self.resolved_width(contract_bits)
934        } else {
935            self.dut_width
936        }
937    }
938
939    /// This output bind with its values marked by DUT pulse `valid`.
940    pub fn valid_on(mut self, valid: &str) -> Bind {
941        self.valid = Some(valid.to_string());
942        self
943    }
944
945    /// The whole output contract port `contract` from bits `[dut_lsb, dut_lsb + width)` of DUT port
946    /// `dut`.
947    pub fn from_dut(contract: &str, dut: &str, dut_lsb: u32, width: u32) -> Bind {
948        Bind { contract: contract.to_string(), lsb: 0, width, dut: dut.to_string(), dut_lsb, valid: None, dut_width: 0 }
949    }
950
951    fn resolved_width(&self, contract_bits: u32) -> u32 {
952        if self.width == 0 {
953            contract_bits - self.lsb
954        } else {
955            self.width
956        }
957    }
958}
959
960/// The memory protocol a [`MemBind`] speaks.
961#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
962pub enum MemKind {
963    /// Calyx's `seq_mem`: `<p>_addr0`, `_content_en`, `_write_en`, `_write_data`, `_read_data`, `_done`.
964    /// A read or a write issued with `content_en` completes one cycle later, when `done` rises.
965    SeqMem,
966    /// A block-RAM interface with `ports` ports `i`: `<p>_address<i>`, `_ce<i>`, and `_q<i>` for a
967    /// read (one cycle later) or `_we<i>` with `_d<i>` for a write.
968    Bram { ports: u32 },
969}
970
971/// Contract ports carried through a memory interface instead of scalar ports. The memory's words
972/// are the listed contract ports in order, each split into `words_per_port` words (least
973/// significant first), so one wide port can become an array and several ports one array.
974#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
975pub struct MemBind {
976    pub prefix: String,
977    pub kind: MemKind,
978    pub contract: Vec<String>,
979    pub words_per_port: u32,
980    /// The memory's shape when its interface takes one address per dimension, as Calyx gives a
981    /// Dahlia `decl x: bit<N>[R][C]` (`addr0` the row, `addr1` the column): the words row-major.
982    /// Empty: one flat address.
983    #[serde(default, skip_serializing_if = "Vec::is_empty")]
984    pub dims: Vec<u32>,
985}
986
987/// The address bits that index `n` words.
988fn index_bits(n: u32) -> u32 {
989    (32 - (n.max(2) - 1).leading_zeros()).max(1)
990}
991
992impl MemBind {
993    /// A Calyx `seq_mem` holding contract port `contract` as `words` words.
994    pub fn seq_mem(contract: &str, prefix: &str, words: u32) -> MemBind {
995        MemBind::seq_mem_rows(&[contract.to_string()], prefix, words)
996    }
997
998    /// A Calyx `seq_mem` whose words are the contract ports `contract` in order, each as
999    /// `words_per_port` words.
1000    pub fn seq_mem_rows(contract: &[String], prefix: &str, words_per_port: u32) -> MemBind {
1001        MemBind {
1002            prefix: prefix.to_string(),
1003            kind: MemKind::SeqMem,
1004            contract: contract.to_vec(),
1005            words_per_port,
1006            dims: Vec::new(),
1007        }
1008    }
1009
1010    /// This memory addressed by one port per dimension of `dims`, row-major.
1011    pub fn shaped(mut self, dims: &[u32]) -> MemBind {
1012        self.dims = dims.to_vec();
1013        self
1014    }
1015
1016    /// A `ports`-port block RAM whose words are the contract ports `contract`, one each.
1017    pub fn bram(contract: &[&str], prefix: &str, ports: u32) -> MemBind {
1018        MemBind {
1019            prefix: prefix.to_string(),
1020            kind: MemKind::Bram { ports },
1021            contract: contract.iter().map(|c| c.to_string()).collect(),
1022            words_per_port: 1,
1023            dims: Vec::new(),
1024        }
1025    }
1026
1027    fn words(&self) -> u32 {
1028        self.contract.len() as u32 * self.words_per_port
1029    }
1030
1031    fn addr_bits(&self) -> u32 {
1032        index_bits(self.words())
1033    }
1034
1035    /// The address ports and the width each needs: `addr0` for a flat memory, `addr<k>` for
1036    /// dimension `k` of a shaped one.
1037    fn address_ports(&self) -> Vec<(String, u32)> {
1038        if self.dims.is_empty() {
1039            vec![(self.port("addr0"), self.addr_bits())]
1040        } else {
1041            self.dims.iter().enumerate().map(|(k, d)| (self.port(&format!("addr{k}")), index_bits(*d))).collect()
1042        }
1043    }
1044
1045    /// The flat word index the address wires `q_<prefix>_addr<k>` select, row-major.
1046    fn word_index(&self) -> String {
1047        if self.dims.is_empty() {
1048            return format!("q_{}_addr0", self.prefix);
1049        }
1050        let mut e = format!("q_{}_addr0", self.prefix);
1051        for (k, d) in self.dims.iter().enumerate().skip(1) {
1052            e = format!("({e} * {d} + q_{}_addr{k})", self.prefix);
1053        }
1054        e
1055    }
1056
1057    fn port(&self, suffix: &str) -> String {
1058        format!("{}_{suffix}", self.prefix)
1059    }
1060}
1061
1062/// One implementation of a kernel, as the harness drives it.
1063#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1064pub struct Candidate {
1065    pub kernel: String,
1066    pub name: String,
1067    pub top: String,
1068    pub clock: String,
1069    pub reset: Option<Reset>,
1070    pub protocol: Protocol,
1071    pub inputs: Vec<Bind>,
1072    pub outputs: Vec<Bind>,
1073    /// Contract ports carried through memory interfaces instead of scalar ports.
1074    #[serde(default)]
1075    pub mems: Vec<MemBind>,
1076    /// Contract operations per call: binds name slot `j` as `port@j`.
1077    #[serde(default = "one")]
1078    pub batch: u32,
1079    /// The DUT top module's ports, as its netlist declares them.
1080    pub ports: Vec<DutPort>,
1081}
1082
1083fn one() -> u32 {
1084    1
1085}
1086
1087/// The harness module's name for `c`.
1088pub fn harness_module(c: &Candidate) -> String {
1089    format!("qorbist_{}", c.name)
1090}
1091
1092fn find<'a>(c: &'a Candidate, name: &str) -> Result<&'a DutPort, QorError> {
1093    c.ports.iter().find(|p| p.name == name).ok_or_else(|| QorError::PortMissing {
1094        candidate: c.name.clone(),
1095        port: name.to_string(),
1096    })
1097}
1098
1099fn expect_dir(c: &Candidate, p: &DutPort, dir: Dir) -> Result<(), QorError> {
1100    if p.dir == dir {
1101        Ok(())
1102    } else {
1103        Err(QorError::PortDirection { candidate: c.name.clone(), port: p.name.clone() })
1104    }
1105}
1106
1107/// The canonical `port@slot` key of a contract reference.
1108fn slot_key(r: &str) -> String {
1109    let (p, j) = contract_ref(r);
1110    format!("{p}@{j}")
1111}
1112
1113/// Every `port@slot` key of `ports` over a batch of `batch`, with the port's width.
1114fn slot_keys(ports: &[(&'static str, u32)], batch: u32) -> Vec<(String, u32)> {
1115    (0..batch)
1116        .flat_map(|j| ports.iter().map(move |(n, b)| (format!("{n}@{j}"), *b)))
1117        .collect()
1118}
1119
1120/// Every contract bit of `keys` is bound exactly once — by a bind, a memory, a register-file slot, or
1121/// a beat — and every bind names a contract port of the batch.
1122fn check_binds(
1123    c: &Candidate,
1124    binds: &[Bind],
1125    slots: &[RegSlot],
1126    beats: &[(String, u32, u32)],
1127    keys: &[(String, u32)],
1128    dir: Dir,
1129) -> Result<(), QorError> {
1130    for (key, bits) in keys {
1131        let mut covered = vec![false; *bits as usize];
1132        let mut cover = |lo: u32, hi: u32, twice_ok: bool| -> Result<(), QorError> {
1133            for bit in lo..hi.min(*bits) {
1134                if covered[bit as usize] && !twice_ok {
1135                    return Err(QorError::Unmapped {
1136                        candidate: c.name.clone(),
1137                        contract: format!("{key} (bit {bit} bound twice)"),
1138                    });
1139                }
1140                covered[bit as usize] = true;
1141            }
1142            Ok(())
1143        };
1144        for b in binds.iter().filter(|b| slot_key(&b.contract) == *key) {
1145            let w = b.resolved_width(*bits);
1146            let p = find(c, &b.dut)?;
1147            expect_dir(c, p, dir)?;
1148            if b.dut_width > 0 && (dir == Dir::In || b.dut_width > w) {
1149                return Err(QorError::Protocol(format!(
1150                    "{}: `{}` takes {} bits of {} for a {w}-bit field; only an output may be narrower in the DUT",
1151                    c.name, b.contract, b.dut_width, b.dut
1152                )));
1153            }
1154            let need = b.dut_lsb + b.dut_bits(*bits);
1155            if p.bits < need {
1156                return Err(QorError::PortWidth { candidate: c.name.clone(), port: p.name.clone(), need, have: p.bits });
1157            }
1158            cover(b.lsb, b.lsb + w, false)?;
1159        }
1160        for m in c.mems.iter().filter(|m| m.contract.iter().any(|n| slot_key(n) == *key)) {
1161            check_mem(c, m, *bits, dir)?;
1162            cover(0, *bits, false)?;
1163        }
1164        for r in launch_regions(c) {
1165            for _ in r.contract.iter().filter(|n| slot_key(n) == *key) {
1166                if r.words_per_port == 0 || bits % r.words_per_port != 0 {
1167                    return Err(QorError::Protocol(format!(
1168                        "{}: {} words do not divide the {bits}-bit port {key} in the region at line {:#x}",
1169                        c.name, r.words_per_port, r.base_line
1170                    )));
1171                }
1172                cover(0, *bits, false)?;
1173            }
1174        }
1175        for s in slots.iter().filter(|s| slot_key(&s.contract) == *key) {
1176            cover(s.lsb, s.lsb + s.width, false)?;
1177        }
1178        for (_, lsb, width) in beats.iter().filter(|(r, _, _)| slot_key(r) == *key) {
1179            cover(*lsb, lsb + width, true)?;
1180        }
1181        if covered.iter().any(|b| !b) {
1182            return Err(QorError::Unmapped { candidate: c.name.clone(), contract: key.clone() });
1183        }
1184    }
1185    let known = |r: &str| keys.iter().any(|(k, _)| *k == slot_key(r));
1186    for r in binds
1187        .iter()
1188        .map(|b| b.contract.as_str())
1189        .chain(slots.iter().map(|s| s.contract.as_str()))
1190        .chain(beats.iter().map(|(r, _, _)| r.as_str()))
1191    {
1192        if !known(r) {
1193            return Err(QorError::Unmapped { candidate: c.name.clone(), contract: format!("{r} (not a contract port)") });
1194        }
1195    }
1196    Ok(())
1197}
1198
1199/// The memory interface of `m` fits the DUT: every signal present, running the right way, wide
1200/// enough for one word of the `bits`-wide contract port it carries in direction `dir`.
1201fn check_mem(c: &Candidate, m: &MemBind, bits: u32, dir: Dir) -> Result<(), QorError> {
1202    if m.words_per_port == 0 || bits % m.words_per_port != 0 {
1203        return Err(QorError::Protocol(format!(
1204            "{}: {} words do not divide the {bits}-bit port behind `{}`",
1205            c.name, m.words_per_port, m.prefix
1206        )));
1207    }
1208    if !m.dims.is_empty() {
1209        if matches!(m.kind, MemKind::Bram { .. }) {
1210            return Err(QorError::Protocol(format!(
1211                "{}: the block RAM `{}` has one flat address per port and takes no shape",
1212                c.name, m.prefix
1213            )));
1214        }
1215        let cells: u64 = m.dims.iter().map(|d| *d as u64).product();
1216        if cells != m.words() as u64 {
1217            return Err(QorError::Protocol(format!(
1218                "{}: the shape {:?} of `{}` holds {cells} words, its contract ports {}",
1219                c.name,
1220                m.dims,
1221                m.prefix,
1222                m.words()
1223            )));
1224        }
1225    }
1226    let word = bits / m.words_per_port;
1227    let mut need: Vec<(String, Dir, u32)> = Vec::new();
1228    match &m.kind {
1229        MemKind::SeqMem => {
1230            let data = if dir == Dir::In { "read_data" } else { "write_data" };
1231            need.extend(m.address_ports().into_iter().map(|(name, w)| (name, Dir::Out, w)));
1232            for (suffix, pdir, w) in [
1233                ("content_en", Dir::Out, 1),
1234                ("write_en", Dir::Out, 1),
1235                ("write_data", Dir::Out, 0),
1236                ("read_data", Dir::In, 0),
1237                ("done", Dir::In, 1),
1238            ] {
1239                need.push((m.port(suffix), pdir, if suffix == data { word } else { w }));
1240            }
1241        }
1242        MemKind::Bram { ports } => {
1243            for i in 0..*ports {
1244                need.push((m.port(&format!("address{i}")), Dir::Out, m.addr_bits()));
1245                need.push((m.port(&format!("ce{i}")), Dir::Out, 1));
1246                if dir == Dir::In {
1247                    need.push((m.port(&format!("q{i}")), Dir::In, word));
1248                } else {
1249                    need.push((m.port(&format!("we{i}")), Dir::Out, 1));
1250                    need.push((m.port(&format!("d{i}")), Dir::Out, word));
1251                }
1252            }
1253        }
1254    }
1255    for (name, pdir, w) in need {
1256        let p = find(c, &name)?;
1257        expect_dir(c, p, pdir)?;
1258        if p.bits < w {
1259            return Err(QorError::PortWidth { candidate: c.name.clone(), port: p.name.clone(), need: w, have: p.bits });
1260        }
1261    }
1262    Ok(())
1263}
1264
1265/// The results each slot of a beat schedule must be able to hold. The harness folds one result per
1266/// cycle in operation order, so a slot whose result arrives while an earlier slot of its call is
1267/// still outstanding waits — and when the call after it delivers that slot again before the fold
1268/// reaches it (results handed back last first), the slot queues two. Found by running the schedule
1269/// itself: slot `j` of call `n` is captured at `n * period + captures[j]` and folded once every
1270/// earlier result has been. A schedule that brings more results per call than a call has cycles
1271/// outruns the fold and is refused.
1272pub fn beat_capture_depths(period: u32, captures: &[u32]) -> Result<Vec<u32>, String> {
1273    let nb = captures.len();
1274    if nb == 0 || period == 0 || nb as u32 > period {
1275        return Err(format!("{nb} results every {period} cycles outrun a fold of one result per cycle"));
1276    }
1277    let deepest = *captures.iter().max().unwrap();
1278    let calls = (deepest / period + 1) as usize * 4 + nb + 4;
1279    let end = calls as u32 * period + deepest + 2 * nb as u32 + 2;
1280    let mut count = vec![0u32; nb];
1281    let mut depth = vec![1u32; nb];
1282    let mut fp = 0usize;
1283    for t in 0..end {
1284        let pop = (count[fp] > 0).then_some(fp);
1285        for (j, off) in captures.iter().enumerate() {
1286            let push = t >= *off && (t - off) % period == 0 && ((t - off) / period) < calls as u32;
1287            let popped = pop == Some(j);
1288            count[j] = count[j] + push as u32 - popped as u32;
1289            depth[j] = depth[j].max(count[j]);
1290        }
1291        if pop.is_some() {
1292            fp = (fp + 1) % nb;
1293        }
1294    }
1295    Ok(depth)
1296}
1297
1298/// Refuse a candidate whose binding, clock, reset or protocol ports do not fit its DUT's ports.
1299pub fn check_ports(k: &Kernel, c: &Candidate) -> Result<(), QorError> {
1300    if c.batch == 0 {
1301        return Err(QorError::Protocol(format!("{}: a batch of 0 operations", c.name)));
1302    }
1303    let ins: Vec<(&'static str, u32)> = k.inputs.iter().map(|p| (p.name, p.bits)).collect();
1304    let outs: Vec<(&'static str, u32)> = k.outputs.iter().map(|p| (p.name, p.bits)).collect();
1305    let (load_slots, read_slots): (&[RegSlot], &[RegSlot]) = match &c.protocol {
1306        Protocol::LoadGoRead { load_slots, read_slots, .. } => (load_slots, read_slots),
1307        _ => (&[], &[]),
1308    };
1309    let beat_slices: Vec<(String, u32, u32)> = match &c.protocol {
1310        Protocol::Beats { drives, .. } => drives
1311            .iter()
1312            .filter_map(|d| match &d.value {
1313                DriveValue::Slice { contract, lsb, width } | DriveValue::Previous { contract, lsb, width } => {
1314                    Some((contract.clone(), *lsb, *width))
1315                }
1316                DriveValue::Const(_) => None,
1317            })
1318            .collect(),
1319        _ => Vec::new(),
1320    };
1321    // A held port is one register for the whole batch: only its slot 0 is bound.
1322    let in_keys: Vec<(String, u32)> = slot_keys(&ins, c.batch)
1323        .into_iter()
1324        .filter(|(key, _)| {
1325            let (p, j) = contract_ref(key);
1326            j == 0 || k.inputs.iter().any(|i| i.name == p && i.hold <= 1)
1327        })
1328        .collect();
1329    check_binds(c, &c.inputs, load_slots, &beat_slices, &in_keys, Dir::In)?;
1330    check_binds(c, &c.outputs, read_slots, &[], &slot_keys(&outs, c.batch), Dir::Out)?;
1331    for p in k.inputs.iter().filter(|p| p.hold > 1) {
1332        if c.batch > 1 && p.hold % c.batch != 0 {
1333            return Err(QorError::Protocol(format!(
1334                "{}: a batch of {} does not tile the {}-operation hold of `{}`",
1335                c.name, c.batch, p.hold, p.name
1336            )));
1337        }
1338    }
1339    let strobed = c.outputs.iter().filter(|b| b.valid.is_some()).count();
1340    if strobed > 0 {
1341        let output_mems = c.mems.iter().any(|m| m.contract.iter().any(|n| k.outputs.iter().any(|o| o.name == contract_ref(n).0)));
1342        let scheduled = matches!(c.protocol, Protocol::Beats { .. } | Protocol::LoadGoRead { .. } | Protocol::Launch { .. });
1343        if strobed != c.outputs.len() || output_mems || scheduled {
1344            return Err(QorError::Protocol(format!(
1345                "{}: every output is strobed or none is, through ports, under a handshake ({strobed} of {} binds carry a valid, output memories: {output_mems}, {})",
1346                c.name,
1347                c.outputs.len(),
1348                c.protocol.label()
1349            )));
1350        }
1351        for b in &c.outputs {
1352            let slots: std::collections::BTreeSet<u32> =
1353                c.outputs.iter().filter(|o| o.dut == b.dut).map(|o| contract_ref(&o.contract).1).collect();
1354            if slots.len() != 1 {
1355                return Err(QorError::Protocol(format!(
1356                    "{}: the strobed port `{}` carries results of {} operations of a call; a strobe marks one",
1357                    c.name,
1358                    b.dut,
1359                    slots.len()
1360                )));
1361            }
1362        }
1363        for b in &c.outputs {
1364            let v = find(c, b.valid.as_deref().unwrap_or_default())?;
1365            expect_dir(c, v, Dir::Out)?;
1366            if v.bits != 1 {
1367                return Err(QorError::Protocol(format!("{}: strobe `{}` is {} bits, not 1", c.name, v.name, v.bits)));
1368            }
1369        }
1370    }
1371    let clk = find(c, &c.clock)?;
1372    expect_dir(c, clk, Dir::In)?;
1373    if let Some(r) = &c.reset {
1374        let p = find(c, &r.port)?;
1375        expect_dir(c, p, Dir::In)?;
1376    }
1377    let control: Vec<(&String, Dir)> = match &c.protocol {
1378        Protocol::Registered { .. } => vec![],
1379        Protocol::StartDone { start, done } | Protocol::Pipelined { start, done, .. } => {
1380            vec![(start, Dir::In), (done, Dir::Out)]
1381        }
1382        Protocol::GoDone { go, done } => vec![(go, Dir::In), (done, Dir::Out)],
1383        Protocol::EnableDone { enable, done, clear } => vec![(enable, Dir::In), (done, Dir::Out), (clear, Dir::In)],
1384        Protocol::Beats { period, drives, captures } => {
1385            if captures.len() != c.batch as usize {
1386                return Err(QorError::Protocol(format!(
1387                    "{}: {} capture offsets for a batch of {}",
1388                    c.name,
1389                    captures.len(),
1390                    c.batch
1391                )));
1392            }
1393            beat_capture_depths(*period, captures).map_err(|e| QorError::Protocol(format!("{}: {e}", c.name)))?;
1394            for d in drives {
1395                if d.beat >= *period {
1396                    return Err(QorError::Protocol(format!("{}: beat {} of a {period}-cycle call", c.name, d.beat)));
1397                }
1398                let p = find(c, &d.dut)?;
1399                expect_dir(c, p, Dir::In)?;
1400                if let DriveValue::Slice { width, .. } | DriveValue::Previous { width, .. } = &d.value {
1401                    if p.bits < *width {
1402                        return Err(QorError::PortWidth { candidate: c.name.clone(), port: p.name.clone(), need: *width, have: p.bits });
1403                    }
1404                }
1405            }
1406            vec![]
1407        }
1408        Protocol::Launch { start, busy, dcr, setup, flush, bus } => {
1409            if bus.ports == 0 || bus.line_bytes == 0 || bus.addr_bits == 0 || bus.addr_bits > 64 {
1410                return Err(QorError::Protocol(format!("{}: a bus of {} ports of {}-byte lines", c.name, bus.ports, bus.line_bytes)));
1411            }
1412            for (name, dir, need) in bus.signals() {
1413                let p = find(c, &name)?;
1414                expect_dir(c, p, dir)?;
1415                if p.bits != need {
1416                    return Err(QorError::PortWidth { candidate: c.name.clone(), port: p.name.clone(), need, have: p.bits });
1417                }
1418            }
1419            let line_bits = bus.line_bits() as u64;
1420            let mut spans: Vec<(u64, u64, String)> = Vec::new();
1421            for img in &bus.image {
1422                if img.hex.len() != 2 * bus.line_bytes as usize || !img.hex.bytes().all(|b| b.is_ascii_hexdigit()) {
1423                    return Err(QorError::Protocol(format!(
1424                        "{}: the image line {:#x} is not {} bytes of hex",
1425                        c.name, img.line, bus.line_bytes
1426                    )));
1427                }
1428                spans.push((img.line, img.line + 1, format!("image line {:#x}", img.line)));
1429            }
1430            for r in &bus.regions {
1431                let mut dirs = Vec::new();
1432                let mut content = 0u64;
1433                for n in &r.contract {
1434                    let port = contract_ref(n).0;
1435                    let (dir, bits) = match (k.inputs.iter().find(|p| p.name == port), k.outputs.iter().find(|p| p.name == port)) {
1436                        (Some(p), _) => (Dir::In, p.bits),
1437                        (_, Some(p)) => (Dir::Out, p.bits),
1438                        _ => {
1439                            return Err(QorError::Unmapped {
1440                                candidate: c.name.clone(),
1441                                contract: format!("{n} (not a contract port)"),
1442                            })
1443                        }
1444                    };
1445                    if r.stride_bits != 0 && r.stride_bits < bits {
1446                        return Err(QorError::Protocol(format!(
1447                            "{}: the {bits}-bit port {n} does not fit the {}-bit stride of the region at line {:#x}",
1448                            c.name, r.stride_bits, r.base_line
1449                        )));
1450                    }
1451                    dirs.push(dir);
1452                    content += r.span(bits) as u64;
1453                }
1454                if dirs.windows(2).any(|w| w[0] != w[1]) {
1455                    return Err(QorError::Protocol(format!(
1456                        "{}: the region at line {:#x} mixes operands and results",
1457                        c.name, r.base_line
1458                    )));
1459                }
1460                let lines = content.div_ceil(line_bits).max(1);
1461                if bus.addr_bits < 64 && r.base_line + lines > 1u64 << bus.addr_bits {
1462                    return Err(QorError::Protocol(format!(
1463                        "{}: the region at line {:#x} runs past {}-bit line addresses",
1464                        c.name, r.base_line, bus.addr_bits
1465                    )));
1466                }
1467                spans.push((r.base_line, r.base_line + lines, format!("the region at line {:#x}", r.base_line)));
1468            }
1469            spans.sort();
1470            if let Some(w) = spans.windows(2).find(|w| w[1].0 < w[0].1) {
1471                return Err(QorError::Protocol(format!("{}: {} and {} share a line", c.name, w[0].2, w[1].2)));
1472            }
1473            let reg_bits = |v: u64| 64 - v.leading_zeros();
1474            let addrs = setup.iter().map(|w| w.addr as u64).chain(flush.map(|f| f as u64));
1475            for (port, need) in [
1476                (&dcr.addr, addrs.map(reg_bits).max().unwrap_or(1).max(1)),
1477                (&dcr.data, setup.iter().map(|w| reg_bits(w.data)).max().unwrap_or(1).max(1)),
1478            ] {
1479                let p = find(c, port)?;
1480                expect_dir(c, p, Dir::In)?;
1481                if p.bits < need {
1482                    return Err(QorError::PortWidth { candidate: c.name.clone(), port: p.name.clone(), need, have: p.bits });
1483                }
1484            }
1485            vec![(start, Dir::In), (busy, Dir::Out), (&dcr.valid, Dir::In), (&dcr.rw, Dir::In), (&dcr.rsp_valid, Dir::Out)]
1486        }
1487        Protocol::LoadGoRead { load, din, go, done, raddr, dout, reset_each_op, load_slots, read_slots } => {
1488            if *reset_each_op && c.reset.is_none() {
1489                return Err(QorError::Protocol(format!("{}: resetting each operation needs the DUT's reset port", c.name)));
1490            }
1491            let widest = |s: &[RegSlot]| s.iter().map(|s| s.width).max().unwrap_or(1);
1492            for (port, dir, need) in [
1493                (din, Dir::In, widest(load_slots)),
1494                (dout, Dir::Out, widest(read_slots)),
1495                (raddr, Dir::In, (32 - (read_slots.len().max(2) as u32 - 1).leading_zeros()).max(1)),
1496            ] {
1497                let p = find(c, port)?;
1498                expect_dir(c, p, dir)?;
1499                if p.bits < need {
1500                    return Err(QorError::PortWidth { candidate: c.name.clone(), port: p.name.clone(), need, have: p.bits });
1501                }
1502            }
1503            vec![(load, Dir::In), (go, Dir::In), (done, Dir::Out)]
1504        }
1505    };
1506    for (name, dir) in control {
1507        let p = find(c, name)?;
1508        expect_dir(c, p, dir)?;
1509        if p.bits != 1 {
1510            return Err(QorError::Protocol(format!(
1511                "{}: control port `{name}` is {} bits, not 1",
1512                c.name, p.bits
1513            )));
1514        }
1515    }
1516    Ok(())
1517}
1518
1519fn hex64(v: u64) -> String {
1520    format!("64'h{v:016x}")
1521}
1522
1523fn sized_const(bits: u32, v: i128) -> String {
1524    let m = if bits >= 64 { v as u64 } else { (v as u64) & ((1u64 << bits) - 1) };
1525    format!("{bits}'h{m:x}")
1526}
1527
1528/// The Verilog expression of a field over lane wire `lane` in `mode` (64 bits wide).
1529fn field_verilog(f: &Field, lane: &str, mode: Mode) -> String {
1530    let w = f.width;
1531    if f.lanes == 1 {
1532        match mode {
1533            Mode::Edge => {
1534                let (lo, hi) = f.range();
1535                format!("({lane}[0] ? {} : {})", hex64(hi as u64), hex64(lo as u64))
1536            }
1537            Mode::Shaped => {
1538                let slice = if w >= 64 {
1539                    lane.to_string()
1540                } else if f.signed {
1541                    format!("{{{{{}{{{lane}[{}]}}}}, {lane}[{}:0]}}", 64 - w, w - 1, w - 1)
1542                } else {
1543                    format!("{{{}'d0, {lane}[{}:0]}}", 64 - w, w - 1)
1544                };
1545                if f.base == 0 {
1546                    slice
1547                } else {
1548                    format!("({} + {slice})", hex64(f.base as u64))
1549                }
1550            }
1551        }
1552    } else {
1553        let used = f.lanes * w;
1554        let pad = if used < 64 { format!("{}'d0, ", 64 - used) } else { String::new() };
1555        match mode {
1556            Mode::Shaped => format!("{{{pad}{lane}[{}:0]}}", used - 1),
1557            Mode::Edge => {
1558                let (lo, hi) = f.slice_bounds();
1559                let parts: Vec<String> = (0..f.lanes)
1560                    .rev()
1561                    .map(|j| format!("({lane}[{j}] ? {} : {})", sized_const(w, hi), sized_const(w, lo)))
1562                    .collect();
1563                format!("{{{pad}{}}}", parts.join(", "))
1564            }
1565        }
1566    }
1567}
1568
1569fn rotl_verilog(src: &str, r: u32) -> String {
1570    if r == 0 {
1571        src.to_string()
1572    } else {
1573        format!("{{{src}[{}:0], {src}[63:{}]}}", 63 - r, 64 - r)
1574    }
1575}
1576
1577fn input_bits(k: &Kernel, name: &str) -> u32 {
1578    k.inputs.iter().find(|p| p.name == name).map(|p| p.bits).unwrap_or(0)
1579}
1580
1581fn output_bits(k: &Kernel, name: &str) -> u32 {
1582    k.outputs.iter().find(|p| p.name == name).map(|p| p.bits).unwrap_or(0)
1583}
1584
1585fn field_signed(k: &Kernel, name: &str) -> bool {
1586    k.inputs
1587        .iter()
1588        .find(|p| p.name == name)
1589        .map(|p| p.fields.iter().any(|f| f.signed || f.base < 0))
1590        .unwrap_or(false)
1591}
1592
1593/// The Verilog of a stimulus jump: `name` is `src` advanced `n` steps, one parity per bit.
1594fn jump_verilog(name: &str, src: &str, n: u32) -> String {
1595    let mut v = format!("  wire [63:0] {name};\n");
1596    for (i, row) in jump_rows(n).iter().enumerate() {
1597        v.push_str(&format!("  assign {name}[{i}] = ^({src} & {});\n", hex64(*row)));
1598    }
1599    v
1600}
1601
1602/// The harness side of a launched device's memory bus: every region's lines (an input region's are
1603/// the call's operands; an output region's are registers the device's writes land in, with a `_next`
1604/// that has this cycle's writes applied, which is what a capture takes), the constant image lines, and
1605/// per port an in-order queue of the reads still to answer, each answered with its line as it is then.
1606#[allow(clippy::too_many_arguments)]
1607fn emit_line_bus(
1608    k: &Kernel,
1609    bus: &LineBus,
1610    opnd: &dyn Fn(&str) -> String,
1611    v: &mut String,
1612    conns: &mut Vec<(String, String)>,
1613    reset: &mut String,
1614    step: &mut String,
1615    outputs: &mut Vec<(String, String)>,
1616) {
1617    let (np, lb, ab, tb, bytes) = (bus.ports, bus.line_bits(), bus.addr_bits, bus.tag_bits, bus.line_bytes);
1618    const DEPTH: u32 = 32;
1619    for (name, _, w) in bus.signals() {
1620        let suffix = name.strip_prefix(&format!("{}_", bus.prefix)).unwrap_or(&name).to_string();
1621        let wire = if suffix == "req_ready" { "lb_ready".to_string() } else { format!("lb_{suffix}") };
1622        v.push_str(&format!("  wire [{}:0] {wire};\n", w - 1));
1623        conns.push((name, wire));
1624    }
1625    v.push_str(&format!("  assign lb_ready = {{{np}{{1'b1}}}};\n  integer lb_w;\n"));
1626    for p in 0..np {
1627        v.push_str(&format!(
1628            "  wire lb_v{p} = {};\n  wire lb_rw{p} = {};\n  wire [{}:0] lb_a{p} = {};\n  wire [{}:0] lb_d{p} = {};\n  wire [{}:0] lb_be{p} = {};\n  wire [{}:0] lb_t{p} = {};\n  wire lb_rr{p} = {};\n",
1629            bus.slot("lb_req_valid", p, 1),
1630            bus.slot("lb_req_rw", p, 1),
1631            ab - 1,
1632            bus.slot("lb_req_addr", p, ab),
1633            lb - 1,
1634            bus.slot("lb_req_data", p, lb),
1635            bytes - 1,
1636            bus.slot("lb_req_byteen", p, bytes),
1637            tb - 1,
1638            bus.slot("lb_req_tag", p, tb),
1639            bus.slot("lb_rsp_ready", p, 1),
1640        ));
1641    }
1642    // The regions, and the lookup every port answers its reads with.
1643    let mut lookup = format!("  function [{}:0] lb_line(input [{}:0] l);\n    begin\n      case (l)\n", lb - 1, ab - 1);
1644    let hex_digits = ab.div_ceil(4) as usize;
1645    for img in &bus.image {
1646        lookup.push_str(&format!("        {ab}'h{:0hex_digits$x}: lb_line = {lb}'h{};\n", img.line, img.hex));
1647    }
1648    lookup.push_str(&format!("        default: lb_line = {lb}'d0;\n      endcase\n"));
1649    for (ri, r) in bus.regions.iter().enumerate() {
1650        let first = contract_ref(&r.contract[0]).0;
1651        let is_input = k.inputs.iter().any(|p| p.name == first);
1652        let bits_of = |n: &str| {
1653            let port = contract_ref(n).0;
1654            if is_input { input_bits(k, port) } else { output_bits(k, port) }
1655        };
1656        let content: u32 = r.contract.iter().map(|n| r.span(bits_of(n))).sum();
1657        let lines = content.div_ceil(lb).max(1);
1658        let total = lines * lb;
1659        let (base, end) = (r.base_line, r.base_line + lines as u64);
1660        let name = format!("lr_{ri}");
1661        if is_input {
1662            let parts: Vec<String> = r
1663                .contract
1664                .iter()
1665                .rev()
1666                .map(|n| {
1667                    let (bits, o) = (bits_of(n), opnd(n));
1668                    let ext = r.span(bits) - bits;
1669                    if ext == 0 {
1670                        o
1671                    } else if field_signed(k, contract_ref(n).0) {
1672                        format!("{{{{{ext}{{{o}[{}]}}}}, {o}}}", bits - 1)
1673                    } else {
1674                        format!("{{{ext}'d0, {o}}}")
1675                    }
1676                })
1677                .collect();
1678            let body = if total > content {
1679                format!("{{{}'d0, {}}}", total - content, parts.join(", "))
1680            } else {
1681                format!("{{{}}}", parts.join(", "))
1682            };
1683            v.push_str(&format!("  wire [{}:0] {name} = {body};\n", total - 1));
1684        } else {
1685            v.push_str(&format!("  reg  [{t}:0] {name};\n  reg  [{t}:0] {name}_next;\n  integer lb_n{ri};\n", t = total - 1));
1686            reset.push_str(&format!("      {name} <= {total}'d0;\n"));
1687            let mut next = format!("  always @(*) begin\n    {name}_next = {name};\n");
1688            for p in 0..np {
1689                let hit = format!("lb_v{p} && lb_rw{p} && lb_a{p} >= {ab}'d{base} && lb_a{p} < {ab}'d{end}");
1690                step.push_str(&format!(
1691                    "      if ({hit}) for (lb_w = 0; lb_w < {bytes}; lb_w = lb_w + 1) if (lb_be{p}[lb_w]) {name}[(lb_a{p} - {ab}'d{base}) * {lb} + lb_w * 8 +: 8] <= lb_d{p}[lb_w * 8 +: 8];\n"
1692                ));
1693                next.push_str(&format!(
1694                    "    if ({hit}) for (lb_n{ri} = 0; lb_n{ri} < {bytes}; lb_n{ri} = lb_n{ri} + 1) if (lb_be{p}[lb_n{ri}]) {name}_next[(lb_a{p} - {ab}'d{base}) * {lb} + lb_n{ri} * 8 +: 8] = lb_d{p}[lb_n{ri} * 8 +: 8];\n"
1695                ));
1696            }
1697            next.push_str("  end\n");
1698            v.push_str(&next);
1699            let mut off = 0;
1700            for n in &r.contract {
1701                let b = bits_of(n);
1702                outputs.push((slot_key(n), format!("{name}_next[{}:{}]", off + b - 1, off)));
1703                off += r.span(b);
1704            }
1705        }
1706        lookup.push_str(&format!(
1707            "      if (l >= {ab}'d{base} && l < {ab}'d{end}) lb_line = {name}[(l - {ab}'d{base}) * {lb} +: {lb}];\n"
1708        ));
1709    }
1710    lookup.push_str("    end\n  endfunction\n");
1711    v.push_str(&lookup);
1712    // Per port, the reads still to answer, oldest first.
1713    let (qa, qn) = (index_bits(DEPTH), index_bits(DEPTH + 1));
1714    for p in 0..np {
1715        v.push_str(&format!(
1716            "  reg  [{a1}:0] lq_a{p} [0:{d1}];\n  reg  [{t1}:0] lq_t{p} [0:{d1}];\n  reg  [{q1}:0] lq_h{p}, lq_l{p};\n  reg  [{n1}:0] lq_n{p};\n\
1717             \x20 wire lb_rd{p} = lb_v{p} && !lb_rw{p};\n  wire lb_pop{p} = (lq_n{p} != {qn}'d0) && lb_rr{p};\n\
1718             \x20 assign {} = (lq_n{p} != {qn}'d0);\n  assign {} = lb_line(lq_a{p}[lq_h{p}]);\n  assign {} = lq_t{p}[lq_h{p}];\n",
1719            bus.slot("lb_rsp_valid", p, 1),
1720            bus.slot("lb_rsp_data", p, lb),
1721            bus.slot("lb_rsp_tag", p, tb),
1722            a1 = ab - 1,
1723            d1 = DEPTH - 1,
1724            t1 = tb - 1,
1725            q1 = qa - 1,
1726            n1 = qn - 1,
1727        ));
1728        reset.push_str(&format!("      lq_h{p} <= {qa}'d0;\n      lq_l{p} <= {qa}'d0;\n      lq_n{p} <= {qn}'d0;\n"));
1729        step.push_str(&format!(
1730            "      if (lb_rd{p}) begin\n        lq_a{p}[lq_l{p}] <= lb_a{p};\n        lq_t{p}[lq_l{p}] <= lb_t{p};\n        lq_l{p} <= lq_l{p} + {qa}'d1;\n        if (lq_n{p} == {qn}'d{DEPTH} && !lb_pop{p}) ovf <= 1'b1;\n      end\n\
1731             \x20     if (lb_pop{p}) lq_h{p} <= lq_h{p} + {qa}'d1;\n      lq_n{p} <= lq_n{p} + (lb_rd{p} ? {qn}'d1 : {qn}'d0) - (lb_pop{p} ? {qn}'d1 : {qn}'d0);\n"
1732        ));
1733    }
1734}
1735
1736/// The wrapper `qorbist_<candidate>(clk, rst, sig)` around `c` for kernel `k` in `mode`. Call
1737/// [`check_ports`] first: the wrapper assumes the binding fits.
1738///
1739/// The operands of the call being presented sit in registers (`p_<port>`, one set per batch slot as
1740/// `p_<port>_<j>`), computed one call ahead from the stimulus state by a per-port function; a batch
1741/// of `n` advances the state `n` steps through one GF(2) matrix per slot. Each result is registered
1742/// when it is captured and folded into the MISR one per cycle in operation order — so no wrapper path
1743/// is longer than one field adder, one jump network or one fold tree, and the measured timing is the
1744/// candidate's.
1745pub fn emit_harness(k: &Kernel, c: &Candidate, mode: Mode) -> String {
1746    emit_harness_as(k, c, mode, &harness_module(c))
1747}
1748
1749/// [`emit_harness`] as the module `name` — the harness hardened as a block of its own, whose top module the flow
1750/// names after the block.
1751pub fn emit_harness_as(k: &Kernel, c: &Candidate, mode: Mode, name: &str) -> String {
1752    let nb = c.batch.max(1);
1753    let sl = |base: &str, j: u32| if nb == 1 { base.to_string() } else { format!("{base}_{j}") };
1754    let opnd = |r: &str| {
1755        let (p, j) = contract_ref(r);
1756        sl(&format!("o_{p}"), j)
1757    };
1758    let mut v = String::new();
1759    v.push_str(&format!(
1760        "// {m} — the comparative-synthesis harness around `{top}` ({id} {kname}, {proto}{batch}).\n\
1761         // Operands: xorshift64(13,7,17) from 64'h{SEED:016x}, advanced per accepted operation, one rotated\n\
1762         // lane word per field, shaped into the {id} contract's domains ({mode:?}). Results: a 64-bit MISR\n\
1763         // advanced per valid result, so the signature is independent of latency and stalls.\n\
1764         module {m}(input clk, input rst, output [15:0] sig);\n\
1765         \x20 reg  [63:0] s;\n\
1766         \x20 reg  [63:0] misr;\n\
1767         \x20 reg  [31:0] ops;\n\
1768         \x20 reg  [31:0] issued;\n",
1769        m = name,
1770        top = c.top,
1771        id = k.id,
1772        kname = k.name,
1773        proto = c.protocol.label(),
1774        batch = if nb > 1 { format!(", {nb} operations per call") } else { String::new() },
1775    ));
1776    if nb == 1 {
1777        v.push_str(
1778            "  wire [63:0] x1 = s ^ (s << 13);\n  wire [63:0] x2 = x1 ^ (x1 >> 7);\n  wire [63:0] s_next = x2 ^ (x2 << 17);\n",
1779        );
1780    } else {
1781        v.push_str("  wire [63:0] s_0 = s;\n");
1782        for j in 1..nb {
1783            v.push_str(&jump_verilog(&format!("s_{j}"), "s", j));
1784        }
1785        v.push_str(&jump_verilog("s_jump", "s", nb));
1786    }
1787    let state = |j: u32| if nb == 1 { "s".to_string() } else { format!("s_{j}") };
1788
1789    // Under go/done the component restarts in the cycle it raises `done`: from that cycle on it is
1790    // shown the next call's operands (`o_<port>`); every other protocol is shown `p_<port>`.
1791    let restart_done = match &c.protocol {
1792        Protocol::GoDone { done, .. } => {
1793            v.push_str(&format!("  wire q_ctl_{done};\n"));
1794            Some(format!("q_ctl_{done}"))
1795        }
1796        _ => None,
1797    };
1798
1799    // One function per input port: its value for the operation whose state is `st`.
1800    let mut lane = 0usize;
1801    let mut operand_reset = String::new();
1802    let mut operand_step = String::new();
1803    for p in &k.inputs {
1804        let b = p.bits;
1805        let n = p.name;
1806        let locals: Vec<String> = (0..p.fields.len()).map(|i| format!("l{}", lane + i)).collect();
1807        v.push_str(&format!(
1808            "  function [{}:0] v_{n}(input [63:0] st);\n    reg [63:0] {};\n    begin\n",
1809            b - 1,
1810            locals.join(", ")
1811        ));
1812        let mut chunks = Vec::new();
1813        for f in &p.fields {
1814            let lw = format!("l{lane}");
1815            v.push_str(&format!(
1816                "      {lw} = {} ^ {};\n",
1817                rotl_verilog("st", lane_rotation(lane)),
1818                hex64((lane as u64).wrapping_mul(LANE_MIX))
1819            ));
1820            chunks.push(field_verilog(f, &lw, mode));
1821            lane += 1;
1822        }
1823        chunks.reverse();
1824        v.push_str(&format!("      v_{n} = {{{}}};\n    end\n  endfunction\n", chunks.join(", ")));
1825        if p.hold > 1 {
1826            // One register for the whole batch: a call never straddles a reload.
1827            let lg = p.hold.trailing_zeros();
1828            let reg = sl(&format!("p_{n}"), 0);
1829            v.push_str(&format!("  reg  [{}:0] {reg};\n", b - 1));
1830            for j in 1..nb {
1831                v.push_str(&format!("  wire [{}:0] {} = {reg};\n", b - 1, sl(&format!("p_{n}"), j)));
1832            }
1833            operand_reset.push_str(&format!("      {reg} <= v_{n}({});\n", hex64(SEED)));
1834            let reload = format!("(issued[{}:0] == {lg}'d{})", lg - 1, p.hold - nb);
1835            operand_step.push_str(&format!("        if {reload} {reg} <= v_{n}(s);\n"));
1836            for j in 0..nb {
1837                let pj = sl(&format!("p_{n}"), j);
1838                let oj = sl(&format!("o_{n}"), j);
1839                match &restart_done {
1840                    Some(done) => v.push_str(&format!("  wire [{}:0] {oj} = {done} ? ({reload} ? v_{n}(s) : {pj}) : {pj};\n", b - 1)),
1841                    None => v.push_str(&format!("  wire [{}:0] {oj} = {pj};\n", b - 1)),
1842                }
1843            }
1844        } else {
1845            for j in 0..nb {
1846                let pj = sl(&format!("p_{n}"), j);
1847                let oj = sl(&format!("o_{n}"), j);
1848                v.push_str(&format!("  reg  [{}:0] {pj};\n", b - 1));
1849                operand_reset.push_str(&format!("      {pj} <= v_{n}({});\n", hex64(advance(SEED, j))));
1850                operand_step.push_str(&format!("        {pj} <= v_{n}({});\n", state(j)));
1851                match &restart_done {
1852                    Some(done) => v.push_str(&format!("  wire [{}:0] {oj} = {done} ? v_{n}({}) : {pj};\n", b - 1, state(j))),
1853                    None => v.push_str(&format!("  wire [{}:0] {oj} = {pj};\n", b - 1)),
1854                }
1855            }
1856        }
1857    }
1858
1859    // DUT input expressions, per DUT port: the bound contract slices, extended to the port width.
1860    let mut dut_conns: Vec<(String, String)> = Vec::new();
1861    let mut by_dut_in: Vec<(String, Vec<&Bind>)> = Vec::new();
1862    for b in &c.inputs {
1863        match by_dut_in.iter_mut().find(|(d, _)| *d == b.dut) {
1864            Some((_, list)) => list.push(b),
1865            None => by_dut_in.push((b.dut.clone(), vec![b])),
1866        }
1867    }
1868    for (dut, binds) in &by_dut_in {
1869        let widths: Vec<u32> = binds
1870            .iter()
1871            .map(|b| b.resolved_width(input_bits(k, contract_ref(&b.contract).0)))
1872            .collect();
1873        let bound: u32 = widths.iter().sum();
1874        let port_bits = c.ports.iter().find(|p| &p.name == dut).map(|p| p.bits).unwrap_or(bound);
1875        let body = binds
1876            .iter()
1877            .zip(&widths)
1878            .rev()
1879            .map(|(b, w)| format!("{}[{}:{}]", opnd(&b.contract), b.lsb + w - 1, b.lsb))
1880            .collect::<Vec<_>>()
1881            .join(", ");
1882        let ext = port_bits.saturating_sub(bound);
1883        let top = binds.last().map(|b| (b, *widths.last().unwrap_or(&1)));
1884        let expr = match top {
1885            _ if ext == 0 => format!("{{{body}}}"),
1886            Some((b, w)) if field_signed(k, contract_ref(&b.contract).0) => {
1887                format!("{{{{{ext}{{{}[{}]}}}}, {body}}}", opnd(&b.contract), b.lsb + w - 1)
1888            }
1889            _ => format!("{{{ext}'d0, {body}}}"),
1890        };
1891        v.push_str(&format!("  wire [{}:0] d_{dut} = {expr};\n", port_bits - 1));
1892        dut_conns.push((dut.clone(), format!("d_{dut}")));
1893    }
1894
1895    // Memory interfaces: inputs answer reads from the presented operands, outputs keep each write.
1896    let mut mem_reset = String::new();
1897    let mut mem_step = String::new();
1898    let mut mem_outputs: Vec<(String, String)> = Vec::new();
1899    let mut mem_next = String::new();
1900    for m in &c.mems {
1901        let first = m.contract.first().cloned().unwrap_or_default();
1902        let first_port = contract_ref(&first).0;
1903        let is_input = k.inputs.iter().any(|p| p.name == first_port);
1904        let bits = if is_input { input_bits(k, first_port) } else { output_bits(k, first_port) };
1905        let word = bits / m.words_per_port;
1906        let total = word * m.words();
1907        let pre = &m.prefix;
1908        let width_of = |name: &str, fallback: u32| c.ports.iter().find(|p| p.name == name).map(|p| p.bits).unwrap_or(fallback);
1909        if is_input {
1910            let content = m.contract.iter().rev().map(|n| opnd(n)).collect::<Vec<_>>().join(", ");
1911            v.push_str(&format!("  wire [{}:0] mem_{pre} = {{{content}}};\n", total - 1));
1912        } else {
1913            v.push_str(&format!("  reg  [{t}:0] mem_{pre};\n  reg  [{t}:0] mem_{pre}_next;\n", t = total - 1));
1914            for (j, n) in m.contract.iter().enumerate() {
1915                mem_outputs.push((slot_key(n), format!("mem_{pre}_next[{}:{}]", (j as u32 + 1) * bits - 1, j as u32 * bits)));
1916            }
1917            mem_next.push_str(&format!("  always @(*) begin\n    mem_{pre}_next = mem_{pre};\n"));
1918        }
1919        let word_at = |addr: &str| {
1920            if m.words() == 1 {
1921                format!("mem_{pre}")
1922            } else {
1923                format!("mem_{pre}[{addr} * {word} +: {word}]")
1924            }
1925        };
1926        let next_at = |addr: &str| {
1927            if m.words() == 1 {
1928                format!("mem_{pre}_next")
1929            } else {
1930                format!("mem_{pre}_next[{addr} * {word} +: {word}]")
1931            }
1932        };
1933        match &m.kind {
1934            MemKind::SeqMem => {
1935                let (rd, wd) = (width_of(&m.port("read_data"), word), width_of(&m.port("write_data"), word));
1936                for (k, (name, bits)) in m.address_ports().into_iter().enumerate() {
1937                    v.push_str(&format!("  wire [{}:0] q_{pre}_addr{k};\n", width_of(&name, bits) - 1));
1938                    dut_conns.push((name, format!("q_{pre}_addr{k}")));
1939                }
1940                v.push_str(&format!(
1941                    "  wire q_{pre}_content_en, q_{pre}_write_en;\n  wire [{}:0] q_{pre}_write_data;\n  reg  [{}:0] m_{pre}_read;\n  reg  m_{pre}_done;\n",
1942                    wd - 1,
1943                    rd - 1
1944                ));
1945                let at = m.word_index();
1946                for (suffix, wire) in [
1947                    ("content_en", format!("q_{pre}_content_en")),
1948                    ("write_en", format!("q_{pre}_write_en")),
1949                    ("write_data", format!("q_{pre}_write_data")),
1950                    ("read_data", format!("m_{pre}_read")),
1951                    ("done", format!("m_{pre}_done")),
1952                ] {
1953                    dut_conns.push((m.port(suffix), wire));
1954                }
1955                mem_reset.push_str(&format!("      m_{pre}_done <= 1'b0;\n"));
1956                mem_step.push_str(&format!("      m_{pre}_done <= q_{pre}_content_en;\n"));
1957                if is_input {
1958                    mem_step.push_str(&format!(
1959                        "      if (q_{pre}_content_en && !q_{pre}_write_en) m_{pre}_read <= {};\n",
1960                        word_at(&at)
1961                    ));
1962                } else {
1963                    mem_step.push_str(&format!(
1964                        "      if (q_{pre}_content_en && q_{pre}_write_en) {} <= q_{pre}_write_data[{}:0];\n      else if (q_{pre}_content_en) m_{pre}_read <= {};\n",
1965                        word_at(&at),
1966                        word - 1,
1967                        word_at(&at)
1968                    ));
1969                    mem_next.push_str(&format!(
1970                        "    if (q_{pre}_content_en && q_{pre}_write_en) {} = q_{pre}_write_data[{}:0];\n",
1971                        next_at(&at),
1972                        word - 1
1973                    ));
1974                }
1975            }
1976            MemKind::Bram { ports } => {
1977                for i in 0..*ports {
1978                    let ad = width_of(&m.port(&format!("address{i}")), m.addr_bits());
1979                    v.push_str(&format!("  wire [{}:0] q_{pre}_address{i};\n  wire q_{pre}_ce{i};\n", ad - 1));
1980                    dut_conns.push((m.port(&format!("address{i}")), format!("q_{pre}_address{i}")));
1981                    dut_conns.push((m.port(&format!("ce{i}")), format!("q_{pre}_ce{i}")));
1982                    let has = |s: &str| c.ports.iter().any(|p| p.name == m.port(s));
1983                    if has(&format!("q{i}")) {
1984                        let qd = width_of(&m.port(&format!("q{i}")), word);
1985                        v.push_str(&format!("  reg  [{}:0] m_{pre}_q{i};\n", qd - 1));
1986                        dut_conns.push((m.port(&format!("q{i}")), format!("m_{pre}_q{i}")));
1987                        let guard = if has(&format!("we{i}")) { format!(" && !q_{pre}_we{i}") } else { String::new() };
1988                        mem_step.push_str(&format!(
1989                            "      if (q_{pre}_ce{i}{guard}) m_{pre}_q{i} <= {};\n",
1990                            word_at(&format!("q_{pre}_address{i}"))
1991                        ));
1992                    }
1993                    if !is_input {
1994                        let dd = width_of(&m.port(&format!("d{i}")), word);
1995                        v.push_str(&format!("  wire q_{pre}_we{i};\n  wire [{}:0] q_{pre}_d{i};\n", dd - 1));
1996                        dut_conns.push((m.port(&format!("we{i}")), format!("q_{pre}_we{i}")));
1997                        dut_conns.push((m.port(&format!("d{i}")), format!("q_{pre}_d{i}")));
1998                        mem_step.push_str(&format!(
1999                            "      if (q_{pre}_ce{i} && q_{pre}_we{i}) {} <= q_{pre}_d{i}[{}:0];\n",
2000                            word_at(&format!("q_{pre}_address{i}")),
2001                            word - 1
2002                        ));
2003                        mem_next.push_str(&format!(
2004                            "    if (q_{pre}_ce{i} && q_{pre}_we{i}) {} = q_{pre}_d{i}[{}:0];\n",
2005                            next_at(&format!("q_{pre}_address{i}")),
2006                            word - 1
2007                        ));
2008                    }
2009                }
2010            }
2011        }
2012        if !is_input {
2013            mem_next.push_str("  end\n");
2014        }
2015    }
2016    v.push_str(&mem_next);
2017    if let Protocol::Launch { bus, .. } = &c.protocol {
2018        emit_line_bus(k, bus, &opnd, &mut v, &mut dut_conns, &mut mem_reset, &mut mem_step, &mut mem_outputs);
2019    }
2020
2021    // DUT outputs: one wire per output port, with an in-order queue when its values are strobed.
2022    let mut out_wires: Vec<String> = Vec::new();
2023    for b in &c.outputs {
2024        if !out_wires.contains(&b.dut) {
2025            out_wires.push(b.dut.clone());
2026        }
2027    }
2028    let strobed = c.outputs.iter().any(|b| b.valid.is_some());
2029    // A strobed port's queue belongs to the one operation of the call it carries, and is taken when every queue of
2030    // that operation holds its result.
2031    let slot_of = |d: &str| c.outputs.iter().find(|b| b.dut == d).map_or(0, |b| contract_ref(&b.contract).1);
2032    let strobe_capture = |j: u32| if nb == 1 { "capture".to_string() } else { format!("capture_{j}") };
2033    let mut fifo_reset = String::new();
2034    let mut fifo_step = String::new();
2035    let mut fifo_ready: Vec<Vec<String>> = vec![Vec::new(); nb as usize];
2036    for d in &out_wires {
2037        let bound = c
2038            .outputs
2039            .iter()
2040            .filter(|b| &b.dut == d)
2041            .map(|b| b.resolved_width(output_bits(k, contract_ref(&b.contract).0)))
2042            .max()
2043            .unwrap_or(1);
2044        let w = c.ports.iter().find(|p| &p.name == d).map(|p| p.bits).unwrap_or(bound);
2045        v.push_str(&format!("  wire [{}:0] q_{d};\n", w.max(1) - 1));
2046        dut_conns.push((d.clone(), format!("q_{d}")));
2047        if let Some(valid) = c.outputs.iter().find(|b| &b.dut == d).and_then(|b| b.valid.clone()) {
2048            v.push_str(&format!(
2049                "  wire q_{d}_vld;\n  reg  [{wb}:0] f_{d} [0:15];\n  reg  [3:0] wp_{d}, rp_{d};\n  reg  [4:0] n_{d};\n  wire [{wb}:0] h_{d} = f_{d}[rp_{d}];\n",
2050                wb = w.max(1) - 1
2051            ));
2052            dut_conns.push((valid, format!("q_{d}_vld")));
2053            fifo_reset.push_str(&format!("      wp_{d} <= 4'd0;\n      rp_{d} <= 4'd0;\n      n_{d} <= 5'd0;\n"));
2054            let slot = slot_of(d);
2055            let take = strobe_capture(slot);
2056            fifo_step.push_str(&format!(
2057                "      if (q_{d}_vld) begin\n        f_{d}[wp_{d}] <= q_{d};\n        wp_{d} <= wp_{d} + 4'd1;\n      end\n\
2058                 \x20     if (q_{d}_vld && n_{d} == 5'd16 && !{take}) ovf <= 1'b1;\n\
2059                 \x20     if ({take}) rp_{d} <= rp_{d} + 4'd1;\n\
2060                 \x20     n_{d} <= n_{d} + (q_{d}_vld ? 5'd1 : 5'd0) - ({take} ? 5'd1 : 5'd0);\n"
2061            ));
2062            fifo_ready[slot as usize].push(format!("(n_{d} != 5'd0)"));
2063        }
2064    }
2065    let source = |d: &str| if strobed { format!("h_{d}") } else { format!("q_{d}") };
2066
2067    // Results: per batch slot, the result wires and the registers they are captured into — a queue
2068    // where a beat schedule delivers the slot again before the fold reaches it.
2069    let register_file = matches!(c.protocol, Protocol::LoadGoRead { .. });
2070    let depths: Vec<u32> = match &c.protocol {
2071        Protocol::Beats { period, captures, .. } => {
2072            beat_capture_depths(*period, captures).unwrap_or_else(|_| vec![1; nb as usize])
2073        }
2074        _ => vec![1; nb as usize],
2075    };
2076    let mut result_capture: Vec<String> = vec![String::new(); nb as usize];
2077    let mut queue_reset = String::new();
2078    for j in 0..nb {
2079        let depth = depths[j as usize];
2080        if depth > 1 {
2081            let (ab, cb) = (index_bits(depth), index_bits(depth + 1));
2082            v.push_str(&format!("  reg  [{}:0] wp_{j}, rp_{j};\n  reg  [{}:0] cn_{j};\n", ab - 1, cb - 1));
2083            queue_reset.push_str(&format!("      wp_{j} <= {ab}'d0;\n      rp_{j} <= {ab}'d0;\n      cn_{j} <= {cb}'d0;\n"));
2084        }
2085        for p in &k.outputs {
2086            let n = p.name;
2087            let (rj, rq) = (sl(&format!("r_{n}"), j), sl(&format!("r_{n}"), j) + "_q");
2088            let key = format!("{n}@{j}");
2089            let expr = if register_file {
2090                v.push_str(&format!("  reg  [{}:0] rd_{n};\n", p.bits - 1));
2091                format!("rd_{n}")
2092            } else if let Some((_, slice)) = mem_outputs.iter().find(|(kk, _)| *kk == key) {
2093                slice.clone()
2094            } else {
2095                let mut binds: Vec<&Bind> = c.outputs.iter().filter(|b| slot_key(&b.contract) == key).collect();
2096                binds.sort_by_key(|b| std::cmp::Reverse(b.lsb));
2097                let parts: Vec<String> = binds
2098                    .iter()
2099                    .map(|b| {
2100                        let w = b.resolved_width(p.bits);
2101                        let dw = b.dut_bits(p.bits);
2102                        let bits = format!("{}[{}:{}]", source(&b.dut), b.dut_lsb + dw - 1, b.dut_lsb);
2103                        if dw < w {
2104                            format!("{{{{{}{{{}[{}]}}}}, {bits}}}", w - dw, source(&b.dut), b.dut_lsb + dw - 1)
2105                        } else {
2106                            bits
2107                        }
2108                    })
2109                    .collect();
2110                format!("{{{}}}", parts.join(", "))
2111            };
2112            if depth > 1 {
2113                v.push_str(&format!(
2114                    "  wire [{b}:0] {rj} = {expr};\n  reg  [{b}:0] {rq}_e [0:{}];\n  wire [{b}:0] {rq} = {rq}_e[rp_{j}];\n",
2115                    depth - 1,
2116                    b = p.bits - 1
2117                ));
2118                result_capture[j as usize].push_str(&format!("        {rq}_e[wp_{j}] <= {rj};\n"));
2119            } else {
2120                v.push_str(&format!("  wire [{b}:0] {rj} = {expr};\n  reg  [{b}:0] {rq};\n", b = p.bits - 1));
2121                result_capture[j as usize].push_str(&format!("        {rq} <= {rj};\n"));
2122            }
2123        }
2124    }
2125
2126    // The fold of each slot's registered result, and the one the fold pointer selects.
2127    let fp_bits = (32 - (nb.max(2) - 1).leading_zeros()).max(1);
2128    for j in 0..nb {
2129        let mut c_idx = 0u32;
2130        let mut terms = Vec::new();
2131        for p in &k.outputs {
2132            let rq = sl(&format!("r_{}", p.name), j) + "_q";
2133            for i in 0..p.bits.div_ceil(64) {
2134                let cb = chunk_bits(p.bits, i as usize);
2135                let chunk = if cb == 64 {
2136                    format!("{rq}[{}:{}]", 64 * i + 63, 64 * i)
2137                } else {
2138                    format!("{{{}'d0, {rq}[{}:{}]}}", 64 - cb, 64 * i + cb - 1, 64 * i)
2139                };
2140                let name = sl(&format!("rc{c_idx}"), j);
2141                v.push_str(&format!("  wire [63:0] {name} = {chunk};\n"));
2142                terms.push(rotl_verilog(&name, c_idx % 64));
2143                c_idx += 1;
2144            }
2145        }
2146        v.push_str(&format!("  wire [63:0] {} = {};\n", sl("fold", j), terms.join(" ^ ")));
2147    }
2148    v.push_str(&format!("  reg  [{}:0] full;\n", nb - 1));
2149    if nb == 1 {
2150        v.push_str("  wire folding = full[0];\n  wire [63:0] fold_sel = fold;\n");
2151        for p in &k.outputs {
2152            v.push_str(&format!("  wire [{}:0] fr_{n} = r_{n}_q;\n", p.bits - 1, n = p.name));
2153        }
2154    } else {
2155        v.push_str(&format!("  reg  [{}:0] fp;\n  wire folding = full[fp];\n", fp_bits - 1));
2156        let sel = |base: &dyn Fn(u32) -> String| {
2157            (0..nb).rev().fold(String::from("64'd0"), |acc, j| {
2158                if acc == "64'd0" && j == nb - 1 {
2159                    base(j)
2160                } else {
2161                    format!("(fp == {fp_bits}'d{j}) ? {} : ({acc})", base(j))
2162                }
2163            })
2164        };
2165        v.push_str(&format!("  wire [63:0] fold_sel = {};\n", sel(&|j| format!("fold_{j}"))));
2166        for p in &k.outputs {
2167            let n = p.name;
2168            v.push_str(&format!("  wire [{}:0] fr_{n} = {};\n", p.bits - 1, sel(&|j| format!("r_{n}_{j}_q"))));
2169        }
2170    }
2171
2172    // Protocol control: `accept` advances the operands, each slot's capture registers its result.
2173    let mut control_regs = String::new();
2174    let mut control_reset = String::new();
2175    let mut control_step = String::new();
2176    let mut slot_capture: Vec<String> = vec!["capture".to_string(); nb as usize];
2177    match &c.protocol {
2178        Protocol::Registered { latency } => {
2179            v.push_str("  wire accept = 1'b1;\n");
2180            if *latency == 0 {
2181                v.push_str("  wire proto_capture = 1'b1;\n");
2182            } else {
2183                let l = *latency;
2184                control_regs.push_str(&format!("  reg  [{}:0] vld;\n", l - 1));
2185                control_reset.push_str(&format!("      vld <= {l}'d0;\n"));
2186                if l == 1 {
2187                    control_step.push_str("      vld <= 1'b1;\n");
2188                } else {
2189                    control_step.push_str(&format!("      vld <= {{vld[{}:0], 1'b1}};\n", l - 2));
2190                }
2191                v.push_str(&format!("  wire proto_capture = vld[{}];\n", l - 1));
2192            }
2193        }
2194        Protocol::StartDone { start, done } => {
2195            control_regs.push_str("  reg  busy;\n");
2196            control_reset.push_str("      busy <= 1'b0;\n");
2197            control_step.push_str(&format!(
2198                "      if (!busy) busy <= 1'b1;\n      else if (q_ctl_{done}) busy <= 1'b0;\n"
2199            ));
2200            v.push_str(&format!("  wire q_ctl_{done};\n  wire c_{start} = !rst && !busy;\n"));
2201            v.push_str(&format!("  wire accept = busy && q_ctl_{done};\n  wire proto_capture = busy && q_ctl_{done};\n"));
2202            dut_conns.push((start.clone(), format!("c_{start}")));
2203            dut_conns.push((done.clone(), format!("q_ctl_{done}")));
2204        }
2205        Protocol::Pipelined { start, done, ii } => {
2206            let ii = (*ii).max(1);
2207            control_regs.push_str("  reg  [31:0] tick;\n");
2208            control_reset.push_str("      tick <= 32'd0;\n");
2209            control_step.push_str(&format!(
2210                "      tick <= (tick == 32'd{}) ? 32'd0 : tick + 32'd1;\n",
2211                ii - 1
2212            ));
2213            v.push_str(&format!(
2214                "  wire q_ctl_{done};\n  wire c_{start} = !rst && (tick == 32'd0);\n  wire accept = c_{start};\n  wire proto_capture = q_ctl_{done};\n"
2215            ));
2216            dut_conns.push((start.clone(), format!("c_{start}")));
2217            dut_conns.push((done.clone(), format!("q_ctl_{done}")));
2218        }
2219        Protocol::GoDone { go, done } => {
2220            v.push_str(&format!(
2221                "  wire c_{go} = !rst;\n  wire accept = q_ctl_{done};\n  wire proto_capture = q_ctl_{done};\n"
2222            ));
2223            dut_conns.push((go.clone(), format!("c_{go}")));
2224            dut_conns.push((done.clone(), format!("q_ctl_{done}")));
2225        }
2226        Protocol::EnableDone { enable, done, clear } => {
2227            control_regs.push_str("  reg  clearing;\n");
2228            control_reset.push_str("      clearing <= 1'b0;\n");
2229            control_step.push_str(&format!("      clearing <= !clearing && q_ctl_{done};\n"));
2230            v.push_str(&format!(
2231                "  wire q_ctl_{done};\n  wire c_{enable} = !rst && !clearing;\n  wire c_{clear} = clearing;\n\
2232                 \x20 wire accept = !clearing && q_ctl_{done};\n  wire proto_capture = accept;\n"
2233            ));
2234            dut_conns.push((enable.clone(), format!("c_{enable}")));
2235            dut_conns.push((done.clone(), format!("q_ctl_{done}")));
2236            dut_conns.push((clear.clone(), format!("c_{clear}")));
2237        }
2238        Protocol::Beats { period, drives, captures } => {
2239            let pw = (32 - (period.max(&2) - 1).leading_zeros()).max(1);
2240            let deepest = captures.iter().copied().max().unwrap_or(0);
2241            control_regs.push_str(&format!("  reg  [{}:0] beat;\n", pw - 1));
2242            control_reset.push_str(&format!("      beat <= {pw}'d0;\n"));
2243            control_step.push_str(&format!(
2244                "      beat <= (beat == {pw}'d{}) ? {pw}'d0 : beat + {pw}'d1;\n",
2245                period - 1
2246            ));
2247            v.push_str(&format!("  wire accept = (beat == {pw}'d{});\n  wire proto_capture = 1'b0;\n", period - 1));
2248            if deepest > 0 {
2249                control_regs.push_str(&format!("  reg  [{}:0] began;\n", deepest - 1));
2250                control_reset.push_str(&format!("      began <= {deepest}'d0;\n"));
2251                if deepest == 1 {
2252                    control_step.push_str(&format!("      began <= (beat == {pw}'d0);\n"));
2253                } else {
2254                    control_step.push_str(&format!("      began <= {{began[{}:0], (beat == {pw}'d0)}};\n", deepest - 2));
2255                }
2256            }
2257            for (j, off) in captures.iter().enumerate() {
2258                slot_capture[j] = if *off == 0 {
2259                    format!("(!rst && beat == {pw}'d0)")
2260                } else {
2261                    format!("began[{}]", off - 1)
2262                };
2263            }
2264            let mut by_port: Vec<(String, Vec<&Drive>)> = Vec::new();
2265            for d in drives {
2266                match by_port.iter_mut().find(|(p, _)| *p == d.dut) {
2267                    Some((_, list)) => list.push(d),
2268                    None => by_port.push((d.dut.clone(), vec![d])),
2269                }
2270            }
2271            // The previous call's operands, for lagged drives: taken from the call's registers as the
2272            // next call begins, zero before the first call.
2273            let mut kept: Vec<String> = Vec::new();
2274            for d in drives {
2275                if let DriveValue::Previous { contract, .. } = &d.value {
2276                    let (p, j) = contract_ref(contract);
2277                    let prev = sl(&format!("pv_{p}"), j);
2278                    if !kept.contains(&prev) {
2279                        let b = input_bits(k, p);
2280                        v.push_str(&format!("  reg  [{}:0] {prev};\n", b - 1));
2281                        operand_reset.push_str(&format!("      {prev} <= {b}'d0;\n"));
2282                        operand_step.push_str(&format!("        {prev} <= {};\n", sl(&format!("p_{p}"), j)));
2283                        kept.push(prev);
2284                    }
2285                }
2286            }
2287            for (port, list) in &by_port {
2288                let w = c.ports.iter().find(|p| &p.name == port).map(|p| p.bits).unwrap_or(64);
2289                let mut arms = String::new();
2290                for d in list {
2291                    let value = match &d.value {
2292                        DriveValue::Const(x) => format!("{w}'d{x}"),
2293                        DriveValue::Slice { contract, lsb, width } | DriveValue::Previous { contract, lsb, width } => {
2294                            let src = match &d.value {
2295                                DriveValue::Previous { .. } => {
2296                                    let (p, j) = contract_ref(contract);
2297                                    sl(&format!("pv_{p}"), j)
2298                                }
2299                                _ => opnd(contract),
2300                            };
2301                            let slice = format!("{src}[{}:{}]", lsb + width - 1, lsb);
2302                            let ext = w.saturating_sub(*width);
2303                            if ext == 0 {
2304                                slice
2305                            } else if field_signed(k, contract_ref(contract).0) {
2306                                format!("{{{{{ext}{{{src}[{}]}}}}, {slice}}}", lsb + width - 1)
2307                            } else {
2308                                format!("{{{ext}'d0, {slice}}}")
2309                            }
2310                        }
2311                    };
2312                    arms.push_str(&format!("      {pw}'d{}: bd_{port} = {value};\n", d.beat));
2313                }
2314                v.push_str(&format!(
2315                    "  reg  [{}:0] bd_{port};\n  always @(*) begin\n    bd_{port} = {w}'d0;\n    case (beat)\n{arms}      default: ;\n    endcase\n  end\n",
2316                    w - 1
2317                ));
2318                dut_conns.push((port.clone(), format!("bd_{port}")));
2319            }
2320        }
2321        Protocol::Launch { start, busy, dcr, setup, flush, .. } => {
2322            // 0 setup writes, 1 start, 2 until busy rises, 3 until it falls, 4 flush read, 5 until it is
2323            // answered, 6 capture — once the previous call's results are all folded, since a call no
2324            // longer than the fold of a batch would otherwise land on results still unfolded.
2325            let port_bits = |n: &str| c.ports.iter().find(|p| p.name == n).map(|p| p.bits).unwrap_or(1);
2326            let (aw, dw) = (port_bits(&dcr.addr), port_bits(&dcr.data));
2327            let sw = index_bits(setup.len().max(1) as u32);
2328            control_regs.push_str(&format!("  reg  [2:0] lph;\n  reg  [{}:0] lsi;\n", sw - 1));
2329            let first = if setup.is_empty() { 1 } else { 0 };
2330            control_reset.push_str(&format!("      lph <= 3'd{first};\n      lsi <= {sw}'d0;\n"));
2331            let mut arms = String::new();
2332            for (i, w) in setup.iter().enumerate() {
2333                arms.push_str(&format!("        {sw}'d{i}: begin c_dcr_addr = {aw}'d{}; c_dcr_data = {dw}'d{}; end\n", w.addr, w.data));
2334            }
2335            let flush_arm = match flush {
2336                Some(f) => format!("    if (lph == 3'd4) begin c_dcr_valid = 1'b1; c_dcr_rw = 1'b0; c_dcr_addr = {aw}'d{f}; end\n"),
2337                None => String::new(),
2338            };
2339            v.push_str(&format!(
2340                "  wire q_ctl_{busy};\n  wire q_ctl_{rsp};\n  wire c_{start} = (lph == 3'd1);\n\
2341                 \x20 reg  c_dcr_valid, c_dcr_rw;\n  reg  [{a1}:0] c_dcr_addr;\n  reg  [{d1}:0] c_dcr_data;\n\
2342                 \x20 always @(*) begin\n    c_dcr_valid = 1'b0;\n    c_dcr_rw = 1'b1;\n    c_dcr_addr = {aw}'d0;\n    c_dcr_data = {dw}'d0;\n\
2343                 \x20   if (lph == 3'd0) begin\n      c_dcr_valid = 1'b1;\n      case (lsi)\n{arms}        default: ;\n      endcase\n    end\n{flush_arm}  end\n\
2344                 \x20 wire proto_capture = (lph == 3'd6) && !(|full);\n  wire accept = proto_capture;\n",
2345                rsp = dcr.rsp_valid,
2346                a1 = aw - 1,
2347                d1 = dw - 1,
2348            ));
2349            let after_busy = if flush.is_some() { 4 } else { 6 };
2350            control_step.push_str(&format!(
2351                "      case (lph)\n\
2352                 \x20       3'd0: begin if (lsi == {sw}'d{last}) lph <= 3'd1; else lsi <= lsi + {sw}'d1; end\n\
2353                 \x20       3'd1: lph <= 3'd2;\n\
2354                 \x20       3'd2: if (q_ctl_{busy}) lph <= 3'd3;\n\
2355                 \x20       3'd3: if (!q_ctl_{busy}) lph <= 3'd{after_busy};\n\
2356                 \x20       3'd4: lph <= 3'd5;\n\
2357                 \x20       3'd5: if (q_ctl_{rsp}) lph <= 3'd6;\n\
2358                 \x20       3'd6: if (!(|full)) lph <= 3'd1;\n\
2359                 \x20       default: lph <= 3'd1;\n\
2360                 \x20     endcase\n",
2361                last = setup.len().saturating_sub(1),
2362                rsp = dcr.rsp_valid,
2363            ));
2364            dut_conns.push((start.clone(), format!("c_{start}")));
2365            dut_conns.push((busy.clone(), format!("q_ctl_{busy}")));
2366            dut_conns.push((dcr.valid.clone(), "c_dcr_valid".into()));
2367            dut_conns.push((dcr.rw.clone(), "c_dcr_rw".into()));
2368            dut_conns.push((dcr.addr.clone(), "c_dcr_addr".into()));
2369            dut_conns.push((dcr.data.clone(), "c_dcr_data".into()));
2370            dut_conns.push((dcr.rsp_valid.clone(), format!("q_ctl_{}", dcr.rsp_valid)));
2371        }
2372        Protocol::LoadGoRead { load, din, go, done, raddr, dout, reset_each_op, load_slots, read_slots } => {
2373            let port_bits = |n: &str, fallback: u32| c.ports.iter().find(|p| p.name == n).map(|p| p.bits).unwrap_or(fallback);
2374            let (dw, ow, aw) = (port_bits(din, 64), port_bits(dout, 64), port_bits(raddr, 8));
2375            let (ns, nr) = (load_slots.len(), read_slots.len());
2376            control_regs.push_str("  reg  [2:0] ph;\n  reg  [15:0] li;\n  wire dut_rst = (ph == 3'd0);\n");
2377            let mut din_case = String::new();
2378            for (i, s) in load_slots.iter().enumerate() {
2379                let slice = format!("p_{}[{}:{}]", s.contract, s.lsb + s.width - 1, s.lsb);
2380                let value = if s.width < dw { format!("{{{}'d0, {slice}}}", dw - s.width) } else { slice };
2381                din_case.push_str(&format!("      16'd{i}: c_din = {value};\n"));
2382            }
2383            let mut read_case = String::new();
2384            for (i, s) in read_slots.iter().enumerate() {
2385                read_case.push_str(&format!(
2386                    "          16'd{i}: rd_{}[{}:{}] <= q_dout[{}:0];\n",
2387                    s.contract,
2388                    s.lsb + s.width - 1,
2389                    s.lsb,
2390                    s.width - 1
2391                ));
2392            }
2393            let start = if *reset_each_op { 0 } else { 1 };
2394            v.push_str(&format!(
2395                "  wire q_ctl_{done};\n  wire [{ow1}:0] q_dout;\n  reg  [{dw1}:0] c_din;\n  always @(*) begin\n    c_din = {dw}'d0;\n    case (li)\n{din_case}      default: ;\n    endcase\n  end\n\
2396                 \x20 wire c_{load} = (ph == 3'd1);\n  wire c_{go} = (ph == 3'd2);\n  wire [{aw1}:0] c_{raddr} = li[{aw1}:0];\n\
2397                 \x20 wire proto_capture = (ph == 3'd5);\n  wire accept = proto_capture;\n",
2398                ow1 = ow - 1,
2399                dw1 = dw - 1,
2400                aw1 = aw - 1,
2401            ));
2402            dut_conns.push((load.clone(), format!("c_{load}")));
2403            dut_conns.push((din.clone(), "c_din".into()));
2404            dut_conns.push((go.clone(), format!("c_{go}")));
2405            dut_conns.push((done.clone(), format!("q_ctl_{done}")));
2406            dut_conns.push((raddr.clone(), format!("c_{raddr}")));
2407            dut_conns.push((dout.clone(), "q_dout".into()));
2408            control_reset.push_str(&format!("      ph <= 3'd{start};\n      li <= 16'd0;\n"));
2409            control_step.push_str(&format!(
2410                "      case (ph)\n\
2411                 \x20       3'd0: begin ph <= 3'd1; li <= 16'd0; end\n\
2412                 \x20       3'd1: begin if (li == 16'd{last_load}) begin ph <= 3'd2; li <= 16'd0; end else li <= li + 16'd1; end\n\
2413                 \x20       3'd2: ph <= 3'd3;\n\
2414                 \x20       3'd3: if (q_ctl_{done}) begin ph <= 3'd4; li <= 16'd0; end\n\
2415                 \x20       3'd4: begin\n\
2416                 \x20         case (li)\n{read_case}\x20         default: ;\n\x20         endcase\n\
2417                 \x20         if (li == 16'd{last_read}) ph <= 3'd5; else li <= li + 16'd1;\n\
2418                 \x20       end\n\
2419                 \x20       3'd5: begin ph <= 3'd{start}; li <= 16'd0; end\n\
2420                 \x20       default: ;\n      endcase\n",
2421                last_load = ns.saturating_sub(1),
2422                last_read = nr.saturating_sub(1),
2423            ));
2424        }
2425    }
2426    v.push_str(&control_regs);
2427    if strobed {
2428        for j in 0..nb {
2429            v.push_str(&format!("  wire {} = {};\n", strobe_capture(j), fifo_ready[j as usize].join(" && ")));
2430            slot_capture[j as usize] = strobe_capture(j);
2431        }
2432    } else {
2433        v.push_str("  wire capture = proto_capture;\n");
2434    }
2435    v.push_str("  reg  ovf;\n");
2436
2437    // The DUT.
2438    let mut conns = vec![format!(".{}(clk)", c.clock)];
2439    let per_op_reset = matches!(c.protocol, Protocol::LoadGoRead { reset_each_op: true, .. });
2440    if let Some(r) = &c.reset {
2441        let level = if per_op_reset { "rst || dut_rst" } else { "rst" };
2442        conns.push(format!(
2443            ".{}({})",
2444            r.port,
2445            if r.active_high { level.to_string() } else { format!("!({level})") }
2446        ));
2447    }
2448    for (port, expr) in &dut_conns {
2449        conns.push(format!(".{port}({expr})"));
2450    }
2451    // An input the binding does not name is held at 0, never left floating.
2452    for p in c.ports.iter().filter(|p| p.dir == Dir::In) {
2453        let named = p.name == c.clock
2454            || c.reset.as_ref().map_or(false, |r| r.port == p.name)
2455            || dut_conns.iter().any(|(port, _)| *port == p.name);
2456        if !named {
2457            conns.push(format!(".{}({}'d0)", p.name, p.bits));
2458        }
2459    }
2460    v.push_str(&format!(
2461        "  (* keep_hierarchy *) {} u_dut(\n    {}\n  );\n",
2462        c.top,
2463        conns.join(",\n    ")
2464    ));
2465
2466    // Capture each slot; fold the slot the pointer selects; refuse a capture onto an unfolded result.
2467    let mut capture_step = String::new();
2468    for j in 0..nb {
2469        let cap = &slot_capture[j as usize];
2470        let folds_j = if nb == 1 { "folding".to_string() } else { format!("(folding && fp == {fp_bits}'d{j})") };
2471        let depth = depths[j as usize];
2472        if depth > 1 {
2473            let (ab, cb) = (index_bits(depth), index_bits(depth + 1));
2474            let next = |r: &str| format!("({r} == {ab}'d{}) ? {ab}'d0 : {r} + {ab}'d1", depth - 1);
2475            let count = format!("cn_{j} + ({cap} ? {cb}'d1 : {cb}'d0) - ({folds_j} ? {cb}'d1 : {cb}'d0)");
2476            capture_step.push_str(&format!(
2477                "      if ({cap}) begin\n{}        wp_{j} <= {};\n        if (cn_{j} == {cb}'d{depth} && !{folds_j}) ovf <= 1'b1;\n      end\n\
2478                 \x20     if ({folds_j}) rp_{j} <= {};\n      cn_{j} <= {count};\n      full[{j}] <= ({count}) != {cb}'d0;\n",
2479                result_capture[j as usize],
2480                next(&format!("wp_{j}")),
2481                next(&format!("rp_{j}")),
2482            ));
2483        } else {
2484            capture_step.push_str(&format!(
2485                "      if ({cap}) begin\n{}        if (full[{j}] && !{folds_j}) ovf <= 1'b1;\n      end\n      full[{j}] <= {cap} || (full[{j}] && !{folds_j});\n",
2486                result_capture[j as usize]
2487            ));
2488        }
2489    }
2490    let advance_fp = if nb == 1 {
2491        String::new()
2492    } else {
2493        format!("        fp <= (fp == {fp_bits}'d{}) ? {fp_bits}'d0 : fp + {fp_bits}'d1;\n", nb - 1)
2494    };
2495    let reset_fp = if nb == 1 { String::new() } else { format!("      fp <= {fp_bits}'d0;\n") } + &queue_reset;
2496    let (state_reset, state_step, issued_step) = if nb == 1 {
2497        (hex64(next_state(SEED)), "s_next".to_string(), "32'd1".to_string())
2498    } else {
2499        (hex64(advance(SEED, nb)), "s_jump".to_string(), format!("32'd{nb}"))
2500    };
2501    v.push_str(&format!(
2502        "  assign sig = misr[15:0];\n\
2503         \x20 always @(posedge clk) begin\n\
2504         \x20   if (rst) begin\n\
2505         \x20     s <= {state_reset};\n\
2506         \x20     misr <= 64'd0;\n\
2507         \x20     ops <= 32'd0;\n\
2508         \x20     issued <= 32'd0;\n\
2509         \x20     full <= {nb}'d0;\n\
2510         \x20     ovf <= 1'b0;\n\
2511         {reset_fp}{operand_reset}{control_reset}{mem_reset}{fifo_reset}\
2512         \x20   end else begin\n\
2513         {control_step}{mem_step}{fifo_step}\
2514         \x20     if (accept) begin\n\
2515         \x20       s <= {state_step};\n\
2516         \x20       issued <= issued + {issued_step};\n\
2517         {operand_step}\
2518         \x20     end\n\
2519         {capture_step}\
2520         \x20     if (folding) begin\n\
2521         \x20       misr <= {{misr[62:0], ^misr}} ^ fold_sel;\n\
2522         \x20       ops <= ops + 32'd1;\n\
2523         {advance_fp}\
2524         \x20     end\n\
2525         \x20   end\n\
2526         \x20 end\n\
2527         endmodule\n",
2528    ));
2529    v
2530}
2531
2532/// The power block `<name>` of a design measured without a functional harness — a memory controller, driven at
2533/// gate level by the K7 traffic testbench: its clock and reset renamed `clk` and `rst` (active high), every other port
2534/// passed straight through under its own name, the design kept whole as `u_dut`, so the flow's cells are told apart
2535/// from its own. A port named `clk` or `rst` that is not the clock or reset would collide, and is refused.
2536pub fn emit_pass_wrapper(name: &str, top: &str, clock: &str, reset: Option<&Reset>, ports: &[DutPort]) -> String {
2537    let mut decls = vec!["input clk".to_string()];
2538    if reset.is_some() {
2539        decls.push("input rst".to_string());
2540    }
2541    let mut conns = Vec::new();
2542    for p in ports {
2543        if p.name == clock {
2544            conns.push(format!(".{}(clk)", p.name));
2545            continue;
2546        }
2547        if let Some(r) = reset.filter(|r| r.port == p.name) {
2548            conns.push(format!(".{}({})", p.name, if r.active_high { "rst" } else { "!rst" }));
2549            continue;
2550        }
2551        assert!(p.name != "clk" && p.name != "rst", "{top}: port {} would collide with the block's clock or reset", p.name);
2552        let range = if p.bits > 1 { format!("[{}:0] ", p.bits - 1) } else { String::new() };
2553        decls.push(format!("{} {range}{}", if p.dir == Dir::In { "input" } else { "output" }, p.name));
2554        conns.push(format!(".{0}({0})", p.name));
2555    }
2556    format!(
2557        "// {name} — `{top}` as a block of its own: every port passed through, its clock and reset as clk and rst, the \
2558         design kept whole.\nmodule {name}(\n  {}\n);\n  (* keep_hierarchy *) {top} u_dut(\n    {}\n  );\nendmodule\n",
2559        decls.join(",\n  "),
2560        conns.join(",\n    ")
2561    )
2562}
2563
2564/// The synthesis wrapper `qorsyn_<name>(clk, rst, sig)` of a design measured without a functional
2565/// contract (a memory controller, whose functional gate is a board memtest): every input of `top`
2566/// from a register the seeded xorshift64 stimulus refills each cycle, one rotated lane word per 64
2567/// bits as the harness draws operands; every output bit folded into a registered 64-bit XOR whose low
2568/// 16 bits are `sig`; the design kept whole as `u_dut`, so its cells and its register-to-register
2569/// paths are what synthesis and place-and-route measure.
2570pub fn emit_fold_wrapper(name: &str, top: &str, clock: &str, reset: Option<&Reset>, ports: &[DutPort]) -> String {
2571    let mut v = format!(
2572        "// qorsyn_{name} — the synthesis wrapper around `{top}`: registered stimulus in, every output folded out.\n\
2573         module qorsyn_{name}(input clk, input rst, output [15:0] sig);\n\
2574         \x20 reg  [63:0] s;\n  reg  [63:0] fold;\n\
2575         \x20 wire [63:0] x1 = s ^ (s << 13);\n  wire [63:0] x2 = x1 ^ (x1 >> 7);\n  wire [63:0] s_next = x2 ^ (x2 << 17);\n"
2576    );
2577    let mut conns = Vec::new();
2578    let mut refill = String::new();
2579    let mut terms = Vec::new();
2580    let mut lane = 0usize;
2581    for p in ports {
2582        if p.name == clock {
2583            conns.push(format!(".{}(clk)", p.name));
2584            continue;
2585        }
2586        if let Some(r) = reset.filter(|r| r.port == p.name) {
2587            conns.push(format!(".{}({})", p.name, if r.active_high { "rst" } else { "!rst" }));
2588            continue;
2589        }
2590        match p.dir {
2591            Dir::In => {
2592                v.push_str(&format!("  reg  [{}:0] in_{};\n", p.bits - 1, p.name));
2593                let mut chunks = Vec::new();
2594                for _ in 0..p.bits.div_ceil(64) {
2595                    chunks.push(format!("({} ^ {})", rotl_verilog("s", lane_rotation(lane)), hex64((lane as u64).wrapping_mul(LANE_MIX))));
2596                    lane += 1;
2597                }
2598                chunks.reverse();
2599                refill.push_str(&format!("    in_{} <= {{{}}};\n", p.name, chunks.join(", ")));
2600                conns.push(format!(".{0}(in_{0})", p.name));
2601            }
2602            Dir::Out => {
2603                v.push_str(&format!("  wire [{}:0] out_{};\n", p.bits - 1, p.name));
2604                for c in 0..p.bits.div_ceil(64) {
2605                    let (lo, hi) = (64 * c, (64 * c + 64).min(p.bits) - 1);
2606                    let width = hi - lo + 1;
2607                    let k = terms.len();
2608                    let chunk = if width == 64 {
2609                        format!("out_{}[{hi}:{lo}]", p.name)
2610                    } else {
2611                        format!("{{{}'d0, out_{}[{hi}:{lo}]}}", 64 - width, p.name)
2612                    };
2613                    v.push_str(&format!("  wire [63:0] fo_{k} = {chunk};\n"));
2614                    terms.push(rotl_verilog(&format!("fo_{k}"), (k % 64) as u32));
2615                }
2616                conns.push(format!(".{0}(out_{0})", p.name));
2617            }
2618        }
2619    }
2620    let folded = if terms.is_empty() { "64'd0".to_string() } else { terms.join(" ^ ") };
2621    v.push_str(&format!(
2622        "  (* keep_hierarchy *) {top} u_dut(\n    {}\n  );\n\
2623         \x20 always @(posedge clk) begin\n{refill}    if (rst) begin\n      s <= {};\n      fold <= 64'd0;\n    end else begin\n\
2624         \x20     s <= s_next;\n      fold <= {{fold[62:0], fold[63]}} ^ {folded};\n    end\n  end\n\
2625         \x20 assign sig = fold[15:0];\nendmodule\n",
2626        conns.join(",\n    "),
2627        hex64(SEED),
2628    ));
2629    v
2630}
2631
2632/// A testbench that resets the harness for five cycles and runs it until `ops` results are folded.
2633/// It prints `QOR first cycles <n>` when the first result is folded, `QOR mid ops <n> cycles <n>` at
2634/// half the run, and `QOR ops <n> misr <hex> cycles <n>` at the end; an unknown MISR bit prints
2635/// `QOR X op <n>` and stops. Two watchdogs end a run that cannot finish: `max_cycles` in all
2636/// (`QOR TIMEOUT ops <n>`), and `max_idle` cycles without a folded result (`QOR TIMEOUT idle
2637/// <max_idle> cycles ops <n> cycle <n>`), which refuses a candidate that has stopped answering long
2638/// before the whole budget would.
2639pub fn emit_testbench(c: &Candidate, ops: usize, max_cycles: u64, max_idle: u64) -> String {
2640    emit_testbench_as(&harness_module(c), ops, max_cycles, max_idle)
2641}
2642
2643/// [`emit_testbench`] around the harness module `m` — one emitted by [`emit_harness_as`].
2644pub fn emit_testbench_as(m: &str, ops: usize, max_cycles: u64, max_idle: u64) -> String {
2645    let mid = (ops / 2).max(1);
2646    format!(
2647        "`timescale 1ns/1ps\n\
2648         module tb_qor;\n\
2649         \x20 reg clk = 1'b0;\n\
2650         \x20 reg rst = 1'b1;\n\
2651         \x20 wire [15:0] sig;\n\
2652         \x20 integer cycles = 0;\n\
2653         \x20 integer idle = 0;\n\
2654         \x20 integer last_ops = 0;\n\
2655         \x20 reg first_seen = 1'b0;\n\
2656         \x20 reg mid_seen = 1'b0;\n\
2657         \x20 {m} u_h(.clk(clk), .rst(rst), .sig(sig));\n\
2658         \x20 always #5 clk = ~clk;\n\
2659         \x20 initial begin\n\
2660         \x20   repeat (5) @(posedge clk);\n\
2661         \x20   rst <= 1'b0;\n\
2662         \x20 end\n\
2663         \x20 always @(posedge clk) begin\n\
2664         \x20   if (!rst) begin\n\
2665         \x20     cycles = cycles + 1;\n\
2666         \x20     if (^u_h.misr === 1'bx || u_h.ovf === 1'b1) begin\n\
2667         \x20       $display(\"QOR X op %0d\", u_h.ops);\n\
2668         \x20       $finish;\n\
2669         \x20     end\n\
2670         \x20     if (!first_seen && u_h.ops >= 1) begin\n\
2671         \x20       first_seen = 1'b1;\n\
2672         \x20       $display(\"QOR first cycles %0d\", cycles);\n\
2673         \x20     end\n\
2674         \x20     if (!mid_seen && u_h.ops >= {mid}) begin\n\
2675         \x20       mid_seen = 1'b1;\n\
2676         \x20       $display(\"QOR mid ops %0d cycles %0d\", u_h.ops, cycles);\n\
2677         \x20     end\n\
2678         \x20     if (u_h.ops >= {ops}) begin\n\
2679         \x20       $display(\"QOR ops %0d misr %016h cycles %0d\", u_h.ops, u_h.misr, cycles);\n\
2680         \x20       $finish;\n\
2681         \x20     end\n\
2682         \x20     if (cycles > {max_cycles}) begin\n\
2683         \x20       $display(\"QOR TIMEOUT ops %0d\", u_h.ops);\n\
2684         \x20       $finish;\n\
2685         \x20     end\n\
2686         \x20     if (u_h.ops != last_ops) begin last_ops = u_h.ops; idle = 0; end\n\
2687         \x20     else idle = idle + 1;\n\
2688         \x20     if (idle >= {max_idle}) begin\n\
2689         \x20       $display(\"QOR TIMEOUT idle {max_idle} cycles ops %0d cycle %0d\", u_h.ops, cycles);\n\
2690         \x20       $finish;\n\
2691         \x20     end\n\
2692         \x20   end\n\
2693         \x20 end\n\
2694         endmodule\n",
2695    )
2696}
2697
2698/// [`emit_testbench`]'s run with every folded result printed as `QOR r <op> <port hex>...` (kernel output
2699/// order, each port in hex at its contract width), for locating the first operation a candidate gets wrong.
2700pub fn emit_trace_testbench(k: &Kernel, c: &Candidate, ops: usize, max_cycles: u64, max_idle: u64) -> String {
2701    emit_trace_testbench_as(k, &harness_module(c), ops, max_cycles, max_idle)
2702}
2703
2704/// [`emit_trace_testbench`] around the harness module `m` — one emitted by [`emit_harness_as`].
2705pub fn emit_trace_testbench_as(k: &Kernel, m: &str, ops: usize, max_cycles: u64, max_idle: u64) -> String {
2706    let tb = emit_testbench_as(m, ops, max_cycles, max_idle);
2707    let fmt: Vec<&str> = k.outputs.iter().map(|_| "%h").collect();
2708    let args: Vec<String> = k.outputs.iter().map(|p| format!("u_h.fr_{}", p.name)).collect();
2709    let trace = format!(
2710        "  always @(posedge clk) if (!rst && u_h.folding) $display(\"QOR r %0d {}\", u_h.ops, {});\nendmodule\n",
2711        fmt.join(" "),
2712        args.join(", ")
2713    );
2714    tb.trim_end().trim_end_matches("endmodule").to_string() + &trace
2715}
2716
2717/// The golden results of `results` as `emit_trace_testbench` prints them: one line per operation,
2718/// `<op> <port hex>...`, each port in hex at its contract width.
2719pub fn trace_lines(k: &Kernel, results: &[Words]) -> String {
2720    let mut s = String::new();
2721    for (op, r) in results.iter().enumerate() {
2722        let ports: Vec<String> = k
2723            .outputs
2724            .iter()
2725            .zip(r)
2726            .map(|(p, chunks)| {
2727                let digits = p.bits.div_ceil(4) as usize;
2728                let mut hex = String::new();
2729                for (i, w) in chunks.iter().enumerate().rev() {
2730                    let bits = chunk_bits(p.bits, i);
2731                    hex.push_str(&format!("{:0width$x}", mask_bits(*w, bits), width = bits.div_ceil(4) as usize));
2732                }
2733                format!("{:0>digits$}", hex.trim_start_matches('0'), digits = digits)
2734            })
2735            .collect();
2736        s.push_str(&format!("{op} {}\n", ports.join(" ")));
2737    }
2738    s
2739}
2740
2741/// A finished testbench run: results folded, the signature, total cycles, the cycle the first
2742/// result was folded on, and the (ops, cycles) mark at half the run.
2743#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2744pub struct Run {
2745    pub ops: usize,
2746    pub misr: u64,
2747    pub cycles: u64,
2748    pub first_cycles: Option<u64>,
2749    pub mid: Option<(usize, u64)>,
2750}
2751
2752impl Run {
2753    /// Cycles per result in steady state: the second half of the run, so the pipeline fill is out.
2754    pub fn steady_cycles_per_op(&self) -> Option<f64> {
2755        let (mo, mc) = self.mid?;
2756        (self.ops > mo).then(|| (self.cycles - mc) as f64 / (self.ops - mo) as f64)
2757    }
2758}
2759
2760/// The verdict a testbench printed.
2761pub fn parse_run(stdout: &str) -> Result<Run, QorError> {
2762    let num = |s: &str, line: &str| s.parse::<u64>().map_err(|_| QorError::Testbench(line.to_string()));
2763    let mut first_cycles = None;
2764    let mut mid = None;
2765    for line in stdout.lines() {
2766        let words: Vec<&str> = line.split_whitespace().collect();
2767        match words.as_slice() {
2768            ["QOR", "X", "op", n, ..] => {
2769                return Err(QorError::Unknown { op: n.parse().unwrap_or(usize::MAX) });
2770            }
2771            ["QOR", "TIMEOUT", ..] => return Err(QorError::Testbench(line.trim().to_string())),
2772            ["QOR", "first", "cycles", c, ..] => first_cycles = Some(num(c, line)?),
2773            ["QOR", "mid", "ops", n, "cycles", c, ..] => mid = Some((num(n, line)? as usize, num(c, line)?)),
2774            ["QOR", "ops", n, "misr", h, rest @ ..] => {
2775                let ops = num(n, line)? as usize;
2776                let misr = u64::from_str_radix(h, 16).map_err(|_| QorError::Testbench(line.to_string()))?;
2777                let cycles = match rest {
2778                    ["cycles", c, ..] => num(c, line)?,
2779                    _ => 0,
2780                };
2781                return Ok(Run { ops, misr, cycles, first_cycles, mid });
2782            }
2783            _ => {}
2784        }
2785    }
2786    Err(QorError::Testbench(format!("no QOR verdict in {} lines of output", stdout.lines().count())))
2787}
2788
2789/// The manifest of third-party candidates.
2790pub const MANIFEST: &str = "benchmarks/silicon/qor/candidates.json";
2791
2792/// One generated RTL file of a third-party candidate and the SHA3-256 of its bytes.
2793#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
2794pub struct ManifestFile {
2795    pub path: String,
2796    pub sha3_256: String,
2797}
2798
2799/// A third-party candidate: the harness-facing description, the tool (a key of
2800/// `scripts/qor/qor-tools.toml`), the source it read and the exact command that generated its files.
2801#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
2802pub struct ManifestEntry {
2803    pub candidate: Candidate,
2804    pub tool: String,
2805    pub source: String,
2806    pub generate: String,
2807    pub files: Vec<ManifestFile>,
2808    pub interface_note: String,
2809}
2810
2811/// A candidate a tool would not build — its crash, its error, or its deadline — kept in the manifest
2812/// so the comparison prints a refusal in that cell rather than a silent gap.
2813#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
2814pub struct ManifestRefusal {
2815    pub kernel: String,
2816    pub name: String,
2817    pub tool: String,
2818    pub generate: String,
2819    pub reason: String,
2820}
2821
2822#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
2823pub struct Manifest {
2824    pub schema: String,
2825    pub candidates: Vec<ManifestEntry>,
2826    #[serde(default)]
2827    pub refusals: Vec<ManifestRefusal>,
2828}
2829
2830/// The manifest under `root`.
2831pub fn read_manifest(root: &std::path::Path) -> Result<Manifest, QorError> {
2832    let path = root.join(MANIFEST);
2833    let text = std::fs::read_to_string(&path).map_err(|e| QorError::Build(format!("{}: {e}", path.display())))?;
2834    serde_json::from_str(&text).map_err(|e| QorError::Build(format!("{}: {e}", path.display())))
2835}
2836
2837/// The lowercase hex SHA3-256 of `bytes`.
2838pub fn sha3_hex(bytes: &[u8]) -> String {
2839    logicaffeine_system::keccak::sha3_256_bytes(bytes)
2840        .iter()
2841        .map(|b| format!("{b:02x}"))
2842        .collect()
2843}
2844
2845/// Read manifest entry `name` under `root` and its files, refusing any file whose bytes no longer
2846/// hash to the recorded SHA3-256.
2847pub fn manifest_candidate(
2848    root: &std::path::Path,
2849    name: &str,
2850) -> Result<(Candidate, Vec<(String, String)>), QorError> {
2851    let entry = read_manifest(root)?
2852        .candidates
2853        .into_iter()
2854        .find(|e| e.candidate.name == name)
2855        .ok_or_else(|| QorError::Build(format!("{name}: not in {MANIFEST}")))?;
2856    let mut files = Vec::new();
2857    for f in &entry.files {
2858        let p = root.join(&f.path);
2859        let bytes = std::fs::read(&p).map_err(|e| QorError::Build(format!("{}: {e}", p.display())))?;
2860        let got = sha3_hex(&bytes);
2861        if got != f.sha3_256 {
2862            return Err(QorError::Build(format!(
2863                "{}: SHA3-256 {got} is not the recorded {}; regenerate the candidate",
2864                f.path, f.sha3_256
2865            )));
2866        }
2867        let file_name = std::path::Path::new(&f.path)
2868            .file_name()
2869            .map(|n| n.to_string_lossy().to_string())
2870            .unwrap_or_else(|| f.path.clone());
2871        files.push((file_name, String::from_utf8_lossy(&bytes).to_string()));
2872    }
2873    Ok((entry.candidate, files))
2874}
2875
2876fn dut_port(name: &str, dir: Dir, bits: u32) -> DutPort {
2877    DutPort { name: name.to_string(), dir, bits }
2878}
2879
2880/// A combinational kernel module `inner` (ports `ins` → `outs`) in input and output registers:
2881/// the operands latch on one edge and the result on the next, a registered latency of 2. Returns
2882/// the module and its ports.
2883fn registered(top: &str, inner: &str, ins: &[(&str, u32, bool)], outs: &[(&str, u32)]) -> (String, Vec<DutPort>) {
2884    let decl_in: Vec<String> = ins
2885        .iter()
2886        .map(|(n, b, s)| format!("input {}[{}:0] {n}", if *s { "signed " } else { "" }, b - 1))
2887        .collect();
2888    let decl_out: Vec<String> = outs.iter().map(|(n, b)| format!("output reg [{}:0] {n}", b - 1)).collect();
2889    let mut v = format!(
2890        "// {top} — `{inner}` with its operands and results in registers (registered latency 2).\n\
2891         module {top}(input clk, {}, {});\n",
2892        decl_in.join(", "),
2893        decl_out.join(", ")
2894    );
2895    for (n, b, s) in ins {
2896        v.push_str(&format!("  reg {}[{}:0] {n}_r;\n", if *s { "signed " } else { "" }, b - 1));
2897    }
2898    for (n, b) in outs {
2899        v.push_str(&format!("  wire [{}:0] {n}_w;\n", b - 1));
2900    }
2901    let conns: Vec<String> = ins
2902        .iter()
2903        .map(|(n, _, _)| format!(".{n}({n}_r)"))
2904        .chain(outs.iter().map(|(n, _)| format!(".{n}({n}_w)")))
2905        .collect();
2906    v.push_str(&format!("  {inner} u_k({});\n  always @(posedge clk) begin\n", conns.join(", ")));
2907    for (n, _, _) in ins {
2908        v.push_str(&format!("    {n}_r <= {n};\n"));
2909    }
2910    for (n, _) in outs {
2911        v.push_str(&format!("    {n} <= {n}_w;\n"));
2912    }
2913    v.push_str("  end\nendmodule\n");
2914    let ports = std::iter::once(dut_port("clk", Dir::In, 1))
2915        .chain(ins.iter().map(|(n, b, _)| dut_port(n, Dir::In, *b)))
2916        .chain(outs.iter().map(|(n, b)| dut_port(n, Dir::Out, *b)))
2917        .collect();
2918    (v, ports)
2919}
2920
2921/// The K6 SIMT engine compiled from the shader `a * x + b` (uniform `a`, `b`; varying `x`), four
2922/// predicated lanes with a cross-lane sum, under multiply strategy `arith`.
2923fn simt_engine(arith: LaneArith) -> SimtEngine {
2924    let src = format!("{}## Main\n    Show shade(2, 3, 4).\n", shade_program());
2925    let mut eng = SimtEngine::from_lane_source(&src, "shade", SIMT_LANES, vec![true, false, true], true, Some(AluOp::Add));
2926    eng.arith = arith;
2927    eng
2928}
2929
2930/// The register-file candidate of the SIMT engine `eng`, named `name`: one load per engine slot
2931/// (uniforms, the mask, then each lane's varying), a reset before each step (the engine's load
2932/// counter counts from reset), and one read per lane result and the sum.
2933fn simt_candidate(name: &str, module: &str, eng: &SimtEngine) -> (Candidate, Vec<(String, String)>) {
2934    let whole = |p: &str, bits: u32| RegSlot { contract: p.to_string(), lsb: 0, width: bits };
2935    let load_slots = load_slots(eng)
2936        .into_iter()
2937        .map(|s| match s {
2938            Slot::Uniform(0) => whole("a", 64),
2939            Slot::Uniform(_) => whole("b", 64),
2940            Slot::Mask => whole("mask", SIMT_LANES as u32),
2941            Slot::Varying(j, _) => whole(["x0", "x1", "x2", "x3"][j], 64),
2942        })
2943        .collect();
2944    let read_slots = ["y0", "y1", "y2", "y3", "sum"].iter().map(|p| whole(p, 64)).collect();
2945    let verilog = emit_gpu_simt_verilog(module, eng);
2946    let top = format!("gpu_{module}");
2947    let c = Candidate {
2948        kernel: "K6".into(),
2949        name: name.into(),
2950        top: top.clone(),
2951        clock: "clk".into(),
2952        reset: Some(Reset { port: "rst".into(), active_high: true }),
2953        protocol: Protocol::LoadGoRead {
2954            load: "load".into(),
2955            din: "din".into(),
2956            go: "go".into(),
2957            done: "done".into(),
2958            raddr: "raddr".into(),
2959            dout: "dout".into(),
2960            reset_each_op: true,
2961            load_slots,
2962            read_slots,
2963        },
2964        inputs: Vec::new(),
2965        outputs: Vec::new(),
2966        mems: Vec::new(),
2967        batch: 1,
2968        ports: vec![
2969            dut_port("clk", Dir::In, 1),
2970            dut_port("rst", Dir::In, 1),
2971            dut_port("load", Dir::In, 1),
2972            dut_port("din", Dir::In, 64),
2973            dut_port("go", Dir::In, 1),
2974            dut_port("raddr", Dir::In, 8),
2975            dut_port("dout", Dir::Out, 64),
2976            dut_port("done", Dir::Out, 1),
2977        ],
2978    };
2979    (c, vec![(format!("{top}.v"), verilog)])
2980}
2981
2982/// The period of one weight-stationary call: `GEMM_DIM` weight-row loads, `GEMM_DIM` activation rows,
2983/// then the drain until the last row has passed every column — the weights live in the PEs, so the
2984/// next call's loads must wait for it.
2985pub fn gemm_ws_period() -> u32 {
2986    let d = GEMM_DIM as u32;
2987    d + d + (d - 2)
2988}
2989
2990/// The LOGOS weight-stationary GEMM tile with run-time weights on the K3 beat schedule of period
2991/// `period`: load the tile's weight rows, stream its activation rows, drain; row `t`'s result emerges
2992/// `pipelined_latency` cycles after its activation.
2993pub fn gemm_ws_candidate(period: u32) -> (Candidate, Vec<(String, String)>) {
2994    gemm_ws_candidate_acc(period, GEMM_ACC_BITS)
2995}
2996
2997/// [`gemm_ws_candidate`] with `acc_bits`-wide accumulators (each sum sign-extended onto its output
2998/// port), named for the width when it is not the contract's.
2999pub fn gemm_ws_candidate_acc(period: u32, acc_bits: u32) -> (Candidate, Vec<(String, String)>) {
3000    let d = GEMM_DIM as u32;
3001    let lat = pipelined_latency(GEMM_DIM, GEMM_DIM) as u32;
3002    let mut drives = Vec::new();
3003    for t in 0..d {
3004        drives.push(Drive { beat: t, dut: "w_load".into(), value: DriveValue::Const(1) });
3005        drives.push(Drive { beat: t, dut: "w_row".into(), value: DriveValue::Const(t as u64) });
3006        drives.push(Drive {
3007            beat: t,
3008            dut: "w_data".into(),
3009            value: DriveValue::Slice { contract: "w@0".into(), lsb: d * GEMM_BITS * t, width: d * GEMM_BITS },
3010        });
3011        for r in 0..d {
3012            drives.push(Drive {
3013                beat: d + t,
3014                dut: format!("a{r}"),
3015                value: DriveValue::Slice { contract: format!("a@{t}"), lsb: GEMM_BITS * r, width: GEMM_BITS },
3016            });
3017        }
3018    }
3019    let row_bits = (usize::BITS - (GEMM_DIM.max(2) - 1).leading_zeros()).max(1);
3020    let mut ports = vec![
3021        dut_port("clk", Dir::In, 1),
3022        dut_port("rst", Dir::In, 1),
3023        dut_port("w_load", Dir::In, 1),
3024        dut_port("w_row", Dir::In, row_bits),
3025        dut_port("w_data", Dir::In, d * GEMM_BITS),
3026    ];
3027    ports.extend((0..d).map(|r| dut_port(&format!("a{r}"), Dir::In, 64)));
3028    ports.extend((0..d).map(|c| dut_port(&format!("o{c}"), Dir::Out, 64)));
3029    let outputs = (0..d)
3030        .flat_map(|t| (0..d).map(move |c| Bind::slice(&format!("o@{t}"), GEMM_ACC_BITS * c, GEMM_ACC_BITS, &format!("o{c}"))))
3031        .collect();
3032    let c = Candidate {
3033        kernel: "K3".into(),
3034        name: acc_name("k3_logos_ws", acc_bits),
3035        top: "qor_k3_ws".into(),
3036        clock: "clk".into(),
3037        reset: Some(Reset { port: "rst".into(), active_high: true }),
3038        protocol: Protocol::Beats { period, drives, captures: (0..d).map(|t| d + t + lat).collect() },
3039        inputs: Vec::new(),
3040        outputs,
3041        mems: Vec::new(),
3042        batch: d,
3043        ports,
3044    };
3045    let rtl = emit_systolic_pipelined_loadable_verilog("qor_k3_ws", GEMM_DIM, GEMM_DIM, GEMM_BITS, acc_bits);
3046    (c, vec![("qor_k3_ws.v".into(), rtl)])
3047}
3048
3049/// The LOGOS output-stationary GEMM array streaming one tile every `GEMM_DIM` beats: beat `t` carries
3050/// lane `t` of every row's activations and weight row `t`, the first beat raises `first`, and row `i`
3051/// of a tile is read `os_streaming_result_offset` cycles after its first beat.
3052pub fn gemm_os_candidate() -> (Candidate, Vec<(String, String)>) {
3053    gemm_os_candidate_acc(GEMM_ACC_BITS)
3054}
3055
3056/// [`gemm_os_candidate`] with `acc_bits`-wide accumulators (each sum sign-extended onto its output
3057/// port), named for the width when it is not the contract's.
3058pub fn gemm_os_candidate_acc(acc_bits: u32) -> (Candidate, Vec<(String, String)>) {
3059    let d = GEMM_DIM as u32;
3060    let mut drives = vec![Drive { beat: 0, dut: "first".into(), value: DriveValue::Const(1) }];
3061    for t in 0..d {
3062        for i in 0..d {
3063            drives.push(Drive {
3064                beat: t,
3065                dut: format!("aw{i}"),
3066                value: DriveValue::Slice { contract: format!("a@{i}"), lsb: GEMM_BITS * t, width: GEMM_BITS },
3067            });
3068        }
3069        for j in 0..d {
3070            drives.push(Drive {
3071                beat: t,
3072                dut: format!("wn{j}"),
3073                value: DriveValue::Slice { contract: "w@0".into(), lsb: d * GEMM_BITS * t + GEMM_BITS * j, width: GEMM_BITS },
3074            });
3075        }
3076    }
3077    let mut ports = vec![dut_port("clk", Dir::In, 1), dut_port("rst", Dir::In, 1), dut_port("first", Dir::In, 1)];
3078    ports.extend((0..d).map(|i| dut_port(&format!("aw{i}"), Dir::In, 64)));
3079    ports.extend((0..d).map(|j| dut_port(&format!("wn{j}"), Dir::In, 64)));
3080    for i in 0..d {
3081        ports.extend((0..d).map(|j| dut_port(&format!("res{i}_{j}"), Dir::Out, 64)));
3082    }
3083    let outputs = (0..d)
3084        .flat_map(|i| (0..d).map(move |j| Bind::slice(&format!("o@{i}"), GEMM_ACC_BITS * j, GEMM_ACC_BITS, &format!("res{i}_{j}"))))
3085        .collect();
3086    let captures = (0..GEMM_DIM).map(|i| os_streaming_result_offset(GEMM_DIM, i, GEMM_DIM) as u32).collect();
3087    let c = Candidate {
3088        kernel: "K3".into(),
3089        name: acc_name("k3_logos_os", acc_bits),
3090        top: "qor_k3_os".into(),
3091        clock: "clk".into(),
3092        reset: Some(Reset { port: "rst".into(), active_high: true }),
3093        protocol: Protocol::Beats { period: d, drives, captures },
3094        inputs: Vec::new(),
3095        outputs,
3096        mems: Vec::new(),
3097        batch: d,
3098        ports,
3099    };
3100    let rtl = emit_systolic_os_streaming_verilog("qor_k3_os", &SystolicOs::new(GEMM_DIM, GEMM_DIM, GEMM_DIM), GEMM_BITS, acc_bits);
3101    (c, vec![("qor_k3_os.v".into(), rtl)])
3102}
3103
3104/// The streamed K6 engine's register placement: a register after every `K6_STREAMED_EVERY`-th level of its multiply
3105/// tree (`LaneArith::Streamed`). MEASURED (2026-10-17, Artix-7 fabric map, yosys 0.33 synth_xilinx + nextpnr-xilinx,
3106/// median of the seeds that routed): every level = 3421 LUTs at 128.2 MHz and 24 clocks an operation, 1.56 million
3107/// results a second per thousand LUTs; every second = 3403 at 106.6 MHz and 21 clocks, 1.49; every third = 3394 at
3108/// 90.7 and 20, 1.34; the whole tree in one stage = 3387 at 63.7 and 19, 0.99. Fewer registers hold half (every
3109/// second) to a quarter (one stage) of the flip-flops.
3110pub const K6_STREAMED_EVERY: usize = 1;
3111
3112/// The K6 ports engine's register placement: a register after every `K6_PORTS_EVERY`-th level of each lane's multiply
3113/// tree and after its last (`simt_ports`). MEASURED (2026-10-18, Sky130 HD: yosys 0.33 with OpenLane v1's `DELAY 0` ABC
3114/// script at a 2 ns target, OpenSTA at the typical corner, no wires; a step every clock throughout): every level =
3115/// 536,394 µm² at 3.35 ns and 8 clocks; every second = 488,462 at 3.86 and 5; every third = 468,797 at 5.10 and 4; the
3116/// product only = 437,985 at 8.30 and 3; no register in the tree = 423,320 at 9.27 and 2. Every second is within 5% of
3117/// every level in steps a second per area, with 39% of its flip-flops and three clocks fewer.
3118pub const K6_PORTS_EVERY: usize = 2;
3119
3120/// The SIMT engine `eng`'s step on ports (`simt_ports`), named `name`: every operand from the harness's registers at
3121/// once, each lane's product a tree registered every [`K6_PORTS_EVERY`] levels, every lane's output and the sum
3122/// registered together, a step every clock.
3123fn simt_ports_candidate(name: &str, module: &str, eng: &SimtEngine) -> Result<(Candidate, Vec<(String, String)>), QorError> {
3124    let levels = mul_tree_levels(64);
3125    let p = emit_simt_ports_verilog(module, eng, |l| (l + 1) % K6_PORTS_EVERY == 0 || l + 1 == levels).map_err(QorError::Build)?;
3126    let lanes = ["x0", "x1", "x2", "x3"];
3127    let results = ["y0", "y1", "y2", "y3"];
3128    let mut ports = vec![
3129        dut_port("clk", Dir::In, 1),
3130        dut_port("a", Dir::In, 64),
3131        dut_port("b", Dir::In, 64),
3132        dut_port("mask", Dir::In, SIMT_LANES as u32),
3133    ];
3134    ports.extend(lanes.iter().map(|x| dut_port(x, Dir::In, 64)));
3135    ports.extend(results.iter().map(|y| dut_port(y, Dir::Out, 64)));
3136    ports.push(dut_port("reduced", Dir::Out, 64));
3137    let mut inputs = vec![Bind::whole("a", "a"), Bind::whole("b", "b"), Bind::whole("mask", "mask")];
3138    inputs.extend(lanes.iter().map(|x| Bind::whole(x, x)));
3139    let mut outputs: Vec<Bind> = results.iter().map(|y| Bind::whole(y, y)).collect();
3140    outputs.push(Bind::whole("sum", "reduced"));
3141    let c = Candidate {
3142        kernel: "K6".into(),
3143        name: name.into(),
3144        top: module.into(),
3145        clock: "clk".into(),
3146        reset: None,
3147        protocol: Protocol::Registered { latency: p.latency },
3148        inputs,
3149        outputs,
3150        mems: Vec::new(),
3151        batch: 1,
3152        ports,
3153    };
3154    Ok((c, vec![(format!("{module}.v"), p.verilog)]))
3155}
3156
3157/// Every LOGOS candidate of kernel `id`, the default first. K1 has the decision fabric in registers, sequenced one
3158/// cell a cycle, pipelined one carry chain to a stage for a fabric with carry chains (`fabric_pipe::decision_ops`,
3159/// `DECISION_CHAIN_BUDGET`), pipelined for standard cells (each compare's sums folded into it, `DECISION_CELLS_BUDGET`,
3160/// its ports read), and hoisted into one clock (`select_hoist`, its sums folded into the compares that read them, its
3161/// ports read). K4 has the Keccak core
3162/// a round a clock and a half round a clock (`keccak_core_half`). K2 has the residual in registers, sequenced one cell
3163/// a cycle, and pipelined (`fabric_pipe`, the RTL the `fabric_horner_seq` block hardens). K5 has the butterfly in
3164/// registers and its pipeline registered once a multiply and after every step. K6 has one per lane multiply strategy:
3165/// the sequential shift-add (the signoff-clean default), the two-stage decomposed FMA, the single-cycle
3166/// combinational multiply and the streamed shared multiply, and the engine's step on ports (`simt_ports`).
3167pub fn logos_candidates(id: &str) -> Result<Vec<(Candidate, Vec<(String, String)>)>, QorError> {
3168    match id {
3169        "K3" => Ok(vec![
3170            gemm_ws_candidate(gemm_ws_period()),
3171            gemm_os_candidate(),
3172            gemm_ws_candidate_acc(gemm_ws_period(), GEMM_MATCHED_ACC_BITS),
3173            gemm_os_candidate_acc(GEMM_MATCHED_ACC_BITS),
3174        ]),
3175        "K1" => {
3176            let k = kernel("K1").ok_or_else(|| QorError::Build("K1 is not in the catalog".into()))?;
3177            let df = decision_fabric_from_source(DECIDE_FN, DECIDE_NAME);
3178            let inputs: Vec<Bind> = k.inputs.iter().enumerate().map(|(i, p)| Bind::whole(p.name, &format!("in{i}"))).collect();
3179            let (ops, result) = decision_ops(&df).map_err(QorError::Build)?;
3180            let sched = schedule_pipeline(&ops, result, DECISION_CHAIN_BUDGET);
3181            let (folded, fores) = carry_save_lower_with(&ops, result, 64, SumSharing::Fold, FinalAdd::Native);
3182            let cells = schedule_pipeline(&folded, fores, DECISION_CELLS_BUDGET).reading(Inputs::Ports);
3183            let (hops, hres) = decision_ops(&hoist_selects(&df, 64).map_err(QorError::Build)?).map_err(QorError::Build)?;
3184            let (flat, fres) = carry_save_lower_with(&hops, hres, 64, SumSharing::Fold, FinalAdd::PrefixTree);
3185            let one = schedule_pipeline(&flat, fres, f64::INFINITY).reading(Inputs::Ports);
3186            Ok(vec![
3187                logos_candidate("K1")?,
3188                seq_fabric_candidate("K1", "hft", &df, inputs.clone(), "order"),
3189                residual_pipe_candidate("K1", "k1_logos_pipe", "hft_pipe", &ops, &sched, inputs.clone(), "order"),
3190                residual_pipe_candidate("K1", "k1_logos_cells", "hft_cells", &folded, &cells, inputs.clone(), "order"),
3191                residual_pipe_candidate("K1", "k1_logos_hoisted", "hft_hoisted", &flat, &one, inputs, "order"),
3192            ])
3193        }
3194        "K2" => {
3195            let df = decision_fabric_from_source(&horner_program(), "poly");
3196            let (ops, sched) = horner_seq_pipeline();
3197            Ok(vec![
3198                logos_candidate("K2")?,
3199                seq_fabric_candidate("K2", "horner", &df, vec![Bind::whole("x", "in0")], "y"),
3200                residual_pipe_candidate("K2", "k2_logos_pipe", "horner_seq", &ops, &sched, vec![Bind::whole("x", "in0")], "y"),
3201            ])
3202        }
3203        "K4" => {
3204            let (core, files) = logos_candidate("K4")?;
3205            let mut half = core.clone();
3206            half.name = "k4_logos_half".into();
3207            half.top = "keccak_core_half".into();
3208            Ok(vec![(core, files), (half, vec![("keccak_core_half.v".into(), emit_keccak_core_half_verilog())])])
3209        }
3210        "K5" => Ok(vec![
3211            logos_candidate("K5")?,
3212            ntt_butterfly_pipe_candidate("k5_logos_pipe", "ntt_butterfly_pipe", NttCuts::PER_MULTIPLY),
3213            ntt_butterfly_pipe_candidate("k5_logos_deep", "ntt_butterfly_deep", NttCuts::EVERY_STEP),
3214        ]),
3215        "K6" => Ok(vec![
3216            simt_candidate("k6_logos_seqfma", "qor_k6_seqfma", &simt_engine(LaneArith::SequentialFma { select_slice: 8 })),
3217            simt_candidate("k6_logos_fma2", "qor_k6_fma2", &simt_engine(LaneArith::DecomposedFma { stages: 2 })),
3218            simt_candidate("k6_logos_comb", "qor_k6_comb", &simt_engine(LaneArith::Combinational)),
3219            simt_candidate("k6_logos_streamed", "qor_k6_streamed", &simt_engine(LaneArith::Streamed { every: K6_STREAMED_EVERY })),
3220            simt_ports_candidate("k6_logos_ports", "qor_k6_ports", &simt_engine(LaneArith::Combinational))?,
3221        ]),
3222        other => Ok(vec![logos_candidate(other)?]),
3223    }
3224}
3225
3226/// A K5 pipelined butterfly (`emit_ntt_butterfly_pipelined_verilog`) registered after the steps `cuts` names: its
3227/// own operand and result registers, one butterfly a cycle.
3228fn ntt_butterfly_pipe_candidate(name: &str, top: &str, cuts: NttCuts) -> (Candidate, Vec<(String, String)>) {
3229    let ports = vec![
3230        dut_port("clk", Dir::In, 1),
3231        dut_port("a", Dir::In, 16),
3232        dut_port("b", Dir::In, 16),
3233        dut_port("zeta", Dir::In, 16),
3234        dut_port("hi", Dir::Out, 16),
3235        dut_port("lo", Dir::Out, 16),
3236    ];
3237    let c = Candidate {
3238        kernel: "K5".to_string(),
3239        name: name.to_string(),
3240        top: top.to_string(),
3241        clock: "clk".to_string(),
3242        reset: None,
3243        protocol: Protocol::Registered { latency: cuts.latency() },
3244        inputs: vec![Bind::whole("a", "a"), Bind::whole("b", "b"), Bind::whole("zeta", "zeta")],
3245        outputs: vec![Bind::whole("hi", "hi"), Bind::whole("lo", "lo")],
3246        mems: Vec::new(),
3247        batch: 1,
3248        ports,
3249    };
3250    (c, vec![(format!("{top}.v"), emit_ntt_butterfly_pipelined_verilog(top, cuts))])
3251}
3252
3253/// A compiled residual pipelined by `fabric_pipe` (`emit_fabric_pipelined_verilog`): module `fabric_{module}`, its
3254/// inputs `in0..` registered into the first stage, one result a cycle on `result`, `sched.latency()` edges after its
3255/// operands.
3256fn residual_pipe_candidate(
3257    id: &str,
3258    name: &str,
3259    module: &str,
3260    ops: &[FabricOp],
3261    sched: &PipelineSchedule,
3262    inputs: Vec<Bind>,
3263    output: &str,
3264) -> (Candidate, Vec<(String, String)>) {
3265    let top = format!("fabric_{module}");
3266    let n_in = inputs.len();
3267    let mut ports = vec![dut_port("clk", Dir::In, 1)];
3268    ports.extend((0..n_in).map(|k| dut_port(&format!("in{k}"), Dir::In, 64)));
3269    ports.push(dut_port("result", Dir::Out, 64));
3270    let c = Candidate {
3271        kernel: id.to_string(),
3272        name: name.to_string(),
3273        top: top.clone(),
3274        clock: "clk".to_string(),
3275        reset: None,
3276        protocol: Protocol::Registered { latency: sched.latency() as u32 },
3277        inputs,
3278        outputs: vec![Bind::whole(output, "result")],
3279        mems: Vec::new(),
3280        batch: 1,
3281        ports,
3282    };
3283    (c, vec![(format!("{top}.v"), emit_fabric_pipelined_verilog(module, n_in, ops, sched))])
3284}
3285
3286/// The sequenced form of a compiled fabric (`emit_seq_datapath_verilog`): the same cells, one a cycle — its reset
3287/// starts an operation, `done` ends it, and the harness holds the operands meanwhile.
3288fn seq_fabric_candidate(
3289    id: &str,
3290    short: &str,
3291    df: &DecisionFabric,
3292    inputs: Vec<Bind>,
3293    output: &str,
3294) -> (Candidate, Vec<(String, String)>) {
3295    let top = format!("seq_{short}");
3296    let mut ports = vec![dut_port("clk", Dir::In, 1), dut_port("rst", Dir::In, 1)];
3297    ports.extend((0..df.n_in).map(|k| dut_port(&format!("in{k}"), Dir::In, 64)));
3298    ports.push(dut_port("result", Dir::Out, 64));
3299    ports.push(dut_port("done", Dir::Out, 1));
3300    let c = Candidate {
3301        kernel: id.to_string(),
3302        name: format!("{}_logos_seq", id.to_lowercase()),
3303        top: top.clone(),
3304        clock: "clk".to_string(),
3305        reset: None,
3306        protocol: Protocol::StartDone { start: "rst".into(), done: "done".into() },
3307        inputs,
3308        outputs: vec![Bind::whole(output, "result")],
3309        mems: Vec::new(),
3310        batch: 1,
3311        ports,
3312    };
3313    (c, vec![(format!("{top}.v"), emit_seq_datapath_verilog(short, df))])
3314}
3315
3316/// The LOGOS candidate of kernel `id`: its harness-facing description and its Verilog files
3317/// (file name, contents). K1, K2 and K6 are compiled from their LOGOS programs; K4 and K5 come from
3318/// the RTL generators; the remaining kernels' candidates are built by their own protocols.
3319pub fn logos_candidate(id: &str) -> Result<(Candidate, Vec<(String, String)>), QorError> {
3320    let base = |name: &str, top: &str, protocol: Protocol, inputs: Vec<Bind>, outputs: Vec<Bind>, ports: Vec<DutPort>| Candidate {
3321        kernel: id.to_string(),
3322        name: name.to_string(),
3323        top: top.to_string(),
3324        clock: "clk".to_string(),
3325        reset: None,
3326        protocol,
3327        inputs,
3328        outputs,
3329        mems: Vec::new(),
3330        batch: 1,
3331        ports,
3332    };
3333    match id {
3334        "K1" => {
3335            let df = decision_fabric_from_source(DECIDE_FN, DECIDE_NAME);
3336            let comb = emit_decision_fabric_verilog("hft", &df);
3337            let ins: Vec<String> = (0..df.n_in).map(|k| format!("in{k}")).collect();
3338            let decl: Vec<(&str, u32, bool)> = ins.iter().map(|n| (n.as_str(), 64, true)).collect();
3339            let (wrap, ports) = registered("qor_k1_logos", "decision_hft", &decl, &[("result", 64)]);
3340            let kernel = kernel("K1").ok_or_else(|| QorError::Build("K1 is not in the catalog".into()))?;
3341            let inputs = kernel
3342                .inputs
3343                .iter()
3344                .zip(&ins)
3345                .map(|(p, d)| Bind::whole(p.name, d))
3346                .collect();
3347            Ok((
3348                base("k1_logos", "qor_k1_logos", Protocol::Registered { latency: 2 }, inputs, vec![Bind::whole("order", "result")], ports),
3349                vec![("decision_hft.v".into(), comb), ("qor_k1_logos.v".into(), wrap)],
3350            ))
3351        }
3352        "K2" => {
3353            let (ops, result, n_in) = residual_from_source(&horner_program(), "poly");
3354            if n_in != 1 {
3355                return Err(QorError::Build(format!("the Horner residual takes {n_in} inputs, not 1")));
3356            }
3357            let comb = emit_fabric_verilog("horner", n_in, &ops, result);
3358            let (wrap, ports) = registered("qor_k2_logos", "fabric_horner", &[("in0", 64, true)], &[("result", 64)]);
3359            Ok((
3360                base("k2_logos", "qor_k2_logos", Protocol::Registered { latency: 2 }, vec![Bind::whole("x", "in0")], vec![Bind::whole("y", "result")], ports),
3361                vec![("fabric_horner.v".into(), comb), ("qor_k2_logos.v".into(), wrap)],
3362            ))
3363        }
3364        "K4" => {
3365            let mut c = base(
3366                "k4_logos",
3367                "keccak_core",
3368                Protocol::StartDone { start: "start".into(), done: "done".into() },
3369                vec![Bind::whole("s_in", "s_in")],
3370                vec![Bind::whole("s_out", "s_out")],
3371                vec![
3372                    dut_port("clk", Dir::In, 1),
3373                    dut_port("rst", Dir::In, 1),
3374                    dut_port("start", Dir::In, 1),
3375                    dut_port("s_in", Dir::In, 1600),
3376                    dut_port("done", Dir::Out, 1),
3377                    dut_port("s_out", Dir::Out, 1600),
3378                ],
3379            );
3380            c.reset = Some(Reset { port: "rst".into(), active_high: true });
3381            Ok((c, vec![("keccak_core.v".into(), emit_keccak_core_verilog())]))
3382        }
3383        "K5" => {
3384            let (wrap, ports) = registered(
3385                "qor_k5_logos",
3386                "ntt_butterfly",
3387                &[("a", 16, true), ("b", 16, true), ("zeta", 16, true)],
3388                &[("hi", 16), ("lo", 16)],
3389            );
3390            Ok((
3391                base(
3392                    "k5_logos",
3393                    "qor_k5_logos",
3394                    Protocol::Registered { latency: 2 },
3395                    vec![Bind::whole("a", "a"), Bind::whole("b", "b"), Bind::whole("zeta", "zeta")],
3396                    vec![Bind::whole("hi", "hi"), Bind::whole("lo", "lo")],
3397                    ports,
3398                ),
3399                vec![("ntt_butterfly.v".into(), emit_ntt_butterfly_verilog()), ("qor_k5_logos.v".into(), wrap)],
3400            ))
3401        }
3402        "K3" | "K6" => Ok(logos_candidates(id)?.remove(0)),
3403        other => Err(QorError::Build(format!("{other}: the LOGOS candidate is built by its own protocol"))),
3404    }
3405}
3406
3407#[cfg(test)]
3408#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/qor_harness/tests.rs"]
3409mod tests;