Skip to main content

logicaffeine_compile/codegen_sva/
fp_hard.rs

1//! The engine's f32 multiply and add on the card's floating-point DSP block (`fourteennm_fp_mac`), as the oracle
2//! ([`super::fp_oracle`]) measured it: a wrapper with the pipelined unit's ports (`logos_fmul_p`'s and
3//! `logos_fadd_p`'s: `clk`, `a`, `b`, `y`) and exactly its latency, computing the engine's own arithmetic
4//! ([`Semantics::ENGINE`]) on a block whose semantics are another member of the family, wherever a thin layer of
5//! logic around the block can make them one.
6//!
7//! The block is registered fully, as the IP registers each mode, when that is no deeper than the latency, and with its
8//! output register alone otherwise; registers after it make up the rest ([`plan`]). Around it: a denormal operand read
9//! as a zero of its sign when the block keeps denormal operands, a denormal result written as a zero of its sign when
10//! the block keeps denormal results (a result rounded at the denormals' precision and flushed unless it reaches the
11//! least normal, as the engine's is), and every NaN result the engine's `0x7fc00000` when the block returns another; a
12//! multiply's denormal operand is decided beside the block rather than flushed before it, so the block multiplies the
13//! significands the engine's multiply does. A
14//! block that flushes its results otherwise (tininess decided another way, or the flushed zero always positive) loses
15//! what no logic after it can restore, and is refused by name. [`hard_verilog`] writes the wrapper free-running;
16//! [`hard_verilog_ce`] writes it stepping at a clock enable, the block's registers enabled with the rest.
17
18use std::fmt::{self, Write};
19
20use super::fp_hypotheses::{Denormals, FlushSign, Nan, Semantics, Tininess};
21use super::fp_oracle::{fp_mac, fma_vectors, Oracle, ADD_REGISTERS, LEAST_REGISTERS, MULTIPLY_REGISTERS};
22
23/// The two operations a block computes for the engine.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum Op {
26    Multiply,
27    Add,
28}
29
30impl Op {
31    fn mode(self) -> &'static str {
32        match self {
33            Op::Multiply => "sp_mult",
34            Op::Add => "sp_add",
35        }
36    }
37
38    fn name(self) -> &'static str {
39        match self {
40            Op::Multiply => "multiply",
41            Op::Add => "add",
42        }
43    }
44}
45
46/// What the oracle measured of one mode of the block: the semantics it computes and its latency fully registered and
47/// with its output register alone.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct Block {
50    pub semantics: Semantics,
51    pub full: usize,
52    pub least: usize,
53}
54
55/// How the block is registered in a wrapper.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum Registering {
58    Full,
59    Least,
60}
61
62/// A wrapper's build: the block's registering, the registers after it, and what the logic around it corrects.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct Plan {
65    pub registering: Registering,
66    pub pads: usize,
67    pub flush_operands: bool,
68    pub flush_results: bool,
69    pub canonical_nan: bool,
70}
71
72/// Why a block cannot be the engine's unit.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum HardError {
75    /// Even with its output register alone the block is deeper than the unit's latency.
76    TooDeep { op: &'static str, latency: usize, least: usize },
77    /// The block computes something no logic around it makes the arithmetic it stands in for.
78    Semantics { op: &'static str, semantics: String, why: String },
79}
80
81impl fmt::Display for HardError {
82    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83        match self {
84            HardError::TooDeep { op, latency, least } => {
85                write!(f, "the block's {op} is {least} clocks deep with its output register alone, past the unit's {latency}")
86            }
87            HardError::Semantics { op, semantics, why } => write!(f, "the block's {op} ({semantics}) {why}"),
88        }
89    }
90}
91
92/// The wrapper's build for the block's `op` at `latency` clocks, standing in for the engine's own arithmetic
93/// ([`plan_for`] of [`Semantics::ENGINE`]).
94pub fn plan(op: Op, block: &Block, latency: usize) -> Result<Plan, HardError> {
95    plan_for(op, block, latency, &Semantics::ENGINE)
96}
97
98/// How many results `t` decides tiny, in order: after rounding at the denormals' place fewest, then after rounding to
99/// 24 bits, then on the exact result every one below the least normal.
100fn tinier(t: Tininess) -> u8 {
101    match t {
102        Tininess::AfterRoundingAtDenormals => 0,
103        Tininess::AfterRoundingUnbounded => 1,
104        Tininess::BeforeRounding => 2,
105    }
106}
107
108/// The wrapper's build for the block's `op` at `latency` clocks that makes it compute `target`: the logic around it
109/// flushes the denormal operands the block reads as themselves where `target` reads zeros, flushes the denormal results
110/// the block keeps where `target` flushes them at the denormals' place keeping their sign, and writes `0x7fc00000`
111/// for every NaN where that is `target`'s one NaN and the block's is another. Refused by name is a block that reads a
112/// zero or flushes a result where `target` keeps a denormal, decides tininess elsewhere than `target`, flushes to
113/// another sign, or returns NaNs `target` does not and the wrapper cannot write.
114pub fn plan_for(op: Op, block: &Block, latency: usize, target: &Semantics) -> Result<Plan, HardError> {
115    let s = &block.semantics;
116    let who = if *target == Semantics::ENGINE { "the engine".to_string() } else { target.name() };
117    let refuse = |why: String| Err(HardError::Semantics { op: op.name(), semantics: s.name(), why });
118    if s.denormal_inputs == Denormals::Flush && target.denormal_inputs == Denormals::Keep {
119        return refuse(format!("reads a denormal operand as a zero, which {who} reads as itself"));
120    }
121    match (s.denormal_results, target.denormal_results) {
122        (Denormals::Flush, Denormals::Keep) => return refuse(format!("flushes a result {who} keeps as a denormal")),
123        (Denormals::Flush, Denormals::Flush) => {
124            if tinier(s.tininess) > tinier(target.tininess) {
125                return refuse(format!("decides a result tiny before {who} does, and flushes what {who} would round into the least normal"));
126            }
127            if tinier(s.tininess) < tinier(target.tininess) {
128                return refuse(format!("rounds into the least normal a result {who} decides tiny and flushes"));
129            }
130            if s.flushed_sign != target.flushed_sign {
131                return refuse(match target.flushed_sign {
132                    FlushSign::Kept => format!("flushes a tiny result to +0 whatever its sign, which {who} keeps"),
133                    FlushSign::Positive => format!("flushes a tiny result with its sign, which {who} drops"),
134                });
135            }
136        }
137        (Denormals::Keep, Denormals::Flush) => {
138            if target.tininess != Tininess::AfterRoundingAtDenormals || target.flushed_sign != FlushSign::Kept {
139                return refuse(format!("keeps a denormal result, which flushed after it is no result {who} decides tiny"));
140            }
141        }
142        (Denormals::Keep, Denormals::Keep) => {}
143    }
144    if s.nan != target.nan && target.nan != Nan::Canonical(0x7fc0_0000) {
145        return refuse(format!("returns NaNs {who} does not, and the logic after it writes only 0x7fc00000"));
146    }
147    let (registering, depth) = if block.full <= latency {
148        (Registering::Full, block.full)
149    } else if block.least <= latency {
150        (Registering::Least, block.least)
151    } else {
152        return Err(HardError::TooDeep { op: op.name(), latency, least: block.least });
153    };
154    Ok(Plan {
155        registering,
156        pads: latency - depth,
157        flush_operands: s.denormal_inputs == Denormals::Keep && target.denormal_inputs == Denormals::Flush,
158        flush_results: s.denormal_results == Denormals::Keep && target.denormal_results == Denormals::Flush,
159        canonical_nan: s.nan != target.nan,
160    })
161}
162
163/// The wrapper `module` for the block's `op` at `latency` clocks, free-running.
164pub fn hard_verilog(op: Op, module: &str, block: &Block, latency: usize) -> Result<String, HardError> {
165    wrapper(op, module, block, latency, false, &Semantics::ENGINE)
166}
167
168/// The wrapper `module` for the block's `op` at `latency` steps, every register (the block's and the pads) stepping
169/// where `step_en`, as the engine's enabled cores step.
170pub fn hard_verilog_ce(op: Op, module: &str, block: &Block, latency: usize) -> Result<String, HardError> {
171    wrapper(op, module, block, latency, true, &Semantics::ENGINE)
172}
173
174/// [`hard_verilog`] computing `target` ([`plan_for`]): `hard_verilog` is this of [`Semantics::ENGINE`], byte for byte.
175pub fn hard_verilog_for(op: Op, module: &str, block: &Block, latency: usize, target: &Semantics) -> Result<String, HardError> {
176    wrapper(op, module, block, latency, false, target)
177}
178
179/// [`hard_verilog_ce`] computing `target` ([`plan_for`]): `hard_verilog_ce` is this of [`Semantics::ENGINE`], byte for
180/// byte.
181pub fn hard_verilog_ce_for(op: Op, module: &str, block: &Block, latency: usize, target: &Semantics) -> Result<String, HardError> {
182    wrapper(op, module, block, latency, true, target)
183}
184
185/// The NaN `target` returns, as a Verilog expression over the operands `a` and `b` (and the operand classes `na`, `nb`).
186fn target_nan(target: &Semantics) -> String {
187    let word = |v: u32| format!("32'h{:04x}_{:04x}", v >> 16, v & 0xffff);
188    match target.nan {
189        Nan::Canonical(v) => word(v),
190        Nan::XorSigned(v) => format!("{{a[31] ^ b[31], 31'h{:08x}}}", v & 0x7fff_ffff),
191        Nan::Propagate { invalid } => format!("(na ? {{a[31], 8'hff, 1'b1, a[21:0]}} : nb ? {{b[31], 8'hff, 1'b1, b[21:0]}} : {})", word(invalid)),
192    }
193}
194
195fn wrapper(op: Op, module: &str, block: &Block, latency: usize, ce: bool, target: &Semantics) -> Result<String, HardError> {
196    let p = plan_for(op, block, latency, target)?;
197    let (ports, enable, step) = if ce {
198        ("input clk, input step_en, input [31:0] a, input [31:0] b, output [31:0] y", "step_en", "always @(posedge clk) if (step_en) begin")
199    } else {
200        ("input clk, input [31:0] a, input [31:0] b, output [31:0] y", "1'b1", "always @(posedge clk) begin")
201    };
202    let (registered, depth) = match p.registering {
203        Registering::Full => ("registered as the IP registers it", block.full),
204        Registering::Least => ("with its output register alone", block.least),
205    };
206    let (what, is) = if *target == Semantics::ENGINE {
207        (format!("The engine's f32 {}", op.name()), format!("the engine's {}", op.name()))
208    } else {
209        (format!("The f32 {} of {}", op.name(), target.name()), format!("that {}", op.name()))
210    };
211    let mut doc = format!(
212        "// {what} on the card's floating-point DSP block, {latency} {}: y is {is} of the operands\n\
213         // {latency} {} before. The block {registered}, {depth} deep{}",
214        if ce { "steps" } else { "clocks" },
215        if ce { "steps" } else { "clocks" },
216        match p.pads {
217            0 => String::new(),
218            1 => ", then a register".to_string(),
219            n => format!(", then {n} registers"),
220        }
221    );
222    let fixes: Vec<&str> = [
223        (p.flush_operands, "a denormal operand read as a zero of its sign"),
224        (p.flush_results, "a denormal result written as a zero of its sign"),
225        (p.canonical_nan, "every NaN result 0x7fc00000"),
226    ]
227    .iter()
228    .filter(|(on, _)| *on)
229    .map(|(_, what)| *what)
230    .collect();
231    if fixes.is_empty() {
232        doc.push_str(".\n");
233    } else {
234        write!(doc, ".\n// Around it: {}.\n", fixes.join(", ")).unwrap();
235    }
236    let mut v = format!("{doc}module {module}({ports});\n");
237    // An add's denormal operand is flushed before the block. A multiply's is decided beside it, the block taking the
238    // operands as they come, so it multiplies what the engine multiplies: to the engine the operand is a zero of its
239    // sign, the product a signed zero, or NaN against an infinity or a NaN.
240    let beside = p.flush_operands && op == Op::Multiply;
241    let operand = |x: &str| if p.flush_operands && !beside { format!("(({x}[30:23] == 8'd0) ? {{{x}[31], 31'd0}} : {x})") } else { x.to_string() };
242    writeln!(v, "  wire [31:0] ia = {}, ib = {};", operand("a"), operand("b")).unwrap();
243    if beside {
244        v.push_str(
245            "  wire        da = (a[30:23] == 8'd0) && (a[22:0] != 23'd0), db = (b[30:23] == 8'd0) && (b[22:0] != 23'd0);\n\
246             \x20 wire        za = (a[30:23] == 8'd0), zb = (b[30:23] == 8'd0);\n\
247             \x20 wire        na = (a[30:23] == 8'hff) && (a[22:0] != 23'd0), nb = (b[30:23] == 8'hff) && (b[22:0] != 23'd0);\n\
248             \x20 wire        fa = (a[30:23] == 8'hff) && (a[22:0] == 23'd0), fb = (b[30:23] == 8'hff) && (b[22:0] == 23'd0);\n\
249             \x20 wire [32:0] side_c = {da || db, (na || nb || (fa && zb) || (fb && za)) ? NAN : {a[31] ^ b[31], 31'd0}};\n"
250                .replace("NAN", &target_nan(target))
251                .as_str(),
252        );
253        writeln!(
254            v,
255            "  // The decision as deep as the block, beside its result.\n  reg [32:0] side [0:{last}];\n  integer j;\n  {step}\n    side[0] <= side_c;\n    for (j = 1; j < {depth}; j = j + 1) side[j] <= side[j-1];\n  end",
256            last = depth - 1
257        )
258        .unwrap();
259    }
260    v.push_str("  wire [31:0] h;\n  wire [7:0] flags;\n");
261    let (used, operands) = match (op, p.registering) {
262        (Op::Multiply, Registering::Full) => (MULTIPLY_REGISTERS, ["32'd0", "ia", "ib"]),
263        (Op::Add, Registering::Full) => (ADD_REGISTERS, ["ia", "ib", "32'd0"]),
264        (Op::Multiply, Registering::Least) => (LEAST_REGISTERS, ["32'd0", "ia", "ib"]),
265        (Op::Add, Registering::Least) => (LEAST_REGISTERS, ["ia", "ib", "32'd0"]),
266    };
267    v.push_str(&fp_mac(op.mode(), "u", used, enable, operands, "h", "flags"));
268    v.push_str("  wire nan = (h[30:23] == 8'hff) && (h[22:0] != 23'd0);\n");
269    let fixed = match (p.canonical_nan, p.flush_results) {
270        (false, false) => "h".to_string(),
271        (true, false) => "nan ? 32'h7fc0_0000 : h".to_string(),
272        (false, true) => "(h[30:23] == 8'd0) ? {h[31], 31'd0} : h".to_string(),
273        (true, true) => "nan ? 32'h7fc0_0000 : (h[30:23] == 8'd0) ? {h[31], 31'd0} : h".to_string(),
274    };
275    if beside {
276        writeln!(v, "  wire [31:0] c = side[{}][32] ? side[{}][31:0] : {fixed};", depth - 1, depth - 1).unwrap();
277    } else {
278        writeln!(v, "  wire [31:0] c = {fixed};").unwrap();
279    }
280    if p.pads == 0 {
281        v.push_str("  assign y = c;\n");
282    } else {
283        writeln!(v, "  reg [31:0] pad [0:{}];\n  integer i;\n  {step}\n    pad[0] <= c;\n    for (i = 1; i < {}; i = i + 1) pad[i] <= pad[i-1];\n  end\n  assign y = pad[{}];", p.pads - 1, p.pads, p.pads - 1).unwrap();
284    }
285    v.push_str("endmodule\n");
286    Ok(v)
287}
288
289/// How the block's multiply-add rounds.
290#[derive(Clone, Copy, Debug, PartialEq, Eq)]
291pub enum Rounding {
292    /// Once, the exact `C + A·B` rounded: a fused multiply-add.
293    Once,
294    /// The product rounded, then the sum.
295    Twice,
296    /// Neither, on the oracle's multiply-add vectors.
297    Other,
298}
299
300/// What the card said of its block: each mode's semantics and depths, the multiply-add's latency and rounding.
301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302pub struct Measurement {
303    pub product: Block,
304    pub sum: Block,
305    pub multiply_add_latency: usize,
306    pub multiply_add: Rounding,
307}
308
309/// One mode's counts as the measurement found them: each class's steps differing from the block over every run, in the
310/// oracle's order, and the steps no class explains.
311#[derive(Clone, Debug, PartialEq, Eq)]
312pub struct Tally {
313    pub differing: Vec<u64>,
314    pub unexplained: u64,
315}
316
317/// Why a measurement names no block.
318#[derive(Clone, Debug, PartialEq, Eq)]
319pub enum MeasureError {
320    Json(String),
321    /// The measurement did not finish, or a field it writes is missing.
322    Incomplete(&'static str),
323    /// No class, or more than one, agreed with the block on every step of every run.
324    Class { op: &'static str, agreeing: usize, unexplained: u64 },
325}
326
327impl fmt::Display for MeasureError {
328    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
329        match self {
330            MeasureError::Json(e) => write!(f, "the measurement: {e}"),
331            MeasureError::Incomplete(what) => write!(f, "the measurement has no {what}"),
332            MeasureError::Class { op, agreeing: 0, unexplained } => {
333                write!(f, "the block's {op} is no member of the family: every class differs somewhere ({unexplained} steps no class explains)")
334            }
335            MeasureError::Class { op, agreeing, .. } => write!(f, "{agreeing} classes agree with the block's {op} everywhere, not one"),
336        }
337    }
338}
339
340impl Measurement {
341    /// The block the oracle's measurement (the host program's JSON, [`super::fp_oracle::HOST_PROGRAM`]) names for
342    /// `oracle`: each mode's one class with no difference over the vectors and both streams, its first member's
343    /// semantics, the latencies measured; and the multiply-add's rounding from its vectors whose C is the product
344    /// rounded and negated, read once rounded as the product's error and twice rounded as zero.
345    pub fn read(oracle: &Oracle, json: &str) -> Result<Measurement, MeasureError> {
346        let v = finished(json)?;
347        let product = block(&v, oracle, Op::Multiply)?;
348        let sum = block(&v, oracle, Op::Add)?;
349        let (multiply_add_latency, multiply_add) = multiply_add(&v)?;
350        Ok(Measurement { product, sum, multiply_add_latency, multiply_add })
351    }
352
353    /// The multiply-add alone: its depth and how it rounds, whatever either block named.
354    pub fn read_multiply_add(json: &str) -> Result<(usize, Rounding), MeasureError> {
355        multiply_add(&finished(json)?)
356    }
357
358    /// The multiply's block alone: its one agreeing class and its depths, whatever the add named.
359    pub fn read_product(oracle: &Oracle, json: &str) -> Result<Block, MeasureError> {
360        block(&finished(json)?, oracle, Op::Multiply)
361    }
362
363    /// The add's block alone: its one agreeing class and its depths, whatever the multiply named.
364    pub fn read_sum(oracle: &Oracle, json: &str) -> Result<Block, MeasureError> {
365        block(&finished(json)?, oracle, Op::Add)
366    }
367
368    /// One mode's counts alone, whatever its classes name.
369    pub fn read_tally(oracle: &Oracle, json: &str, op: Op) -> Result<Tally, MeasureError> {
370        tally(&finished(json)?, oracle, op)
371    }
372
373    /// One mode's depths alone, fully registered and with its output register alone.
374    pub fn read_depths(json: &str, op: Op) -> Result<(usize, usize), MeasureError> {
375        let v = finished(json)?;
376        let keys = Keys::of(op);
377        Ok((latency(&v, keys.full)?, latency(&v, keys.least)?))
378    }
379
380    /// The measurement's seed and the steps of its runs: the vectors, the random stream and the near stream.
381    pub fn read_runs(json: &str) -> Result<(u64, [u64; 3]), MeasureError> {
382        let v = finished(json)?;
383        let seed = v["seed"].as_u64().ok_or(MeasureError::Incomplete("seed"))?;
384        let mut steps = [0; 3];
385        for (n, run) in steps.iter_mut().zip(RUNS) {
386            *n = v["runs"][run]["steps"].as_u64().ok_or(MeasureError::Incomplete("steps"))?;
387        }
388        Ok((seed, steps))
389    }
390}
391
392/// A finished measurement's JSON.
393fn finished(json: &str) -> Result<serde_json::Value, MeasureError> {
394    let v: serde_json::Value = serde_json::from_str(json).map_err(|e| MeasureError::Json(e.to_string()))?;
395    if v["complete"] != true {
396        return Err(MeasureError::Incomplete("finished run"));
397    }
398    Ok(v)
399}
400
401/// The multiply-add's depth, and how it rounds from its records of the multiply-add vectors: once when every result is
402/// the fused one, twice when every result is the product rounded and then the sum (each vector of a quadruple but the
403/// last cancelling to zero), neither otherwise.
404fn multiply_add(v: &serde_json::Value) -> Result<(usize, Rounding), MeasureError> {
405    let records = v["runs"]["records"].as_array().ok_or(MeasureError::Incomplete("records"))?;
406    let word = |r: &serde_json::Value, i: usize| r[i].as_str().and_then(|w| u32::from_str_radix(w, 16).ok());
407    let mut once = true;
408    let mut twice = true;
409    for (k, &[a, b, c]) in fma_vectors().iter().enumerate() {
410        let r = records.iter().find(|r| word(r, 0) == Some(a) && word(r, 1) == Some(b) && word(r, 2) == Some(c)).ok_or(MeasureError::Incomplete("multiply-add records"))?;
411        let got = word(r, 5).ok_or(MeasureError::Incomplete("multiply-add result"))?;
412        let (x, y, z) = (f32::from_bits(a), f32::from_bits(b), f32::from_bits(c));
413        once &= got == ((f64::from(x) * f64::from(y) + f64::from(z)) as f32).to_bits();
414        twice &= got == (x * y + z).to_bits();
415        if k % 4 != 3 {
416            twice &= got == 0;
417        }
418    }
419    let rounding = match (once, twice) {
420        (true, false) => Rounding::Once,
421        (false, true) => Rounding::Twice,
422        _ => Rounding::Other,
423    };
424    Ok((latency(v, "multiply_add")?, rounding))
425}
426
427/// A depth the measurement found, by its key.
428fn latency(v: &serde_json::Value, key: &'static str) -> Result<usize, MeasureError> {
429    v["latency"][key].as_i64().filter(|l| *l > 0).map(|l| l as usize).ok_or(MeasureError::Incomplete(key))
430}
431
432/// The runs a measurement makes, in the order it makes them.
433const RUNS: [&str; 3] = ["vectors", "random", "near"];
434
435/// Where a mode's numbers sit in the measurement: its name, its counters' and unexplained steps' keys in each run, and
436/// its depths' keys.
437struct Keys {
438    name: &'static str,
439    counters: &'static str,
440    lone: &'static str,
441    full: &'static str,
442    least: &'static str,
443}
444
445impl Keys {
446    fn of(op: Op) -> Keys {
447        match op {
448            Op::Multiply => Keys { name: "multiply", counters: "products", lone: "lone_products", full: "product", least: "product_least" },
449            Op::Add => Keys { name: "add", counters: "sums", lone: "lone_sums", full: "sum", least: "sum_least" },
450        }
451    }
452}
453
454/// One mode's counts: each class's counters summed over the runs, one counter a class in every run, and the steps no
455/// class explains summed.
456fn tally(v: &serde_json::Value, oracle: &Oracle, op: Op) -> Result<Tally, MeasureError> {
457    let keys = Keys::of(op);
458    let classes = match op {
459        Op::Multiply => oracle.products.len(),
460        Op::Add => oracle.sums.len(),
461    };
462    let mut tally = Tally { differing: vec![0; classes], unexplained: 0 };
463    for run in RUNS {
464        let counters = v["runs"][run][keys.counters].as_array().filter(|c| c.len() == classes).ok_or(MeasureError::Incomplete("counters"))?;
465        for (sum, c) in tally.differing.iter_mut().zip(counters) {
466            *sum += c.as_u64().ok_or(MeasureError::Incomplete("counters"))?;
467        }
468        tally.unexplained += v["runs"][run][keys.lone].as_u64().ok_or(MeasureError::Incomplete("unexplained steps"))?;
469    }
470    Ok(tally)
471}
472
473/// One block as the measurement found it: the one class that differed on no step of any run (the steps no class
474/// explains when none or several did) and the block's depths, fully registered and least.
475fn block(v: &serde_json::Value, oracle: &Oracle, op: Op) -> Result<Block, MeasureError> {
476    let keys = Keys::of(op);
477    let classes = match op {
478        Op::Multiply => &oracle.products,
479        Op::Add => &oracle.sums,
480    };
481    let tally = tally(v, oracle, op)?;
482    let agreeing: Vec<usize> = (0..classes.len()).filter(|&k| tally.differing[k] == 0).collect();
483    let [one] = agreeing[..] else {
484        return Err(MeasureError::Class { op: keys.name, agreeing: agreeing.len(), unexplained: tally.unexplained });
485    };
486    Ok(Block { semantics: classes[one].semantics, full: latency(v, keys.full)?, least: latency(v, keys.least)? })
487}
488
489#[cfg(test)]
490#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/fp_hard/tests.rs"]
491mod tests;