metrale_grammar/compiler/
mask.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// AdaptiveTokenMask — port of `struct AdaptiveTokenMask` from
4// `cpp/compiled_grammar_impl.h` + `cpp/compiled_grammar.cc`.
5//
6// Preprocessed per-`ParserState` partition of the token vocabulary into
7// three categories:
8//   * accepted  — tokens the state alone proves acceptable;
9//   * rejected  — tokens the state alone proves unacceptable;
10//   * uncertain — tokens whose acceptance needs the parent states.
11//
12// To save memory the accepted/rejected partition is stored in one of
13// three forms (see [`StoreType`]); uncertain indices are always stored
14// directly. All indices are positions into the tokenizer's
15// `sorted_decoded_vocab`, not raw token ids — this is what makes the
16// matcher's bitmask fill fast.
17
18use bitvec::vec::BitVec;
19
20/// Above this many accepted (and rejected) indices, the partition is
21/// stored as a vocab-sized bitset instead of an index vector. Faithful
22/// to the C++ `AdaptiveTokenMask::USE_BITSET_THRESHOLD`.
23pub const USE_BITSET_THRESHOLD: usize = 1000;
24
25/// How an [`AdaptiveTokenMask`] stores its accepted/rejected partition.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StoreType {
28    /// Only the accepted indices are stored; rejected = all − accepted
29    /// − uncertain. Used when `|accepted| < |rejected|`.
30    Accepted,
31    /// Only the rejected indices are stored; accepted = all − rejected
32    /// − uncertain. Used when `|accepted| > |rejected|`.
33    Rejected,
34    /// Accepted token *ids* are stored in a vocab-sized bitset. Used
35    /// when both partitions are large.
36    AcceptedBitset,
37}
38
39/// Preprocessed accept/reject/uncertain partition for one parser state.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct AdaptiveTokenMask {
42    /// Which representation the partition uses.
43    pub store_type: StoreType,
44    /// Accepted sorted-vocab indices (only for [`StoreType::Accepted`]).
45    pub accepted_indices: Vec<i32>,
46    /// Rejected sorted-vocab indices (only for [`StoreType::Rejected`]).
47    pub rejected_indices: Vec<i32>,
48    /// Accepted token ids as a bitset (only for
49    /// [`StoreType::AcceptedBitset`]); length == vocab size.
50    pub accepted_bitset: BitVec,
51    /// Uncertain sorted-vocab indices (always stored directly).
52    pub uncertain_indices: Vec<i32>,
53}
54
55impl AdaptiveTokenMask {
56    /// Build a mask from accepted + rejected + uncertain partitions.
57    ///
58    /// Port of the four-argument C++ constructor. `sorted_decoded_vocab`
59    /// supplies the `index -> token_id` mapping for the bitset form.
60    pub fn from_accepted_rejected(
61        vocab_size: usize,
62        sorted_decoded_vocab: &[(i32, Vec<u8>)],
63        accepted_indices: &[i32],
64        rejected_indices: &[i32],
65        uncertain_indices: &[i32],
66    ) -> Self {
67        let size_acc = accepted_indices.len();
68        let size_rej = rejected_indices.len();
69
70        let store_type = if size_acc >= USE_BITSET_THRESHOLD && size_rej >= USE_BITSET_THRESHOLD {
71            StoreType::AcceptedBitset
72        } else if size_acc < size_rej {
73            StoreType::Accepted
74        } else {
75            StoreType::Rejected
76        };
77
78        let mut mask = Self::empty(store_type, vocab_size);
79        match store_type {
80            StoreType::AcceptedBitset => {
81                for &idx in accepted_indices {
82                    let id = sorted_decoded_vocab[idx as usize].0 as usize;
83                    mask.accepted_bitset.set(id, true);
84                }
85            }
86            StoreType::Accepted => mask.accepted_indices = accepted_indices.to_vec(),
87            StoreType::Rejected => mask.rejected_indices = rejected_indices.to_vec(),
88        }
89        mask.uncertain_indices = uncertain_indices.to_vec();
90        mask
91    }
92
93    /// Build a mask from only accepted + uncertain partitions (the
94    /// rejected partition was not materialized — too large to be worth
95    /// storing). Port of the three-argument C++ constructor.
96    pub fn from_accepted(
97        vocab_size: usize,
98        sorted_decoded_vocab: &[(i32, Vec<u8>)],
99        accepted_indices: &[i32],
100        uncertain_indices: &[i32],
101    ) -> Self {
102        let store_type = if accepted_indices.len() >= USE_BITSET_THRESHOLD {
103            StoreType::AcceptedBitset
104        } else {
105            StoreType::Accepted
106        };
107
108        let mut mask = Self::empty(store_type, vocab_size);
109        match store_type {
110            StoreType::AcceptedBitset => {
111                for &idx in accepted_indices {
112                    let id = sorted_decoded_vocab[idx as usize].0 as usize;
113                    mask.accepted_bitset.set(id, true);
114                }
115            }
116            StoreType::Accepted => mask.accepted_indices = accepted_indices.to_vec(),
117            StoreType::Rejected => unreachable!("from_accepted never produces Rejected"),
118        }
119        mask.uncertain_indices = uncertain_indices.to_vec();
120        mask
121    }
122
123    /// An empty mask of the given store type — used internally and for
124    /// the `vocab_size == 0` degenerate compile path.
125    pub fn empty(store_type: StoreType, vocab_size: usize) -> Self {
126        Self {
127            store_type,
128            accepted_indices: Vec::new(),
129            rejected_indices: Vec::new(),
130            accepted_bitset: BitVec::repeat(false, vocab_size),
131            uncertain_indices: Vec::new(),
132        }
133    }
134
135    /// Materialize the accepted / rejected partitions as explicit
136    /// sorted-vocab index vectors, regardless of [`StoreType`].
137    ///
138    /// Port of the partition-reconstruction logic in
139    /// `AdaptiveTokenMask::Print`. Returns `(accepted, rejected)`.
140    pub fn materialize(&self, sorted_decoded_vocab: &[(i32, Vec<u8>)]) -> (Vec<i32>, Vec<i32>) {
141        let n = sorted_decoded_vocab.len();
142        let uncertain: std::collections::HashSet<i32> =
143            self.uncertain_indices.iter().copied().collect();
144        let mut accepted = Vec::new();
145        let mut rejected = Vec::new();
146
147        match self.store_type {
148            StoreType::AcceptedBitset => {
149                for i in 0..n {
150                    if uncertain.contains(&(i as i32)) {
151                        continue;
152                    }
153                    let id = sorted_decoded_vocab[i].0 as usize;
154                    if self.accepted_bitset[id] {
155                        accepted.push(i as i32);
156                    } else {
157                        rejected.push(i as i32);
158                    }
159                }
160            }
161            StoreType::Accepted => {
162                accepted = self.accepted_indices.clone();
163                let mut acc_ptr = 0usize;
164                for i in 0..n as i32 {
165                    while acc_ptr < accepted.len() && accepted[acc_ptr] < i {
166                        acc_ptr += 1;
167                    }
168                    if acc_ptr < accepted.len() && accepted[acc_ptr] == i {
169                        continue;
170                    }
171                    if uncertain.contains(&i) {
172                        continue;
173                    }
174                    rejected.push(i);
175                }
176            }
177            StoreType::Rejected => {
178                rejected = self.rejected_indices.clone();
179                let mut rej_ptr = 0usize;
180                for i in 0..n as i32 {
181                    while rej_ptr < rejected.len() && rejected[rej_ptr] < i {
182                        rej_ptr += 1;
183                    }
184                    if rej_ptr < rejected.len() && rejected[rej_ptr] == i {
185                        continue;
186                    }
187                    if uncertain.contains(&i) {
188                        continue;
189                    }
190                    accepted.push(i);
191                }
192            }
193        }
194        (accepted, rejected)
195    }
196
197    /// Approximate heap memory used by this mask, in bytes. Port of the
198    /// C++ `MemorySize(const AdaptiveTokenMask&)` friend.
199    pub fn memory_size(&self) -> usize {
200        self.accepted_indices.len() * 4
201            + self.rejected_indices.len() * 4
202            + self.uncertain_indices.len() * 4
203            + self.accepted_bitset.len() / 8
204    }
205}