Skip to main content

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}