@@ -13182,6 +13182,58 @@ static std::vector<llama_grammar_candidate> llama_grammar_reject_candidates(
13182
13182
return rejects;
13183
13183
}
13184
13184
13185
+ static bool llama_grammar_detect_left_recursion(
13186
+ const std::vector<std::vector<llama_grammar_element>> & rules,
13187
+ size_t rule_index,
13188
+ std::vector<bool> * rules_visited,
13189
+ std::vector<bool> * rules_in_progress,
13190
+ std::vector<bool> * rules_may_be_empty) {
13191
+ if ((*rules_in_progress)[rule_index]) {
13192
+ return true;
13193
+ }
13194
+
13195
+ (*rules_in_progress)[rule_index] = true;
13196
+
13197
+ const std::vector<llama_grammar_element> & rule = rules[rule_index];
13198
+
13199
+ // First check if the rule might produce the empty string. This could be done combined with the second
13200
+ // step but it's more readable as two steps.
13201
+ bool at_rule_start = true;
13202
+ for (size_t i = 0; i < rule.size(); i++) {
13203
+ if (llama_grammar_is_end_of_sequence(&rule[i])) {
13204
+ if (at_rule_start) {
13205
+ (*rules_may_be_empty)[rule_index] = true;
13206
+ break;
13207
+ }
13208
+ at_rule_start = true;
13209
+ } else {
13210
+ at_rule_start = false;
13211
+ }
13212
+ }
13213
+
13214
+ // Second, recurse into leftmost nonterminals (or next-leftmost as long as the previous nonterminal may
13215
+ // be empty)
13216
+ bool recurse_into_nonterminal = true;
13217
+ for (size_t i = 0; i < rule.size(); i++) {
13218
+ if (rule[i].type == LLAMA_GRETYPE_RULE_REF && recurse_into_nonterminal) {
13219
+ if (llama_grammar_detect_left_recursion(rules, (size_t)rule[i].value, rules_visited, rules_in_progress, rules_may_be_empty)) {
13220
+ return true;
13221
+ }
13222
+ if (!((*rules_may_be_empty)[(size_t)rule[i].value])) {
13223
+ recurse_into_nonterminal = false;
13224
+ }
13225
+ } else if (llama_grammar_is_end_of_sequence(&rule[i])) {
13226
+ recurse_into_nonterminal = true;
13227
+ } else {
13228
+ recurse_into_nonterminal = false;
13229
+ }
13230
+ }
13231
+
13232
+ (*rules_in_progress)[rule_index] = false;
13233
+ (*rules_visited)[rule_index] = true;
13234
+ return false;
13235
+ }
13236
+
13185
13237
//
13186
13238
// grammar - external
13187
13239
//
@@ -13201,6 +13253,19 @@ struct llama_grammar * llama_grammar_init(
13201
13253
vec_rules[i].push_back({LLAMA_GRETYPE_END, 0});
13202
13254
}
13203
13255
13256
+ // Check for left recursion
13257
+ std::vector<bool> rules_visited(n_rules);
13258
+ std::vector<bool> rules_in_progress(n_rules);
13259
+ std::vector<bool> rules_may_be_empty(n_rules);
13260
+ for (size_t i = 0; i < n_rules; i++) {
13261
+ if (rules_visited[i]) {
13262
+ continue;
13263
+ }
13264
+ if (llama_grammar_detect_left_recursion(vec_rules, i, &rules_visited, &rules_in_progress, &rules_may_be_empty)) {
13265
+ throw std::runtime_error(format("unsupported grammar, left recursion detected for nonterminal at index %zu", i));
13266
+ }
13267
+ }
13268
+
13204
13269
// loop over alternates of start rule to build initial stacks
13205
13270
std::vector<std::vector<const llama_grammar_element *>> stacks;
13206
13271
pos = vec_rules[start_rule_index].data();
@@ -13223,6 +13288,9 @@ struct llama_grammar * llama_grammar_init(
13223
13288
}
13224
13289
} while (true);
13225
13290
13291
+ // Important: vec_rules has to be moved here, not copied, because stacks contains
13292
+ // pointers to elements of vec_rules. If vec_rules were copied into llama_grammar
13293
+ // then the pointers would be invalidated when the local vec_rules goes out of scope.
13226
13294
return new llama_grammar{ std::move(vec_rules), std::move(stacks), {} };
13227
13295
}
13228
13296
0 commit comments