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