csp_solver/domain/bitset.rs
1//! BitsetDomain — O(1) domain for non-negative integers, isomorphic to Python's BitsetDomain.
2
3use super::traits::Domain;
4use crate::bitscan::pop_lowest_bit;
5
6/// A domain backed by a 128-bit bitmask.
7///
8/// Values must be non-negative integers in the range `0..128`.
9/// All operations are O(1) via bitwise arithmetic and popcount.
10/// Isomorphic to the Python `BitsetDomain`.
11#[derive(Clone, PartialEq)]
12pub struct BitsetDomain {
13 bits: u128,
14}
15
16impl std::fmt::Debug for BitsetDomain {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 let vals: Vec<u32> = self.values();
19 write!(f, "BitsetDomain({vals:?})")
20 }
21}
22
23impl BitsetDomain {
24 /// Create a new bitset domain from an iterator of values.
25 ///
26 /// # Panics
27 /// Panics if any value is `>= 128`. This is a **release-mode** `assert!`,
28 /// not a `debug_assert!` (R7): a bare `1u128 << v` with `v >= 128` does not
29 /// fault in release — Rust masks the shift to `v % 128`, silently aliasing
30 /// out-of-range values onto valid bits. Since `new`/`range` are reachable
31 /// from the published py/wasm bindings with caller-supplied values, the
32 /// `0..128` invariant must fail loudly in every build. Both are
33 /// construction-time (not hot), so the branch is free; see `add` for the
34 /// hot-path rationale.
35 pub fn new(values: impl IntoIterator<Item = u32>) -> Self {
36 let mut bits: u128 = 0;
37 for v in values {
38 assert!(v < 128, "BitsetDomain supports values 0..128, got {v}");
39 bits |= 1u128 << v;
40 }
41 Self { bits }
42 }
43
44 /// Create an empty bitset domain.
45 pub fn empty() -> Self {
46 Self { bits: 0 }
47 }
48
49 /// Create a domain containing `0..n`.
50 ///
51 /// # Panics
52 /// Panics if `n > 128` (release-mode; see [`BitsetDomain::new`] for the R7
53 /// rationale — `(1u128 << n) - 1` masks `n` to `n % 128` in release,
54 /// silently truncating the range).
55 pub fn range(n: u32) -> Self {
56 assert!(n <= 128, "BitsetDomain::range supports 0..=128, got {n}");
57 let bits = if n == 128 {
58 u128::MAX
59 } else {
60 (1u128 << n) - 1
61 };
62 Self { bits }
63 }
64
65 /// Raw bits access.
66 pub fn bits(&self) -> u128 {
67 self.bits
68 }
69
70 /// Union with another bitset domain.
71 pub fn union_with(&mut self, other: &Self) {
72 self.bits |= other.bits;
73 }
74
75 /// Intersection with another bitset domain.
76 pub fn intersect_with(&mut self, other: &Self) {
77 self.bits &= other.bits;
78 }
79
80 /// Difference: remove all values present in `other`.
81 pub fn difference_with(&mut self, other: &Self) {
82 self.bits &= !other.bits;
83 }
84
85 /// Iterate over set bits, yielding each value.
86 pub fn iter(&self) -> BitsetIter {
87 BitsetIter { bits: self.bits }
88 }
89}
90
91impl Domain for BitsetDomain {
92 type Value = u32;
93
94 fn size(&self) -> usize {
95 self.bits.count_ones() as usize
96 }
97
98 fn contains(&self, val: &u32) -> bool {
99 if *val >= 128 {
100 return false;
101 }
102 (self.bits & (1u128 << val)) != 0
103 }
104
105 fn remove(&mut self, val: &u32) -> bool {
106 if *val >= 128 {
107 return false;
108 }
109 let mask = 1u128 << val;
110 let was_present = (self.bits & mask) != 0;
111 self.bits &= !mask;
112 was_present
113 }
114
115 fn add(&mut self, val: &u32) {
116 // Release-mode check (R7): even on the hot restore path, `add` must not
117 // silently alias `val >= 128` onto `val % 128`. The branch is perfectly
118 // predicted — every internal caller restores a value that was itself in
119 // `0..128` — and is dwarfed by the bit write, so no unchecked fast path
120 // is warranted; profiling does not justify splitting the API.
121 assert!(*val < 128, "BitsetDomain supports values 0..128, got {val}");
122 self.bits |= 1u128 << val;
123 }
124
125 fn values(&self) -> Vec<u32> {
126 self.iter().collect()
127 }
128
129 fn iter(&self) -> impl Iterator<Item = u32> + use<> {
130 BitsetIter { bits: self.bits }
131 }
132
133 fn singleton_value(&self) -> Option<u32> {
134 if self.bits.count_ones() == 1 {
135 Some(self.bits.trailing_zeros())
136 } else {
137 None
138 }
139 }
140
141 /// O(1) restrict: clear every bit but `val`'s in one bitwise AND,
142 /// instead of the trait default's O(domain size) iterate-and-clear
143 /// loop. Returns a `BitsetIter` (zero-alloc) over the cleared bits.
144 fn restrict_to(&mut self, val: &u32) -> impl Iterator<Item = u32> + use<> {
145 // Release-mode check (R7): same shift-aliasing hazard as `add` — a
146 // masked `1u128 << (*val % 128)` would keep the wrong bit. Well-
147 // predicted hot-path branch (callers restrict to an in-domain value).
148 assert!(*val < 128, "BitsetDomain supports values 0..128, got {val}");
149 let keep_mask = 1u128 << *val;
150 let pruned = self.bits & !keep_mask;
151 self.bits &= keep_mask;
152 BitsetIter { bits: pruned }
153 }
154}
155
156/// Iterator over set bits in a `BitsetDomain`.
157pub struct BitsetIter {
158 bits: u128,
159}
160
161impl Iterator for BitsetIter {
162 type Item = u32;
163
164 fn next(&mut self) -> Option<u32> {
165 pop_lowest_bit(&mut self.bits).map(|idx| idx as u32)
166 }
167
168 fn size_hint(&self) -> (usize, Option<usize>) {
169 let count = self.bits.count_ones() as usize;
170 (count, Some(count))
171 }
172}
173
174impl ExactSizeIterator for BitsetIter {}