Skip to main content

logicaffeine_compile/codegen_sva/
simd.rs

1//! simd — configurable-precision packed SIMD lanes for the fabric datapath (`FabricTarget::simd`).
2//!
3//! HiVec's TOPS/W comes from packing several narrow lanes into one datapath. Our analog packs `lanes` values
4//! of `width` bits into the existing 64-bit word and operates lane-wise. The one thing that must never happen
5//! is a carry bleeding across a lane boundary — the "silent SIMD integer miscompile". So the packed hardware
6//! op is defined here as an explicit SWAR (SIMD-within-a-register) form built from the fabric's own `AluOp`
7//! tiles, and `simd_tile_lock` proves it BIT-IDENTICAL to a lane-wise reference (N independent scalar ops) for
8//! ALL 64-bit inputs via our certified SAT. The reference is the spec; the SWAR form is what silicon runs.
9
10use super::fabric_rtl::{AluOp, FabricOp, Src};
11
12/// A packed lane type: `lanes` values of `width` bits, `lanes * width == 64`.
13#[derive(
14    Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, serde::Serialize, serde::Deserialize, Ord,
15)]
16pub enum LaneTy {
17    I8x8,
18    I16x4,
19    I32x2,
20}
21
22/// A packed integer SIMD operation carried by a `LoopCell::VectorArith` / `CellKind::Simd` tile. `Add` is the
23/// carry-isolated SWAR add (proven ≡ lane-wise by `simd_tile_lock`); the bitwise ops never cross a lane
24/// boundary, so they are the native 64-bit op.
25#[derive(
26    Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, serde::Serialize, serde::Deserialize, Ord,
27)]
28pub enum VecOp {
29    Add,
30    And,
31    Or,
32    Xor,
33}
34
35impl VecOp {
36    /// Evaluate the packed op lane-wise on two 64-bit operand words — the tile's semantics (= N scalar ops).
37    pub fn eval(self, ty: LaneTy, a: u64, b: u64) -> u64 {
38        match self {
39            VecOp::Add => eval_lanewise_add(ty, a, b),
40            VecOp::And => a & b,
41            VecOp::Or => a | b,
42            VecOp::Xor => a ^ b,
43        }
44    }
45
46    /// Combinational Verilog for the packed op over two 64-bit operand expressions. `Add` emits the SWAR form
47    /// (carry blocked at every lane boundary); the bitwise ops are native (inherently lane-isolated).
48    pub fn verilog(self, ty: LaneTy, a: &str, b: &str) -> String {
49        match self {
50            VecOp::Add => {
51                let m = swar_lo_mask(ty);
52                let hm = !m;
53                format!("((({a}) & 64'h{m:016x}) + (({b}) & 64'h{m:016x})) ^ ((({a}) ^ ({b})) & 64'h{hm:016x})")
54            }
55            VecOp::And => format!("(({a}) & ({b}))"),
56            VecOp::Or => format!("(({a}) | ({b}))"),
57            VecOp::Xor => format!("(({a}) ^ ({b}))"),
58        }
59    }
60}
61
62impl LaneTy {
63    pub fn lanes(self) -> usize {
64        match self {
65            LaneTy::I8x8 => 8,
66            LaneTy::I16x4 => 4,
67            LaneTy::I32x2 => 2,
68        }
69    }
70    pub fn width(self) -> usize {
71        64 / self.lanes()
72    }
73    pub const ALL: &'static [LaneTy] = &[LaneTy::I8x8, LaneTy::I16x4, LaneTy::I32x2];
74}
75
76/// The full low-`width` mask replicated across every lane (`0xFFFF…` per lane).
77fn lane_full_mask(ty: LaneTy) -> u64 {
78    let w = ty.width();
79    let lane = if w == 64 { u64::MAX } else { (1u64 << w) - 1 };
80    (0..ty.lanes()).fold(0u64, |m, j| m | (lane << (j * w)))
81}
82
83/// The SWAR carry-block mask: every lane's low `width-1` bits set (top bit of each lane cleared).
84fn swar_lo_mask(ty: LaneTy) -> u64 {
85    let w = ty.width();
86    let lane = (1u64 << (w - 1)) - 1;
87    (0..ty.lanes()).fold(0u64, |m, j| m | (lane << (j * w)))
88}
89
90/// The LANE-WISE reference for packed add — the SPEC: each lane independently computes `(a_j + b_j) mod 2^w`,
91/// expressed as an `AluOp` residual (shift the lane down, add, mask to `width`, shift back, OR the lanes). This
92/// is "N independent scalar adds" made concrete.
93pub fn lanewise_add_body(ty: LaneTy) -> (Vec<FabricOp>, usize) {
94    let (w, lanes) = (ty.width(), ty.lanes());
95    let full = lane_full_mask(ty) & ((1u64 << w) - 1); // low-lane full mask (single lane worth)
96    let mut cells: Vec<FabricOp> = Vec::new();
97    let mut lane_vals: Vec<Src> = Vec::new();
98    for j in 0..lanes {
99        let sh = (j * w) as i64;
100        let (aj, bj) = if j == 0 {
101            (Src::In(0), Src::In(1))
102        } else {
103            cells.push(FabricOp {
104                op: AluOp::UShr,
105                a: Src::In(0),
106                b: Src::Const(sh),
107            });
108            let a = Src::Val(cells.len() - 1);
109            cells.push(FabricOp {
110                op: AluOp::UShr,
111                a: Src::In(1),
112                b: Src::Const(sh),
113            });
114            let b = Src::Val(cells.len() - 1);
115            (a, b)
116        };
117        cells.push(FabricOp {
118            op: AluOp::Add,
119            a: aj,
120            b: bj,
121        });
122        let sum = Src::Val(cells.len() - 1);
123        cells.push(FabricOp {
124            op: AluOp::And,
125            a: sum,
126            b: Src::Const(full as i64),
127        });
128        let masked = Src::Val(cells.len() - 1);
129        let laneval = if j == 0 {
130            masked
131        } else {
132            cells.push(FabricOp {
133                op: AluOp::Shl,
134                a: masked,
135                b: Src::Const(sh),
136            });
137            Src::Val(cells.len() - 1)
138        };
139        lane_vals.push(laneval);
140    }
141    // OR the lanes together.
142    let mut acc = lane_vals[0];
143    for &lv in &lane_vals[1..] {
144        cells.push(FabricOp {
145            op: AluOp::Or,
146            a: acc,
147            b: lv,
148        });
149        acc = Src::Val(cells.len() - 1);
150    }
151    let result = match acc {
152        Src::Val(k) => k,
153        _ => cells.len() - 1,
154    };
155    (cells, result)
156}
157
158/// The PACKED SWAR add — one datapath op that blocks carry at every lane boundary:
159/// `((a & M) + (b & M)) ^ ((a ^ b) & ~M)`, where `M` clears each lane's top bit. This is what the SIMD tile
160/// emits; `simd_tile_lock` certifies it equals [`lanewise_add_body`].
161pub fn swar_add_body(ty: LaneTy) -> (Vec<FabricOp>, usize) {
162    let m = swar_lo_mask(ty) as i64;
163    let hm = !swar_lo_mask(ty) as i64;
164    let cells = vec![
165        FabricOp {
166            op: AluOp::And,
167            a: Src::In(0),
168            b: Src::Const(m),
169        }, // v0 = a & M
170        FabricOp {
171            op: AluOp::And,
172            a: Src::In(1),
173            b: Src::Const(m),
174        }, // v1 = b & M
175        FabricOp {
176            op: AluOp::Add,
177            a: Src::Val(0),
178            b: Src::Val(1),
179        }, // v2 = (a&M)+(b&M)
180        FabricOp {
181            op: AluOp::Xor,
182            a: Src::In(0),
183            b: Src::In(1),
184        }, // v3 = a ^ b
185        FabricOp {
186            op: AluOp::And,
187            a: Src::Val(3),
188            b: Src::Const(hm),
189        }, // v4 = (a^b) & ~M
190        FabricOp {
191            op: AluOp::Xor,
192            a: Src::Val(2),
193            b: Src::Val(4),
194        }, // v5 = result
195    ];
196    (cells, 5)
197}
198
199/// A deliberately NAIVE (non-isolated) packed add — a single 64-bit add. Carry bleeds across lane boundaries,
200/// so it must NOT equal the lane-wise spec (the non-vacuity witness for the isolation proof).
201pub fn naive_add_body() -> (Vec<FabricOp>, usize) {
202    (
203        vec![FabricOp {
204            op: AluOp::Add,
205            a: Src::In(0),
206            b: Src::In(1),
207        }],
208        0,
209    )
210}
211
212/// Packed-float add for **F32×2** (two IEEE-754 `f32` lanes in one 64-bit word, lane 0 low) — the SPEC: each
213/// lane an independent scalar `f32` add. A NaN/Inf/subnormal in one lane cannot perturb the other; SAT cannot
214/// bitblast IEEE arithmetic, so `simd_tile_lock` certifies this by per-lane bit-exactness + a cross-lane
215/// non-contamination battery (the way the scalar float lane is already proven). This is the model the parallel
216/// `fp32` FPU lanes realize in silicon.
217pub fn eval_lanewise_fadd_f32x2(a: u64, b: u64) -> u64 {
218    let lo = f32::from_bits(a as u32) + f32::from_bits(b as u32);
219    let hi = f32::from_bits((a >> 32) as u32) + f32::from_bits((b >> 32) as u32);
220    ((hi.to_bits() as u64) << 32) | lo.to_bits() as u64
221}
222
223/// Packed-float multiply for **F32×2** (see [`eval_lanewise_fadd_f32x2`]).
224pub fn eval_lanewise_fmul_f32x2(a: u64, b: u64) -> u64 {
225    let lo = f32::from_bits(a as u32) * f32::from_bits(b as u32);
226    let hi = f32::from_bits((a >> 32) as u32) * f32::from_bits((b >> 32) as u32);
227    ((hi.to_bits() as u64) << 32) | lo.to_bits() as u64
228}
229
230/// Pack two `f32` lanes into one 64-bit word (lane 0 low).
231pub fn pack_f32x2(lane0: f32, lane1: f32) -> u64 {
232    ((lane1.to_bits() as u64) << 32) | lane0.to_bits() as u64
233}
234
235/// The synthesizable combinational IEEE-754 `f32` adder — a faithful port of the PROVEN f64 `fp_addsub`
236/// (`benchmarks/silicon/fpu.v`): align both significands in a wide 128-bit window (64 bits of alignment room,
237/// so every bit survives to sticky), add/sub, leading-zero-count, normalize the leading 1 to bit 26 (24-bit
238/// significand at `[26:3]`, guard/round/sticky at `[2:0]`), round-to-nearest-ties-even, and pack (overflow→Inf,
239/// underflow→subnormal via `pack_sub32`). `sub=1` computes `a-b`. Proven bit-exact to Rust `f32` under iverilog
240/// by `simd_float_rtl_lock`. This module is instantiated once per lane by the packed F32×2 tile.
241pub const FP32_ADD_V: &str = r#"module fp32_add(input [31:0] a, input [31:0] b_in, input sub, output reg [31:0] y);
242  localparam [31:0] QNAN = 32'h7FC00000;
243  reg [31:0] b;
244  reg sa, sb;
245  reg [7:0] ea, eb;
246  reg [22:0] ma, mb;
247  reg azero,bzero,ainf,binf,anan,bnan,asub,bsub;
248  reg [8:0] xa, xb;
249  reg ha, hb;
250  reg [23:0] siga, sigb;
251  reg [11:0] exp_l, shamt, lead;
252  reg [23:0] sig_big, sig_small;
253  reg sgn_big, sgn_small, res_sign, op_is_sub;
254  reg [127:0] big_ext, small_ext, sum_ext, norm_sig;
255  integer i;
256  reg signed [11:0] norm_exp;
257  reg [24:0] rounded;
258  reg guard, roundb, sticky, lsb, round_up;
259  reg [23:0] final_sig;
260
261  function [31:0] pack_sub32(input sgn, input [127:0] wsum, input [11:0] wlead, input signed [11:0] bexp);
262    reg [11:0] rshift; reg [127:0] win; reg g2,r2,s2,l2,up2; reg [24:0] rr;
263    begin
264      rshift = 12'd1 - bexp;
265      if (wlead >= 12'd26) begin
266        win = wsum >> (wlead - 12'd26);
267        if (|(wsum & ((128'd1 << (wlead - 12'd26)) - 128'd1))) win[0] = 1'b1;
268      end else begin
269        win = wsum << (12'd26 - wlead);
270      end
271      if (rshift >= 12'd128) begin s2 = |win; win = 128'd0; win[0] = s2; end
272      else if (rshift != 12'd0) begin
273        if (|(win & ((128'd1 << rshift) - 128'd1))) begin win = win >> rshift; win[0] = 1'b1; end
274        else win = win >> rshift;
275      end
276      g2 = win[2]; r2 = win[1]; s2 = win[0]; l2 = win[3];
277      up2 = g2 & (r2 | s2 | l2);
278      rr = {1'b0, win[26:3]} + (up2 ? 25'd1 : 25'd0);
279      if (rr[23]) pack_sub32 = {sgn, 8'd1, rr[22:0]};
280      else        pack_sub32 = {sgn, 8'd0, rr[22:0]};
281    end
282  endfunction
283
284  always @(*) begin
285    b  = sub ? {~b_in[31], b_in[30:0]} : b_in;
286    sa = a[31]; sb = b[31];
287    ea = a[30:23]; eb = b[30:23];
288    ma = a[22:0];  mb = b[22:0];
289    azero = (ea==8'd0)  && (ma==23'd0); bzero = (eb==8'd0)  && (mb==23'd0);
290    ainf  = (ea==8'hFF) && (ma==23'd0); binf  = (eb==8'hFF) && (mb==23'd0);
291    anan  = (ea==8'hFF) && (ma!=23'd0); bnan  = (eb==8'hFF) && (mb!=23'd0);
292    asub  = (ea==8'd0)  && (ma!=23'd0); bsub  = (eb==8'd0)  && (mb!=23'd0);
293    xa = asub ? 9'd1 : {1'b0, ea};
294    xb = bsub ? 9'd1 : {1'b0, eb};
295    ha = (ea!=8'd0); hb = (eb!=8'd0);
296    siga = {ha, ma}; sigb = {hb, mb};
297    y = 32'd0;
298    // pre-set datapath regs (avoid latches in the combinational always)
299    exp_l=12'd0; shamt=12'd0; sig_big=24'd0; sig_small=24'd0; sgn_big=1'b0; sgn_small=1'b0;
300    op_is_sub=1'b0; res_sign=1'b0; big_ext=128'd0; small_ext=128'd0; sum_ext=128'd0; lead=12'd0;
301    norm_exp=12'sd0; norm_sig=128'd0; rounded=25'd0; final_sig=24'd0;
302    guard=1'b0; roundb=1'b0; sticky=1'b0; lsb=1'b0; round_up=1'b0;
303
304    if (anan || bnan) y = QNAN;
305    else if (ainf && binf) y = (sa==sb) ? {sa,8'hFF,23'd0} : QNAN;
306    else if (ainf) y = {sa,8'hFF,23'd0};
307    else if (binf) y = {sb,8'hFF,23'd0};
308    else if (azero && bzero) y = (sa==sb) ? {sa,31'd0} : 32'd0;
309    else if (azero) y = b;
310    else if (bzero) y = a;
311    else begin
312      if (xa > xb || (xa == xb && siga >= sigb)) begin
313        exp_l={3'd0,xa}; sig_big=siga; sgn_big=sa; sig_small=sigb; sgn_small=sb; shamt={3'd0,(xa-xb)};
314      end else begin
315        exp_l={3'd0,xb}; sig_big=sigb; sgn_big=sb; sig_small=siga; sgn_small=sa; shamt={3'd0,(xb-xa)};
316      end
317      op_is_sub = (sgn_big != sgn_small);
318      res_sign  = sgn_big;
319      big_ext = {40'd0, sig_big, 64'd0};
320      if (shamt >= 12'd128) begin small_ext = 128'd0; small_ext[0] = |sig_small; end
321      else begin
322        small_ext = ({40'd0, sig_small, 64'd0}) >> shamt;
323        if (shamt != 12'd0 && |(({40'd0, sig_small, 64'd0}) & ((128'd1 << shamt) - 128'd1))) small_ext[0] = 1'b1;
324      end
325      if (op_is_sub) sum_ext = big_ext - small_ext; else sum_ext = big_ext + small_ext;
326      if (sum_ext == 128'd0) y = 32'd0;
327      else begin
328        lead = 12'd0;
329        for (i = 127; i >= 0; i = i - 1) if (sum_ext[i] && lead == 12'd0) lead = i[11:0];
330        norm_exp = $signed({3'd0, exp_l[8:0]}) + $signed({3'd0, lead[8:0]}) - 12'sd87;
331        if (lead >= 12'd26) begin
332          norm_sig = sum_ext >> (lead - 12'd26);
333          if (|(sum_ext & ((128'd1 << (lead - 12'd26)) - 128'd1))) norm_sig[0] = 1'b1;
334        end else norm_sig = sum_ext << (12'd26 - lead);
335        guard=norm_sig[2]; roundb=norm_sig[1]; sticky=norm_sig[0]; lsb=norm_sig[3];
336        round_up = guard & (roundb | sticky | lsb);
337        rounded = {1'b0, norm_sig[26:3]} + (round_up ? 25'd1 : 25'd0);
338        if (rounded[24]) begin final_sig = rounded[24:1]; norm_exp = norm_exp + 12'sd1; end
339        else final_sig = rounded[23:0];
340        if (norm_exp >= 12'sd255) y = {res_sign, 8'hFF, 23'd0};
341        else if (norm_exp <= 12'sd0) y = pack_sub32(res_sign, sum_ext, lead, norm_exp);
342        else y = {res_sign, norm_exp[7:0], final_sig[22:0]};
343      end
344    end
345  end
346endmodule
347"#;
348
349/// The synthesizable combinational IEEE-754 `f32` multiplier — a faithful port of the proven f64 `fp_mul`:
350/// pre-normalize each significand (leading 1 to bit 23), multiply (24×24=48-bit, leading at 46/47), normalize
351/// the leading 1 to bit 47, round-to-nearest-ties-even, and pack (overflow→Inf, underflow→subnormal, rounded
352/// once from the EXACT product to avoid double-rounding). Proven bit-exact to Rust `f32` under iverilog.
353pub const FP32_MUL_V: &str = r#"module fp32_mul(input [31:0] a, input [31:0] b, output reg [31:0] y);
354  localparam [31:0] QNAN = 32'h7FC00000;
355  reg sa, sb, rs;
356  reg [7:0] ea, eb;
357  reg [22:0] ma, mb;
358  reg azero,bzero,ainf,binf,anan,bnan,ha,hb;
359  reg [23:0] siga, sigb, sa_sh, sb_sh;
360  reg [4:0] lza, lzb;
361  reg [47:0] prod, norm;
362  reg signed [11:0] exp_sum, e, fe, bexp;
363  reg [26:0] mant;
364  reg g, r, s, l, up;
365  reg [24:0] rnd, rr;
366  reg [23:0] fsig;
367  reg [11:0] rsh;
368  reg [63:0] wide;
369  reg g2, r2, s2, l2, up2;
370
371  function [4:0] clz24(input [23:0] v);
372    integer j; reg dn;
373    begin clz24 = 5'd24; dn = 1'b0;
374      for (j = 23; j >= 0; j = j - 1) if (v[j] && !dn) begin clz24 = 5'd23 - j[4:0]; dn = 1'b1; end
375    end
376  endfunction
377
378  always @(*) begin
379    sa = a[31]; sb = b[31]; rs = sa ^ sb;
380    ea = a[30:23]; eb = b[30:23];
381    ma = a[22:0];  mb = b[22:0];
382    azero=(ea==8'd0)&&(ma==23'd0); bzero=(eb==8'd0)&&(mb==23'd0);
383    ainf =(ea==8'hFF)&&(ma==23'd0); binf =(eb==8'hFF)&&(mb==23'd0);
384    anan =(ea==8'hFF)&&(ma!=23'd0); bnan =(eb==8'hFF)&&(mb!=23'd0);
385    ha=(ea!=8'd0); hb=(eb!=8'd0);
386    siga={ha,ma}; sigb={hb,mb};
387    y=32'd0;
388    lza=5'd0; lzb=5'd0; sa_sh=24'd0; sb_sh=24'd0; prod=48'd0; norm=48'd0;
389    exp_sum=12'sd0; e=12'sd0; fe=12'sd0; bexp=12'sd0; mant=27'd0; rnd=25'd0; rr=25'd0;
390    fsig=24'd0; rsh=12'd0; wide=64'd0; g=0;r=0;s=0;l=0;up=0;g2=0;r2=0;s2=0;l2=0;up2=0;
391
392    if (anan || bnan) y = QNAN;
393    else if ((ainf && bzero) || (binf && azero)) y = QNAN;
394    else if (ainf || binf) y = {rs, 8'hFF, 23'd0};
395    else if (azero || bzero) y = {rs, 31'd0};
396    else begin
397      lza = clz24(siga); lzb = clz24(sigb);
398      exp_sum = (ea!=8'd0 ? $signed({4'd0,ea})-12'sd127 : -12'sd126)
399              + (eb!=8'd0 ? $signed({4'd0,eb})-12'sd127 : -12'sd126)
400              - $signed({7'd0,lza}) - $signed({7'd0,lzb});
401      sa_sh = (siga << lza); sb_sh = (sigb << lzb);
402      prod = sa_sh * sb_sh;
403      if (prod[47]) begin norm = prod; e = exp_sum + 12'sd1; end
404      else          begin norm = prod << 1; e = exp_sum; end
405
406      if (e < -12'sd126) begin
407        wide = ({16'd0, norm}) >> 12'd21;
408        if (|(({16'd0, norm}) & ((64'd1 << 12'd21) - 64'd1))) wide[0] = 1'b1;
409        rsh = (-12'sd126) - e;
410        if (rsh >= 12'd64) begin s2 = |wide; wide = 64'd0; wide[0] = s2; end
411        else if (rsh != 12'd0) begin
412          if (|(wide & ((64'd1 << rsh) - 64'd1))) begin wide = wide >> rsh; wide[0] = 1'b1; end
413          else wide = wide >> rsh;
414        end
415        g2=wide[2]; r2=wide[1]; s2=wide[0]; l2=wide[3];
416        up2 = g2 & (r2 | s2 | l2);
417        rr = {1'b0, wide[26:3]} + (up2 ? 25'd1 : 25'd0);
418        if (rr[23]) y = {rs, 8'd1, rr[22:0]};
419        else        y = {rs, 8'd0, rr[22:0]};
420      end else begin
421        mant = {norm[47:24], norm[23], norm[22], (|norm[21:0])};
422        g=mant[2]; r=mant[1]; s=mant[0]; l=mant[3];
423        up = g & (r | s | l);
424        rnd = {1'b0, mant[26:3]} + (up ? 25'd1 : 25'd0);
425        if (rnd[24]) begin fsig = rnd[24:1]; fe = e + 12'sd1; end
426        else         begin fsig = rnd[23:0]; fe = e; end
427        if (fe > 12'sd127) y = {rs, 8'hFF, 23'd0};
428        else if (fe < -12'sd126) y = {rs, 8'd1, fsig[22:0]};
429        else begin bexp = fe + 12'sd127; y = {rs, bexp[7:0], fsig[22:0]}; end
430      end
431    end
432  end
433endmodule
434"#;
435
436/// A self-checking combinational testbench for the scalar `fp32_mul`: drive `n` `(a,b)` vectors, compare `y`
437/// against Rust's `f32` multiply oracle in `fp32_mul_o.hex`.
438pub fn emit_fp32_scalar_mul_testbench(n: usize) -> String {
439    format!(
440        r#"`timescale 1ns/1ps
441module tb_fp32_mul;
442  reg [31:0] a, b; wire [31:0] y;
443  reg [31:0] ia [0:{last}]; reg [31:0] ib [0:{last}]; reg [31:0] oo [0:{last}];
444  integer i, errors = 0;
445  fp32_mul dut(.a(a), .b(b), .y(y));
446  initial begin
447    $readmemh("fp32_mul_a.hex", ia);
448    $readmemh("fp32_mul_b.hex", ib);
449    $readmemh("fp32_mul_o.hex", oo);
450    for (i = 0; i < {n}; i = i + 1) begin
451      a = ia[i]; b = ib[i]; #1;
452      if (y !== oo[i]) begin errors = errors + 1; if (errors <= 20) $display("FAIL %0d: a=%h b=%h got=%h exp=%h", i, a, b, y, oo[i]); end
453    end
454    if (errors == 0) $display("PASS: {n} fp32 muls bit-exact to Rust f32");
455    else $display("FAIL: %0d fp32 mul mismatches", errors);
456    $finish;
457  end
458endmodule
459"#,
460        last = n - 1,
461    )
462}
463
464/// The synthesizable combinational IEEE-754 `f16` (binary16) adder — the f32 adder narrowed to 5-bit exponent,
465/// 10-bit mantissa, bias 15 (window 128-bit, LSB0=64, leading-ref=74, normalize-target=13). Proven bit-exact
466/// to the [`f16_add`] reference under iverilog; instantiated once per lane by the packed F16×4 tile.
467pub const FP16_ADD_V: &str = r#"module fp16_add(input [15:0] a, input [15:0] b_in, input sub, output reg [15:0] y);
468  localparam [15:0] QNAN = 16'h7E00;
469  reg [15:0] b;
470  reg sa, sb;
471  reg [4:0] ea, eb;
472  reg [9:0] ma, mb;
473  reg azero,bzero,ainf,binf,anan,bnan,asub,bsub;
474  reg [5:0] xa, xb;
475  reg ha, hb;
476  reg [10:0] siga, sigb, sig_big, sig_small, final_sig;
477  reg [11:0] exp_l, shamt, lead;
478  reg sgn_big, sgn_small, res_sign, op_is_sub;
479  reg [127:0] big_ext, small_ext, sum_ext, norm_sig;
480  integer i;
481  reg signed [11:0] norm_exp;
482  reg [11:0] rounded;
483  reg guard, roundb, sticky, lsb, round_up;
484
485  function [15:0] pack_sub16(input sgn, input [127:0] wsum, input [11:0] wlead, input signed [11:0] bexp);
486    reg [11:0] rshift; reg [127:0] win; reg g2,r2,s2,l2,up2; reg [11:0] rr;
487    begin
488      rshift = 12'd1 - bexp;
489      if (wlead >= 12'd13) begin
490        win = wsum >> (wlead - 12'd13);
491        if (|(wsum & ((128'd1 << (wlead - 12'd13)) - 128'd1))) win[0] = 1'b1;
492      end else begin
493        win = wsum << (12'd13 - wlead);
494      end
495      if (rshift >= 12'd128) begin s2 = |win; win = 128'd0; win[0] = s2; end
496      else if (rshift != 12'd0) begin
497        if (|(win & ((128'd1 << rshift) - 128'd1))) begin win = win >> rshift; win[0] = 1'b1; end
498        else win = win >> rshift;
499      end
500      g2 = win[2]; r2 = win[1]; s2 = win[0]; l2 = win[3];
501      up2 = g2 & (r2 | s2 | l2);
502      rr = {1'b0, win[13:3]} + (up2 ? 12'd1 : 12'd0);
503      if (rr[10]) pack_sub16 = {sgn, 5'd1, rr[9:0]};
504      else        pack_sub16 = {sgn, 5'd0, rr[9:0]};
505    end
506  endfunction
507
508  always @(*) begin
509    b  = sub ? {~b_in[15], b_in[14:0]} : b_in;
510    sa = a[15]; sb = b[15];
511    ea = a[14:10]; eb = b[14:10];
512    ma = a[9:0];   mb = b[9:0];
513    azero = (ea==5'd0)  && (ma==10'd0); bzero = (eb==5'd0)  && (mb==10'd0);
514    ainf  = (ea==5'h1F) && (ma==10'd0); binf  = (eb==5'h1F) && (mb==10'd0);
515    anan  = (ea==5'h1F) && (ma!=10'd0); bnan  = (eb==5'h1F) && (mb!=10'd0);
516    asub  = (ea==5'd0)  && (ma!=10'd0); bsub  = (eb==5'd0)  && (mb!=10'd0);
517    xa = asub ? 6'd1 : {1'b0, ea};
518    xb = bsub ? 6'd1 : {1'b0, eb};
519    ha = (ea!=5'd0); hb = (eb!=5'd0);
520    siga = {ha, ma}; sigb = {hb, mb};
521    y = 16'd0;
522    exp_l=12'd0; shamt=12'd0; sig_big=11'd0; sig_small=11'd0; sgn_big=1'b0; sgn_small=1'b0;
523    op_is_sub=1'b0; res_sign=1'b0; big_ext=128'd0; small_ext=128'd0; sum_ext=128'd0; lead=12'd0;
524    norm_exp=12'sd0; norm_sig=128'd0; rounded=12'd0; final_sig=11'd0;
525    guard=1'b0; roundb=1'b0; sticky=1'b0; lsb=1'b0; round_up=1'b0;
526
527    if (anan || bnan) y = QNAN;
528    else if (ainf && binf) y = (sa==sb) ? {sa,5'h1F,10'd0} : QNAN;
529    else if (ainf) y = {sa,5'h1F,10'd0};
530    else if (binf) y = {sb,5'h1F,10'd0};
531    else if (azero && bzero) y = (sa==sb) ? {sa,15'd0} : 16'd0;
532    else if (azero) y = b;
533    else if (bzero) y = a;
534    else begin
535      if (xa > xb || (xa == xb && siga >= sigb)) begin
536        exp_l={6'd0,xa}; sig_big=siga; sgn_big=sa; sig_small=sigb; sgn_small=sb; shamt={6'd0,(xa-xb)};
537      end else begin
538        exp_l={6'd0,xb}; sig_big=sigb; sgn_big=sb; sig_small=siga; sgn_small=sa; shamt={6'd0,(xb-xa)};
539      end
540      op_is_sub = (sgn_big != sgn_small);
541      res_sign  = sgn_big;
542      big_ext = {53'd0, sig_big, 64'd0};
543      if (shamt >= 12'd128) begin small_ext = 128'd0; small_ext[0] = |sig_small; end
544      else begin
545        small_ext = ({53'd0, sig_small, 64'd0}) >> shamt;
546        if (shamt != 12'd0 && |(({53'd0, sig_small, 64'd0}) & ((128'd1 << shamt) - 128'd1))) small_ext[0] = 1'b1;
547      end
548      if (op_is_sub) sum_ext = big_ext - small_ext; else sum_ext = big_ext + small_ext;
549      if (sum_ext == 128'd0) y = 16'd0;
550      else begin
551        lead = 12'd0;
552        for (i = 127; i >= 0; i = i - 1) if (sum_ext[i] && lead == 12'd0) lead = i[11:0];
553        norm_exp = $signed({6'd0, exp_l[5:0]}) + $signed({3'd0, lead[8:0]}) - 12'sd74;
554        if (lead >= 12'd13) begin
555          norm_sig = sum_ext >> (lead - 12'd13);
556          if (|(sum_ext & ((128'd1 << (lead - 12'd13)) - 128'd1))) norm_sig[0] = 1'b1;
557        end else norm_sig = sum_ext << (12'd13 - lead);
558        guard=norm_sig[2]; roundb=norm_sig[1]; sticky=norm_sig[0]; lsb=norm_sig[3];
559        round_up = guard & (roundb | sticky | lsb);
560        rounded = {1'b0, norm_sig[13:3]} + (round_up ? 12'd1 : 12'd0);
561        if (rounded[11]) begin final_sig = rounded[11:1]; norm_exp = norm_exp + 12'sd1; end
562        else final_sig = rounded[10:0];
563        if (norm_exp >= 12'sd31) y = {res_sign, 5'h1F, 10'd0};
564        else if (norm_exp <= 12'sd0) y = pack_sub16(res_sign, sum_ext, lead, norm_exp);
565        else y = {res_sign, norm_exp[4:0], final_sig[9:0]};
566      end
567    end
568  end
569endmodule
570"#;
571
572/// The synthesizable combinational IEEE-754 `f16` multiplier — the f32 multiplier narrowed to binary16
573/// (11-bit significand product = 22 bits, leading at 20/21). Proven bit-exact to [`f16_mul`] under iverilog.
574pub const FP16_MUL_V: &str = r#"module fp16_mul(input [15:0] a, input [15:0] b, output reg [15:0] y);
575  localparam [15:0] QNAN = 16'h7E00;
576  reg sa, sb, rs;
577  reg [4:0] ea, eb;
578  reg [9:0] ma, mb;
579  reg azero,bzero,ainf,binf,anan,bnan,ha,hb;
580  reg [10:0] siga, sigb, sa_sh, sb_sh, fsig;
581  reg [4:0] lza, lzb;
582  reg [21:0] prod, norm;
583  reg signed [11:0] exp_sum, e, fe, bexp;
584  reg [13:0] mant;
585  reg g, r, s, l, up;
586  reg [11:0] rnd, rr, rsh;
587  reg [63:0] wide;
588  reg g2, r2, s2, l2, up2;
589
590  function [4:0] clz11(input [10:0] v);
591    integer j; reg dn;
592    begin clz11 = 5'd11; dn = 1'b0;
593      for (j = 10; j >= 0; j = j - 1) if (v[j] && !dn) begin clz11 = 5'd10 - j[4:0]; dn = 1'b1; end
594    end
595  endfunction
596
597  always @(*) begin
598    sa = a[15]; sb = b[15]; rs = sa ^ sb;
599    ea = a[14:10]; eb = b[14:10];
600    ma = a[9:0];   mb = b[9:0];
601    azero=(ea==5'd0)&&(ma==10'd0); bzero=(eb==5'd0)&&(mb==10'd0);
602    ainf =(ea==5'h1F)&&(ma==10'd0); binf =(eb==5'h1F)&&(mb==10'd0);
603    anan =(ea==5'h1F)&&(ma!=10'd0); bnan =(eb==5'h1F)&&(mb!=10'd0);
604    ha=(ea!=5'd0); hb=(eb!=5'd0);
605    siga={ha,ma}; sigb={hb,mb};
606    y=16'd0;
607    lza=5'd0; lzb=5'd0; sa_sh=11'd0; sb_sh=11'd0; prod=22'd0; norm=22'd0;
608    exp_sum=12'sd0; e=12'sd0; fe=12'sd0; bexp=12'sd0; mant=14'd0; rnd=12'd0; rr=12'd0;
609    fsig=11'd0; rsh=12'd0; wide=64'd0; g=0;r=0;s=0;l=0;up=0;g2=0;r2=0;s2=0;l2=0;up2=0;
610
611    if (anan || bnan) y = QNAN;
612    else if ((ainf && bzero) || (binf && azero)) y = QNAN;
613    else if (ainf || binf) y = {rs, 5'h1F, 10'd0};
614    else if (azero || bzero) y = {rs, 15'd0};
615    else begin
616      lza = clz11(siga); lzb = clz11(sigb);
617      exp_sum = (ea!=5'd0 ? $signed({7'd0,ea})-12'sd15 : -12'sd14)
618              + (eb!=5'd0 ? $signed({7'd0,eb})-12'sd15 : -12'sd14)
619              - $signed({7'd0,lza}) - $signed({7'd0,lzb});
620      sa_sh = (siga << lza); sb_sh = (sigb << lzb);
621      prod = sa_sh * sb_sh;
622      if (prod[21]) begin norm = prod; e = exp_sum + 12'sd1; end
623      else          begin norm = prod << 1; e = exp_sum; end
624
625      if (e < -12'sd14) begin
626        wide = ({42'd0, norm}) >> 12'd8;
627        if (|(({42'd0, norm}) & ((64'd1 << 12'd8) - 64'd1))) wide[0] = 1'b1;
628        rsh = (-12'sd14) - e;
629        if (rsh >= 12'd64) begin s2 = |wide; wide = 64'd0; wide[0] = s2; end
630        else if (rsh != 12'd0) begin
631          if (|(wide & ((64'd1 << rsh) - 64'd1))) begin wide = wide >> rsh; wide[0] = 1'b1; end
632          else wide = wide >> rsh;
633        end
634        g2=wide[2]; r2=wide[1]; s2=wide[0]; l2=wide[3];
635        up2 = g2 & (r2 | s2 | l2);
636        rr = {1'b0, wide[13:3]} + (up2 ? 12'd1 : 12'd0);
637        if (rr[10]) y = {rs, 5'd1, rr[9:0]};
638        else        y = {rs, 5'd0, rr[9:0]};
639      end else begin
640        mant = {norm[21:11], norm[10], norm[9], (|norm[8:0])};
641        g=mant[2]; r=mant[1]; s=mant[0]; l=mant[3];
642        up = g & (r | s | l);
643        rnd = {1'b0, mant[13:3]} + (up ? 12'd1 : 12'd0);
644        if (rnd[11]) begin fsig = rnd[11:1]; fe = e + 12'sd1; end
645        else         begin fsig = rnd[10:0]; fe = e; end
646        if (fe > 12'sd15) y = {rs, 5'h1F, 10'd0};
647        else if (fe < -12'sd14) y = {rs, 5'd1, fsig[9:0]};
648        else begin bexp = fe + 12'sd15; y = {rs, bexp[4:0], fsig[9:0]}; end
649      end
650    end
651  end
652endmodule
653"#;
654
655/// Emit the packed **F16×4 add** tile: four `fp16_add` lanes over the 64-bit operands. Module `simd_fadd_f16x4`
656/// (ports `a,b,r`). Compile alongside [`FP16_ADD_V`]; proven ≡ [`eval_lanewise_fadd_f16x4`] under iverilog.
657pub fn emit_simd_fadd_f16x4_verilog() -> String {
658    let mut m =
659        String::from("module simd_fadd_f16x4(input [63:0] a, input [63:0] b, output [63:0] r);\n");
660    for k in 0..4 {
661        let (lo, hi) = (k * 16, k * 16 + 15);
662        m.push_str(&format!(
663            "  wire [15:0] y{k};\n  fp16_add u{k}(.a(a[{hi}:{lo}]), .b_in(b[{hi}:{lo}]), .sub(1'b0), .y(y{k}));\n"
664        ));
665    }
666    m.push_str("  assign r = {y3, y2, y1, y0};\nendmodule\n");
667    m
668}
669
670/// Emit the packed **F16×4 multiply** tile: four `fp16_mul` lanes. Module `simd_fmul_f16x4`. Compile alongside
671/// [`FP16_MUL_V`]; proven ≡ [`eval_lanewise_fmul_f16x4`] under iverilog.
672pub fn emit_simd_fmul_f16x4_verilog() -> String {
673    let mut m =
674        String::from("module simd_fmul_f16x4(input [63:0] a, input [63:0] b, output [63:0] r);\n");
675    for k in 0..4 {
676        let (lo, hi) = (k * 16, k * 16 + 15);
677        m.push_str(&format!(
678            "  wire [15:0] y{k};\n  fp16_mul u{k}(.a(a[{hi}:{lo}]), .b(b[{hi}:{lo}]), .y(y{k}));\n"
679        ));
680    }
681    m.push_str("  assign r = {y3, y2, y1, y0};\nendmodule\n");
682    m
683}
684
685/// A self-checking combinational testbench for the scalar `fp32_add`: drive `n` `(a,b)` 32-bit vectors from
686/// `fp32_add_a.hex`/`_b.hex`, compare the settled `y` against Rust's `f32` add oracle in `fp32_add_o.hex`.
687pub fn emit_fp32_scalar_add_testbench(n: usize) -> String {
688    format!(
689        r#"`timescale 1ns/1ps
690module tb_fp32_add;
691  reg [31:0] a, b; wire [31:0] y;
692  reg [31:0] ia [0:{last}]; reg [31:0] ib [0:{last}]; reg [31:0] oo [0:{last}];
693  integer i, errors = 0;
694  fp32_add dut(.a(a), .b_in(b), .sub(1'b0), .y(y));
695  initial begin
696    $readmemh("fp32_add_a.hex", ia);
697    $readmemh("fp32_add_b.hex", ib);
698    $readmemh("fp32_add_o.hex", oo);
699    for (i = 0; i < {n}; i = i + 1) begin
700      a = ia[i]; b = ib[i]; #1;
701      if (y !== oo[i]) begin errors = errors + 1; if (errors <= 20) $display("FAIL %0d: a=%h b=%h got=%h exp=%h", i, a, b, y, oo[i]); end
702    end
703    if (errors == 0) $display("PASS: {n} fp32 adds bit-exact to Rust f32");
704    else $display("FAIL: %0d fp32 add mismatches", errors);
705    $finish;
706  end
707endmodule
708"#,
709        last = n - 1,
710    )
711}
712
713/// Emit the packed SWAR add as a COMBINATIONAL Verilog module — the SIMD tile the RTL flow tapes out:
714/// `r = ((a & M) + (b & M)) ^ ((a ^ b) & ~M)`, one datapath op, carry blocked at every lane boundary.
715/// Proven ≡ the lane-wise spec under iverilog by `simd_rtl_lock` (and ≡ N scalar adds by SAT in `simd_tile_lock`).
716pub fn emit_simd_add_verilog(name: &str, ty: LaneTy) -> String {
717    let m = swar_lo_mask(ty);
718    let hm = !m;
719    format!(
720        "// simd_{name} — packed {lanes}×{width}-bit SWAR add (carry-isolated, combinational, 1 clock).\n\
721         module simd_{name}(input [63:0] a, input [63:0] b, output [63:0] r);\n\
722         \x20 assign r = ((a & 64'h{m:016x}) + (b & 64'h{m:016x})) ^ ((a ^ b) & 64'h{hm:016x});\n\
723         endmodule\n",
724        lanes = ty.lanes(),
725        width = ty.width(),
726    )
727}
728
729/// A self-checking combinational testbench for `simd_{name}`: drive `n` `(a,b)` vectors from
730/// `simd_{name}_a.hex`/`_b.hex`, compare the settled `r` against the lane-wise oracle in `_o.hex`, print PASS.
731pub fn emit_simd_add_testbench(name: &str, n: usize) -> String {
732    format!(
733        r#"`timescale 1ns/1ps
734module tb_simd_{name};
735  reg [63:0] a, b; wire [63:0] r;
736  reg [63:0] ia [0:{last}]; reg [63:0] ib [0:{last}]; reg [63:0] oo [0:{last}];
737  integer i, errors = 0;
738  simd_{name} dut(.a(a), .b(b), .r(r));
739  initial begin
740    $readmemh("simd_{name}_a.hex", ia);
741    $readmemh("simd_{name}_b.hex", ib);
742    $readmemh("simd_{name}_o.hex", oo);
743    for (i = 0; i < {n}; i = i + 1) begin
744      a = ia[i]; b = ib[i]; #1;
745      if (r !== oo[i]) begin errors = errors + 1; $display("FAIL vec %0d: got %h exp %h", i, r, oo[i]); end
746    end
747    if (errors == 0) $display("PASS: {n} SIMD adds match the lane-wise spec");
748    else $display("FAIL: %0d SIMD mismatches", errors);
749    $finish;
750  end
751endmodule
752"#,
753        last = n - 1,
754    )
755}
756
757/// Emit the packed **F32×2 add** tile: two `fp32_add` lanes (low = bits `[31:0]`, high = `[63:32]`) driven in
758/// parallel from the 64-bit operands. Module name `simd_fadd_f32x2` (ports `a,b,r`), so the standard SIMD
759/// testbench drives it. Compile alongside [`FP32_ADD_V`]; proven ≡ [`eval_lanewise_fadd_f32x2`] under iverilog.
760pub fn emit_simd_fadd_f32x2_verilog() -> String {
761    "module simd_fadd_f32x2(input [63:0] a, input [63:0] b, output [63:0] r);\n\
762     \x20 wire [31:0] lo, hi;\n\
763     \x20 fp32_add u0(.a(a[31:0]),  .b_in(b[31:0]),  .sub(1'b0), .y(lo));\n\
764     \x20 fp32_add u1(.a(a[63:32]), .b_in(b[63:32]), .sub(1'b0), .y(hi));\n\
765     \x20 assign r = {hi, lo};\n\
766     endmodule\n"
767        .to_string()
768}
769
770/// Emit the packed **F32×2 multiply** tile: two `fp32_mul` lanes in parallel. Module name `simd_fmul_f32x2`.
771/// Compile alongside [`FP32_MUL_V`]; proven ≡ [`eval_lanewise_fmul_f32x2`] under iverilog.
772pub fn emit_simd_fmul_f32x2_verilog() -> String {
773    "module simd_fmul_f32x2(input [63:0] a, input [63:0] b, output [63:0] r);\n\
774     \x20 wire [31:0] lo, hi;\n\
775     \x20 fp32_mul u0(.a(a[31:0]),  .b(b[31:0]),  .y(lo));\n\
776     \x20 fp32_mul u1(.a(a[63:32]), .b(b[63:32]), .y(hi));\n\
777     \x20 assign r = {hi, lo};\n\
778     endmodule\n"
779        .to_string()
780}
781
782/// Widen an IEEE-754 binary16 bit pattern to `f32` — EXACT (every f16 value is representable in f32).
783pub fn f16_to_f32(bits: u16) -> f32 {
784    let sign = if bits & 0x8000 != 0 { -1.0f32 } else { 1.0 };
785    let exp = ((bits >> 10) & 0x1F) as i32;
786    let mant = (bits & 0x3FF) as f32;
787    if exp == 0x1F {
788        if mant == 0.0 {
789            return sign * f32::INFINITY;
790        }
791        return f32::from_bits(0x7FC0_0000); // canonical qNaN
792    }
793    if exp == 0 {
794        // subnormal (or ±0): value = mant * 2^-24
795        return sign * mant * (-24f32).exp2();
796    }
797    sign * (1.0 + mant / 1024.0) * ((exp - 15) as f32).exp2()
798}
799
800/// Round an `f32` to an IEEE-754 binary16 bit pattern, round-to-nearest-ties-even (overflow→Inf,
801/// underflow→subnormal/0, NaN→canonical qNaN). The one rounding step in the f16 reference; anchored by
802/// known-value assertions in `simd_tile_lock`.
803pub fn f32_to_f16(x: f32) -> u16 {
804    let bits = x.to_bits();
805    let sign = ((bits >> 16) & 0x8000) as u16;
806    let exp = ((bits >> 23) & 0xFF) as i32;
807    let mant = bits & 0x7F_FFFF; // 23-bit
808    if exp == 0xFF {
809        return if mant != 0 {
810            sign | 0x7E00
811        } else {
812            sign | 0x7C00
813        };
814    }
815    let e = exp - 127; // unbiased
816    if e > 15 {
817        return sign | 0x7C00; // overflow → Inf
818    }
819    if e < -14 {
820        // subnormal / underflow: value = full * 2^(e-23), rendered as frac * 2^-24 ⇒ frac = full * 2^(e+1)
821        if e < -25 {
822            return sign; // strictly less than half the smallest subnormal → ±0
823        }
824        let full = (1u32 << 23) | mant; // 24-bit significand
825        let shift = (-e - 1) as u32; // 14..=24
826        let frac = full >> shift;
827        let rem = full & ((1u32 << shift) - 1);
828        let half = 1u32 << (shift - 1);
829        let mut f = frac;
830        if rem > half || (rem == half && (frac & 1) == 1) {
831            f += 1; // RNE
832        }
833        if f >= 1024 {
834            return sign | 0x0400; // rounded up to the smallest normal
835        }
836        return sign | f as u16;
837    }
838    // normal: round the 23-bit mantissa to 10 bits (RNE)
839    let frac = mant >> 13;
840    let rem = mant & 0x1FFF;
841    let half = 0x1000;
842    let mut f = frac;
843    if rem > half || (rem == half && (frac & 1) == 1) {
844        f += 1;
845    }
846    let mut exp_out = (e + 15) as u16; // 1..=30
847    if f >= 1024 {
848        f = 0;
849        exp_out += 1;
850    }
851    if exp_out >= 31 {
852        return sign | 0x7C00; // overflow → Inf
853    }
854    sign | (exp_out << 10) | f as u16
855}
856
857/// One correctly-rounded IEEE-754 binary16 add: widen (exact), add in f32 (exact — the sum of two f16s fits in
858/// f32's 24-bit significand), round back RNE. Equals the native binary16 add bit-for-bit (both correctly rounded).
859pub fn f16_add(a: u16, b: u16) -> u16 {
860    f32_to_f16(f16_to_f32(a) + f16_to_f32(b))
861}
862
863/// One correctly-rounded IEEE-754 binary16 multiply (see [`f16_add`]; the product of two f16s is exact in f32).
864pub fn f16_mul(a: u16, b: u16) -> u16 {
865    f32_to_f16(f16_to_f32(a) * f16_to_f32(b))
866}
867
868/// Pack four f16 lanes into a 64-bit word (lane 0 low).
869pub fn pack_f16x4(l0: u16, l1: u16, l2: u16, l3: u16) -> u64 {
870    (l0 as u64) | ((l1 as u64) << 16) | ((l2 as u64) << 32) | ((l3 as u64) << 48)
871}
872
873/// Packed-float add for **F16×4** — the SPEC: four independent scalar binary16 adds. Lanes are isolated (a
874/// NaN/Inf/subnormal in one cannot perturb another); the parallel `fp16` FPU lanes realize it in silicon.
875pub fn eval_lanewise_fadd_f16x4(a: u64, b: u64) -> u64 {
876    let lane = |sh: u32| f16_add((a >> sh) as u16, (b >> sh) as u16);
877    pack_f16x4(lane(0), lane(16), lane(32), lane(48))
878}
879
880/// Packed-float multiply for **F16×4** (see [`eval_lanewise_fadd_f16x4`]).
881pub fn eval_lanewise_fmul_f16x4(a: u64, b: u64) -> u64 {
882    let lane = |sh: u32| f16_mul((a >> sh) as u16, (b >> sh) as u16);
883    pack_f16x4(lane(0), lane(16), lane(32), lane(48))
884}
885
886/// Evaluate the lane-wise add SPEC concretely (each lane an independent `width`-bit wrapping add).
887pub fn eval_lanewise_add(ty: LaneTy, a: u64, b: u64) -> u64 {
888    let w = ty.width();
889    let full = if w == 64 { u64::MAX } else { (1u64 << w) - 1 };
890    let mut out = 0u64;
891    for j in 0..ty.lanes() {
892        let sh = j * w;
893        let aj = (a >> sh) & full;
894        let bj = (b >> sh) & full;
895        out |= ((aj.wrapping_add(bj)) & full) << sh;
896    }
897    out
898}