metrale_grammar/compiler/
compiled_grammar.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// CompiledGrammar — port of `class CompiledGrammar` / `CompiledGrammar
4// ::Impl` from `cpp/compiled_grammar.cc` + `cpp/compiled_grammar_impl.h`.
5//
6// A grammar compiled against a specific tokenizer: it bundles the
7// optimized, FSM-accelerated grammar, the tokenizer info, and a lazy
8// per-parser-state adaptive-token-mask cache. The matcher (W6) consults
9// `get_or_compute_mask` to fill the logit bitmask fast.
10//
11// XGrammar-2 JIT (lazy) MASK COMPILATION
12// --------------------------------------
13// The eager port precomputed an `AdaptiveTokenMask` for *every*
14// reachable scanable state up front. For tool-call JSON-schema grammars
15// that is hundreds of masks per `compile_*` call — most never used by a
16// single generation. This port computes each state's mask LAZILY, on
17// first lookup by the matcher, and caches it in `mask_cache`. Only the
18// states a generation actually visits get a mask computed, once each.
19
20use std::sync::{Arc, Mutex};
21
22use ahash::AHashMap;
23
24use crate::earley::ParserState;
25use crate::grammar::GrammarData;
26use crate::grammar::functor::hash_sequence;
27use crate::support::hash::hash_combine;
28use crate::tokenizer::TokenizerInfo;
29
30use super::decompose::GrammarDecomposition;
31use super::mask::AdaptiveTokenMask;
32use super::mask_gen::MaskGenerator;
33use super::rule_cache::{RuleLevelCache, RuleMaskKey};
34
35/// The inner, shared state of a [`CompiledGrammar`]. Port of
36/// `CompiledGrammar::Impl`.
37#[derive(Debug)]
38pub struct CompiledGrammarImpl {
39    /// Maximum synchronous prewarm workers, inherited from the compiler.
40    pub prewarm_max_threads: usize,
41    /// The optimized, FSM-accelerated grammar (shared — the lazy
42    /// `MaskGenerator` needs an `Arc<GrammarData>`).
43    pub grammar: Arc<GrammarData>,
44    /// The tokenizer this grammar was compiled against.
45    pub tokenizer_info: TokenizerInfo,
46    /// Lazy per-parser-state adaptive-token-mask cache. Equivalent to
47    /// the C++ `adaptive_token_mask_cache` — a plain hash map, populated
48    /// on demand: empty after compilation, filled by [`CompiledGrammar::
49    /// get_or_compute_mask`] as the matcher reaches each state.
50    ///
51    /// The matcher drives one `CompiledGrammar` single-threaded per
52    /// request, so the C++ uses a plain `unordered_map` with no locking.
53    /// We cannot drop the lock entirely, however: [`super::super::
54    /// matcher::BatchGrammarMatcher`]'s rayon `par_iter_mut` fill path
55    /// runs many matchers that were all cloned from one `CompiledGrammar`
56    /// — they share this `Arc<CompiledGrammarImpl>` and may call
57    /// `get_or_compute_mask` concurrently. A single uncontended `Mutex`
58    /// lock is far cheaper than `DashMap`'s shard-hash + shard-select
59    /// machinery on every per-token lookup, while still keeping
60    /// `CompiledGrammarImpl: Sync`.
61    pub mask_cache: Mutex<AHashMap<ParserState, Arc<AdaptiveTokenMask>>>,
62    /// TagDispatch second-slice precomputation, keyed by rule id. Built
63    /// once at compile time and retained here so on-demand mask
64    /// computation can feed it to the `MaskGenerator`. `Arc`-wrapped
65    /// so `MaskGenerator::new` clones a pointer, not the map.
66    pub tag_slice: Arc<AHashMap<i32, Vec<bool>>>,
67    /// The cross-grammar [`RuleLevelCache`], shared from the owning
68    /// [`super::GrammarCompiler`] (`None` when the compiler's cache is
69    /// disabled, or for the `vocab_size == 0` degenerate path). The lazy
70    /// mask path consults it on a per-state cache miss before falling
71    /// back to a full `MaskGenerator` recomputation — a rule
72    /// structurally identical to one seen in any previous request
73    /// reuses its computed masks.
74    pub rule_cache: Option<RuleLevelCache>,
75    /// WGRAMMAR static/dynamic decomposition (Tier 3c), computed ONCE
76    /// at compile time by [`super::decompose::decompose_static_regions`].
77    /// Classifies every rule as fixed scaffolding (with its literal
78    /// bytes precomputed) or a dynamic value slot — the compile-time
79    /// index of the grammar's static structure. See the module docs of
80    /// `decompose.rs` for how this composes with Tiers 2 and 3b.
81    pub decomposition: GrammarDecomposition,
82}
83
84impl CompiledGrammarImpl {
85    /// Approximate heap memory usage in bytes. Port of
86    /// `MemorySize(const CompiledGrammar::Impl&)`. The mask term sums
87    /// only the masks computed so far (the lazy cache).
88    pub fn memory_size(&self) -> usize {
89        let grammar_bytes = self.grammar.complete_fsm.memory_size()
90            + self.grammar.num_exprs() as usize * 4
91            + self.grammar.num_rules() as usize * 32;
92        let mask_bytes: usize = self
93            .mask_cache
94            .lock()
95            .expect("mask_cache mutex poisoned")
96            .values()
97            .map(|m| m.memory_size())
98            .sum();
99        grammar_bytes + mask_bytes
100    }
101
102    /// Build the cross-grammar [`RuleMaskKey`] for a canonical parser
103    /// state, returning it together with the shared [`RuleLevelCache`].
104    ///
105    /// `None` — the rule-level cache is unavailable for this state — when
106    /// any of the following hold (faithful to the upstream
107    /// `rule_level_cache_is_available` guard in `grammar_compiler.cc`):
108    ///   * the compiler has no `rule_cache`;
109    ///   * the rule has no structural FSM hash (the hasher could not
110    ///     hash it — e.g. an unbroken complex cycle);
111    ///   * the canonical FSM node id has no entry in the rule's
112    ///     `per_rule_fsm_new_state_ids` renumbering.
113    ///
114    /// KEY DERIVATION. The structural part is `(fsm_hash,
115    /// fsm_new_node_id, state_cnt, edge_cnt)` — exactly the upstream
116    /// `RuleLevelCache::Impl::NodeKey`. Upstream caches the
117    /// *no-lookahead* mask under the bare `fsm_hash` and the
118    /// lookahead-aware mask under `HashCombine(fsm_hash, lookahead_hash,
119    /// is_exact_lookahead)`. This port's `MaskGenerator` yields the
120    /// final lookahead-applied mask in one shot, so the key folds in the
121    /// lookahead hash (and `is_exact_lookahead`) whenever the rule has a
122    /// lookahead assertion, and folds in `is_root` — guaranteeing a
123    /// cache hit is byte-identical to a fresh recompute. A plain
124    /// no-lookahead non-root rule therefore keys on the bare structural
125    /// hash and reuses freely across grammars.
126    fn rule_mask_key(
127        &self,
128        canonical: &ParserState,
129        is_root: bool,
130    ) -> Option<(&RuleLevelCache, RuleMaskKey)> {
131        let cache = self.rule_cache.as_ref()?;
132        let rule_id = canonical.rule_id;
133        let base_hash = (*self.grammar.per_rule_fsm_hashes.get(rule_id as usize)?)?;
134
135        // Map the FSM node id (`element_id`) to its canonical renumbered
136        // id via `per_rule_fsm_new_state_ids` (a list of original->new
137        // pairs, exactly as the C++ `original_to_new_id` scan).
138        let renumber = self
139            .grammar
140            .per_rule_fsm_new_state_ids
141            .get(rule_id as usize)?;
142        let fsm_new_node_id = renumber
143            .iter()
144            .find(|(orig, _)| *orig == canonical.element_id)
145            .map(|(_, new)| *new)?;
146
147        let fsm = self.grammar.per_rule_fsms[rule_id as usize]
148            .as_ref()
149            .expect("optimized grammar must have a per-rule FSM");
150        let state_cnt = fsm.num_states() as i32;
151        let edge_cnt = fsm.num_edges() as i32;
152
153        // Fold lookahead + root into the hash so a hit is byte-exact.
154        let rule = self.grammar.rule(rule_id);
155        let mut fsm_hash = base_hash;
156        if rule.lookahead_assertion_id != -1 {
157            // `hash_sequence` may return `None` for a non-hashable
158            // lookahead sequence; treat that as "cache unavailable" so
159            // we never key two semantically different masks alike.
160            let la_hash = hash_sequence(&self.grammar, rule.lookahead_assertion_id)?;
161            fsm_hash = hash_combine(&[fsm_hash, la_hash, rule.is_exact_lookahead as u64]);
162        }
163        if is_root {
164            fsm_hash = hash_combine(&[fsm_hash, u64::MAX]);
165        }
166
167        Some((
168            cache,
169            RuleMaskKey {
170                fsm_hash,
171                fsm_new_node_id,
172                state_cnt,
173                edge_cnt,
174            },
175        ))
176    }
177}
178
179/// A grammar compiled against a tokenizer — the result of
180/// preprocessing performed by [`super::GrammarCompiler`].
181///
182/// Cheap to clone: the inner state is shared via [`Arc`], matching the
183/// C++ pimpl `shared_ptr` semantics.
184#[derive(Debug, Clone)]
185pub struct CompiledGrammar {
186    pimpl: Arc<CompiledGrammarImpl>,
187}
188
189impl CompiledGrammar {
190    /// Wrap an already-built [`CompiledGrammarImpl`].
191    pub fn from_impl(pimpl: Arc<CompiledGrammarImpl>) -> Self {
192        Self { pimpl }
193    }
194
195    /// The associated optimized grammar.
196    pub fn grammar(&self) -> &GrammarData {
197        &self.pimpl.grammar
198    }
199
200    /// Shared-pointer access to the optimized grammar — used to seed a
201    /// lazily-constructed `MaskGenerator`.
202    pub fn grammar_arc(&self) -> Arc<GrammarData> {
203        Arc::clone(&self.pimpl.grammar)
204    }
205
206    /// The associated tokenizer info.
207    pub fn tokenizer_info(&self) -> &TokenizerInfo {
208        &self.pimpl.tokenizer_info
209    }
210
211    /// The WGRAMMAR static/dynamic decomposition (Tier 3c) — the
212    /// compile-time split of every rule body into fixed scaffolding
213    /// spans (with precomputed literal bytes) and dynamic value-slot
214    /// spans.
215    ///
216    /// Computed once during compilation. A scheduler / matcher can
217    /// consult it — before the first decode step — to learn the
218    /// grammar's static/dynamic structure without any per-token work.
219    /// Each [`Segment::Static`](super::Segment::Static) span is exactly
220    /// a forced-byte run Tier 3b would otherwise rediscover lazily; the
221    /// genuine WGRAMMAR delta is doing this at compile time.
222    pub fn decomposition(&self) -> &GrammarDecomposition {
223        &self.pimpl.decomposition
224    }
225
226    /// Get the adaptive token mask for a canonical parser state,
227    /// computing and caching it on first access (the XGrammar-2 JIT
228    /// lazy path).
229    ///
230    /// `canonical` MUST be the canonical cache key — `rule_start_pos =
231    /// -1`, `sub_element_id`/`repeat_count`/`partial_codepoint = 0`,
232    /// `sequence_id = body_expr_id` — exactly the tuple the eager port
233    /// used. `is_root` is whether `canonical.rule_id` is the grammar
234    /// root (the root rule has no uncertain tokens).
235    ///
236    /// The returned `Arc` is byte-identical to what the old eager
237    /// compile produced: same `MaskGenerator`, same canonical key.
238    pub fn get_or_compute_mask(
239        &self,
240        canonical: ParserState,
241        is_root: bool,
242    ) -> Arc<AdaptiveTokenMask> {
243        // Fast path: a cache hit takes one uncontended `Mutex` lock and
244        // an `Arc` clone — nothing else.
245        if let Some(hit) = self
246            .pimpl
247            .mask_cache
248            .lock()
249            .expect("mask_cache mutex poisoned")
250            .get(&canonical)
251        {
252            return Arc::clone(hit);
253        }
254        // Per-grammar miss: compute the mask WITHOUT holding the lock —
255        // the `MaskGenerator` scan is expensive, and serializing it
256        // under the lock would defeat the parallel `BatchGrammarMatcher`
257        // fill. A concurrent computer of the same state simply does
258        // duplicate work; the `entry` double-check below keeps a single
259        // canonical `Arc`.
260        //
261        // Before recomputing, consult the cross-grammar `RuleLevelCache`
262        // (Tier 2): a rule structurally identical to one seen in a
263        // previous request — even from a different grammar — already has
264        // its masks computed. The cross-grammar key is structural
265        // (FSM hash + canonical node + node/edge counts), so a hit is
266        // byte-identical to a fresh `MaskGenerator` run.
267        let computed = match self.pimpl.rule_mask_key(&canonical, is_root) {
268            Some((cache, key)) => {
269                cache.get_or_compute(key, || Self::compute_mask(&self.pimpl, canonical, is_root))
270            }
271            None => Arc::new(Self::compute_mask(&self.pimpl, canonical, is_root)),
272        };
273        let mut cache = self
274            .pimpl
275            .mask_cache
276            .lock()
277            .expect("mask_cache mutex poisoned");
278        Arc::clone(cache.entry(canonical).or_insert(computed))
279    }
280
281    /// Run a full `MaskGenerator` scan for `canonical` — the
282    /// authoritative (uncached) mask computation. `&Arc<AHashMap>`
283    /// deref-coerces to the `&AHashMap` argument.
284    fn compute_mask(
285        pimpl: &Arc<CompiledGrammarImpl>,
286        canonical: ParserState,
287        is_root: bool,
288    ) -> AdaptiveTokenMask {
289        let mut generator = MaskGenerator::new(
290            Arc::clone(&pimpl.grammar),
291            canonical,
292            &pimpl.tokenizer_info,
293            &pimpl.tag_slice,
294        );
295        generator.get_adaptive_token_mask(is_root)
296    }
297
298    /// Approximate memory usage in bytes. Port of
299    /// `CompiledGrammar::MemorySizeBytes`.
300    pub fn memory_size_bytes(&self) -> usize {
301        self.pimpl.memory_size()
302    }
303
304    /// OVERLAPPED MASK GENERATION (Tier 2, "configurable JIT").
305    ///
306    /// Eagerly compute the `k` most-expensive adaptive-token masks,
307    /// populating the lazy JIT cache before the matcher needs them. Uses
308    /// at most the compiler's `max_threads`, including the caller, and
309    /// joins all workers before returning. Metrale Engine calls this synchronously
310    /// before prompt prefill; no GPU overlap is implied.
311    ///
312    /// COST RANKING. A state's mask cost is dominated by the breadth of
313    /// its first-character scan: every distinct first byte the state can
314    /// accept seeds an `EarleyParser` walk over the matching slice of
315    /// the sorted vocabulary. We therefore rank reachable scanable
316    /// states by their char-range edge count (a cheap, monotonic proxy
317    /// for that breadth) and warm the top `k`.
318    ///
319    /// Returns the number of masks actually computed (`<= k`, and `<=`
320    /// the number of reachable scanable states). `k == 0`, or an empty
321    /// vocabulary, computes nothing. Idempotent: a state already in the
322    /// cache is recomputed cheaply via `get_or_compute_mask`'s fast path
323    /// and still counts toward the return value.
324    pub fn compile_top_k_masks(&self, k: usize) -> usize {
325        use crate::earley::NO_PREV_INPUT_POS;
326
327        if k == 0 || self.pimpl.tokenizer_info.vocab_size() == 0 {
328            return 0;
329        }
330        let grammar = &self.pimpl.grammar;
331        let root_rule_id = grammar.root_rule_id();
332
333        // Collect (cost, canonical_state, is_root) for every reachable
334        // scanable state, where cost is the char-range edge count.
335        let mut ranked: Vec<(usize, ParserState, bool)> = Vec::new();
336        for rule_id in 0..grammar.num_rules() {
337            let rule = grammar.rule(rule_id);
338            let fsm = grammar.per_rule_fsms[rule_id as usize]
339                .as_ref()
340                .expect("optimized grammar must have a per-rule FSM");
341            let mut reachable = ahash::AHashSet::new();
342            fsm.reachable_states(&mut reachable);
343            let is_root = rule_id == root_rule_id;
344            for state_id in reachable {
345                let char_edges = fsm
346                    .fsm()
347                    .edges(state_id as usize)
348                    .iter()
349                    .filter(|e| e.is_char_range())
350                    .count();
351                if char_edges == 0 {
352                    continue; // not scanable — no mask to compute
353                }
354                let canonical =
355                    ParserState::new(rule_id, rule.body_expr_id, state_id, NO_PREV_INPUT_POS, 0);
356                ranked.push((char_edges, canonical, is_root));
357            }
358        }
359
360        // Most expensive first. `select_nth_unstable` would suffice, but
361        // a full sort keeps the warm order deterministic for tests and
362        // the list is small (one entry per scanable FSM state).
363        ranked.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.0));
364        let take = k.min(ranked.len());
365        let pending = self.uncached_masks(&ranked[..take]);
366        super::prewarm::for_each_bounded(&pending, self.pimpl.prewarm_max_threads, &|&(
367            _,
368            canonical,
369            is_root,
370        )| {
371            self.get_or_compute_mask(canonical, is_root);
372        });
373        take
374    }
375
376    pub(super) fn uncached_masks(
377        &self,
378        selected: &[(usize, ParserState, bool)],
379    ) -> Vec<(usize, ParserState, bool)> {
380        let cache = self
381            .pimpl
382            .mask_cache
383            .lock()
384            .expect("mask_cache mutex poisoned");
385        selected
386            .iter()
387            .copied()
388            .filter(|(_, state, _)| !cache.contains_key(state))
389            .collect()
390    }
391
392    /// Shared-pointer access to the inner state — used by the matcher.
393    pub fn inner(&self) -> &Arc<CompiledGrammarImpl> {
394        &self.pimpl
395    }
396
397    /// Enumerate every reachable scanable canonical [`ParserState`] and
398    /// its (lazily-computed) [`AdaptiveTokenMask`].
399    ///
400    /// This is exactly the state set the *eager* compiler precomputed —
401    /// reproduced here so tests can verify the JIT result equals the
402    /// old eager result and that the partition invariants hold. Each
403    /// returned mask is materialized through [`Self::get_or_compute_mask`],
404    /// i.e. the lazy path. Test-only.
405    #[cfg(test)]
406    pub(crate) fn all_reachable_masks(&self) -> Vec<(ParserState, Arc<AdaptiveTokenMask>)> {
407        use crate::earley::NO_PREV_INPUT_POS;
408
409        let mut out = Vec::new();
410        if self.pimpl.tokenizer_info.vocab_size() == 0 {
411            return out;
412        }
413        let grammar = &self.pimpl.grammar;
414        let root_rule_id = grammar.root_rule_id();
415        for rule_id in 0..grammar.num_rules() {
416            let rule = grammar.rule(rule_id);
417            let fsm = grammar.per_rule_fsms[rule_id as usize]
418                .as_ref()
419                .expect("optimized grammar must have a per-rule FSM");
420            let mut reachable = ahash::AHashSet::new();
421            fsm.reachable_states(&mut reachable);
422            let is_root = rule_id == root_rule_id;
423            for state_id in reachable {
424                let scanable = fsm
425                    .fsm()
426                    .edges(state_id as usize)
427                    .iter()
428                    .any(|e| e.is_char_range());
429                if !scanable {
430                    continue;
431                }
432                let canonical =
433                    ParserState::new(rule_id, rule.body_expr_id, state_id, NO_PREV_INPUT_POS, 0);
434                let mask = self.get_or_compute_mask(canonical, is_root);
435                out.push((canonical, mask));
436            }
437        }
438        out
439    }
440}