binius_core/constraint_system/shift.rs
1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3use std::{iter, mem::MaybeUninit};
4
5use binius_utils::serialization::{DeserializeBytes, SerializationError, SerializeBytes};
6use bytes::{Buf, BufMut};
7
8#[cfg(test)]
9use super::ValueVec;
10use super::{ValueIndex, WordSource};
11use crate::word::Word;
12
13/// A different variants of shifting a value.
14///
15/// Note that there is no shift left arithmetic because it is redundant.
16///
17/// The discriminant is stored in a single byte.
18#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
19#[repr(u8)]
20pub enum ShiftVariant {
21 /// Shift logical left.
22 Sll = 0,
23 /// Shift logical right.
24 Slr = 1,
25 /// Shift arithmetic right.
26 ///
27 /// This is similar to the logical shift right but instead of shifting in 0 bits it will
28 /// replicate the sign bit.
29 Sar = 2,
30 /// Rotate right.
31 ///
32 /// Rotates bits to the right, with bits shifted off the right end wrapping around to the left.
33 Rotr = 3,
34 /// Shift logical left on 32-bit halves.
35 ///
36 /// Performs independent logical left shifts on the upper and lower 32-bit halves of the word.
37 /// Only uses the lower 5 bits of the shift amount (0-31).
38 Sll32 = 4,
39 /// Shift logical right on 32-bit halves.
40 ///
41 /// Performs independent logical right shifts on the upper and lower 32-bit halves of the word.
42 /// Only uses the lower 5 bits of the shift amount (0-31).
43 Srl32 = 5,
44 /// Shift arithmetic right on 32-bit halves.
45 ///
46 /// Performs independent arithmetic right shifts on the upper and lower 32-bit halves of the
47 /// word. Sign extends each 32-bit half independently. Only uses the lower 5 bits of the shift
48 /// amount (0-31).
49 Sra32 = 6,
50 /// Rotate right on 32-bit halves.
51 ///
52 /// Performs independent rotate right operations on the upper and lower 32-bit halves of the
53 /// word. Bits shifted off the right end wrap around to the left within each 32-bit half.
54 /// Only uses the lower 5 bits of the shift amount (0-31).
55 Rotr32 = 7,
56}
57
58/// A callback that runs against one concrete word-level shift.
59///
60/// Resolving a variant once turns it into a closure over words, handed to the callback here.
61/// Each variant gets its own specialized copy, so the callback's loop keeps no per-word branch.
62trait ShiftKernel {
63 /// What the callback produces.
64 type Output;
65
66 /// Runs the callback against `shift`, the resolved word operation.
67 fn call(self, shift: impl Fn(Word) -> Word) -> Self::Output;
68}
69
70/// Applies a resolved shift to a single word.
71struct ShiftOneWord {
72 /// The word the shift is applied to.
73 word: Word,
74}
75
76impl ShiftKernel for ShiftOneWord {
77 type Output = Word;
78
79 #[inline]
80 fn call(self, shift: impl Fn(Word) -> Word) -> Word {
81 // A single word carries no loop to specialize, so the resolved shift runs once.
82 shift(self.word)
83 }
84}
85
86/// Writes one shifted source word into each output cell, initializing it.
87struct WriteShiftedWords<'a> {
88 /// The uninitialized output cells.
89 out: &'a mut [MaybeUninit<Word>],
90 /// The source words to shift, one per output cell.
91 src: &'a [Word],
92}
93
94impl ShiftKernel for WriteShiftedWords<'_> {
95 type Output = ();
96
97 #[inline]
98 fn call(self, shift: impl Fn(Word) -> Word) {
99 // Positions line up one to one, so the pair iterator stops at the shorter slice.
100 for (out_i, &src_i) in iter::zip(self.out, self.src) {
101 out_i.write(shift(src_i));
102 }
103 }
104}
105
106/// XORs one shifted source word into each output cell.
107struct XorShiftedWords<'a> {
108 /// The output cells, each holding a running XOR.
109 out: &'a mut [Word],
110 /// The source words to shift, one per output cell.
111 src: &'a [Word],
112}
113
114impl ShiftKernel for XorShiftedWords<'_> {
115 type Output = ();
116
117 #[inline]
118 fn call(self, shift: impl Fn(Word) -> Word) {
119 // Positions line up one to one, so the pair iterator stops at the shorter slice.
120 for (out_i, &src_i) in iter::zip(self.out, self.src) {
121 *out_i = *out_i ^ shift(src_i);
122 }
123 }
124}
125
126impl ShiftVariant {
127 /// Every variant, ordered so that the array index equals the discriminant.
128 ///
129 /// Callers that must cover all variants iterate this: random fixtures, exhaustive checks.
130 pub const ALL: [Self; 8] = [
131 Self::Sll,
132 Self::Slr,
133 Self::Sar,
134 Self::Rotr,
135 Self::Sll32,
136 Self::Srl32,
137 Self::Sra32,
138 Self::Rotr32,
139 ];
140
141 /// Decodes a variant from its `u8` discriminant.
142 ///
143 /// The discriminants match the `#[repr(u8)]` layout: `0..=7` map to the eight variants.
144 /// Any other byte returns `None`.
145 #[inline]
146 pub const fn from_u8(byte: u8) -> Option<Self> {
147 match byte {
148 0 => Some(ShiftVariant::Sll),
149 1 => Some(ShiftVariant::Slr),
150 2 => Some(ShiftVariant::Sar),
151 3 => Some(ShiftVariant::Rotr),
152 4 => Some(ShiftVariant::Sll32),
153 5 => Some(ShiftVariant::Srl32),
154 6 => Some(ShiftVariant::Sra32),
155 7 => Some(ShiftVariant::Rotr32),
156 _ => None,
157 }
158 }
159
160 /// Whether this variant operates on the two 32-bit halves independently.
161 ///
162 /// - The `*32` family shifts each half on its own.
163 /// - It reads only the lower 5 bits of the amount.
164 /// - Every other variant acts on the whole 64-bit word.
165 #[inline]
166 pub const fn is_half_word(self) -> bool {
167 matches!(
168 self,
169 ShiftVariant::Sll32 | ShiftVariant::Srl32 | ShiftVariant::Sra32 | ShiftVariant::Rotr32
170 )
171 }
172
173 /// Whether this variant wraps the bits it moves out, rather than discarding them.
174 ///
175 /// A cyclic variant loses nothing, so any two of its shifts compose however far they move;
176 /// every other variant drops what it carries past the end.
177 #[inline]
178 pub const fn is_cyclic(self) -> bool {
179 matches!(self, ShiftVariant::Rotr | ShiftVariant::Rotr32)
180 }
181
182 /// The exclusive upper bound on a valid shift amount for this variant.
183 ///
184 /// - Half-word (`*32`) variants read only the lower 5 bits, so amounts run `0..32`.
185 /// - Full-width variants take amounts `0..64`.
186 ///
187 /// Construction, validation, and deserialization all enforce this same bound.
188 /// A value that passes any of them therefore denotes the same shift everywhere.
189 #[inline]
190 pub const fn max_amount(self) -> usize {
191 if self.is_half_word() { 32 } else { 64 }
192 }
193
194 /// Resolves this variant to its word-level operation and runs a callback against it.
195 ///
196 /// This is the single place that says what a variant does to a word:
197 /// - Logical left and logical right shift zeros in.
198 /// - Arithmetic right replicates the sign bit.
199 /// - Rotate wraps the bits that fall off one end around to the other.
200 /// - The half-word forms apply the same operation to each 32-bit half on its own.
201 ///
202 /// # Arguments
203 ///
204 /// - `amount`: the shift amount in bits, below this variant's upper bound.
205 /// - `kernel`: the callback to run against the resolved operation.
206 ///
207 /// # Performance
208 ///
209 /// The variant is decided here, once, ahead of whatever loop the callback runs.
210 /// Each branch hands over a distinct zero-sized closure, leaving no branch in the callback.
211 #[inline]
212 fn dispatch<K: ShiftKernel>(self, amount: u32, kernel: K) -> K::Output {
213 match self {
214 ShiftVariant::Sll => kernel.call(move |word| word << amount),
215 ShiftVariant::Slr => kernel.call(move |word| word >> amount),
216 ShiftVariant::Sar => kernel.call(move |word| word.sar(amount)),
217 ShiftVariant::Rotr => kernel.call(move |word| word.rotr(amount)),
218 ShiftVariant::Sll32 => kernel.call(move |word| word.sll32(amount)),
219 ShiftVariant::Srl32 => kernel.call(move |word| word.srl32(amount)),
220 ShiftVariant::Sra32 => kernel.call(move |word| word.sra32(amount)),
221 ShiftVariant::Rotr32 => kernel.call(move |word| word.rotr32(amount)),
222 }
223 }
224
225 /// Applies this shift to a 64-bit word and returns the result.
226 ///
227 /// Full-width variants act on the whole 64-bit word.
228 /// The half-word variants act on the upper and lower 32-bit halves independently.
229 ///
230 /// # Arguments
231 /// - The word to shift.
232 /// - The shift amount in bits.
233 ///
234 /// # Performance
235 ///
236 /// Which operation to run is decided on every call.
237 /// To shift many words by one fixed variant, resolve the variant once instead.
238 #[inline]
239 pub fn apply(self, word: Word, amount: usize) -> Word {
240 // The word-level operators count the amount in 32 bits.
241 self.dispatch(amount as u32, ShiftOneWord { word })
242 }
243
244 /// Applies this shift to each source word and writes the result to the matching output cell.
245 ///
246 /// Every output cell is written, so the caller need not initialize them first.
247 /// Cells past the end of either slice are left alone.
248 ///
249 /// # Arguments
250 ///
251 /// - `out`: the cells to initialize, one per source word.
252 /// - `src`: the words to shift.
253 /// - `amount`: the shift amount in bits, below this variant's upper bound.
254 #[inline]
255 pub fn write_shifted(self, out: &mut [MaybeUninit<Word>], src: &[Word], amount: u32) {
256 self.dispatch(amount, WriteShiftedWords { out, src });
257 }
258
259 /// Applies this shift to each source word and XORs the result into the matching output cell.
260 ///
261 /// Cells past the end of either slice are left alone.
262 ///
263 /// # Arguments
264 ///
265 /// - `out`: the cells to accumulate into, one per source word.
266 /// - `src`: the words to shift.
267 /// - `amount`: the shift amount in bits, below this variant's upper bound.
268 #[inline]
269 pub fn xor_shifted(self, out: &mut [Word], src: &[Word], amount: u32) {
270 self.dispatch(amount, XorShiftedWords { out, src });
271 }
272}
273
274impl SerializeBytes for ShiftVariant {
275 fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> {
276 (*self as u8).serialize(write_buf)
277 }
278}
279
280impl DeserializeBytes for ShiftVariant {
281 fn deserialize(read_buf: impl Buf) -> Result<Self, SerializationError>
282 where
283 Self: Sized,
284 {
285 let index = u8::deserialize(read_buf)?;
286 match index {
287 0 => Ok(ShiftVariant::Sll),
288 1 => Ok(ShiftVariant::Slr),
289 2 => Ok(ShiftVariant::Sar),
290 3 => Ok(ShiftVariant::Rotr),
291 4 => Ok(ShiftVariant::Sll32),
292 5 => Ok(ShiftVariant::Srl32),
293 6 => Ok(ShiftVariant::Sra32),
294 7 => Ok(ShiftVariant::Rotr32),
295 _ => Err(SerializationError::UnknownEnumVariant {
296 name: "ShiftVariant",
297 index,
298 }),
299 }
300 }
301}
302
303/// One shift: an operation paired with the distance it moves by.
304///
305/// The amount is always below the variant's [`max_amount`](ShiftVariant::max_amount), so a `Shift`
306/// that exists denotes the same operation wherever it is read.
307///
308/// Every variant is the identity at amount 0, so the amount alone does not fix how the identity is
309/// spelled. [`Shift::IDENTITY`] is the canonical spelling, and [`Self::is_canonical`] is what says
310/// which spelling a constraint system may carry.
311///
312/// The amount is stored as a byte to keep the struct small: constraint systems hold millions of
313/// these.
314///
315/// ```
316/// use binius_core::{constraint_system::Shift, word::Word};
317///
318/// let word = Word::from_u64(0xf0);
319/// assert_eq!(Shift::srl(4).apply(word), Word::from_u64(0x0f));
320/// assert_eq!(Shift::IDENTITY.apply(word), word);
321///
322/// // Every variant is the identity at amount 0, but only one spelling is canonical.
323/// assert!(Shift::rotr(0).is_identity());
324/// assert!(!Shift::rotr(0).is_canonical());
325/// ```
326#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
327pub struct Shift {
328 /// The operation this shift performs.
329 pub variant: ShiftVariant,
330 /// The number of bits to shift by, below the variant's upper bound.
331 pub amount: u8,
332}
333
334impl Shift {
335 /// The canonical shift that leaves a word untouched.
336 pub const IDENTITY: Self = Self {
337 variant: ShiftVariant::Sll,
338 amount: 0,
339 };
340
341 /// A shift of `amount` bits by `variant`.
342 ///
343 /// # Panics
344 ///
345 /// Panics if the amount is not below the variant's [`max_amount`](ShiftVariant::max_amount):
346 /// 32 for the half-word (`*32`) variants, 64 for the rest.
347 pub const fn new(variant: ShiftVariant, amount: usize) -> Self {
348 // A const context cannot format, so the amount and variant are left to the panic location
349 // rather than spelled into the message.
350 assert!(amount < variant.max_amount(), "shift amount out of range for this variant");
351 Self {
352 variant,
353 // An amount below 64 always fits in the byte-sized field.
354 amount: amount as u8,
355 }
356 }
357
358 /// Shift Left Logical.
359 ///
360 /// # Panics
361 /// Panics if the shift amount is greater than or equal to 64.
362 pub const fn sll(amount: usize) -> Self {
363 Self::new(ShiftVariant::Sll, amount)
364 }
365
366 /// Shift Right Logical.
367 ///
368 /// # Panics
369 /// Panics if the shift amount is greater than or equal to 64.
370 pub const fn srl(amount: usize) -> Self {
371 Self::new(ShiftVariant::Slr, amount)
372 }
373
374 /// Shift Right Arithmetic.
375 ///
376 /// This is similar to the Shift Right Logical but instead of shifting in 0 bits it will
377 /// replicate the sign bit.
378 ///
379 /// # Panics
380 /// Panics if the shift amount is greater than or equal to 64.
381 pub const fn sar(amount: usize) -> Self {
382 Self::new(ShiftVariant::Sar, amount)
383 }
384
385 /// Rotate Right.
386 ///
387 /// Rotates bits to the right, with bits shifted off the right end wrapping around to the left.
388 ///
389 /// # Panics
390 /// Panics if the shift amount is greater than or equal to 64.
391 pub const fn rotr(amount: usize) -> Self {
392 Self::new(ShiftVariant::Rotr, amount)
393 }
394
395 /// Shift Left Logical on 32-bit halves.
396 ///
397 /// Performs independent logical left shifts on the upper and lower 32-bit halves.
398 ///
399 /// # Panics
400 /// Panics if the shift amount is greater than or equal to 32.
401 pub const fn sll32(amount: usize) -> Self {
402 Self::new(ShiftVariant::Sll32, amount)
403 }
404
405 /// Shift Right Logical on 32-bit halves.
406 ///
407 /// Performs independent logical right shifts on the upper and lower 32-bit halves.
408 ///
409 /// # Panics
410 /// Panics if the shift amount is greater than or equal to 32.
411 pub const fn srl32(amount: usize) -> Self {
412 Self::new(ShiftVariant::Srl32, amount)
413 }
414
415 /// Shift Right Arithmetic on 32-bit halves.
416 ///
417 /// Performs independent arithmetic right shifts on the upper and lower 32-bit halves,
418 /// sign extending each half independently.
419 ///
420 /// # Panics
421 /// Panics if the shift amount is greater than or equal to 32.
422 pub const fn sra32(amount: usize) -> Self {
423 Self::new(ShiftVariant::Sra32, amount)
424 }
425
426 /// Rotate Right on 32-bit halves.
427 ///
428 /// Performs independent rotate right operations on the upper and lower 32-bit halves.
429 ///
430 /// # Panics
431 /// Panics if the shift amount is greater than or equal to 32.
432 pub const fn rotr32(amount: usize) -> Self {
433 Self::new(ShiftVariant::Rotr32, amount)
434 }
435
436 /// Whether this shift leaves every word untouched.
437 ///
438 /// Every variant is the identity at amount 0, so this holds for more shifts than
439 /// [`Shift::IDENTITY`] alone.
440 #[inline]
441 pub const fn is_identity(self) -> bool {
442 self.amount == 0
443 }
444
445 /// Whether this is the canonical spelling of the operation it denotes.
446 ///
447 /// Only the identity has more than one spelling, and [`Shift::IDENTITY`] is the one to use.
448 /// Constraint systems carry canonical shifts only, so that two terms denoting the same shifted
449 /// word compare equal.
450 #[inline]
451 pub const fn is_canonical(self) -> bool {
452 !self.is_identity() || matches!(self.variant, ShiftVariant::Sll)
453 }
454
455 /// Where this shift sits in the enumeration of every `(variant, amount)` spelling.
456 ///
457 /// The variant indexes runs of `Word::BITS`, and the amount indexes within a run:
458 ///
459 /// ```text
460 /// [ Sll 0 .. Sll 63 | Slr 0 .. Slr 63 | ... | Rotr32 0 .. Rotr32 63 ]
461 /// 0 63 64 127 448 511
462 /// ```
463 ///
464 /// A reduction keying one table entry per spelling addresses it by this index.
465 /// So does the prover's multilinear over the same axis pair, which is what lets the two agree.
466 ///
467 /// # Panics
468 ///
469 /// Panics if the amount is not below `Word::BITS`.
470 /// Above it a shift would index into the next variant's run, sharing an entry with another
471 /// shift.
472 #[inline]
473 pub const fn index(self) -> usize {
474 assert!((self.amount as usize) < Word::BITS, "shift amount is not below the word width");
475 self.variant as usize * Word::BITS + self.amount as usize
476 }
477
478 /// The shift one index of the spelling enumeration names.
479 ///
480 /// The inverse of [`Self::index`].
481 /// The high bits of an index pick the variant's run, the low bits the amount inside it:
482 ///
483 /// ```text
484 /// variant = index / Word::BITS
485 /// amount = index % Word::BITS
486 /// ```
487 ///
488 /// A caller holding an index rather than a shift recovers the spelling with no side table.
489 ///
490 /// # Panics
491 ///
492 /// Panics if the index is not below `ShiftVariant::ALL.len() * Word::BITS`.
493 /// Above that it names no run, so [`Self::index`] could not have produced it.
494 #[inline]
495 pub const fn from_index(index: usize) -> Self {
496 assert!(
497 index < ShiftVariant::ALL.len() * Word::BITS,
498 "shift index is not below the enumeration length"
499 );
500 let Some(variant) = ShiftVariant::from_u8((index / Word::BITS) as u8) else {
501 // The assertion above bounds the quotient by the number of variants.
502 unreachable!()
503 };
504 Self {
505 variant,
506 // The remainder is below `Word::BITS`, so it fits the byte-sized field.
507 amount: (index % Word::BITS) as u8,
508 }
509 }
510
511 /// Applies this shift to a word and returns the result.
512 ///
513 /// # Performance
514 ///
515 /// Which operation to run is decided on every call. To shift many words by one fixed shift,
516 /// resolve the variant once instead — see [`ShiftVariant::write_shifted`] and
517 /// [`ShiftVariant::xor_shifted`].
518 #[inline]
519 pub fn apply(self, word: Word) -> Word {
520 self.variant.apply(word, self.amount as usize)
521 }
522
523 /// Classifies the composition of two shifts, `outer` applied to the result of `inner`.
524 ///
525 /// This is the merge rule for a shift sequence: it says whether the two collapse to one shift,
526 /// clear the word, or genuinely need both slots.
527 ///
528 /// Collapsing is not just a matter of adding amounts. Two shifts collapse when the second
529 /// continues the first — which happens for more pairs than sharing a variant, since a shift
530 /// that has already cleared the sign bit or carried every bit past the halfway point leaves
531 /// the next shift nothing to distinguish. `chained` enumerates those, and `degenerate` the
532 /// cases where one shift has flattened the word past the other's notice.
533 ///
534 /// Reporting [`Composition::Pair`] where a collapse exists would cost a shift slot but never a
535 /// wrong answer; the tests check against an independent bit-level model so that does not
536 /// happen silently.
537 ///
538 /// # Arguments
539 ///
540 /// - `inner`: the shift applied first.
541 /// - `outer`: the shift applied to its result.
542 pub fn compose(inner: Shift, outer: Shift) -> Composition {
543 // The identity leaves the other shift to stand alone, whichever side it is on.
544 if inner.is_identity() {
545 return Composition::Single(outer);
546 }
547 if outer.is_identity() {
548 return Composition::Single(inner);
549 }
550
551 if let Some(single) = degenerate(inner, outer) {
552 return Composition::Single(single);
553 }
554 match chained(inner, outer) {
555 Some((variant, distance)) => chained_composition(variant, distance),
556 None => Composition::Pair,
557 }
558 }
559}
560
561impl SerializeBytes for Shift {
562 fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
563 self.variant.serialize(&mut write_buf)?;
564 // Keep the wire format a usize so serialized systems stay byte-compatible.
565 (self.amount as usize).serialize(write_buf)
566 }
567}
568
569impl DeserializeBytes for Shift {
570 fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError>
571 where
572 Self: Sized,
573 {
574 let variant = ShiftVariant::deserialize(&mut read_buf)?;
575 let amount = usize::deserialize(read_buf)?;
576
577 // Reject any amount the variant cannot represent.
578 // Half-word variants cap at 32, full-width at 64.
579 // This mirrors the bound `Shift::new` enforces.
580 // An amount below 64 always fits in the byte-sized field.
581 if amount >= variant.max_amount() {
582 return Err(SerializationError::InvalidConstruction {
583 name: "Shift::amount",
584 });
585 }
586
587 Ok(Shift {
588 variant,
589 amount: amount as u8,
590 })
591 }
592}
593
594/// What the composition of two shifts denotes.
595///
596/// Composing two shifts does not always need two: they may collapse to one shift, or clear the
597/// word outright. A caller merging shifts has to tell those apart — the collapsed cases save a
598/// shift slot, and the cleared case means the term contributes nothing and should be dropped
599/// rather than encoded.
600#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
601pub enum Composition {
602 /// The two collapse to this single shift, which may be [`Shift::IDENTITY`].
603 Single(Shift),
604 /// The two clear every bit, so the term they apply to is zero.
605 Zero,
606 /// The two do not collapse; both are needed, in the order they were given.
607 Pair,
608}
609
610/// Two shifts that chain, as the one direction they move bits in and the distance they cover
611/// together.
612///
613/// Chaining is what makes a composition collapse: the second shift continues the first rather than
614/// undoing part of it, so only the total distance matters and the variant's own overflow rule
615/// decides what comes out.
616///
617/// Returns `None` when the two do not chain, which is the common case.
618fn chained(inner: Shift, outer: Shift) -> Option<(ShiftVariant, usize)> {
619 let distance = inner.amount as usize + outer.amount as usize;
620 // Two shifts of one variant always chain.
621 if inner.variant == outer.variant {
622 return Some((inner.variant, distance));
623 }
624
625 // Neither shift is the identity here, so every amount below is at least 1.
626 Some((
627 match (inner.variant, outer.variant) {
628 // A logical right shift clears the sign bit, so an arithmetic one behind it has no
629 // sign left to replicate and moves zeros in just the same.
630 (ShiftVariant::Slr, ShiftVariant::Sar) => ShiftVariant::Slr,
631 (ShiftVariant::Srl32, ShiftVariant::Sra32) => ShiftVariant::Srl32,
632
633 // Once a full-width shift has carried everything past the halfway point, each 32-bit
634 // half holds only bits from one side of the word, so a half-wise shift in the same
635 // direction continues it as if it were full-width.
636 (ShiftVariant::Sll, ShiftVariant::Sll32) if inner.amount >= 32 => ShiftVariant::Sll,
637 (ShiftVariant::Sll32, ShiftVariant::Sll) if outer.amount >= 32 => ShiftVariant::Sll,
638 (ShiftVariant::Slr, ShiftVariant::Srl32) if inner.amount >= 32 => ShiftVariant::Slr,
639 (ShiftVariant::Srl32, ShiftVariant::Slr) if outer.amount >= 32 => ShiftVariant::Slr,
640 (ShiftVariant::Sar, ShiftVariant::Sra32) if inner.amount >= 32 => ShiftVariant::Sar,
641 (ShiftVariant::Sra32, ShiftVariant::Sar) if outer.amount >= 32 => ShiftVariant::Sar,
642
643 // The same, where the half-wise shift is the arithmetic one: it needs both halves'
644 // sign bits already cleared, which costs one more bit of travel than the cases above.
645 (ShiftVariant::Slr, ShiftVariant::Sra32) if inner.amount >= 33 => ShiftVariant::Slr,
646 (ShiftVariant::Srl32, ShiftVariant::Sar) if outer.amount >= 32 => ShiftVariant::Slr,
647
648 _ => return None,
649 },
650 distance,
651 ))
652}
653
654/// The single shift a composition collapses to for reasons other than chaining.
655///
656/// These are the degenerate cases, where one shift has already flattened the word enough that the
657/// other cannot tell the difference.
658const fn degenerate(inner: Shift, outer: Shift) -> Option<Shift> {
659 match (inner.variant, outer.variant) {
660 // Shifted arithmetically all the way, a word is all zeros or all ones. Rotating a word of
661 // one repeated bit leaves it alone.
662 (ShiftVariant::Sar, ShiftVariant::Rotr | ShiftVariant::Rotr32) if inner.amount == 63 => {
663 Some(inner)
664 }
665 (ShiftVariant::Sra32, ShiftVariant::Rotr32) if inner.amount == 31 => Some(inner),
666
667 // Keeping only the top bit keeps the very bit an arithmetic right shift replicates, so
668 // whatever that shift did before it does not show.
669 (ShiftVariant::Sar | ShiftVariant::Sra32, ShiftVariant::Slr) if outer.amount == 63 => {
670 Some(outer)
671 }
672 (ShiftVariant::Sra32, ShiftVariant::Srl32) if outer.amount == 31 => Some(outer),
673
674 _ => None,
675 }
676}
677
678/// What a chained shift of the given total distance comes to, once its own overflow rule applies.
679///
680/// This is where the variants differ: a logical shift runs out of word and clears it, an
681/// arithmetic one saturates at the sign, and a rotation wraps.
682fn chained_composition(variant: ShiftVariant, distance: usize) -> Composition {
683 let width = variant.max_amount();
684 match variant {
685 // Bits carried past the end are gone; carry everything past it and nothing is left.
686 ShiftVariant::Sll | ShiftVariant::Sll32 | ShiftVariant::Slr | ShiftVariant::Srl32 => {
687 if distance < width {
688 Composition::Single(Shift::new(variant, distance))
689 } else {
690 Composition::Zero
691 }
692 }
693 // Every position past the shift reads the sign bit, so travel beyond the width adds
694 // nothing.
695 ShiftVariant::Sar | ShiftVariant::Sra32 => {
696 Composition::Single(Shift::new(variant, distance.min(width - 1)))
697 }
698 // A rotation loses nothing, so a full turn is the identity.
699 ShiftVariant::Rotr | ShiftVariant::Rotr32 => match distance % width {
700 0 => Composition::Single(Shift::IDENTITY),
701 distance => Composition::Single(Shift::new(variant, distance)),
702 },
703 }
704}
705
706/// Similar to [`ValueIndex`], but represents a value that has been shifted.
707///
708/// This is used in the operands to constraints like [`AndConstraint`](super::AndConstraint).
709///
710/// A term carries a *sequence* of two shifts rather than one. The inner shift, `shift_seq[0]`,
711/// applies to the word first; the outer shift, `shift_seq[1]`, applies to its result. Two shifts
712/// express maps no single shift can: clearing the low bits and returning the rest to where they
713/// started needs both, since no one shift both drops bits and leaves the others in place.
714///
715/// # Canonical form
716///
717/// A lone shift goes in the inner slot, so `shift_seq[0].is_identity()` implies
718/// `shift_seq[1].is_identity()`. That splits every term into three classes:
719///
720/// ```text
721/// unshifted s_1 = s_2 = 0 spelled Shift::IDENTITY twice
722/// singly shifted s_2 = 0 != s_1 the lone shift sits inner
723/// doubly shifted s_2 != 0 both slots carry work
724/// ```
725///
726/// A doubly shifted term must not collapse: [`Shift::compose`] of the two reports
727/// [`Composition::Pair`], never [`Composition::Single`] (the two merge into one shift) or
728/// [`Composition::Zero`] (the two clear every bit, so the term is identically zero).
729/// [`ConstraintSystem::validate`](super::ConstraintSystem::validate) enforces both rules.
730///
731/// The `[Shift; 2]` spelling is not itself canonical as a *map*: per the composition derivation,
732/// 108,571 irreducible spellings denote only 74,341 distinct maps. Nothing in the reduction depends
733/// on that normalization for correctness, and buying it back needs a table far larger than the few
734/// dozen shifts a real constraint system uses.
735#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
736pub struct ShiftedValueIndex {
737 /// The index of this value in the input values vector.
738 pub value_index: ValueIndex,
739 /// The two shifts applied to the value, inner first.
740 pub shift_seq: [Shift; 2],
741}
742
743impl ShiftedValueIndex {
744 /// A value shifted by a sequence of two shifts, `shift_seq[0]` first.
745 pub const fn new(value_index: ValueIndex, shift_seq: [Shift; 2]) -> Self {
746 Self {
747 value_index,
748 shift_seq,
749 }
750 }
751
752 /// A value shifted by one shift, which the canonical form places in the inner slot.
753 pub const fn single(value_index: ValueIndex, shift: Shift) -> Self {
754 Self::new(value_index, [shift, Shift::IDENTITY])
755 }
756
757 /// The shift applied first.
758 #[inline]
759 pub const fn inner(&self) -> Shift {
760 self.shift_seq[0]
761 }
762
763 /// The shift applied to the inner shift's result.
764 #[inline]
765 pub const fn outer(&self) -> Shift {
766 self.shift_seq[1]
767 }
768
769 /// Whether this term leaves its word untouched.
770 ///
771 /// The canonical form puts a lone shift inner, so an identity inner shift settles it.
772 #[inline]
773 pub const fn is_unshifted(&self) -> bool {
774 self.inner().is_identity()
775 }
776
777 /// Whether this term genuinely needs both shift slots.
778 #[inline]
779 pub const fn is_doubly_shifted(&self) -> bool {
780 !self.outer().is_identity()
781 }
782
783 /// Create a value index that just uses the specified value, unshifted.
784 pub const fn plain(value_index: ValueIndex) -> Self {
785 Self::single(value_index, Shift::IDENTITY)
786 }
787
788 /// Shift Left Logical by the given number of bits.
789 ///
790 /// # Panics
791 /// Panics if the shift amount is greater than or equal to 64.
792 pub const fn sll(value_index: ValueIndex, amount: usize) -> Self {
793 Self::single(value_index, Shift::sll(amount))
794 }
795
796 /// Shift Right Logical by the given number of bits.
797 ///
798 /// # Panics
799 /// Panics if the shift amount is greater than or equal to 64.
800 pub const fn srl(value_index: ValueIndex, amount: usize) -> Self {
801 Self::single(value_index, Shift::srl(amount))
802 }
803
804 /// Shift Right Arithmetic by the given number of bits.
805 ///
806 /// This is similar to the Shift Right Logical but instead of shifting in 0 bits it will
807 /// replicate the sign bit.
808 ///
809 /// # Panics
810 /// Panics if the shift amount is greater than or equal to 64.
811 pub const fn sar(value_index: ValueIndex, amount: usize) -> Self {
812 Self::single(value_index, Shift::sar(amount))
813 }
814
815 /// Rotate Right by the given number of bits.
816 ///
817 /// Rotates bits to the right, with bits shifted off the right end wrapping around to the left.
818 ///
819 /// # Panics
820 /// Panics if the shift amount is greater than or equal to 64.
821 pub const fn rotr(value_index: ValueIndex, amount: usize) -> Self {
822 Self::single(value_index, Shift::rotr(amount))
823 }
824
825 /// Shift Left Logical on 32-bit halves by the given number of bits.
826 ///
827 /// Performs independent logical left shifts on the upper and lower 32-bit halves.
828 /// Only uses the lower 5 bits of the shift amount (0-31).
829 ///
830 /// # Panics
831 /// Panics if the shift amount is greater than or equal to 32.
832 pub const fn sll32(value_index: ValueIndex, amount: usize) -> Self {
833 Self::single(value_index, Shift::sll32(amount))
834 }
835
836 /// Shift Right Logical on 32-bit halves by the given number of bits.
837 ///
838 /// Performs independent logical right shifts on the upper and lower 32-bit halves.
839 /// Only uses the lower 5 bits of the shift amount (0-31).
840 ///
841 /// # Panics
842 /// Panics if the shift amount is greater than or equal to 32.
843 pub const fn srl32(value_index: ValueIndex, amount: usize) -> Self {
844 Self::single(value_index, Shift::srl32(amount))
845 }
846
847 /// Shift Right Arithmetic on 32-bit halves by the given number of bits.
848 ///
849 /// Performs independent arithmetic right shifts on the upper and lower 32-bit halves.
850 /// Sign extends each 32-bit half independently. Only uses the lower 5 bits of the shift amount
851 /// (0-31).
852 ///
853 /// # Panics
854 /// Panics if the shift amount is greater than or equal to 32.
855 pub const fn sra32(value_index: ValueIndex, amount: usize) -> Self {
856 Self::single(value_index, Shift::sra32(amount))
857 }
858
859 /// Rotate Right on 32-bit halves by the given number of bits.
860 ///
861 /// Performs independent rotate right operations on the upper and lower 32-bit halves.
862 /// Bits shifted off the right end wrap around to the left within each 32-bit half.
863 ///
864 /// # Panics
865 /// Panics if the shift amount is greater than or equal to 32.
866 pub const fn rotr32(value_index: ValueIndex, amount: usize) -> Self {
867 Self::single(value_index, Shift::rotr32(amount))
868 }
869
870 /// Evaluates this term against a word source.
871 ///
872 /// A term names one value and a sequence of two shifts to apply to it.
873 /// It contributes one shifted word to the XOR that forms an operand.
874 #[inline]
875 pub fn eval(&self, source: &impl WordSource) -> Word {
876 // Look up the referenced word, then apply the two shifts in sequence, inner first.
877 let [inner, outer] = self.shift_seq;
878 outer.apply(inner.apply(source.word(self.value_index)))
879 }
880}
881
882/// Evaluates an operand — the XOR of its shifted-value terms — against any [`WordSource`].
883///
884/// An empty operand evaluates to the zero word, the XOR identity.
885#[inline]
886pub fn eval_operand(source: &impl WordSource, operand: &[ShiftedValueIndex]) -> Word {
887 operand
888 .iter()
889 .fold(Word::ZERO, |acc, term| acc ^ term.eval(source))
890}
891
892impl SerializeBytes for ShiftedValueIndex {
893 fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
894 self.value_index.serialize(&mut write_buf)?;
895 for shift in &self.shift_seq {
896 shift.serialize(&mut write_buf)?;
897 }
898 Ok(())
899 }
900}
901
902impl DeserializeBytes for ShiftedValueIndex {
903 fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError>
904 where
905 Self: Sized,
906 {
907 let value_index = ValueIndex::deserialize(&mut read_buf)?;
908 let inner = Shift::deserialize(&mut read_buf)?;
909 let outer = Shift::deserialize(read_buf)?;
910 Ok(ShiftedValueIndex::new(value_index, [inner, outer]))
911 }
912}
913
914#[cfg(test)]
915mod tests {
916 use proptest::prelude::*;
917
918 use super::*;
919
920 // What each variant means, spelled out over raw integers.
921 // Independent of the word methods the implementation names, so a mis-wired variant fails here.
922 fn reference_shift(variant: ShiftVariant, word: u64, amount: u32) -> u64 {
923 // Half-word variants act on each 32-bit half on its own, reading only the low 5 bits.
924 let halves = |op: fn(u32, u32) -> u32| {
925 let amount = amount & 0x1F;
926 op(word as u32, amount) as u64 | ((op((word >> 32) as u32, amount) as u64) << 32)
927 };
928 match variant {
929 ShiftVariant::Sll => word << amount,
930 ShiftVariant::Slr => word >> amount,
931 ShiftVariant::Sar => (word as i64 >> amount) as u64,
932 ShiftVariant::Rotr => word.rotate_right(amount),
933 ShiftVariant::Sll32 => halves(|half, n| half << n),
934 ShiftVariant::Srl32 => halves(|half, n| half >> n),
935 ShiftVariant::Sra32 => halves(|half, n| ((half as i32) >> n) as u32),
936 ShiftVariant::Rotr32 => halves(|half, n| half.rotate_right(n)),
937 }
938 }
939
940 /// The width the half-word (`*32`) variants act over.
941 const HALF_WORD_BITS: usize = 32;
942
943 /// The bit an output position reads, for the positions that read nothing.
944 const READS_ZERO: u8 = u8::MAX;
945
946 /// What a shift does to a word, as one input bit position per output bit position.
947 ///
948 /// This is an independent model of a shift's meaning, written from the definitions rather than
949 /// from the composition rules it is used to check. Entry `i` is the input bit that output bit
950 /// `i` reads, or [`READS_ZERO`].
951 fn bit_map(shift: Shift) -> [u8; Word::BITS] {
952 let mut map = [READS_ZERO; Word::BITS];
953 let amount = shift.amount as usize;
954 let width = if shift.variant.is_half_word() {
955 HALF_WORD_BITS
956 } else {
957 Word::BITS
958 };
959 for (half, positions) in map.chunks_exact_mut(width).enumerate() {
960 let base = (half * width) as u8;
961 for (out, slot) in positions.iter_mut().enumerate() {
962 let read = match shift.variant {
963 ShiftVariant::Sll | ShiftVariant::Sll32 => out.checked_sub(amount),
964 ShiftVariant::Slr | ShiftVariant::Srl32 => {
965 Some(out + amount).filter(|&read| read < width)
966 }
967 ShiftVariant::Sar | ShiftVariant::Sra32 => Some((out + amount).min(width - 1)),
968 ShiftVariant::Rotr | ShiftVariant::Rotr32 => Some((out + amount) % width),
969 };
970 if let Some(read) = read {
971 *slot = base + read as u8;
972 }
973 }
974 }
975 map
976 }
977
978 /// What composing two shifts denotes, worked out at the bit level.
979 ///
980 /// Composition is function composition of the maps: output bit `i` reads whatever the inner
981 /// shift put where the outer shift looks. The result is then matched against the alphabet.
982 fn reference_composition(
983 inner: Shift,
984 outer: Shift,
985 by_map: &std::collections::HashMap<[u8; Word::BITS], Shift>,
986 ) -> Composition {
987 let (inner_map, outer_map) = (bit_map(inner), bit_map(outer));
988 let mut composed = [READS_ZERO; Word::BITS];
989 for (slot, &read) in iter::zip(&mut composed, &outer_map) {
990 if read != READS_ZERO {
991 *slot = inner_map[read as usize];
992 }
993 }
994
995 if composed == [READS_ZERO; Word::BITS] {
996 return Composition::Zero;
997 }
998 match by_map.get(&composed) {
999 Some(&single) => Composition::Single(single),
1000 None => Composition::Pair,
1001 }
1002 }
1003
1004 /// Every canonical shift, in the order the alphabet is enumerated.
1005 fn canonical_shifts() -> Vec<Shift> {
1006 ShiftVariant::ALL
1007 .into_iter()
1008 .flat_map(|variant| {
1009 (0..variant.max_amount()).map(move |amount| Shift::new(variant, amount))
1010 })
1011 .filter(|shift| shift.is_canonical())
1012 .collect()
1013 }
1014
1015 #[test]
1016 fn bit_map_matches_applying_the_shift() {
1017 // The bit map is the oracle the composition rules are checked against, so it is pinned to
1018 // the word operations first.
1019 //
1020 // One-hot words pin every entry: setting only input bit `b` makes the output exactly the
1021 // positions whose map entry is `b`, so a map that reads the wrong bit shows up here.
1022 for shift in canonical_shifts() {
1023 let map = bit_map(shift);
1024 for bit in 0..Word::BITS {
1025 let expected = shift.apply(Word(1u64 << bit)).as_u64();
1026 let from_map = map
1027 .iter()
1028 .enumerate()
1029 .filter(|&(_, &read)| read as usize == bit)
1030 .map(|(out, _)| 1u64 << out)
1031 .fold(0u64, |acc, position| acc | position);
1032 assert_eq!(from_map, expected, "{shift:?} disagrees on input bit {bit}");
1033 }
1034 }
1035 }
1036
1037 #[test]
1038 fn the_canonical_alphabet_has_no_two_spellings_of_one_shift() {
1039 // The alphabet is 4 full-width variants at 63 non-zero amounts, 4 half-word ones at 31,
1040 // and the identity: 4 * 63 + 4 * 31 + 1.
1041 let shifts = canonical_shifts();
1042 assert_eq!(shifts.len(), 4 * 63 + 4 * 31 + 1);
1043
1044 // Every one of them denotes a distinct operation, which is what makes `Single` name one
1045 // shift unambiguously.
1046 let maps = shifts
1047 .iter()
1048 .map(|&shift| bit_map(shift))
1049 .collect::<std::collections::HashSet<_>>();
1050 assert_eq!(maps.len(), shifts.len());
1051 }
1052
1053 #[test]
1054 fn compose_matches_the_bit_level_model() {
1055 // The casework in `compose` is a closed form for something the bits already determine.
1056 // This checks the two agree on every ordered pair of the alphabet — so a missing rule, or
1057 // one whose guard is off by a bit of travel, fails here rather than costing a shift slot
1058 // silently.
1059 let shifts = canonical_shifts();
1060 let by_map = shifts
1061 .iter()
1062 .map(|&shift| (bit_map(shift), shift))
1063 .collect::<std::collections::HashMap<_, _>>();
1064 for &inner in &shifts {
1065 for &outer in &shifts {
1066 assert_eq!(
1067 Shift::compose(inner, outer),
1068 reference_composition(inner, outer, &by_map),
1069 "composing {inner:?} then {outer:?}"
1070 );
1071 }
1072 }
1073 }
1074
1075 #[test]
1076 fn compose_classifies_the_whole_alphabet_the_way_the_derivation_does() {
1077 // The split from the BINIUS-408 design pass, as a regression: of the 377^2 ordered pairs,
1078 // 23,046 collapse to one shift, 10,512 clear the word, and 108,571 need both slots. The
1079 // counts move only if the shift alphabet itself changes.
1080 let shifts = canonical_shifts();
1081 let mut single = 0;
1082 let mut zero = 0;
1083 let mut pair = 0;
1084 for &inner in &shifts {
1085 for &outer in &shifts {
1086 match Shift::compose(inner, outer) {
1087 Composition::Single(_) => single += 1,
1088 Composition::Zero => zero += 1,
1089 Composition::Pair => pair += 1,
1090 }
1091 }
1092 }
1093 assert_eq!(single + zero + pair, shifts.len() * shifts.len());
1094 assert_eq!((single, zero, pair), (23_046, 10_512, 108_571));
1095 }
1096
1097 #[test]
1098 fn compose_catches_the_collapses_amount_arithmetic_misses() {
1099 // Saturating past the width is still a shift: every position already reads the sign bit.
1100 assert_eq!(
1101 Shift::compose(Shift::sar(5), Shift::sar(60)),
1102 Composition::Single(Shift::sar(63))
1103 );
1104 // Shifting the whole word out clears it, rather than shifting by the sum of the amounts.
1105 assert_eq!(Shift::compose(Shift::sll(40), Shift::sll(30)), Composition::Zero);
1106 // Rotations wrap, so a full turn is the identity.
1107 assert_eq!(
1108 Shift::compose(Shift::rotr(7), Shift::rotr(57)),
1109 Composition::Single(Shift::IDENTITY)
1110 );
1111 // The identity composes with anything, leaving the other shift alone.
1112 for shift in [Shift::rotr(9), Shift::sar(3), Shift::sll32(4)] {
1113 assert_eq!(Shift::compose(Shift::IDENTITY, shift), Composition::Single(shift));
1114 assert_eq!(Shift::compose(shift, Shift::IDENTITY), Composition::Single(shift));
1115 }
1116 // Clearing the low bits needs both slots: no single shift both drops bits and returns the
1117 // rest to where they started.
1118 assert_eq!(Shift::compose(Shift::srl(3), Shift::sll(3)), Composition::Pair);
1119 }
1120
1121 #[test]
1122 fn compose_collapses_a_half_word_shift_into_a_full_width_one() {
1123 // Once a full-width shift has carried everything past the halfway point, a half-wise shift
1124 // in the same direction continues it — the case plain amount arithmetic over variants
1125 // misses, and the one an inlined `*32` gadget is most likely to produce.
1126 assert_eq!(
1127 Shift::compose(Shift::sll(32), Shift::sll32(1)),
1128 Composition::Single(Shift::sll(33))
1129 );
1130 assert_eq!(
1131 Shift::compose(Shift::sll32(1), Shift::sll(32)),
1132 Composition::Single(Shift::sll(33))
1133 );
1134 // The arithmetic half-wise shift needs one more bit of travel, since it wants both halves'
1135 // sign bits already clear.
1136 assert_eq!(
1137 Shift::compose(Shift::srl(33), Shift::sra32(1)),
1138 Composition::Single(Shift::srl(34))
1139 );
1140 assert_eq!(Shift::compose(Shift::srl(32), Shift::sra32(1)), Composition::Pair);
1141 }
1142
1143 #[test]
1144 fn all_covers_every_discriminant_in_order() {
1145 // `ALL` is indexed by discriminant, and every entry decodes back to itself.
1146 for (discriminant, variant) in ShiftVariant::ALL.into_iter().enumerate() {
1147 assert_eq!(variant as usize, discriminant);
1148 assert_eq!(ShiftVariant::from_u8(discriminant as u8), Some(variant));
1149 }
1150 // The list is exhaustive: the next discriminant, and any byte above it, decode to nothing.
1151 assert_eq!(ShiftVariant::from_u8(ShiftVariant::ALL.len() as u8), None);
1152 assert_eq!(ShiftVariant::from_u8(255), None);
1153 }
1154
1155 #[test]
1156 fn every_variant_is_the_identity_at_amount_zero() {
1157 // The batched witness builder leans on this: at amount 0 it copies instead of dispatching.
1158 for variant in ShiftVariant::ALL {
1159 for word in [
1160 Word::ZERO,
1161 Word::ONE,
1162 Word::ALL_ONE,
1163 Word(0x0123_4567_89AB_CDEF),
1164 ] {
1165 assert_eq!(variant.apply(word, 0), word, "{variant:?} is not the identity at 0");
1166 }
1167 }
1168 }
1169
1170 proptest! {
1171 // Invariant: each variant resolves to the operation it denotes, at every valid amount.
1172 #[test]
1173 fn dispatch_matches_the_reference_for_every_variant(word in any::<u64>()) {
1174 for variant in ShiftVariant::ALL {
1175 // Amounts run 0..max, so both extremes are covered.
1176 for amount in 0..variant.max_amount() {
1177 let expected = Word(reference_shift(variant, word, amount as u32));
1178 // Both entry points share one resolution step, so checking both pins it.
1179 prop_assert_eq!(variant.apply(Word(word), amount), expected);
1180 let kernel = ShiftOneWord { word: Word(word) };
1181 prop_assert_eq!(variant.dispatch(amount as u32, kernel), expected);
1182 }
1183 }
1184 }
1185 }
1186
1187 #[test]
1188 fn slice_forms_stop_at_the_shorter_slice() {
1189 // Fixture state: 3 source words, 2 output cells, shifting left by 1.
1190 //
1191 // src: [1, 2, 4] -> [2, 4, 8]
1192 // out: [0, 0] only two cells to fill, so the third result is dropped
1193 let src = [Word(1), Word(2), Word(4)];
1194 let mut out = [Word::ZERO; 2];
1195 ShiftVariant::Sll.xor_shifted(&mut out, &src, 1);
1196 assert_eq!(out, [Word(2), Word(4)]);
1197
1198 // Fixture state: 2 source words, 3 output cells preset to 1.
1199 //
1200 // src: [1, 2] -> [2, 4]
1201 // out: [1, 1, 1] XOR gives [3, 5, _], and the trailing cell keeps its value
1202 let mut out = [Word::ONE; 3];
1203 ShiftVariant::Sll.xor_shifted(&mut out, &src[..2], 1);
1204 assert_eq!(out, [Word(3), Word(5), Word::ONE]);
1205 }
1206
1207 proptest! {
1208 // Invariant: the slice forms agree with the single-word form, cell by cell.
1209 #[test]
1210 fn slice_forms_match_the_single_word_form(src in prop::collection::vec(any::<u64>(), 1..24)) {
1211 let src: Vec<Word> = src.into_iter().map(Word).collect();
1212 for variant in ShiftVariant::ALL {
1213 for amount in 0..variant.max_amount() {
1214 let expected: Vec<Word> = src.iter().map(|&w| variant.apply(w, amount)).collect();
1215
1216 // The write form fills cells that start out uninitialized.
1217 let mut out = vec![MaybeUninit::uninit(); src.len()];
1218 variant.write_shifted(&mut out, &src, amount as u32);
1219 // Safety: the call above wrote every cell, since the slices are the same length.
1220 let written: Vec<Word> =
1221 out.iter().map(|cell| unsafe { cell.assume_init() }).collect();
1222 prop_assert_eq!(&written, &expected);
1223
1224 // Folding into zeroed cells reduces the XOR to the shift itself.
1225 let mut out = vec![Word::ZERO; src.len()];
1226 variant.xor_shifted(&mut out, &src, amount as u32);
1227 prop_assert_eq!(&out, &expected);
1228
1229 // Folding the same term a second time cancels it, restoring the zeroes.
1230 variant.xor_shifted(&mut out, &src, amount as u32);
1231 prop_assert_eq!(out, vec![Word::ZERO; src.len()]);
1232 }
1233 }
1234 }
1235 }
1236
1237 #[test]
1238 fn test_shift_variant_serialization_round_trip() {
1239 for variant in ShiftVariant::ALL {
1240 let mut buf = Vec::new();
1241 variant.serialize(&mut buf).unwrap();
1242
1243 let deserialized = ShiftVariant::deserialize(&mut buf.as_slice()).unwrap();
1244 assert_eq!(variant, deserialized);
1245 }
1246 }
1247
1248 #[test]
1249 fn test_shift_variant_unknown_variant() {
1250 // Create invalid variant index
1251 let mut buf = Vec::new();
1252 255u8.serialize(&mut buf).unwrap();
1253
1254 let result = ShiftVariant::deserialize(&mut buf.as_slice());
1255 assert!(result.is_err());
1256 match result.unwrap_err() {
1257 SerializationError::UnknownEnumVariant { name, index } => {
1258 assert_eq!(name, "ShiftVariant");
1259 assert_eq!(index, 255);
1260 }
1261 _ => panic!("Expected UnknownEnumVariant error"),
1262 }
1263 }
1264
1265 #[test]
1266 fn test_shifted_value_index_serialization_round_trip() {
1267 let shifted_value_index = ShiftedValueIndex::srl(ValueIndex::private(42), 23);
1268
1269 let mut buf = Vec::new();
1270 shifted_value_index.serialize(&mut buf).unwrap();
1271
1272 let deserialized = ShiftedValueIndex::deserialize(&mut buf.as_slice()).unwrap();
1273 assert_eq!(shifted_value_index.value_index, deserialized.value_index);
1274 assert_eq!(shifted_value_index.shift_seq, deserialized.shift_seq);
1275 match (deserialized.inner().variant, deserialized.outer().variant) {
1276 (ShiftVariant::Slr, ShiftVariant::Sll) => {}
1277 _ => panic!("ShiftVariant mismatch"),
1278 }
1279 }
1280
1281 #[test]
1282 fn test_shifted_value_index_invalid_amount() {
1283 // Create a buffer with invalid shift amount (>= 64)
1284 let mut buf = Vec::new();
1285 ValueIndex::constant(0).serialize(&mut buf).unwrap();
1286 ShiftVariant::Sll.serialize(&mut buf).unwrap();
1287 64usize.serialize(&mut buf).unwrap(); // Invalid amount
1288
1289 let result = ShiftedValueIndex::deserialize(&mut buf.as_slice());
1290 assert!(result.is_err());
1291 match result.unwrap_err() {
1292 SerializationError::InvalidConstruction { name } => {
1293 assert_eq!(name, "Shift::amount");
1294 }
1295 _ => panic!("Expected InvalidConstruction error"),
1296 }
1297 }
1298
1299 #[test]
1300 fn test_max_amount_and_is_half_word() {
1301 // The four full-width variants come first, then the four half-word ones.
1302 let (full_width, half_word) = ShiftVariant::ALL.split_at(4);
1303 // Full-width variants take amounts up to 63.
1304 for &variant in full_width {
1305 assert!(!variant.is_half_word());
1306 assert_eq!(variant.max_amount(), 64);
1307 }
1308 // Half-word variants take amounts up to 31.
1309 for &variant in half_word {
1310 assert!(variant.is_half_word());
1311 assert_eq!(variant.max_amount(), 32);
1312 }
1313 }
1314
1315 // Deserializes a raw (variant, amount) inner shift, bypassing the constructors.
1316 // This lets out-of-range half-word amounts reach the deserialization path.
1317 // The outer slot carries the identity, as the canonical form of a lone shift requires.
1318 fn deserialize_amount(
1319 shift_variant: ShiftVariant,
1320 amount: usize,
1321 ) -> Result<ShiftedValueIndex, SerializationError> {
1322 let mut buf = Vec::new();
1323 ValueIndex::constant(0).serialize(&mut buf).unwrap();
1324 shift_variant.serialize(&mut buf).unwrap();
1325 amount.serialize(&mut buf).unwrap();
1326 Shift::IDENTITY.serialize(&mut buf).unwrap();
1327 ShiftedValueIndex::deserialize(&mut buf.as_slice())
1328 }
1329
1330 #[test]
1331 fn test_deserialize_rejects_half_word_amount_at_or_above_32() {
1332 // 31 is the largest amount a half-word variant can carry.
1333 assert_eq!(
1334 deserialize_amount(ShiftVariant::Sll32, 31).unwrap(),
1335 ShiftedValueIndex::sll32(ValueIndex::constant(0), 31)
1336 );
1337 // 32 exceeds the 5-bit range and must be rejected.
1338 match deserialize_amount(ShiftVariant::Sll32, 32).unwrap_err() {
1339 SerializationError::InvalidConstruction { name } => {
1340 assert_eq!(name, "Shift::amount");
1341 }
1342 other => panic!("Expected InvalidConstruction, got: {other:?}"),
1343 }
1344 // A full-width variant still accepts 32 and up to 63.
1345 assert_eq!(
1346 deserialize_amount(ShiftVariant::Sll, 32).unwrap(),
1347 ShiftedValueIndex::sll(ValueIndex::constant(0), 32)
1348 );
1349 assert_eq!(
1350 deserialize_amount(ShiftVariant::Sll, 63).unwrap(),
1351 ShiftedValueIndex::sll(ValueIndex::constant(0), 63)
1352 );
1353 }
1354
1355 #[test]
1356 fn index_places_a_shift_by_variant_then_amount() {
1357 // Runs of `Word::BITS` amounts, one run per variant. A table indexed the other way round
1358 // would weight a shifted word by another shift's scalar.
1359 //
1360 // [ Sll 0 .. Sll 63 | Slr 0 .. Slr 63 | ... ]
1361 // 0 63 64 127
1362 assert_eq!(Shift::IDENTITY.index(), 0);
1363 assert_eq!(Shift::sll(5).index(), 5);
1364 assert_eq!(Shift::srl(0).index(), Word::BITS);
1365 assert_eq!(Shift::srl(3).index(), Word::BITS + 3);
1366 assert_eq!(Shift::rotr32(31).index(), ShiftVariant::Rotr32 as usize * Word::BITS + 31);
1367
1368 // Every spelling lands in its own slot, inside the enumeration.
1369 let mut seen = vec![false; ShiftVariant::ALL.len() * Word::BITS];
1370 for variant in ShiftVariant::ALL {
1371 for amount in 0..variant.max_amount() {
1372 let index = Shift::new(variant, amount).index();
1373 assert!(!seen[index], "{variant:?} {amount} shares an index");
1374 seen[index] = true;
1375 }
1376 }
1377 }
1378
1379 proptest! {
1380 // Invariant: `from_index` inverts `index` over the whole enumeration.
1381 //
1382 // from_index(index(s)) == s for every spelling s
1383 // index(from_index(i)) == i for every index i in range
1384 //
1385 // A caller that decodes an index instead of carrying a side table relies on both.
1386 #[test]
1387 fn from_index_inverts_index(index in 0usize..ShiftVariant::ALL.len() * Word::BITS) {
1388 let shift = Shift::from_index(index);
1389 prop_assert_eq!(shift.index(), index);
1390 prop_assert_eq!(Shift::from_index(shift.index()), shift);
1391 }
1392 }
1393
1394 // The last run ends at `ALL.len() * Word::BITS`, so that index names no variant.
1395 #[test]
1396 #[should_panic(expected = "shift index is not below the enumeration length")]
1397 fn from_index_rejects_an_index_past_the_last_run() {
1398 let _ = Shift::from_index(ShiftVariant::ALL.len() * Word::BITS);
1399 }
1400
1401 // An amount at the word width would index into the next variant's run, aliasing another shift.
1402 // The fields are public, so a hand-built shift can carry one even though `new` rejects it.
1403 #[test]
1404 #[should_panic(expected = "shift amount is not below the word width")]
1405 fn index_rejects_an_amount_at_the_word_width() {
1406 let shift = Shift {
1407 variant: ShiftVariant::Sll,
1408 amount: Word::BITS as u8,
1409 };
1410 let _ = shift.index();
1411 }
1412
1413 #[test]
1414 fn a_term_serialization_round_trips_both_shift_slots() {
1415 // The outer slot is on the wire too, so a doubly shifted term survives the round trip.
1416 // Clearing the low bits and returning the rest is the canonical example of a genuine pair.
1417 let term = ShiftedValueIndex::new(ValueIndex::private(7), [Shift::srl(3), Shift::sll(3)]);
1418 assert_eq!(Shift::compose(term.inner(), term.outer()), Composition::Pair);
1419
1420 let mut buf = Vec::new();
1421 term.serialize(&mut buf).unwrap();
1422 assert_eq!(ShiftedValueIndex::deserialize(buf.as_slice()).unwrap(), term);
1423 }
1424
1425 #[test]
1426 fn a_term_applies_its_two_shifts_inner_first() {
1427 // Order matters: `srl(4)` then `sll(4)` clears the word's low nibble, while the reverse
1428 // order clears its top one. A term that applied the outer shift first would swap the two.
1429 // The fixture sets bits in both nibbles so each pair drops something.
1430 let values = ValueVec::new_from_data(0, &[], &[Word::from_u64(0xf000_0000_0000_abcd)]);
1431
1432 let clear_low =
1433 ShiftedValueIndex::new(ValueIndex::private(0), [Shift::srl(4), Shift::sll(4)]);
1434 assert_eq!(clear_low.eval(&values), Word::from_u64(0xf000_0000_0000_abc0));
1435
1436 let clear_top =
1437 ShiftedValueIndex::new(ValueIndex::private(0), [Shift::sll(4), Shift::srl(4)]);
1438 assert_eq!(clear_top.eval(&values), Word::from_u64(0x0000_0000_0000_abcd));
1439
1440 // A lone shift sits inner, and the identity outer leaves its result alone.
1441 assert_eq!(
1442 ShiftedValueIndex::srl(ValueIndex::private(0), 4).eval(&values),
1443 Word::from_u64(0x0f00_0000_0000_0abc)
1444 );
1445 }
1446
1447 #[test]
1448 fn the_term_classes_read_off_the_shift_sequence() {
1449 let index = ValueIndex::private(0);
1450
1451 // Unshifted: the canonical form spells the identity in both slots.
1452 let unshifted = ShiftedValueIndex::plain(index);
1453 assert!(unshifted.is_unshifted());
1454 assert!(!unshifted.is_doubly_shifted());
1455
1456 // Singly shifted: the lone shift sits inner, so the term is not unshifted.
1457 let singly = ShiftedValueIndex::rotr(index, 5);
1458 assert!(!singly.is_unshifted());
1459 assert!(!singly.is_doubly_shifted());
1460
1461 // Doubly shifted: the outer slot carries work of its own.
1462 let doubly = ShiftedValueIndex::new(index, [Shift::srl(3), Shift::sll(3)]);
1463 assert!(!doubly.is_unshifted());
1464 assert!(doubly.is_doubly_shifted());
1465 }
1466
1467 #[test]
1468 fn shifted_value_index_fits_in_a_word() {
1469 // Layout: value_index (u32, 4 bytes) + two Shifts (variant byte + amount byte each).
1470 // That fills the u32 alignment exactly: 4 + 2 * 2 = 8 bytes.
1471 // Holding this at one word matters: systems carry millions of these on the prover hot path.
1472 assert_eq!(size_of::<ShiftedValueIndex>(), 8);
1473 }
1474}