@@ -235,6 +235,113 @@ pub(super) fn bitwise_binary_op<F: FnMut(u64, u64) -> u64>(
235235 BitBuffer :: new ( bytes. freeze ( ) , len)
236236}
237237
238+ /// Combine `left` and `right` word-by-word with `op`, calling `comb` on each result word so a
239+ /// caller can fold a reduction (e.g. `count_ones`) into the same pass. The word given to `comb`
240+ /// has bits past `len` cleared; the buffer keeps `op`'s raw output in those positions.
241+ pub ( super ) fn bitwise_binary_op_with < F , C > (
242+ left : & BitBuffer ,
243+ right : & BitBuffer ,
244+ op : F ,
245+ comb : C ,
246+ ) -> BitBuffer
247+ where
248+ F : FnMut ( u64 , u64 ) -> u64 ,
249+ C : FnMut ( u64 ) ,
250+ {
251+ assert_eq ! ( left. len( ) , right. len( ) ) ;
252+ let len = left. len ( ) ;
253+ if len == 0 {
254+ return BitBuffer :: empty ( ) ;
255+ }
256+
257+ // Byte-aligned operands: logical bits map onto physical `u64` words, so we can read the backing
258+ // bytes straight as words instead of shifting through `iter_padded`.
259+ if left. offset ( ) . is_multiple_of ( 8 ) && right. offset ( ) . is_multiple_of ( 8 ) {
260+ let n_bytes = len. div_ceil ( 8 ) ;
261+ let l_start = left. offset ( ) / 8 ;
262+ let r_start = right. offset ( ) / 8 ;
263+ let lhs = & left. inner ( ) . as_slice ( ) [ l_start..l_start + n_bytes] ;
264+ let rhs = & right. inner ( ) . as_slice ( ) [ r_start..r_start + n_bytes] ;
265+
266+ let ( lhs_words, lhs_tail) = lhs. as_chunks :: < 8 > ( ) ;
267+ let ( rhs_words, rhs_tail) = rhs. as_chunks :: < 8 > ( ) ;
268+
269+ let words = lhs_words
270+ . iter ( )
271+ . zip ( rhs_words)
272+ . map ( |( l, r) | ( u64:: from_le_bytes ( * l) , u64:: from_le_bytes ( * r) ) )
273+ . chain ( ( !lhs_tail. is_empty ( ) ) . then ( || ( read_u64_le ( lhs_tail) , read_u64_le ( rhs_tail) ) ) ) ;
274+ return combine_words ( words, len, op, comb) ;
275+ }
276+
277+ // Sub-byte offset: `iter_padded` realigns the bits and zero-pads the final word.
278+ let words = left
279+ . chunks ( )
280+ . iter_padded ( )
281+ . zip ( right. chunks ( ) . iter_padded ( ) ) ;
282+ combine_words ( words, len, op, comb)
283+ }
284+
285+ /// Combine an iterator of `(left, right)` words via `op` into a `len`-bit [`BitBuffer`], folding
286+ /// `comb` over each in-range result word. Consumes exactly `ceil(len / 64)` words (any extra the
287+ /// producer yields, e.g. `iter_padded`'s trailing pad word, is dropped). Only the final word can
288+ /// hold bits past `len`, so it alone is masked for `comb`; the buffer keeps `op`'s raw output.
289+ #[ inline]
290+ fn combine_words < I , F , C > ( words : I , len : usize , mut op : F , mut comb : C ) -> BitBuffer
291+ where
292+ I : Iterator < Item = ( u64 , u64 ) > ,
293+ F : FnMut ( u64 , u64 ) -> u64 ,
294+ C : FnMut ( u64 ) ,
295+ {
296+ let n_words = len. div_ceil ( 64 ) ;
297+ let mut out: BufferMut < u64 > = BufferMut :: with_capacity ( n_words) ;
298+ let mut words = words. take ( n_words) ;
299+
300+ // Words fully within `len` need no masking; handling the final word separately keeps this loop
301+ // uniform, with no per-word branch.
302+ for ( l, r) in words. by_ref ( ) . take ( n_words - 1 ) {
303+ let word = op ( l, r) ;
304+ comb ( word) ;
305+ out. push ( word) ;
306+ }
307+ // Final word: clear bits past `len` for `comb` only.
308+ if let Some ( ( l, r) ) = words. next ( ) {
309+ let word = op ( l, r) ;
310+ comb ( word & last_word_mask ( len) ) ;
311+ out. push ( word) ;
312+ }
313+
314+ let mut bytes = out. into_byte_buffer ( ) ;
315+ bytes. truncate ( len. div_ceil ( 8 ) ) ;
316+ BitBuffer :: new ( bytes. freeze ( ) , len)
317+ }
318+
319+ /// Mask of the in-range bits in the final word: the low `len % 64` bits, or all 64 when `len` is
320+ /// a multiple of 64.
321+ #[ inline]
322+ fn last_word_mask ( len : usize ) -> u64 {
323+ let rem = len % 64 ;
324+ if rem == 0 {
325+ u64:: MAX
326+ } else {
327+ ( 1u64 << rem) - 1
328+ }
329+ }
330+
331+ /// Like [`bitwise_binary_op`], but also returns the result's set-bit count, folded into the same
332+ /// pass instead of re-scanning the buffer.
333+ pub ( super ) fn bitwise_binary_op_counted < F : FnMut ( u64 , u64 ) -> u64 > (
334+ left : & BitBuffer ,
335+ right : & BitBuffer ,
336+ op : F ,
337+ ) -> ( BitBuffer , usize ) {
338+ let mut count = 0usize ;
339+ let buffer = bitwise_binary_op_with ( left, right, op, |word| {
340+ count += word. count_ones ( ) as usize ;
341+ } ) ;
342+ ( buffer, count)
343+ }
344+
238345#[ cfg( test) ]
239346mod tests {
240347 use std:: ops:: Not ;
@@ -367,6 +474,44 @@ mod tests {
367474 }
368475 }
369476
477+ /// The fused path must yield the same buffer and true count as the reference
478+ /// (`bitwise_binary_op` + a separate `true_count`), across all ops, offsets, and lengths,
479+ /// including lengths like 127 whose final word holds out-of-range bits.
480+ #[ rstest]
481+ #[ case:: aligned( 0 , 0 ) ]
482+ #[ case:: equal_nonzero( 3 , 3 ) ]
483+ #[ case:: byte_aligned( 8 , 16 ) ]
484+ #[ case:: mismatch( 0 , 5 ) ]
485+ fn counted_matches_reference ( #[ case] left_offset : usize , #[ case] right_offset : usize ) {
486+ #[ allow( clippy:: cast_possible_truncation) ]
487+ let make = |offset : usize , len : usize , salt : u8 | -> BitBuffer {
488+ let bytes: ByteBufferMut = ( 0 ..( offset + len) . div_ceil ( 8 ) . max ( 1 ) )
489+ . map ( |i| ( i as u8 ) . wrapping_mul ( 31 ) . wrapping_add ( salt) )
490+ . collect ( ) ;
491+ BitBufferMut :: from_buffer ( bytes, offset, len) . freeze ( )
492+ } ;
493+ let ops: [ fn ( u64 , u64 ) -> u64 ; 4 ] =
494+ [ |a, b| a & b, |a, b| a | b, |a, b| a ^ b, |a, b| a & !b] ;
495+
496+ for len in [ 1usize , 5 , 8 , 63 , 64 , 65 , 127 , 128 , 129 , 200 , 256 ] {
497+ let left = make ( left_offset, len, 0xC3 ) ;
498+ let right = make ( right_offset, len, 0x5A ) ;
499+ for op in ops {
500+ let ( got_buf, got_count) = bitwise_binary_op_counted ( & left, & right, op) ;
501+ let expected_buf = bitwise_binary_op ( & left, & right, op) ;
502+ assert_eq ! (
503+ got_buf, expected_buf,
504+ "buffer mismatch loff={left_offset} roff={right_offset} len={len}"
505+ ) ;
506+ assert_eq ! (
507+ got_count,
508+ expected_buf. true_count( ) ,
509+ "count mismatch loff={left_offset} roff={right_offset} len={len}"
510+ ) ;
511+ }
512+ }
513+ }
514+
370515 /// Regression test for a bug where [`bitwise_unary_op`] produced corrupt results when
371516 /// the [`BitBuffer`]'s underlying byte pointer was not u64-aligned. Slicing a buffer by
372517 /// a non-multiple-of-8 number of bytes can cause this misalignment. The bug only
0 commit comments