fgf/bits.rs
1//! Bit-packed GF(2) buffers: one element per bit.
2//!
3//! GF(2) is the one field whose natural element width is sub-byte, so it does
4//! not fit the byte-per-element [`crate::ops`] surface. Instead, elements are
5//! packed eight per byte and the operations here are whole-buffer or masked
6//! bit ranges over plain `&[u8]`:
7//!
8//! - element `i` lives in **bit `i % 8` (LSB-first) of byte `i / 8`**;
9//! - a word is the little-endian `u64` assembly of eight bytes, so element
10//! `i` is bit `i % 64` of word `i / 64`;
11//! - bits past the logical length are **padding**. The invariant is that
12//! they are zero; it is *caller-maintained*. Range operations leave bits
13//! outside their range untouched. Whole-buffer operations also process
14//! padding and preserve zero padding when their inputs satisfy the
15//! invariant. No operation promises to sanitize nonzero padding.
16//!
17//! Because a byte holds eight elements and the last byte may be partial, the
18//! element count is not recoverable from the byte length: operations whose
19//! meaning depends on the exact bit count take an explicit `bits`, and every
20//! range operation checks `from <= to <= bits`. Buffers may be longer than
21//! [`bytes_for`]`(bits)` — a consumer with word-aligned storage passes its
22//! whole row and the surplus bits are padding like any other.
23//!
24//! Multiplying by a coefficient is masked out of the API on purpose: the
25//! coefficient of GF(2) is a bit — multiply by zero is "skip", by one is
26//! "XOR" — so there is no prepared-coefficient form and no [`crate::ops`]
27//! counterpart. What *is* prepared is geometry: a [`XorRange`] derives a
28//! bit range's window and masks once for [`xor_range_with`] to apply to
29//! many buffer pairs. The scalar oracle for this surface is
30//! [`crate::gf2::Elem`](crate::field::gf2::Elem).
31//!
32//! ```
33//! use fgf::bits;
34//! use fgf::field::Elem;
35//! // Seven elements: 1,0,0,1,1,1,1 -> LSB-first byte 0b0111_1001.
36//! let mut a = [0u8; 1];
37//! bits::set_range(&mut a, 7, 0, 7);
38//! bits::clear_range(&mut a, 7, 1, 3);
39//! assert_eq!(a[0], 0b0111_1001);
40//! assert_eq!(bits::weight(&a, 7), 5);
41//!
42//! // The GF(2) inner product of a vector with itself is its weight mod 2.
43//! let parity = bits::dot_product(&a, &a, 7);
44//! assert!(parity.is_one());
45//!
46//! // Whole-buffer addition is XOR of equal-length buffers.
47//! let b = [0xff];
48//! bits::xor_assign(&mut a, &b);
49//! assert_eq!(bits::weight(&a, 7), 2);
50//! ```
51
52use crate::field::gf2;
53use crate::kernel;
54
55/// Number of bytes needed to hold `bits` packed elements.
56///
57/// ```text
58/// bits 0 1 8 9 64 65
59/// bytes 0 1 1 2 8 9
60/// ```
61#[must_use]
62pub const fn bytes_for(bits: usize) -> usize {
63 bits.div_ceil(8)
64}
65
66#[inline]
67fn check_pair(name: &str, left_name: &str, left: usize, right_name: &str, right: usize) {
68 assert_eq!(
69 left, right,
70 "{name}: {left_name} is {left} bytes but {right_name} is {right} bytes"
71 );
72}
73
74#[inline]
75fn check_range(name: &str, bits: usize, from: usize, to: usize) {
76 assert!(from <= to, "{name}: range start {from} exceeds end {to}");
77 assert!(
78 to <= bits,
79 "{name}: range end {to} exceeds the {bits} live bits"
80 );
81}
82
83#[inline]
84fn check_holds(name: &str, operand: &str, len: usize, bits: usize) {
85 assert!(
86 bytes_for(bits) <= len,
87 "{name}: {bits} bits need at least {} bytes but {operand} holds {len}",
88 bytes_for(bits)
89 );
90}
91
92/// `dst ^= src`: GF(2) addition over whole buffers.
93///
94/// XOR of zero padding stays zero, so no bit count is needed. Buffers must
95/// not alias.
96///
97/// # Panics
98/// Panics if the slices differ in length.
99pub fn xor_assign(dst: &mut [u8], src: &[u8]) {
100 check_pair("bits::xor_assign", "dst", dst.len(), "src", src.len());
101 kernel::gf2::xor(dst, src);
102}
103
104/// `dst ^= src` over the bit range `[from, to)`.
105///
106/// Bits outside the range — including padding — are untouched. `dst` and
107/// `src` must not alias.
108///
109/// The checked one-shot form of [`xor_range_with`]: an operation applied
110/// once pays one construction; an operation applied to many buffer pairs
111/// over the same range should prepare a [`XorRange`] instead.
112///
113/// # Panics
114/// Panics if the slices differ in length, if `from > to`, if `to > bits`,
115/// or if the buffers are shorter than [`bytes_for`]`(bits)`.
116#[inline]
117pub fn xor_range(dst: &mut [u8], src: &[u8], bits: usize, from: usize, to: usize) {
118 check_pair("bits::xor_range", "dst", dst.len(), "src", src.len());
119 check_range("bits::xor_range", bits, from, to);
120 check_holds("bits::xor_range", "dst", dst.len(), bits);
121 xor_range_with(dst, src, &XorRange::new(bits, from, to));
122}
123
124/// A prepared bit range for repeated `dst ^= src`: the window and end
125/// masks of `[from, to)` derived once, applied to many buffer pairs.
126///
127/// The prepared form of [`xor_range`], in the same split [`crate::ops`]
128/// uses for prepared coefficients: an elimination that XORs the same
129/// column range into many rows pays the range checks and mask arithmetic
130/// once per range instead of once per row. An empty range is valid and
131/// [`xor_range_with`] applies it as a no-op.
132/// The plan is independent of the buffers it is applied to: buffers may be
133/// longer than the planned window (the surplus is padding like any other),
134/// and bits outside the window are untouched.
135///
136/// That is the flexibility the prepared form buys over the one-shot
137/// [`xor_range`]: the one-shot validates `from <= to <= bits` against the
138/// logical bit count of every call, while a prepared range is validated
139/// once — against its own `[from, to)` at construction — and then applies
140/// to any buffer pair covering its window, whatever each buffer's logical
141/// length. A caller folding the same column range into many equal-length
142/// rows derives the window and masks exactly once.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct XorRange {
145 window: kernel::gf2::Window,
146}
147
148impl XorRange {
149 /// Prepares the bit range `[from, to)` of a vector `bits` long.
150 ///
151 /// # Panics
152 ///
153 /// Panics if `from > to` or `to > bits`.
154 #[must_use]
155 pub fn new(bits: usize, from: usize, to: usize) -> Self {
156 check_range("XorRange::new", bits, from, to);
157 Self {
158 window: kernel::gf2::window(from, to),
159 }
160 }
161}
162
163/// `dst ^= src` over a prepared [`XorRange`]: the per-row apply of the
164/// split form of [`xor_range`].
165///
166/// Bits outside the planned window — including padding — are untouched.
167/// `dst` and `src` must not alias. The buffers may differ in length; each
168/// must cover the planned window.
169///
170/// # Panics
171///
172/// Panics if either buffer is shorter than the planned window.
173#[inline]
174pub fn xor_range_with(dst: &mut [u8], src: &[u8], range: &XorRange) {
175 let end = range.window.end;
176 if end == 0 {
177 return;
178 }
179 assert!(
180 end <= dst.len() && end <= src.len(),
181 "bits::xor_range_with: the planned range needs {end} bytes but dst holds {} and src holds {}",
182 dst.len(),
183 src.len()
184 );
185 kernel::gf2::xor_window(dst, src, &range.window);
186}
187
188/// `dst = a & b`: elementwise GF(2) multiplication.
189///
190/// AND of zero padding stays zero, so no bit count is needed. The output
191/// buffers may alias neither input.
192///
193/// # Panics
194/// Panics if the slices differ in length.
195pub fn and_into(dst: &mut [u8], a: &[u8], b: &[u8]) {
196 check_pair("bits::and_into", "dst", dst.len(), "a", a.len());
197 check_pair("bits::and_into", "a", a.len(), "b", b.len());
198 kernel::gf2::and_into(dst, a, b);
199}
200
201/// `dst &= src`: in-place elementwise GF(2) multiplication.
202///
203/// # Panics
204/// Panics if the slices differ in length.
205pub fn and_assign(dst: &mut [u8], src: &[u8]) {
206 check_pair("bits::and_assign", "dst", dst.len(), "src", src.len());
207 kernel::gf2::and_assign(dst, src);
208}
209
210/// `dst &= !mask`: clears every bit of `dst` where `mask` has a 1.
211///
212/// The mask's padding is irrelevant; zero bits of `dst` stay zero, so the
213/// padding invariant holds regardless.
214///
215/// # Panics
216/// Panics if the slices differ in length.
217pub fn andnot_assign(dst: &mut [u8], mask: &[u8]) {
218 check_pair("bits::andnot_assign", "dst", dst.len(), "mask", mask.len());
219 kernel::gf2::andnot_assign(dst, mask);
220}
221
222/// Zeros the bit range `[from, to)` of `dst`.
223///
224/// # Panics
225/// Panics if `from > to`, if `to > bits`, or if `dst` is shorter than
226/// [`bytes_for`]`(bits)`.
227#[inline]
228pub fn clear_range(dst: &mut [u8], bits: usize, from: usize, to: usize) {
229 check_range("bits::clear_range", bits, from, to);
230 check_holds("bits::clear_range", "dst", dst.len(), bits);
231 kernel::gf2::clear_range(dst, from, to);
232}
233
234/// Sets the bit range `[from, to)` of `dst` to one.
235///
236/// # Panics
237/// Panics if `from > to`, if `to > bits`, or if `dst` is shorter than
238/// [`bytes_for`]`(bits)`.
239pub fn set_range(dst: &mut [u8], bits: usize, from: usize, to: usize) {
240 check_range("bits::set_range", bits, from, to);
241 check_holds("bits::set_range", "dst", dst.len(), bits);
242 kernel::gf2::set_range(dst, from, to);
243}
244
245/// Hamming weight of the live bits `[0, bits)`.
246///
247/// Padding bits never count, even when the buffer holds more bytes than
248/// [`bytes_for`]`(bits)`. The count is `usize`, which no buffer can
249/// overflow; the underlying `u32` popcount kernel is exact only while a
250/// segment holds at most `2^32 − 1` set bits, so the count is summed over
251/// byte-aligned segments that each stay inside that range.
252///
253/// # Panics
254/// Panics if `buf` is shorter than [`bytes_for`]`(bits)`.
255#[must_use]
256pub fn weight(buf: &[u8], bits: usize) -> usize {
257 const SEGMENT_BITS: usize = (u32::MAX as usize / 8) * 8;
258 check_holds("bits::weight", "buf", buf.len(), bits);
259 // The shared kernel folds into `u32` accumulators, exact for any
260 // segment whose set bits fit a `u32`. Split on byte-aligned segments
261 // (the kernel is byte-indexed from the slice start) and sum in `usize`.
262 let mut total = 0usize;
263 let mut done = 0;
264 while done < bits {
265 let segment = (bits - done).min(SEGMENT_BITS);
266 // Each segment carries at most SEGMENT_BITS ≤ u32::MAX set bits, so
267 // the `u32` count widens to `usize` without loss on every target
268 // this crate builds for (`u32 as usize` is total for the value).
269 total += kernel::gf2::weight(&buf[done / 8..], segment) as usize;
270 done += segment;
271 }
272 total
273}
274
275/// The GF(2) inner product of two packed vectors: the parity of
276/// `popcount(a & b)` over the live bits.
277///
278/// Returned as a [`gf2::Elem`]; use
279/// [`is_one`](crate::field::Elem::is_one) to test it. Padding bits
280/// contribute nothing.
281///
282/// # Panics
283/// Panics if the slices differ in length or are shorter than
284/// [`bytes_for`]`(bits)`.
285#[must_use]
286pub fn dot_product(a: &[u8], b: &[u8], bits: usize) -> gf2::Elem {
287 check_pair("bits::dot_product", "a", a.len(), "b", b.len());
288 check_holds("bits::dot_product", "a", a.len(), bits);
289 #[allow(clippy::cast_possible_truncation)] // parity is exactly 0 or 1
290 gf2::Elem::from_raw(kernel::gf2::parity(a, b, bits) as u8)
291}
292
293/// `dst ^= srcs[i]` for every set bit `i` of `selector`: the combination
294/// step of subset-XOR over a dense row set.
295///
296/// An empty selector is a no-op. `dst` must not alias any source.
297///
298/// # Panics
299/// Panics if any source differs in length from `dst`, if `selector` names a
300/// source at or beyond `srcs.len()`, or if `dst` is shorter than
301/// [`bytes_for`]`(bits)`.
302pub fn xor_gather(dst: &mut [u8], bits: usize, selector: u64, srcs: &[&[u8]]) {
303 check_holds("bits::xor_gather", "dst", dst.len(), bits);
304 for (index, &src) in srcs.iter().enumerate() {
305 assert_eq!(
306 src.len(),
307 dst.len(),
308 "bits::xor_gather: source {index} is {} bytes but dst is {} bytes",
309 src.len(),
310 dst.len()
311 );
312 }
313 if srcs.len() < 64 {
314 let excess = selector >> srcs.len();
315 let count = srcs.len();
316 assert!(
317 excess == 0,
318 "bits::xor_gather: selector names source {} but only {count} sources exist",
319 count + excess.trailing_zeros() as usize
320 );
321 }
322 kernel::gf2::xor_gather(dst, srcs, selector);
323}