Skip to main content

logicaffeine_base/
word.rs

1//! Fixed-width wrapping integers — the ring ℤ/2ᵏℤ.
2//!
3//! Unlike [`crate::numeric::BigInt`], whose arithmetic is exact and unbounded, a `Word`
4//! is **total and wrapping**: `Word32::MAX.add(Word32::ONE) == Word32::ZERO`. This is the
5//! natural home for the bit-twiddling primitives — ChaCha20 lives over `Word32`, Keccak
6//! over `Word64` — where each operation is a single native instruction and the modular
7//! semantics are exact rather than an overflow into arbitrary precision. Rotation
8//! (`rotl`/`rotr`) is width-defined and lives only here.
9
10macro_rules! impl_word {
11    ($name:ident, $prim:ty, $bits:literal) => {
12        #[doc = concat!("A ", $bits, "-bit wrapping integer: the ring ℤ/2^", $bits, "ℤ, where every operation is total and wraps modulo 2^", $bits, ".")]
13        #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
14        #[repr(transparent)]
15        pub struct $name(pub $prim);
16
17        impl $name {
18            /// Bit width of the ring.
19            pub const BITS: u32 = <$prim>::BITS;
20            /// Additive identity.
21            pub const ZERO: Self = Self(0);
22            /// Multiplicative identity.
23            pub const ONE: Self = Self(1);
24            /// The all-ones value (`2^BITS − 1`).
25            pub const MAX: Self = Self(<$prim>::MAX);
26
27            /// The underlying primitive value.
28            #[inline]
29            pub const fn get(self) -> $prim {
30                self.0
31            }
32
33            /// Wrapping addition in ℤ/2ᵏ.
34            #[inline]
35            pub const fn add(self, o: Self) -> Self {
36                Self(self.0.wrapping_add(o.0))
37            }
38
39            /// Wrapping subtraction in ℤ/2ᵏ.
40            #[inline]
41            pub const fn sub(self, o: Self) -> Self {
42                Self(self.0.wrapping_sub(o.0))
43            }
44
45            /// Wrapping multiplication in ℤ/2ᵏ.
46            #[inline]
47            pub const fn mul(self, o: Self) -> Self {
48                Self(self.0.wrapping_mul(o.0))
49            }
50
51            /// Bitwise AND.
52            #[inline]
53            pub const fn bitand(self, o: Self) -> Self {
54                Self(self.0 & o.0)
55            }
56
57            /// Bitwise OR.
58            #[inline]
59            pub const fn bitor(self, o: Self) -> Self {
60                Self(self.0 | o.0)
61            }
62
63            /// Bitwise XOR.
64            #[inline]
65            pub const fn bitxor(self, o: Self) -> Self {
66                Self(self.0 ^ o.0)
67            }
68
69            /// Bitwise complement.
70            #[inline]
71            pub const fn not(self) -> Self {
72                Self(!self.0)
73            }
74
75            /// Logical left shift by `n` (the count is taken modulo `BITS`, matching the
76            /// hardware shift and the language's `shifted left by`).
77            #[inline]
78            pub const fn shl(self, n: u32) -> Self {
79                Self(self.0.wrapping_shl(n))
80            }
81
82            /// Logical right shift by `n` (count modulo `BITS`).
83            #[inline]
84            pub const fn shr(self, n: u32) -> Self {
85                Self(self.0.wrapping_shr(n))
86            }
87
88            /// Left rotation by `n` — a width-defined bit-permutation (the crypto primitive).
89            #[inline]
90            pub const fn rotl(self, n: u32) -> Self {
91                Self(self.0.rotate_left(n))
92            }
93
94            /// Right rotation by `n`.
95            #[inline]
96            pub const fn rotr(self, n: u32) -> Self {
97                Self(self.0.rotate_right(n))
98            }
99        }
100
101        // Operator traits delegate to the wrapping primitives, so generated Rust can write the
102        // natural `a + b` / `a ^ b` and get ring semantics — no per-site `wrapping_*` in codegen.
103        impl ::core::ops::Add for $name {
104            type Output = Self;
105            #[inline]
106            fn add(self, o: Self) -> Self { Self(self.0.wrapping_add(o.0)) }
107        }
108        impl ::core::ops::Sub for $name {
109            type Output = Self;
110            #[inline]
111            fn sub(self, o: Self) -> Self { Self(self.0.wrapping_sub(o.0)) }
112        }
113        impl ::core::ops::Mul for $name {
114            type Output = Self;
115            #[inline]
116            fn mul(self, o: Self) -> Self { Self(self.0.wrapping_mul(o.0)) }
117        }
118        impl ::core::ops::BitAnd for $name {
119            type Output = Self;
120            #[inline]
121            fn bitand(self, o: Self) -> Self { Self(self.0 & o.0) }
122        }
123        impl ::core::ops::BitOr for $name {
124            type Output = Self;
125            #[inline]
126            fn bitor(self, o: Self) -> Self { Self(self.0 | o.0) }
127        }
128        impl ::core::ops::BitXor for $name {
129            type Output = Self;
130            #[inline]
131            fn bitxor(self, o: Self) -> Self { Self(self.0 ^ o.0) }
132        }
133        impl ::core::ops::Not for $name {
134            type Output = Self;
135            #[inline]
136            fn not(self) -> Self { Self(!self.0) }
137        }
138        // `/` and `%` are the underlying UNSIGNED integer division / remainder — so `x % 2^k` is a
139        // mask and `x / 2^k` a shift (the crypto reduction primitives), with no sign-correction.
140        impl ::core::ops::Div for $name {
141            type Output = Self;
142            #[inline]
143            fn div(self, o: Self) -> Self { Self(self.0 / o.0) }
144        }
145        impl ::core::ops::Rem for $name {
146            type Output = Self;
147            #[inline]
148            fn rem(self, o: Self) -> Self { Self(self.0 % o.0) }
149        }
150        impl ::core::ops::Shl<u32> for $name {
151            type Output = Self;
152            #[inline]
153            fn shl(self, n: u32) -> Self { Self(self.0.wrapping_shl(n)) }
154        }
155        impl ::core::ops::Shr<u32> for $name {
156            type Output = Self;
157            #[inline]
158            fn shr(self, n: u32) -> Self { Self(self.0.wrapping_shr(n)) }
159        }
160        // Displays as the underlying unsigned value (decimal) — the canonical scalar form the
161        // tree-walker prints, so tw / VM / AOT render a word identically.
162        impl ::core::fmt::Display for $name {
163            #[inline]
164            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
165                ::core::write!(f, "{}", self.0)
166            }
167        }
168    };
169}
170
171impl_word!(Word8, u8, "8");
172impl_word!(Word16, u16, "16");
173impl_word!(Word32, u32, "32");
174impl_word!(Word64, u64, "64");
175
176/// A fixed-width wrapping integer of either supported width — the single runtime carrier for
177/// `Word32`/`Word64` across the interpreter, VM, and wire, so the rest of the system matches on
178/// one `Word` value rather than a variant per width. Binary ops require matching widths; a
179/// width mismatch is a type error the caller reports (the ops return `None`).
180#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
181pub enum WordVal {
182    /// A 32-bit wrapping value.
183    W32(Word32),
184    /// A 64-bit wrapping value.
185    W64(Word64),
186}
187
188impl WordVal {
189    /// Bit width, 32 or 64.
190    #[inline]
191    pub const fn width(self) -> u32 {
192        match self {
193            WordVal::W32(_) => 32,
194            WordVal::W64(_) => 64,
195        }
196    }
197
198    /// The value zero-extended to `u64` — the canonical scalar form for display, hashing into
199    /// other contexts, and the wire.
200    #[inline]
201    pub const fn to_u64(self) -> u64 {
202        match self {
203            WordVal::W32(w) => w.0 as u64,
204            WordVal::W64(w) => w.0,
205        }
206    }
207
208    /// Build a word of the given `width` from the low bits of `bits` (32 → truncates to `u32`).
209    #[inline]
210    pub const fn from_u64(width: u32, bits: u64) -> Option<Self> {
211        match width {
212            32 => Some(WordVal::W32(Word32(bits as u32))),
213            64 => Some(WordVal::W64(Word64(bits))),
214            _ => None,
215        }
216    }
217
218    #[inline]
219    fn zip(
220        self,
221        o: Self,
222        f32: impl FnOnce(Word32, Word32) -> Word32,
223        f64: impl FnOnce(Word64, Word64) -> Word64,
224    ) -> Option<Self> {
225        match (self, o) {
226            (WordVal::W32(a), WordVal::W32(b)) => Some(WordVal::W32(f32(a, b))),
227            (WordVal::W64(a), WordVal::W64(b)) => Some(WordVal::W64(f64(a, b))),
228            _ => None,
229        }
230    }
231
232    /// Wrapping addition; `None` on a width mismatch.
233    #[inline]
234    pub fn add(self, o: Self) -> Option<Self> {
235        self.zip(o, Word32::add, Word64::add)
236    }
237    /// Wrapping subtraction; `None` on a width mismatch.
238    #[inline]
239    pub fn sub(self, o: Self) -> Option<Self> {
240        self.zip(o, Word32::sub, Word64::sub)
241    }
242    /// Wrapping multiplication; `None` on a width mismatch.
243    #[inline]
244    pub fn mul(self, o: Self) -> Option<Self> {
245        self.zip(o, Word32::mul, Word64::mul)
246    }
247    /// Bitwise AND; `None` on a width mismatch.
248    #[inline]
249    pub fn bitand(self, o: Self) -> Option<Self> {
250        self.zip(o, Word32::bitand, Word64::bitand)
251    }
252    /// Bitwise OR; `None` on a width mismatch.
253    #[inline]
254    pub fn bitor(self, o: Self) -> Option<Self> {
255        self.zip(o, Word32::bitor, Word64::bitor)
256    }
257    /// Bitwise XOR; `None` on a width mismatch.
258    #[inline]
259    pub fn bitxor(self, o: Self) -> Option<Self> {
260        self.zip(o, Word32::bitxor, Word64::bitxor)
261    }
262
263    /// Bitwise complement (width-preserving).
264    #[inline]
265    pub const fn not(self) -> Self {
266        match self {
267            WordVal::W32(w) => WordVal::W32(w.not()),
268            WordVal::W64(w) => WordVal::W64(w.not()),
269        }
270    }
271    /// Logical left shift by `n` (width-preserving).
272    #[inline]
273    pub const fn shl(self, n: u32) -> Self {
274        match self {
275            WordVal::W32(w) => WordVal::W32(w.shl(n)),
276            WordVal::W64(w) => WordVal::W64(w.shl(n)),
277        }
278    }
279    /// Logical right shift by `n` (width-preserving).
280    #[inline]
281    pub const fn shr(self, n: u32) -> Self {
282        match self {
283            WordVal::W32(w) => WordVal::W32(w.shr(n)),
284            WordVal::W64(w) => WordVal::W64(w.shr(n)),
285        }
286    }
287    /// Left rotation by `n` (width-preserving) — the bit-permutation crypto needs.
288    #[inline]
289    pub const fn rotl(self, n: u32) -> Self {
290        match self {
291            WordVal::W32(w) => WordVal::W32(w.rotl(n)),
292            WordVal::W64(w) => WordVal::W64(w.rotl(n)),
293        }
294    }
295    /// Right rotation by `n` (width-preserving).
296    #[inline]
297    pub const fn rotr(self, n: u32) -> Self {
298        match self {
299            WordVal::W32(w) => WordVal::W32(w.rotr(n)),
300            WordVal::W64(w) => WordVal::W64(w.rotr(n)),
301        }
302    }
303}
304
305impl std::fmt::Display for WordVal {
306    /// A word renders as its unsigned decimal value (no width suffix), so a `Word32` holding 5
307    /// and an `Int` 5 print identically — the value is what `Show` reports.
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        write!(f, "{}", self.to_u64())
310    }
311}
312
313// ── SIMD lane vectors — fixed-width vectors over the Word ring ─────────────────────────────────
314//
315// `Lanes8Word32` is 8 lanes of `Word32` = exactly one AVX2 `__m256i`. The whole point of the type
316// is that a vectorized algorithm WRITTEN in Logos over these lanes compiles to the same instructions
317// as hand-written AVX2 (each lane op IS an intrinsic). The runtime carries the portable scalar
318// representation `[u32; 8]`; every lane op has an AVX2 fast path AND a scalar fallback, and the two
319// are proven byte-identical by a fuzz test (the SIMD-correctness proof at the base level). The
320// language's tier differential (tree-walker == VM == AOT) then proves the pipeline lowers it right.
321
322/// Eight lanes of `Word32` (one 256-bit SIMD register). Operations are lane-wise over the ℤ/2³² ring.
323#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
324#[repr(C, align(32))]
325pub struct Lanes8Word32(pub [u32; 8]);
326
327impl Lanes8Word32 {
328    /// The number of lanes.
329    pub const LANES: usize = 8;
330
331    /// Broadcast one value into all eight lanes.
332    #[inline]
333    pub const fn splat(x: u32) -> Self {
334        Self([x; 8])
335    }
336
337    /// Pack the first eight `Word32`s of a slice into a lane vector (shorter slices zero-fill).
338    #[inline]
339    pub fn from_words(s: &[Word32]) -> Self {
340        let mut a = [0u32; 8];
341        for (i, w) in s.iter().take(8).enumerate() {
342            a[i] = w.0;
343        }
344        Self(a)
345    }
346
347    /// The lanes as eight `Word32`s.
348    #[inline]
349    pub fn to_words(self) -> [Word32; 8] {
350        self.0.map(Word32)
351    }
352
353    /// Lane `i` (0-based) as a `Word32`.
354    #[inline]
355    pub fn lane(self, i: usize) -> Word32 {
356        Word32(self.0[i])
357    }
358
359    /// Lane-wise XOR (`vpxor`). `#[inline(always)]` + compile-time `cfg(target_feature="avx2")`
360    /// intrinsics so a hot Logos lane kernel (ChaCha/NTT) inlines register-resident under `+avx2`
361    /// (no per-op `#[target_feature]` call boundary — that pessimizes ~20× on Keccak-scale kernels).
362    #[inline(always)]
363    pub fn bitxor(self, o: Self) -> Self {
364        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
365        unsafe {
366            use std::arch::x86_64::*;
367            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
368            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
369            let mut r = [0u32; 8];
370            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_xor_si256(a, b));
371            return Self(r);
372        }
373        #[allow(unreachable_code)]
374        {
375            let mut r = [0u32; 8];
376            for i in 0..8 {
377                r[i] = self.0[i] ^ o.0[i];
378            }
379            Self(r)
380        }
381    }
382
383    /// Scalar lane-wise reference implementations — the *spec* the AVX2 lowerings are fuzz-checked
384    /// against. Test-only: each dispatched op inlines its own `#[allow(unreachable_code)]` scalar
385    /// fallback, so these exist purely as the independent differential oracle.
386    #[cfg(test)]
387    #[inline]
388    fn bitxor_scalar(self, o: Self) -> Self {
389        Self(core::array::from_fn(|i| self.0[i] ^ o.0[i]))
390    }
391    #[cfg(test)]
392    #[inline]
393    fn add_scalar(self, o: Self) -> Self {
394        Self(core::array::from_fn(|i| self.0[i].wrapping_add(o.0[i])))
395    }
396    #[cfg(test)]
397    #[inline]
398    fn sub_scalar(self, o: Self) -> Self {
399        Self(core::array::from_fn(|i| self.0[i].wrapping_sub(o.0[i])))
400    }
401    #[cfg(test)]
402    #[inline]
403    fn rotl_scalar(self, n: u32) -> Self {
404        Self(core::array::from_fn(|i| self.0[i].rotate_left(n)))
405    }
406    #[cfg(test)]
407    #[inline]
408    fn montmul32_scalar(self, b: Self, q: Self, qinv: Self) -> Self {
409        Self(core::array::from_fn(|i| {
410            let p = (self.0[i] as i32 as i64) * (b.0[i] as i32 as i64);
411            let t = (p as i32).wrapping_mul(qinv.0[i] as i32) as i64;
412            (((p - t * (q.0[i] as i32 as i64)) >> 32) as i32) as u32
413        }))
414    }
415
416    /// Lane-wise AND (`_mm256_and_si256`) — the MD5 F/G-function bit mixing; LLVM lowers the loop to one
417    /// `vpand`. AND/OR/NOT have no cross-lane dependency, so the scalar form auto-vectorizes cleanly.
418    #[inline]
419    pub fn bitand(self, o: Self) -> Self {
420        let mut r = [0u32; 8];
421        for i in 0..8 {
422            r[i] = self.0[i] & o.0[i];
423        }
424        Self(r)
425    }
426
427    /// Lane-wise OR (`_mm256_or_si256`).
428    #[inline]
429    pub fn bitor(self, o: Self) -> Self {
430        let mut r = [0u32; 8];
431        for i in 0..8 {
432            r[i] = self.0[i] | o.0[i];
433        }
434        Self(r)
435    }
436
437    /// Lane-wise complement (`vpxor` with all-ones) — MD5's `~b`/`~d` terms.
438    #[inline]
439    pub fn not(self) -> Self {
440        let mut r = [0u32; 8];
441        for i in 0..8 {
442            r[i] = !self.0[i];
443        }
444        Self(r)
445    }
446
447    /// Lane-wise wrapping add in ℤ/2³² (`vpaddd`) — cfg-inline so it fuses into hot Logos lane kernels.
448    #[inline(always)]
449    pub fn add(self, o: Self) -> Self {
450        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
451        unsafe {
452            use std::arch::x86_64::*;
453            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
454            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
455            let mut r = [0u32; 8];
456            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_add_epi32(a, b));
457            return Self(r);
458        }
459        #[allow(unreachable_code)]
460        {
461            let mut r = [0u32; 8];
462            for i in 0..8 {
463                r[i] = self.0[i].wrapping_add(o.0[i]);
464            }
465            Self(r)
466        }
467    }
468
469    /// Lane-wise left rotation by `n` (ChaCha diffusion) — `(x<<n)|(x>>(32−n))` via `vpslld`/`vpsrld`.
470    /// cfg-inline; `n` is taken mod 32 (n = 0 → the `32−n = 32` shift zeroes → identity).
471    #[inline(always)]
472    pub fn rotl(self, n: u32) -> Self {
473        let n = n % 32;
474        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
475        unsafe {
476            use std::arch::x86_64::*;
477            let x = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
478            let l = _mm256_sll_epi32(x, _mm_cvtsi32_si128(n as i32));
479            let r_sh = _mm256_srl_epi32(x, _mm_cvtsi32_si128((32 - n) as i32));
480            let mut r = [0u32; 8];
481            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_or_si256(l, r_sh));
482            return Self(r);
483        }
484        #[allow(unreachable_code)]
485        {
486            let mut r = [0u32; 8];
487            for i in 0..8 {
488                r[i] = self.0[i].rotate_left(n);
489            }
490            Self(r)
491        }
492    }
493
494    /// Lane-wise wrapping subtract in ℤ/2³² (`vpsubd`) — the i32 NTT butterfly's difference. cfg-inline.
495    #[inline(always)]
496    pub fn sub(self, o: Self) -> Self {
497        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
498        unsafe {
499            use std::arch::x86_64::*;
500            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
501            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
502            let mut r = [0u32; 8];
503            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_sub_epi32(a, b));
504            return Self(r);
505        }
506        #[allow(unreachable_code)]
507        {
508            let mut r = [0u32; 8];
509            for i in 0..8 {
510                r[i] = self.0[i].wrapping_sub(o.0[i]);
511            }
512            Self(r)
513        }
514    }
515
516    /// The signed i32 Montgomery multiply — per lane `montgomery_reduce(aᵢ·bᵢ) = (aᵢbᵢ − t·q)≫32`,
517    /// `t = (aᵢbᵢ mod 2³²)·qinv`, the ML-DSA (Dilithium) NTT butterfly's multiply (q = 8380417,
518    /// q,qinv broadcast). AVX2: `vpmuldq` the even and the (≫32) odd 32-bit lanes to eight i64
519    /// products, reduce each (the result lands in the high 32 bits), recombine with `vpblendd`.
520    #[inline(always)]
521    pub fn montmul32(self, b: Self, q: Self, qinv: Self) -> Self {
522        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
523        unsafe {
524            use std::arch::x86_64::*;
525            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
526            let bb = _mm256_loadu_si256(b.0.as_ptr() as *const __m256i);
527            let qv = _mm256_loadu_si256(q.0.as_ptr() as *const __m256i);
528            let qiv = _mm256_loadu_si256(qinv.0.as_ptr() as *const __m256i);
529            // Eight i64 products: even 32-bit lanes directly, odd lanes shifted into even position.
530            let pe = _mm256_mul_epi32(a, bb);
531            let po = _mm256_mul_epi32(_mm256_srli_epi64(a, 32), _mm256_srli_epi64(bb, 32));
532            // Reduce: t = (p mod 2³²)·qinv; re = p − t·q lives in the high 32.
533            let te = _mm256_mul_epi32(pe, qiv);
534            let re = _mm256_sub_epi64(pe, _mm256_mul_epi32(te, qv));
535            let to = _mm256_mul_epi32(po, qiv);
536            let ro = _mm256_sub_epi64(po, _mm256_mul_epi32(to, qv));
537            let res = _mm256_blend_epi32(_mm256_srli_epi64(re, 32), ro, 0xAA);
538            let mut r = [0u32; 8];
539            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, res);
540            return Self(r);
541        }
542        #[allow(unreachable_code)]
543        {
544            let qq = q.0[0] as i32 as i64;
545            let qi = qinv.0[0] as i32;
546            let mut r = [0u32; 8];
547            for i in 0..8 {
548                let p = (self.0[i] as i32 as i64) * (b.0[i] as i32 as i64);
549                let t = (p as i32).wrapping_mul(qi) as i64;
550                r[i] = (((p - t * qq) >> 32) as i32) as u32;
551            }
552            Self(r)
553        }
554    }
555
556    /// Broadcast each `2h`-block's low `h` lanes into both halves — the within-vector NTT source-low
557    /// duplication for 8 i32 lanes, stride `h ∈ {4,2,1}`. `h=4`→`vperm2i128(0x00)` (128-bit halves);
558    /// `h=2`→`vpshufd(0x44)`; `h=1`→`vpshufd(0xA0)`. (The byte op is the i16 stride-`2h` shuffle.)
559    #[inline(always)]
560    pub fn ntt_bcast_lo(self, h: usize) -> Self {
561        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
562        unsafe {
563            use std::arch::x86_64::*;
564            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
565            let v = match h {
566                4 => _mm256_permute2x128_si256::<0x00>(a, a),
567                2 => _mm256_shuffle_epi32::<0x44>(a),
568                1 => _mm256_shuffle_epi32::<0xA0>(a),
569                _ => unreachable!("i32 within-vector NTT stride is 4/2/1"),
570            };
571            let mut r = [0u32; 8];
572            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, v);
573            return Self(r);
574        }
575        #[allow(unreachable_code)]
576        Self(core::array::from_fn(|i| self.0[(i / (2 * h)) * (2 * h) + (i % (2 * h)) % h]))
577    }
578
579    /// Broadcast each `2h`-block's high `h` lanes into both halves. `h=4`→`vperm2i128(0x11)`;
580    /// `h=2`→`vpshufd(0xEE)`; `h=1`→`vpshufd(0xF5)`.
581    #[inline(always)]
582    pub fn ntt_bcast_hi(self, h: usize) -> Self {
583        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
584        unsafe {
585            use std::arch::x86_64::*;
586            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
587            let v = match h {
588                4 => _mm256_permute2x128_si256::<0x11>(a, a),
589                2 => _mm256_shuffle_epi32::<0xEE>(a),
590                1 => _mm256_shuffle_epi32::<0xF5>(a),
591                _ => unreachable!("i32 within-vector NTT stride is 4/2/1"),
592            };
593            let mut r = [0u32; 8];
594            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, v);
595            return Self(r);
596        }
597        #[allow(unreachable_code)]
598        Self(core::array::from_fn(|i| self.0[(i / (2 * h)) * (2 * h) + h + (i % (2 * h)) % h]))
599    }
600
601    /// Recombine the `+`/`−` halves: each `2h`-block's low `h` from `self`, high `h` from `o`.
602    /// `h=4`→`vperm2i128(0x30)`; `h=2`→`vpblendd(0xCC)`; `h=1`→`vpblendd(0xAA)`.
603    #[inline(always)]
604    pub fn ntt_blend(self, o: Self, h: usize) -> Self {
605        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
606        unsafe {
607            use std::arch::x86_64::*;
608            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
609            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
610            let v = match h {
611                4 => _mm256_permute2x128_si256::<0x30>(a, b),
612                2 => _mm256_blend_epi32::<0xCC>(a, b),
613                1 => _mm256_blend_epi32::<0xAA>(a, b),
614                _ => unreachable!("i32 within-vector NTT stride is 4/2/1"),
615            };
616            let mut r = [0u32; 8];
617            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, v);
618            return Self(r);
619        }
620        #[allow(unreachable_code)]
621        Self(core::array::from_fn(|i| if (i % (2 * h)) < h { self.0[i] } else { o.0[i] }))
622    }
623}
624
625// Operator traits delegate to the lane ops, so generated Rust writes the natural `v ^ w` / `v + w`
626// and gets the AVX2 lowering — the same free-operator path Word uses.
627impl ::core::ops::BitXor for Lanes8Word32 {
628    type Output = Self;
629    #[inline]
630    fn bitxor(self, o: Self) -> Self {
631        Lanes8Word32::bitxor(self, o)
632    }
633}
634impl ::core::ops::Add for Lanes8Word32 {
635    type Output = Self;
636    #[inline]
637    fn add(self, o: Self) -> Self {
638        Lanes8Word32::add(self, o)
639    }
640}
641impl ::core::ops::Sub for Lanes8Word32 {
642    type Output = Self;
643    #[inline]
644    fn sub(self, o: Self) -> Self {
645        Lanes8Word32::sub(self, o)
646    }
647}
648impl ::core::ops::BitAnd for Lanes8Word32 {
649    type Output = Self;
650    #[inline]
651    fn bitand(self, o: Self) -> Self {
652        Lanes8Word32::bitand(self, o)
653    }
654}
655impl ::core::ops::BitOr for Lanes8Word32 {
656    type Output = Self;
657    #[inline]
658    fn bitor(self, o: Self) -> Self {
659        Lanes8Word32::bitor(self, o)
660    }
661}
662impl ::core::ops::Not for Lanes8Word32 {
663    type Output = Self;
664    #[inline]
665    fn not(self) -> Self {
666        Lanes8Word32::not(self)
667    }
668}
669
670/// Four lanes of `Word32` = one 128-bit register (`__m128i`) — the SHA-1 state/message carrier. Unlike
671/// the arithmetic lane types, its vocabulary is the four Intel SHA operations (`sha1rnds4`/`sha1msg1`/
672/// `sha1msg2`/`sha1nexte`), so SHA-1 WRITTEN in Logos over these compiles to the `sha1rnds4` hardware
673/// sequence (AOT) and runs the byte-identical software spec [`crate::sha_ops`] on the interpreter.
674/// Lane `i` is bits `[32i+31 : 32i]` — index 0 low, index 3 high — matching `_mm_loadu_si128`.
675#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
676#[repr(C, align(16))]
677pub struct Lanes4Word32(pub [u32; 4]);
678
679impl Lanes4Word32 {
680    /// The number of lanes.
681    pub const LANES: usize = 4;
682
683    /// Broadcast one value into all four lanes.
684    #[inline]
685    pub const fn splat(x: u32) -> Self {
686        Self([x; 4])
687    }
688
689    /// Pack the first four `Word32`s of a slice (shorter slices zero-fill), lane 0 = element 0.
690    #[inline]
691    pub fn from_words(s: &[Word32]) -> Self {
692        let mut a = [0u32; 4];
693        for (i, w) in s.iter().take(4).enumerate() {
694            a[i] = w.0;
695        }
696        Self(a)
697    }
698
699    /// The lanes as four `Word32`s.
700    #[inline]
701    pub fn to_words(self) -> [Word32; 4] {
702        self.0.map(Word32)
703    }
704
705    /// Lane `i` (0-based) as a `Word32`.
706    #[inline]
707    pub fn lane(self, i: usize) -> Word32 {
708        Word32(self.0[i])
709    }
710
711    /// Four SHA-1 rounds (`sha1rnds4`), `func` ∈ 0..=3 — the Intel SHA-NI instruction when the CPU has
712    /// it, else the byte-identical software spec ([`crate::sha_ops`], proven equal by fuzz). `self` is
713    /// the ABCD state, `msg` the four message dwords with the round's E folded in.
714    #[inline(always)]
715    pub fn sha1rnds4(self, msg: Self, func: u32) -> Self {
716        // When the crate is built with SHA statically enabled (`target-cpu=native` or
717        // `-C target-feature=+sha`), the intrinsic is callable WITHOUT a `#[target_feature]` boundary,
718        // so this inlines and — chained across a compress — LLVM keeps the lanes in XMM registers
719        // (no per-op detect branch, no memory round-trip). The `not` arm is the portable runtime-detect.
720        #[cfg(all(target_arch = "x86_64", target_feature = "sha"))]
721        unsafe {
722            use core::arch::x86_64::*;
723            let a: __m128i = core::mem::transmute(self.0);
724            let b: __m128i = core::mem::transmute(msg.0);
725            let r = match func & 3 {
726                0 => _mm_sha1rnds4_epu32(a, b, 0),
727                1 => _mm_sha1rnds4_epu32(a, b, 1),
728                2 => _mm_sha1rnds4_epu32(a, b, 2),
729                _ => _mm_sha1rnds4_epu32(a, b, 3),
730            };
731            Self(core::mem::transmute(r))
732        }
733        #[cfg(not(all(target_arch = "x86_64", target_feature = "sha")))]
734        {
735            #[cfg(target_arch = "x86_64")]
736            {
737                if shani_available() {
738                    return unsafe { self.sha1rnds4_hw(msg, func) };
739                }
740            }
741            Self(crate::sha_ops::sha1rnds4(self.0, msg.0, func))
742        }
743    }
744    /// Message-schedule step 1 (`sha1msg1`).
745    #[inline(always)]
746    pub fn sha1msg1(self, o: Self) -> Self {
747        #[cfg(all(target_arch = "x86_64", target_feature = "sha"))]
748        unsafe {
749            use core::arch::x86_64::*;
750            let a: __m128i = core::mem::transmute(self.0);
751            let b: __m128i = core::mem::transmute(o.0);
752            Self(core::mem::transmute(_mm_sha1msg1_epu32(a, b)))
753        }
754        #[cfg(not(all(target_arch = "x86_64", target_feature = "sha")))]
755        {
756            #[cfg(target_arch = "x86_64")]
757            {
758                if shani_available() {
759                    return unsafe { self.sha1msg1_hw(o) };
760                }
761            }
762            Self(crate::sha_ops::sha1msg1(self.0, o.0))
763        }
764    }
765    /// Message-schedule step 2 (`sha1msg2`).
766    #[inline(always)]
767    pub fn sha1msg2(self, o: Self) -> Self {
768        #[cfg(all(target_arch = "x86_64", target_feature = "sha"))]
769        unsafe {
770            use core::arch::x86_64::*;
771            let a: __m128i = core::mem::transmute(self.0);
772            let b: __m128i = core::mem::transmute(o.0);
773            Self(core::mem::transmute(_mm_sha1msg2_epu32(a, b)))
774        }
775        #[cfg(not(all(target_arch = "x86_64", target_feature = "sha")))]
776        {
777            #[cfg(target_arch = "x86_64")]
778            {
779                if shani_available() {
780                    return unsafe { self.sha1msg2_hw(o) };
781                }
782            }
783            Self(crate::sha_ops::sha1msg2(self.0, o.0))
784        }
785    }
786    /// Fold the next round constant E (`sha1nexte`).
787    #[inline(always)]
788    pub fn sha1nexte(self, o: Self) -> Self {
789        #[cfg(all(target_arch = "x86_64", target_feature = "sha"))]
790        unsafe {
791            use core::arch::x86_64::*;
792            let a: __m128i = core::mem::transmute(self.0);
793            let b: __m128i = core::mem::transmute(o.0);
794            Self(core::mem::transmute(_mm_sha1nexte_epu32(a, b)))
795        }
796        #[cfg(not(all(target_arch = "x86_64", target_feature = "sha")))]
797        {
798            #[cfg(target_arch = "x86_64")]
799            {
800                if shani_available() {
801                    return unsafe { self.sha1nexte_hw(o) };
802                }
803            }
804            Self(crate::sha_ops::sha1nexte(self.0, o.0))
805        }
806    }
807
808    #[cfg(all(target_arch = "x86_64", not(target_feature = "sha")))]
809    #[target_feature(enable = "sha,sse2,sse4.1")]
810    unsafe fn sha1rnds4_hw(self, msg: Self, func: u32) -> Self {
811        use std::arch::x86_64::*;
812        let a = _mm_loadu_si128(self.0.as_ptr() as *const __m128i);
813        let b = _mm_loadu_si128(msg.0.as_ptr() as *const __m128i);
814        // The round-function selector is a compile-time immediate, so branch to the four forms.
815        let r = match func & 3 {
816            0 => _mm_sha1rnds4_epu32(a, b, 0),
817            1 => _mm_sha1rnds4_epu32(a, b, 1),
818            2 => _mm_sha1rnds4_epu32(a, b, 2),
819            _ => _mm_sha1rnds4_epu32(a, b, 3),
820        };
821        let mut out = [0u32; 4];
822        _mm_storeu_si128(out.as_mut_ptr() as *mut __m128i, r);
823        Self(out)
824    }
825
826    #[cfg(all(target_arch = "x86_64", not(target_feature = "sha")))]
827    #[target_feature(enable = "sha,sse2,ssse3,sse4.1")]
828    unsafe fn sha1msg1_hw(self, o: Self) -> Self {
829        use std::arch::x86_64::*;
830        let a = _mm_loadu_si128(self.0.as_ptr() as *const __m128i);
831        let b = _mm_loadu_si128(o.0.as_ptr() as *const __m128i);
832        let mut out = [0u32; 4];
833        _mm_storeu_si128(out.as_mut_ptr() as *mut __m128i, _mm_sha1msg1_epu32(a, b));
834        Self(out)
835    }
836
837    #[cfg(all(target_arch = "x86_64", not(target_feature = "sha")))]
838    #[target_feature(enable = "sha,sse2,ssse3,sse4.1")]
839    unsafe fn sha1msg2_hw(self, o: Self) -> Self {
840        use std::arch::x86_64::*;
841        let a = _mm_loadu_si128(self.0.as_ptr() as *const __m128i);
842        let b = _mm_loadu_si128(o.0.as_ptr() as *const __m128i);
843        let mut out = [0u32; 4];
844        _mm_storeu_si128(out.as_mut_ptr() as *mut __m128i, _mm_sha1msg2_epu32(a, b));
845        Self(out)
846    }
847
848    #[cfg(all(target_arch = "x86_64", not(target_feature = "sha")))]
849    #[target_feature(enable = "sha,sse2,sse4.1")]
850    unsafe fn sha1nexte_hw(self, o: Self) -> Self {
851        use std::arch::x86_64::*;
852        let a = _mm_loadu_si128(self.0.as_ptr() as *const __m128i);
853        let b = _mm_loadu_si128(o.0.as_ptr() as *const __m128i);
854        let mut out = [0u32; 4];
855        _mm_storeu_si128(out.as_mut_ptr() as *mut __m128i, _mm_sha1nexte_epu32(a, b));
856        Self(out)
857    }
858
859    /// Lane-wise wrapping add (`_mm_add_epi32`) — folds the round E into the message dwords and the
860    /// per-block state back into the running hash. LLVM lowers the four-lane loop to one `paddd`.
861    #[inline]
862    pub fn add(self, o: Self) -> Self {
863        let mut out = [0u32; 4];
864        for i in 0..4 {
865            out[i] = self.0[i].wrapping_add(o.0[i]);
866        }
867        Self(out)
868    }
869
870    /// Lane-wise XOR (`_mm_xor_si128`) — the message-schedule W_t ⊕ W_{t-2} coupling; lowers to `pxor`.
871    #[inline]
872    pub fn bitxor(self, o: Self) -> Self {
873        let mut out = [0u32; 4];
874        for i in 0..4 {
875            out[i] = self.0[i] ^ o.0[i];
876        }
877        Self(out)
878    }
879}
880
881impl ::core::ops::Add for Lanes4Word32 {
882    type Output = Self;
883    #[inline]
884    fn add(self, o: Self) -> Self {
885        Lanes4Word32::add(self, o)
886    }
887}
888impl ::core::ops::BitXor for Lanes4Word32 {
889    type Output = Self;
890    #[inline]
891    fn bitxor(self, o: Self) -> Self {
892        Lanes4Word32::bitxor(self, o)
893    }
894}
895
896/// Sixteen `Word8` lanes = one 128-bit register (`__m128i`) — the BYTE-SHUFFLE carrier for SIMD text
897/// codecs (hex encode/decode). Its vocabulary is `shuffle` (`pshufb`, a 16-entry byte LUT), byte AND,
898/// per-byte shift, and the two byte interleaves — so a hex codec WRITTEN in Logos over it compiles to
899/// the `pshufb` sequence (AOT, when SSSE3 is statically enabled) and runs the byte-identical scalar
900/// spec on the interpreter. Lane `i` is byte `i` (index 0 low), matching `_mm_loadu_si128`.
901#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
902#[repr(C, align(16))]
903pub struct Lanes16Word8(pub [u8; 16]);
904
905impl Lanes16Word8 {
906    /// The number of lanes.
907    pub const LANES: usize = 16;
908
909    /// Broadcast one byte into all sixteen lanes.
910    #[inline]
911    pub const fn splat(x: u8) -> Self {
912        Self([x; 16])
913    }
914
915    /// Pack the first sixteen bytes of a slice (shorter slices zero-fill).
916    #[inline]
917    pub fn from_bytes(s: &[u8]) -> Self {
918        let mut a = [0u8; 16];
919        for (i, b) in s.iter().take(16).enumerate() {
920            a[i] = *b;
921        }
922        Self(a)
923    }
924
925    /// The sixteen lane bytes.
926    #[inline]
927    pub fn to_bytes(self) -> [u8; 16] {
928        self.0
929    }
930
931    /// Lane `i` (0-based).
932    #[inline]
933    pub fn lane(self, i: usize) -> u8 {
934        self.0[i]
935    }
936
937    /// Byte shuffle (`pshufb`): `out[i] = if idx[i] & 0x80 { 0 } else { self[idx[i] & 0x0f] }` — a
938    /// 16-entry byte lookup, the core of SIMD hex codecs (nibble → hex char; hyphen strip).
939    #[inline]
940    pub fn shuffle(self, idx: Self) -> Self {
941        #[cfg(all(target_arch = "x86_64", target_feature = "ssse3"))]
942        unsafe {
943            use core::arch::x86_64::*;
944            let a: __m128i = core::mem::transmute(self.0);
945            let b: __m128i = core::mem::transmute(idx.0);
946            Self(core::mem::transmute(_mm_shuffle_epi8(a, b)))
947        }
948        #[cfg(not(all(target_arch = "x86_64", target_feature = "ssse3")))]
949        {
950            let mut r = [0u8; 16];
951            for i in 0..16 {
952                let x = idx.0[i];
953                r[i] = if x & 0x80 != 0 { 0 } else { self.0[(x & 0x0f) as usize] };
954            }
955            Self(r)
956        }
957    }
958
959    /// Lane-wise AND (`_mm_and_si128`) — the low-nibble mask; auto-vectorizes to `pand`.
960    #[inline]
961    pub fn bitand(self, o: Self) -> Self {
962        let mut r = [0u8; 16];
963        for i in 0..16 {
964            r[i] = self.0[i] & o.0[i];
965        }
966        Self(r)
967    }
968
969    /// Per-byte logical shift right by `n` — the high-nibble extract (`v >> 4`).
970    #[inline]
971    pub fn shr_bytes(self, n: u32) -> Self {
972        let mut r = [0u8; 16];
973        for i in 0..16 {
974            r[i] = self.0[i] >> n;
975        }
976        Self(r)
977    }
978
979    /// Interleave the low eight bytes (`_mm_unpacklo_epi8`): `[a0,b0,a1,b1,…,a7,b7]`.
980    #[inline]
981    pub fn interleave_lo(self, o: Self) -> Self {
982        #[cfg(all(target_arch = "x86_64", target_feature = "ssse3"))]
983        unsafe {
984            use core::arch::x86_64::*;
985            let a: __m128i = core::mem::transmute(self.0);
986            let b: __m128i = core::mem::transmute(o.0);
987            Self(core::mem::transmute(_mm_unpacklo_epi8(a, b)))
988        }
989        #[cfg(not(all(target_arch = "x86_64", target_feature = "ssse3")))]
990        {
991            let mut r = [0u8; 16];
992            for i in 0..8 {
993                r[2 * i] = self.0[i];
994                r[2 * i + 1] = o.0[i];
995            }
996            Self(r)
997        }
998    }
999
1000    /// Interleave the high eight bytes (`_mm_unpackhi_epi8`): `[a8,b8,…,a15,b15]`.
1001    #[inline]
1002    pub fn interleave_hi(self, o: Self) -> Self {
1003        #[cfg(all(target_arch = "x86_64", target_feature = "ssse3"))]
1004        unsafe {
1005            use core::arch::x86_64::*;
1006            let a: __m128i = core::mem::transmute(self.0);
1007            let b: __m128i = core::mem::transmute(o.0);
1008            Self(core::mem::transmute(_mm_unpackhi_epi8(a, b)))
1009        }
1010        #[cfg(not(all(target_arch = "x86_64", target_feature = "ssse3")))]
1011        {
1012            let mut r = [0u8; 16];
1013            for i in 0..8 {
1014                r[2 * i] = self.0[8 + i];
1015                r[2 * i + 1] = o.0[8 + i];
1016            }
1017            Self(r)
1018        }
1019    }
1020
1021    /// Per-byte wrapping add (`_mm_add_epi8`) — the ASCII→nibble decode (`lo + 9·hibit`).
1022    #[inline]
1023    pub fn byte_add(self, o: Self) -> Self {
1024        let mut r = [0u8; 16];
1025        for i in 0..16 {
1026            r[i] = self.0[i].wrapping_add(o.0[i]);
1027        }
1028        Self(r)
1029    }
1030
1031    /// Multiply-add adjacent byte pairs (`pmaddubsw`): `self` unsigned × `o` signed, summing pairs into
1032    /// eight saturating `i16` lanes stored little-endian. Weights `[16,1,…]` fuse each nibble pair into
1033    /// the decoded byte (hex parse). Result carries 8 `u16` in its 16 bytes; narrow with `packus`.
1034    #[inline]
1035    pub fn maddubs(self, o: Self) -> Self {
1036        let mut r = [0u8; 16];
1037        for i in 0..8 {
1038            let a0 = self.0[2 * i] as i32;
1039            let a1 = self.0[2 * i + 1] as i32;
1040            let b0 = o.0[2 * i] as i8 as i32;
1041            let b1 = o.0[2 * i + 1] as i8 as i32;
1042            let s = (a0 * b0 + a1 * b1).clamp(-32768, 32767) as i16 as u16;
1043            r[2 * i] = (s & 0xff) as u8;
1044            r[2 * i + 1] = (s >> 8) as u8;
1045        }
1046        Self(r)
1047    }
1048
1049    /// Pack two `8×i16` vectors to `16×u8` with unsigned saturation (`packuswb`): `self`'s eight 16-bit
1050    /// lanes → bytes 0–7, `o`'s → bytes 8–15. Narrows the `maddubs` output back to bytes.
1051    #[inline]
1052    pub fn packus(self, o: Self) -> Self {
1053        let mut r = [0u8; 16];
1054        for i in 0..8 {
1055            let a = (self.0[2 * i] as u16 | (self.0[2 * i + 1] as u16) << 8) as i16;
1056            r[i] = a.clamp(0, 255) as u8;
1057            let b = (o.0[2 * i] as u16 | (o.0[2 * i + 1] as u16) << 8) as i16;
1058            r[8 + i] = b.clamp(0, 255) as u8;
1059        }
1060        Self(r)
1061    }
1062}
1063
1064impl ::core::ops::BitAnd for Lanes16Word8 {
1065    type Output = Self;
1066    #[inline]
1067    fn bitand(self, o: Self) -> Self {
1068        Lanes16Word8::bitand(self, o)
1069    }
1070}
1071
1072/// True when the CPU has the SHA + SSE4.1 (+ SSSE3) instructions the SHA-NI lane ops use; `std` caches
1073/// the detection so this is a cheap load after the first call. Only the portable build path (SHA not
1074/// statically enabled) runtime-detects; a `target-feature=+sha` build skips it entirely.
1075#[cfg(all(target_arch = "x86_64", not(target_feature = "sha")))]
1076#[inline]
1077fn shani_available() -> bool {
1078    std::is_x86_feature_detected!("sha")
1079        && std::is_x86_feature_detected!("ssse3")
1080        && std::is_x86_feature_detected!("sse4.1")
1081}
1082
1083/// Four lanes of `Word64` (one 256-bit SIMD register) — the Poly1305 accumulator lane vector.
1084/// Carries `[u64; 4]`; ops have an AVX2 fast path proven byte-identical to the scalar lanes.
1085#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
1086#[repr(C, align(32))]
1087pub struct Lanes4Word64(pub [u64; 4]);
1088
1089impl Lanes4Word64 {
1090    /// The number of lanes.
1091    pub const LANES: usize = 4;
1092
1093    /// Pack the first four `Word64`s of a slice into a lane vector (shorter slices zero-fill).
1094    #[inline]
1095    pub fn from_words(s: &[Word64]) -> Self {
1096        let mut a = [0u64; 4];
1097        for (i, w) in s.iter().take(4).enumerate() {
1098            a[i] = w.0;
1099        }
1100        Self(a)
1101    }
1102
1103    /// The lanes as four `Word64`s.
1104    #[inline]
1105    pub fn to_words(self) -> [Word64; 4] {
1106        self.0.map(Word64)
1107    }
1108
1109    /// Lane `i` (0-based) as a `Word64`.
1110    #[inline]
1111    pub fn lane(self, i: usize) -> Word64 {
1112        Word64(self.0[i])
1113    }
1114
1115    /// The horizontal sum of the four lanes (wrapping in ℤ/2⁶⁴) — combines the per-lane partial
1116    /// products in a 4-way Poly1305 multiply. Reads the scalar array; no SIMD needed.
1117    #[inline]
1118    pub fn hsum(self) -> u64 {
1119        self.0[0]
1120            .wrapping_add(self.0[1])
1121            .wrapping_add(self.0[2])
1122            .wrapping_add(self.0[3])
1123    }
1124
1125    /// Lane-wise wrapping add in ℤ/2⁶⁴ (`vpaddq`). `#[inline(always)]` + compile-time
1126    /// `cfg(target_feature="avx2")` (no runtime `is_x86_feature_detected` branch, no `#[target_feature]`
1127    /// call boundary) so the 4-way Poly1305 accumulator stays register-resident under `+avx2`.
1128    #[inline(always)]
1129    pub fn add(self, o: Self) -> Self {
1130        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1131        unsafe {
1132            use std::arch::x86_64::*;
1133            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1134            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
1135            let mut r = [0u64; 4];
1136            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_add_epi64(a, b));
1137            return Self(r);
1138        }
1139        #[allow(unreachable_code)]
1140        {
1141            let mut r = [0u64; 4];
1142            for i in 0..4 {
1143                r[i] = self.0[i].wrapping_add(o.0[i]);
1144            }
1145            Self(r)
1146        }
1147    }
1148
1149    #[cfg(test)]
1150    #[inline]
1151    fn add_scalar(self, o: Self) -> Self {
1152        let mut r = [0u64; 4];
1153        for i in 0..4 {
1154            r[i] = self.0[i].wrapping_add(o.0[i]);
1155        }
1156        Self(r)
1157    }
1158
1159    /// Lane-wise widening multiply of the low 32 bits: `(aₗₒ·bₗₒ)` per lane → a 64-bit product
1160    /// (`vpmuludq`). `#[inline(always)]` + compile-time `cfg(target_feature="avx2")` so the Poly1305
1161    /// 4-way limb multiply inlines register-resident under `+avx2`.
1162    #[inline(always)]
1163    pub fn mul_lo32_wide(self, o: Self) -> Self {
1164        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1165        unsafe {
1166            use std::arch::x86_64::*;
1167            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1168            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
1169            let mut r = [0u64; 4];
1170            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_mul_epu32(a, b));
1171            return Self(r);
1172        }
1173        #[allow(unreachable_code)]
1174        {
1175            let mut r = [0u64; 4];
1176            for i in 0..4 {
1177                r[i] = (self.0[i] & 0xffff_ffff) * (o.0[i] & 0xffff_ffff);
1178            }
1179            Self(r)
1180        }
1181    }
1182
1183    #[cfg(test)]
1184    #[inline]
1185    fn mul_lo32_wide_scalar(self, o: Self) -> Self {
1186        let mut r = [0u64; 4];
1187        for i in 0..4 {
1188            r[i] = (self.0[i] & 0xffff_ffff) * (o.0[i] & 0xffff_ffff);
1189        }
1190        Self(r)
1191    }
1192
1193    // ── Bitwise ops — the 4-way Keccak-f[1600] substrate (θ/ρ/π/χ/ι over 4 independent states) ─────
1194    // `#[inline(always)]` with COMPILE-TIME `cfg(target_feature="avx2")` intrinsics (NOT a separate
1195    // `#[target_feature]` fn) so they inline straight into a hot kernel (Keccak's ~600 ops) with the
1196    // vectors register-resident — no per-op call boundary, no per-op runtime detect. This is what
1197    // closes the ~20× gap of a `#[target_feature]`-per-op lane Keccak (measured). A build with
1198    // `-C target-feature=+avx2` / `target-cpu=native` takes the fast path; otherwise portable scalar.
1199
1200    /// Broadcast one `u64` into all four lanes (Keccak's ι round-constant XOR is a splat).
1201    #[inline(always)]
1202    pub fn splat(x: u64) -> Self {
1203        Self([x; 4])
1204    }
1205
1206    /// Lane-wise XOR (`vpxor`) — θ column parity, χ, and ι.
1207    #[inline(always)]
1208    pub fn bitxor(self, o: Self) -> Self {
1209        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1210        unsafe {
1211            use std::arch::x86_64::*;
1212            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1213            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
1214            let mut r = [0u64; 4];
1215            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_xor_si256(a, b));
1216            return Self(r);
1217        }
1218        #[allow(unreachable_code)]
1219        Self([self.0[0] ^ o.0[0], self.0[1] ^ o.0[1], self.0[2] ^ o.0[2], self.0[3] ^ o.0[3]])
1220    }
1221
1222    /// Lane-wise AND (`vpand`).
1223    #[inline(always)]
1224    pub fn and(self, o: Self) -> Self {
1225        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1226        unsafe {
1227            use std::arch::x86_64::*;
1228            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1229            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
1230            let mut r = [0u64; 4];
1231            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_and_si256(a, b));
1232            return Self(r);
1233        }
1234        #[allow(unreachable_code)]
1235        Self([self.0[0] & o.0[0], self.0[1] & o.0[1], self.0[2] & o.0[2], self.0[3] & o.0[3]])
1236    }
1237
1238    /// Lane-wise AND-NOT `(¬self) & o` (`vpandn`) — Keccak's χ nonlinearity `¬bᵢ₊₁ ∧ bᵢ₊₂` in one op.
1239    #[inline(always)]
1240    pub fn andnot(self, o: Self) -> Self {
1241        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1242        unsafe {
1243            use std::arch::x86_64::*;
1244            let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1245            let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
1246            let mut r = [0u64; 4];
1247            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_andnot_si256(a, b));
1248            return Self(r);
1249        }
1250        #[allow(unreachable_code)]
1251        Self([!self.0[0] & o.0[0], !self.0[1] & o.0[1], !self.0[2] & o.0[2], !self.0[3] & o.0[3]])
1252    }
1253
1254    /// Lane-wise left rotation by `n` (mod 64) — Keccak's ρ offsets and θ's D term. `(x<<n)|(x>>(64−n))`
1255    /// via `vpsllq`/`vpsrlq` (n = 0 is the identity — the `64−n = 64` shift zeroes).
1256    #[inline(always)]
1257    pub fn rotl(self, n: u32) -> Self {
1258        let n = n % 64;
1259        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1260        unsafe {
1261            use std::arch::x86_64::*;
1262            if n == 0 {
1263                return self;
1264            }
1265            let x = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1266            let l = _mm256_sll_epi64(x, _mm_cvtsi32_si128(n as i32));
1267            let r_sh = _mm256_srl_epi64(x, _mm_cvtsi32_si128((64 - n) as i32));
1268            let mut r = [0u64; 4];
1269            _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, _mm256_or_si256(l, r_sh));
1270            return Self(r);
1271        }
1272        #[allow(unreachable_code)]
1273        {
1274            let mut r = [0u64; 4];
1275            for i in 0..4 {
1276                r[i] = self.0[i].rotate_left(n);
1277            }
1278            Self(r)
1279        }
1280    }
1281}
1282
1283impl ::core::ops::Add for Lanes4Word64 {
1284    type Output = Self;
1285    #[inline]
1286    fn add(self, o: Self) -> Self {
1287        Lanes4Word64::add(self, o)
1288    }
1289}
1290
1291impl ::core::ops::BitXor for Lanes4Word64 {
1292    type Output = Self;
1293    #[inline]
1294    fn bitxor(self, o: Self) -> Self {
1295        Lanes4Word64::bitxor(self, o)
1296    }
1297}
1298
1299impl ::core::ops::BitAnd for Lanes4Word64 {
1300    type Output = Self;
1301    #[inline]
1302    fn bitand(self, o: Self) -> Self {
1303        Lanes4Word64::and(self, o)
1304    }
1305}
1306
1307/// Sixteen lanes of `Word16` (one 256-bit SIMD register) — the NTT coefficient lane vector. Carries
1308/// `[u16; 16]`; the multiply-high is the SIGNED `_mm256_mulhi_epi16` (the Montgomery butterfly's
1309/// `mulhi`). Ops have an AVX2 fast path proven byte-identical to the scalar lanes.
1310#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
1311#[repr(C, align(32))]
1312pub struct Lanes16Word16(pub [u16; 16]);
1313
1314/// The AVX2 256-bit lane vector — the real `__m256i` intrinsic on x86_64, a placeholder elsewhere.
1315/// The SIMD fast path is `#[cfg(target_arch = "x86_64")]`-compiled out on other targets (e.g. the
1316/// `wasm32` web build), so the placeholder is never constructed; this keeps the lane ops' signatures
1317/// and closures arch-independent while the scalar fallback runs everywhere.
1318#[cfg(target_arch = "x86_64")]
1319type Avx256 = std::arch::x86_64::__m256i;
1320#[cfg(not(target_arch = "x86_64"))]
1321type Avx256 = [u16; 16];
1322
1323// The four AVX2 lane butterflies, isolated behind safe wrappers so the lane methods' closures stay
1324// arch-independent. On x86_64 each is the intrinsic (the caller `binop` runtime-detects avx2 before
1325// invoking it); on other targets the SIMD path is `#[cfg]`-compiled out, so these are never called.
1326#[cfg(target_arch = "x86_64")]
1327#[inline]
1328fn simd_add(a: Avx256, b: Avx256) -> Avx256 {
1329    unsafe { std::arch::x86_64::_mm256_add_epi16(a, b) }
1330}
1331#[cfg(target_arch = "x86_64")]
1332#[inline]
1333fn simd_sub(a: Avx256, b: Avx256) -> Avx256 {
1334    unsafe { std::arch::x86_64::_mm256_sub_epi16(a, b) }
1335}
1336#[cfg(target_arch = "x86_64")]
1337#[inline]
1338fn simd_mullo(a: Avx256, b: Avx256) -> Avx256 {
1339    unsafe { std::arch::x86_64::_mm256_mullo_epi16(a, b) }
1340}
1341#[cfg(target_arch = "x86_64")]
1342#[inline]
1343fn simd_mulhi(a: Avx256, b: Avx256) -> Avx256 {
1344    unsafe { std::arch::x86_64::_mm256_mulhi_epi16(a, b) }
1345}
1346#[cfg(not(target_arch = "x86_64"))]
1347#[inline]
1348fn simd_add(_a: Avx256, _b: Avx256) -> Avx256 {
1349    unreachable!("SIMD lane path is x86_64-only")
1350}
1351#[cfg(not(target_arch = "x86_64"))]
1352#[inline]
1353fn simd_sub(_a: Avx256, _b: Avx256) -> Avx256 {
1354    unreachable!("SIMD lane path is x86_64-only")
1355}
1356#[cfg(not(target_arch = "x86_64"))]
1357#[inline]
1358fn simd_mullo(_a: Avx256, _b: Avx256) -> Avx256 {
1359    unreachable!("SIMD lane path is x86_64-only")
1360}
1361#[cfg(not(target_arch = "x86_64"))]
1362#[inline]
1363fn simd_mulhi(_a: Avx256, _b: Avx256) -> Avx256 {
1364    unreachable!("SIMD lane path is x86_64-only")
1365}
1366
1367impl Lanes16Word16 {
1368    /// The number of lanes.
1369    pub const LANES: usize = 16;
1370
1371    /// Broadcast one value into all sixteen lanes.
1372    #[inline]
1373    pub const fn splat(x: u16) -> Self {
1374        Self([x; 16])
1375    }
1376
1377    /// Pack the first sixteen `Word16`s of a slice into a lane vector (shorter slices zero-fill).
1378    #[inline]
1379    pub fn from_words(s: &[Word16]) -> Self {
1380        let mut a = [0u16; 16];
1381        for (i, w) in s.iter().take(16).enumerate() {
1382            a[i] = w.0;
1383        }
1384        Self(a)
1385    }
1386
1387    /// The lanes as sixteen `Word16`s.
1388    #[inline]
1389    pub fn to_words(self) -> [Word16; 16] {
1390        self.0.map(Word16)
1391    }
1392
1393    /// Lane `i` (0-based) as a `Word16`.
1394    #[inline]
1395    pub fn lane(self, i: usize) -> Word16 {
1396        Word16(self.0[i])
1397    }
1398
1399    /// Lane-wise wrapping add in ℤ/2¹⁶ (`vpaddw`).
1400    #[inline(always)]
1401    pub fn add(self, o: Self) -> Self {
1402        self.binop(o, |a, b| a.wrapping_add(b), |a, b| simd_add(a, b))
1403    }
1404
1405    /// Lane-wise wrapping subtract in ℤ/2¹⁶ (`vpsubw`).
1406    #[inline(always)]
1407    pub fn sub(self, o: Self) -> Self {
1408        self.binop(o, |a, b| a.wrapping_sub(b), |a, b| simd_sub(a, b))
1409    }
1410
1411    /// Lane-wise low 16 bits of the product (`vpmullw`) — interpretation-independent.
1412    #[inline(always)]
1413    pub fn mullo(self, o: Self) -> Self {
1414        self.binop(o, |a, b| a.wrapping_mul(b), |a, b| simd_mullo(a, b))
1415    }
1416
1417    /// Lane-wise high 16 bits of the SIGNED product (`vpmulhw`) — the Montgomery butterfly's `mulhi`.
1418    #[inline(always)]
1419    pub fn mulhi(self, o: Self) -> Self {
1420        self.binop(
1421            o,
1422            |a, b| (((a as i16 as i32) * (b as i16 as i32)) >> 16) as u16,
1423            |a, b| simd_mulhi(a, b),
1424        )
1425    }
1426
1427    /// Broadcast each `2h`-block's low `h` lanes into both of its halves — the within-vector NTT
1428    /// butterfly's source-low duplication, at stride `h ∈ {8,4,2}`. `h=8`→`vperm2i128(v,v,0x00)`;
1429    /// `h=4`→`vpshufd(v,0x44)`; `h=2`→`vpshufd(v,0xA0)` (all per-128-bit-lane, so the multiple
1430    /// blocks packed in one register are handled at once).
1431    #[inline(always)]
1432    pub fn ntt_bcast_lo(self, h: usize) -> Self {
1433        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1434        {
1435            use std::arch::x86_64::*;
1436            let mut r = [0u16; 16];
1437            unsafe {
1438                let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1439                let v = match h {
1440                    8 => _mm256_permute2x128_si256::<0x00>(a, a),
1441                    4 => _mm256_shuffle_epi32::<0x44>(a),
1442                    2 => _mm256_shuffle_epi32::<0xA0>(a),
1443                    _ => unreachable!("within-vector NTT stride is 8/4/2"),
1444                };
1445                _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, v);
1446            }
1447            return Self(r);
1448        }
1449        #[allow(unreachable_code)]
1450        Self(core::array::from_fn(|i| self.0[(i / (2 * h)) * (2 * h) + (i % (2 * h)) % h]))
1451    }
1452
1453    /// Broadcast each `2h`-block's high `h` lanes into both of its halves — the source-high
1454    /// duplication. `h=8`→`vperm2i128(v,v,0x11)`; `h=4`→`vpshufd(v,0xEE)`; `h=2`→`vpshufd(v,0xF5)`.
1455    #[inline(always)]
1456    pub fn ntt_bcast_hi(self, h: usize) -> Self {
1457        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1458        {
1459            use std::arch::x86_64::*;
1460            let mut r = [0u16; 16];
1461            unsafe {
1462                let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1463                let v = match h {
1464                    8 => _mm256_permute2x128_si256::<0x11>(a, a),
1465                    4 => _mm256_shuffle_epi32::<0xEE>(a),
1466                    2 => _mm256_shuffle_epi32::<0xF5>(a),
1467                    _ => unreachable!("within-vector NTT stride is 8/4/2"),
1468                };
1469                _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, v);
1470            }
1471            return Self(r);
1472        }
1473        #[allow(unreachable_code)]
1474        Self(core::array::from_fn(|i| self.0[(i / (2 * h)) * (2 * h) + h + (i % (2 * h)) % h]))
1475    }
1476
1477    /// Recombine the `+`/`−` halves of the within-vector butterfly: each `2h`-block's low `h` lanes
1478    /// from `self`, high `h` from `o`. `h=8`→`vperm2i128(a,b,0x30)`; `h=4`→`vpblendd(a,b,0xCC)`;
1479    /// `h=2`→`vpblendd(a,b,0xAA)`.
1480    #[inline(always)]
1481    pub fn ntt_blend(self, o: Self, h: usize) -> Self {
1482        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1483        {
1484            use std::arch::x86_64::*;
1485            let mut r = [0u16; 16];
1486            unsafe {
1487                let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1488                let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
1489                let v = match h {
1490                    8 => _mm256_permute2x128_si256::<0x30>(a, b),
1491                    4 => _mm256_blend_epi32::<0xCC>(a, b),
1492                    2 => _mm256_blend_epi32::<0xAA>(a, b),
1493                    _ => unreachable!("within-vector NTT stride is 8/4/2"),
1494                };
1495                _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, v);
1496            }
1497            return Self(r);
1498        }
1499        #[allow(unreachable_code)]
1500        Self(core::array::from_fn(|i| if (i % (2 * h)) < h { self.0[i] } else { o.0[i] }))
1501    }
1502
1503    /// Shared add/sub/mullo/mulhi dispatcher. `#[inline(always)]` + compile-time
1504    /// `cfg(target_feature="avx2")` (NOT a runtime `is_x86_feature_detected` branch) so a chain of
1505    /// NTT butterflies inlines register-resident under `+avx2`: LLVM forwards each op's store-to-`r`
1506    /// into the next op's load and keeps the coefficients in one YMM register. A runtime branch is
1507    /// opaque to that forwarding — it forces a load/store round-trip per op (the ~20× lane pessimization).
1508    #[inline(always)]
1509    fn binop(
1510        self,
1511        o: Self,
1512        scalar: impl Fn(u16, u16) -> u16,
1513        #[allow(unused_variables)] vector: impl Fn(Avx256, Avx256) -> Avx256,
1514    ) -> Self {
1515        #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
1516        {
1517            use std::arch::x86_64::*;
1518            let mut r = [0u16; 16];
1519            unsafe {
1520                let a = _mm256_loadu_si256(self.0.as_ptr() as *const __m256i);
1521                let b = _mm256_loadu_si256(o.0.as_ptr() as *const __m256i);
1522                _mm256_storeu_si256(r.as_mut_ptr() as *mut __m256i, vector(a, b));
1523            }
1524            return Self(r);
1525        }
1526        #[allow(unreachable_code)]
1527        Self(core::array::from_fn(|i| scalar(self.0[i], o.0[i])))
1528    }
1529}
1530
1531macro_rules! lanes16_op {
1532    ($trait:ident, $tm:ident, $m:ident) => {
1533        impl ::core::ops::$trait for Lanes16Word16 {
1534            type Output = Self;
1535            #[inline]
1536            fn $tm(self, o: Self) -> Self {
1537                Lanes16Word16::$m(self, o)
1538            }
1539        }
1540    };
1541}
1542lanes16_op!(Add, add, add);
1543lanes16_op!(Sub, sub, sub);
1544lanes16_op!(Mul, mul, mullo);
1545
1546/// A SIMD lane vector of any supported lane config — the single runtime carrier the interpreter/VM
1547/// match on, mirroring [`WordVal`]. Binary ops require the same config; a mismatch returns `None`.
1548#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
1549pub enum LanesVal {
1550    /// 8 lanes of `Word32` (one `__m256i`).
1551    L8W32(Lanes8Word32),
1552    /// 4 lanes of `Word64` (one `__m256i`) — the Poly1305 accumulator.
1553    L4W64(Lanes4Word64),
1554    /// 16 lanes of `Word16` (one `__m256i`) — NTT coefficients.
1555    L16W16(Lanes16Word16),
1556    /// 4 lanes of `Word32` (one `__m128i`) — the SHA-1 state/message register (SHA-NI ops).
1557    L4W32(Lanes4Word32),
1558    /// 16 lanes of `Word8` (one `__m128i`) — the byte-shuffle register for SIMD text codecs.
1559    L16W8(Lanes16Word8),
1560}
1561
1562impl LanesVal {
1563    /// The lane count.
1564    #[inline]
1565    pub const fn lanes(self) -> usize {
1566        match self {
1567            LanesVal::L8W32(_) => 8,
1568            LanesVal::L4W64(_) => 4,
1569            LanesVal::L16W16(_) => 16,
1570            LanesVal::L4W32(_) => 4,
1571            LanesVal::L16W8(_) => 16,
1572        }
1573    }
1574
1575    /// The lane-element bit width.
1576    #[inline]
1577    pub const fn elem_bits(self) -> u32 {
1578        match self {
1579            LanesVal::L8W32(_) => 32,
1580            LanesVal::L4W64(_) => 64,
1581            LanesVal::L16W16(_) => 16,
1582            LanesVal::L4W32(_) => 32,
1583            LanesVal::L16W8(_) => 8,
1584        }
1585    }
1586
1587    /// The Logos type name.
1588    #[inline]
1589    pub const fn type_name(self) -> &'static str {
1590        match self {
1591            LanesVal::L8W32(_) => "Lanes8Word32",
1592            LanesVal::L4W64(_) => "Lanes4Word64",
1593            LanesVal::L16W16(_) => "Lanes16Word16",
1594            LanesVal::L4W32(_) => "Lanes4Word32",
1595            LanesVal::L16W8(_) => "Lanes16Word8",
1596        }
1597    }
1598
1599    /// Lane `i`'s value, zero-extended to `u64` (for unpacking back into a Seq and for display).
1600    #[inline]
1601    pub fn lane(self, i: usize) -> u64 {
1602        match self {
1603            LanesVal::L8W32(v) => v.lane(i).0 as u64,
1604            LanesVal::L4W64(v) => v.lane(i).0,
1605            LanesVal::L16W16(v) => v.lane(i).0 as u64,
1606            LanesVal::L4W32(v) => v.lane(i).0 as u64,
1607            LanesVal::L16W8(v) => v.lane(i) as u64,
1608        }
1609    }
1610
1611    /// Byte-shuffle vocabulary, defined only on the `L16W8` config (SIMD hex codec); `None` otherwise.
1612    #[inline]
1613    pub fn shuffle(self, idx: Self) -> Option<Self> {
1614        match (self, idx) {
1615            (LanesVal::L16W8(a), LanesVal::L16W8(b)) => Some(LanesVal::L16W8(a.shuffle(b))),
1616            _ => None,
1617        }
1618    }
1619    #[inline]
1620    pub fn byte_and(self, o: Self) -> Option<Self> {
1621        match (self, o) {
1622            (LanesVal::L16W8(a), LanesVal::L16W8(b)) => Some(LanesVal::L16W8(a.bitand(b))),
1623            _ => None,
1624        }
1625    }
1626    #[inline]
1627    pub fn shr_bytes(self, n: u32) -> Option<Self> {
1628        match self {
1629            LanesVal::L16W8(a) => Some(LanesVal::L16W8(a.shr_bytes(n))),
1630            _ => None,
1631        }
1632    }
1633    #[inline]
1634    pub fn interleave_lo(self, o: Self) -> Option<Self> {
1635        match (self, o) {
1636            (LanesVal::L16W8(a), LanesVal::L16W8(b)) => Some(LanesVal::L16W8(a.interleave_lo(b))),
1637            _ => None,
1638        }
1639    }
1640    #[inline]
1641    pub fn interleave_hi(self, o: Self) -> Option<Self> {
1642        match (self, o) {
1643            (LanesVal::L16W8(a), LanesVal::L16W8(b)) => Some(LanesVal::L16W8(a.interleave_hi(b))),
1644            _ => None,
1645        }
1646    }
1647    #[inline]
1648    pub fn byte_add(self, o: Self) -> Option<Self> {
1649        match (self, o) {
1650            (LanesVal::L16W8(a), LanesVal::L16W8(b)) => Some(LanesVal::L16W8(a.byte_add(b))),
1651            _ => None,
1652        }
1653    }
1654    #[inline]
1655    pub fn maddubs_bytes(self, o: Self) -> Option<Self> {
1656        match (self, o) {
1657            (LanesVal::L16W8(a), LanesVal::L16W8(b)) => Some(LanesVal::L16W8(a.maddubs(b))),
1658            _ => None,
1659        }
1660    }
1661    #[inline]
1662    pub fn packus_bytes(self, o: Self) -> Option<Self> {
1663        match (self, o) {
1664            (LanesVal::L16W8(a), LanesVal::L16W8(b)) => Some(LanesVal::L16W8(a.packus(b))),
1665            _ => None,
1666        }
1667    }
1668
1669    /// The four SHA-1 (SHA-NI) operations, defined only on the `L4W32` config; `None` otherwise.
1670    #[inline]
1671    pub fn sha1rnds4(self, msg: Self, func: u32) -> Option<Self> {
1672        match (self, msg) {
1673            (LanesVal::L4W32(a), LanesVal::L4W32(b)) => Some(LanesVal::L4W32(a.sha1rnds4(b, func))),
1674            _ => None,
1675        }
1676    }
1677    #[inline]
1678    pub fn sha1msg1(self, o: Self) -> Option<Self> {
1679        match (self, o) {
1680            (LanesVal::L4W32(a), LanesVal::L4W32(b)) => Some(LanesVal::L4W32(a.sha1msg1(b))),
1681            _ => None,
1682        }
1683    }
1684    #[inline]
1685    pub fn sha1msg2(self, o: Self) -> Option<Self> {
1686        match (self, o) {
1687            (LanesVal::L4W32(a), LanesVal::L4W32(b)) => Some(LanesVal::L4W32(a.sha1msg2(b))),
1688            _ => None,
1689        }
1690    }
1691    #[inline]
1692    pub fn sha1nexte(self, o: Self) -> Option<Self> {
1693        match (self, o) {
1694            (LanesVal::L4W32(a), LanesVal::L4W32(b)) => Some(LanesVal::L4W32(a.sha1nexte(b))),
1695            _ => None,
1696        }
1697    }
1698
1699    /// Lane-wise XOR; `None` on a config mismatch (or a config that does not define XOR).
1700    #[inline]
1701    pub fn bitxor(self, o: Self) -> Option<Self> {
1702        match (self, o) {
1703            (LanesVal::L8W32(a), LanesVal::L8W32(b)) => Some(LanesVal::L8W32(a.bitxor(b))),
1704            (LanesVal::L4W32(a), LanesVal::L4W32(b)) => Some(LanesVal::L4W32(a.bitxor(b))),
1705            _ => None,
1706        }
1707    }
1708
1709    /// Lane-wise AND (the MD5 F/G mixing) — `None` on a config mismatch or a config without AND.
1710    #[inline]
1711    pub fn bitand(self, o: Self) -> Option<Self> {
1712        match (self, o) {
1713            (LanesVal::L8W32(a), LanesVal::L8W32(b)) => Some(LanesVal::L8W32(a.bitand(b))),
1714            (LanesVal::L16W8(a), LanesVal::L16W8(b)) => Some(LanesVal::L16W8(a.bitand(b))),
1715            _ => None,
1716        }
1717    }
1718
1719    /// Lane-wise OR — `None` on a config mismatch or a config without OR.
1720    #[inline]
1721    pub fn bitor(self, o: Self) -> Option<Self> {
1722        match (self, o) {
1723            (LanesVal::L8W32(a), LanesVal::L8W32(b)) => Some(LanesVal::L8W32(a.bitor(b))),
1724            _ => None,
1725        }
1726    }
1727
1728    /// Lane-wise complement — `None` for a config without NOT.
1729    #[inline]
1730    pub fn lane_not(self) -> Option<Self> {
1731        match self {
1732            LanesVal::L8W32(a) => Some(LanesVal::L8W32(a.not())),
1733            _ => None,
1734        }
1735    }
1736
1737    /// Lane-wise wrapping add; `None` on a config mismatch.
1738    #[inline]
1739    pub fn add(self, o: Self) -> Option<Self> {
1740        match (self, o) {
1741            (LanesVal::L8W32(a), LanesVal::L8W32(b)) => Some(LanesVal::L8W32(a.add(b))),
1742            (LanesVal::L4W64(a), LanesVal::L4W64(b)) => Some(LanesVal::L4W64(a.add(b))),
1743            (LanesVal::L16W16(a), LanesVal::L16W16(b)) => Some(LanesVal::L16W16(a.add(b))),
1744            (LanesVal::L4W32(a), LanesVal::L4W32(b)) => Some(LanesVal::L4W32(a.add(b))),
1745            _ => None,
1746        }
1747    }
1748
1749    /// Lane-wise wrapping subtract; `None` on a config mismatch (the i16 NTT and the i32 ML-DSA NTT).
1750    #[inline]
1751    pub fn sub(self, o: Self) -> Option<Self> {
1752        match (self, o) {
1753            (LanesVal::L16W16(a), LanesVal::L16W16(b)) => Some(LanesVal::L16W16(a.sub(b))),
1754            (LanesVal::L8W32(a), LanesVal::L8W32(b)) => Some(LanesVal::L8W32(a.sub(b))),
1755            _ => None,
1756        }
1757    }
1758
1759    /// Lane-wise low-16 multiply (`vpmullw`); `None` unless `Word16`.
1760    #[inline]
1761    pub fn mullo(self, o: Self) -> Option<Self> {
1762        match (self, o) {
1763            (LanesVal::L16W16(a), LanesVal::L16W16(b)) => Some(LanesVal::L16W16(a.mullo(b))),
1764            _ => None,
1765        }
1766    }
1767
1768    /// Lane-wise SIGNED high-16 multiply (`vpmulhw`, the Montgomery `mulhi`); `None` unless `Word16`.
1769    #[inline]
1770    pub fn mulhi(self, o: Self) -> Option<Self> {
1771        match (self, o) {
1772            (LanesVal::L16W16(a), LanesVal::L16W16(b)) => Some(LanesVal::L16W16(a.mulhi(b))),
1773            _ => None,
1774        }
1775    }
1776
1777    /// Within-vector NTT source-low duplication at stride `h` (`vperm2i128`/`vpshufd`); `None`
1778    /// unless `Word16`.
1779    #[inline]
1780    pub fn ntt_bcast_lo(self, h: usize) -> Option<Self> {
1781        match self {
1782            LanesVal::L16W16(a) => Some(LanesVal::L16W16(a.ntt_bcast_lo(h))),
1783            LanesVal::L8W32(a) => Some(LanesVal::L8W32(a.ntt_bcast_lo(h))),
1784            _ => None,
1785        }
1786    }
1787
1788    /// Within-vector NTT source-high duplication at stride `h`; `None` unless `Word16`/`Word32`.
1789    #[inline]
1790    pub fn ntt_bcast_hi(self, h: usize) -> Option<Self> {
1791        match self {
1792            LanesVal::L16W16(a) => Some(LanesVal::L16W16(a.ntt_bcast_hi(h))),
1793            LanesVal::L8W32(a) => Some(LanesVal::L8W32(a.ntt_bcast_hi(h))),
1794            _ => None,
1795        }
1796    }
1797
1798    /// Within-vector NTT half-recombine at stride `h` (`vperm2i128`/`vpblendd`); `None` unless both
1799    /// are `Word16`/`Word32`.
1800    #[inline]
1801    pub fn ntt_blend(self, o: Self, h: usize) -> Option<Self> {
1802        match (self, o) {
1803            (LanesVal::L16W16(a), LanesVal::L16W16(b)) => Some(LanesVal::L16W16(a.ntt_blend(b, h))),
1804            (LanesVal::L8W32(a), LanesVal::L8W32(b)) => Some(LanesVal::L8W32(a.ntt_blend(b, h))),
1805            _ => None,
1806        }
1807    }
1808
1809    /// Lane-wise left rotation by `n`; `None` for a config that does not define rotation.
1810    #[inline]
1811    pub fn rotl(self, n: u32) -> Option<Self> {
1812        match self {
1813            LanesVal::L8W32(a) => Some(LanesVal::L8W32(a.rotl(n))),
1814            _ => None,
1815        }
1816    }
1817
1818    /// Lane-wise widening low-32 multiply (the Poly1305 4-way limb product); `None` unless `Word64`.
1819    #[inline]
1820    pub fn mul_lo32_wide(self, o: Self) -> Option<Self> {
1821        match (self, o) {
1822            (LanesVal::L4W64(a), LanesVal::L4W64(b)) => Some(LanesVal::L4W64(a.mul_lo32_wide(b))),
1823            _ => None,
1824        }
1825    }
1826
1827    /// The signed i32 Montgomery multiply (ML-DSA NTT butterfly), `q`/`qinv` broadcast; `None` unless
1828    /// all four are `Word32`.
1829    #[inline]
1830    pub fn montmul32(self, b: Self, q: Self, qinv: Self) -> Option<Self> {
1831        match (self, b, q, qinv) {
1832            (LanesVal::L8W32(a), LanesVal::L8W32(b), LanesVal::L8W32(q), LanesVal::L8W32(qi)) => {
1833                Some(LanesVal::L8W32(a.montmul32(b, q, qi)))
1834            }
1835            _ => None,
1836        }
1837    }
1838
1839    /// Horizontal sum of the lanes as an `Int`.
1840    #[inline]
1841    pub fn hsum(self) -> i64 {
1842        match self {
1843            LanesVal::L8W32(a) => (0..8).map(|i| a.lane(i).0 as i64).sum(),
1844            LanesVal::L4W64(a) => a.hsum() as i64,
1845            LanesVal::L16W16(a) => (0..16).map(|i| a.lane(i).0 as i64).sum(),
1846            LanesVal::L4W32(a) => (0..4).map(|i| a.lane(i).0 as i64).sum(),
1847            LanesVal::L16W8(a) => (0..16).map(|i| a.lane(i) as i64).sum(),
1848        }
1849    }
1850}
1851
1852#[cfg(test)]
1853mod tests {
1854    use super::*;
1855
1856    #[test]
1857    fn word32_add_wraps_not_promotes() {
1858        // The whole point vs BigInt: MAX + 1 wraps to 0, it never grows.
1859        assert_eq!(Word32::MAX.add(Word32::ONE), Word32::ZERO);
1860        assert_eq!(Word32(0xFFFF_FFFF).add(Word32(2)), Word32(1));
1861    }
1862
1863    #[test]
1864    fn word64_add_wraps_not_promotes() {
1865        assert_eq!(Word64::MAX.add(Word64::ONE), Word64::ZERO);
1866        assert_eq!(Word64(u64::MAX).add(Word64(5)), Word64(4));
1867    }
1868
1869    #[test]
1870    fn div_rem_shl_shr_operators_are_unsigned_primitive() {
1871        use core::ops::{Div, Rem, Shl, Shr};
1872        // `% 2^k` is a mask, `/ 2^k` a shift — the crypto reduction primitives, unsigned.
1873        assert_eq!(Word32(0x1234_5678).rem(Word32(65536)), Word32(0x5678), "% 2^16 masks the low bits");
1874        assert_eq!(Word32(0x1234_5678).div(Word32(65536)), Word32(0x1234), "/ 2^16 shifts right 16");
1875        assert_eq!(Word32(7).div(Word32(2)), Word32(3), "unsigned truncating division");
1876        assert_eq!(Word32(7).rem(Word32(3)), Word32(1), "unsigned remainder");
1877        assert_eq!(Word32(1).shl(31), Word32(0x8000_0000), "<< 31 sets the top bit");
1878        assert_eq!(Word32(0x8000_0000).shr(31), Word32(1), ">> 31 is a logical (unsigned) shift");
1879        assert_eq!(Word64(1).shl(63).shr(63), Word64(1), "Word64 logical shift round-trips the top bit");
1880        assert_eq!(Word8(200).div(Word8(8)), Word8(25), "Word8 unsigned division (200 ÷ 8)");
1881    }
1882
1883    #[test]
1884    fn rotl_is_cyclic_and_inverse_of_rotr() {
1885        let x = Word32(0x1234_5678);
1886        assert_eq!(x.rotl(8), Word32(0x3456_7812), "rotl by 8 is the documented permutation");
1887        assert_eq!(x.rotl(0), x, "rotate by 0 is identity");
1888        assert_eq!(x.rotl(32), x, "a full rotation is identity");
1889        for n in 0..Word32::BITS {
1890            assert_eq!(x.rotl(n).rotr(n), x, "rotr undoes rotl by {n}");
1891        }
1892    }
1893
1894    #[test]
1895    fn agrees_with_native_wrapping_ops_over_fuzz() {
1896        // Cross-check every op against the primitive's own wrapping arithmetic — the oracle.
1897        let mut s = 0xDEAD_BEEF_CAFE_F00Du64;
1898        let mut next = || {
1899            s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
1900            s
1901        };
1902        for _ in 0..5000 {
1903            let a = next() as u32;
1904            let b = next() as u32;
1905            let n = (next() % 32) as u32;
1906            assert_eq!(Word32(a).add(Word32(b)).get(), a.wrapping_add(b), "add");
1907            assert_eq!(Word32(a).sub(Word32(b)).get(), a.wrapping_sub(b), "sub");
1908            assert_eq!(Word32(a).mul(Word32(b)).get(), a.wrapping_mul(b), "mul");
1909            assert_eq!(Word32(a).bitxor(Word32(b)).get(), a ^ b, "xor");
1910            assert_eq!(Word32(a).bitand(Word32(b)).get(), a & b, "and");
1911            assert_eq!(Word32(a).bitor(Word32(b)).get(), a | b, "or");
1912            assert_eq!(Word32(a).not().get(), !a, "not");
1913            assert_eq!(Word32(a).shl(n).get(), a.wrapping_shl(n), "shl");
1914            assert_eq!(Word32(a).shr(n).get(), a.wrapping_shr(n), "shr");
1915            assert_eq!(Word32(a).rotl(n).get(), a.rotate_left(n), "rotl");
1916            assert_eq!(Word32(a).rotr(n).get(), a.rotate_right(n), "rotr");
1917        }
1918    }
1919
1920    #[test]
1921    fn lanes8word32_avx2_xor_equals_scalar_lanes() {
1922        // The SIMD-correctness proof at the base level: the AVX2 lowering of lane-wise XOR must be
1923        // byte-identical to the scalar lanes (the spec). Fuzz the dispatching `bitxor` AND the raw
1924        // scalar path so both are exercised regardless of the host's feature set.
1925        let mut s = 0x0BAD_F00D_1234_5678u64;
1926        let mut next = || {
1927            s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1928            s
1929        };
1930        for _ in 0..2000 {
1931            let a = Lanes8Word32(core::array::from_fn(|_| next() as u32));
1932            let b = Lanes8Word32(core::array::from_fn(|_| next() as u32));
1933            let n = (next() % 64) as u32; // exercises the n ≥ 32 reduction too
1934            // XOR.
1935            let xor_exp = Lanes8Word32(core::array::from_fn(|i| a.0[i] ^ b.0[i]));
1936            assert_eq!(a.bitxor_scalar(b), xor_exp, "scalar lane xor is the spec");
1937            assert_eq!(a.bitxor(b), xor_exp, "dispatched (AVX2) xor == spec");
1938            // Wrapping add.
1939            let add_exp = Lanes8Word32(core::array::from_fn(|i| a.0[i].wrapping_add(b.0[i])));
1940            assert_eq!(a.add_scalar(b), add_exp, "scalar lane add is the spec");
1941            assert_eq!(a.add(b), add_exp, "dispatched (AVX2) add == spec");
1942            // Left rotation (the ChaCha diffusion op).
1943            let rot_exp = Lanes8Word32(core::array::from_fn(|i| a.0[i].rotate_left(n % 32)));
1944            assert_eq!(a.rotl_scalar(n % 32), rot_exp, "scalar lane rotl is the spec");
1945            assert_eq!(a.rotl(n), rot_exp, "dispatched (AVX2) rotl == spec (n mod 32)");
1946            // Wrapping subtract (the i32 NTT butterfly difference).
1947            let sub_exp = Lanes8Word32(core::array::from_fn(|i| a.0[i].wrapping_sub(b.0[i])));
1948            assert_eq!(a.sub_scalar(b), sub_exp, "scalar lane sub32 is the spec");
1949            assert_eq!(a.sub(b), sub_exp, "dispatched (AVX2) sub32 == spec");
1950            // The signed i32 Montgomery multiply (ML-DSA q=8380417): AVX2 vpmuldq path == scalar.
1951            const Q: i32 = 8_380_417;
1952            const QINV: i32 = 58_728_449;
1953            let qv = Lanes8Word32::splat(Q as u32);
1954            let qiv = Lanes8Word32::splat(QINV as u32);
1955            // Inputs in [−q, q] (the NTT's working range) so the i64 product stays well within range.
1956            let ar = Lanes8Word32(core::array::from_fn(|i| (a.0[i] as i32 % Q) as u32));
1957            let br = Lanes8Word32(core::array::from_fn(|i| (b.0[i] as i32 % Q) as u32));
1958            let mont_exp = Lanes8Word32(core::array::from_fn(|i| {
1959                let p = (ar.0[i] as i32 as i64) * (br.0[i] as i32 as i64);
1960                let t = (p as i32).wrapping_mul(QINV) as i64;
1961                (((p - t * Q as i64) >> 32) as i32) as u32
1962            }));
1963            assert_eq!(ar.montmul32_scalar(br, qv, qiv), mont_exp, "scalar montmul32 is the spec");
1964            assert_eq!(ar.montmul32(br, qv, qiv), mont_exp, "dispatched (AVX2) montmul32 == spec");
1965            // The i32 within-vector-NTT permutes at every stride (h=4 vperm2i128, h=2/1 vpshufd/vpblendd).
1966            for &h in &[4usize, 2, 1] {
1967                let bl = Lanes8Word32(core::array::from_fn(|i| a.0[(i / (2 * h)) * (2 * h) + (i % (2 * h)) % h]));
1968                let bh = Lanes8Word32(core::array::from_fn(|i| a.0[(i / (2 * h)) * (2 * h) + h + (i % (2 * h)) % h]));
1969                let bd = Lanes8Word32(core::array::from_fn(|i| if (i % (2 * h)) < h { a.0[i] } else { b.0[i] }));
1970                assert_eq!(a.ntt_bcast_lo(h), bl, "i32 ntt_bcast_lo({h}) (AVX2) == scalar gather");
1971                assert_eq!(a.ntt_bcast_hi(h), bh, "i32 ntt_bcast_hi({h}) (AVX2) == scalar gather");
1972                assert_eq!(a.ntt_blend(b, h), bd, "i32 ntt_blend({h}) (AVX2) == scalar gather");
1973            }
1974        }
1975        // splat / lane / pack / unpack round-trip.
1976        let v = Lanes8Word32::splat(0xABCD_1234);
1977        assert_eq!(v.lane(3), Word32(0xABCD_1234));
1978        assert_eq!(v.to_words()[7], Word32(0xABCD_1234));
1979        let packed = Lanes8Word32::from_words(&[Word32(1), Word32(2), Word32(3)]);
1980        assert_eq!(packed.0, [1, 2, 3, 0, 0, 0, 0, 0], "short slice zero-fills");
1981    }
1982
1983    #[test]
1984    fn lanes4word64_avx2_add_and_widemul_equal_scalar_lanes() {
1985        // The SIMD-correctness proof for the Poly1305 lane config: AVX2 add (`vpaddq`) and the
1986        // widening low-32 multiply (`vpmuludq`) must be byte-identical to the scalar lanes.
1987        let mut s = 0x1357_9bdf_2468_ace0u64;
1988        let mut next = || {
1989            s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1990            s
1991        };
1992        for _ in 0..2000 {
1993            let a = Lanes4Word64(core::array::from_fn(|_| next()));
1994            let b = Lanes4Word64(core::array::from_fn(|_| next()));
1995            let add_exp = Lanes4Word64(core::array::from_fn(|i| a.0[i].wrapping_add(b.0[i])));
1996            assert_eq!(a.add_scalar(b), add_exp, "scalar lane add64 is the spec");
1997            assert_eq!(a.add(b), add_exp, "dispatched (AVX2) add64 == spec");
1998            let mul_exp =
1999                Lanes4Word64(core::array::from_fn(|i| (a.0[i] & 0xffff_ffff) * (b.0[i] & 0xffff_ffff)));
2000            assert_eq!(a.mul_lo32_wide_scalar(b), mul_exp, "scalar widening mul is the spec");
2001            assert_eq!(a.mul_lo32_wide(b), mul_exp, "dispatched (AVX2) vpmuludq == spec");
2002            // Horizontal sum.
2003            let hs = a.0[0].wrapping_add(a.0[1]).wrapping_add(a.0[2]).wrapping_add(a.0[3]);
2004            assert_eq!(a.hsum(), hs, "horizontal sum");
2005        }
2006        let v = Lanes4Word64::from_words(&[Word64(5), Word64(9)]);
2007        assert_eq!(v.0, [5, 9, 0, 0], "short slice zero-fills");
2008        assert_eq!(v.lane(1), Word64(9));
2009        assert_eq!(LanesVal::L4W64(v).hsum(), 14, "5 + 9 + 0 + 0");
2010        assert_eq!(LanesVal::L4W64(v).type_name(), "Lanes4Word64");
2011    }
2012
2013    #[test]
2014    fn lanes16word16_avx2_ntt_ops_equal_scalar_lanes() {
2015        // The SIMD-correctness proof for the NTT lane config: AVX2 add/sub (`vpaddw`/`vpsubw`),
2016        // low-16 multiply (`vpmullw`), and SIGNED high-16 multiply (`vpmulhw`) must be byte-identical
2017        // to the scalar lanes (the latter is the Montgomery butterfly's `mulhi`).
2018        let mut s = 0x2468_ace0_1357_9bdfu64;
2019        let mut next = || {
2020            s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
2021            (s >> 40) as u16
2022        };
2023        for _ in 0..3000 {
2024            let a = Lanes16Word16(core::array::from_fn(|_| next()));
2025            let b = Lanes16Word16(core::array::from_fn(|_| next()));
2026            let add_exp = Lanes16Word16(core::array::from_fn(|i| a.0[i].wrapping_add(b.0[i])));
2027            let sub_exp = Lanes16Word16(core::array::from_fn(|i| a.0[i].wrapping_sub(b.0[i])));
2028            let lo_exp = Lanes16Word16(core::array::from_fn(|i| a.0[i].wrapping_mul(b.0[i])));
2029            let hi_exp = Lanes16Word16(core::array::from_fn(|i| {
2030                (((a.0[i] as i16 as i32) * (b.0[i] as i16 as i32)) >> 16) as u16
2031            }));
2032            assert_eq!(a.add(b), add_exp, "lane add16 == spec");
2033            assert_eq!(a.sub(b), sub_exp, "lane sub16 == spec");
2034            assert_eq!(a.mullo(b), lo_exp, "lane mullo16 == spec");
2035            assert_eq!(a.mulhi(b), hi_exp, "lane (signed) mulhi16 == spec");
2036            // The within-vector-NTT lane permutes at every stride (h=8 `vperm2i128`, h=4/2
2037            // `vpshufd`/`vpblendd`): the dispatched (AVX2) path must equal the scalar gather.
2038            for &h in &[8usize, 4, 2] {
2039                let bl = Lanes16Word16(core::array::from_fn(|i| a.0[(i / (2 * h)) * (2 * h) + (i % (2 * h)) % h]));
2040                let bh = Lanes16Word16(core::array::from_fn(|i| a.0[(i / (2 * h)) * (2 * h) + h + (i % (2 * h)) % h]));
2041                let bd = Lanes16Word16(core::array::from_fn(|i| if (i % (2 * h)) < h { a.0[i] } else { b.0[i] }));
2042                assert_eq!(a.ntt_bcast_lo(h), bl, "ntt_bcast_lo({h}) (AVX2) == scalar gather");
2043                assert_eq!(a.ntt_bcast_hi(h), bh, "ntt_bcast_hi({h}) (AVX2) == scalar gather");
2044                assert_eq!(a.ntt_blend(b, h), bd, "ntt_blend({h}) (AVX2) == scalar gather");
2045            }
2046        }
2047        assert_eq!(Lanes16Word16::splat(7).lane(9), Word16(7));
2048        let p = Lanes16Word16::from_words(&[Word16(3), Word16(5)]);
2049        assert_eq!(p.0[0..3], [3, 5, 0], "short slice zero-fills");
2050        assert_eq!(LanesVal::L16W16(p).type_name(), "Lanes16Word16");
2051        assert_eq!(LanesVal::L16W16(p).lanes(), 16);
2052    }
2053
2054    #[test]
2055    fn wordval_dispatches_by_width_and_rejects_mismatch() {
2056        let a = WordVal::W32(Word32(0xFFFF_FFFF));
2057        let b = WordVal::W32(Word32(1));
2058        assert_eq!(a.add(b), Some(WordVal::W32(Word32(0))), "same-width add wraps");
2059        assert_eq!(a.width(), 32);
2060        assert_eq!(a.to_u64(), 0xFFFF_FFFF);
2061
2062        let c = WordVal::W64(Word64(1));
2063        assert_eq!(a.add(c), None, "mixed-width add is a type error");
2064        assert_eq!(a.bitxor(c), None, "mixed-width xor is a type error");
2065
2066        // Unary ops preserve width.
2067        assert_eq!(WordVal::W64(Word64(0)).not(), WordVal::W64(Word64(u64::MAX)));
2068        assert_eq!(WordVal::W32(Word32(0x1234_5678)).rotl(8), WordVal::W32(Word32(0x3456_7812)));
2069
2070        // from_u64 / width round-trip.
2071        assert_eq!(WordVal::from_u64(32, 0x1_0000_0005), Some(WordVal::W32(Word32(5))), "32 truncates");
2072        assert_eq!(WordVal::from_u64(64, 5), Some(WordVal::W64(Word64(5))));
2073        assert_eq!(WordVal::from_u64(16, 5), None, "only 32/64 are valid widths");
2074
2075        // Display is the unsigned decimal value.
2076        assert_eq!(WordVal::W32(Word32(42)).to_string(), "42");
2077    }
2078}