Skip to main content

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}