Skip to main content

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 {}