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