Skip to main content

logicaffeine_compile/codegen_sva/
fp_oracle.rs

1//! The hard floating-point oracle: a D5005 persona that measures which binary32 semantics the card's floating-point DSP
2//! block computes (`fourteennm_fp_mac`, whose simulation model is encrypted), by running the block beside every class
3//! of the hypothesis family ([`super::fp_hypotheses`]) as soft units on one stream of operands and counting, class by
4//! class, the results that differ from the block's.
5//!
6//! The persona is an engine build whose engine is the oracle. [`Oracle::engine_verilog`] writes a `logos_engine` with
7//! the engine's ports, the engine's own crossing ([`bridge_verilog`]) and the oracle's core as its `core` instance, so
8//! the shell, its commands and the timing constraints an engine build carries hold for it unchanged: the core steps at
9//! the compute clock's rising edges, every path inside it given half a compute cycle.
10//!
11//! Each step the core takes one operand triple (A, B, C) from its [`Source`] and hands it to three hard blocks: the
12//! multiply A·B (`sp_mult`, ay·az), the add A+B (`sp_add`, ax+ay) and the multiply-add C+A·B (`sp_mult_add`,
13//! ax+ay·az), every register of each on the compute step. The triple also enters a delay line the soft units read
14//! [`Run::tap`] + 1 steps late, and each block's results wait [`Run::delays`] steps more, so a block of latency `L`
15//! given `tap + 1 - L` meets the soft units on its own operands, whatever the three blocks' latencies. The host
16//! measures them first: the multiply's and the add's as the taps at which their classes come nearest every vector
17//! ([`Source::Vectors`]; a block no class names still has its tap), the multiply-add's as the shift at which its results with C zero are the products. Every step
18//! the core adds one to the counter of each class whose result differs from the block's, and counts the steps no
19//! product class or no sum class explains; a recording run ([`Run::record`]) stores every step ([`Record`]), a
20//! counting run the steps nothing explains, up to [`Oracle::record_cap`]. Beside them a multiply and an add with only
21//! their output registered take the same triples, their results recorded as they leave them: the shift at which they
22//! are the aligned blocks' is their latency, the fewest steps a block of each mode can be.
23//!
24//! The host's commands: `ENGINE_TABLE` into table 0 at entry 0, the stream's seed in bits 63:0 of the line;
25//! `ENGINE_RUN`, ARG0 the [`Run`] word and ARG1 the triples to take; `STORE_READ`, the store of [`STORE_BEATS`]
26//! beats ([`Store::read`]): beat 0 the header, then the counters eight to a beat (the product classes', then the sum
27//! classes'), then the records. [`HOST_PROGRAM`] runs the whole measurement and prints it as one JSON object.
28
29use std::collections::HashMap;
30use std::fmt::{self, Write};
31
32use super::fp_hypotheses::{fadd_verilog, family, fmul_verilog, Semantics};
33use crate::crypto_accel::sha256;
34use crate::spatial::decode_engine::ofs::{afu_sources, AfuConfig, AfuFile, LocalMemoryLine};
35use crate::spatial::decode_engine::rtl::engine::{bridge_verilog, EngineParams};
36use crate::spatial::decode_engine::rtl::ram::ram_ce_verilog;
37
38/// The header's first word: `LOGOSFPO`.
39pub const MAGIC: u64 = 0x4f50_4653_4f47_4f4c;
40
41/// The latencies the soft side can be delayed to: a tap of 0 to `TAPS - 1`, a block latency of 1 to `TAPS` steps.
42pub const TAPS: usize = 16;
43
44/// The store's beats, 512 bits each.
45pub const STORE_BEATS: usize = 4096;
46
47/// Steps a run takes past its last triple, so every result of every triple leaves the delay line.
48pub const DRAIN: usize = TAPS + 1;
49
50/// Where a run's operands come from.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum Source {
53    /// The discriminator vectors and the multiply-add's, in the oracle's ROM, taken in turn from the first.
54    Vectors,
55    /// Three words of a xorshift stream from the seed.
56    Random,
57    /// The stream drawn where the semantics part: a's exponent zero, one, two, the top two or any; then, half the
58    /// time, b's placing the product within a few binades of the least normal; a quarter, b's a's or within a few of
59    /// it, its significand a's but for the low byte and its sign the other, so the sum cancels deep; else b's exponent
60    /// any or an end.
61    Near,
62}
63
64/// A run's word (`ENGINE_RUN`'s ARG0): its source in bits 1:0, whether it records every step in bit 2, the soft
65/// units' tap in bits 7:4, and the steps each hard block's results wait before they meet the soft units: the
66/// multiply's in bits 11:8, the add's in 15:12, the multiply-add's in 19:16.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub struct Run {
69    pub source: Source,
70    pub record: bool,
71    pub tap: u8,
72    pub delays: [u8; 3],
73}
74
75impl Run {
76    pub fn word(&self) -> u32 {
77        let source = match self.source {
78            Source::Vectors => 0,
79            Source::Random => 1,
80            Source::Near => 2,
81        };
82        let [m, a, f] = self.delays.map(|d| u32::from(d) & 0xf);
83        source | (self.record as u32) << 2 | (u32::from(self.tap) & 0xf) << 4 | m << 8 | a << 12 | f << 16
84    }
85
86    /// The run of a soft tap that meets blocks of `latencies` (the multiply's, the add's, the multiply-add's):
87    /// the least tap all three reach, each block's results delayed the rest of the way. `None` for a latency
88    /// outside 1 to [`TAPS`].
89    pub fn aligned(source: Source, record: bool, latencies: [usize; 3]) -> Option<Run> {
90        if latencies.iter().any(|l| !(1..=TAPS).contains(l)) {
91            return None;
92        }
93        let tap = latencies.iter().max().expect("three") - 1;
94        Some(Run { source, record, tap: tap as u8, delays: latencies.map(|l| (tap + 1 - l) as u8) })
95    }
96
97    pub fn of(word: u32) -> Option<Run> {
98        let source = match word & 3 {
99            0 => Source::Vectors,
100            1 => Source::Random,
101            2 => Source::Near,
102            _ => return None,
103        };
104        let nibble = |at: u32| (word >> at & 0xf) as u8;
105        (word >> 20 == 0 && word & 8 == 0).then_some(Run { source, record: word & 4 != 0, tap: nibble(4), delays: [nibble(8), nibble(12), nibble(16)] })
106    }
107}
108
109/// A class of the family: its members' names (all one function, proven), and the semantics of the first, which the
110/// oracle builds.
111#[derive(Clone, Debug, PartialEq)]
112pub struct Class {
113    pub members: Vec<String>,
114    pub semantics: Semantics,
115}
116
117/// What the oracle is built from: the classes of the multiply and of the add, and the vectors its ROM holds.
118#[derive(Clone, Debug, PartialEq)]
119pub struct Oracle {
120    pub products: Vec<Class>,
121    pub sums: Vec<Class>,
122    pub vectors: Vec<[u32; 3]>,
123}
124
125/// Why a discriminator ledger cannot make an oracle.
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum OracleError {
128    Json(String),
129    /// The ledger lacks the multiply's or the add's table.
130    Missing(&'static str),
131    /// A member name no semantics of the family has.
132    Unknown(String),
133    /// A word that is not eight hex digits.
134    Word(String),
135    /// More classes than the records' agreement bits hold, or more vectors than the ROM's index reaches.
136    TooMany(&'static str, usize),
137}
138
139impl fmt::Display for OracleError {
140    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141        match self {
142            OracleError::Json(e) => write!(f, "the discriminator ledger: {e}"),
143            OracleError::Missing(op) => write!(f, "the discriminator ledger has no {op} table"),
144            OracleError::Unknown(m) => write!(f, "no member of the family is named {m}"),
145            OracleError::Word(w) => write!(f, "not a binary32 word: {w}"),
146            OracleError::TooMany(what, n) => write!(f, "{n} {what}, more than the oracle holds"),
147        }
148    }
149}
150
151#[derive(serde::Deserialize)]
152struct LedgerClass {
153    members: Vec<String>,
154}
155
156#[derive(serde::Deserialize)]
157struct LedgerOp {
158    op: String,
159    vectors: Vec<[String; 2]>,
160    classes: Vec<LedgerClass>,
161}
162
163/// The most product classes a record's agreement bits hold: the two words it gives the multiply's agreement, the
164/// least-registered multiply's result beside the flags.
165pub const MAX_PRODUCT_CLASSES: usize = 128;
166
167/// The most sum classes a record's agreement bits hold: three halves of the two words it gives the add, the fourth the
168/// least-registered add's result.
169pub const MAX_SUM_CLASSES: usize = 96;
170
171/// The multiply-add's own vectors: A and B in [1, 2) whose product is inexact, C the product rounded and negated, so a
172/// fused multiply-add gives the product's rounding error and one rounding after another gives zero; then C a power of
173/// two beside the product.
174pub fn fma_vectors() -> Vec<[u32; 3]> {
175    let mut state = 0x243f_6a88_85a3_08d3u64;
176    let mut next = move || {
177        state ^= state << 13;
178        state ^= state >> 7;
179        state ^= state << 17;
180        state
181    };
182    let mut out = Vec::new();
183    while out.len() < 64 {
184        let r = next();
185        let (a, b) = (0x3f80_0000 | (r as u32 & 0x7f_ffff), 0x3f80_0000 | ((r >> 32) as u32 & 0x7f_ffff));
186        let p = f32::from_bits(a) * f32::from_bits(b);
187        if f64::from(p) == f64::from(f32::from_bits(a)) * f64::from(f32::from_bits(b)) {
188            continue;
189        }
190        let c = if out.len() % 4 == 3 { (f32::from_bits(0x4000_0000) * if r & 1 == 0 { 1.0 } else { -1.0 }).to_bits() } else { (-p).to_bits() };
191        out.push([a, b, c]);
192    }
193    out
194}
195
196impl Oracle {
197    /// The oracle `benchmarks/silicon/d5005/fp_discriminators.json` describes: each class's first member, the
198    /// multiply's and the add's vectors with C zero, then [`fma_vectors`].
199    pub fn from_ledger(json: &str) -> Result<Oracle, OracleError> {
200        let ops: Vec<LedgerOp> = serde_json::from_str(json).map_err(|e| OracleError::Json(e.to_string()))?;
201        let named: HashMap<String, Semantics> = family().into_iter().map(|s| (s.name(), s)).collect();
202        let op = |name: &'static str| ops.iter().find(|o| o.op == name).ok_or(OracleError::Missing(name));
203        let classes = |o: &LedgerOp| -> Result<Vec<Class>, OracleError> {
204            o.classes
205                .iter()
206                .map(|c| {
207                    let first = c.members.first().ok_or(OracleError::Missing("member"))?;
208                    let semantics = *named.get(first).ok_or_else(|| OracleError::Unknown(first.clone()))?;
209                    for m in &c.members {
210                        named.get(m).ok_or_else(|| OracleError::Unknown(m.clone()))?;
211                    }
212                    Ok(Class { members: c.members.clone(), semantics })
213                })
214                .collect()
215        };
216        let word = |w: &String| u32::from_str_radix(w, 16).ok().filter(|_| w.len() == 8).ok_or_else(|| OracleError::Word(w.clone()));
217        let (mul, add) = (op("fmul")?, op("fadd")?);
218        let mut vectors = Vec::new();
219        for [a, b] in mul.vectors.iter().chain(&add.vectors) {
220            vectors.push([word(a)?, word(b)?, 0]);
221        }
222        vectors.extend(fma_vectors());
223        let oracle = Oracle { products: classes(mul)?, sums: classes(add)?, vectors };
224        for (what, n, most) in [("product classes", oracle.products.len(), MAX_PRODUCT_CLASSES), ("sum classes", oracle.sums.len(), MAX_SUM_CLASSES)] {
225            if n == 0 || n > most {
226                return Err(OracleError::TooMany(what, n));
227            }
228        }
229        if oracle.vectors.len() > 1 << 16 {
230            return Err(OracleError::TooMany("vectors", oracle.vectors.len()));
231        }
232        Ok(oracle)
233    }
234
235    /// The store's beats of counters, eight to a beat.
236    pub fn counter_beats(&self) -> usize {
237        (self.products.len() + self.sums.len()).div_ceil(8)
238    }
239
240    /// The store's first record beat.
241    pub fn records_at(&self) -> usize {
242        1 + self.counter_beats()
243    }
244
245    /// The most records a run stores.
246    pub fn record_cap(&self) -> usize {
247        STORE_BEATS - self.records_at()
248    }
249
250    /// Bits of the vector ROM's index.
251    fn index_bits(&self) -> usize {
252        (usize::BITS - (self.vectors.len().max(2) - 1).leading_zeros()) as usize
253    }
254
255    /// The oracle's core (`logos_fp_oracle`) with its soft classes, its vector ROM and its store: every module the
256    /// engine top instantiates but the crossing and the hard block.
257    pub fn core_verilog(&self) -> String {
258        let mut v = self.units_verilog();
259        v.push_str(&self.rom_verilog());
260        v.push_str(&ram_ce_verilog(&[512]));
261        v.push_str(&self.core_text());
262        v
263    }
264
265    /// The modules the oracle writes itself, which say what the card measures and how its record reads: its classes'
266    /// units, its vector ROM and delay line, its core. The rest of [`Oracle::engine_verilog`] is the library's it
267    /// embeds: the engine's crossing, the store's memory, and the persona's engine top over the engine's ports.
268    pub fn own_verilog(&self) -> String {
269        let mut v = self.units_verilog();
270        v.push_str(&self.rom_verilog());
271        v.push_str(&self.core_text());
272        v
273    }
274
275    /// The soft classes' units: each product class's multiply, each sum class's add.
276    fn units_verilog(&self) -> String {
277        let mut v = String::new();
278        for (k, c) in self.products.iter().enumerate() {
279            v.push_str(&fmul_verilog(&format!("logos_fpo_mul{k}"), &c.semantics));
280        }
281        for (k, c) in self.sums.iter().enumerate() {
282            v.push_str(&fadd_verilog(&format!("logos_fpo_add{k}"), &c.semantics));
283        }
284        v
285    }
286
287    /// The persona's `logos_engine`: the core behind the engine's crossing, with every module it is built of but the
288    /// hard block, which Quartus places.
289    pub fn engine_verilog(&self) -> String {
290        format!("{}\n{}\n{}", bridge_verilog(), self.core_verilog(), top_text())
291    }
292
293    fn rom_verilog(&self) -> String {
294        let ib = self.index_bits();
295        let mut v = format!(
296            "// The oracle's vectors, (C, B, A) a word: the multiply's and the add's discriminators, then the multiply-add's.\n\
297             module logos_fpo_vectors(input [{}:0] i, output reg [95:0] v);\n  always @* case (i)\n",
298            ib - 1
299        );
300        for (k, [a, b, c]) in self.vectors.iter().enumerate() {
301            writeln!(v, "    {ib}'d{k}: v = 96'h{c:08x}_{b:08x}_{a:08x};").unwrap();
302        }
303        v.push_str("    default: v = 96'd0;\n  endcase\nendmodule\n\n");
304        v.push_str(LATE);
305        v
306    }
307
308    fn core_text(&self) -> String {
309        let (nm, na) = (self.products.len(), self.sums.len());
310        let ib = self.index_bits();
311        let mut units = String::new();
312        for k in 0..nm {
313            writeln!(units, "  wire [31:0] pm{k};\n  logos_fpo_mul{k} um{k}(.a(t[31:0]), .b(t[63:32]), .y(pm{k}));").unwrap();
314        }
315        for k in 0..na {
316            writeln!(units, "  wire [31:0] pa{k};\n  logos_fpo_add{k} ua{k}(.a(t[31:0]), .b(t[63:32]), .y(pa{k}));").unwrap();
317        }
318        let agree = |n: usize, w: &str, hard: &str| (0..n).rev().map(|k| format!("{w}{k} == {hard}")).collect::<Vec<_>>().join(", ");
319        let mut counters = String::new();
320        let mut clear = String::new();
321        let mut bump = String::new();
322        for k in 0..nm {
323            writeln!(counters, "  reg [47:0] cm{k};").unwrap();
324            write!(clear, " cm{k} <= 48'd0;").unwrap();
325            writeln!(bump, "          if (!agree_m[{k}]) cm{k} <= cm{k} + 48'd1;").unwrap();
326        }
327        for k in 0..na {
328            writeln!(counters, "  reg [47:0] ca{k};").unwrap();
329            write!(clear, " ca{k} <= 48'd0;").unwrap();
330            writeln!(bump, "          if (!agree_a[{k}]) ca{k} <= ca{k} + 48'd1;").unwrap();
331        }
332        let all: Vec<String> = (0..nm).map(|k| format!("cm{k}")).chain((0..na).map(|k| format!("ca{k}"))).collect();
333        let mut dump = String::new();
334        for (b, chunk) in all.chunks(8).enumerate() {
335            let words: Vec<String> = (0..8).rev().map(|i| chunk.get(i).map_or("64'd0".to_string(), |c| format!("{{16'd0, {c}}}"))).collect();
336            writeln!(dump, "      {}: dump_word = {{{}}};", b + 1, words.join(", ")).unwrap();
337        }
338        CORE.replace("@NM@", &nm.to_string())
339            .replace("@NA@", &na.to_string())
340            .replace("@NV@", &self.vectors.len().to_string())
341            .replace("@IB@", &ib.to_string())
342            .replace("@IBM@", &(ib - 1).to_string())
343            .replace("@BEATS@", &STORE_BEATS.to_string())
344            .replace("@AB@", &(STORE_BEATS.trailing_zeros()).to_string())
345            .replace("@RECORDS@", &self.records_at().to_string())
346            .replace("@RECORDS_M1@", &(self.records_at() - 1).to_string())
347            .replace("@NVM1@", &(self.vectors.len() - 1).to_string())
348            .replace("@CAP@", &self.record_cap().to_string())
349            .replace("@TAPS@", &TAPS.to_string())
350            .replace("@DRAIN@", &DRAIN.to_string())
351            .replace("@MAGIC@", &format!("64'h{MAGIC:016x}"))
352            .replace("@HARD@", &hard_blocks())
353            .replace("@UNITS@", &units)
354            .replace("@AGREE_M@", &agree(nm, "pm", "hm"))
355            .replace("@AGREE_A@", &agree(na, "pa", "ha"))
356            .replace("@COUNTERS@", &counters)
357            .replace("@CLEAR@", &clear)
358            .replace("@BUMP@", &bump)
359            .replace("@DUMP@", &dump)
360    }
361}
362
363/// The block's clock parameters, in the order the FP DSP IP (`altera_fpdsp_block`) sets them.
364const CLOCKS: [&str; 13] = [
365    "accumulate_clock",
366    "ax_clock",
367    "ay_clock",
368    "az_clock",
369    "accum_pipeline_clock",
370    "ax_chainin_pl_clock",
371    "mult_pipeline_clock",
372    "accum_2nd_pipeline_clock",
373    "ax_chainin_2nd_pl_clock",
374    "mult_2nd_pipeline_clock",
375    "accum_adder_clock",
376    "adder_input_clock",
377    "output_clock",
378];
379
380/// The registers each mode uses fully registered, every operand the mode reads crossing as many to the adder: the
381/// multiply's and the multiply-add's their input register and two pipeline registers (AX the chain-in pipeline's, AY
382/// and AZ the multiplier's), the add's its input register alone, Quartus allowing an add no multiplier pipeline.
383pub const MULTIPLY_REGISTERS: &[&str] = &["ay_clock", "az_clock", "mult_pipeline_clock", "mult_2nd_pipeline_clock", "output_clock"];
384pub const ADD_REGISTERS: &[&str] = &["ax_clock", "ay_clock", "adder_input_clock", "output_clock"];
385pub const MULTIPLY_ADD_REGISTERS: &[&str] = &[
386    "ax_clock",
387    "ay_clock",
388    "az_clock",
389    "ax_chainin_pl_clock",
390    "mult_pipeline_clock",
391    "ax_chainin_2nd_pl_clock",
392    "mult_2nd_pipeline_clock",
393    "adder_input_clock",
394    "output_clock",
395];
396
397/// The register a least-registered block keeps: its output's.
398pub const LEAST_REGISTERS: &[&str] = &["output_clock"];
399
400/// One `fourteennm_fp_mac` named `name` in `mode` (`sp_mult`, `sp_add`, `sp_mult_add`), the registers of `used` on
401/// clock 0 enabled by `enable` and every other `NONE`, the operands `[ax, ay, az]` (only those its mode reads connected,
402/// [`MODE_OPERANDS`], and no internal-use port), the result `y` and the eight flags
403/// `flags` (the multiply's then the add's {inexact, invalid, overflow, underflow}); with the wires its chain-out and its
404/// test output drive, `{name}_chain` and `{name}_dft`, which nothing reads.
405pub(crate) fn fp_mac(mode: &str, name: &str, used: &[&str], enable: &str, operands: [&str; 3], y: &str, flags: &str) -> String {
406    let params: Vec<String> = CLOCKS.iter().map(|c| format!(".{c}(\"{}\")", if used.contains(c) { "0" } else { "NONE" })).collect();
407    let read = MODE_OPERANDS.iter().find(|(m, _)| *m == mode).unwrap_or_else(|| panic!("no block mode {mode}")).1;
408    let ports: String = ["ax", "ay", "az"].iter().zip(operands).zip(read).filter(|(_, r)| *r).map(|((p, o), _)| format!(".{p}({o}), ")).collect();
409    format!(
410        "  wire [31:0] {name}_chain;\n  wire {name}_dft;\n  fourteennm_fp_mac #({}, .clear_type(\"NONE\"), .use_chainin(\"false\"), \
411         .operation_mode(\"{mode}\"), .adder_subtract(\"false\")) {name}(\n    .clk({{2'b00, clk}}), .ena({{2'b00, {enable}}}), .clr(2'b00), \
412         {ports}.accumulate(1'b0), .chainin(32'd0), .resulta({y}), .chainout({name}_chain),\n    .mult_inexact({flags}[0]), \
413         .mult_invalid({flags}[1]), .mult_overflow({flags}[2]), .mult_underflow({flags}[3]),\n    .adder_inexact({flags}[4]), \
414         .adder_invalid({flags}[5]), .adder_overflow({flags}[6]), .adder_underflow({flags}[7]), .dftout({name}_dft));\n",
415        params.join(", ")
416    )
417}
418
419/// The operands each mode of the block reads, of `ax`, `ay` and `az`: the multiply ay·az, the add ax+ay, the multiply-add
420/// ax+ay·az. Quartus refuses a block with a port its mode does not read connected.
421const MODE_OPERANDS: [(&str, [bool; 3]); 3] = [("sp_mult", [false, true, true]), ("sp_add", [true, true, false]), ("sp_mult_add", [true, true, true])];
422
423/// The oracle's hard blocks: the multiply, the add and the multiply-add fully registered, then the multiply and the
424/// add with their output register alone, each stepping at the compute step.
425fn hard_blocks() -> String {
426    let blocks: [(&str, &str, &[&str], [&str; 3], [&str; 2]); 5] = [
427        ("sp_mult", "hmul", MULTIPLY_REGISTERS, ["32'd0", "opr[31:0]", "opr[63:32]"], ["hm_now", "fm_now"]),
428        ("sp_add", "hadd", ADD_REGISTERS, ["opr[31:0]", "opr[63:32]", "32'd0"], ["ha_now", "fa_now"]),
429        ("sp_mult_add", "hfma", MULTIPLY_ADD_REGISTERS, ["opr[95:64]", "opr[31:0]", "opr[63:32]"], ["hf_now", "ff_now"]),
430        ("sp_mult", "hmul1", LEAST_REGISTERS, ["32'd0", "opr[31:0]", "opr[63:32]"], ["hm1_now", "fm1_now"]),
431        ("sp_add", "hadd1", LEAST_REGISTERS, ["opr[31:0]", "opr[63:32]", "32'd0"], ["ha1_now", "fa1_now"]),
432    ];
433    blocks.iter().map(|&(mode, name, used, operands, [y, flags])| fp_mac(mode, name, used, "step_en", operands, y, flags)).collect()
434}
435
436/// The engine's ports as the crossing names them: whether the engine takes it, its width's brackets, its name.
437fn engine_ports() -> Vec<(bool, String, String)> {
438    let bridge = bridge_verilog();
439    let start = bridge.find("input fmid,").expect("the crossing's ports") + "input fmid,".len();
440    let end = start + bridge[start..].find(");").expect("their end");
441    bridge[start..end]
442        .split(",\n")
443        .map(|pair| {
444            let first = pair.split(", ").next().expect("a port pair").trim();
445            let input = first.starts_with("input");
446            let last = first.split_whitespace().last().expect("a name");
447            let name = last.strip_prefix("s_").expect("the shell's side of a port").to_string();
448            let width = first.find('[').map_or(String::new(), |a| first[a..=first.find(']').expect("a width's end")].to_string());
449            (input, width, name)
450        })
451        .collect()
452}
453
454/// `logos_engine`: the oracle's core stepping in the user clock at the compute clock's rising edges, reading its store
455/// the user cycle after the falling edges, its ports through the engine's crossing.
456fn top_text() -> String {
457    let ports = engine_ports();
458    let decls: Vec<String> = ports
459        .iter()
460        .map(|(input, width, name)| format!("{} {}{name}", if *input { "input" } else { "output" }, if width.is_empty() { String::new() } else { format!("{width} ") }))
461        .collect();
462    let mut wires = String::new();
463    let mut bridge = vec![".sclk(clk), .fclk(fclk), .frst(frst), .fedge(fedge), .fmid(fmid)".to_string()];
464    let mut core = vec![".clk(fclk), .step_en(fedge), .read_en(cr)".to_string()];
465    for (_, width, name) in &ports {
466        writeln!(wires, "  wire {}f_{name};", if width.is_empty() { String::new() } else { format!("{width} ") }).unwrap();
467        bridge.push(format!(".s_{name}({name}), .f_{name}(f_{name})"));
468        core.push(format!(".{name}(f_{name})"));
469    }
470    format!(
471        "// The oracle as the shell's engine: its core stepping in the user clock (fclk) at the compute clock's rising edges\n\
472         // (fedge), its store read the user cycle after the falling edges, the shell's values and its own through the\n\
473         // engine's crossing.\n\
474         module logos_engine\n  (input clk,\n   {},\n   input fclk, input frst, input fedge, input fmid);\n  \
475         reg cr;\n  always @(posedge fclk) cr <= fmid;\n{wires}  logos_engine_bridge bridge({});\n  logos_fp_oracle core({});\nendmodule\n",
476        decls.join(",\n   "),
477        bridge.join(",\n    "),
478        core.join(",\n    ")
479    )
480}
481
482/// A hard block's result and flags delayed 0 to 15 steps.
483const LATE: &str = r#"// x delayed d steps, 0 to 15, at the compute step; every register clear from reset.
484module logos_fpo_late(input clk, input step_en, input rst, input [3:0] d, input [39:0] x, output [39:0] y);
485  reg [39:0] line [1:15];
486  integer i;
487  always @(posedge clk) if (step_en) begin
488    if (rst) for (i = 1; i < 16; i = i + 1) line[i] <= 40'd0;
489    else begin
490      line[1] <= x;
491      for (i = 2; i < 16; i = i + 1) line[i] <= line[i-1];
492    end
493  end
494  assign y = d == 4'd0 ? x : line[d];
495endmodule
496
497"#;
498
499const CORE: &str = r#"// The oracle's core: each step an operand triple (C, B, A) from the run's source into the three hard blocks and a delay
500// line, the soft classes reading the line at the run's tap, so each meets the hard results of its own operands; a counter
501// a class, one more for each result of it the block's differs from; the steps no class explains counted and their
502// records stored, or every step's in a recording run; then the header and the counters stored, and the core idle.
503module logos_fp_oracle
504  (input clk, input step_en, input read_en, input rst,
505   input start, input [31:0] job_at, input [31:0] job_end, input [15:0] rows, input state_f16, input cache_int8, input two_words, input per_block,
506   output busy, output reg fault, output reg [31:0] pc,
507   input host_wr, input [2:0] host_table, input [31:0] host_addr, input [511:0] host_wdata,
508   input [31:0] host_raddr, output [511:0] host_rdata,
509   output mem_rd, output [31:0] mem_rd_line, input mem_rd_valid, input [511:0] mem_rd_data,
510   output mem_wr, output [31:0] mem_wr_line, output [511:0] mem_wr_data, output [63:0] mem_wr_mask, input mem_wr_ready);
511  localparam NM = @NM@, NA = @NA@, NV = @NV@, TAPS = @TAPS@, RECORDS = @RECORDS@, CAP = @CAP@;
512  assign mem_rd = 1'b0;
513  assign mem_rd_line = 32'd0;
514  assign mem_wr = 1'b0;
515  assign mem_wr_line = 32'd0;
516  assign mem_wr_data = 512'd0;
517  assign mem_wr_mask = 64'd0;
518
519  localparam IDLE = 2'd0, RUN = 2'd1, DUMP = 2'd2;
520  reg [1:0] state;
521  assign busy = state != IDLE;
522  reg [31:0] word, left;
523  reg [5:0] drain;
524  reg [63:0] seed, s;
525  reg [@IBM@:0] at;
526  reg [63:0] steps, lone_m, lone_a, records;
527  reg [11:0] beat;
528
529  // The stream: two xorshift words a step.
530  function [63:0] xs(input [63:0] x);
531    reg [63:0] y;
532    begin
533      y = x ^ (x << 13);
534      y = y ^ (y >> 7);
535      xs = y ^ (y << 17);
536    end
537  endfunction
538  wire [63:0] r1 = xs(s), r2 = xs(r1);
539  wire [95:0] rom_v;
540  logos_fpo_vectors rom(.i(at), .v(rom_v));
541  // Near: a's exponent zero, one, two, 255, 254 or any. Half the time b's places the product within -4 to 3 binades of
542  // the least normal (127 - ea + d), a's then from the lower half when any, which b's can meet; a quarter, b's is a's
543  // on a coin or within as many binades of it (ea + d), its significand a's but for the low byte and its sign the
544  // other, so the sum cancels deep; else b's is any, or one of the ends.
545  wire [1:0] kb = r2[7:6];
546  wire [2:0] ka = r2[2:0];
547  wire [7:0] ea = ka == 3'd0 ? 8'd0 : ka == 3'd1 ? 8'd1 : ka == 3'd2 ? 8'd2 : ka == 3'd3 ? 8'd255 : ka == 3'd4 ? 8'd254
548                : kb[1] ? r1[30:23] : {1'b0, r1[29:23]};
549  wire signed [10:0] d = $signed({{8{r2[5]}}, r2[5:3]});
550  wire cancel = kb == 2'd2;
551  wire [7:0] eend = r2[9:8] == 2'd0 ? 8'd0 : r2[9:8] == 2'd1 ? 8'd1 : r2[9:8] == 2'd2 ? 8'd254 : 8'd255;
552  wire signed [10:0] eb_w = !kb[1] ? 11'sd127 - $signed({3'd0, ea}) + d : cancel ? $signed({3'd0, ea}) + (r2[11] ? 11'sd0 : d)
553                          : r2[10] ? $signed({3'd0, r1[62:55]}) : $signed({3'd0, eend});
554  wire [7:0] eb = eb_w < 11'sd0 ? 8'd0 : eb_w > 11'sd255 ? 8'd255 : eb_w[7:0];
555  wire [22:0] mb = cancel ? r1[22:0] ^ {15'd0, r1[39:32]} : r1[54:32];
556  wire [95:0] near_v = {r2[63:32], cancel ? !r1[31] : r1[63], eb, mb, r1[31], ea, r1[22:0]};
557  wire [95:0] next_v = word[1:0] == 2'd0 ? rom_v : word[1:0] == 2'd1 ? {r2[31:0], r1} : near_v;
558
559  // The triple the hard blocks take this step, and the delay line the soft classes read at the tap.
560  reg [95:0] opr;
561  reg ov;
562  reg [95:0] dl [0:TAPS-1];
563  reg [TAPS-1:0] dv;
564  wire [3:0] tap = word[7:4];
565  wire [95:0] t = dl[tap];
566  wire tv = dv[tap];
567
568  // The hard blocks' results and flags as they leave them, then each delayed the run's steps for its block.
569  wire [31:0] hm_now, ha_now, hf_now, hm1_now, ha1_now;
570  wire [7:0] fm_now, fa_now, ff_now, fm1_now, fa1_now;
571@HARD@
572  wire [31:0] hm, ha, hf;
573  wire [7:0] fm, fa, ff;
574  logos_fpo_late lm(.clk(clk), .step_en(step_en), .rst(rst), .d(word[11:8]), .x({fm_now, hm_now}), .y({fm, hm}));
575  logos_fpo_late la(.clk(clk), .step_en(step_en), .rst(rst), .d(word[15:12]), .x({fa_now, ha_now}), .y({fa, ha}));
576  logos_fpo_late lf(.clk(clk), .step_en(step_en), .rst(rst), .d(word[19:16]), .x({ff_now, hf_now}), .y({ff, hf}));
577@UNITS@
578  wire [NM-1:0] agree_m = {@AGREE_M@};
579  wire [NA-1:0] agree_a = {@AGREE_A@};
580  wire lonely = !(|agree_m) || !(|agree_a);
581  wire keep = tv && records < 64'd@CAP@ && (word[2] || lonely);
582@COUNTERS@
583  // A record: (B, A), (the hard product, C), (the hard multiply-add, the hard sum), the three blocks' flags {underflow,
584  // overflow, invalid, inexact} of the multiply then the add with the least-registered multiply's result as it leaves
585  // it, the product classes' agreement, the sum classes' agreement and the least-registered add's result.
586  wire [127:0] am = agree_m;
587  wire [95:0] aa = agree_a;
588  wire [511:0] record = {ha1_now, aa, am, 8'd0, hm1_now, ff, fa, fm, hf, ha, hm, t[95:64], t[63:0]};
589  reg [511:0] dump_word;
590  always @* case (beat)
591      0: dump_word = {64'd@NA@, 64'd@NM@, {32'd0, word}, records, lone_a, lone_m, steps, @MAGIC@};
592@DUMP@      default: dump_word = 512'd0;
593  endcase
594  wire st_we = (state == RUN && keep) || state == DUMP;
595  wire [@AB@-1:0] st_wa = state == DUMP ? beat[@AB@-1:0] : RECORDS + records[@AB@-1:0];
596  wire [511:0] st_wd = state == DUMP ? dump_word : record;
597  logos_ram_ce_w512 #(.D(@BEATS@), .A(@AB@)) store(.clk(clk), .ce(step_en), .cr(read_en), .ra(host_raddr[@AB@-1:0]),
598    .rd(host_rdata), .we(st_we), .wa(st_wa), .wd(st_wd), .wm({64{1'b1}}));
599
600  integer i;
601  always @(posedge clk) if (step_en) begin
602    if (rst) begin
603      state <= IDLE; fault <= 1'b0; pc <= 32'd0; seed <= 64'd0; opr <= 96'd0; ov <= 1'b0; dv <= {TAPS{1'b0}};
604    end else begin
605      if (host_wr && host_table == 3'd0 && host_addr == 32'd0) seed <= host_wdata[63:0];
606      case (state)
607        IDLE: if (start) begin
608          state <= RUN; word <= job_at; left <= job_end; drain <= @DRAIN@;
609          s <= seed == 64'd0 ? 64'h9e37_79b9_7f4a_7c15 : seed; at <= 0;
610          steps <= 0; lone_m <= 0; lone_a <= 0; records <= 0; pc <= 32'd0; ov <= 1'b0; dv <= {TAPS{1'b0}};
611         @CLEAR@
612        end
613        RUN: begin
614          if (left != 32'd0) begin
615            opr <= next_v; ov <= 1'b1; left <= left - 32'd1;
616            if (word[1:0] == 2'd0) at <= at == @IB@'d@NVM1@ ? @IB@'d0 : at + 1'b1;
617            else s <= r2;
618          end else begin
619            ov <= 1'b0;
620            if (drain == 6'd0) begin state <= DUMP; beat <= 12'd0; end else drain <= drain - 6'd1;
621          end
622          dl[0] <= opr;
623          for (i = 1; i < TAPS; i = i + 1) dl[i] <= dl[i-1];
624          dv <= {dv[TAPS-2:0], ov};
625          if (tv) begin
626            steps <= steps + 64'd1;
627            pc <= pc + 32'd1;
628            if (!(|agree_m)) lone_m <= lone_m + 64'd1;
629            if (!(|agree_a)) lone_a <= lone_a + 64'd1;
630@BUMP@          end
631          if (keep) records <= records + 64'd1;
632        end
633        default: if (beat == 12'd@RECORDS_M1@) state <= IDLE; else beat <= beat + 12'd1;
634      endcase
635    end
636  end
637endmodule
638"#;
639
640/// The floating-point DSP block's interface as the device declares it: its parameters and its ports.
641const FP_MAC_HEADER: &str = "module fourteennm_fp_mac #(parameter accumulate_clock = \"none\", ax_clock = \"none\", ay_clock = \"none\",\n  \
642     az_clock = \"none\", accum_pipeline_clock = \"none\", ax_chainin_pl_clock = \"none\", mult_pipeline_clock = \"none\",\n  \
643     accum_2nd_pipeline_clock = \"none\", ax_chainin_2nd_pl_clock = \"none\", mult_2nd_pipeline_clock = \"none\",\n  \
644     accum_adder_clock = \"none\", adder_input_clock = \"none\", output_clock = \"none\", clear_type = \"NONE\",\n  \
645     use_chainin = \"false\", operation_mode = \"sp_mult\", adder_subtract = \"false\")\n  \
646     (input [2:0] clk, input [2:0] ena, input [1:0] clr, input [31:0] ax, input [31:0] ay, input [31:0] az,\n   \
647     input accumulate, input [31:0] chainin, input dfxlfsrena, input dfxmisrena, output [31:0] resulta,\n   \
648     output [31:0] chainout, output adder_inexact, output adder_invalid, output adder_overflow, output adder_underflow,\n   \
649     output mult_inexact, output mult_invalid, output mult_overflow, output mult_underflow, output dftout);\n";
650
651/// The floating-point DSP block as the timing model takes it, a box only Quartus elaborates: its interface, one DSP
652/// block large (`logos_box_area`, two halves), its results registers inside it.
653pub fn fp_mac_blackbox() -> String {
654    format!("(* blackbox, logos_box_area = \"0,0,2,0,0\" *)\n{FP_MAC_HEADER}endmodule\n")
655}
656
657/// How many steps deep a stand-in block is: fully registered, the multiply, the add and the multiply-add; with its
658/// output register alone, the multiply and the add.
659#[derive(Clone, Copy, Debug, PartialEq, Eq)]
660pub struct Depths {
661    pub full: [usize; 3],
662    pub least: [usize; 2],
663}
664
665/// `fourteennm_fp_mac` as a simulator runs the oracle: `product` and `sum` the semantics a block's multiply and add
666/// compute (the multiply-add one after the other), `depths` the steps deep a block is in each mode and registering,
667/// each register zero from configuration.
668/// The device's own model is encrypted; this one stands in for it so a simulation of the persona can be held to what
669/// the oracle must say of a block whose semantics are known.
670pub fn hard_model_verilog(product: &Semantics, sum: &Semantics, depths: Depths) -> String {
671    for latency in depths.full.iter().chain(&depths.least) {
672        assert!((1..=TAPS).contains(latency), "a block {latency} steps deep");
673    }
674    let ([lm, la, lf], [lm1, la1]) = (depths.full, depths.least);
675    format!(
676        "{}{}// A floating-point DSP block for simulation: the multiply ay·az ({lm} steps deep, {lm1} with its pipeline unregistered),\n\
677         // the add ax+ay ({la}, {la1}), the multiply-add ax+ay·az ({lf}), at clk[0] where ena[0].\n\
678         {FP_MAC_HEADER}  wire [31:0] p, q, r;\n  \
679         logos_fpo_hard_mul m(.a(ay), .b(az), .y(p));\n  \
680         logos_fpo_hard_add s(.a(ax), .b(ay), .y(q));\n  \
681         logos_fpo_hard_add f(.a(ax), .b(p), .y(r));\n  \
682         wire [31:0] y = operation_mode == \"sp_mult\" ? p : operation_mode == \"sp_add\" ? q : r;\n  \
683         localparam integer L = operation_mode == \"sp_mult\" ? (mult_pipeline_clock == \"NONE\" ? {lm1} : {lm})\n  \
684         \x20 : operation_mode == \"sp_add\" ? (adder_input_clock == \"NONE\" ? {la1} : {la}) : {lf};\n  \
685         // Every register zero from configuration, as the device's are.\n  \
686         reg [31:0] line [0:{last}];\n  \
687         integer i;\n  \
688         initial for (i = 0; i <= {last}; i = i + 1) line[i] = 32'd0;\n  \
689         always @(posedge clk[0]) if (ena[0]) begin\n    line[0] <= y;\n    for (i = 1; i <= {last}; i = i + 1) line[i] <= line[i-1];\n  end\n  \
690         assign resulta = line[L-1];\n  \
691         assign chainout = 32'd0;\n  \
692         assign {{adder_inexact, adder_invalid, adder_overflow, adder_underflow}} = 4'd0;\n  \
693         assign {{mult_inexact, mult_invalid, mult_overflow, mult_underflow}} = 4'd0;\n  \
694         assign dftout = 1'b0;\nendmodule\n",
695        fmul_verilog("logos_fpo_hard_mul", product),
696        fadd_verilog("logos_fpo_hard_add", sum),
697        last = TAPS - 1,
698    )
699}
700
701/// The smallest engine the shell is built for: the oracle stands in its place, so nothing of it is the oracle's but
702/// the store's depth.
703fn shell_engine() -> EngineParams {
704    EngineParams {
705        rows: 1,
706        vector_rows: 1,
707        side_rows: 1,
708        pair_rows: 1,
709        store_beats: STORE_BEATS,
710        job_words: 1,
711        tensors: 1,
712        layers: 1,
713        ropes: 1,
714        formats: Vec::new(),
715        delta: None,
716        attend: None,
717        function_units: 16,
718        folds: false,
719        streaming: None,
720    }
721}
722
723/// The persona's accelerator configuration: an engine build on plain 512-bit lines, its compute clock the user clock
724/// divided by `divide`, its build identity (the BUILD register) the first eight bytes of the oracle engine's SHA-256.
725pub fn persona_config(oracle: &Oracle, divide: u32) -> AfuConfig {
726    let digest = sha256(oracle.engine_verilog().as_bytes());
727    AfuConfig {
728        name: "logos_d5005_fp_oracle",
729        build: u64::from_be_bytes(digest[..8].try_into().expect("eight bytes")),
730        compute_divide: divide,
731        ..AfuConfig::engine(shell_engine(), LocalMemoryLine::Data512)
732    }
733}
734
735/// Every file of the persona's tree for `scripts/d5005/build-afu.sh`: the engine build's files
736/// ([`afu_sources`] of [`persona_config`]) with the oracle for its engine (the card's floating-point DSP blocks left to
737/// Quartus), its simulation twin the oracle beside a stand-in block computing `model`, `depths` deep (what
738/// `scripts/d5005/run-cosim.sh` runs), and the measurement the card runs, [`HOST_PROGRAM`] with its class table
739/// ([`host_header`]).
740pub fn persona_sources(oracle: &Oracle, divide: u32, model: &Semantics, depths: Depths) -> Vec<AfuFile> {
741    let engine = oracle.engine_verilog();
742    let mut files: Vec<AfuFile> = afu_sources(&persona_config(oracle, divide))
743        .into_iter()
744        .map(|f| match f.path {
745            "hw/rtl/logos_engine.v" => AfuFile { path: f.path, text: engine.clone() },
746            "sim/logos_engine_gated.v" => AfuFile { path: f.path, text: format!("{engine}\n{}", hard_model_verilog(model, model, depths)) },
747            // The core proven one clock on its own hierarchy and boxed in the shell's check is the oracle's.
748            p if p.ends_with("logos_d5005_domains.json") => {
749                assert_eq!(f.text.matches("\"logos_engine_core_ce\"").count(), 1, "the engine's core in the clock-domain map");
750                AfuFile { path: f.path, text: f.text.replace("\"logos_engine_core_ce\"", "\"logos_fp_oracle\"") }
751            }
752            _ => f,
753        })
754        .collect();
755    files.push(AfuFile { path: "sw/logos_fp_oracle.c", text: HOST_PROGRAM.to_string() });
756    files.push(AfuFile { path: "sw/logos_fp_oracle.h", text: host_header(oracle) });
757    files
758}
759
760/// The store's header.
761#[derive(Clone, Copy, Debug, PartialEq, Eq)]
762pub struct Header {
763    pub magic: u64,
764    /// The triples compared.
765    pub steps: u64,
766    /// Steps no product class explains, and no sum class.
767    pub lone_products: u64,
768    pub lone_sums: u64,
769    /// Records stored.
770    pub records: u64,
771    pub run: u32,
772    pub product_classes: u64,
773    pub sum_classes: u64,
774}
775
776/// A record: the operands, the three hard results and their flags (bits 3:0 the multiply's {inexact, invalid,
777/// overflow, underflow} and 7:4 the add's, of each block), which classes agree with the block, and the
778/// least-registered blocks' results.
779#[derive(Clone, Debug, PartialEq, Eq)]
780pub struct Record {
781    pub a: u32,
782    pub b: u32,
783    pub c: u32,
784    pub product: u32,
785    pub sum: u32,
786    pub fma: u32,
787    pub flags: [u8; 3],
788    pub products_agree: Vec<bool>,
789    pub sums_agree: Vec<bool>,
790    /// The least-registered multiply's and add's results as they leave them, on the triple their latency before.
791    pub least: [u32; 2],
792}
793
794/// The store read back: the header, each class's counter, the records.
795#[derive(Clone, Debug, PartialEq, Eq)]
796pub struct Store {
797    pub header: Header,
798    pub products: Vec<u64>,
799    pub sums: Vec<u64>,
800    pub records: Vec<Record>,
801}
802
803impl Store {
804    /// The store from its beats (each eight 64-bit words, the lowest first), as far as the header's record count
805    /// reaches; `None` for a header that is not the oracle's or does not name `oracle`'s classes.
806    pub fn read(oracle: &Oracle, beats: &[[u64; 8]]) -> Option<Store> {
807        let h = beats.first()?;
808        let header = Header {
809            magic: h[0],
810            steps: h[1],
811            lone_products: h[2],
812            lone_sums: h[3],
813            records: h[4],
814            run: h[5] as u32,
815            product_classes: h[6],
816            sum_classes: h[7],
817        };
818        let (nm, na) = (oracle.products.len(), oracle.sums.len());
819        if header.magic != MAGIC || header.product_classes != nm as u64 || header.sum_classes != na as u64 {
820            return None;
821        }
822        let counters: Vec<u64> = beats.get(1..oracle.records_at())?.iter().flatten().copied().collect();
823        let records = beats
824            .get(oracle.records_at()..oracle.records_at() + header.records as usize)?
825            .iter()
826            .map(|w| {
827                let bit = |base: usize, k: usize| w[base + k / 64] >> (k % 64) & 1 == 1;
828                Record {
829                    a: w[0] as u32,
830                    b: (w[0] >> 32) as u32,
831                    c: w[1] as u32,
832                    product: (w[1] >> 32) as u32,
833                    sum: w[2] as u32,
834                    fma: (w[2] >> 32) as u32,
835                    flags: [w[3] as u8, (w[3] >> 8) as u8, (w[3] >> 16) as u8],
836                    products_agree: (0..nm).map(|k| bit(4, k)).collect(),
837                    sums_agree: (0..na).map(|k| bit(6, k)).collect(),
838                    least: [(w[3] >> 24) as u32, (w[7] >> 32) as u32],
839                }
840            })
841            .collect();
842        Some(Store { header, products: counters[..nm].to_vec(), sums: counters[nm..nm + na].to_vec(), records })
843    }
844}
845
846/// The host program's table of the oracle's classes (`sw/logos_fp_oracle.h`): their counts, the store's layout, and
847/// each class's members by name.
848pub fn host_header(oracle: &Oracle) -> String {
849    let mut h = String::from("/* The oracle the persona was built with: its classes, by their members' names, and its store's layout. */\n");
850    writeln!(h, "#define LOGOS_FPO_MAGIC 0x{MAGIC:016x}ull").unwrap();
851    writeln!(h, "#define LOGOS_FPO_PRODUCTS {}", oracle.products.len()).unwrap();
852    writeln!(h, "#define LOGOS_FPO_SUMS {}", oracle.sums.len()).unwrap();
853    writeln!(h, "#define LOGOS_FPO_VECTORS {}", oracle.vectors.len()).unwrap();
854    writeln!(h, "#define LOGOS_FPO_TAPS {TAPS}").unwrap();
855    writeln!(h, "#define LOGOS_FPO_STORE_BEATS {STORE_BEATS}").unwrap();
856    writeln!(h, "#define LOGOS_FPO_RECORDS_AT {}", oracle.records_at()).unwrap();
857    writeln!(h, "#define LOGOS_FPO_RECORD_CAP {}", oracle.record_cap()).unwrap();
858    for (name, classes) in [("logos_fpo_products", &oracle.products), ("logos_fpo_sums", &oracle.sums)] {
859        writeln!(h, "static const char *const {name}[] = {{").unwrap();
860        for c in classes {
861            writeln!(h, "    \"{}\",", c.members.join(" ")).unwrap();
862        }
863        h.push_str("};\n");
864    }
865    h
866}
867
868/// The measurement on the card (`sw/logos_fp_oracle.c`): the multiply's and the add's latencies as the taps at which
869/// their classes come nearest every vector (counting runs over the vectors at each tap, no delay: the first tap of the
870/// fewest disagreements, so a block no class names still has its depth); the multiply-add's as
871/// the shift at which its results on the vectors whose C is zero are the products (a recording run at the last tap);
872/// then, the three aligned ([`Run::aligned`]), a recording run over the vectors and counting runs of `<steps>` triples
873/// from the random and the near streams; one JSON object on stdout. The exit status is 0 when every latency was found
874/// and every run completed, 1 when a latency was not, 2 when the card cannot be had or a command failed.
875pub const HOST_PROGRAM: &str = r##"/* Measure the card's floating-point DSP block with the oracle persona: find the multiply's and the add's latencies
876   as the taps at which their classes come nearest every vector, the multiply-add's as the shift at which its results with
877   C zero are the products; then, the three aligned, record every vector's results and count each class's
878   disagreements over <steps> triples of the random stream and of the near stream. One JSON object on stdout.
879
880     logos_fp_oracle [<steps>] [--seed <n>] */
881#include "logos_core.h"
882#include "logos_fp_oracle.h"
883
884#include <inttypes.h>
885#include <stdio.h>
886#include <stdlib.h>
887#include <string.h>
888
889#ifndef LOGOS_PROGRAM_MAIN
890#define LOGOS_PROGRAM_MAIN main
891#endif
892
893static uint64_t tag = 1;
894
895static int run(logos_dev *dev, uint8_t op, uint64_t a0, uint64_t a1, uint64_t a2) {
896    uint64_t t = tag++;
897    logos_record r = logos_core_run(dev, op, t, a0, a1, a2);
898    return (r.status >> 8) == t && (r.status & 0xff) == LOGOS_D5005_STATUS_OK;
899}
900
901/* A run's word: its source, whether it records every step, the soft tap, each block's delay. */
902static uint64_t run_word(unsigned source, int record, unsigned tap, const unsigned d[3]) {
903    return source | (uint64_t)(record != 0) << 2 | (uint64_t)(tap & 15) << 4 | (uint64_t)(d[0] & 15) << 8 |
904           (uint64_t)(d[1] & 15) << 12 | (uint64_t)(d[2] & 15) << 16;
905}
906
907/* One run of `steps` triples, its store read into `buf`: the header and the counters, then the records the header
908   counts. */
909static int oracle_run(logos_dev *dev, uint64_t *buf, uint64_t io, uint64_t word, uint64_t steps) {
910    if (!run(dev, LOGOS_D5005_OP_ENGINE_RUN, word, steps, 0)) return 0;
911    if (!run(dev, LOGOS_D5005_OP_STORE_READ, io >> 6, 0, LOGOS_FPO_RECORDS_AT)) return 0;
912    if (buf[0] != LOGOS_FPO_MAGIC || buf[6] != LOGOS_FPO_PRODUCTS || buf[7] != LOGOS_FPO_SUMS || buf[4] > LOGOS_FPO_RECORD_CAP) return 0;
913    return buf[4] == 0 || run(dev, LOGOS_D5005_OP_STORE_READ, (io >> 6) + LOGOS_FPO_RECORDS_AT, LOGOS_FPO_RECORDS_AT, buf[4]);
914}
915
916static uint64_t counter(const uint64_t *buf, unsigned k) { return buf[8 + k]; }
917
918static uint64_t least(const uint64_t *buf, unsigned from, unsigned n) {
919    uint64_t m = UINT64_MAX;
920    for (unsigned k = 0; k < n; ++k) if (counter(buf, from + k) < m) m = counter(buf, from + k);
921    return m;
922}
923
924/* Record r's words: (B, A), (the product, C), (the multiply-add, the sum), the flags with the least-registered
925   multiply's result above them, the product classes' agreement in two words, the sum classes' with the
926   least-registered add's result above it. */
927static const uint64_t *record(const uint64_t *buf, uint64_t r) { return buf + 8 * (LOGOS_FPO_RECORDS_AT + r); }
928
929/* The 32 bits of word `word` of record `r` from bit `at`. */
930static uint32_t field(const uint64_t *buf, uint64_t r, unsigned word, unsigned at) { return (uint32_t)(record(buf, r)[word] >> at); }
931
932/* The shift s at which field `seen` of record k is, most often, field `want` of record k + s, over the records whose C
933   is zero when `c_zero` (over all otherwise); -1 when no shift matches more than half of them. A result recorded as it
934   leaves a block of latency L, the run at tap t, is that of the record t + 1 - L on. */
935static int best_shift(const uint64_t *buf, const unsigned seen[2], const unsigned want[2], int c_zero, uint64_t *best, uint64_t *n) {
936    int shift = -1;
937    *best = 0;
938    *n = 0;
939    for (unsigned s = 0; s < LOGOS_FPO_TAPS; ++s) {
940        uint64_t matches = 0, of = 0;
941        for (uint64_t k = 0; k + s < buf[4]; ++k) {
942            if (c_zero && field(buf, k + s, 1, 0) != 0) continue;
943            ++of;
944            if (field(buf, k, seen[0], seen[1]) == field(buf, k + s, want[0], want[1])) ++matches;
945        }
946        if (matches > *best) *best = matches, *n = of, shift = (int)s;
947    }
948    return shift >= 0 && 2 * *best > *n ? shift : -1;
949}
950
951static void counters(const uint64_t *buf, const char *name) {
952    printf("    \"%s\": {\"steps\": %" PRIu64 ", \"lone_products\": %" PRIu64 ", \"lone_sums\": %" PRIu64 ", \"records\": %" PRIu64 ",\n",
953           name, buf[1], buf[2], buf[3], buf[4]);
954    printf("      \"products\": [");
955    for (unsigned k = 0; k < LOGOS_FPO_PRODUCTS; ++k) printf("%s%" PRIu64, k ? ", " : "", counter(buf, k));
956    printf("],\n      \"sums\": [");
957    for (unsigned k = 0; k < LOGOS_FPO_SUMS; ++k) printf("%s%" PRIu64, k ? ", " : "", counter(buf, LOGOS_FPO_PRODUCTS + k));
958    printf("]}");
959}
960
961static void records(const uint64_t *buf) {
962    printf("    \"records\": [");
963    for (uint64_t r = 0; r < buf[4]; ++r) {
964        const uint64_t *w = record(buf, r);
965        printf("%s\n      [\"%08" PRIx32 "\", \"%08" PRIx32 "\", \"%08" PRIx32 "\", \"%08" PRIx32 "\", \"%08" PRIx32 "\", \"%08" PRIx32 "\", \"%06" PRIx32 "\", \"%08" PRIx32 "\", \"%08" PRIx32 "\"]",
966               r ? "," : "", (uint32_t)w[0], (uint32_t)(w[0] >> 32), (uint32_t)w[1], (uint32_t)(w[1] >> 32), (uint32_t)w[2],
967               (uint32_t)(w[2] >> 32), (uint32_t)(w[3] & 0xffffff), (uint32_t)(w[3] >> 24), (uint32_t)(w[7] >> 32));
968    }
969    printf("]");
970}
971
972int LOGOS_PROGRAM_MAIN(int argc, char **argv) {
973    uint64_t steps = 1ull << 24, seed = 0x5eedf00du;
974    for (int i = 1; i < argc; ++i) {
975        if (strcmp(argv[i], "--seed") == 0 && i + 1 < argc) seed = strtoull(argv[++i], NULL, 0);
976        else steps = strtoull(argv[i], NULL, 0);
977    }
978    logos_dev *dev = logos_open(LOGOS_D5005_AFU_ID);
979    if (!dev) {
980        fprintf(stderr, "logos_fp_oracle: no card\n");
981        return 2;
982    }
983    uint64_t io = 0;
984    uint64_t *buf = logos_buffer(dev, (size_t)LOGOS_FPO_STORE_BEATS * 64, &io);
985    memset(buf, 0, (size_t)LOGOS_FPO_STORE_BEATS * 64);
986    buf[0] = seed;
987    int ok = run(dev, LOGOS_D5005_OP_ENGINE_TABLE, io >> 6, 0, 1);
988    const unsigned none[3] = {0, 0, 0};
989    int tm = -1, ta = -1, lf = -1;
990    uint64_t pmin = UINT64_MAX, smin = UINT64_MAX;
991    printf("{\n  \"seed\": %" PRIu64 ",\n  \"taps\": [", seed);
992    for (unsigned t = 0; ok && t < LOGOS_FPO_TAPS; ++t) {
993        ok = oracle_run(dev, buf, io, run_word(0, 0, t, none), LOGOS_FPO_VECTORS);
994        uint64_t p = ok ? least(buf, 0, LOGOS_FPO_PRODUCTS) : UINT64_MAX, s = ok ? least(buf, LOGOS_FPO_PRODUCTS, LOGOS_FPO_SUMS) : UINT64_MAX;
995        printf("%s{\"tap\": %u, \"least_product\": %" PRIu64 ", \"least_sum\": %" PRIu64 "}", t ? ", " : "", t, p, s);
996        if (ok && p < pmin) pmin = p, tm = (int)t;
997        if (ok && s < smin) smin = s, ta = (int)t;
998    }
999    printf("],\n");
1000    /* The multiply-add's results at the last tap undelayed: the shift at which they are the products of the triples
1001       whose C is zero. */
1002    uint64_t fbest = 0, fn = 0, mbest = 0, mn = 0, abest = 0, an = 0;
1003    const unsigned fma_at[2] = {2, 32}, product_at[2] = {1, 32}, sum_at[2] = {2, 0}, least_product[2] = {3, 24}, least_sum[2] = {7, 32};
1004    if (ok && tm >= 0 && ta >= 0) {
1005        const unsigned d[3] = {15u - (unsigned)tm, 15u - (unsigned)ta, 0};
1006        ok = oracle_run(dev, buf, io, run_word(0, 1, 15, d), LOGOS_FPO_VECTORS);
1007        int s = ok ? best_shift(buf, fma_at, product_at, 1, &fbest, &fn) : -1;
1008        if (s >= 0) lf = 16 - s;
1009    }
1010    int found = tm >= 0 && ta >= 0 && lf > 0, lm1 = -1, la1 = -1;
1011    unsigned tap = 0, d[3] = {0, 0, 0};
1012    if (ok && found) {
1013        int l[3] = {tm + 1, ta + 1, lf};
1014        int top = l[0] > l[1] ? l[0] : l[1];
1015        top = top > l[2] ? top : l[2];
1016        tap = (unsigned)(top - 1);
1017        for (int k = 0; k < 3; ++k) d[k] = tap + 1 - (unsigned)l[k];
1018        ok = oracle_run(dev, buf, io, run_word(0, 1, tap, d), LOGOS_FPO_VECTORS);
1019        /* The least-registered blocks' results as they leave them: the shift at which they are the aligned ones'. */
1020        int sm = ok ? best_shift(buf, least_product, product_at, 0, &mbest, &mn) : -1, sa = ok ? best_shift(buf, least_sum, sum_at, 0, &abest, &an) : -1;
1021        if (sm >= 0) lm1 = (int)tap + 1 - sm;
1022        if (sa >= 0) la1 = (int)tap + 1 - sa;
1023    }
1024    printf("  \"latency\": {\"product\": %d, \"sum\": %d, \"multiply_add\": %d, \"product_least\": %d, \"sum_least\": %d,\n", tm + 1, ta + 1, lf, lm1, la1);
1025    printf("    \"matches\": {\"multiply_add\": [%" PRIu64 ", %" PRIu64 "], \"product_least\": [%" PRIu64 ", %" PRIu64 "], \"sum_least\": [%" PRIu64 ", %" PRIu64 "]}},\n",
1026           fbest, fn, mbest, mn, abest, an);
1027    printf("  \"runs\": {\n");
1028    if (ok && found) {
1029        counters(buf, "vectors");
1030        printf(",\n");
1031        records(buf);
1032        const char *names[2] = {"random", "near"};
1033        for (unsigned source = 1; ok && source <= 2; ++source) {
1034            ok = oracle_run(dev, buf, io, run_word(source, 0, tap, d), steps);
1035            if (ok) {
1036                printf(",\n");
1037                counters(buf, names[source - 1]);
1038            }
1039        }
1040        printf("\n  },\n  \"run_word\": %" PRIu64 ",\n", run_word(0, 0, tap, d));
1041    } else {
1042        printf("\n  },\n");
1043    }
1044    printf("  \"complete\": %s\n}\n", ok ? "true" : "false");
1045    logos_buffer_free(dev, buf);
1046    logos_close(dev);
1047    return !ok ? 2 : !found ? 1 : 0;
1048}
1049"##;
1050
1051#[cfg(test)]
1052#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/fp_oracle/tests.rs"]
1053mod tests;