Skip to main content

binius_math/field_buffer/
chunks.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! Aligned chunks of a field buffer.
5//!
6//! The buffer's chunk methods live here, alongside everything they hand out:
7//!
8//! ```text
9//! Chunks, ChunksMut  iterators over the buffer's aligned chunks
10//! ChunkMut           guard over one mutably borrowed chunk
11//! SubWordChunk       locating a chunk inside the word it shares with its neighbours
12//! ```
13//!
14//! # How a chunk is shaped
15//!
16//! A chunk of `2^k` elements starts at a multiple of its own size, so chunks of one size tile the
17//! buffer exactly, with none left over.
18//! Its size against the packing width decides which of two shapes it takes:
19//!
20//! ```text
21//! chunk >= one packed word  ->  a run of whole words, borrowed from the store
22//! chunk <  one packed word  ->  some lanes of one word, shared with its neighbours
23//! ```
24//!
25//! The second shape is why a chunk cannot always be lent out as a mutable slice of words.
26//! Lending one means copying its lanes out and merging them back when the borrow ends.
27
28use std::{
29	iter,
30	marker::PhantomData,
31	ops::{Deref, DerefMut, Range},
32	slice,
33};
34
35use binius_field::PackedField;
36use binius_utils::rayon::{iter::Either, prelude::*, slice::ParallelSlice};
37
38use super::{FieldBuffer, FieldSlice, FieldSliceData, FieldSliceMut};
39
40impl<P: PackedField, Data: Deref<Target = [P]>> FieldBuffer<P, Data> {
41	/// Get an aligned chunk of size `2^log_chunk_size`.
42	///
43	/// A chunk's start offset is a multiple of its size.
44	/// So this yields the same chunk that stepping the shared iterator to that index would.
45	///
46	/// # Preconditions
47	///
48	/// * `log_chunk_size` must be at most `log_len`.
49	/// * `chunk_index` must be less than the chunk count.
50	#[inline]
51	#[track_caller]
52	pub fn chunk(&self, log_chunk_size: usize, chunk_index: usize) -> FieldSlice<'_, P> {
53		assert!(
54			log_chunk_size <= self.log_len,
55			"precondition: log_chunk_size must be at most log_len"
56		);
57
58		let chunk_count = 1 << (self.log_len - log_chunk_size);
59		assert!(
60			chunk_index < chunk_count,
61			"precondition: chunk_index must be less than chunk_count"
62		);
63
64		let words = if log_chunk_size >= P::LOG_WIDTH {
65			// A whole run of words, borrowed straight from the store.
66			let words_per_chunk = 1 << (log_chunk_size - P::LOG_WIDTH);
67			FieldSliceData::Slice(&self.words[chunk_index * words_per_chunk..][..words_per_chunk])
68		} else {
69			// Lanes inside one word, copied out so the chunk starts at lane 0.
70			FieldSliceData::Single(
71				SubWordChunk::new(log_chunk_size, chunk_index).repack(&self.words),
72			)
73		};
74
75		FieldBuffer {
76			log_len: log_chunk_size,
77			words,
78		}
79	}
80
81	/// Split the buffer into chunks of size `2^log_chunk_size`.
82	///
83	/// Any size up to the buffer's length works, matching what the parallel iterator accepts.
84	/// A chunk of at least one packed word borrows a run of the store.
85	/// A smaller one arrives as a copy of its lanes, repacked to start at lane 0.
86	///
87	/// # Preconditions
88	///
89	/// * `log_chunk_size` must be at most `log_len`.
90	#[track_caller]
91	pub fn chunks(&self, log_chunk_size: usize) -> Chunks<'_, P> {
92		assert!(
93			log_chunk_size <= self.log_len,
94			"precondition: log_chunk_size must be at most log_len"
95		);
96
97		let chunk_count = 1 << (self.log_len - log_chunk_size);
98		Chunks::new(self.as_ref(), log_chunk_size, chunk_count)
99	}
100
101	/// Creates an iterator over chunks of size `2^log_chunk_size` in parallel.
102	///
103	/// # Preconditions
104	///
105	/// * `log_chunk_size` must be at most `log_len`.
106	#[track_caller]
107	pub fn par_chunks(
108		&self,
109		log_chunk_size: usize,
110	) -> impl IndexedParallelIterator<Item = FieldSlice<'_, P>> {
111		assert!(
112			log_chunk_size <= self.log_len,
113			"precondition: log_chunk_size must be at most log_len"
114		);
115
116		if log_chunk_size >= P::LOG_WIDTH {
117			// Each chunk spans one or more packed elements
118			let packed_chunk_size = 1 << (log_chunk_size - P::LOG_WIDTH);
119			Either::Left(
120				self.as_ref()
121					.par_chunks(packed_chunk_size)
122					.map(move |chunk| FieldBuffer {
123						log_len: log_chunk_size,
124						words: FieldSliceData::Slice(chunk),
125					}),
126			)
127		} else {
128			// Multiple chunks fit within a single packed element
129			let chunk_count = 1 << (self.log_len - log_chunk_size);
130			let words = self.as_ref();
131			Either::Right(
132				(0..chunk_count)
133					.into_par_iter()
134					.map(move |chunk_index| FieldBuffer {
135						log_len: log_chunk_size,
136						words: FieldSliceData::Single(
137							SubWordChunk::new(log_chunk_size, chunk_index).repack(words),
138						),
139					}),
140			)
141		}
142	}
143
144	/// Creates a parallel iterator over the scalars of each chunk of `2^log_chunk_size` elements.
145	///
146	/// The scalar-yielding counterpart to the parallel chunk iterator:
147	///
148	/// ```text
149	/// chunk i  ->  scalars [i * 2^log_chunk_size, (i+1) * 2^log_chunk_size)
150	/// ```
151	///
152	/// A chunk takes one of two shapes, chosen once before any scalar is read:
153	///
154	/// ```text
155	/// chunk >= one packed word  ->  a run of whole words
156	/// chunk <  one packed word  ->  a lane range inside a single word
157	/// ```
158	///
159	/// The buffer-yielding iterator instead repacks a sub-word chunk into an owned word.
160	/// Prefer this method when the consumer only reads scalars, since it copies nothing.
161	///
162	/// # Preconditions
163	///
164	/// * `log_chunk_size` must be at most `log_len`.
165	#[track_caller]
166	pub fn par_chunk_scalars(
167		&self,
168		log_chunk_size: usize,
169	) -> impl IndexedParallelIterator<Item: Iterator<Item = P::Scalar> + Send + Clone + '_> {
170		assert!(
171			log_chunk_size <= self.log_len,
172			"precondition: log_chunk_size must be at most log_len"
173		);
174
175		let words = self.as_ref();
176		if log_chunk_size >= P::LOG_WIDTH {
177			// A chunk is a run of whole words:
178			//
179			//     store = 2^(log_len - LOG_WIDTH) words
180			//     chunk = 2^(log_chunk_size - LOG_WIDTH) words
181			//
182			// Both counts are powers of two, so the runs tile the store with none left over.
183			let words_per_chunk = 1 << (log_chunk_size - P::LOG_WIDTH);
184			Either::Left(
185				words
186					.par_chunks(words_per_chunk)
187					.map(|chunk| Either::Left(P::iter_slice(chunk))),
188			)
189		} else {
190			// Several chunks share one word, so the count comes from the logical length:
191			//
192			//     log_len = 1, LOG_WIDTH = 2, log_chunk_size = 0
193			//     word = [s_0, s_1, dead, dead]  ->  chunks [s_0], [s_1]
194			//
195			// A buffer narrower than one word never turns its dead lanes into a chunk.
196			let chunk_count = 1 << (self.log_len - log_chunk_size);
197
198			Either::Right((0..chunk_count).into_par_iter().map(move |chunk_index| {
199				let chunk = SubWordChunk::new(log_chunk_size, chunk_index);
200				Either::Right(chunk.scalars(words[chunk.word_index()]))
201			}))
202		}
203	}
204}
205
206impl<P: PackedField, Data: DerefMut<Target = [P]>> FieldBuffer<P, Data> {
207	/// Split the buffer into mutable chunks of size `2^log_chunk_size`.
208	///
209	/// A chunk must span whole words, unlike the shared iterator, which takes any size.
210	/// Chunks below the packing width share a word, so lending them out means lending copies.
211	/// Each copy live at once would need its own write-back into that word.
212	///
213	/// ```text
214	/// WIDTH = 4, log_chunk_size = 1  ->  word 0 = [chunk 0 | chunk 1]
215	/// ```
216	///
217	/// Reach for the single-chunk mutable accessor at such a size, which guards one copy.
218	///
219	/// # Preconditions
220	///
221	/// * `log_chunk_size` must be at least `P::LOG_WIDTH` and at most `log_len`.
222	#[track_caller]
223	pub fn chunks_mut(&mut self, log_chunk_size: usize) -> ChunksMut<'_, P> {
224		assert!(
225			log_chunk_size >= P::LOG_WIDTH && log_chunk_size <= self.log_len,
226			"precondition: log_chunk_size must be in range [P::LOG_WIDTH, log_len]"
227		);
228
229		let chunk_count = 1 << (self.log_len - log_chunk_size);
230		ChunksMut::new(self.as_mut(), log_chunk_size, chunk_count)
231	}
232
233	/// Get a mutable aligned chunk of size `2^log_chunk_size`.
234	///
235	/// Addresses the same chunk as the shared accessor, and lends it mutably.
236	///
237	/// A chunk of at least one packed word is lent straight from the store, so edits land at once.
238	/// A smaller one shares a word with its neighbours, so it comes back behind a write-back guard.
239	/// The guard hands out a copy of the chunk's lanes and merges the edits back when it drops.
240	///
241	/// Unlike the mutable chunk iterator, this takes any chunk size up to the buffer's length.
242	/// Lending exactly one chunk is what makes a sub-word size safe.
243	/// A second live copy of the same word would merge over the first.
244	///
245	/// # Preconditions
246	///
247	/// * `log_chunk_size` must be at most `log_len`.
248	/// * `chunk_index` must be less than the chunk count.
249	#[track_caller]
250	pub fn chunk_mut(&mut self, log_chunk_size: usize, chunk_index: usize) -> ChunkMut<'_, P> {
251		assert!(
252			log_chunk_size <= self.log_len,
253			"precondition: log_chunk_size must be at most log_len"
254		);
255
256		let chunk_count = 1 << (self.log_len - log_chunk_size);
257		assert!(
258			chunk_index < chunk_count,
259			"precondition: chunk_index must be less than chunk_count"
260		);
261
262		// The chunk size against the packing width settles which shape the guard takes, and that
263		// is the guard's own business rather than the buffer's.
264		ChunkMut::new(log_chunk_size, chunk_index, &mut self.words)
265	}
266}
267
268/// The location of one chunk that is smaller than a packed word.
269///
270/// A chunk this small does not get a word to itself.
271/// Several of them share one word, so a chunk is a run of lanes inside it:
272///
273/// ```text
274/// WIDTH = 4, log_chunk_size = 1, so 2 chunks per word
275///
276/// chunk 0 -> word 0, lane 0     chunk 2 -> word 1, lane 0
277/// chunk 1 -> word 0, lane 2     chunk 3 -> word 1, lane 2
278/// ```
279///
280/// The chunk index splits in two to get there.
281/// High bits pick the word, low bits pick the lanes within it.
282///
283/// The packing width is part of the type.
284/// So a location can only ever be read against the packing it was computed for.
285#[derive(Debug, Clone, Copy)]
286struct SubWordChunk<P> {
287	/// Which word of the backing store holds the chunk.
288	word_index: usize,
289	/// Which lane of that word the chunk's first element sits at.
290	lane_offset: usize,
291	/// Element count of the chunk, as a base-2 logarithm.
292	log_len: usize,
293	/// Ties the arithmetic above to one packing width.
294	packing: PhantomData<P>,
295}
296
297impl<P: PackedField> SubWordChunk<P> {
298	/// Locates the chunk at `chunk_index` among chunks of `2^log_chunk_size` elements.
299	///
300	/// The size must be below the packing width, which is what makes several chunks share a word.
301	#[inline]
302	const fn new(log_chunk_size: usize, chunk_index: usize) -> Self {
303		let log_chunks_per_word = P::LOG_WIDTH - log_chunk_size;
304		let chunk_subindex = chunk_index & ((1 << log_chunks_per_word) - 1);
305		Self {
306			word_index: chunk_index >> log_chunks_per_word,
307			lane_offset: chunk_subindex << log_chunk_size,
308			log_len: log_chunk_size,
309			packing: PhantomData,
310		}
311	}
312
313	/// Which word of the backing store holds the chunk.
314	#[inline]
315	const fn word_index(self) -> usize {
316		self.word_index
317	}
318
319	/// Element count of the chunk, as a base-2 logarithm.
320	#[inline]
321	const fn log_len(self) -> usize {
322		self.log_len
323	}
324
325	/// Reads the chunk's elements out of the word holding it.
326	#[inline]
327	fn scalars(self, word: P) -> impl Iterator<Item = P::Scalar> + Send + Clone {
328		(0..1 << self.log_len).map(move |i| word.get(self.lane_offset | i))
329	}
330
331	/// Copies the chunk into a word of its own, elements starting at lane 0.
332	///
333	/// Lanes past the chunk come out zero, since packing from scalars starts from a zeroed word.
334	#[inline]
335	fn repack(self, words: &[P]) -> P {
336		P::from_scalars(self.scalars(words[self.word_index]))
337	}
338
339	/// Copies an edited chunk back into the lanes it came from.
340	///
341	/// The inverse of copying the chunk out.
342	/// Lane `i` of the chunk lands back at the lane it was read from.
343	///
344	/// ```text
345	/// WIDTH = 4, log_len = 1, lane_offset = 2
346	///
347	/// chunk  [ y z . . ]
348	/// word   [ a b y z ]   lanes 0 and 1 keep whatever they held
349	/// ```
350	///
351	/// Lanes of the word outside the chunk are left untouched, since neighbouring chunks own them.
352	#[inline]
353	fn merge_into(self, word: &mut P, chunk: &P) {
354		// The chunk's elements sit at lanes 0..2^log_len, so the loop walks exactly those.
355		for i in 0..1 << self.log_len {
356			// The lane offset is a multiple of the chunk length, so the bits below it are free.
357			// Setting them with an OR therefore addresses lane i of this chunk and no other.
358			word.set(self.lane_offset | i, chunk.get(i));
359		}
360	}
361}
362
363/// How a shared chunk iterator walks the store, settled by the chunk size before the first step.
364///
365/// ```text
366/// chunk >= one packed word  ->  Words, stepping runs of the store
367/// chunk <  one packed word  ->  Lanes, stepping chunk indices into one word each
368/// ```
369#[derive(Clone)]
370enum ChunkSource<'a, P: PackedField> {
371	/// Runs of words, one per chunk, cut off at the buffer's logical chunk count.
372	Words(iter::Take<slice::Chunks<'a, P>>),
373	/// Chunk indices to locate lanes with, alongside the store those lanes are repacked from.
374	Lanes {
375		words: &'a [P],
376		indices: Range<usize>,
377	},
378}
379
380/// Iterator over a buffer's chunks of a fixed size, each borrowed as a shared view.
381///
382/// Yielded by asking a buffer for its chunks.
383/// A chunk of at least one packed word is a borrowed run of words.
384/// A smaller one is a copy of its lanes, repacked to start at lane 0.
385pub struct Chunks<'a, P: PackedField> {
386	/// Where the next chunk comes from, one shape or the other for the whole iteration.
387	source: ChunkSource<'a, P>,
388	/// Element count of each chunk, as a base-2 logarithm.
389	log_chunk_size: usize,
390}
391
392impl<'a, P: PackedField> Chunks<'a, P> {
393	/// Builds the iterator over `words`, whose length must be the buffer's live word count.
394	#[inline]
395	fn new(words: &'a [P], log_chunk_size: usize, chunk_count: usize) -> Self {
396		let source = if log_chunk_size >= P::LOG_WIDTH {
397			let words_per_chunk = 1 << (log_chunk_size - P::LOG_WIDTH);
398			ChunkSource::Words(words.chunks(words_per_chunk).take(chunk_count))
399		} else {
400			ChunkSource::Lanes {
401				words,
402				indices: 0..chunk_count,
403			}
404		};
405		Self {
406			source,
407			log_chunk_size,
408		}
409	}
410}
411
412impl<'a, P: PackedField> Iterator for Chunks<'a, P> {
413	type Item = FieldSlice<'a, P>;
414
415	#[inline]
416	fn next(&mut self) -> Option<Self::Item> {
417		let words = match &mut self.source {
418			ChunkSource::Words(runs) => FieldSliceData::Slice(runs.next()?),
419			ChunkSource::Lanes { words, indices } => FieldSliceData::Single(
420				SubWordChunk::<P>::new(self.log_chunk_size, indices.next()?).repack(words),
421			),
422		};
423		Some(FieldBuffer {
424			log_len: self.log_chunk_size,
425			words,
426		})
427	}
428
429	#[inline]
430	fn size_hint(&self) -> (usize, Option<usize>) {
431		match &self.source {
432			ChunkSource::Words(runs) => runs.size_hint(),
433			ChunkSource::Lanes { indices, .. } => indices.size_hint(),
434		}
435	}
436}
437
438impl<P: PackedField> ExactSizeIterator for Chunks<'_, P> {}
439
440impl<P: PackedField> Clone for Chunks<'_, P> {
441	fn clone(&self) -> Self {
442		Self {
443			source: self.source.clone(),
444			log_chunk_size: self.log_chunk_size,
445		}
446	}
447}
448
449/// Iterator over a buffer's chunks of a fixed size, each borrowed as a mutable view.
450///
451/// The mutable counterpart of the shared chunk iterator, restricted to chunks of whole words.
452/// The chunks are disjoint, so each is lent out for the whole iteration rather than one at a time.
453pub struct ChunksMut<'a, P: PackedField> {
454	/// Runs of words, one per chunk, cut off at the buffer's logical chunk count.
455	runs: iter::Take<slice::ChunksMut<'a, P>>,
456	/// Element count of each chunk, as a base-2 logarithm.
457	log_chunk_size: usize,
458}
459
460impl<'a, P: PackedField> ChunksMut<'a, P> {
461	/// Builds the iterator over `words`, whose length must be the buffer's live word count.
462	#[inline]
463	fn new(words: &'a mut [P], log_chunk_size: usize, chunk_count: usize) -> Self {
464		let words_per_chunk = 1 << (log_chunk_size - P::LOG_WIDTH);
465		Self {
466			runs: words.chunks_mut(words_per_chunk).take(chunk_count),
467			log_chunk_size,
468		}
469	}
470}
471
472impl<'a, P: PackedField> Iterator for ChunksMut<'a, P> {
473	type Item = FieldSliceMut<'a, P>;
474
475	#[inline]
476	fn next(&mut self) -> Option<Self::Item> {
477		self.runs.next().map(|run| FieldBuffer {
478			log_len: self.log_chunk_size,
479			words: run,
480		})
481	}
482
483	#[inline]
484	fn size_hint(&self) -> (usize, Option<usize>) {
485		self.runs.size_hint()
486	}
487}
488
489impl<P: PackedField> ExactSizeIterator for ChunksMut<'_, P> {}
490
491/// Guards one mutably borrowed chunk of a buffer.
492///
493/// The chunk size against the packing width decides which of two shapes the guard takes:
494///
495/// ```text
496/// chunk >= one packed word  ->  a run of whole words, lent straight from the store
497/// chunk <  one packed word  ->  the chunk's lanes copied into a word of their own
498/// ```
499///
500/// The first shape edits the store itself.
501/// Every edit is therefore already in place, and dropping the guard does nothing.
502///
503/// The second shape edits a copy taken when the guard is built.
504/// That copy holds the chunk's lanes shifted down to start at lane 0.
505/// Dropping the guard writes them back to the lanes they came from, and nothing else.
506///
507/// ```text
508/// WIDTH = 4, chunks of 2 elements, chunk 1 of word 0
509///
510/// word before   [ a b c d ]
511/// detached      [ c d . . ]   lanes 2..4 copied down to lanes 0..2
512/// after edits   [ y z . . ]
513/// word on drop  [ a b y z ]   lanes 0..2 written back to lanes 2..4
514/// ```
515///
516/// So an edit to a sub-word chunk reaches the buffer when the guard drops, and not before.
517/// Neighbouring chunks sharing the word keep their elements, since the merge skips their lanes.
518///
519/// Only one such guard can exist at a time, since it borrows the buffer mutably.
520/// Two live guards over chunks of one word would each merge a stale copy.
521/// The later merge would then undo the earlier one.
522#[derive(Debug)]
523pub struct ChunkMut<'a, P: PackedField>(ChunkMutInner<'a, P>);
524
525impl<'a, P: PackedField> ChunkMut<'a, P> {
526	/// Guards chunk `chunk_index` of `2^log_chunk_size` elements, taken out of `words`.
527	fn new(log_chunk_size: usize, chunk_index: usize, words: &'a mut [P]) -> Self {
528		if log_chunk_size >= P::LOG_WIDTH {
529			// Whole words: the chunk is a run of the store, so it is lent out as it lies.
530			let words_per_chunk = 1 << (log_chunk_size - P::LOG_WIDTH);
531			let chunk = &mut words[chunk_index * words_per_chunk..][..words_per_chunk];
532			return Self(ChunkMutInner::Borrowed {
533				log_len: log_chunk_size,
534				chunk,
535			});
536		}
537
538		// Sub-word: several chunks share one word.
539		// This one is therefore copied into a word of its own, starting at lane 0.
540		//
541		//     WIDTH = 4, chunks of 2 elements
542		//     word 1 = [ chunk 2 | chunk 3 ]  ->  chunk 3 detaches to [ x y . . ]
543		let location = SubWordChunk::new(log_chunk_size, chunk_index);
544		let chunk = location.repack(words);
545
546		// The guard keeps the word the copy came from, and merges the copy back on drop.
547		let parent = &mut words[location.word_index()];
548		Self(ChunkMutInner::Detached {
549			location,
550			chunk,
551			parent,
552		})
553	}
554
555	/// Lends the chunk out as a mutable view, its first element at index 0.
556	///
557	/// A chunk of whole words is a view straight onto the store, so edits land at once.
558	/// A narrower chunk is a view onto the detached copy, so edits land when this guard drops.
559	pub const fn chunk(&mut self) -> FieldSliceMut<'_, P> {
560		match &mut self.0 {
561			// The copy is one word wide.
562			// So the view is that single word, cut down to the chunk's element count.
563			ChunkMutInner::Detached {
564				location,
565				chunk,
566				parent: _,
567			} => FieldBuffer {
568				log_len: location.log_len(),
569				words: slice::from_mut(chunk),
570			},
571			// The run of words is already the chunk, so the view spans all of it.
572			ChunkMutInner::Borrowed { log_len, chunk } => FieldBuffer {
573				log_len: *log_len,
574				words: chunk,
575			},
576		}
577	}
578}
579
580impl<P: PackedField> Drop for ChunkMut<'_, P> {
581	fn drop(&mut self) {
582		match &mut self.0 {
583			// A detached copy is the only shape with anything to write back.
584			ChunkMutInner::Detached {
585				location,
586				chunk,
587				parent,
588			} => location.merge_into(parent, chunk),
589			// A chunk lent from the store was edited in place, so there is nothing to merge.
590			ChunkMutInner::Borrowed { .. } => {}
591		}
592	}
593}
594
595/// The two shapes a mutably borrowed chunk takes, decided by its size against the packing width.
596#[derive(Debug)]
597enum ChunkMutInner<'a, P: PackedField> {
598	/// A chunk below one packed word, lifted out of the lanes it shares with its neighbours.
599	Detached {
600		/// Which lanes of the parent word the chunk occupies.
601		location: SubWordChunk<P>,
602		/// The detached copy the caller edits.
603		chunk: P,
604		/// The word the copy is merged back into.
605		parent: &'a mut P,
606	},
607	/// A chunk of one or more whole words, borrowed from the store and edited there.
608	Borrowed {
609		/// Element count of the chunk, as a base-2 logarithm.
610		log_len: usize,
611		/// The run of words the chunk occupies.
612		chunk: &'a mut [P],
613	},
614}
615
616#[cfg(test)]
617mod tests {
618	use proptest::prelude::*;
619	use rand::{SeedableRng, rngs::StdRng};
620
621	use super::*;
622	use crate::test_utils::{B128, Packed128b, random_field_buffer};
623
624	type P = Packed128b;
625	type F = B128;
626
627	#[test]
628	fn chunk() {
629		let log_len = 8;
630		let values: Vec<F> = (0..1 << log_len).map(F::new).collect();
631		let buffer = FieldBuffer::<P>::from_values(&values);
632
633		for log_chunk_size in 0..=log_len {
634			let chunk_count = 1 << (log_len - log_chunk_size);
635
636			for chunk_index in 0..chunk_count {
637				let chunk = buffer.chunk(log_chunk_size, chunk_index);
638				for i in 0..1 << log_chunk_size {
639					assert_eq!(chunk.get(i), buffer.get(chunk_index << log_chunk_size | i));
640				}
641			}
642		}
643	}
644
645	#[test]
646	#[should_panic(expected = "precondition")]
647	fn chunk_invalid_size() {
648		let log_len = 8;
649		let values: Vec<F> = (0..1 << log_len).map(F::new).collect();
650		let buffer = FieldBuffer::<P>::from_values(&values);
651		let _ = buffer.chunk(log_len + 1, 0);
652	}
653
654	#[test]
655	#[should_panic(expected = "precondition")]
656	fn chunk_invalid_index() {
657		let log_len = 8;
658		let values: Vec<F> = (0..1 << log_len).map(F::new).collect();
659		let buffer = FieldBuffer::<P>::from_values(&values);
660		let _ = buffer.chunk(4, 1 << (log_len - 4)); // out of range
661	}
662
663	#[test]
664	fn chunk_mut() {
665		// A packed word holds 4 lanes, so log_len 4 fills exactly 4 words.
666		// The sweep below therefore straddles the word boundary:
667		//
668		//     log_chunk_size 0  ->  1 element    ->  4 chunks share one word
669		//     log_chunk_size 1  ->  2 elements   ->  2 chunks share one word
670		//     log_chunk_size 2  ->  4 elements   ->  one whole word each
671		//     log_chunk_size 3  ->  8 elements   ->  two whole words each
672		//     log_chunk_size 4  ->  16 elements  ->  the whole 4-word store
673		let log_len = 4;
674		let values: Vec<F> = (0..1u128 << log_len).map(F::new).collect();
675
676		for log_chunk_size in 0..=log_len {
677			let mut buffer = FieldBuffer::<P>::from_values(&values);
678			let chunk_count = 1 << (log_len - log_chunk_size);
679
680			// Rewrite every element through its own chunk, one guard at a time.
681			// Each guard merges before the next detaches, so chunks sharing a word chain safely.
682			for chunk_index in 0..chunk_count {
683				let mut guard = buffer.chunk_mut(log_chunk_size, chunk_index);
684				let mut chunk = guard.chunk();
685
686				// The view is the chunk alone, so its indices run from 0 whatever the shape.
687				assert_eq!(chunk.len(), 1 << log_chunk_size);
688				for i in 0..1 << log_chunk_size {
689					let old = u128::from(chunk.get(i).val());
690					chunk.set(i, F::new(old * 10));
691				}
692			}
693
694			// Every element comes back scaled, including those sharing a word with a neighbour.
695			for index in 0..1 << log_len {
696				assert_eq!(
697					buffer.get(index),
698					F::new(index as u128 * 10),
699					"log_chunk_size={log_chunk_size}, index={index}"
700				);
701			}
702		}
703
704		// A sub-word guard must write back its own lanes and leave the rest of the word alone.
705		//
706		//     word 0 = [ 0 1 2 3 ], chunks of 2 elements
707		//     chunk 1 = elements 2..4, so lanes 0..2 must survive untouched
708		let mut buffer = FieldBuffer::<P>::from_values(&values);
709		{
710			let mut guard = buffer.chunk_mut(1, 1);
711			let mut chunk = guard.chunk();
712			chunk.set(0, F::new(70));
713			chunk.set(1, F::new(80));
714		}
715		assert_eq!(buffer.get(0), F::new(0));
716		assert_eq!(buffer.get(1), F::new(1));
717		assert_eq!(buffer.get(2), F::new(70));
718		assert_eq!(buffer.get(3), F::new(80));
719
720		// The words past the edited one are untouched as well.
721		for index in 4..16 {
722			assert_eq!(buffer.get(index), F::new(index as u128));
723		}
724
725		// A buffer shorter than one packed word still splits into sub-word chunks.
726		// 2 elements live in a 4-lane word, so the 2 dead lanes must not become elements.
727		let mut small = FieldBuffer::<P>::from_values(&[F::new(10), F::new(20)]);
728		{
729			let mut guard = small.chunk_mut(0, 1);
730			guard.chunk().set(0, F::new(21));
731		}
732		assert_eq!(small.len(), 2);
733		assert_eq!(small.iter_scalars().collect::<Vec<_>>(), vec![F::new(10), F::new(21)]);
734
735		// A whole-word guard writes into the store directly, and touches no neighbouring word.
736		let mut buffer = FieldBuffer::<P>::from_values(&values);
737		{
738			let mut guard = buffer.chunk_mut(3, 1); // elements 8..16, words 2 and 3
739			let mut chunk = guard.chunk();
740			for i in 0..8 {
741				chunk.set(i, F::new(100 + i as u128));
742			}
743		}
744		for index in 0..8 {
745			assert_eq!(buffer.get(index), F::new(index as u128));
746		}
747		for index in 8..16 {
748			assert_eq!(buffer.get(index), F::new(100 + (index - 8) as u128));
749		}
750	}
751
752	#[test]
753	#[should_panic(expected = "precondition")]
754	fn chunk_mut_invalid_size() {
755		let mut buffer = FieldBuffer::<P>::zeros(4);
756		// 5 > 4: no chunk that size exists in a 16-element buffer.
757		let _ = buffer.chunk_mut(5, 0);
758	}
759
760	#[test]
761	#[should_panic(expected = "precondition")]
762	fn chunk_mut_invalid_index() {
763		let mut buffer = FieldBuffer::<P>::zeros(4);
764		// 16 elements in chunks of 4 gives 4 chunks, indexed 0..4.
765		let _ = buffer.chunk_mut(2, 4);
766	}
767
768	#[test]
769	fn chunks() {
770		let values: Vec<F> = (0..16).map(F::new).collect();
771		let buffer = FieldBuffer::<P>::from_values(&values);
772
773		// Split into 4 chunks of size 4
774		let chunks: Vec<_> = buffer.chunks(2).collect();
775		assert_eq!(chunks.len(), 4);
776
777		for (chunk_idx, chunk) in chunks.into_iter().enumerate() {
778			assert_eq!(chunk.len(), 4);
779			for i in 0..4 {
780				let expected = F::new((chunk_idx * 4 + i) as u128);
781				assert_eq!(chunk.get(i), expected);
782			}
783		}
784	}
785
786	#[test]
787	#[should_panic(expected = "precondition")]
788	fn chunks_invalid_size_too_large() {
789		let values: Vec<F> = (0..16).map(F::new).collect();
790		let buffer = FieldBuffer::<P>::from_values(&values);
791		let _ = buffer.chunks(5).collect::<Vec<_>>();
792	}
793
794	#[test]
795	fn chunks_below_packing_width() {
796		// A word holds 4 lanes, so pairs of elements share one:
797		//
798		//     word 0 = [s_0, s_1, s_2, s_3]  ->  chunks [s_0, s_1], [s_2, s_3]
799		let values: Vec<F> = (0..16).map(F::new).collect();
800		let buffer = FieldBuffer::<P>::from_values(&values);
801
802		let chunks: Vec<_> = buffer.chunks(1).collect();
803		assert_eq!(chunks.len(), 8);
804
805		// Each chunk owns a repacked word, so its two elements sit at lanes 0 and 1.
806		for (chunk_idx, chunk) in chunks.into_iter().enumerate() {
807			assert_eq!(chunk.len(), 2);
808			assert_eq!(chunk.get(0), F::new((chunk_idx * 2) as u128));
809			assert_eq!(chunk.get(1), F::new((chunk_idx * 2 + 1) as u128));
810		}
811
812		// A single-element chunk is the narrowest shape, one lane copied out on its own.
813		let singles: Vec<_> = buffer.chunks(0).collect();
814		assert_eq!(singles.len(), 16);
815		for (index, chunk) in singles.into_iter().enumerate() {
816			assert_eq!(chunk.len(), 1);
817			assert_eq!(chunk.get(0), F::new(index as u128));
818		}
819	}
820
821	#[test]
822	fn par_chunks() {
823		let values: Vec<F> = (0..16).map(F::new).collect();
824		let buffer = FieldBuffer::<P>::from_values(&values);
825
826		// Split into 4 chunks of size 4
827		let chunks: Vec<_> = buffer.par_chunks(2).collect();
828		assert_eq!(chunks.len(), 4);
829
830		for (chunk_idx, chunk) in chunks.into_iter().enumerate() {
831			assert_eq!(chunk.len(), 4);
832			for i in 0..4 {
833				let expected = F::new((chunk_idx * 4 + i) as u128);
834				assert_eq!(chunk.get(i), expected);
835			}
836		}
837
838		// Test small chunk sizes (below P::LOG_WIDTH)
839		// P::LOG_WIDTH = 2, so par_chunks(0) and par_chunks(1) should work
840		// Split into 8 chunks of size 2 (log_chunk_size = 1)
841		let chunks: Vec<_> = buffer.par_chunks(1).collect();
842		assert_eq!(chunks.len(), 8);
843		for (chunk_idx, chunk) in chunks.into_iter().enumerate() {
844			assert_eq!(chunk.len(), 2);
845			for i in 0..2 {
846				let expected = F::new((chunk_idx * 2 + i) as u128);
847				assert_eq!(chunk.get(i), expected);
848			}
849		}
850
851		// Split into 16 chunks of size 1 (log_chunk_size = 0)
852		let chunks: Vec<_> = buffer.par_chunks(0).collect();
853		assert_eq!(chunks.len(), 16);
854		for (chunk_idx, chunk) in chunks.into_iter().enumerate() {
855			assert_eq!(chunk.len(), 1);
856			let expected = F::new(chunk_idx as u128);
857			assert_eq!(chunk.get(0), expected);
858		}
859	}
860
861	#[test]
862	fn par_chunk_scalars_ignores_dead_lanes() {
863		// Fixture state: 2 scalars occupy one 4-lane word, leaving two lanes dead.
864		//
865		//     word = [s_0, s_1, dead, dead]
866		let values: Vec<F> = (0..2).map(F::new).collect();
867		let buffer = FieldBuffer::<P>::from_values(&values);
868
869		// One scalar per chunk: 2 live scalars give 2 chunks, not the word's 4 lanes.
870		let chunks: Vec<Vec<F>> = buffer
871			.par_chunk_scalars(0)
872			.map(|chunk| chunk.collect())
873			.collect();
874		assert_eq!(chunks, vec![vec![values[0]], vec![values[1]]]);
875	}
876
877	#[test]
878	#[should_panic(expected = "precondition")]
879	fn par_chunks_invalid_size() {
880		let values: Vec<F> = (0..16).map(F::new).collect();
881		let buffer = FieldBuffer::<P>::from_values(&values);
882		let _ = buffer.par_chunks(5).collect::<Vec<_>>();
883	}
884
885	#[test]
886	fn chunks_mut() {
887		let mut buffer = FieldBuffer::<P>::zeros(4); // 16 elements
888
889		// Modify via chunks
890		let mut chunks: Vec<_> = buffer.chunks_mut(2).collect();
891		assert_eq!(chunks.len(), 4);
892
893		for (chunk_idx, chunk) in chunks.iter_mut().enumerate() {
894			for i in 0..chunk.len() {
895				chunk.set(i, F::new((chunk_idx * 10 + i) as u128));
896			}
897		}
898
899		// Verify modifications
900		for chunk_idx in 0..4 {
901			for i in 0..4 {
902				let expected = F::new((chunk_idx * 10 + i) as u128);
903				assert_eq!(buffer.get(chunk_idx * 4 + i), expected);
904			}
905		}
906	}
907
908	#[test]
909	#[should_panic(expected = "precondition")]
910	fn chunks_mut_invalid_size() {
911		let mut buffer = FieldBuffer::<P>::zeros(4); // 16 elements
912		let _ = buffer.chunks_mut(0).collect::<Vec<_>>();
913	}
914
915	proptest! {
916		#[test]
917		fn par_chunk_scalars_partitions_the_buffer(
918			(log_len, log_chunk_size) in (0usize..=6).prop_flat_map(|n| (Just(n), 0usize..=n)),
919		) {
920			// Invariant: the chunks tile the buffer exactly.
921			// The sweep reaches chunk sizes on both sides of the packing width.
922			let values: Vec<F> = (0..1u128 << log_len).map(F::new).collect();
923			let buffer = FieldBuffer::<P>::from_values(&values);
924
925			let chunks: Vec<Vec<F>> = buffer
926				.par_chunk_scalars(log_chunk_size)
927				.map(|chunk| chunk.collect())
928				.collect();
929
930			// The count follows the logical length, not the backing word count.
931			prop_assert_eq!(chunks.len(), 1 << (log_len - log_chunk_size));
932
933			// Cross-check: each chunk matches what the serial accessor returns at the same index.
934			for (index, scalars) in chunks.iter().enumerate() {
935				let expected: Vec<F> = buffer.chunk(log_chunk_size, index).iter_scalars().collect();
936				prop_assert_eq!(scalars, &expected);
937			}
938
939			// Concatenating the chunks reproduces the buffer, in order and without gaps.
940			prop_assert_eq!(chunks.concat(), values);
941		}
942
943		#[test]
944		fn chunk_mut_merges_like_direct_writes(
945			(log_len, log_chunk_size, chunk_index) in (0usize..=6)
946				.prop_flat_map(|log_len| (Just(log_len), 0usize..=log_len))
947				.prop_flat_map(|(log_len, log_chunk_size)| {
948					(Just(log_len), Just(log_chunk_size), 0usize..1 << (log_len - log_chunk_size))
949				}),
950			seed in any::<u64>(),
951		) {
952			// Invariant: a chunk edited through its guard equals the same scalars written by index.
953			//
954			// A packed word holds 4 lanes, so the sweep covers chunk sizes on both sides of it.
955			// Exactly one chunk is edited here.
956			// The elements left alone then pin that the merge stays inside the chunk's lanes.
957			let mut rng = StdRng::seed_from_u64(seed);
958			let original = random_field_buffer::<P>(&mut rng, log_len);
959
960			// Fresh scalars, one per element of the chosen chunk, distinct from a random fill.
961			let replacements: Vec<F> = (0..1u128 << log_chunk_size)
962				.map(|i| F::new(i * 7 + 1))
963				.collect();
964
965			// Path under test: one guard over one chunk, merged when it drops.
966			let mut guarded = original.clone();
967			{
968				let mut guard = guarded.chunk_mut(log_chunk_size, chunk_index);
969				let mut chunk = guard.chunk();
970				for (i, &value) in replacements.iter().enumerate() {
971					chunk.set(i, value);
972				}
973			}
974
975			// Reference path: the same scalars written straight in, at the indices the chunk spans.
976			let mut direct = original;
977			for (i, &value) in replacements.iter().enumerate() {
978				direct.set(chunk_index << log_chunk_size | i, value);
979			}
980
981			// Equality compares every live scalar, so this covers the edited and untouched alike.
982			prop_assert_eq!(guarded, direct);
983		}
984
985		#[test]
986		fn serial_and_parallel_chunks_agree(
987			(log_len, log_chunk_size) in (0usize..=8).prop_flat_map(|n| (Just(n), 0usize..=n)),
988		) {
989			// Invariant: both chunk iterators yield the same chunks, scalar for scalar.
990			// A packed word holds 4 lanes, so the sweep reaches sizes on both sides of it.
991			let values: Vec<F> = (0..1u128 << log_len).map(F::new).collect();
992			let buffer = FieldBuffer::<P>::from_values(&values);
993
994			let serial: Vec<Vec<F>> = buffer
995				.chunks(log_chunk_size)
996				.map(|chunk| chunk.iter_scalars().collect())
997				.collect();
998			let parallel: Vec<Vec<F>> = buffer
999				.par_chunks(log_chunk_size)
1000				.map(|chunk| chunk.iter_scalars().collect())
1001				.collect();
1002
1003			// The count follows the logical length, not the backing word count.
1004			prop_assert_eq!(serial.len(), 1 << (log_len - log_chunk_size));
1005			prop_assert_eq!(&serial, &parallel);
1006
1007			// Concatenating the chunks reproduces the buffer, in order and without gaps.
1008			prop_assert_eq!(serial.concat(), values);
1009		}
1010	}
1011}