Skip to main content

binius_circuits/
popcount.rs

1// Copyright 2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3//! Popcount circuit implementation for counting 1-bits in a 64-bit word.
4//!
5//! This module implements the population count (popcount) operation using
6//! the SWAR (SIMD Within A Register) algorithm, optimized for Binius64's
7//! constraint system.
8//!
9//! # Example
10//!
11//! ```rust,ignore
12//! use binius_core::word::Word;
13//! use binius_frontend::crate::popcount::popcount;
14//! use binius_frontend::compiler::CircuitBuilder;
15//!
16//! // Build circuit
17//! let mut builder = CircuitBuilder::new();
18//! let input = builder.add_witness();
19//! let output = builder.add_witness();
20//! let computed = popcount(&mut builder, input);
21//! builder.assert_eq("popcount_result", computed, output);
22//! let circuit = builder.build();
23//!
24//! // Fill witness
25//! let mut w = circuit.new_witness_filler();
26//! w[input] = Word(0xFF);  // 8 bits set
27//! w[output] = Word(8);
28//!
29//! // Verify
30//! circuit.populate_wire_witness(&mut w).unwrap();
31//! ```
32
33use binius_frontend::{CircuitBuilder, Wire};
34
35use crate::util::clear_high_bits;
36
37/// Computes the population count (number of 1-bits) of a 64-bit word.
38///
39/// This function implements the SWAR algorithm to efficiently count bits
40/// using parallel operations within a single 64-bit register.
41///
42/// # Arguments
43/// * `builder` - The circuit builder to add constraints to
44/// * `input` - Wire containing the 64-bit value to count bits in
45///
46/// # Returns
47/// * Wire containing the popcount result (value between 0 and 64)
48pub fn popcount(builder: &mut CircuitBuilder, input: Wire) -> Wire {
49	// SWAR Algorithm Implementation
50	// Reference: https://nimrod.blog/posts/algorithms-behind-popcount/#swar-algorithm
51
52	// Create constant masks used in SWAR
53	let mask_5555 = builder.add_constant_64(0x5555555555555555); // 0101...
54	let mask_3333 = builder.add_constant_64(0x3333333333333333); // 0011...
55	let mask_0f0f = builder.add_constant_64(0x0F0F0F0F0F0F0F0F); // 00001111...
56	let mask_00ff = builder.add_constant_64(0x00FF00FF00FF00FF); // 8 ones, 8 zeros...
57	let mask_0000ffff = builder.add_constant_64(0x0000FFFF0000FFFF); // 16 ones, 16 zeros...
58
59	// Step 1: Count bits in 2-bit groups using subtraction trick
60	// n = n - ((n >> 1) & 0x5555555555555555)
61	let n_shr_1 = builder.shr(input, 1);
62	let masked_shr_1 = builder.band(n_shr_1, mask_5555);
63	let zero = builder.add_constant_64(0);
64	let (n_step1, _borrow) = builder.isub_bin_bout(input, masked_shr_1, zero);
65
66	// Step 2: Sum adjacent 2-bit groups into 4-bit groups
67	// n = (n & 0x3333333333333333) + ((n >> 2) & 0x3333333333333333)
68	// IMPORTANT: We MUST mask before adding here!
69	// After Step 1, 4-bit groups can contain values up to 1010 (binary) = 10 (decimal)
70	// Without masking first, adding 1010 + 1010 = 10100 would overflow 4 bits
71	// and create carries that corrupt adjacent groups.
72	// Masking first ensures we only add the 2-bit counts (max value 2+2=4).
73	let n_masked_3333 = builder.band(n_step1, mask_3333);
74	let n_shr_2 = builder.shr(n_step1, 2);
75	let n_shr_2_masked = builder.band(n_shr_2, mask_3333);
76	let (n_step2, _carry) = builder.iadd(n_masked_3333, n_shr_2_masked);
77
78	// Step 3: Sum adjacent 4-bit groups into 8-bit groups
79	// n = (n + (n >> 4)) & 0x0F0F0F0F0F0F0F0F
80	// NOTE: Here we can safely add THEN mask (unlike Step 2)
81	// After Step 2, max value per 4-bit group is 4 (0100 binary)
82	// Adding 0100 + 0100 = 1000 (8) still fits in 4 bits, no overflow!
83	let n_shr_4 = builder.shr(n_step2, 4);
84	let (n_sum3, _carry) = builder.iadd(n_step2, n_shr_4);
85	let n_step3 = builder.band(n_sum3, mask_0f0f);
86
87	// Step 4: Sum adjacent 8-bit groups into 16-bit groups
88	// n = (n + (n >> 8)) & 0x00FF00FF00FF00FF
89	let n_shr_8 = builder.shr(n_step3, 8);
90	let (n_sum4, _carry) = builder.iadd(n_step3, n_shr_8);
91	let n_step4 = builder.band(n_sum4, mask_00ff);
92
93	// Step 5: Sum adjacent 16-bit groups into 32-bit groups
94	// n = (n + (n >> 16)) & 0x0000FFFF0000FFFF
95	let n_shr_16 = builder.shr(n_step4, 16);
96	let (n_sum5, _carry) = builder.iadd(n_step4, n_shr_16);
97	let n_step5 = builder.band(n_sum5, mask_0000ffff);
98
99	// Step 6: Sum adjacent 32-bit groups to get final 64-bit result
100	// n = (n + (n >> 32)) & 0x00000000FFFFFFFF
101	let n_shr_32 = builder.shr(n_step5, 32);
102	let (n_sum6, _carry) = builder.iadd(n_step5, n_shr_32);
103
104	// The final result is in the lower bits and represents the popcount (0-64)
105	clear_high_bits(builder, n_sum6, 32)
106}
107
108#[cfg(test)]
109mod tests {
110	// Note: Proptest uses deterministic seeding by default for reproducible tests.
111	// The default seed is always 0 unless explicitly configured otherwise.
112	// See: https://docs.rs/proptest/latest/proptest/test_runner/struct.Config.html#structfield.rng_algorithm
113	use binius_core::word::Word;
114	use proptest::prelude::*;
115
116	use super::*;
117
118	/// Helper function to build a test circuit with popcount
119	fn build_popcount_circuit() -> (binius_frontend::Circuit, Wire, Wire) {
120		let mut builder = CircuitBuilder::new();
121		let input = builder.add_witness();
122		let output = builder.add_witness();
123		let computed = popcount(&mut builder, input);
124		builder.assert_eq("popcount_result", computed, output);
125		let circuit = builder.build();
126		(circuit, input, output)
127	}
128
129	/// Helper to test a specific popcount value
130	fn test_popcount_value(value: u64) {
131		let (circuit, input, output) = build_popcount_circuit();
132		let mut w = circuit.new_witness_filler();
133
134		let expected = value.count_ones() as u64;
135		w[input] = Word(value);
136		w[output] = Word(expected);
137
138		circuit
139			.populate_wire_witness(&mut w)
140			.unwrap_or_else(|_| panic!("Popcount of 0x{:016x} should be {}", value, expected));
141	}
142
143	#[test]
144	fn test_popcount_basic() {
145		// Build the circuit
146		let (circuit, input, output) = build_popcount_circuit();
147
148		// Create witness filler
149		let mut w = circuit.new_witness_filler();
150
151		// Test with a simple value
152		let test_value = 0b10110101u64; // Binary: has 5 bits set
153		let expected_count = test_value.count_ones() as u64;
154		assert_eq!(expected_count, 5, "Expected count should be 5");
155
156		w[input] = Word(test_value);
157		w[output] = Word(expected_count);
158
159		// Verify the circuit constraints are satisfied
160		circuit
161			.populate_wire_witness(&mut w)
162			.expect("Circuit should be satisfied with correct popcount");
163	}
164
165	proptest! {
166		#[test]
167		fn test_popcount_edge_cases(
168			value in prop::sample::select(vec![
169				0x0000000000000000u64, // all zeros
170				0xFFFFFFFFFFFFFFFFu64, // all ones
171				0x5555555555555555u64, // alternating 01
172				0xAAAAAAAAAAAAAAAAu64, // alternating 10
173				0x00000000000000FFu64, // one byte set
174				0x000000000000FFFFu64, // two bytes set
175				0x00000000FFFFFFFFu64, // four bytes set
176				0x0F0F0F0F0F0F0F0Fu64, // nibble pattern
177				0xCCCCCCCCCCCCCCCCu64, // 11001100 pattern
178			])
179		) {
180			test_popcount_value(value);
181		}
182
183		#[test]
184		fn test_single_bit_positions(bit_pos in 0u32..64) {
185			// Test single bit set at each position
186			test_popcount_value(1u64 << bit_pos);
187		}
188
189		#[test]
190		fn test_all_bits_except_one(byte_idx in 0usize..8) {
191			// Test all bits set except one (sampling every 8th bit)
192			test_popcount_value(0xFFFFFFFFFFFFFFFF ^ (1u64 << (byte_idx * 8)));
193		}
194	}
195
196	proptest! {
197		#[test]
198		fn test_popcount_known_bit_counts(
199			value in prop::sample::select(vec![
200				0x0000000000000001u64, // 1 bit set
201				0x0000000000000003u64, // 2 bits set
202				0x000000000000000Fu64, // 4 bits set
203				0x00000000000000FFu64, // 8 bits set
204				0x000000000000FFFFu64, // 16 bits set
205				0x00000000FFFFFFFFu64, // 32 bits set
206				0x7FFFFFFFFFFFFFFFu64, // 63 bits set
207			])
208		) {
209			test_popcount_value(value);
210		}
211	}
212
213	proptest! {
214		#![proptest_config(ProptestConfig::with_cases(100))]
215		#[test]
216		fn test_popcount_random_values(value in any::<u64>()) {
217			// Tests 100 random values
218			test_popcount_value(value);
219		}
220	}
221
222	proptest! {
223		#[test]
224		fn test_popcount_incorrect_output_fails(
225			(input, wrong_offset) in prop::sample::select(vec![
226				(0xFFu64, -1i8),        // 8 bits set, test with count 7
227				(0x0Fu64, 1i8),         // 4 bits set, test with count 5
228				(0xFFFFu64, -3i8),      // 16 bits set, test with count 13
229				(0x7u64, 2i8),          // 3 bits set, test with count 5
230			])
231		) {
232			let (circuit, input_wire, output_wire) = build_popcount_circuit();
233			let mut w = circuit.new_witness_filler();
234
235			let correct_count = input.count_ones() as u64;
236			// Note: Wrapping arithmetic is intentional here. If wrong_offset is negative
237			// and larger than correct_count, we get a very large u64 due to wrapping.
238			// This is fine - we're testing that the circuit rejects ANY incorrect value,
239			// including wrapped values. All our test cases avoid actual underflow.
240			let wrong_count = (correct_count as i64 + wrong_offset as i64) as u64;
241
242			w[input_wire] = Word(input);
243			w[output_wire] = Word(wrong_count);
244
245			let result = circuit.populate_wire_witness(&mut w);
246			assert!(result.is_err(),
247				"Circuit should fail with incorrect count {} instead of {}",
248				wrong_count, correct_count);
249		}
250
251		#[test]
252		fn test_popcount_overflow_fails(
253			overflow_amount in 1u64..100
254		) {
255			// Test that counts > 64 are rejected
256			let (circuit, input_wire, output_wire) = build_popcount_circuit();
257			let mut w = circuit.new_witness_filler();
258
259			w[input_wire] = Word(0xFFFFFFFFFFFFFFFF);
260			w[output_wire] = Word(64 + overflow_amount);
261
262			let result = circuit.populate_wire_witness(&mut w);
263			assert!(result.is_err(),
264				"Circuit should fail with popcount = {}", 64 + overflow_amount);
265		}
266	}
267
268	proptest! {
269		#[test]
270		fn test_step2_masking_edge_cases(
271			value in prop::sample::select(vec![
272				0xFFFFFFFFFFFFFFFFu64,  // All bits set - produces 0xAAAA... after Step 1
273				0xCCCCCCCCCCCCCCCCu64,  // Pattern 11001100 - interesting Step 1 result
274				0xF0F0F0F0F0F0F0F0u64,  // Pattern 11110000 - another edge case
275				0xE0E0E0E0E0E0E0E0u64,  // Pattern 11100000 - tests masking
276			])
277		) {
278			// These patterns specifically stress Step 2's mask-before-add requirement
279			// After Step 1, they produce values that would overflow 4-bit groups
280			// if we incorrectly added before masking
281			test_popcount_value(value);
282		}
283	}
284
285	proptest! {
286		#[test]
287		fn test_step3_onwards_no_overflow(
288			pattern in prop::sample::select(vec![
289				0x0F0F0F0F0F0F0F0Fu64, // nibble boundaries (tests Step 3)
290				0x00FF00FF00FF00FFu64, // byte boundaries (tests Step 4)
291				0x0000FFFF0000FFFFu64, // 16-bit boundaries (tests Step 5)
292				0x00000000FFFFFFFFu64, // 32-bit boundaries (tests Step 6)
293				0xFFFF00000000FFFFu64, // split pattern (tests accumulation)
294			])
295		) {
296			// This verifies Steps 3-6 handle add-then-mask safely
297			test_popcount_value(pattern);
298		}
299	}
300
301	#[test]
302	fn test_incorrect_step2_would_fail() {
303		// This test documents why Step 2 MUST mask before adding.
304		// We demonstrate this by showing what the incorrect result would be.
305
306		// Simulate what would happen with incorrect Step 2 implementation
307		let input: u64 = 0xFFFFFFFFFFFFFFFF;
308
309		// Step 1: Correct subtraction
310		let step1 = input.wrapping_sub((input >> 1) & 0x5555555555555555);
311		assert_eq!(step1, 0xAAAAAAAAAAAAAAAA, "Step 1 should produce 0xAAAA...");
312
313		// Incorrect Step 2: Add first, then mask (WRONG!)
314		let incorrect_sum = step1.wrapping_add(step1 >> 2);
315		let incorrect_step2 = incorrect_sum & 0x3333333333333333;
316
317		// Correct Step 2: Mask first, then add
318		let correct_step2 =
319			(step1 & 0x3333333333333333).wrapping_add((step1 >> 2) & 0x3333333333333333);
320
321		// Show they produce different results
322		assert_ne!(incorrect_step2, correct_step2, "Incorrect Step 2 produces different result!");
323
324		// The incorrect approach gives wrong final count
325		// Continue with incorrect value through remaining steps
326		let mut incorrect = incorrect_step2;
327		incorrect = (incorrect.wrapping_add(incorrect >> 4)) & 0x0F0F0F0F0F0F0F0F;
328		incorrect = (incorrect.wrapping_add(incorrect >> 8)) & 0x00FF00FF00FF00FF;
329		incorrect = (incorrect.wrapping_add(incorrect >> 16)) & 0x0000FFFF0000FFFF;
330		incorrect = (incorrect.wrapping_add(incorrect >> 32)) & 0x00000000FFFFFFFF;
331
332		// The incorrect approach gives 15 instead of 64!
333		assert_eq!(incorrect, 15, "Incorrect Step 2 leads to wrong count of 15");
334		assert_eq!(input.count_ones() as u64, 64, "Correct count should be 64");
335
336		// This dramatic difference (15 vs 64) shows how badly the algorithm breaks
337		// when Step 2 doesn't mask before adding!
338		println!(
339			"Demonstrated: Incorrect Step 2 gives {} instead of {}",
340			incorrect,
341			input.count_ones()
342		);
343	}
344}