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#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
12pub enum ShiftVariant {
13	/// Shift logical left.
14	Sll,
15	/// Shift logical right.
16	Slr,
17	/// Shift arithmetic right.
18	///
19	/// This is similar to the logical shift right but instead of shifting in 0 bits it will
20	/// replicate the sign bit.
21	Sar,
22	/// Rotate right.
23	///
24	/// Rotates bits to the right, with bits shifted off the right end wrapping around to the left.
25	Rotr,
26	/// Shift logical left on 32-bit halves.
27	///
28	/// Performs independent logical left shifts on the upper and lower 32-bit halves of the word.
29	/// Only uses the lower 5 bits of the shift amount (0-31).
30	Sll32,
31	/// Shift logical right on 32-bit halves.
32	///
33	/// Performs independent logical right shifts on the upper and lower 32-bit halves of the word.
34	/// Only uses the lower 5 bits of the shift amount (0-31).
35	Srl32,
36	/// Shift arithmetic right on 32-bit halves.
37	///
38	/// Performs independent arithmetic right shifts on the upper and lower 32-bit halves of the
39	/// word. Sign extends each 32-bit half independently. Only uses the lower 5 bits of the shift
40	/// amount (0-31).
41	Sra32,
42	/// Rotate right on 32-bit halves.
43	///
44	/// Performs independent rotate right operations on the upper and lower 32-bit halves of the
45	/// word. Bits shifted off the right end wrap around to the left within each 32-bit half.
46	/// Only uses the lower 5 bits of the shift amount (0-31).
47	Rotr32,
48}
49
50impl ShiftVariant {
51	/// Applies this shift to a 64-bit word and returns the result.
52	///
53	/// The variant selects which word-level operation runs.
54	/// Full-width variants act on the whole 64-bit word.
55	/// The 32-bit variants act on the upper and lower halves independently.
56	///
57	/// # Arguments
58	/// - The word to shift.
59	/// - The shift amount in bits.
60	#[inline]
61	pub fn apply(self, word: Word, amount: usize) -> Word {
62		// The word-level operators take the amount as a 32-bit count.
63		let amount = amount as u32;
64		// Dispatch to the matching operation:
65		// - logical left / logical right shift in zeros.
66		// - arithmetic right replicates the sign bit.
67		// - rotate wraps bits around the word.
68		// - the `*32` family applies the same op to each 32-bit half on its own.
69		match self {
70			ShiftVariant::Sll => word << amount,
71			ShiftVariant::Slr => word >> amount,
72			ShiftVariant::Sar => word.sar(amount),
73			ShiftVariant::Rotr => word.rotr(amount),
74			ShiftVariant::Sll32 => word.sll32(amount),
75			ShiftVariant::Srl32 => word.srl32(amount),
76			ShiftVariant::Sra32 => word.sra32(amount),
77			ShiftVariant::Rotr32 => word.rotr32(amount),
78		}
79	}
80}
81
82impl SerializeBytes for ShiftVariant {
83	fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> {
84		let index = match self {
85			ShiftVariant::Sll => 0u8,
86			ShiftVariant::Slr => 1u8,
87			ShiftVariant::Sar => 2u8,
88			ShiftVariant::Rotr => 3u8,
89			ShiftVariant::Sll32 => 4u8,
90			ShiftVariant::Srl32 => 5u8,
91			ShiftVariant::Sra32 => 6u8,
92			ShiftVariant::Rotr32 => 7u8,
93		};
94		index.serialize(write_buf)
95	}
96}
97
98impl DeserializeBytes for ShiftVariant {
99	fn deserialize(read_buf: impl Buf) -> Result<Self, SerializationError>
100	where
101		Self: Sized,
102	{
103		let index = u8::deserialize(read_buf)?;
104		match index {
105			0 => Ok(ShiftVariant::Sll),
106			1 => Ok(ShiftVariant::Slr),
107			2 => Ok(ShiftVariant::Sar),
108			3 => Ok(ShiftVariant::Rotr),
109			4 => Ok(ShiftVariant::Sll32),
110			5 => Ok(ShiftVariant::Srl32),
111			6 => Ok(ShiftVariant::Sra32),
112			7 => Ok(ShiftVariant::Rotr32),
113			_ => Err(SerializationError::UnknownEnumVariant {
114				name: "ShiftVariant",
115				index,
116			}),
117		}
118	}
119}
120
121/// Similar to [`ValueIndex`], but represents a value that has been shifted by a certain amount.
122///
123/// This is used in the operands to constraints like [`AndConstraint`](super::AndConstraint).
124///
125/// The canonical formto represent a value without any shifting is [`ShiftVariant::Sll`] with
126/// amount equals 0.
127#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
128pub struct ShiftedValueIndex {
129	/// The index of this value in the input values vector.
130	pub value_index: ValueIndex,
131	/// The flavour of the shift that the value must be shifted by.
132	pub shift_variant: ShiftVariant,
133	/// The number of bits to shift by.
134	///
135	/// Stored as a byte to keep the struct small: constraint systems hold millions of these.
136	/// Must be less than 64.
137	pub amount: u8,
138}
139
140impl ShiftedValueIndex {
141	/// Create a value index that just uses the specified value. Equivalent to [`Self::sll`] with
142	/// amount equals 0.
143	pub const fn plain(value_index: ValueIndex) -> Self {
144		Self {
145			value_index,
146			shift_variant: ShiftVariant::Sll,
147			amount: 0,
148		}
149	}
150
151	/// Shift Left Logical by the given number of bits.
152	///
153	/// # Panics
154	/// Panics if the shift amount is greater than or equal to 64.
155	pub fn sll(value_index: ValueIndex, amount: usize) -> Self {
156		assert!(amount < 64, "shift amount n={amount} out of range");
157		Self {
158			value_index,
159			shift_variant: ShiftVariant::Sll,
160			amount: amount as u8,
161		}
162	}
163
164	/// Shift Right Logical by the given number of bits.
165	///
166	/// # Panics
167	/// Panics if the shift amount is greater than or equal to 64.
168	pub fn srl(value_index: ValueIndex, amount: usize) -> Self {
169		assert!(amount < 64, "shift amount n={amount} out of range");
170		Self {
171			value_index,
172			shift_variant: ShiftVariant::Slr,
173			amount: amount as u8,
174		}
175	}
176
177	/// Shift Right Arithmetic by the given number of bits.
178	///
179	/// This is similar to the Shift Right Logical but instead of shifting in 0 bits it will
180	/// replicate the sign bit.
181	///
182	/// # Panics
183	/// Panics if the shift amount is greater than or equal to 64.
184	pub fn sar(value_index: ValueIndex, amount: usize) -> Self {
185		assert!(amount < 64, "shift amount n={amount} out of range");
186		Self {
187			value_index,
188			shift_variant: ShiftVariant::Sar,
189			amount: amount as u8,
190		}
191	}
192
193	/// Rotate Right by the given number of bits.
194	///
195	/// Rotates bits to the right, with bits shifted off the right end wrapping around to the left.
196	///
197	/// # Panics
198	/// Panics if the shift amount is greater than or equal to 64.
199	pub fn rotr(value_index: ValueIndex, amount: usize) -> Self {
200		assert!(amount < 64, "shift amount n={amount} out of range");
201		Self {
202			value_index,
203			shift_variant: ShiftVariant::Rotr,
204			amount: amount as u8,
205		}
206	}
207
208	/// Shift Left Logical on 32-bit halves by the given number of bits.
209	///
210	/// Performs independent logical left shifts on the upper and lower 32-bit halves.
211	/// Only uses the lower 5 bits of the shift amount (0-31).
212	///
213	/// # Panics
214	/// Panics if the shift amount is greater than or equal to 32.
215	pub fn sll32(value_index: ValueIndex, amount: usize) -> Self {
216		assert!(amount < 32, "shift amount n={amount} out of range for 32-bit shift");
217		Self {
218			value_index,
219			shift_variant: ShiftVariant::Sll32,
220			amount: amount as u8,
221		}
222	}
223
224	/// Shift Right Logical on 32-bit halves by the given number of bits.
225	///
226	/// Performs independent logical right shifts on the upper and lower 32-bit halves.
227	/// Only uses the lower 5 bits of the shift amount (0-31).
228	///
229	/// # Panics
230	/// Panics if the shift amount is greater than or equal to 32.
231	pub fn srl32(value_index: ValueIndex, amount: usize) -> Self {
232		assert!(amount < 32, "shift amount n={amount} out of range for 32-bit shift");
233		Self {
234			value_index,
235			shift_variant: ShiftVariant::Srl32,
236			amount: amount as u8,
237		}
238	}
239
240	/// Shift Right Arithmetic on 32-bit halves by the given number of bits.
241	///
242	/// Performs independent arithmetic right shifts on the upper and lower 32-bit halves.
243	/// Sign extends each 32-bit half independently. Only uses the lower 5 bits of the shift amount
244	/// (0-31).
245	///
246	/// # Panics
247	/// Panics if the shift amount is greater than or equal to 32.
248	pub fn sra32(value_index: ValueIndex, amount: usize) -> Self {
249		assert!(amount < 32, "shift amount n={amount} out of range for 32-bit shift");
250		Self {
251			value_index,
252			shift_variant: ShiftVariant::Sra32,
253			amount: amount as u8,
254		}
255	}
256
257	/// Rotate Right on 32-bit halves by the given number of bits.
258	///
259	/// Performs independent rotate right operations on the upper and lower 32-bit halves.
260	/// Bits shifted off the right end wrap around to the left within each 32-bit half.
261	/// Only uses the lower 5 bits of the shift amount (0-31).
262	///
263	/// # Panics
264	/// Panics if the shift amount is greater than or equal to 32.
265	pub fn rotr32(value_index: ValueIndex, amount: usize) -> Self {
266		assert!(amount < 32, "shift amount n={amount} out of range for 32-bit rotate");
267		Self {
268			value_index,
269			shift_variant: ShiftVariant::Rotr32,
270			amount: amount as u8,
271		}
272	}
273
274	/// Evaluates this term against a witness.
275	///
276	/// A term names one value and a shift to apply to it.
277	/// It contributes one shifted word to the XOR that forms an operand.
278	#[inline]
279	pub fn eval(&self, witness: &ValueVec) -> Word {
280		// Look up the referenced word, then apply this term's shift.
281		self.shift_variant
282			.apply(witness[self.value_index], self.amount as usize)
283	}
284}
285
286impl SerializeBytes for ShiftedValueIndex {
287	fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
288		self.value_index.serialize(&mut write_buf)?;
289		self.shift_variant.serialize(&mut write_buf)?;
290		// Keep the wire format a u32 so serialized systems stay byte-compatible.
291		(self.amount as usize).serialize(write_buf)
292	}
293}
294
295impl DeserializeBytes for ShiftedValueIndex {
296	fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError>
297	where
298		Self: Sized,
299	{
300		let value_index = ValueIndex::deserialize(&mut read_buf)?;
301		let shift_variant = ShiftVariant::deserialize(&mut read_buf)?;
302		let amount = usize::deserialize(read_buf)?;
303
304		// Validate the range before narrowing to the byte-sized field.
305		// A value below 64 always fits in a u8.
306		if amount >= 64 {
307			return Err(SerializationError::InvalidConstruction {
308				name: "ShiftedValueIndex::amount",
309			});
310		}
311
312		Ok(ShiftedValueIndex {
313			value_index,
314			shift_variant,
315			amount: amount as u8,
316		})
317	}
318}
319
320#[cfg(test)]
321mod tests {
322	use super::*;
323
324	#[test]
325	fn test_shift_variant_serialization_round_trip() {
326		let variants = [
327			ShiftVariant::Sll,
328			ShiftVariant::Slr,
329			ShiftVariant::Sar,
330			ShiftVariant::Rotr,
331		];
332
333		for variant in variants {
334			let mut buf = Vec::new();
335			variant.serialize(&mut buf).unwrap();
336
337			let deserialized = ShiftVariant::deserialize(&mut buf.as_slice()).unwrap();
338			match (variant, deserialized) {
339				(ShiftVariant::Sll, ShiftVariant::Sll)
340				| (ShiftVariant::Slr, ShiftVariant::Slr)
341				| (ShiftVariant::Sar, ShiftVariant::Sar)
342				| (ShiftVariant::Rotr, ShiftVariant::Rotr) => {}
343				_ => panic!("ShiftVariant round trip failed: {:?} != {:?}", variant, deserialized),
344			}
345		}
346	}
347
348	#[test]
349	fn test_shift_variant_unknown_variant() {
350		// Create invalid variant index
351		let mut buf = Vec::new();
352		255u8.serialize(&mut buf).unwrap();
353
354		let result = ShiftVariant::deserialize(&mut buf.as_slice());
355		assert!(result.is_err());
356		match result.unwrap_err() {
357			SerializationError::UnknownEnumVariant { name, index } => {
358				assert_eq!(name, "ShiftVariant");
359				assert_eq!(index, 255);
360			}
361			_ => panic!("Expected UnknownEnumVariant error"),
362		}
363	}
364
365	#[test]
366	fn test_shifted_value_index_serialization_round_trip() {
367		let shifted_value_index = ShiftedValueIndex::srl(ValueIndex(42), 23);
368
369		let mut buf = Vec::new();
370		shifted_value_index.serialize(&mut buf).unwrap();
371
372		let deserialized = ShiftedValueIndex::deserialize(&mut buf.as_slice()).unwrap();
373		assert_eq!(shifted_value_index.value_index, deserialized.value_index);
374		assert_eq!(shifted_value_index.amount, deserialized.amount);
375		match (shifted_value_index.shift_variant, deserialized.shift_variant) {
376			(ShiftVariant::Slr, ShiftVariant::Slr) => {}
377			_ => panic!("ShiftVariant mismatch"),
378		}
379	}
380
381	#[test]
382	fn test_shifted_value_index_invalid_amount() {
383		// Create a buffer with invalid shift amount (>= 64)
384		let mut buf = Vec::new();
385		ValueIndex(0).serialize(&mut buf).unwrap();
386		ShiftVariant::Sll.serialize(&mut buf).unwrap();
387		64usize.serialize(&mut buf).unwrap(); // Invalid amount
388
389		let result = ShiftedValueIndex::deserialize(&mut buf.as_slice());
390		assert!(result.is_err());
391		match result.unwrap_err() {
392			SerializationError::InvalidConstruction { name } => {
393				assert_eq!(name, "ShiftedValueIndex::amount");
394			}
395			_ => panic!("Expected InvalidConstruction error"),
396		}
397	}
398
399	#[test]
400	fn shifted_value_index_fits_in_a_word() {
401		// Layout: value_index (u32, 4 bytes) + shift_variant (1 byte) + amount (u8, 1 byte).
402		// Padded to the u32 alignment, that is 8 bytes.
403		// Holding this at one word matters: systems carry millions of these on the prover hot path.
404		assert_eq!(size_of::<ShiftedValueIndex>(), 8);
405	}
406}