praxis_runtime/bitset.rs
1//! `BitSet` (§6.1).
2//!
3//! A compact set of non-negative integers, backed by a [`ReprCVec<u64>`] of
4//! words. Occupancy is bit `i` of word `i / 64`. Nullary in user syntax
5//! (`BitSet`, no type arg); elements are always `Int`. Iterable (yields `Int`).
6//!
7//! `BitSet` is its own GC payload: the words are a `Drop` vector, so the
8//! descriptor's `drop_value` releases them on sweep. Equality/hash are
9//! structural (two bitsets are equal iff they hold the same bits).
10//!
11//! The container is a [`ReprCVec`] and not a `std::Vec` because generated code
12//! reads its two leading words inline for `bs.contains(x)` (ADR-118 part 2);
13//! [`INLINE_BITSET_SITE`] is the one value that says where they are.
14
15use std::fmt::Write as _;
16
17use crate::descriptor::{BuiltinTypeId, DynamicHasher, FormatSink, Tracer, TypeDescriptor};
18use crate::repr_c_vec::ReprCVec;
19
20/// A value a `BitSet` can actually hold: non-negative, and small enough that
21/// the word vector backing it stays a real allocation.
22///
23/// This is the only route from a user-supplied `Int` to a bit position (RT-07),
24/// so the range check happens where the value enters, once. A negative member
25/// and one whose word vector the host could not serve are both unrepresentable
26/// rather than merely unreached.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
28pub struct BitIndex(usize);
29
30impl BitIndex {
31 /// The largest member a `BitSet` accepts: 2^32 - 1, whose word vector is
32 /// 512 MiB. A cap rather than "anything a `usize` holds", for the same
33 /// reason [`GridExtent::MAX_CELLS`](crate::collections::GridExtent::MAX_CELLS)
34 /// is one — a `Vec` request the host cannot serve is not an error the
35 /// process survives.
36 pub const MAX: i64 = u32::MAX as i64;
37
38 /// The most words a `BitSet` can ever hold, which is
39 /// [`MAX`](Self::MAX)`/64 + 1`.
40 ///
41 /// **This bound is what lets generated code omit the range test**
42 /// (ADR-118 part 2). `bs.contains(x)` inline is `word = (x as u64) >> 6;
43 /// if word >= words.len() { false } else { … }`, with no separate check
44 /// that `x` is a member `BitIndex::new` would accept — because for every
45 /// `i64` outside `0..=MAX` the *unsigned* shift already lands at or above
46 /// this number, and the word count never reaches it:
47 ///
48 /// * `x < 0` → `x as u64 >= 2^63` → `word >= 2^57`;
49 /// * `x > MAX` → `x as u64 >= 2^32` → `word >= 2^26 == MAX_WORDS`;
50 /// * and [`BitSetPayload::insert`] resizes to `word + 1` for a `BitIndex`,
51 /// so `words.len() <= MAX_WORDS` always.
52 ///
53 /// So `word >= words.len()` subsumes the range test, exactly, for every
54 /// value of the type. `the_word_probe_generated_code_emits_answers_contains`
55 /// is that claim checked against `contains` rather than argued, in the
56 /// module that owns the range — `small_int`'s
57 /// `the_unsigned_range_test_generated_code_emits_answers_index_of` in its
58 /// second place.
59 pub const MAX_WORDS: usize = (Self::MAX as usize) / 64 + 1;
60
61 /// The bit `value` names, or `None` if it is negative or above
62 /// [`MAX`](Self::MAX).
63 #[must_use]
64 pub const fn new(value: i64) -> Option<BitIndex> {
65 if value < 0 || value > Self::MAX {
66 return None;
67 }
68 Some(BitIndex(value as usize))
69 }
70
71 /// The word holding this bit, and its position within that word.
72 #[inline]
73 const fn word_and_bit(self) -> (usize, usize) {
74 (self.0 / 64, self.0 % 64)
75 }
76}
77
78/// The `BitSet` payload: a growable vector of 64-bit words. Bit `i` is in word
79/// `i / 64` at position `i % 64`. The field is `Drop`.
80#[repr(C)]
81pub struct BitSetPayload {
82 /// The words. Trailing zero words may be present; equality/hash trim them.
83 ///
84 /// A [`ReprCVec`](crate::ReprCVec) rather than a `std::Vec` for ADR-118's
85 /// reason: generated code reads the words pointer and the word count inline
86 /// for `bs.contains(x)`, and `std::Vec` is `#[repr(Rust)]` — its three words
87 /// live inside a private `RawVec` in no guaranteed order. The container
88 /// gives those three words a fixed layout; `Vec` still does every
89 /// allocation.
90 pub words: ReprCVec<u64>,
91}
92
93// The offsets generated code bakes for `bs.contains(x)` (ADR-118 part 2), in
94// the tree rather than in a sentence. `words` is the only field, so its
95// element pointer is at payload+0 and its length at payload+8.
96const _: () = assert!(std::mem::offset_of!(BitSetPayload, words) == 0);
97// The payload is one container and nothing else, which is what fixes the
98// block's size class and the pacer's page density.
99const _: () = assert!(std::mem::size_of::<BitSetPayload>() == 24);
100const _: () = assert!(std::mem::align_of::<BitSetPayload>() == 8);
101
102/// The one site generated code may read a `BitSet`'s words through (ADR-118
103/// part 2), minted beside the payload for [`INLINE_VEC_SITE`]'s reason.
104///
105/// [`INLINE_VEC_SITE`]: crate::collections::INLINE_VEC_SITE
106#[cfg(not(feature = "std-vec-payload"))]
107pub const INLINE_BITSET_SITE: crate::repr_c_vec::InlineSliceSite =
108 crate::repr_c_vec::InlineSliceSite::new(
109 BuiltinTypeId::BitSet,
110 std::mem::align_of::<BitSetPayload>(),
111 std::mem::offset_of!(BitSetPayload, words),
112 std::mem::size_of::<u64>(),
113 );
114
115impl BitSetPayload {
116 /// True iff bit `i` is set.
117 pub(crate) fn contains(&self, i: BitIndex) -> bool {
118 let (word, bit) = i.word_and_bit();
119 match self.words.get(word) {
120 Some(w) => (w >> bit) & 1 == 1,
121 None => false,
122 }
123 }
124
125 /// Set bit `i`, growing the words vector as needed. The growth is bounded
126 /// by [`BitIndex::MAX`], which is what makes this a real allocation.
127 ///
128 /// **The only thing in the tree that grows `words`**, which is what makes
129 /// [`BitIndex::MAX_WORDS`] a bound rather than a hope — and generated code
130 /// leans on that bound to skip a range test. The assertion says so at the
131 /// one site that could falsify it.
132 pub(crate) fn insert(&mut self, i: BitIndex) {
133 let (word, bit) = i.word_and_bit();
134 if self.words.len() <= word {
135 self.words.resize(word + 1, 0);
136 }
137 debug_assert!(
138 self.words.len() <= BitIndex::MAX_WORDS,
139 "a BitSet's word count is bounded by BitIndex::MAX, and generated \
140 code reads that bound as licence to skip the range test"
141 );
142 self.words[word] |= 1u64 << bit;
143 }
144
145 /// Clear bit `i` (no-op if not set or beyond the current words).
146 pub(crate) fn remove(&mut self, i: BitIndex) {
147 let (word, bit) = i.word_and_bit();
148 if let Some(w) = self.words.get_mut(word) {
149 *w &= !(1u64 << bit);
150 }
151 }
152
153 /// The number of set bits (popcount across all words).
154 pub(crate) fn count(&self) -> usize {
155 self.words.iter().map(|w| w.count_ones() as usize).sum()
156 }
157
158 /// Every set bit's value, **ascending**.
159 ///
160 /// A `BitSet` is the one keyed collection whose deterministic order needs no
161 /// decision: the bits *are* the members and their word order is their
162 /// numeric order. `for i in b` iterates a snapshot of this (REP-15,
163 /// ADR-066), and it is the order [`bitset_format`] already prints.
164 pub(crate) fn members(&self) -> impl Iterator<Item = i64> + '_ {
165 self.words.iter().enumerate().flat_map(|(word_idx, &word)| {
166 let mut bits = word;
167 std::iter::from_fn(move || {
168 if bits == 0 {
169 return None;
170 }
171 let bit = bits.trailing_zeros() as usize;
172 bits &= bits - 1; // clear the lowest set bit
173 Some((word_idx * 64 + bit) as i64)
174 })
175 })
176 }
177}
178
179unsafe fn bitset_trace(_payload: *mut u8, _tracer: &mut dyn Tracer) {
180 // BitSet holds no GcRefs (bits are not objects); nothing to trace.
181}
182
183unsafe fn bitset_drop(payload: *mut u8) {
184 // SAFETY: caller guarantees `payload` points at an initialized BitSetPayload.
185 unsafe { std::ptr::drop_in_place(payload as *mut BitSetPayload) };
186}
187
188unsafe fn bitset_format(payload: *const u8, out: &mut FormatSink<'_>) {
189 // SAFETY: caller guarantees `payload` points at an initialized BitSetPayload.
190 let p = unsafe { &*(payload as *const BitSetPayload) };
191 let _ = out.write_str("{");
192 for (i, value) in p.members().enumerate() {
193 if i > 0 {
194 let _ = out.write_str(", ");
195 }
196 let _ = write!(out, "{value}");
197 }
198 let _ = out.write_str("}");
199}
200
201unsafe fn bitset_equals(a: *const u8, b: *const u8) -> bool {
202 // SAFETY: caller guarantees both pointers point at initialized BitSetPayloads.
203 let pa = unsafe { &*(a as *const BitSetPayload) };
204 let pb = unsafe { &*(b as *const BitSetPayload) };
205 // Compare up to the longer vector's length, treating missing words as zero.
206 let len = pa.words.len().max(pb.words.len());
207 for i in 0..len {
208 let wa = pa.words.get(i).copied().unwrap_or(0);
209 let wb = pb.words.get(i).copied().unwrap_or(0);
210 if wa != wb {
211 return false;
212 }
213 }
214 true
215}
216
217unsafe fn bitset_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
218 // SAFETY: caller guarantees `payload` points at an initialized BitSetPayload.
219 let p = unsafe { &*(payload as *const BitSetPayload) };
220 // Order-independent: hash each set bit's value and XOR. This is more
221 // expensive than hashing words but is robust to trailing-zero-word
222 // differences (two equal bitsets with different word-vector lengths).
223 // The enumeration is [`BitSetPayload::members`], the module's one bit walk;
224 // its ascending order is not load-bearing here, only its membership.
225 let mut acc: u64 = 0;
226 for value in p.members() {
227 let mut h = crate::descriptor::StructHasher::new();
228 h.write_bytes(&(value as u64).to_le_bytes());
229 acc ^= h.finish();
230 }
231 hasher.write_bytes(&acc.to_le_bytes());
232}
233
234/// Descriptor for `BitSet` (§6.1, TypeId 15). Equatable and hashable (structural
235/// over the set of bits), so a BitSet can be a value in another collection.
236pub static BITSET: TypeDescriptor = TypeDescriptor::builtin::<BitSetPayload>(
237 BuiltinTypeId::BitSet,
238 "BitSet",
239 bitset_trace,
240 bitset_drop,
241 bitset_format,
242 Some(bitset_equals),
243 Some(bitset_hash),
244 // No container order: a mutable collection can never be a `Map` key or a
245 // `Set` member (ADR-057 D4), so nothing ever has to put one in a
246 // deterministic sequence (ADR-138).
247 None,
248)
249.with_owned_bytes(bitset_owned_bytes);
250
251impl BitSetPayload {
252 /// The word buffer this payload owns beyond its GC block, for GC pacing
253 /// (RT-04) — `capacity`, not `len`.
254 ///
255 /// One statement of the size, with two readers (ADR-121):
256 /// [`VecPayload::owned_bytes`](crate::collections::VecPayload::owned_bytes)
257 /// is that statement.
258 #[must_use]
259 pub(crate) fn owned_bytes(&self) -> usize {
260 self.words.capacity() * std::mem::size_of::<u64>()
261 }
262}
263
264unsafe fn bitset_owned_bytes(payload: *const u8) -> usize {
265 // SAFETY: caller guarantees `payload` points at an initialized BitSetPayload.
266 let p = unsafe { &*(payload as *const BitSetPayload) };
267 p.owned_bytes()
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn bitset_descriptor_reports_capabilities() {
276 assert!(BITSET.is_equatable() && BITSET.is_hashable());
277 assert_eq!(BITSET.name, "BitSet");
278 }
279
280 /// Shorthand: a bit that is in range by construction.
281 fn bit(i: i64) -> BitIndex {
282 BitIndex::new(i).expect("in-range test bit")
283 }
284
285 /// **REP-15.** A `BitSet`'s members come out ascending, across word
286 /// boundaries — the order `for i in b` walks and the order it prints.
287 #[test]
288 fn a_bitsets_members_come_out_ascending() {
289 let mut b = BitSetPayload {
290 words: ReprCVec::new(),
291 };
292 // Deliberately inserted out of order and spanning three words, so a
293 // per-word or per-insertion order is a different sequence.
294 for i in [130, 5, 64, 0, 63] {
295 b.insert(bit(i));
296 }
297 assert_eq!(b.members().collect::<Vec<_>>(), vec![0, 5, 63, 64, 130]);
298 // The same rule the formatter uses, so `out(b)` and a `for` agree.
299 let mut rendered = String::new();
300 // SAFETY: `b` is an initialized BitSetPayload.
301 unsafe {
302 bitset_format(
303 (&b as *const BitSetPayload).cast::<u8>(),
304 &mut crate::FormatSink::display(&mut rendered),
305 )
306 };
307 assert_eq!(rendered, "{0, 5, 63, 64, 130}");
308 // An empty one yields nothing rather than one member or forever.
309 let empty = BitSetPayload {
310 words: ReprCVec::new(),
311 };
312 assert_eq!(empty.members().count(), 0);
313 // …and so does one whose words are present but all zero, which is the
314 // shape `remove` leaves behind.
315 let cleared = BitSetPayload {
316 words: ReprCVec::from_vec(vec![0, 0]),
317 };
318 assert_eq!(cleared.members().count(), 0);
319 }
320
321 #[test]
322 fn bitset_insert_contains_count() {
323 let mut b = BitSetPayload {
324 words: ReprCVec::new(),
325 };
326 b.insert(bit(0));
327 b.insert(bit(63));
328 b.insert(bit(64));
329 b.insert(bit(1000));
330 assert!(b.contains(bit(0)));
331 assert!(b.contains(bit(63)));
332 assert!(b.contains(bit(64)));
333 assert!(b.contains(bit(1000)));
334 assert!(!b.contains(bit(1)));
335 assert!(!b.contains(bit(65)));
336 assert_eq!(b.count(), 4);
337 }
338
339 #[test]
340 fn bitset_remove_clears_bit() {
341 let mut b = BitSetPayload {
342 words: ReprCVec::new(),
343 };
344 b.insert(bit(5));
345 assert!(b.contains(bit(5)));
346 b.remove(bit(5));
347 assert!(!b.contains(bit(5)));
348 // Removing an unset bit is a no-op.
349 b.remove(bit(999));
350 }
351
352 /// **ADR-118 part 2's load-bearing arithmetic**, checked against
353 /// [`BitSetPayload::contains`] rather than argued.
354 ///
355 /// This is the sequence the backend emits, transcribed: an unsigned shift,
356 /// a compare against the word count, a shift and a mask — and **no range
357 /// test**, because [`BitIndex::MAX_WORDS`] makes the compare subsume it.
358 /// The claim is exact for every `i64`, which is precisely why it is the
359 /// kind of thing that gets believed rather than checked: the interesting
360 /// values are the ones no program produces on purpose.
361 ///
362 /// `small_int`'s `the_unsigned_range_test_generated_code_emits_answers_index_of`
363 /// is the same test for ADR-113's range identity, and this is written to
364 /// its shape: both extremes of the type, every boundary, and a dense sweep.
365 #[test]
366 fn the_word_probe_generated_code_emits_answers_contains() {
367 /// Exactly what the emitted fast path computes, in the same order.
368 fn probe(words: &[u64], member: i64) -> bool {
369 let word = (member as u64) >> 6;
370 if word >= words.len() as u64 {
371 return false;
372 }
373 let w = words[word as usize];
374 (w >> ((member as u64) & 63)) & 1 == 1
375 }
376
377 let mut b = BitSetPayload {
378 words: ReprCVec::new(),
379 };
380 for i in [0, 1, 63, 64, 65, 127, 128, 1000, 4095, 4096] {
381 b.insert(bit(i));
382 }
383
384 // The extremes and the boundaries, where the two forms could disagree
385 // and where no program would look.
386 let corners = [
387 i64::MIN,
388 i64::MIN + 1,
389 -4096,
390 -65,
391 -64,
392 -1,
393 0,
394 1,
395 63,
396 64,
397 BitIndex::MAX - 1,
398 BitIndex::MAX,
399 BitIndex::MAX + 1,
400 i64::MAX - 1,
401 i64::MAX,
402 ];
403 for member in corners {
404 let expected = BitIndex::new(member).is_some_and(|i| b.contains(i));
405 assert_eq!(
406 probe(&b.words, member),
407 expected,
408 "the word probe and `contains` disagree at {member}"
409 );
410 }
411 // …and densely across the populated range plus a margin on both sides.
412 for member in -64_i64..4200 {
413 let expected = BitIndex::new(member).is_some_and(|i| b.contains(i));
414 assert_eq!(probe(&b.words, member), expected, "at {member}");
415 }
416
417 // The bound the omitted range test rests on, stated as a number: a
418 // word count can never reach the index a non-member shifts down to.
419 assert_eq!(BitIndex::MAX_WORDS, 1 << 26);
420 assert!((BitIndex::MAX as u64) >> 6 < BitIndex::MAX_WORDS as u64);
421 assert!(((BitIndex::MAX + 1) as u64) >> 6 >= BitIndex::MAX_WORDS as u64);
422 assert!((-1_i64 as u64) >> 6 >= BitIndex::MAX_WORDS as u64);
423 assert!((i64::MIN as u64) >> 6 >= BitIndex::MAX_WORDS as u64);
424 }
425
426 /// RT-07. A member the set cannot hold has no `BitIndex`, so there is no
427 /// value to hand `insert` — the resize toward a huge word count is
428 /// unwritable rather than merely unreached.
429 #[test]
430 fn a_bit_outside_the_representable_range_has_no_index() {
431 assert!(BitIndex::new(-1).is_none(), "negative");
432 assert!(BitIndex::new(i64::MIN).is_none(), "most negative");
433 assert!(
434 BitIndex::new(BitIndex::MAX + 1).is_none(),
435 "one past the cap"
436 );
437 assert!(
438 BitIndex::new(i64::MAX).is_none(),
439 "the value that asked for a 10^16-word Vec"
440 );
441 assert!(BitIndex::new(0).is_some(), "zero is a member");
442 assert!(
443 BitIndex::new(BitIndex::MAX).is_some(),
444 "the cap is a member"
445 );
446 }
447}