Skip to main content

binius_circuits/
fixed_byte_vec.rs

1// Copyright 2025-2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3
4use std::ops::{Range, RangeInclusive};
5
6use binius_core::word::Word;
7use binius_frontend::{CircuitBuilder, Wire, WitnessFiller};
8
9/// A variable-length byte vector with fixed capacity determined at circuit construction time.
10///
11/// This struct represents a byte vector whose actual length can vary at runtime (stored in
12/// `len_bytes`), but whose maximum capacity is fixed and determined by the number of `data` wires
13/// allocated.
14///
15/// ## Capacity Model
16/// - Each wire in the `data` vector holds up to 8 bytes packed in little-endian format
17/// - The capacity in bytes = `data.len() * 8`
18/// - The actual length is stored in the `len_bytes` wire and can be any value from 0 to the
19///   capacity
20///
21/// ## Compile-time length range
22/// Callers very often know a tighter compile-time bound on `len_bytes` than `[0, capacity]` (a
23/// constant-length field, a `concat` output bounded by the sum of its inputs, …). `len_range`
24/// records that bound so gadgets like [`concat`](crate::concat::concat) can elide the dynamic
25/// machinery that would otherwise handle the full `[0, capacity]` range. Invariants:
26/// - `len_range.end() <= capacity` (in bytes), and
27/// - the runtime `len_bytes` satisfies `len_range.start() <= len_bytes <= len_range.end()` (the
28///   bound is inclusive on both ends — the upper end mirrors the capacity, which `len_bytes` is
29///   allowed to reach).
30///
31/// This range is only sound to rely on when the wire is genuinely constrained to it: the
32/// constructors here either fix `len_bytes` to a compile-time constant (`new_const_len`,
33/// `truncate`, `slice_const_range`) or default to the full `0..capacity` range (`new`,
34/// `new_inout`, `new_witness`). [`new_with_len_range`](ByteVec::new_with_len_range) trusts the
35/// caller to have enforced the range by other means (e.g. `concat`, where the output length is the
36/// sum of already-constrained input lengths).
37///
38/// ## Example
39/// ```ignore
40/// // Create a ByteVec with capacity for 32 bytes (4 wires)
41/// let byte_vec = ByteVec::new_witness(builder, 32);
42/// // byte_vec.data.len() == 4 (since 32 / 8 = 4)
43/// // Can hold any actual length from 0 to 32 bytes at runtime
44/// ```
45#[derive(Clone)]
46pub struct ByteVec {
47	/// The actual length of valid data in bytes (runtime value, can be 0 to capacity).
48	pub len_bytes: Wire,
49	/// The data wires, each holding up to 8 bytes. The number of wires determines
50	/// the capacity: capacity = data.len() * 8.
51	pub data: Vec<Wire>,
52	/// Compile-time bound on `len_bytes`: `len_range.start() <= len_bytes <= len_range.end()`. See
53	/// the struct docs for the invariants and how the range is established.
54	pub len_range: RangeInclusive<usize>,
55}
56
57impl ByteVec {
58	/// Creates a new fixed byte vector using the given wires and wire
59	/// containing the length of the data in bytes.
60	///
61	/// The length range defaults to the full `0..capacity`, preserving the fully-dynamic behavior.
62	pub fn new(data: Vec<Wire>, len_bytes: Wire) -> Self {
63		let capacity = data.len() * Word::BYTES;
64		Self::new_with_len_range(data, len_bytes, 0..=capacity)
65	}
66
67	/// Creates a new fixed byte vector with an explicit compile-time `len_range`.
68	///
69	/// The caller is responsible for ensuring `len_bytes` is actually constrained to lie within
70	/// `len_range`; this constructor only records the bound (and checks it against the capacity).
71	///
72	/// # Panics
73	/// * If `len_range.start() > len_range.end()`
74	/// * If `len_range.end()` exceeds the capacity (`data.len() * 8`)
75	pub fn new_with_len_range(
76		data: Vec<Wire>,
77		len_bytes: Wire,
78		len_range: RangeInclusive<usize>,
79	) -> Self {
80		let capacity = data.len() * Word::BYTES;
81		assert!(len_range.start() <= len_range.end(), "invalid len_range: start > end");
82		assert!(
83			*len_range.end() <= capacity,
84			"len_range.end {} exceeds capacity {capacity}",
85			len_range.end()
86		);
87		Self {
88			len_bytes,
89			data,
90			len_range,
91		}
92	}
93
94	/// Creates a constant-length byte vector: `len_bytes` is fixed to the compile-time constant
95	/// `len`, so `len_range = len..=len`.
96	///
97	/// # Panics
98	/// * If `len` exceeds the capacity (`data.len() * 8`)
99	pub fn new_const_len(b: &CircuitBuilder, data: Vec<Wire>, len: usize) -> Self {
100		let len_bytes = b.add_constant_64(len as u64);
101		Self::new_with_len_range(data, len_bytes, len..=len)
102	}
103
104	/// Creates a new fixed byte vector with the given maximum length as inout wires.
105	pub fn new_inout(b: &CircuitBuilder, max_len: usize) -> Self {
106		let len_bytes = b.add_inout();
107		let data = (0..max_len).map(|_| b.add_inout()).collect();
108		Self::new(data, len_bytes)
109	}
110
111	/// Creates a new fixed byte vector with the given maximum length as witness wires.
112	pub fn new_witness(b: &CircuitBuilder, max_len: usize) -> Self {
113		let len_bytes = b.add_inout();
114		let data = (0..max_len).map(|_| b.add_witness()).collect();
115		Self::new(data, len_bytes)
116	}
117
118	/// Populate the length wire with the actual vector size in bytes.
119	///
120	/// # Panics
121	/// * If `len_bytes` lies outside `self.len_range`.
122	pub fn populate_len_bytes(&self, w: &mut WitnessFiller<'_>, len_bytes: usize) {
123		self.assert_len_in_range(len_bytes);
124		w[self.len_bytes] = Word(len_bytes as u64);
125	}
126
127	/// Asserts that a concrete byte length lies within the compile-time `len_range`.
128	fn assert_len_in_range(&self, len_bytes: usize) {
129		assert!(
130			self.len_range.contains(&len_bytes),
131			"len_bytes {len_bytes} outside len_range {:?}",
132			self.len_range
133		);
134	}
135
136	/// Populate the [`ByteVec`] with bytes.
137	///
138	/// This method packs bytes into 64-bit words using little-endian ordering,
139	///
140	/// # Panics
141	/// * If bytes.len() exceeds self.max_len
142	pub fn populate_bytes_le(&self, w: &mut WitnessFiller<'_>, bytes: &[u8]) {
143		self.assert_len_in_range(bytes.len());
144		w.pack_bytes_le(&self.data, bytes);
145		w[self.len_bytes] = Word(bytes.len() as u64);
146	}
147
148	/// Populate the vector's data from a byte slice.
149	///
150	/// Packs the bytes into 64-bit words in little-endian order and ensures
151	/// any unused words are zeroed out.
152	///
153	/// # Panics
154	/// Panics if `data_bytes.len()` > `self.max_len_bytes()`
155	pub fn populate_data(&self, w: &mut WitnessFiller<'_>, data_bytes: &[u8]) {
156		assert!(
157			data_bytes.len() <= self.max_len_bytes(),
158			"vector data length {} exceeds maximum {}",
159			data_bytes.len(),
160			self.max_len_bytes()
161		);
162
163		// Pack bytes into 64-bit words (little-endian)
164		for (i, chunk) in data_bytes.chunks(8).enumerate() {
165			if i < self.data.len() {
166				let mut word = 0u64;
167				for (j, &byte) in chunk.iter().enumerate() {
168					word |= (byte as u64) << (j * 8);
169				}
170				w[self.data[i]] = Word(word);
171			}
172		}
173
174		// Zero out any remaining words beyond the actual data
175		for i in data_bytes.len().div_ceil(8)..self.data.len() {
176			w[self.data[i]] = Word::ZERO;
177		}
178	}
179
180	/// Returns the maximum length of this vector in bytes.
181	pub const fn max_len_bytes(&self) -> usize {
182		self.data.len() * 8
183	}
184
185	/// Construct a new [`ByteVec`] by truncating to `num_wires`.
186	///
187	/// # Panics
188	/// * If num_wires exceeds self.data.len()
189	pub fn truncate(&self, b: &CircuitBuilder, num_wires: usize) -> ByteVec {
190		assert!(num_wires <= self.data.len(), "num_wires must be less than self.data.len()");
191
192		let trimmed_wires = self.data[0..num_wires].to_vec();
193		ByteVec::new_const_len(b, trimmed_wires, num_wires << 3)
194	}
195
196	/// Extracts a slice at a compile-time constant range.
197	///
198	/// This operation is significantly more efficient than the dynamic `Slice` circuit
199	/// because the range is known at circuit construction time, allowing for:
200	/// - Direct computation of which words are needed (no multiplexers)
201	/// - Compile-time shift amounts (no dynamic shift selection)
202	/// - Reduced constraint count
203	///
204	/// # Arguments
205	/// * `b` - Circuit builder
206	/// * `range` - Compile-time constant byte range to extract
207	///
208	/// # Returns
209	/// A new `ByteVec` containing the extracted slice with capacity rounded up to
210	/// the next word boundary (8 bytes).
211	///
212	/// # Constraints
213	/// - Validates at runtime that `range.end <= self.len_bytes`
214	/// - If the range is not aligned to 8-byte boundaries, words are shifted appropriately
215	/// - Bytes of the final word beyond the slice length are unconstrained (a [`ByteVec`] makes no
216	///   guarantee about byte values past its length).
217	///
218	/// # Panics
219	/// * If `range.start > range.end`
220	/// * If `range.end > self.len_range.end()`
221	///
222	/// # Example
223	/// ```ignore
224	/// // Extract bytes 3-11 from a ByteVec
225	/// let slice = byte_vec.slice_const_range(&builder, 3..11);
226	/// // slice will have capacity of 16 bytes (2 words) but length of 8 bytes
227	/// ```
228	pub fn slice_const_range(&self, b: &CircuitBuilder, range: Range<usize>) -> ByteVec {
229		assert!(range.start <= range.end, "Invalid range: start > end");
230		assert!(
231			range.end <= *self.len_range.end(),
232			"Range end {} exceeds length bound {}",
233			range.end,
234			self.len_range.end()
235		);
236
237		let slice_len = range.len();
238
239		// Return early if slice is empty
240		if slice_len == 0 {
241			return ByteVec::new_const_len(b, Vec::new(), 0);
242		}
243
244		// Validate that `range.end <= self.len_bytes` at runtime. For a const-length vec
245		// `len_bytes` is structurally pinned to `len_range.end()`, and the compile-time bound
246		// above already guarantees `range.end <= len_range.end() == len_bytes`, so the check is
247		// provably redundant and only emitted for dynamic-length vecs.
248		if self.len_range.start() != self.len_range.end() {
249			let range_end_const = b.add_constant_64(range.end as u64);
250			let valid = b.icmp_ule(range_end_const, self.len_bytes);
251			b.assert_true("slice_range_check", valid);
252		}
253
254		let output_words = extract_const_range(b, &self.data, range);
255		ByteVec::new_const_len(b, output_words, slice_len)
256	}
257}
258
259/// Extracts `data[range]` (bytes packed little-endian into 64-bit words) as
260/// `range.len().div_ceil(8)` words.
261///
262/// Bytes of the final word beyond `range.len()` are left as-is (whatever the source words held);
263/// callers that care about those bytes must mask them, but a [`ByteVec`] makes no guarantee about
264/// byte values past its length, so the common case needs no mask.
265///
266/// All indices are compile-time constants, so this lowers to constant shifts (no multiplexer /
267/// dynamic-shift machinery). This is the constant-offset extraction primitive shared by
268/// [`ByteVec::slice_const_range`] and [`concat`](crate::concat::concat).
269///
270/// # Panics
271/// * If `range.start > range.end`
272/// * If `range.end` exceeds the capacity (`data.len() * 8`)
273pub(crate) fn extract_const_range(
274	b: &CircuitBuilder,
275	data: &[Wire],
276	range: Range<usize>,
277) -> Vec<Wire> {
278	assert!(range.start <= range.end, "invalid range: start > end");
279	assert!(
280		range.end <= data.len() * Word::BYTES,
281		"range.end {} exceeds capacity {}",
282		range.end,
283		data.len() * Word::BYTES
284	);
285
286	let slice_len = range.len();
287	if slice_len == 0 {
288		return Vec::new();
289	}
290
291	let start_word_idx = range.start / Word::BYTES;
292	// Word index containing the last byte of the sliced data.
293	let last_word_index = (range.end - 1) / Word::BYTES;
294	let byte_offset = range.start % Word::BYTES;
295	let num_output_words = slice_len.div_ceil(Word::BYTES);
296
297	// Extract words with shifting if needed. Bytes of the final word beyond the slice length are
298	// left as-is (a `ByteVec` makes no guarantee about byte values past its length).
299	if byte_offset == 0 {
300		// Aligned case: directly copy the words.
301		data[start_word_idx..start_word_idx + num_output_words].to_vec()
302	} else {
303		// Unaligned case: combine bytes from two adjacent words.
304		(0..num_output_words)
305			.map(|i| {
306				let source_idx = start_word_idx + i;
307
308				let current_word = data[source_idx];
309				// Shift current word right by byte_offset bytes to align.
310				let shifted_current = b.shr(current_word, (byte_offset * 8) as u32);
311
312				if source_idx < last_word_index {
313					let next_word = data[source_idx + 1];
314					// Shift next word left to fill in the high bytes.
315					let shifted_next = b.shl(next_word, ((Word::BYTES - byte_offset) * 8) as u32);
316					// Combine the two parts (XOR is cheaper than OR).
317					b.bxor(shifted_current, shifted_next)
318				} else {
319					shifted_current
320				}
321			})
322			.collect()
323	}
324}
325
326#[cfg(test)]
327mod tests {
328
329	use super::{ByteVec, CircuitBuilder, Word};
330
331	#[test]
332	fn test_slice_const_range_aligned() {
333		let b = CircuitBuilder::new();
334
335		// Create a ByteVec with 32 bytes capacity (4 words)
336		let byte_vec = ByteVec::new_witness(&b, 4);
337
338		// Extract aligned slice: bytes 8-16 (word 1)
339		let slice = byte_vec.slice_const_range(&b, 8..16);
340
341		assert_eq!(slice.data.len(), 1, "Slice should have 1 word");
342
343		let circuit = b.build();
344		let mut filler = circuit.new_witness_filler();
345
346		// Populate input with known data
347		let input_data: Vec<u8> = (0..32).map(|i| i as u8).collect();
348		byte_vec.populate_bytes_le(&mut filler, &input_data);
349
350		// Expected slice: bytes 8-15 (word 1)
351		let expected_word = 0x0f0e0d0c0b0a0908u64;
352		filler[slice.data[0]] = Word(expected_word);
353
354		circuit.populate_wire_witness(&mut filler).unwrap();
355
356		// Verify constraints
357		let cs = circuit.constraint_system();
358		cs.verify(&filler.into_value_vec()).unwrap();
359	}
360
361	#[test]
362	fn test_slice_const_range_unaligned() {
363		let b = CircuitBuilder::new();
364
365		// Create a ByteVec with 32 bytes capacity (4 words)
366		let byte_vec = ByteVec::new_witness(&b, 4);
367
368		// Extract unaligned slice: bytes 3-11 (spans across words 0 and 1)
369		let slice = byte_vec.slice_const_range(&b, 3..11);
370
371		assert_eq!(slice.data.len(), 1, "Slice should have 1 word");
372		// The test writes this directly, so pin it or pooling could reclaim its slot first.
373		b.force_commit(slice.data[0]);
374
375		let circuit = b.build();
376		let mut filler = circuit.new_witness_filler();
377
378		// Populate input with known data
379		let input_data: Vec<u8> = (0..32).map(|i| i as u8).collect();
380		byte_vec.populate_bytes_le(&mut filler, &input_data);
381
382		// Expected slice: bytes 3-10 (8 bytes total)
383		// Bytes: 03 04 05 06 07 08 09 0a
384		let expected_word = 0x0a09080706050403u64;
385		filler[slice.data[0]] = Word(expected_word);
386
387		circuit.populate_wire_witness(&mut filler).unwrap();
388
389		// Verify constraints
390		let cs = circuit.constraint_system();
391		cs.verify(&filler.into_value_vec()).unwrap();
392	}
393
394	#[test]
395	fn test_slice_const_range_partial_word() {
396		let b = CircuitBuilder::new();
397
398		// Create a ByteVec with 24 bytes capacity (3 words)
399		let byte_vec = ByteVec::new_witness(&b, 3);
400
401		// Extract partial word: bytes 0-5
402		let slice = byte_vec.slice_const_range(&b, 0..5);
403
404		assert_eq!(slice.data.len(), 1, "Slice should have 1 word (rounded up)");
405
406		let circuit = b.build();
407		let mut filler = circuit.new_witness_filler();
408
409		// Populate input with known data
410		let input_data: Vec<u8> = (0..24).map(|i| i as u8).collect();
411		byte_vec.populate_bytes_le(&mut filler, &input_data);
412
413		// Expected slice: bytes 0-7 of the source word. The slice length is 5, but the bytes past
414		// the length are no longer masked to zero (a `ByteVec` makes no guarantee about them), so
415		// the aligned extraction is the source word verbatim.
416		let expected_word = 0x0706050403020100u64; // Note: byte 0 is LSB
417		filler[slice.data[0]] = Word(expected_word);
418
419		circuit.populate_wire_witness(&mut filler).unwrap();
420
421		// Verify constraints
422		let cs = circuit.constraint_system();
423		cs.verify(&filler.into_value_vec()).unwrap();
424	}
425
426	#[test]
427	fn test_slice_const_range_unaligned_partial() {
428		let b = CircuitBuilder::new();
429
430		// Create a ByteVec with 24 bytes capacity (3 words)
431		let byte_vec = ByteVec::new_witness(&b, 3);
432
433		// Extract unaligned partial word: bytes 3-8 (5 bytes)
434		let slice = byte_vec.slice_const_range(&b, 3..8);
435
436		assert_eq!(slice.data.len(), 1, "Slice should have 1 word");
437		// The test writes this directly, so pin it or pooling could reclaim its slot first.
438		b.force_commit(slice.data[0]);
439
440		let circuit = b.build();
441		let mut filler = circuit.new_witness_filler();
442
443		// Populate input with known data
444		let input_data: Vec<u8> = (0..24).map(|i| i as u8).collect();
445		byte_vec.populate_bytes_le(&mut filler, &input_data);
446
447		// Expected slice: bytes 3-7 (5 bytes), shifted down by the 3-byte offset. The high 3 bytes
448		// happen to be zero here because the right shift fills with zeros (not because of any
449		// trailing-byte mask, which is no longer applied).
450		let expected_word = 0x0000000706050403u64; // bytes 03 04 05 06 07, LSB first
451		filler[slice.data[0]] = Word(expected_word);
452
453		circuit.populate_wire_witness(&mut filler).unwrap();
454
455		// Verify constraints
456		let cs = circuit.constraint_system();
457		cs.verify(&filler.into_value_vec()).unwrap();
458	}
459
460	#[test]
461	fn test_slice_const_range_empty() {
462		let b = CircuitBuilder::new();
463
464		// Create a ByteVec with 16 bytes capacity (2 words)
465		let byte_vec = ByteVec::new_witness(&b, 2);
466
467		// Extract empty slice
468		let slice = byte_vec.slice_const_range(&b, 5..5);
469
470		assert_eq!(slice.data.len(), 0, "Empty slice should have 0 words");
471
472		let circuit = b.build();
473		let mut filler = circuit.new_witness_filler();
474
475		// Populate input
476		let input_data: Vec<u8> = (0..16).map(|i| i as u8).collect();
477		byte_vec.populate_bytes_le(&mut filler, &input_data);
478
479		circuit.populate_wire_witness(&mut filler).unwrap();
480
481		// Verify constraints
482		let cs = circuit.constraint_system();
483		cs.verify(&filler.into_value_vec()).unwrap();
484	}
485
486	#[test]
487	fn test_slice_const_range_bounds_check_valid() {
488		let b = CircuitBuilder::new();
489
490		// Create a ByteVec with 16 bytes capacity (2 words)
491		let byte_vec = ByteVec::new_witness(&b, 2);
492
493		// Extract slice up to the full length
494		let slice = byte_vec.slice_const_range(&b, 0..16);
495
496		let circuit = b.build();
497		let mut filler = circuit.new_witness_filler();
498
499		// Populate with exactly 16 bytes
500		let input_data: Vec<u8> = (0..16).map(|i| i as u8).collect();
501		byte_vec.populate_bytes_le(&mut filler, &input_data);
502
503		// Manually populate slice data
504		for (i, chunk) in input_data.chunks(8).enumerate() {
505			let mut word = 0u64;
506			for (j, &byte) in chunk.iter().enumerate() {
507				word |= (byte as u64) << (j * 8);
508			}
509			filler[slice.data[i]] = Word(word);
510		}
511
512		// Should succeed: range.end (16) == len_bytes (16)
513		circuit.populate_wire_witness(&mut filler).unwrap();
514
515		// Verify constraints
516		let cs = circuit.constraint_system();
517		cs.verify(&filler.into_value_vec()).unwrap();
518	}
519
520	#[test]
521	fn test_slice_const_range_bounds_check_fail() {
522		let b = CircuitBuilder::new();
523
524		// Create a ByteVec with 16 bytes capacity (2 words)
525		let byte_vec = ByteVec::new_witness(&b, 2);
526
527		// Extract slice beyond capacity
528		let slice = byte_vec.slice_const_range(&b, 0..16);
529
530		let circuit = b.build();
531		let mut filler = circuit.new_witness_filler();
532
533		// Populate with only 10 bytes (less than range.end)
534		let input_data: Vec<u8> = (0..10).map(|i| i as u8).collect();
535		byte_vec.populate_bytes_le(&mut filler, &input_data);
536
537		// Manually populate slice data
538		for i in 0..2 {
539			let start = i * 8;
540			let end = (start + 8).min(input_data.len());
541			let mut word = 0u64;
542			if start < input_data.len() {
543				for (j, &byte) in input_data[start..end].iter().enumerate() {
544					word |= (byte as u64) << (j * 8);
545				}
546			}
547			filler[slice.data[i]] = Word(word);
548		}
549
550		// Should fail: range.end (16) > len_bytes (10)
551		let result = circuit.populate_wire_witness(&mut filler);
552		assert!(result.is_err(), "Should fail bounds check when range.end > len_bytes");
553	}
554
555	#[test]
556	fn test_slice_const_range_multi_word() {
557		let b = CircuitBuilder::new();
558
559		// Create a ByteVec with 32 bytes capacity (4 words)
560		let byte_vec = ByteVec::new_witness(&b, 4);
561
562		// Extract multi-word slice: bytes 5-21 (16 bytes, spans 3 source words)
563		let slice = byte_vec.slice_const_range(&b, 5..21);
564
565		assert_eq!(slice.data.len(), 2, "Slice should have 2 words");
566		// The test writes these directly, so pin them or pooling could reclaim their slots first.
567		for &wire in &slice.data {
568			b.force_commit(wire);
569		}
570
571		let circuit = b.build();
572		let mut filler = circuit.new_witness_filler();
573
574		// Populate input with known data
575		let input_data: Vec<u8> = (0..32).map(|i| i as u8).collect();
576		byte_vec.populate_bytes_le(&mut filler, &input_data);
577
578		// Extract bytes 5-20 manually for verification
579		let slice_bytes: Vec<u8> = input_data[5..21].to_vec();
580
581		// Pack into words
582		for (i, chunk) in slice_bytes.chunks(8).enumerate() {
583			let mut word = 0u64;
584			for (j, &byte) in chunk.iter().enumerate() {
585				word |= (byte as u64) << (j * 8);
586			}
587			filler[slice.data[i]] = Word(word);
588		}
589
590		circuit.populate_wire_witness(&mut filler).unwrap();
591
592		// Verify constraints
593		let cs = circuit.constraint_system();
594		cs.verify(&filler.into_value_vec()).unwrap();
595	}
596
597	#[test]
598	#[should_panic(expected = "Invalid range")]
599	#[allow(clippy::reversed_empty_ranges)]
600	fn test_slice_const_range_invalid_range() {
601		let b = CircuitBuilder::new();
602		let byte_vec = ByteVec::new_witness(&b, 4);
603
604		// Should panic: start > end
605		byte_vec.slice_const_range(&b, 10..5);
606	}
607
608	#[test]
609	#[should_panic(expected = "exceeds length bound")]
610	fn test_slice_const_range_exceeds_capacity() {
611		let b = CircuitBuilder::new();
612		let byte_vec = ByteVec::new_witness(&b, 2); // 16 bytes capacity, len_range 0..=16
613
614		// Should panic: range.end > len_range.end()
615		byte_vec.slice_const_range(&b, 0..20);
616	}
617
618	#[test]
619	fn test_slice_const_range_unaligned_at_capacity_boundary() {
620		// Test the edge case where we slice to the very end with non-zero byte offset
621		// This exercises the next_word bounds check
622		let b = CircuitBuilder::new();
623
624		// Create a ByteVec with 16 bytes capacity (2 words)
625		let byte_vec = ByteVec::new_witness(&b, 2);
626
627		// Extract unaligned slice all the way to the end: bytes 5-16
628		// This requires accessing a "next word" that doesn't exist
629		let slice = byte_vec.slice_const_range(&b, 5..16);
630
631		assert_eq!(slice.data.len(), 2, "Slice should have 2 words");
632		// The test writes these directly, so pin them or pooling could reclaim their slots first.
633		for &wire in &slice.data {
634			b.force_commit(wire);
635		}
636
637		let circuit = b.build();
638		let mut filler = circuit.new_witness_filler();
639
640		// Populate input with known data
641		let input_data: Vec<u8> = (0..16).map(|i| i as u8).collect();
642		byte_vec.populate_bytes_le(&mut filler, &input_data);
643
644		// Expected slice: bytes 5-15 (11 bytes)
645		let slice_bytes: Vec<u8> = input_data[5..16].to_vec();
646
647		// Pack into words
648		for (i, chunk) in slice_bytes.chunks(8).enumerate() {
649			let mut word = 0u64;
650			for (j, &byte) in chunk.iter().enumerate() {
651				word |= (byte as u64) << (j * 8);
652			}
653			filler[slice.data[i]] = Word(word);
654		}
655
656		circuit.populate_wire_witness(&mut filler).unwrap();
657
658		// Verify constraints
659		let cs = circuit.constraint_system();
660		cs.verify(&filler.into_value_vec()).unwrap();
661	}
662
663	#[test]
664	fn test_new_defaults_to_full_len_range() {
665		let b = CircuitBuilder::new();
666		let v = ByteVec::new_witness(&b, 4); // 4 wires = 32 bytes capacity
667		assert_eq!(v.len_range, 0..=32);
668	}
669
670	#[test]
671	fn test_new_const_len_sets_point_range() {
672		let b = CircuitBuilder::new();
673		let data = vec![b.add_witness(); 4]; // 32 bytes capacity
674		let v = ByteVec::new_const_len(&b, data, 18);
675		assert_eq!(v.len_range, 18..=18);
676	}
677
678	#[test]
679	#[should_panic(expected = "exceeds capacity")]
680	fn test_new_const_len_exceeds_capacity_panics() {
681		let b = CircuitBuilder::new();
682		let data = vec![b.add_witness(); 2]; // 16 bytes capacity
683		ByteVec::new_const_len(&b, data, 17);
684	}
685
686	#[test]
687	#[should_panic(expected = "exceeds capacity")]
688	fn test_new_with_len_range_end_exceeds_capacity_panics() {
689		let b = CircuitBuilder::new();
690		let data = vec![b.add_witness(); 2]; // 16 bytes capacity
691		let len_bytes = b.add_inout();
692		ByteVec::new_with_len_range(data, len_bytes, 0..=17);
693	}
694
695	#[test]
696	#[should_panic(expected = "outside len_range")]
697	fn test_populate_len_bytes_out_of_range_panics() {
698		let b = CircuitBuilder::new();
699		// Constant-length vector pins len_range to 8..8; populating any other length is a bug.
700		let v = ByteVec::new_const_len(&b, vec![b.add_witness(); 2], 8);
701		let circuit = b.build();
702		let mut filler = circuit.new_witness_filler();
703		v.populate_len_bytes(&mut filler, 9);
704	}
705
706	#[test]
707	fn test_slice_const_range_const_len_skips_runtime_check() {
708		// A const-length vec pins `len_bytes` structurally, so `slice_const_range` skips the
709		// runtime `range.end <= len_bytes` check. A sub-range slice must still build and verify.
710		let b = CircuitBuilder::new();
711		let data: Vec<_> = (0..2).map(|_| b.add_witness()).collect(); // 16 bytes capacity
712		let v = ByteVec::new_const_len(&b, data, 16);
713		let slice = v.slice_const_range(&b, 0..11);
714		assert_eq!(slice.data.len(), 2);
715
716		let circuit = b.build();
717		let mut filler = circuit.new_witness_filler();
718		v.populate_data(&mut filler, &(0..16).map(|i| i as u8).collect::<Vec<_>>());
719
720		circuit.populate_wire_witness(&mut filler).unwrap();
721		let cs = circuit.constraint_system();
722		cs.verify(&filler.into_value_vec()).unwrap();
723	}
724
725	#[test]
726	fn test_slice_const_range_propagates_point_len_range() {
727		let b = CircuitBuilder::new();
728		let v = ByteVec::new_witness(&b, 4);
729		let s = v.slice_const_range(&b, 3..11); // 8-byte slice
730		assert_eq!(s.len_range, 8..=8);
731	}
732}