1use 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
48pub const SEED: u64 = 0x1234_5678_9ABC_DEF1;
50
51pub const LANE_MIX: u64 = 0x9E37_79B9_7F4A_7C15;
53
54pub 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
63fn lane_rotation(k: usize) -> u32 {
65 ((11 * k) % 64) as u32
66}
67
68pub fn advance(s: u64, n: u32) -> u64 {
70 (0..n).fold(s, |s, _| next_state(s))
71}
72
73pub 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
88pub 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
97pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum Mode {
105 Shaped,
107 Edge,
109}
110
111#[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 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 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#[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#[derive(Clone, Debug, PartialEq)]
213pub struct OutPort {
214 pub name: &'static str,
215 pub bits: u32,
216}
217
218#[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
235pub type Words = Vec<Vec<u64>>;
237
238#[derive(Clone, Copy)]
240pub enum Golden {
241 Vm { program: fn() -> String, func: &'static str },
244 VmLanes { program: fn() -> String, func: &'static str, lanes: usize },
247 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#[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
309pub const GEMM_DIM: usize = 8;
311pub const GEMM_BITS: u32 = 8;
312pub const GEMM_ACC_BITS: u32 = 32;
314pub 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
339pub const SIMT_LANES: usize = 4;
341
342pub 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
436pub 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
453pub 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#[derive(Clone, Debug, PartialEq)]
486pub enum QorError {
487 PortMissing { candidate: String, port: String },
489 PortWidth { candidate: String, port: String, need: u32, have: u32 },
491 PortDirection { candidate: String, port: String },
493 Unmapped { candidate: String, contract: String },
495 Protocol(String),
497 Golden(String),
499 Unknown { op: usize },
501 Testbench(String),
503 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
532fn 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
547fn 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
556fn 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
589pub 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
630pub 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
643pub fn misr_step(misr: u64, folded: u64) -> u64 {
645 ((misr << 1) | (misr.count_ones() as u64 & 1)) ^ folded
646}
647
648pub fn signature(k: &Kernel, results: &[Words]) -> u64 {
650 results.iter().fold(0u64, |m, r| misr_step(m, fold(k, r)))
651}
652
653#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
655pub enum Dir {
656 In,
657 Out,
658}
659
660#[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#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
670pub struct Reset {
671 pub port: String,
672 pub active_high: bool,
673}
674
675#[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#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
685pub enum DriveValue {
686 Const(u64),
687 Slice { contract: String, lsb: u32, width: u32 },
689 Previous { contract: String, lsb: u32, width: u32 },
693}
694
695#[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#[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#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
717pub struct DcrWrite {
718 pub addr: u32,
719 pub data: u64,
720}
721
722#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
725pub struct ImageLine {
726 pub line: u64,
727 pub hex: String,
728}
729
730#[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 #[serde(default)]
742 pub stride_bits: u32,
743}
744
745impl LineRegion {
746 fn span(&self, bits: u32) -> u32 {
748 bits.max(self.stride_bits)
749 }
750}
751
752#[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 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 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#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
810pub enum Protocol {
811 Registered { latency: u32 },
813 StartDone { start: String, done: String },
816 Pipelined { start: String, done: String, ii: u32 },
818 GoDone { go: String, done: String },
822 EnableDone { enable: String, done: String, clear: String },
826 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 Beats { period: u32, drives: Vec<Drive>, captures: Vec<u32> },
845 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
879fn launch_regions(c: &Candidate) -> &[LineRegion] {
881 match &c.protocol {
882 Protocol::Launch { bus, .. } => &bus.regions,
883 _ => &[],
884 }
885}
886
887#[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 #[serde(default)]
899 pub dut_lsb: u32,
900 #[serde(default)]
903 pub valid: Option<String>,
904 #[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 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 pub fn sign_extended(mut self, dut_width: u32) -> Bind {
926 self.dut_width = dut_width;
927 self
928 }
929
930 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 pub fn valid_on(mut self, valid: &str) -> Bind {
941 self.valid = Some(valid.to_string());
942 self
943 }
944
945 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#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
962pub enum MemKind {
963 SeqMem,
966 Bram { ports: u32 },
969}
970
971#[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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
984 pub dims: Vec<u32>,
985}
986
987fn index_bits(n: u32) -> u32 {
989 (32 - (n.max(2) - 1).leading_zeros()).max(1)
990}
991
992impl MemBind {
993 pub fn seq_mem(contract: &str, prefix: &str, words: u32) -> MemBind {
995 MemBind::seq_mem_rows(&[contract.to_string()], prefix, words)
996 }
997
998 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 pub fn shaped(mut self, dims: &[u32]) -> MemBind {
1012 self.dims = dims.to_vec();
1013 self
1014 }
1015
1016 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 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 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#[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 #[serde(default)]
1075 pub mems: Vec<MemBind>,
1076 #[serde(default = "one")]
1078 pub batch: u32,
1079 pub ports: Vec<DutPort>,
1081}
1082
1083fn one() -> u32 {
1084 1
1085}
1086
1087pub 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
1107fn slot_key(r: &str) -> String {
1109 let (p, j) = contract_ref(r);
1110 format!("{p}@{j}")
1111}
1112
1113fn 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
1120fn 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
1199fn 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
1265pub 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
1298pub 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 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
1528fn 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
1593fn 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#[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 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 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
1736pub fn emit_harness(k: &Kernel, c: &Candidate, mode: Mode) -> String {
1746 emit_harness_as(k, c, mode, &harness_module(c))
1747}
1748
1749pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
2532pub 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
2564pub 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
2632pub 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
2643pub 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
2698pub 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
2704pub 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
2717pub 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#[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 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
2760pub 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
2789pub const MANIFEST: &str = "benchmarks/silicon/qor/candidates.json";
2791
2792#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
2794pub struct ManifestFile {
2795 pub path: String,
2796 pub sha3_256: String,
2797}
2798
2799#[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#[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
2830pub 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
2837pub 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
2845pub 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
2880fn 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
2921fn 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
2930fn 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
2982pub fn gemm_ws_period() -> u32 {
2986 let d = GEMM_DIM as u32;
2987 d + d + (d - 2)
2988}
2989
2990pub fn gemm_ws_candidate(period: u32) -> (Candidate, Vec<(String, String)>) {
2994 gemm_ws_candidate_acc(period, GEMM_ACC_BITS)
2995}
2996
2997pub 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
3049pub fn gemm_os_candidate() -> (Candidate, Vec<(String, String)>) {
3053 gemm_os_candidate_acc(GEMM_ACC_BITS)
3054}
3055
3056pub 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
3104pub const K6_STREAMED_EVERY: usize = 1;
3111
3112pub const K6_PORTS_EVERY: usize = 2;
3119
3120fn 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
3157pub 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
3226fn 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
3253fn 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
3286fn 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
3316pub 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;