Skip to main content

binius_core/constraint_system/
shift.rs

1// Copyright 2025 Irreducible Inc.
2use binius_utils::serialization::{DeserializeBytes, SerializationError, SerializeBytes};
3use bytes::{Buf, BufMut};
4
5use super::{ValueIndex, ValueVec};
6use crate::word::Word;
7
8/// A different variants of shifting a value.
9///
10/// Note that there is no shift left arithmetic because it is redundant.
11///
12/// The discriminant is stored in a single byte.
13#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
14#[repr(u8)]
15pub enum ShiftVariant {
16	/// Shift logical left.
17	Sll = 0,
18	/// Shift logical right.
19	Slr = 1,
20	/// Shift arithmetic right.
21	///
22	/// This is similar to the logical shift right but instead of shifting in 0 bits it will
23	/// replicate the sign bit.
24	Sar = 2,
25	/// Rotate right.
26	///
27	/// Rotates bits to the right, with bits shifted off the right end wrapping around to the left.
28	Rotr = 3,
29	/// Shift logical left on 32-bit halves.
30	///
31	/// Performs independent logical left shifts on the upper and lower 32-bit halves of the word.
32	/// Only uses the lower 5 bits of the shift amount (0-31).
33	Sll32 = 4,
34	/// Shift logical right on 32-bit halves.
35	///
36	/// Performs independent logical right 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	Srl32 = 5,
39	/// Shift arithmetic right on 32-bit halves.
40	///
41	/// Performs independent arithmetic right shifts on the upper and lower 32-bit halves of the
42	/// word. Sign extends each 32-bit half independently. Only uses the lower 5 bits of the shift
43	/// amount (0-31).
44	Sra32 = 6,
45	/// Rotate right on 32-bit halves.
46	///
47	/// Performs independent rotate right operations on the upper and lower 32-bit halves of the
48	/// word. Bits shifted off the right end wrap around to the left within each 32-bit half.
49	/// Only uses the lower 5 bits of the shift amount (0-31).
50	Rotr32 = 7,
51}
52
53impl ShiftVariant {
54	/// Decodes a variant from its `u8` discriminant.
55	///
56	/// The discriminants match the `#[repr(u8)]` layout: `0..=7` map to the eight variants.
57	/// Any other byte returns `None`.
58	#[inline]
59	pub const fn from_u8(byte: u8) -> Option<Self> {
60		match byte {
61			0 => Some(ShiftVariant::Sll),
62			1 => Some(ShiftVariant::Slr),
63			2 => Some(ShiftVariant::Sar),
64			3 => Some(ShiftVariant::Rotr),
65			4 => Some(ShiftVariant::Sll32),
66			5 => Some(ShiftVariant::Srl32),
67			6 => Some(ShiftVariant::Sra32),
68			7 => Some(ShiftVariant::Rotr32),
69			_ => None,
70		}
71	}
72
73	/// Whether this variant operates on the two 32-bit halves independently.
74	///
75	/// - The `*32` family shifts each half on its own.
76	/// - It reads only the lower 5 bits of the amount.
77	/// - Every other variant acts on the whole 64-bit word.
78	#[inline]
79	pub const fn is_half_word(self) -> bool {
80		matches!(
81			self,
82			ShiftVariant::Sll32 | ShiftVariant::Srl32 | ShiftVariant::Sra32 | ShiftVariant::Rotr32
83		)
84	}
85
86	/// The exclusive upper bound on a valid shift amount for this variant.
87	///
88	/// - Half-word (`*32`) variants read only the lower 5 bits, so amounts run `0..32`.
89	/// - Full-width variants take amounts `0..64`.
90	///
91	/// Construction, validation, and deserialization all enforce this same bound.
92	/// A value that passes any of them therefore denotes the same shift everywhere.
93	#[inline]
94	pub const fn max_amount(self) -> usize {
95		if self.is_half_word() { 32 } else { 64 }
96	}
97
98	/// Applies this shift to a 64-bit word and returns the result.
99	///
100	/// The variant selects which word-level operation runs.
101	/// Full-width variants act on the whole 64-bit word.
102	/// The 32-bit variants act on the upper and lower halves independently.
103	///
104	/// # Arguments
105	/// - The word to shift.
106	/// - The shift amount in bits.
107	#[inline]
108	pub fn apply(self, word: Word, amount: usize) -> Word {
109		// The word-level operators take the amount as a 32-bit count.
110		let amount = amount as u32;
111		// Dispatch to the matching operation:
112		// - logical left / logical right shift in zeros.
113		// - arithmetic right replicates the sign bit.
114		// - rotate wraps bits around the word.
115		// - the `*32` family applies the same op to each 32-bit half on its own.
116		match self {
117			ShiftVariant::Sll => word << amount,
118			ShiftVariant::Slr => word >> amount,
119			ShiftVariant::Sar => word.sar(amount),
120			ShiftVariant::Rotr => word.rotr(amount),
121			ShiftVariant::Sll32 => word.sll32(amount),
122			ShiftVariant::Srl32 => word.srl32(amount),
123			ShiftVariant::Sra32 => word.sra32(amount),
124			ShiftVariant::Rotr32 => word.rotr32(amount),
125		}
126	}
127}
128
129impl SerializeBytes for ShiftVariant {
130	fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> {
131		(*self as u8).serialize(write_buf)
132	}
133}
134
135impl DeserializeBytes for ShiftVariant {
136	fn deserialize(read_buf: impl Buf) -> Result<Self, SerializationError>
137	where
138		Self: Sized,
139	{
140		let index = u8::deserialize(read_buf)?;
141		match index {
142			0 => Ok(ShiftVariant::Sll),
143			1 => Ok(ShiftVariant::Slr),
144			2 => Ok(ShiftVariant::Sar),
145			3 => Ok(ShiftVariant::Rotr),
146			4 => Ok(ShiftVariant::Sll32),
147			5 => Ok(ShiftVariant::Srl32),
148			6 => Ok(ShiftVariant::Sra32),
149			7 => Ok(ShiftVariant::Rotr32),
150			_ => Err(SerializationError::UnknownEnumVariant {
151				name: "ShiftVariant",
152				index,
153			}),
154		}
155	}
156}
157
158/// Similar to [`ValueIndex`], but represents a value that has been shifted by a certain amount.
159///
160/// This is used in the operands to constraints like [`AndConstraint`](super::AndConstraint).
161///
162/// The canonical formto represent a value without any shifting is [`ShiftVariant::Sll`] with
163/// amount equals 0.
164#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
165pub struct ShiftedValueIndex {
166	/// The index of this value in the input values vector.
167	pub value_index: ValueIndex,
168	/// The flavour of the shift that the value must be shifted by.
169	pub shift_variant: ShiftVariant,
170	/// The number of bits to shift by.
171	///
172	/// Stored as a byte to keep the struct small: constraint systems hold millions of these.
173	/// Must be less than 64.
174	pub amount: u8,
175}
176
177impl ShiftedValueIndex {
178	/// Create a value index that just uses the specified value. Equivalent to [`Self::sll`] with
179	/// amount equals 0.
180	pub const fn plain(value_index: ValueIndex) -> Self {
181		Self {
182			value_index,
183			shift_variant: ShiftVariant::Sll,
184			amount: 0,
185		}
186	}
187
188	/// Shift Left Logical by the given number of bits.
189	///
190	/// # Panics
191	/// Panics if the shift amount is greater than or equal to 64.
192	pub fn sll(value_index: ValueIndex, amount: usize) -> Self {
193		assert!(amount < 64, "shift amount n={amount} out of range");
194		Self {
195			value_index,
196			shift_variant: ShiftVariant::Sll,
197			amount: amount as u8,
198		}
199	}
200
201	/// Shift Right Logical by the given number of bits.
202	///
203	/// # Panics
204	/// Panics if the shift amount is greater than or equal to 64.
205	pub fn srl(value_index: ValueIndex, amount: usize) -> Self {
206		assert!(amount < 64, "shift amount n={amount} out of range");
207		Self {
208			value_index,
209			shift_variant: ShiftVariant::Slr,
210			amount: amount as u8,
211		}
212	}
213
214	/// Shift Right Arithmetic by the given number of bits.
215	///
216	/// This is similar to the Shift Right Logical but instead of shifting in 0 bits it will
217	/// replicate the sign bit.
218	///
219	/// # Panics
220	/// Panics if the shift amount is greater than or equal to 64.
221	pub fn sar(value_index: ValueIndex, amount: usize) -> Self {
222		assert!(amount < 64, "shift amount n={amount} out of range");
223		Self {
224			value_index,
225			shift_variant: ShiftVariant::Sar,
226			amount: amount as u8,
227		}
228	}
229
230	/// Rotate Right by the given number of bits.
231	///
232	/// Rotates bits to the right, with bits shifted off the right end wrapping around to the left.
233	///
234	/// # Panics
235	/// Panics if the shift amount is greater than or equal to 64.
236	pub fn rotr(value_index: ValueIndex, amount: usize) -> Self {
237		assert!(amount < 64, "shift amount n={amount} out of range");
238		Self {
239			value_index,
240			shift_variant: ShiftVariant::Rotr,
241			amount: amount as u8,
242		}
243	}
244
245	/// Shift Left Logical on 32-bit halves by the given number of bits.
246	///
247	/// Performs independent logical left shifts on the upper and lower 32-bit halves.
248	/// Only uses the lower 5 bits of the shift amount (0-31).
249	///
250	/// # Panics
251	/// Panics if the shift amount is greater than or equal to 32.
252	pub fn sll32(value_index: ValueIndex, amount: usize) -> Self {
253		assert!(amount < 32, "shift amount n={amount} out of range for 32-bit shift");
254		Self {
255			value_index,
256			shift_variant: ShiftVariant::Sll32,
257			amount: amount as u8,
258		}
259	}
260
261	/// Shift Right Logical on 32-bit halves by the given number of bits.
262	///
263	/// Performs independent logical right shifts on the upper and lower 32-bit halves.
264	/// Only uses the lower 5 bits of the shift amount (0-31).
265	///
266	/// # Panics
267	/// Panics if the shift amount is greater than or equal to 32.
268	pub fn srl32(value_index: ValueIndex, amount: usize) -> Self {
269		assert!(amount < 32, "shift amount n={amount} out of range for 32-bit shift");
270		Self {
271			value_index,
272			shift_variant: ShiftVariant::Srl32,
273			amount: amount as u8,
274		}
275	}
276
277	/// Shift Right Arithmetic on 32-bit halves by the given number of bits.
278	///
279	/// Performs independent arithmetic right shifts on the upper and lower 32-bit halves.
280	/// Sign extends each 32-bit half independently. Only uses the lower 5 bits of the shift amount
281	/// (0-31).
282	///
283	/// # Panics
284	/// Panics if the shift amount is greater than or equal to 32.
285	pub fn sra32(value_index: ValueIndex, amount: usize) -> Self {
286		assert!(amount < 32, "shift amount n={amount} out of range for 32-bit shift");
287		Self {
288			value_index,
289			shift_variant: ShiftVariant::Sra32,
290			amount: amount as u8,
291		}
292	}
293
294	/// Rotate Right on 32-bit halves by the given number of bits.
295	///
296	/// Performs independent rotate right operations on the upper and lower 32-bit halves.
297	/// Bits shifted off the right end wrap around to the left within each 32-bit half.
298	/// Only uses the lower 5 bits of the shift amount (0-31).
299	///
300	/// # Panics
301	/// Panics if the shift amount is greater than or equal to 32.
302	pub fn rotr32(value_index: ValueIndex, amount: usize) -> Self {
303		assert!(amount < 32, "shift amount n={amount} out of range for 32-bit rotate");
304		Self {
305			value_index,
306			shift_variant: ShiftVariant::Rotr32,
307			amount: amount as u8,
308		}
309	}
310
311	/// Evaluates this term against a witness.
312	///
313	/// A term names one value and a shift to apply to it.
314	/// It contributes one shifted word to the XOR that forms an operand.
315	#[inline]
316	pub fn eval(&self, witness: &ValueVec) -> Word {
317		// Look up the referenced word, then apply this term's shift.
318		self.shift_variant
319			.apply(witness[self.value_index], self.amount as usize)
320	}
321}
322
323impl SerializeBytes for ShiftedValueIndex {
324	fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
325		self.value_index.serialize(&mut write_buf)?;
326		self.shift_variant.serialize(&mut write_buf)?;
327		// Keep the wire format a u32 so serialized systems stay byte-compatible.
328		(self.amount as usize).serialize(write_buf)
329	}
330}
331
332impl DeserializeBytes for ShiftedValueIndex {
333	fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError>
334	where
335		Self: Sized,
336	{
337		let value_index = ValueIndex::deserialize(&mut read_buf)?;
338		let shift_variant = ShiftVariant::deserialize(&mut read_buf)?;
339		let amount = usize::deserialize(read_buf)?;
340
341		// Reject any amount the variant cannot represent.
342		// Half-word variants cap at 32, full-width at 64.
343		// This mirrors the bound the constructors enforce.
344		// A value below 64 always fits in the byte-sized field.
345		if amount >= shift_variant.max_amount() {
346			return Err(SerializationError::InvalidConstruction {
347				name: "ShiftedValueIndex::amount",
348			});
349		}
350
351		Ok(ShiftedValueIndex {
352			value_index,
353			shift_variant,
354			amount: amount as u8,
355		})
356	}
357}
358
359#[cfg(test)]
360mod tests {
361	use super::*;
362
363	#[test]
364	fn test_shift_variant_serialization_round_trip() {
365		let variants = [
366			ShiftVariant::Sll,
367			ShiftVariant::Slr,
368			ShiftVariant::Sar,
369			ShiftVariant::Rotr,
370		];
371
372		for variant in variants {
373			let mut buf = Vec::new();
374			variant.serialize(&mut buf).unwrap();
375
376			let deserialized = ShiftVariant::deserialize(&mut buf.as_slice()).unwrap();
377			match (variant, deserialized) {
378				(ShiftVariant::Sll, ShiftVariant::Sll)
379				| (ShiftVariant::Slr, ShiftVariant::Slr)
380				| (ShiftVariant::Sar, ShiftVariant::Sar)
381				| (ShiftVariant::Rotr, ShiftVariant::Rotr) => {}
382				_ => panic!("ShiftVariant round trip failed: {:?} != {:?}", variant, deserialized),
383			}
384		}
385	}
386
387	#[test]
388	fn test_shift_variant_unknown_variant() {
389		// Create invalid variant index
390		let mut buf = Vec::new();
391		255u8.serialize(&mut buf).unwrap();
392
393		let result = ShiftVariant::deserialize(&mut buf.as_slice());
394		assert!(result.is_err());
395		match result.unwrap_err() {
396			SerializationError::UnknownEnumVariant { name, index } => {
397				assert_eq!(name, "ShiftVariant");
398				assert_eq!(index, 255);
399			}
400			_ => panic!("Expected UnknownEnumVariant error"),
401		}
402	}
403
404	#[test]
405	fn test_shifted_value_index_serialization_round_trip() {
406		let shifted_value_index = ShiftedValueIndex::srl(ValueIndex(42), 23);
407
408		let mut buf = Vec::new();
409		shifted_value_index.serialize(&mut buf).unwrap();
410
411		let deserialized = ShiftedValueIndex::deserialize(&mut buf.as_slice()).unwrap();
412		assert_eq!(shifted_value_index.value_index, deserialized.value_index);
413		assert_eq!(shifted_value_index.amount, deserialized.amount);
414		match (shifted_value_index.shift_variant, deserialized.shift_variant) {
415			(ShiftVariant::Slr, ShiftVariant::Slr) => {}
416			_ => panic!("ShiftVariant mismatch"),
417		}
418	}
419
420	#[test]
421	fn test_shifted_value_index_invalid_amount() {
422		// Create a buffer with invalid shift amount (>= 64)
423		let mut buf = Vec::new();
424		ValueIndex(0).serialize(&mut buf).unwrap();
425		ShiftVariant::Sll.serialize(&mut buf).unwrap();
426		64usize.serialize(&mut buf).unwrap(); // Invalid amount
427
428		let result = ShiftedValueIndex::deserialize(&mut buf.as_slice());
429		assert!(result.is_err());
430		match result.unwrap_err() {
431			SerializationError::InvalidConstruction { name } => {
432				assert_eq!(name, "ShiftedValueIndex::amount");
433			}
434			_ => panic!("Expected InvalidConstruction error"),
435		}
436	}
437
438	#[test]
439	fn test_max_amount_and_is_half_word() {
440		// Full-width variants take amounts up to 63.
441		for variant in [
442			ShiftVariant::Sll,
443			ShiftVariant::Slr,
444			ShiftVariant::Sar,
445			ShiftVariant::Rotr,
446		] {
447			assert!(!variant.is_half_word());
448			assert_eq!(variant.max_amount(), 64);
449		}
450		// Half-word variants take amounts up to 31.
451		for variant in [
452			ShiftVariant::Sll32,
453			ShiftVariant::Srl32,
454			ShiftVariant::Sra32,
455			ShiftVariant::Rotr32,
456		] {
457			assert!(variant.is_half_word());
458			assert_eq!(variant.max_amount(), 32);
459		}
460	}
461
462	// Deserializes a raw (variant, amount) buffer, bypassing the constructors.
463	// This lets out-of-range half-word amounts reach the deserialization path.
464	fn deserialize_amount(
465		shift_variant: ShiftVariant,
466		amount: usize,
467	) -> Result<ShiftedValueIndex, SerializationError> {
468		let mut buf = Vec::new();
469		ValueIndex(0).serialize(&mut buf).unwrap();
470		shift_variant.serialize(&mut buf).unwrap();
471		amount.serialize(&mut buf).unwrap();
472		ShiftedValueIndex::deserialize(&mut buf.as_slice())
473	}
474
475	#[test]
476	fn test_deserialize_rejects_half_word_amount_at_or_above_32() {
477		// 31 is the largest amount a half-word variant can carry.
478		assert_eq!(
479			deserialize_amount(ShiftVariant::Sll32, 31).unwrap(),
480			ShiftedValueIndex {
481				value_index: ValueIndex(0),
482				shift_variant: ShiftVariant::Sll32,
483				amount: 31,
484			}
485		);
486		// 32 exceeds the 5-bit range and must be rejected.
487		match deserialize_amount(ShiftVariant::Sll32, 32).unwrap_err() {
488			SerializationError::InvalidConstruction { name } => {
489				assert_eq!(name, "ShiftedValueIndex::amount");
490			}
491			other => panic!("Expected InvalidConstruction, got: {other:?}"),
492		}
493		// A full-width variant still accepts 32 and up to 63.
494		assert_eq!(
495			deserialize_amount(ShiftVariant::Sll, 32).unwrap(),
496			ShiftedValueIndex {
497				value_index: ValueIndex(0),
498				shift_variant: ShiftVariant::Sll,
499				amount: 32,
500			}
501		);
502		assert_eq!(
503			deserialize_amount(ShiftVariant::Sll, 63).unwrap(),
504			ShiftedValueIndex {
505				value_index: ValueIndex(0),
506				shift_variant: ShiftVariant::Sll,
507				amount: 63,
508			}
509		);
510	}
511
512	#[test]
513	fn shifted_value_index_fits_in_a_word() {
514		// Layout: value_index (u32, 4 bytes) + shift_variant (1 byte) + amount (u8, 1 byte).
515		// Padded to the u32 alignment, that is 8 bytes.
516		// Holding this at one word matters: systems carry millions of these on the prover hot path.
517		assert_eq!(size_of::<ShiftedValueIndex>(), 8);
518	}
519
520	#[test]
521	fn test_shift_variant_from_u8_round_trip() {
522		// Every variant decodes back from its own discriminant.
523		let variants = [
524			ShiftVariant::Sll,
525			ShiftVariant::Slr,
526			ShiftVariant::Sar,
527			ShiftVariant::Rotr,
528			ShiftVariant::Sll32,
529			ShiftVariant::Srl32,
530			ShiftVariant::Sra32,
531			ShiftVariant::Rotr32,
532		];
533		for variant in variants {
534			assert_eq!(ShiftVariant::from_u8(variant as u8), Some(variant));
535		}
536		// The discriminants are exactly 0..=7, so 8 and above are undefined.
537		assert_eq!(ShiftVariant::from_u8(8), None);
538		assert_eq!(ShiftVariant::from_u8(255), None);
539	}
540}