Skip to main content

provenant/license_detection/
token_set.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use smallvec::SmallVec;
5use std::cmp::Ordering;
6use std::ops::Deref;
7
8use rkyv::Archive;
9
10use crate::license_detection::index::dictionary::{TokenDictionary, TokenId, TokenKind};
11
12/// A set of token IDs stored as a sorted SmallVec.
13///
14/// Invariant: elements are always sorted and deduplicated.
15/// Construct via `TokenSet::from_token_ids()`, `TokenSet::from_u16_iter()`,
16/// or `.collect()` from an iterator of u16.
17#[derive(Clone, Debug, PartialEq, Eq, Archive, rkyv::Serialize, rkyv::Deserialize)]
18pub struct TokenSet(SmallVec<[u16; 64]>);
19
20impl TokenSet {
21    /// Create a TokenSet from an iterator of u16 token IDs.
22    /// Sorts and deduplicates the input.
23    pub fn from_u16_iter<I: IntoIterator<Item = u16>>(iter: I) -> Self {
24        let mut inner: SmallVec<[u16; 64]> = iter.into_iter().collect();
25        inner.sort_unstable();
26        inner.dedup();
27        Self(inner)
28    }
29
30    /// Create a TokenSet from an iterator of TokenId values.
31    pub fn from_token_ids<I: IntoIterator<Item = TokenId>>(iter: I) -> Self {
32        Self::from_u16_iter(iter.into_iter().map(|tid| tid.raw()))
33    }
34
35    /// Create an empty TokenSet.
36    pub fn new() -> Self {
37        Self(SmallVec::new())
38    }
39
40    /// Number of tokens in the set.
41    pub fn len(&self) -> usize {
42        self.0.len()
43    }
44
45    /// Is the set empty?
46    pub fn is_empty(&self) -> bool {
47        self.0.is_empty()
48    }
49
50    /// Return true if the set contains the given token ID.
51    pub fn contains_token_id(&self, token_id: TokenId) -> bool {
52        self.0.contains(&token_id.raw())
53    }
54
55    /// Get the subset containing only high-value (legalese) tokens.
56    pub fn high_subset(&self, dictionary: &TokenDictionary) -> Self {
57        Self::from_u16_iter(
58            self.iter()
59                .filter(|&tid| dictionary.token_kind(TokenId::new(tid)) == TokenKind::Legalese),
60        )
61    }
62
63    /// Count intersection with another TokenSet (no allocation).
64    pub fn intersection_count(&self, other: &TokenSet) -> usize {
65        let (mut i, mut j, mut count) = (0, 0, 0);
66        while i < self.0.len() && j < other.0.len() {
67            match self.0[i].cmp(&other.0[j]) {
68                Ordering::Less => i += 1,
69                Ordering::Greater => j += 1,
70                Ordering::Equal => {
71                    count += 1;
72                    i += 1;
73                    j += 1;
74                }
75            }
76        }
77        count
78    }
79
80    /// Materialize intersection with another TokenSet.
81    pub fn intersection(&self, other: &TokenSet) -> TokenSet {
82        let mut result = SmallVec::new();
83        let (mut i, mut j) = (0, 0);
84        while i < self.0.len() && j < other.0.len() {
85            match self.0[i].cmp(&other.0[j]) {
86                Ordering::Less => i += 1,
87                Ordering::Greater => j += 1,
88                Ordering::Equal => {
89                    result.push(self.0[i]);
90                    i += 1;
91                    j += 1;
92                }
93            }
94        }
95        Self(result)
96    }
97
98    /// Iterate over the sorted token IDs.
99    pub fn iter(&self) -> impl Iterator<Item = u16> + '_ {
100        self.0.iter().copied()
101    }
102}
103
104impl Deref for TokenSet {
105    type Target = [u16];
106    fn deref(&self) -> &Self::Target {
107        &self.0
108    }
109}
110
111/// Fixed-width bitset over high-value (legalese) token IDs.
112///
113/// Legalese tokens occupy the reserved low ID range `0..len_legalese`, so a
114/// rule's (or query's) high-token set maps onto one bit per ID. Intersection
115/// count is then a word-wise `AND` + `popcount`, which replaces the sorted
116/// two-pointer merge walk in the candidate-selection high-token gate — a
117/// branch-free, allocation-free, constant-per-rule cost regardless of set size.
118///
119/// All bitsets compared together MUST share the same width (`len_legalese`).
120#[derive(Clone, Debug, PartialEq, Eq, Archive, rkyv::Serialize, rkyv::Deserialize)]
121pub struct HighBitset(Box<[u64]>);
122
123impl HighBitset {
124    /// Build a bitset of `len_legalese` bits with the high tokens of `set` set.
125    ///
126    /// `set` is expected to contain only high token IDs (`< len_legalese`); any
127    /// out-of-range ID would be a builder invariant violation, so it is ignored
128    /// rather than panicking in the hot path.
129    pub fn from_token_set(set: &TokenSet, len_legalese: usize) -> Self {
130        let mut words = vec![0u64; len_legalese.div_ceil(64)].into_boxed_slice();
131        for tid in set.iter() {
132            let bit = tid as usize;
133            if let Some(word) = words.get_mut(bit / 64) {
134                *word |= 1u64 << (bit % 64);
135            }
136        }
137        Self(words)
138    }
139
140    /// Count of shared bits with `other`. Both bitsets must share a width
141    /// (built from the same `len_legalese`); `zip` would otherwise silently
142    /// truncate to the shorter operand and undercount.
143    #[inline]
144    pub fn intersection_count(&self, other: &HighBitset) -> usize {
145        debug_assert_eq!(
146            self.0.len(),
147            other.0.len(),
148            "HighBitset width mismatch: {} vs {} words",
149            self.0.len(),
150            other.0.len()
151        );
152        self.0
153            .iter()
154            .zip(other.0.iter())
155            .map(|(a, b)| (a & b).count_ones() as usize)
156            .sum()
157    }
158}
159
160impl Default for TokenSet {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166impl std::iter::FromIterator<u16> for TokenSet {
167    fn from_iter<T: IntoIterator<Item = u16>>(iter: T) -> Self {
168        Self::from_u16_iter(iter)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::license_detection::index::dictionary::tid;
176
177    #[test]
178    fn test_from_token_ids() {
179        let set = TokenSet::from_token_ids([tid(4), tid(2), tid(4), tid(1)]);
180
181        assert_eq!(set.iter().collect::<Vec<_>>(), vec![1, 2, 4]);
182        assert!(set.contains_token_id(tid(1)));
183        assert!(set.contains_token_id(tid(2)));
184        assert!(set.contains_token_id(tid(4)));
185    }
186
187    #[test]
188    fn test_high_subset() {
189        let set = TokenSet::from_u16_iter([1, 2, 5, 10]);
190        let dict = TokenDictionary::new_with_legalese_pairs(&[("one", 1), ("two", 2)]);
191
192        let high_set = set.high_subset(&dict);
193
194        assert_eq!(high_set.iter().collect::<Vec<_>>(), vec![1, 2]);
195    }
196
197    #[test]
198    fn high_bitset_intersection_count_overlap_cases() {
199        let a = TokenSet::from_u16_iter([1, 5, 9, 63, 64, 200]);
200        let b = TokenSet::from_u16_iter([5, 9, 64, 201]);
201        // width 256 is not a multiple of 64's underlying word count boundary
202        // checks; use a width that leaves a partial trailing word (200 -> 4 words).
203        let width = 256;
204        let ba = HighBitset::from_token_set(&a, width);
205        let bb = HighBitset::from_token_set(&b, width);
206        // partial overlap: {5, 9, 64}
207        assert_eq!(ba.intersection_count(&bb), 3);
208        // full self-overlap
209        assert_eq!(ba.intersection_count(&ba), a.len());
210        // zero overlap
211        let c = HighBitset::from_token_set(&TokenSet::from_u16_iter([2, 3, 4]), width);
212        let d = HighBitset::from_token_set(&TokenSet::from_u16_iter([10, 11]), width);
213        assert_eq!(c.intersection_count(&d), 0);
214        // matches the TokenSet reference on the shared subset
215        assert_eq!(ba.intersection_count(&bb), a.intersection_count(&b));
216    }
217
218    #[test]
219    fn high_bitset_handles_non_multiple_of_64_width_and_boundary_bits() {
220        // width 65 -> 2 words; exercises a bit in the trailing partial word (64).
221        let width = 65;
222        let a = HighBitset::from_token_set(&TokenSet::from_u16_iter([0, 63, 64]), width);
223        let b = HighBitset::from_token_set(&TokenSet::from_u16_iter([63, 64]), width);
224        assert_eq!(a.intersection_count(&b), 2);
225    }
226
227    #[test]
228    fn high_bitset_skips_ids_beyond_allocated_words() {
229        // Width rounds up to whole u64 words, so `from_token_set` silently skips
230        // only IDs at/beyond words*64. In practice high token IDs are always
231        // < len_legalese, so this guard is defensive; here width 8 -> 1 word
232        // (64 bits), and id 100 (>= 64) is dropped from both sets.
233        let width = 8;
234        let a = HighBitset::from_token_set(&TokenSet::from_u16_iter([1, 7, 100]), width);
235        let b = HighBitset::from_token_set(&TokenSet::from_u16_iter([7, 100]), width);
236        // id 100 is skipped in both; only id 7 is shared and in range.
237        assert_eq!(a.intersection_count(&b), 1);
238    }
239}