Skip to main content

logicaffeine_system/
word_rt.rs

1//! Runtime support for the `Word8`/`Word16`/`Word32`/`Word64` ring types in COMPILED LOGOS.
2//!
3//! Generated Rust constructs words with `word32(x)`, rotates with `rotl(x, n)`, and shows them.
4//! Arithmetic and bit operators (`+`, `^`, `&`, …) already live as trait impls on the `Word`
5//! newtypes in [`logicaffeine_base`] — each delegating to the wrapping primitive — so the
6//! emitted `a + b` is ring-correct with no per-site `wrapping_*`. This module supplies the
7//! remaining glue the codegen names: the `word*` constructors, the width-generic `rotl`/`rotr`,
8//! and the [`Showable`] impls so `Show` renders a word as its decimal value (matching the
9//! tree-walker and VM byte-for-byte).
10
11use crate::io::Showable;
12use core::fmt;
13use logicaffeine_data::LogosSeq;
14
15pub use logicaffeine_base::{
16    Lanes16Word16, Lanes16Word8, Lanes4Word32, Lanes4Word64, Lanes8Word32, Word16, Word32, Word64,
17    Word8,
18};
19
20/// Construct a word from the low bits of an integer — the compiled form of `word32(x)` etc.
21#[inline]
22pub fn word8(x: i64) -> Word8 {
23    Word8(x as u8)
24}
25#[inline]
26pub fn word16(x: i64) -> Word16 {
27    Word16(x as u16)
28}
29#[inline]
30pub fn word32(x: i64) -> Word32 {
31    Word32(x as u32)
32}
33#[inline]
34pub fn word64(x: i64) -> Word64 {
35    Word64(x as u64)
36}
37
38/// Width-defined bit rotation, available on every word width so `rotl(x, n)` is one name.
39pub trait WordRotate: Copy {
40    fn rotl(self, n: u32) -> Self;
41    fn rotr(self, n: u32) -> Self;
42}
43
44macro_rules! impl_rotate {
45    ($($w:ident => $prim:ty),* $(,)?) => { $(
46        impl WordRotate for logicaffeine_base::$w {
47            #[inline]
48            fn rotl(self, n: u32) -> Self { logicaffeine_base::$w(self.0.rotate_left(n)) }
49            #[inline]
50            fn rotr(self, n: u32) -> Self { logicaffeine_base::$w(self.0.rotate_right(n)) }
51        }
52    )* };
53}
54impl_rotate!(Word8 => u8, Word16 => u16, Word32 => u32, Word64 => u64);
55
56// A SIMD lane vector rotates lane-wise — so generated `rotl(v, 16)` over `Lanes8Word32` lowers to
57// the AVX2 shift-or rotation. `rotr` is the inverse rotation (the lane vocabulary only ships `rotl`).
58impl WordRotate for Lanes8Word32 {
59    #[inline]
60    fn rotl(self, n: u32) -> Self {
61        Lanes8Word32::rotl(self, n)
62    }
63    #[inline]
64    fn rotr(self, n: u32) -> Self {
65        Lanes8Word32::rotl(self, (32 - n % 32) % 32)
66    }
67}
68
69// The 4-way Keccak lane vector rotates lane-wise (64-bit) — generated `rotl(v, n)` over
70// `Lanes4Word64` lowers to the AVX2 `vpsllq`/`vpsrlq` shift-or (ρ offsets, θ's D term).
71impl WordRotate for Lanes4Word64 {
72    #[inline]
73    fn rotl(self, n: u32) -> Self {
74        Lanes4Word64::rotl(self, n)
75    }
76    #[inline]
77    fn rotr(self, n: u32) -> Self {
78        Lanes4Word64::rotl(self, (64 - n % 64) % 64)
79    }
80}
81
82/// Left rotation — the compiled form of `rotl(x, n)`. The amount is taken as `i64` (the type of
83/// a LOGOS `Int`) and reduced to the rotation width.
84#[inline]
85pub fn rotl<W: WordRotate>(x: W, n: i64) -> W {
86    x.rotl(n as u32)
87}
88/// Bitwise AND/OR/NOT — the compiled form of `word_and`/`word_or`/`word_not`. Distinct from the
89/// `and`/`or` keywords (logical short-circuit) so word crypto written in LOGOS (the MD5/SHA-1 round
90/// functions) is bit-exact on every tier. Word8/16/32/64 impl these operators, so they lower to a
91/// single machine `and`/`or`/`not`; a lane vector lowers to the AVX2 vector form.
92#[inline]
93pub fn word_and<W: ::core::ops::BitAnd<Output = W>>(a: W, b: W) -> W {
94    a & b
95}
96#[inline]
97pub fn word_or<W: ::core::ops::BitOr<Output = W>>(a: W, b: W) -> W {
98    a | b
99}
100#[inline]
101pub fn word_not<W: ::core::ops::Not<Output = W>>(a: W) -> W {
102    !a
103}
104
105/// Right rotation — the compiled form of `rotr(x, n)`.
106#[inline]
107pub fn rotr<W: WordRotate>(x: W, n: i64) -> W {
108    x.rotr(n as u32)
109}
110
111macro_rules! impl_showable_word {
112    ($($w:ty),* $(,)?) => { $(
113        impl Showable for $w {
114            #[inline(always)]
115            fn format_show(&self, f: &mut fmt::Formatter) -> fmt::Result {
116                fmt::Display::fmt(self, f)
117            }
118        }
119    )* };
120}
121impl_showable_word!(Word8, Word16, Word32, Word64);
122
123// ── SIMD lane vectors — the compiled forms of the lane constructors/accessors ─────────────────
124
125/// Pack the first 8 `Word32`s of a Seq into a lane vector — the compiled form of `lanes8Word32(s)`.
126#[inline]
127pub fn lanes8_word32(s: &LogosSeq<Word32>) -> Lanes8Word32 {
128    // Pack straight from the backing slice — `from_words` already takes `&[Word32]` and zero-fills a
129    // short slice, so there's no reason to clone the Seq into a fresh Vec on this crypto hot path.
130    Lanes8Word32::from_words(s.0.borrow().as_slice())
131}
132
133/// Broadcast one `Word32` into all 8 lanes — the compiled form of `splat8Word32(x)` (a crypto kernel
134/// loads a shared constant/key word into every block's lane this way).
135#[inline]
136pub fn splat8_word32(x: Word32) -> Lanes8Word32 {
137    Lanes8Word32::splat(x.0)
138}
139
140// ── Byte-shuffle lane (`Lanes16Word8` = one `__m128i`) — the compiled forms of the byte ops a SIMD
141//    hex codec WRITTEN in Logos lowers to: `pshufb`, per-byte shift, and the two byte interleaves. ──
142
143/// Pack the first 16 bytes of a `Seq of Int` into a byte-shuffle register — compiled `lanes16Word8(s)`.
144#[inline]
145pub fn lanes16_word8(s: &LogosSeq<i64>) -> Lanes16Word8 {
146    let mut a = [0u8; 16];
147    for (i, v) in s.0.borrow().iter().take(16).enumerate() {
148        a[i] = *v as u8;
149    }
150    Lanes16Word8(a)
151}
152/// The 16 bytes back as a `Seq of Int` — compiled `seqOfLanes16W8(v)`.
153#[inline]
154pub fn seq_of_lanes16w8(v: Lanes16Word8) -> LogosSeq<i64> {
155    LogosSeq::from_vec(v.0.iter().map(|&b| b as i64).collect())
156}
157/// Broadcast one byte into all 16 lanes — compiled `splat16Word8(x)`.
158#[inline]
159pub fn splat16_word8(x: i64) -> Lanes16Word8 {
160    Lanes16Word8::splat(x as u8)
161}
162/// Byte shuffle (`pshufb`) — compiled `shuffle16(table, idx)`.
163#[inline]
164pub fn shuffle16(table: Lanes16Word8, idx: Lanes16Word8) -> Lanes16Word8 {
165    table.shuffle(idx)
166}
167/// Per-byte logical shift right — compiled `shrBytes16(v, n)`.
168#[inline]
169pub fn shr_bytes16(v: Lanes16Word8, n: i64) -> Lanes16Word8 {
170    v.shr_bytes(n as u32)
171}
172/// Low-eight byte interleave (`_mm_unpacklo_epi8`) — compiled `interleaveLo16(a, b)`.
173#[inline]
174pub fn interleave_lo16(a: Lanes16Word8, b: Lanes16Word8) -> Lanes16Word8 {
175    a.interleave_lo(b)
176}
177/// High-eight byte interleave (`_mm_unpackhi_epi8`) — compiled `interleaveHi16(a, b)`.
178#[inline]
179pub fn interleave_hi16(a: Lanes16Word8, b: Lanes16Word8) -> Lanes16Word8 {
180    a.interleave_hi(b)
181}
182/// Per-byte wrapping add — compiled `byteAdd16(a, b)` (the ASCII→nibble decode).
183#[inline]
184pub fn byte_add16(a: Lanes16Word8, b: Lanes16Word8) -> Lanes16Word8 {
185    a.byte_add(b)
186}
187/// Multiply-add adjacent byte pairs (`pmaddubsw`) — compiled `maddubs16(a, w)` (nibble-pair → byte).
188#[inline]
189pub fn maddubs16(a: Lanes16Word8, w: Lanes16Word8) -> Lanes16Word8 {
190    a.maddubs(w)
191}
192/// Pack two `8×i16` vectors to `16×u8` with unsigned saturation (`packuswb`) — compiled `packus16(a, b)`.
193#[inline]
194pub fn packus16(a: Lanes16Word8, b: Lanes16Word8) -> Lanes16Word8 {
195    a.packus(b)
196}
197
198// ── SHA-1 SHA-NI lane (`Lanes4Word32` = one `__m128i`) — the compiled forms of the SHA-1 ops that a
199//    SHA-1 WRITTEN in Logos over `Lanes4Word32` lowers to (the base type carries the SHA-NI fast
200//    path + software fallback). Pack/unpack move a `Seq of Word32` (4 words) to/from the register. ──
201
202/// Pack the first 4 `Word32`s of a Seq into a SHA-1 lane register — compiled `lanes4Word32(s)`.
203#[inline]
204pub fn lanes4_word32(s: &LogosSeq<Word32>) -> Lanes4Word32 {
205    // Pack straight from the backing slice — no intermediate Vec clone (SHA-1 packs 4 words per lane,
206    // several times per block, so this clone sat on the hottest part of the compiled hash).
207    Lanes4Word32::from_words(s.0.borrow().as_slice())
208}
209/// Pack four `Word32`s straight into a SHA-1 lane register — compiled `lanes4Of(a, b, c, d)`. The
210/// alloc-free constructor (no `Seq`, no heap): the Logos SHA-1 packs a lane this way every round.
211#[inline]
212pub fn lanes4_of(a: Word32, b: Word32, c: Word32, d: Word32) -> Lanes4Word32 {
213    Lanes4Word32([a.0, b.0, c.0, d.0])
214}
215/// The 4 lanes back as a `Seq of Word32` — compiled `seqOfLanes4W32(v)`.
216#[inline]
217pub fn seq_of_lanes4w32(v: Lanes4Word32) -> LogosSeq<Word32> {
218    LogosSeq::from_vec(v.to_words().to_vec())
219}
220/// `sha1rnds4(abcd, msg, func)` — four SHA-1 rounds. Lowers to the `sha1rnds4` instruction.
221#[inline]
222pub fn sha1rnds4(abcd: Lanes4Word32, msg: Lanes4Word32, func: i64) -> Lanes4Word32 {
223    abcd.sha1rnds4(msg, func as u32)
224}
225/// `sha1msg1(a, b)` — message-schedule step 1. Lowers to `sha1msg1`.
226#[inline]
227pub fn sha1msg1(a: Lanes4Word32, b: Lanes4Word32) -> Lanes4Word32 {
228    a.sha1msg1(b)
229}
230/// `sha1msg2(a, b)` — message-schedule step 2. Lowers to `sha1msg2`.
231#[inline]
232pub fn sha1msg2(a: Lanes4Word32, b: Lanes4Word32) -> Lanes4Word32 {
233    a.sha1msg2(b)
234}
235/// `sha1nexte(a, b)` — fold the next round E. Lowers to `sha1nexte`.
236#[inline]
237pub fn sha1nexte(a: Lanes4Word32, b: Lanes4Word32) -> Lanes4Word32 {
238    a.sha1nexte(b)
239}
240
241/// The unsigned value of a `Word32` as an `Int` — the compiled form of `intOfWord32(w)`, used to
242/// serialize a keystream word into bytes (`Int` mod/div) for the XOR against a `Seq of Int` payload.
243#[inline]
244pub fn int_of_word32(w: Word32) -> i64 {
245    w.0 as i64
246}
247
248/// The value of a `Word64` as an `Int` — `intOfWord64(w)`. For values ≥ 2⁶³ this is a negative
249/// `i64` (two's-complement bits); used on byte-masked lanes (`< 256`) in Keccak's squeeze.
250#[inline]
251pub fn int_of_word64(w: Word64) -> i64 {
252    w.0 as i64
253}
254/// `word64Shl(w, n)` — logical shift-left of a `Word64` by `n` bits (Keccak lane byte-packing).
255#[inline]
256pub fn word64_shl(w: Word64, n: i64) -> Word64 {
257    Word64(w.0.wrapping_shl(n as u32))
258}
259/// `word64Shr(w, n)` — logical shift-right of a `Word64` by `n` bits (Keccak squeeze byte-extract).
260#[inline]
261pub fn word64_shr(w: Word64, n: i64) -> Word64 {
262    Word64(w.0.wrapping_shr(n as u32))
263}
264/// `word64And(a, b)` — bitwise AND of two `Word64`s (Keccak χ's `¬b ∧ c`, and byte masking).
265#[inline]
266pub fn word64_and(a: Word64, b: Word64) -> Word64 {
267    Word64(a.0 & b.0)
268}
269/// `word32Shr(w, n)` — logical shift-right of a `Word32` by `n` bits (SHA-256's `σ0`/`σ1` message
270/// schedule, where the shift is NOT a rotate — the vacated high bits are zero).
271#[inline]
272pub fn word32_shr(w: Word32, n: i64) -> Word32 {
273    Word32(w.0.wrapping_shr(n as u32))
274}
275
276/// The unsigned value of a `Word16` as an `Int` (0..2¹⁶−1) — the compiled form of `intOfWord16(w)`,
277/// the ML-KEM NTT's Word16-coefficient → Int boundary.
278#[inline]
279pub fn int_of_word16(w: Word16) -> i64 {
280    w.0 as i64
281}
282
283// ── Lanes4Word64 — the Poly1305 accumulator lane config ───────────────────────────────────────
284
285/// Pack the first 4 `Int`s of a Seq into a `Lanes4Word64` — the compiled form of `lanes4Word64(s)`.
286#[inline]
287pub fn lanes4_word64(s: &LogosSeq<i64>) -> Lanes4Word64 {
288    let mut a = [0u64; 4];
289    for (i, v) in s.iter().take(4).enumerate() {
290        a[i] = v as u64;
291    }
292    Lanes4Word64(a)
293}
294
295/// Unpack a `Lanes4Word64` into a Seq of 4 `Int` lanes — the compiled form of `seqOfLanes4(v)`.
296#[inline]
297pub fn seq_of_lanes4(v: Lanes4Word64) -> LogosSeq<i64> {
298    LogosSeq::from_vec(v.0.iter().map(|&x| x as i64).collect())
299}
300
301/// Lane-wise widening low-32 multiply (`vpmuludq`) — the compiled form of `mul32x32to64(a, b)`.
302#[inline]
303pub fn mul32x32to64(a: Lanes4Word64, b: Lanes4Word64) -> Lanes4Word64 {
304    a.mul_lo32_wide(b)
305}
306
307/// `splat4Word64(x)` — broadcast one `Word64` into all four Keccak lanes (the ι round-constant XOR,
308/// and the all-ones vector for χ's complement). The compiled form of the Logos builtin.
309#[inline]
310pub fn splat4_word64(x: Word64) -> Lanes4Word64 {
311    Lanes4Word64::splat(x.0)
312}
313
314/// `andNot4(a, b)` — Keccak χ's `(¬a) ∧ b` in one lane op (`vpandn`). The compiled form of the builtin.
315#[inline]
316pub fn and_not4(a: Lanes4Word64, b: Lanes4Word64) -> Lanes4Word64 {
317    a.andnot(b)
318}
319
320/// The horizontal sum of a `Lanes4Word64`'s lanes as an `Int` — the compiled form of `hsumLanes4(v)`.
321#[inline]
322pub fn hsum_lanes4(v: Lanes4Word64) -> i64 {
323    v.hsum() as i64
324}
325
326// ── Lanes16Word16 — the NTT coefficient lane config ───────────────────────────────────────────
327
328/// Pack the first 16 `Int`s of a Seq into a `Lanes16Word16` — the compiled form of `lanes16Word16(s)`.
329#[inline]
330pub fn lanes16_word16(s: &LogosSeq<i64>) -> Lanes16Word16 {
331    let mut a = [0u16; 16];
332    for (i, v) in s.iter().take(16).enumerate() {
333        a[i] = v as u16;
334    }
335    Lanes16Word16(a)
336}
337
338/// Unpack a `Lanes16Word16` into a Seq of 16 `Int` lanes (u16 bits) — `seqOfLanes16(v)`.
339#[inline]
340pub fn seq_of_lanes16(v: Lanes16Word16) -> LogosSeq<i64> {
341    LogosSeq::from_vec(v.0.iter().map(|&x| x as i64).collect())
342}
343
344/// Broadcast a `Word16`/`Int` into all 16 lanes — the compiled form of `splat16Word16(x)`.
345#[inline]
346pub fn splat16_word16(x: i64) -> Lanes16Word16 {
347    Lanes16Word16::splat(x as u16)
348}
349
350/// Lane-wise SIGNED high-16 multiply (`vpmulhw`) — the compiled form of `mulhi16(a, b)`.
351#[inline]
352pub fn mulhi16(a: Lanes16Word16, b: Lanes16Word16) -> Lanes16Word16 {
353    a.mulhi(b)
354}
355
356/// The signed i32 Montgomery multiply (`vpmuldq`) — the compiled form of `montmul32(a, b, q, qinv)`,
357/// the ML-DSA NTT butterfly multiply.
358#[inline]
359pub fn montmul32(
360    a: Lanes8Word32,
361    b: Lanes8Word32,
362    q: Lanes8Word32,
363    qinv: Lanes8Word32,
364) -> Lanes8Word32 {
365    a.montmul32(b, q, qinv)
366}
367
368// The within-vector NTT permutes (`nttBcastLo`/`nttBcastHi`/`nttBlend`) compile to inherent-method
369// calls (`v.ntt_bcast_lo(h)`), so Rust resolves the right lane impl by type (Lanes16Word16 i16 /
370// Lanes8Word32 i32) — no per-config free-function wrapper needed.
371
372/// Unpack a lane vector into a Seq of 8 `Word32` — the compiled form of `seqOfLanes8(v)`.
373#[inline]
374pub fn seq_of_lanes8(v: Lanes8Word32) -> LogosSeq<Word32> {
375    LogosSeq::from_vec(v.to_words().to_vec())
376}
377
378// A lane vector renders like a Seq of its lanes (`[l0, l1, …]`), byte-identical to the tree-walker.
379impl Showable for Lanes8Word32 {
380    fn format_show(&self, f: &mut fmt::Formatter) -> fmt::Result {
381        write!(f, "[")?;
382        for (i, w) in self.to_words().iter().enumerate() {
383            if i > 0 {
384                write!(f, ", ")?;
385            }
386            fmt::Display::fmt(w, f)?;
387        }
388        write!(f, "]")
389    }
390}