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}