Skip to content

Commit cef1d23

Browse files
authored
common/grammar : replace problematic backtracking regex [\s\S]* (#18342)
* grammar : add support for std::regex_search() with trigger patterns * common : update hermes2 pro trigger to search instead of match * common : use regex_search with anchoring for partial matching * common : adjust regex partial tests to use new pattern * grammar : check pattern directly instead of adding a type * common : adjust existing patterns to match new semantics
1 parent c69c7eb commit cef1d23

6 files changed

Lines changed: 83 additions & 52 deletions

File tree

common/chat.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,7 +2065,7 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp
20652065
// Trigger on tool calls that appear in the commentary channel
20662066
data.grammar_triggers.push_back({
20672067
COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN,
2068-
"<\\|channel\\|>(commentary|analysis) to"
2068+
"<\\|channel\\|>(?:commentary|analysis) to"
20692069
});
20702070

20712071
// Trigger tool calls that appear in the role section, either at the
@@ -2398,17 +2398,17 @@ static common_chat_params common_chat_params_init_hermes_2_pro(const common_chat
23982398
(inputs.parallel_tool_calls ? "(" + tool_call + ")+" : tool_call));
23992399
// Trigger on some common known "good bad" outputs (only from the start and with a json that's about a specific argument name to avoid false positives)
24002400
data.grammar_triggers.push_back({
2401-
COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
2401+
COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN,
24022402
// If thinking_forced_open, then we capture the </think> tag in the grammar,
24032403
// (important for required tool choice) and in the trigger's first capture (decides what is sent to the grammar)
2404-
std::string(data.thinking_forced_open ? "[\\s\\S]*?(</think>\\s*)" : "(?:<think>[\\s\\S]*?</think>\\s*)?") + (
2404+
std::string(data.thinking_forced_open ? "(</think>\\s*)" : "") + (
24052405
"\\s*("
24062406
"(?:<tool_call>"
24072407
"|<function"
24082408
"|(?:```(?:json|xml)?\n\\s*)?(?:<function_call>|<tools>|<xml><json>|<response>)?"
24092409
"\\s*\\{\\s*\"name\"\\s*:\\s*\"(?:" + string_join(escaped_names, "|") + ")\""
24102410
")"
2411-
")[\\s\\S]*"
2411+
")"
24122412
),
24132413
});
24142414
data.preserved_tokens = {

common/regex-partial.cpp

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ common_regex_match common_regex::search(const std::string & input, size_t pos, b
2727
return res;
2828
}
2929
std::match_results<std::string::const_reverse_iterator> srmatch;
30-
if (std::regex_match(input.rbegin(), input.rend() - pos, srmatch, rx_reversed_partial)) {
30+
if (std::regex_search(input.rbegin(), input.rend() - pos, srmatch, rx_reversed_partial, std::regex_constants::match_continuous)) {
3131
auto group = srmatch[1].str();
3232
if (group.length() != 0) {
3333
auto it = srmatch[1].second.base();
@@ -55,18 +55,18 @@ common_regex_match common_regex::search(const std::string & input, size_t pos, b
5555
to see if a string ends with a partial regex match, but but it's not in std::regex yet.
5656
Instead, we'll the regex into a partial match regex operating as a full match on the reverse iterators of the input.
5757
58-
- /abcd/ -> (dcba|cba|ba|a).* -> ((?:(?:(?:(?:d)?c)?b)?a).*
59-
- /a|b/ -> (a|b).*
58+
- /abcd/ -> ^(dcba|cba|ba|a) -> ^((?:(?:(?:(?:d)?c)?b)?a)
59+
- /a|b/ -> ^(a|b)
6060
- /a*?/ -> error, could match ""
61-
- /a*b/ -> ((?:b)?a*+).* (final repetitions become eager)
62-
- /.*?ab/ -> ((?:b)?a).* (merge .*)
63-
- /a.*?b/ -> ((?:b)?.*?a).* (keep reluctant matches)
64-
- /a(bc)d/ -> ((?:(?:d)?(?:(?:c)?b))?a).*
65-
- /a(bc|de)/ -> ((?:(?:(?:e)?d)?|(?:(?:c)?b)?)?a).*
66-
- /ab{2,4}c/ -> abbb?b?c -> ((?:(?:(?:(?:(?:c)?b)?b)?b?)?b?)?a).*
61+
- /a*b/ -> ^((?:b)?a*+) (final repetitions become eager)
62+
- /.*?ab/ -> ^((?:b)?a) (omit .*)
63+
- /a.*?b/ -> ^((?:b)?.*?a) (keep reluctant matches)
64+
- /a(bc)d/ -> ^((?:(?:d)?(?:(?:c)?b))?a)
65+
- /a(bc|de)/ -> ^((?:(?:(?:e)?d)?|(?:(?:c)?b)?)?a)
66+
- /ab{2,4}c/ -> ^cbbb?b?a -> ^((?:(?:(?:(?:(?:c)?b)?b)?b?)?b?)?a)
6767
68-
The regex will match a reversed string fully, and the end of the first (And only) capturing group will indicate the reversed start of the original partial pattern
69-
(i.e. just where the final .* starts in the inverted pattern; all other groups are turned into non-capturing groups, and reluctant quantifiers are ignored)
68+
The regex will match a reversed string fully, and the end of the first (And only) capturing group will indicate the reversed start of the original partial pattern.
69+
All other groups are turned into non-capturing groups, and reluctant quantifiers are ignored.
7070
*/
7171
std::string regex_to_reversed_partial_regex(const std::string & pattern) {
7272
auto it = pattern.begin();
@@ -177,7 +177,7 @@ std::string regex_to_reversed_partial_regex(const std::string & pattern) {
177177
}
178178
}
179179

180-
// /abcd/ -> (dcba|cba|ba|a).* -> ((?:(?:(?:d)?c)?b)?a).*
180+
// /abcd/ -> ^(dcba|cba|ba|a) -> ^((?:(?:(?:d)?c)?b)?a)
181181
// if n(=4) parts, opening n-1(=3) non-capturing groups after the 1 capturing group
182182
// We'll do the outermost capturing group and final .* in the enclosing function.
183183
std::vector<std::string> res_alts;
@@ -200,5 +200,5 @@ std::string regex_to_reversed_partial_regex(const std::string & pattern) {
200200
throw std::runtime_error("Unmatched '(' in pattern");
201201
}
202202

203-
return "(" + res + ")[\\s\\S]*";
203+
return "^(" + res + ")";
204204
}

common/sampling.cpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,24 +179,30 @@ struct common_sampler * common_sampler_init(const struct llama_model * model, co
179179
#endif // LLAMA_USE_LLGUIDANCE
180180
} else {
181181
std::vector<std::string> trigger_patterns;
182-
std::vector<std::string> patterns_anywhere;
183182
std::vector<llama_token> trigger_tokens;
184183
for (const auto & trigger : params.grammar_triggers) {
185184
switch (trigger.type) {
186185
case COMMON_GRAMMAR_TRIGGER_TYPE_WORD:
187186
{
188187
const auto & word = trigger.value;
189-
patterns_anywhere.push_back(regex_escape(word));
188+
trigger_patterns.push_back(regex_escape(word));
190189
break;
191190
}
192191
case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN:
193192
{
194-
patterns_anywhere.push_back(trigger.value);
193+
trigger_patterns.push_back(trigger.value);
195194
break;
196195
}
197196
case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL:
198197
{
199-
trigger_patterns.push_back(trigger.value);
198+
const auto & pattern = trigger.value;
199+
std::string anchored = "^$";
200+
if (!pattern.empty()) {
201+
anchored = (pattern.front() != '^' ? "^" : "")
202+
+ pattern
203+
+ (pattern.back() != '$' ? "$" : "");
204+
}
205+
trigger_patterns.push_back(anchored);
200206
break;
201207
}
202208
case COMMON_GRAMMAR_TRIGGER_TYPE_TOKEN:
@@ -210,10 +216,6 @@ struct common_sampler * common_sampler_init(const struct llama_model * model, co
210216
}
211217
}
212218

213-
if (!patterns_anywhere.empty()) {
214-
trigger_patterns.push_back("^[\\s\\S]*?(" + string_join(patterns_anywhere, "|") + ")[\\s\\S]*");
215-
}
216-
217219
std::vector<const char *> trigger_patterns_c;
218220
trigger_patterns_c.reserve(trigger_patterns.size());
219221
for (const auto & regex : trigger_patterns) {

src/llama-grammar.cpp

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,44 @@ static void print_rule(
369369
fprintf(file, "\n");
370370
}
371371

372+
//
373+
// Regex utilities
374+
//
375+
376+
size_t llama_grammar_trigger_pattern::find(const std::string & input) const {
377+
auto find_start_pos = [](const std::smatch & match) {
378+
// get from the first matched capturing group to the end of the string
379+
size_t start = std::string::npos;
380+
for (auto i = 1u; i < match.size(); i++) {
381+
if (match.length(i) > 0) {
382+
start = match.position(i);
383+
break;
384+
}
385+
}
386+
if (start == std::string::npos) {
387+
start = match.position(0);
388+
}
389+
return start;
390+
};
391+
392+
if (!pattern.empty() && pattern.front() == '^' && pattern.back() == '$') {
393+
// match against the entire input
394+
std::smatch match;
395+
if (std::regex_match(input, match, regex)) {
396+
return find_start_pos(match);
397+
}
398+
}
399+
400+
// search anywhere
401+
std::smatch match;
402+
if (std::regex_search(input, match, regex)) {
403+
return find_start_pos(match);
404+
}
405+
406+
return std::string::npos;
407+
}
408+
409+
372410
//
373411
// implementation
374412
//
@@ -1312,21 +1350,10 @@ void llama_grammar_accept_impl(struct llama_grammar & grammar, llama_token token
13121350
grammar.trigger_buffer_positions.push_back(std::make_pair(token, position));
13131351
grammar.trigger_buffer += piece;
13141352

1315-
std::smatch match;
13161353
for (const auto & trigger_pattern : grammar.trigger_patterns) {
1317-
if (std::regex_match(grammar.trigger_buffer, match, trigger_pattern.regex)) {
1354+
auto start = trigger_pattern.find(grammar.trigger_buffer);
1355+
if (start != std::string::npos) {
13181356
grammar.awaiting_trigger = false;
1319-
// get from the first matched capturing group to the end of the string
1320-
size_t start = std::string::npos;
1321-
for (auto i = 1u; i < match.size(); i++) {
1322-
if (match.length(i) > 0) {
1323-
start = match.position(i);
1324-
break;
1325-
}
1326-
}
1327-
if (start == std::string::npos) {
1328-
start = match.position(0);
1329-
}
13301357

13311358
// replay tokens that overlap with [start, end)
13321359
for (const auto & [tok, tok_pos] : grammar.trigger_buffer_positions) {

src/llama-grammar.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ struct llama_grammar_parser {
119119
struct llama_grammar_trigger_pattern {
120120
std::string pattern;
121121
std::regex regex;
122+
123+
size_t find(const std::string & input) const;
122124
};
123125

124126
struct llama_grammar {

tests/test-regex-partial.cpp

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -232,52 +232,52 @@ static void test_regex_to_reversed_partial_regex() {
232232
printf("[%s]\n", __func__);
233233

234234
assert_equals<std::string>(
235-
"((?:(?:c)?b)?a)[\\s\\S]*",
235+
"^((?:(?:c)?b)?a)",
236236
regex_to_reversed_partial_regex("abc"));
237237

238238
assert_equals<std::string>(
239-
"(a+)[\\s\\S]*",
239+
"^(a+)",
240240
regex_to_reversed_partial_regex("a+"));
241241

242242
assert_equals<std::string>(
243-
"(a*)[\\s\\S]*",
243+
"^(a*)",
244244
regex_to_reversed_partial_regex("a*"));
245245

246246
assert_equals<std::string>(
247-
"(a?)[\\s\\S]*",
247+
"^(a?)",
248248
regex_to_reversed_partial_regex("a?"));
249249

250250
assert_equals<std::string>(
251-
"([a-z])[\\s\\S]*",
251+
"^([a-z])",
252252
regex_to_reversed_partial_regex("[a-z]"));
253253

254254
assert_equals<std::string>(
255-
"((?:\\w+)?[a-z])[\\s\\S]*",
255+
"^((?:\\w+)?[a-z])",
256256
regex_to_reversed_partial_regex("[a-z]\\w+"));
257257

258258
assert_equals<std::string>(
259-
"((?:a|b))[\\s\\S]*",
259+
"^((?:a|b))",
260260
regex_to_reversed_partial_regex("(?:a|b)"));
261261
assert_equals<std::string>(
262-
"((?:(?:(?:d)?c)?b)?a)[\\s\\S]*",
262+
"^((?:(?:(?:d)?c)?b)?a)",
263263
regex_to_reversed_partial_regex("abcd"));
264264
assert_equals<std::string>(
265-
"((?:b)?a*)[\\s\\S]*", // TODO: ((?:b)?a*+).* ??
265+
"^((?:b)?a*)", // TODO: ((?:b)?a*+).* ??
266266
regex_to_reversed_partial_regex("a*b"));
267267
assert_equals<std::string>(
268-
"((?:(?:b)?a)?.*)[\\s\\S]*",
268+
"^((?:(?:b)?a)?.*)",
269269
regex_to_reversed_partial_regex(".*?ab"));
270270
assert_equals<std::string>(
271-
"((?:(?:b)?.*)?a)[\\s\\S]*",
271+
"^((?:(?:b)?.*)?a)",
272272
regex_to_reversed_partial_regex("a.*?b"));
273273
assert_equals<std::string>(
274-
"((?:(?:d)?(?:(?:c)?b))?a)[\\s\\S]*",
274+
"^((?:(?:d)?(?:(?:c)?b))?a)",
275275
regex_to_reversed_partial_regex("a(bc)d"));
276276
assert_equals<std::string>(
277-
"((?:(?:(?:c)?b|(?:e)?d))?a)[\\s\\S]*",
277+
"^((?:(?:(?:c)?b|(?:e)?d))?a)",
278278
regex_to_reversed_partial_regex("a(bc|de)"));
279279
assert_equals<std::string>(
280-
"((?:(?:(?:(?:(?:c)?b?)?b?)?b)?b)?a)[\\s\\S]*",
280+
"^((?:(?:(?:(?:(?:c)?b?)?b?)?b)?b)?a)",
281281
regex_to_reversed_partial_regex("ab{2,4}c"));
282282
}
283283

0 commit comments

Comments
 (0)