metrale_grammar/matcher/completion.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// GrammarMatcher — grammar-legal completion to a stop-legal state.
4//
5// Powers budget-aware graceful close of structured outputs (Metrale Engine #144):
6// when a length-limited response would otherwise stop with the stop token
7// forbidden mid-structure (e.g. inside an open JSON string), this finds a
8// byte sequence that drives the grammar to a state where the root rule can
9// complete — so a truncated `finish_reason="length"` response is still
10// parseable.
11//
12// The search is a depth-first, CLOSURE-PREFERRING walk: at every choice
13// point it tries structural closers (`"` `}` `]` `,` `:` …) before content
14// bytes, so it commits to closing rather than exploring the exponential
15// space of string/number content continuations. That is what lets it reach
16// DEEP closes — e.g. an unemitted required field needing `,"tags":[]}` —
17// which a breadth-first search would never reach before exhausting its
18// budget on content branches.
19//
20// There is no C++ upstream for this; it is a Metrale Engine addition built on the
21// same byte-level Earley primitives as `find_jump_forward_string`
22// (`advance` / `pop_last_states` / `is_completed`).
23
24use super::matcher::GrammarMatcher;
25
26/// Hard cap on parser byte-advances explored. A close sequence is short in
27/// practice; if none is found under this budget the caller falls back to a
28/// plain finish — strictly no worse than the prior behavior.
29const COMPLETION_NODE_BUDGET: usize = 8192;
30
31/// Priority-ordered "closing" bytes tried before any content byte: string /
32/// container closers and JSON structural separators. Ordering the search to
33/// take these first makes it commit to closing the current structure instead
34/// of extending it.
35const CLOSE_PRIORITY: &[u8] = b"\"}],: \n\t";
36
37/// Max distinct CONTENT (non-priority) bytes explored per node. Forced
38/// positions — e.g. each character of a required key — expose exactly one
39/// acceptable byte, so a small fan-out preserves forced-run closes while
40/// bounding branching at genuine content choice points.
41const CONTENT_FANOUT: usize = 3;
42
43impl GrammarMatcher {
44 /// A grammar-legal byte sequence that advances the grammar from its
45 /// CURRENT state to one where the root rule can complete (a stop token
46 /// is legal), or `None` if none is found within `max_bytes` and the node
47 /// budget. The matcher state is left UNCHANGED.
48 ///
49 /// Returns `Some(empty)` when the grammar can already stop here.
50 ///
51 /// Soundness: every byte of a returned path is applied via
52 /// `parser.advance`, and a path is only returned once
53 /// `parser.is_completed()` holds — so the result is always a grammar-legal
54 /// completion. Closure-first ordering means it is not guaranteed to be the
55 /// globally shortest close, but it is a valid one and is found without
56 /// exploring content breadth.
57 #[must_use]
58 pub fn find_completion_to_accept(&mut self, max_bytes: usize) -> Option<Vec<u8>> {
59 if self.is_terminated() {
60 return None;
61 }
62 if self.parser.is_completed() {
63 return Some(Vec::new());
64 }
65 let mut out = Vec::new();
66 let mut budget = COMPLETION_NODE_BUDGET;
67 if self.close_dfs(max_bytes, &mut out, &mut budget) {
68 // `close_dfs` left the parser advanced through `out`; restore it so
69 // the search is side-effect-free for the caller.
70 if !out.is_empty() {
71 self.parser.pop_last_states(out.len());
72 }
73 Some(out)
74 } else {
75 None
76 }
77 }
78
79 /// Depth-first, closure-preferring search. Appends accepted bytes to
80 /// `out`; returns `true` with the parser left advanced through `out` once
81 /// the grammar can stop. On `false` it has restored the parser and left
82 /// `out` unchanged.
83 fn close_dfs(&mut self, remaining: usize, out: &mut Vec<u8>, budget: &mut usize) -> bool {
84 if self.parser.is_completed() {
85 return true;
86 }
87 if remaining == 0 || *budget == 0 {
88 return false;
89 }
90 let mask = self.parser.acceptable_byte_mask();
91 // 1) Structural closers first — commit to closing the open structure.
92 for &b in CLOSE_PRIORITY {
93 if mask[b as usize] && self.try_byte(b, remaining, out, budget) {
94 return true;
95 }
96 if *budget == 0 {
97 return false;
98 }
99 }
100 // 2) Bounded content fall-back — covers forced required-field bytes
101 // (where exactly one content byte is acceptable).
102 let mut content = 0;
103 for b in 0u16..256 {
104 let byte = b as u8;
105 if !mask[b as usize] || CLOSE_PRIORITY.contains(&byte) {
106 continue;
107 }
108 if content >= CONTENT_FANOUT || *budget == 0 {
109 break;
110 }
111 content += 1;
112 if self.try_byte(byte, remaining, out, budget) {
113 return true;
114 }
115 }
116 false
117 }
118
119 /// Advance by `byte` and recurse. Keeps the byte in `out` (parser left
120 /// advanced) iff the recursion reaches a completion; otherwise rolls the
121 /// byte back. Returns whether a completion was found through `byte`.
122 fn try_byte(
123 &mut self,
124 byte: u8,
125 remaining: usize,
126 out: &mut Vec<u8>,
127 budget: &mut usize,
128 ) -> bool {
129 *budget = budget.saturating_sub(1);
130 if !self.parser.advance(byte) {
131 return false;
132 }
133 out.push(byte);
134 if self.close_dfs(remaining - 1, out, budget) {
135 return true;
136 }
137 out.pop();
138 self.parser.pop_last_states(1);
139 false
140 }
141
142 /// Like [`Self::find_completion_to_accept`], but returns the close as
143 /// content **token ids**, greedily encoded against this matcher's vocab
144 /// (`sorted_decoded_vocab`, which excludes stop/special tokens — so a
145 /// close never emits a control token). Returns `Some(empty)` when the
146 /// grammar can already stop, and `None` when no bounded close exists or
147 /// a close byte is not representable as a content token.
148 #[must_use]
149 pub fn find_completion_token_ids(&mut self, max_bytes: usize) -> Option<Vec<i32>> {
150 let bytes = self.find_completion_to_accept(max_bytes)?;
151 if bytes.is_empty() {
152 return Some(Vec::new());
153 }
154 self.encode_bytes_greedy(&bytes)
155 }
156
157 /// Greedy longest-match encode of `bytes` into content token ids. The
158 /// concatenated token bytes equal `bytes` by construction, so detokenizing
159 /// the result reproduces the close exactly. `None` if any position has no
160 /// covering token.
161 fn encode_bytes_greedy(&self, bytes: &[u8]) -> Option<Vec<i32>> {
162 let vocab = self.tokenizer_info().sorted_decoded_vocab();
163 let mut out = Vec::new();
164 let mut i = 0;
165 while i < bytes.len() {
166 let rem = &bytes[i..];
167 let mut best: Option<(i32, usize)> = None;
168 for (id, tok) in vocab {
169 let len = tok.len();
170 if len == 0 || len > rem.len() || !rem.starts_with(tok.as_slice()) {
171 continue;
172 }
173 if best.is_none_or(|(_, bl)| len > bl) {
174 best = Some((*id, len));
175 }
176 }
177 let (id, len) = best?;
178 out.push(id);
179 i += len;
180 }
181 Some(out)
182 }
183}