Coverage Report

Created: 2026-08-22 07:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/llama.cpp/common/json-schema-to-grammar.cpp
Line
Count
Source
1
#include "json-schema-to-grammar.h"
2
#include "common.h"
3
4
#include <nlohmann/json.hpp>
5
6
#include <algorithm>
7
#include <map>
8
#include <regex>
9
#include <sstream>
10
#include <string>
11
#include <unordered_map>
12
#include <unordered_set>
13
#include <vector>
14
15
using json = nlohmann::ordered_json;
16
17
23.0k
static std::string build_repetition(const std::string & item_rule, int min_items, int max_items, const std::string & separator_rule = "") {
18
23.0k
    auto has_max = max_items != std::numeric_limits<int>::max();
19
20
23.0k
    if (max_items == 0) {
21
567
        return "";
22
567
    }
23
22.4k
    if (min_items == 0 && max_items == 1) {
24
1.46k
        return item_rule + "?";
25
1.46k
    }
26
27
20.9k
    if (separator_rule.empty()) {
28
13.1k
        if (min_items == 1 && !has_max) {
29
113
            return item_rule + "+";
30
113
        }
31
13.0k
        if (min_items == 0 && !has_max) {
32
6.80k
            return item_rule + "*";
33
6.80k
        }
34
6.23k
        return item_rule + "{" + std::to_string(min_items) + "," + (has_max ? std::to_string(max_items) : "") + "}";
35
13.0k
    }
36
37
7.83k
    auto result = item_rule + " " + build_repetition("(" + separator_rule + " " + item_rule + ")", min_items == 0 ? 0 : min_items - 1, has_max ? max_items - 1 : max_items);
38
7.83k
    if (min_items == 0) {
39
7.28k
        result = "(" + result + ")?";
40
7.28k
    }
41
7.83k
    return result;
42
20.9k
}
43
44
12.1k
static void build_min_max_int(int64_t min_value, int64_t max_value, std::stringstream & out, int decimals_left = 16, bool top_level = true) {
45
12.1k
    auto has_min = min_value != std::numeric_limits<int64_t>::min();
46
12.1k
    auto has_max = max_value != std::numeric_limits<int64_t>::max();
47
48
82.9k
    auto digit_range = [&](char from, char to) {
49
82.9k
        out << "[";
50
82.9k
        if (from == to) {
51
38.3k
            out << from;
52
44.5k
        } else {
53
44.5k
            out << from << "-" << to;
54
44.5k
        }
55
82.9k
        out << "]";
56
82.9k
    };
57
47.8k
    auto more_digits = [&](int min_digits, int max_digits) {
58
47.8k
        out << "[0-9]";
59
47.8k
        if (min_digits == max_digits && min_digits == 1) {
60
5.95k
            return;
61
5.95k
        }
62
41.9k
        out << "{";
63
41.9k
        out << min_digits;
64
41.9k
        if (max_digits != min_digits) {
65
16.1k
            out << ",";
66
16.1k
            if (max_digits != std::numeric_limits<int>::max()) {
67
16.1k
                out << max_digits;
68
16.1k
            }
69
16.1k
        }
70
41.9k
        out << "}";
71
41.9k
    };
72
12.1k
    std::function<void(const std::string_view &, const std::string_view &)> uniform_range =
73
38.6k
        [&](const std::string_view & from, const std::string_view & to) {
74
38.6k
            size_t i = 0;
75
39.9k
            while (i < from.length() && i < to.length() && from[i] == to[i]) {
76
1.28k
                i++;
77
1.28k
            }
78
38.6k
            if (i > 0) {
79
1.01k
                out << "\"" << from.substr(0, i) << "\"";
80
1.01k
            }
81
38.6k
            if (i < from.length() && i < to.length()) {
82
38.1k
                if (i > 0) {
83
811
                    out << " ";
84
811
                }
85
38.1k
                auto sub_len = from.length() - i - 1;
86
38.1k
                if (sub_len > 0) {
87
31.2k
                    auto from_sub = from.substr(i + 1);
88
31.2k
                    auto to_sub = to.substr(i + 1);
89
31.2k
                    auto sub_zeros = string_repeat("0", sub_len);
90
31.2k
                    auto sub_nines = string_repeat("9", sub_len);
91
92
31.2k
                    auto to_reached = false;
93
31.2k
                    out << "(";
94
31.2k
                    if (from_sub == sub_zeros) {
95
27.8k
                        digit_range(from[i], to[i] - 1);
96
27.8k
                        out << " ";
97
27.8k
                        more_digits(sub_len, sub_len);
98
27.8k
                    } else {
99
3.43k
                        out << "[" << from[i] << "] ";
100
3.43k
                        out << "(";
101
3.43k
                        uniform_range(from_sub, sub_nines);
102
3.43k
                        out << ")";
103
3.43k
                        if (from[i] < to[i] - 1) {
104
3.29k
                            out << " | ";
105
3.29k
                            if (to_sub == sub_nines) {
106
3.01k
                                digit_range(from[i] + 1, to[i]);
107
3.01k
                                to_reached = true;
108
3.01k
                            } else {
109
285
                                digit_range(from[i] + 1, to[i] - 1);
110
285
                            }
111
3.29k
                            out << " ";
112
3.29k
                            more_digits(sub_len, sub_len);
113
3.29k
                        }
114
3.43k
                    }
115
31.2k
                    if (!to_reached) {
116
28.2k
                        out << " | ";
117
28.2k
                        digit_range(to[i], to[i]);
118
28.2k
                        out << " ";
119
28.2k
                        uniform_range(sub_zeros, to_sub);
120
28.2k
                    }
121
31.2k
                    out << ")";
122
31.2k
                } else {
123
6.89k
                    out << "[" << from[i] << "-" << to[i] << "]";
124
6.89k
                }
125
38.1k
            }
126
38.6k
        };
127
128
12.1k
    if (has_min && has_max) {
129
1.69k
        if (min_value < 0 && max_value < 0) {
130
106
            out << "\"-\" (";
131
106
            build_min_max_int(-max_value, -min_value, out, decimals_left, /* top_level= */ true);
132
106
            out << ")";
133
106
            return;
134
106
        }
135
136
1.59k
        if (min_value < 0) {
137
364
            out << "\"-\" (";
138
364
            build_min_max_int(0, -min_value, out, decimals_left, /* top_level= */ true);
139
364
            out << ") | ";
140
364
            min_value = 0;
141
364
        }
142
143
1.59k
        auto min_s = std::to_string(min_value);
144
1.59k
        auto max_s = std::to_string(max_value);
145
1.59k
        auto min_digits = min_s.length();
146
1.59k
        auto max_digits = max_s.length();
147
148
6.98k
        for (auto digits = min_digits; digits < max_digits; digits++) {
149
5.39k
            uniform_range(min_s, string_repeat("9", digits));
150
5.39k
            min_s = "1" + string_repeat("0", digits);
151
5.39k
            out << " | ";
152
5.39k
        }
153
1.59k
        uniform_range(min_s, max_s);
154
1.59k
        return;
155
1.69k
    }
156
157
10.4k
    auto less_decimals = std::max(decimals_left - 1, 1);
158
159
10.4k
    if (has_min) {
160
9.44k
        if (min_value < 0) {
161
108
            out << "\"-\" (";
162
108
            build_min_max_int(std::numeric_limits<int64_t>::min(), -min_value, out, decimals_left, /* top_level= */ false);
163
108
            out << ") | [0] | [1-9] ";
164
108
            more_digits(0, decimals_left - 1);
165
9.34k
        } else if (min_value == 0) {
166
331
            if (top_level) {
167
99
                out << "[0] | [1-9] ";
168
99
                more_digits(0, less_decimals);
169
232
            } else {
170
232
                more_digits(1, decimals_left);
171
232
            }
172
9.01k
        } else if (min_value <= 9) {
173
1.37k
            char c = '0' + min_value;
174
1.37k
            auto range_start = top_level ? '1' : '0';
175
1.37k
            if (c > range_start) {
176
1.29k
                digit_range(range_start, c - 1);
177
1.29k
                out << " ";
178
1.29k
                more_digits(1, less_decimals);
179
1.29k
                out << " | ";
180
1.29k
            }
181
1.37k
            digit_range(c, '9');
182
1.37k
            out << " ";
183
1.37k
            more_digits(0, less_decimals);
184
7.63k
        } else {
185
7.63k
            auto min_s = std::to_string(min_value);
186
7.63k
            auto len = min_s.length();
187
7.63k
            auto c = min_s[0];
188
189
7.63k
            if (c > '1') {
190
6.47k
                digit_range(top_level ? '1' : '0', c - 1);
191
6.47k
                out << " ";
192
6.47k
                more_digits(len, less_decimals);
193
6.47k
                out << " | ";
194
6.47k
            }
195
7.63k
            digit_range(c, c);
196
7.63k
            out << " (";
197
7.63k
            build_min_max_int(std::stoll(min_s.substr(1)), std::numeric_limits<int64_t>::max(), out, less_decimals, /* top_level= */ false);
198
7.63k
            out << ")";
199
7.63k
            if (c < '9') {
200
6.76k
                out << " | ";
201
6.76k
                digit_range(c + 1, '9');
202
6.76k
                out << " ";
203
6.76k
                more_digits(len - 1, less_decimals);
204
6.76k
            }
205
7.63k
        }
206
9.44k
        return;
207
9.44k
    }
208
209
1.04k
    if (has_max) {
210
1.04k
        if (max_value >= 0) {
211
505
            if (top_level) {
212
399
                out << "\"-\" [1-9] ";
213
399
                more_digits(0, less_decimals);
214
399
                out << " | ";
215
399
            }
216
505
            build_min_max_int(0, max_value, out, decimals_left, /* top_level= */ true);
217
540
        } else {
218
540
            out << "\"-\" (";
219
540
            build_min_max_int(-max_value, std::numeric_limits<int64_t>::max(), out, decimals_left, /* top_level= */ false);
220
540
            out << ")";
221
540
        }
222
1.04k
        return;
223
1.04k
    }
224
225
3
    throw std::runtime_error("At least one of min_value or max_value must be set");
226
1.04k
}
227
228
const std::string SPACE_RULE = "| \" \" | \"\\n\"{1,2} [ \\t]{0,20}";
229
230
struct BuiltinRule {
231
    std::string content;
232
    std::vector<std::string> deps;
233
};
234
235
static std::unordered_map<std::string, BuiltinRule> PRIMITIVE_RULES = {
236
    {"boolean", {"(\"true\" | \"false\")", {}}},
237
    {"decimal-part", {"[0-9]{1,16}", {}}},
238
    {"integral-part", {"[0] | [1-9] [0-9]{0,15}", {}}},
239
    {"number", {"(\"-\"? integral-part) (\".\" decimal-part)? ([eE] [-+]? integral-part)?", {"integral-part", "decimal-part"}}},
240
    {"integer", {"(\"-\"? integral-part)", {"integral-part"}}},
241
    {"value", {"object | array | string | number | boolean | null", {"object", "array", "string", "number", "boolean", "null"}}},
242
    {"object", {"\"{\" space ( string \":\" space value (\",\" space string \":\" space value)* )? space \"}\"", {"string", "value"}}},
243
    {"array", {"\"[\" space ( value (\",\" space value)* )? space \"]\"", {"value"}}},
244
    {"uuid", {"\"\\\"\" [0-9a-fA-F]{8} \"-\" [0-9a-fA-F]{4} \"-\" [0-9a-fA-F]{4} \"-\" [0-9a-fA-F]{4} \"-\" [0-9a-fA-F]{12} \"\\\"\"", {}}},
245
    {"char",   {"[^\"\\\\\\x7F\\x00-\\x1F] | [\\\\] ([\"\\\\bfnrt] | \"u\" [0-9a-fA-F]{4})", {}}},
246
    {"string", {"\"\\\"\" char* \"\\\"\"", {"char"}}},
247
    {"null", {"\"null\"", {}}},
248
};
249
250
static std::unordered_map<std::string, BuiltinRule> STRING_FORMAT_RULES = {
251
    {"date", {"[0-9]{4} \"-\" ( \"0\" [1-9] | \"1\" [0-2] ) \"-\" ( \"0\" [1-9] | [1-2] [0-9] | \"3\" [0-1] )", {}}},
252
    {"time", {"([01] [0-9] | \"2\" [0-3]) \":\" [0-5] [0-9] \":\" [0-5] [0-9] ( \".\" [0-9]{3} )? ( \"Z\" | ( \"+\" | \"-\" ) ( [01] [0-9] | \"2\" [0-3] ) \":\" [0-5] [0-9] )", {}}},
253
    {"date-time", {"date \"T\" time", {"date", "time"}}},
254
    {"date-string", {"\"\\\"\" date \"\\\"\"", {"date"}}},
255
    {"time-string", {"\"\\\"\" time \"\\\"\"", {"time"}}},
256
    {"date-time-string", {"\"\\\"\" date-time \"\\\"\"", {"date-time"}}}
257
};
258
259
669k
static bool is_reserved_name(const std::string & name) {
260
669k
    static const std::unordered_set<std::string> RESERVED_NAMES = [] {
261
1
        std::unordered_set<std::string> s;
262
1
        s.insert("root");
263
12
        for (const auto & p : PRIMITIVE_RULES) {
264
12
            s.insert(p.first);
265
12
        }
266
6
        for (const auto & p : STRING_FORMAT_RULES) {
267
6
            s.insert(p.first);
268
6
        }
269
1
        return s;
270
1
    }();
271
669k
    return RESERVED_NAMES.find(name) != RESERVED_NAMES.end();
272
669k
}
273
274
static std::regex INVALID_RULE_CHARS_RE("[^a-zA-Z0-9-]+");
275
static std::regex GRAMMAR_LITERAL_ESCAPE_RE("[\r\n\"\\\\]");
276
static std::regex GRAMMAR_RANGE_LITERAL_ESCAPE_RE("[\r\n\"\\]\\-\\\\]");
277
static std::unordered_map<char, std::string> GRAMMAR_LITERAL_ESCAPES = {
278
    {'\r', "\\r"}, {'\n', "\\n"}, {'"', "\\\""}, {'-', "\\-"}, {']', "\\]"}, {'\\', "\\\\"}
279
};
280
281
static const int MAX_PATTERN_DEPTH = 100;
282
283
static std::unordered_set<char> NON_LITERAL_SET = {'|', '.', '(', ')', '[', ']', '{', '}', '*', '+', '?', '^', '$'};
284
static std::unordered_set<char> ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = {'^', '$', '.', '[', ']', '(', ')', '|', '{', '}', '*', '+', '?'};
285
286
368k
static std::string replacePattern(const std::string & input, const std::regex & regex, const std::function<std::string(const std::smatch  &)> & replacement) {
287
368k
    std::smatch match;
288
368k
    std::string result;
289
290
368k
    std::string::const_iterator searchStart(input.cbegin());
291
368k
    std::string::const_iterator searchEnd(input.cend());
292
293
1.09M
    while (std::regex_search(searchStart, searchEnd, match, regex)) {
294
727k
        result.append(searchStart, searchStart + match.position());
295
727k
        result.append(replacement(match));
296
727k
        searchStart = match.suffix().first;
297
727k
    }
298
299
368k
    result.append(searchStart, searchEnd);
300
301
368k
    return result;
302
368k
}
303
304
368k
static std::string format_literal(const std::string & literal) {
305
727k
    std::string escaped = replacePattern(literal, GRAMMAR_LITERAL_ESCAPE_RE, [&](const std::smatch & match) {
306
727k
        char c = match.str()[0];
307
727k
        return GRAMMAR_LITERAL_ESCAPES.at(c);
308
727k
    });
309
368k
    return "\"" + escaped + "\"";
310
368k
}
311
312
0
std::string gbnf_format_literal(const std::string & literal) { return format_literal(literal); }
313
314
5.89k
static size_t gbnf_escape_length(const std::string & pattern, size_t pos) {
315
5.89k
    if (pos + 1 >= pattern.length() || pattern[pos] != '\\') {
316
60
        return 0;
317
60
    }
318
5.83k
    size_t n_hex = 0;
319
5.83k
    switch (pattern[pos + 1]) {
320
6
        case 'x': n_hex = 2; break;
321
15
        case 'u': n_hex = 4; break;
322
17
        case 'U': n_hex = 8; break;
323
5.66k
        case 't': case 'r': case 'n': case '\\': case '"': case '[': case ']':
324
5.66k
            return 2;
325
129
        default:
326
129
            return 0;
327
5.83k
    }
328
38
    if (pos + 2 + n_hex > pattern.length()) {
329
0
        return 0;
330
0
    }
331
186
    for (size_t i = pos + 2; i < pos + 2 + n_hex; i++) {
332
173
        char h = pattern[i];
333
173
        if (!((h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') || (h >= 'A' && h <= 'F'))) {
334
25
            return 0;
335
25
        }
336
173
    }
337
13
    return 2 + n_hex;
338
38
}
339
340
class common_schema_converter {
341
private:
342
    friend class common_schema_info;
343
    friend std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options);
344
    std::function<json(const std::string &)> _fetch_json;
345
    bool _dotall;
346
    std::map<std::string, std::string> _rules;
347
    std::unordered_map<std::string, json> _refs;
348
    std::unordered_set<std::string> _refs_being_resolved;
349
    std::vector<std::string> _errors;
350
    std::vector<std::string> _warnings;
351
352
774k
    std::string _add_rule(const std::string & name, const std::string & rule) {
353
774k
        std::string esc_name = regex_replace(name, INVALID_RULE_CHARS_RE, "-");
354
774k
        if (_rules.find(esc_name) == _rules.end() || _rules[esc_name] == rule) {
355
755k
            _rules[esc_name] = rule;
356
755k
            return esc_name;
357
755k
        }
358
19.3k
        int i = 0;
359
38.3k
        while (_rules.find(esc_name + std::to_string(i)) != _rules.end() && _rules[esc_name + std::to_string(i)] != rule) {
360
18.9k
            i++;
361
18.9k
        }
362
19.3k
        std::string key = esc_name + std::to_string(i);
363
19.3k
        _rules[key] = rule;
364
19.3k
        return key;
365
774k
    }
366
367
6.96k
    std::string _generate_union_rule(const std::string & name, const std::vector<json> & alt_schemas) {
368
6.96k
        std::vector<std::string> rules;
369
6.96k
        rules.reserve(alt_schemas.size());
370
109k
        for (size_t i = 0; i < alt_schemas.size(); i++) {
371
102k
            rules.push_back(visit(alt_schemas[i], name + (name.empty() ? "alternative-" : "-") + std::to_string(i)));
372
102k
        }
373
6.96k
        return string_join(rules, " | ");
374
6.96k
    }
375
376
    // thrown when the pattern is a valid regex with no grammar equivalent
377
    struct unsupported_pattern : public std::runtime_error {
378
        using std::runtime_error::runtime_error;
379
    };
380
381
    // thrown when the pattern is not a valid regex
382
    struct invalid_pattern : public std::runtime_error {
383
        using std::runtime_error::runtime_error;
384
    };
385
386
8.28k
    std::string _visit_pattern(const std::string & pattern, const std::string & name) {
387
8.28k
        auto rules_snapshot = _rules;
388
8.28k
        try {
389
8.28k
            return _pattern_to_rule(pattern, name);
390
8.28k
        } catch (const unsupported_pattern & err) {
391
            // revert rules
392
3.72k
            _rules = std::move(rules_snapshot);
393
3.72k
            _warnings.push_back("pattern " + pattern + " is not supported (" + err.what() + "), accepting any string");
394
3.72k
            return _add_rule(name, _add_primitive("string", PRIMITIVE_RULES.at("string")));
395
3.72k
        } catch (const invalid_pattern & err) {
396
2.44k
            _rules = std::move(rules_snapshot);
397
2.44k
            _errors.push_back("Invalid pattern " + pattern + ": " + err.what());
398
2.44k
            return "";
399
2.44k
        }
400
8.28k
    }
401
402
8.28k
    std::string _pattern_to_rule(const std::string & pattern, const std::string & name) {
403
8.28k
        if (pattern.length() < 2 || pattern.front() != '^' || pattern.back() != '$') {
404
2.04k
            throw unsupported_pattern("not anchored with '^' and '$'");
405
2.04k
        }
406
6.24k
        std::string sub_pattern = pattern.substr(1, pattern.length() - 2);
407
6.24k
        std::unordered_map<std::string, std::string> sub_rule_ids;
408
409
6.24k
        size_t i = 0;
410
6.24k
        size_t length = sub_pattern.length();
411
6.24k
        int paren_depth = 0;
412
413
6.24k
        using literal_or_rule = std::pair<std::string, bool>;
414
22.6k
        auto to_rule = [&](const literal_or_rule & ls) {
415
22.6k
            auto is_literal = ls.second;
416
22.6k
            auto s = ls.first;
417
22.6k
            return is_literal ? "\"" + s + "\"" : s;
418
22.6k
        };
419
11.5k
        std::function<literal_or_rule()> transform = [&]() -> literal_or_rule {
420
11.5k
            std::vector<literal_or_rule> seq;
421
422
11.5k
            auto get_dot = [&]() {
423
10.4k
                std::string rule;
424
10.4k
                if (_dotall) {
425
0
                    rule = "[\\U00000000-\\U0010FFFF]";
426
10.4k
                } else {
427
10.4k
                    rule = "[^\\x0A\\x0D]";
428
10.4k
                }
429
10.4k
                return _add_rule("dot", rule);
430
10.4k
            };
431
432
            // Joins the sequence, merging consecutive literals together.
433
11.5k
            auto join_seq = [&]() {
434
6.33k
                std::vector<literal_or_rule> ret;
435
436
6.33k
                std::string literal;
437
14.2k
                auto flush_literal = [&]() {
438
14.2k
                    if (literal.empty()) {
439
7.77k
                        return false;
440
7.77k
                    }
441
6.46k
                    ret.emplace_back(literal, true);
442
6.46k
                    literal.clear();
443
6.46k
                    return true;
444
14.2k
                };
445
446
15.8k
                for (const auto & item : seq) {
447
15.8k
                    auto is_literal = item.second;
448
15.8k
                    if (is_literal) {
449
7.89k
                        literal += item.first;
450
7.91k
                    } else {
451
7.91k
                        flush_literal();
452
7.91k
                        ret.push_back(item);
453
7.91k
                    }
454
15.8k
                }
455
6.33k
                flush_literal();
456
457
6.33k
                std::vector<std::string> results;
458
6.33k
                results.reserve(ret.size());
459
14.3k
                for (const auto & item : ret) {
460
14.3k
                    results.push_back(to_rule(item));
461
14.3k
                }
462
6.33k
                return std::make_pair(string_join(results, " "), false);
463
6.33k
            };
464
465
108k
            while (i < length) {
466
103k
                char c = sub_pattern[i];
467
103k
                if (c == '.') {
468
10.4k
                    seq.emplace_back(get_dot(), false);
469
10.4k
                    i++;
470
92.5k
                } else if (c == '(') {
471
5.30k
                    i++;
472
5.30k
                    if (i < length && sub_pattern[i] == '?') {
473
531
                        if (i + 1 < length && sub_pattern[i + 1] == ':') {
474
511
                            i += 2; // skip "?:" for non-capturing group, treat as regular group
475
511
                        } else {
476
                            // lookaround, named group, inline flags, ...
477
20
                            throw unsupported_pattern("unsupported group syntax");
478
20
                        }
479
531
                    }
480
5.28k
                    paren_depth++;
481
5.28k
                    if (paren_depth > MAX_PATTERN_DEPTH) {
482
10
                        throw unsupported_pattern("pattern nesting too deep");
483
10
                    }
484
5.27k
                    seq.emplace_back("(" + to_rule(transform()) + ")", false);
485
87.2k
                } else if (c == ')') {
486
2.37k
                    i++;
487
2.37k
                    if (paren_depth == 0) {
488
77
                        throw invalid_pattern("unbalanced parentheses");
489
77
                    }
490
2.29k
                    paren_depth--;
491
2.29k
                    return join_seq();
492
84.9k
                } else if (c == '^' || c == '$') {
493
104
                    throw unsupported_pattern("anchor inside the pattern");
494
84.8k
                } else if (c == '[') {
495
1.40k
                    std::string square_brackets = std::string(1, c);
496
1.40k
                    i++;
497
12.7k
                    while (i < length && sub_pattern[i] != ']') {
498
11.3k
                        if (sub_pattern[i] == '\\') {
499
1.45k
                            auto escape_length = gbnf_escape_length(sub_pattern, i);
500
1.45k
                            if (escape_length == 0) {
501
79
                                throw unsupported_pattern("unsupported escape in character class: " + sub_pattern.substr(i, 2));
502
79
                            }
503
1.37k
                            square_brackets += sub_pattern.substr(i, escape_length);
504
1.37k
                            i += escape_length;
505
9.94k
                        } else {
506
9.94k
                            square_brackets += sub_pattern[i];
507
9.94k
                            i++;
508
9.94k
                        }
509
11.3k
                    }
510
1.32k
                    if (i >= length) {
511
1.02k
                        throw invalid_pattern("unterminated character class");
512
1.02k
                    }
513
298
                    square_brackets += ']';
514
298
                    i++;
515
298
                    seq.emplace_back(square_brackets, false);
516
83.4k
                } else if (c == '|') {
517
27.2k
                    seq.emplace_back("|", false);
518
27.2k
                    i++;
519
56.1k
                } else if (c == '*' || c == '+' || c == '?') {
520
2.00k
                    if (seq.empty()) {
521
26
                        throw invalid_pattern("nothing to repeat");
522
26
                    }
523
1.98k
                    seq.back() = std::make_pair(to_rule(seq.back()) + c, false);
524
1.98k
                    i++;
525
54.1k
                } else if (c == '{') {
526
7.08k
                    std::string curly_brackets = std::string(1, c);
527
7.08k
                    i++;
528
2.70M
                    while (i < length && sub_pattern[i] != '}') {
529
2.69M
                        curly_brackets += sub_pattern[i];
530
2.69M
                        i++;
531
2.69M
                    }
532
7.08k
                    if (i >= length) {
533
770
                        throw unsupported_pattern("unterminated curly brackets");
534
770
                    }
535
6.31k
                    curly_brackets += '}';
536
6.31k
                    i++;
537
6.31k
                    auto nums = string_split(curly_brackets.substr(1, curly_brackets.length() - 2), ",");
538
6.31k
                    int min_times = 0;
539
6.31k
                    int max_times = std::numeric_limits<int>::max();
540
6.31k
                    if (nums.size() != 1 && nums.size() != 2) {
541
404
                        throw unsupported_pattern("wrong number of values in curly brackets");
542
404
                    }
543
5.91k
                    try {
544
5.91k
                        if (nums.size() == 1) {
545
5.34k
                            min_times = max_times = std::stoi(nums[0]);
546
5.34k
                        } else {
547
568
                            if (!nums[0].empty()) {
548
412
                                min_times = std::stoi(nums[0]);
549
412
                            }
550
568
                            if (!nums[1].empty()) {
551
401
                                max_times = std::stoi(nums[1]);
552
401
                            }
553
568
                        }
554
5.91k
                    } catch (const std::logic_error &) {
555
69
                        throw unsupported_pattern("invalid number in curly brackets");
556
69
                    }
557
5.84k
                    if (seq.empty()) {
558
0
                        throw invalid_pattern("nothing to repeat");
559
0
                    }
560
5.84k
                    auto &last = seq.back();
561
5.84k
                    auto &sub = last.first;
562
5.84k
                    auto sub_is_literal = last.second;
563
564
5.84k
                    if (!sub_is_literal) {
565
5.02k
                        std::string & sub_id = sub_rule_ids[sub];
566
5.02k
                        if (sub_id.empty()) {
567
4.08k
                            sub_id = _add_rule(name + "-" + std::to_string(sub_rule_ids.size()), sub);
568
4.08k
                        }
569
5.02k
                        sub = sub_id;
570
5.02k
                    }
571
5.84k
                    seq.back().first = build_repetition(
572
5.84k
                        sub_is_literal ? "\"" + sub + "\"" : sub,
573
5.84k
                        min_times,
574
5.84k
                        max_times,
575
5.84k
                        ""
576
5.84k
                    );
577
5.84k
                    seq.back().second = false;
578
47.1k
                } else {
579
47.1k
                    std::string literal;
580
18.1M
                    auto is_non_literal = [&](char c) {
581
18.1M
                        return NON_LITERAL_SET.find(c) != NON_LITERAL_SET.end();
582
18.1M
                    };
583
9.13M
                    while (i < length) {
584
9.13M
                        if (sub_pattern[i] == '\\') {
585
5.91k
                            if (i == length - 1) {
586
1.12k
                                throw invalid_pattern("trailing backslash");
587
1.12k
                            }
588
4.78k
                            char next = sub_pattern[i + 1];
589
4.78k
                            if (ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS.find(next) != ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS.end()) {
590
346
                                i++;
591
346
                                literal += sub_pattern[i];
592
346
                                i++;
593
4.44k
                            } else {
594
4.44k
                                auto escape_length = gbnf_escape_length(sub_pattern, i);
595
4.44k
                                if (escape_length == 0) {
596
135
                                    throw unsupported_pattern("unsupported escape: " + sub_pattern.substr(i, 2));
597
135
                                }
598
4.30k
                                literal += sub_pattern.substr(i, escape_length);
599
4.30k
                                i += escape_length;
600
4.30k
                            }
601
9.12M
                        } else if (sub_pattern[i] == '"') {
602
13
                            literal += "\\\"";
603
13
                            i++;
604
9.12M
                        } else if (!is_non_literal(sub_pattern[i]) &&
605
9.09M
                                (i == length - 1 || literal.empty() || sub_pattern[i + 1] == '.' || !is_non_literal(sub_pattern[i + 1]))) {
606
9.08M
                            literal += sub_pattern[i];
607
9.08M
                            i++;
608
9.08M
                        } else {
609
43.9k
                            break;
610
43.9k
                        }
611
9.13M
                    }
612
45.8k
                    if (literal.empty()) { // nothing was consumed, ex. a stray ']' or '}'
613
91
                        throw unsupported_pattern(std::string("unsupported character: ") + c);
614
91
                    }
615
45.7k
                    seq.emplace_back(literal, true);
616
45.7k
                }
617
103k
            }
618
5.28k
            return join_seq();
619
11.5k
        };
620
621
6.24k
        auto rule = to_rule(transform());
622
6.24k
        if (paren_depth != 0) {
623
188
            throw invalid_pattern("unbalanced parentheses");
624
188
        }
625
626
6.05k
        return _add_rule(name, "\"\\\"\" (" + rule + ") \"\\\"\"");
627
6.24k
    }
628
629
    /*
630
        Returns a rule that matches a JSON string that is none of the provided strings
631
632
        not_strings({"a"})
633
            -> ["] ( [a] char+ | [^"a] char* )? ["]
634
        not_strings({"and", "also"})
635
            -> ["] ( [a] ([l] ([s] ([o] char+ | [^"o] char*) | [^"s] char*) | [n] ([d] char+ | [^"d] char*) | [^"ln] char*) | [^"a] char* )? ["]
636
    */
637
0
    std::string _not_strings(const std::vector<std::string> & strings) {
638
639
0
        struct TrieNode {
640
0
            std::map<char, TrieNode> children;
641
0
            bool is_end_of_string;
642
643
0
            TrieNode() : is_end_of_string(false) {}
644
645
0
            void insert(const std::string & string) {
646
0
                auto *node = this;
647
0
                for (char c : string) {
648
0
                    node = &node->children[c];
649
0
                }
650
0
                node->is_end_of_string = true;
651
0
            }
652
0
        };
653
654
0
        TrieNode trie;
655
0
        for (const auto & s : strings) {
656
0
            trie.insert(s);
657
0
        }
658
659
0
        std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char"));
660
0
        std::ostringstream out;
661
0
        out << "[\"] ( ";
662
0
        std::function<void(const TrieNode &)> visit = [&](const TrieNode & node) {
663
0
            std::ostringstream rejects;
664
0
            auto first = true;
665
0
            for (const auto & kv : node.children) {
666
0
                rejects << kv.first;
667
0
                if (first) {
668
0
                    first = false;
669
0
                } else {
670
0
                    out << " | ";
671
0
                }
672
0
                out << "[" << kv.first << "]";
673
0
                if (!kv.second.children.empty()) {
674
0
                    out << " (";
675
0
                    visit(kv.second);
676
0
                    out << ")";
677
0
                } else if (kv.second.is_end_of_string) {
678
0
                    out << " " << char_rule << "+";
679
0
                }
680
0
            }
681
0
            if (!node.children.empty()) {
682
0
                if (!first) {
683
0
                    out << " | ";
684
0
                }
685
0
                out << "[^\"" << rejects.str() << "] " << char_rule << "*";
686
0
            }
687
0
        };
688
0
        visit(trie);
689
690
0
        out << " )";
691
0
        if (!trie.is_end_of_string) {
692
0
            out << "?";
693
0
        }
694
0
        out << " [\"]";
695
0
        return out.str();
696
0
    }
697
698
36.6k
    std::string _resolve_ref(const std::string & ref) {
699
36.6k
        auto it = ref.find('#');
700
36.6k
        std::string ref_fragment = it != std::string::npos ? ref.substr(it + 1) : ref;
701
36.6k
        static const std::regex nonalphanumeric_regex(R"([^a-zA-Z0-9-]+)");
702
36.6k
        std::string ref_name = "ref" + std::regex_replace(ref_fragment, nonalphanumeric_regex, "-");
703
36.6k
        if (_rules.find(ref_name) == _rules.end() && _refs_being_resolved.find(ref) == _refs_being_resolved.end()) {
704
6.47k
            _refs_being_resolved.insert(ref);
705
6.47k
            json resolved = _refs[ref];
706
6.47k
            ref_name = visit(resolved, ref_name);
707
6.47k
            _refs_being_resolved.erase(ref);
708
6.47k
        }
709
36.6k
        return ref_name;
710
36.6k
    }
711
712
    std::string _build_object_rule(
713
        const std::vector<std::pair<std::string, json>> & properties,
714
        const std::unordered_set<std::string> & required,
715
        const std::string & name,
716
        const json & additional_properties)
717
4.48k
    {
718
4.48k
        std::vector<std::string> required_props;
719
4.48k
        std::vector<std::string> optional_props;
720
4.48k
        std::unordered_map<std::string, std::string> prop_kv_rule_names;
721
4.48k
        std::vector<std::string> prop_names;
722
354k
        for (const auto & kv : properties) {
723
354k
            const auto &prop_name = kv.first;
724
354k
            const auto &prop_schema = kv.second;
725
726
354k
            std::string prop_rule_name = visit(prop_schema, name + (name.empty() ? "" : "-") + prop_name);
727
354k
            prop_kv_rule_names[prop_name] = _add_rule(
728
354k
                name + (name.empty() ? "" : "-") + prop_name + "-kv",
729
354k
                format_literal(json(prop_name).dump()) + " space \":\" space " + prop_rule_name
730
354k
            );
731
354k
            if (required.find(prop_name) != required.end()) {
732
341k
                required_props.push_back(prop_name);
733
341k
            } else {
734
12.5k
                optional_props.push_back(prop_name);
735
12.5k
            }
736
354k
            prop_names.push_back(prop_name);
737
354k
        }
738
4.48k
        if ((additional_properties.is_boolean() && additional_properties.get<bool>()) || additional_properties.is_object()) {
739
322
            std::string sub_name = name + (name.empty() ? "" : "-") + "additional";
740
322
            std::string value_rule =
741
322
                additional_properties.is_object() ? visit(additional_properties, sub_name + "-value")
742
322
                : _add_primitive("value", PRIMITIVE_RULES.at("value"));
743
744
322
            auto key_rule =
745
322
                prop_names.empty() ? _add_primitive("string", PRIMITIVE_RULES.at("string"))
746
322
                : _add_rule(sub_name + "-k", _not_strings(prop_names));
747
322
            std::string kv_rule = _add_rule(sub_name + "-kv", key_rule + " \":\" space " + value_rule);
748
322
            prop_kv_rule_names["*"] = kv_rule;
749
322
            optional_props.push_back("*");
750
322
        }
751
752
4.48k
        std::string rule = "\"{\" space ";
753
346k
        for (size_t i = 0; i < required_props.size(); i++) {
754
341k
            if (i > 0) {
755
340k
                rule += " \",\" space ";
756
340k
            }
757
341k
            rule += prop_kv_rule_names[required_props[i]];
758
341k
        }
759
760
4.48k
        if (!optional_props.empty()) {
761
2.89k
            rule += " (";
762
2.89k
            if (!required_props.empty()) {
763
12
                rule += " \",\" space ( ";
764
12
            }
765
766
299k
            std::function<std::string(const std::vector<std::string> &, bool)> get_recursive_refs = [&](const std::vector<std::string> & ks, bool first_is_optional) {
767
299k
                std::string res;
768
299k
                if (ks.empty()) {
769
0
                    return res;
770
0
                }
771
299k
                const std::string& k = ks[0];
772
299k
                std::string kv_rule_name = prop_kv_rule_names[k];
773
299k
                std::string comma_ref = "( \",\" space " + kv_rule_name + " )";
774
299k
                if (first_is_optional) {
775
287k
                    res = comma_ref + (k == "*" ? "*" : "?");
776
287k
                } else {
777
11.8k
                    res = kv_rule_name + (k == "*" ? " " + comma_ref + "*" : "");
778
11.8k
                }
779
299k
                if (ks.size() > 1) {
780
287k
                    res += " " + _add_rule(
781
287k
                        name + (name.empty() ? "" : "-") + k + "-rest",
782
287k
                        get_recursive_refs(std::vector<std::string>(ks.begin() + 1, ks.end()), true)
783
287k
                    );
784
287k
                }
785
299k
                return res;
786
299k
            };
787
788
14.7k
            for (size_t i = 0; i < optional_props.size(); i++) {
789
11.8k
                if (i > 0) {
790
8.91k
                    rule += " | ";
791
8.91k
                }
792
11.8k
                rule += get_recursive_refs(std::vector<std::string>(optional_props.begin() + i, optional_props.end()), false);
793
11.8k
            }
794
2.89k
            if (!required_props.empty()) {
795
12
                rule += " )";
796
12
            }
797
2.89k
            rule += " )?";
798
2.89k
        }
799
800
4.48k
        rule += " space \"}\"";
801
802
4.48k
        return rule;
803
4.48k
    }
804
805
31.4k
    std::string _add_primitive(const std::string & name, const BuiltinRule & rule) {
806
31.4k
        auto n = _add_rule(name, rule.content);
807
76.5k
        for (const auto & dep : rule.deps) {
808
76.5k
            BuiltinRule dep_rule;
809
76.5k
            auto it = PRIMITIVE_RULES.find(dep);
810
76.5k
            if (it == PRIMITIVE_RULES.end()) {
811
0
                it = STRING_FORMAT_RULES.find(dep);
812
0
                if (it == STRING_FORMAT_RULES.end()) {
813
0
                    _errors.push_back("Rule " + dep + " not known");
814
0
                    continue;
815
0
                }
816
0
            }
817
76.5k
            if (_rules.find(dep) == _rules.end()) {
818
12.4k
                _add_primitive(dep, it->second);
819
12.4k
            }
820
76.5k
        }
821
31.4k
        return n;
822
31.4k
    }
823
824
public:
825
    common_schema_converter(
826
        const std::function<json(const std::string &)> & fetch_json,
827
        bool dotall)
828
4.95k
          : _fetch_json(fetch_json), _dotall(dotall)
829
4.95k
    {
830
4.95k
        _rules["space"] = SPACE_RULE;
831
4.95k
    }
832
833
6.57k
    void resolve_refs(json & schema, const std::string & url) {
834
        /*
835
        * Resolves all $ref fields in the given schema, fetching any remote schemas,
836
        * replacing each $ref with absolute reference URL and populates _refs with the
837
        * respective referenced (sub)schema dictionaries.
838
        */
839
4.21M
        std::function<void(json &)> visit_refs = [&](json & n) {
840
4.21M
            if (n.is_array()) {
841
4.16M
                for (auto & x : n) {
842
4.16M
                    visit_refs(x);
843
4.16M
                }
844
4.20M
            } else if (n.is_object()) {
845
48.2k
                if (n.contains("$ref")) {
846
20.2k
                    std::string ref = n["$ref"];
847
20.2k
                    if (_refs.find(ref) == _refs.end()) {
848
14.9k
                        json target;
849
14.9k
                        if (ref.find("https://") == 0) {
850
3.75k
                            std::string base_url = ref.substr(0, ref.find('#'));
851
3.75k
                            auto it = _refs.find(base_url);
852
3.75k
                            if (it != _refs.end()) {
853
2.13k
                                target = it->second;
854
2.13k
                            } else {
855
                                // Fetch the referenced schema and resolve its refs
856
1.61k
                                auto referenced = _fetch_json(ref);
857
1.61k
                                resolve_refs(referenced, base_url);
858
1.61k
                                _refs[base_url] = referenced;
859
1.61k
                            }
860
3.75k
                            if (ref.find('#') == std::string::npos || ref.substr(ref.find('#') + 1).empty()) {
861
2.57k
                                return;
862
2.57k
                            }
863
11.1k
                        } else if (ref.find("#/") == 0) {
864
5.71k
                            target = schema;
865
5.71k
                            n["$ref"] = url + ref;
866
5.71k
                            ref = url + ref;
867
5.71k
                        } else {
868
5.44k
                            _errors.push_back("Unsupported ref: " + ref);
869
5.44k
                            return;
870
5.44k
                        }
871
6.89k
                        std::string pointer = ref.substr(ref.find('#') + 1);
872
6.89k
                        std::vector<std::string> tokens = string_split(pointer, "/");
873
12.1k
                        for (size_t i = 1; i < tokens.size(); ++i) {
874
10.1k
                            const std::string& sel = tokens[i];
875
10.1k
                            if (target.is_object() && target.contains(sel)) {
876
3.21k
                                target = target[sel];
877
6.88k
                            } else if (target.is_array()) {
878
3.56k
                                size_t sel_index;
879
3.56k
                                try {
880
3.56k
                                    sel_index = std::stoull(sel);
881
3.56k
                                } catch (const std::invalid_argument & e) {
882
930
                                    sel_index = target.size();
883
930
                                }
884
3.56k
                                if (sel_index >= target.size()) {
885
1.52k
                                    _errors.push_back("Error resolving ref " + ref + ": " + sel + " not in " + target.dump());
886
1.52k
                                    return;
887
1.52k
                                }
888
2.03k
                                target = target[sel_index];
889
3.32k
                            } else {
890
3.32k
                                _errors.push_back("Error resolving ref " + ref + ": " + sel + " not in " + target.dump());
891
3.32k
                                return;
892
3.32k
                            }
893
10.1k
                        }
894
2.04k
                        _refs[ref] = target;
895
2.04k
                    }
896
27.9k
                } else {
897
41.5k
                    for (const auto & kv : n.items()) {
898
41.5k
                        visit_refs(kv.value());
899
41.5k
                    }
900
27.9k
                }
901
48.2k
            }
902
4.21M
        };
903
904
6.57k
        visit_refs(schema);
905
6.57k
    }
906
907
14.2k
    static std::string _generate_constant_rule(const json & value) {
908
14.2k
        return format_literal(value.dump());
909
14.2k
    }
910
911
669k
    std::string visit(const json & schema, const std::string & name) {
912
669k
        json schema_type = schema.contains("type") ? schema["type"] : json();
913
669k
        std::string schema_format = schema.contains("format") ? schema["format"].get<std::string>() : "";
914
669k
        std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name;
915
916
669k
        if (schema.contains("$ref")) {
917
36.6k
            return _add_rule(rule_name, _resolve_ref(schema["$ref"]));
918
36.6k
        }
919
632k
        if (schema.contains("oneOf") || schema.contains("anyOf")) {
920
648
            std::vector<json> alt_schemas = schema.contains("oneOf") ? schema["oneOf"].get<std::vector<json>>() : schema["anyOf"].get<std::vector<json>>();
921
648
            return _add_rule(rule_name, _generate_union_rule(name, alt_schemas));
922
648
        }
923
631k
        if (schema_type.is_array()) {
924
6.34k
            std::vector<json> schema_types;
925
66.5k
            for (const auto & t : schema_type) {
926
66.5k
                json schema_copy(schema);
927
66.5k
                schema_copy["type"] = t;
928
66.5k
                schema_types.push_back(schema_copy);
929
66.5k
            }
930
6.34k
            return _add_rule(rule_name, _generate_union_rule(name, schema_types));
931
6.34k
        }
932
625k
        if (schema.contains("const")) {
933
2.29k
            return _add_rule(rule_name, _generate_constant_rule(schema["const"]));
934
2.29k
        }
935
623k
        if (schema.contains("enum")) {
936
2.58k
            std::vector<std::string> enum_values;
937
5.08k
            for (const auto & v : schema["enum"]) {
938
5.08k
                enum_values.push_back(_generate_constant_rule(v));
939
5.08k
            }
940
2.58k
            return _add_rule(rule_name, "(" + string_join(enum_values, " | ") + ")");
941
2.58k
        }
942
620k
        if ((schema_type.is_null() || schema_type == "object")
943
571k
                && (schema.contains("properties") ||
944
569k
                    (schema.contains("additionalProperties") && schema["additionalProperties"] != true))) {
945
3.29k
            std::unordered_set<std::string> required;
946
3.29k
            if (schema.contains("required") && schema["required"].is_array()) {
947
11.2k
                for (const auto & item : schema["required"]) {
948
11.2k
                    if (item.is_string()) {
949
5.69k
                        required.insert(item.get<std::string>());
950
5.69k
                    }
951
11.2k
                }
952
798
            }
953
3.29k
            std::vector<std::pair<std::string, json>> properties;
954
3.29k
            if (schema.contains("properties")) {
955
12.2k
                for (const auto & prop : schema["properties"].items()) {
956
12.2k
                    properties.emplace_back(prop.key(), prop.value());
957
12.2k
                }
958
2.71k
            }
959
3.29k
            return _add_rule(rule_name,
960
3.29k
                _build_object_rule(
961
3.29k
                    properties, required, name,
962
3.29k
                    schema.contains("additionalProperties") ? schema["additionalProperties"] : json()));
963
3.29k
        }
964
617k
        if ((schema_type.is_null() || schema_type == "object" || schema_type == "string") && schema.contains("allOf")) {
965
1.21k
            std::unordered_set<std::string> required;
966
1.21k
            std::vector<std::pair<std::string, json>> properties;
967
1.21k
            std::map<std::string, size_t> enum_values;
968
1.21k
            const std::string& hybrid_name = name;
969
26.3k
            std::function<void(const json &, bool)> add_component = [&](const json & comp_schema, bool is_required) {
970
26.3k
                if (comp_schema.contains("$ref")) {
971
3.83k
                    add_component(_refs[comp_schema["$ref"]], is_required);
972
22.4k
                } else if (comp_schema.contains("properties")) {
973
342k
                    for (const auto & prop : comp_schema["properties"].items()) {
974
342k
                        properties.emplace_back(prop.key(), prop.value());
975
342k
                        if (is_required) {
976
341k
                            required.insert(prop.key());
977
341k
                        }
978
342k
                    }
979
18.6k
                } else if (comp_schema.contains("enum")) {
980
6.85k
                    for (const auto & v : comp_schema["enum"]) {
981
6.85k
                        const auto rule = _generate_constant_rule(v);
982
6.85k
                        if (enum_values.find(rule) == enum_values.end()) {
983
1.57k
                            enum_values[rule] = 0;
984
1.57k
                        }
985
6.85k
                        enum_values[rule] += 1;
986
6.85k
                    }
987
17.2k
                } else {
988
                  // todo warning
989
17.2k
                }
990
26.3k
            };
991
21.9k
            for (const auto & t : schema["allOf"]) {
992
21.9k
                if (t.contains("anyOf")) {
993
822
                    for (const auto & tt : t["anyOf"]) {
994
822
                        add_component(tt, false);
995
822
                    }
996
21.6k
                } else {
997
21.6k
                    add_component(t, true);
998
21.6k
                }
999
21.9k
            }
1000
1.21k
            if (!enum_values.empty()) {
1001
119
                std::vector<std::string> enum_intersection;
1002
1.57k
                for (const auto & p : enum_values) {
1003
1.57k
                    if (p.second == schema["allOf"].size()) {
1004
73
                        enum_intersection.push_back(p.first);
1005
73
                    }
1006
1.57k
                }
1007
119
                if (!enum_intersection.empty()) {
1008
25
                    return _add_rule(rule_name, "(" + string_join(enum_intersection, " | ") + ")");
1009
25
                }
1010
119
            }
1011
1.18k
            return _add_rule(rule_name, _build_object_rule(properties, required, hybrid_name, json()));
1012
1.21k
        }
1013
616k
        if ((schema_type.is_null() || schema_type == "array") && (schema.contains("items") || schema.contains("prefixItems"))) {
1014
11.6k
            json items = schema.contains("items") ? schema["items"] : schema["prefixItems"];
1015
11.6k
            if (items.is_array()) {
1016
2.26k
                std::string rule = "\"[\" space ";
1017
193k
                for (size_t i = 0; i < items.size(); i++) {
1018
191k
                    if (i > 0) {
1019
189k
                        rule += " \",\" space ";
1020
189k
                    }
1021
191k
                    rule += visit(items[i], name + (name.empty() ? "" : "-") + "tuple-" + std::to_string(i));
1022
191k
                }
1023
2.26k
                rule += " space \"]\"";
1024
2.26k
                return _add_rule(rule_name, rule);
1025
2.26k
            }
1026
9.41k
            std::string item_rule_name = visit(items, name + (name.empty() ? "" : "-") + "item");
1027
9.41k
            int min_items = schema.contains("minItems") ? schema["minItems"].get<int>() : 0;
1028
9.41k
            json max_items_json = schema.contains("maxItems") ? schema["maxItems"] : json();
1029
9.41k
            int max_items = max_items_json.is_number_integer() ? max_items_json.get<int>() : std::numeric_limits<int>::max();
1030
1031
9.41k
            return _add_rule(rule_name, "\"[\" space " + build_repetition(item_rule_name, min_items, max_items, "\",\" space") + " space \"]\"");
1032
11.6k
        }
1033
604k
        if ((schema_type.is_null() || schema_type == "string") && schema.contains("pattern")) {
1034
8.29k
            return _visit_pattern(schema["pattern"], rule_name);
1035
8.29k
        }
1036
596k
        if ((schema_type.is_null() || schema_type == "string") && std::regex_match(schema_format, std::regex("^uuid[1-5]?$"))) {
1037
1
            return _add_primitive(rule_name == "root" ? "root" : schema_format, PRIMITIVE_RULES.at("uuid"));
1038
1
        }
1039
596k
        if ((schema_type.is_null() || schema_type == "string") && STRING_FORMAT_RULES.find(schema_format + "-string") != STRING_FORMAT_RULES.end()) {
1040
0
            auto prim_name = schema_format + "-string";
1041
0
            return _add_rule(rule_name, _add_primitive(prim_name, STRING_FORMAT_RULES.at(prim_name)));
1042
0
        }
1043
596k
        if (schema_type == "string" && (schema.contains("minLength") || schema.contains("maxLength"))) {
1044
166
            std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char"));
1045
166
            int min_len = schema.contains("minLength") ? schema["minLength"].get<int>() : 0;
1046
166
            int max_len = schema.contains("maxLength") ? schema["maxLength"].get<int>() : std::numeric_limits<int>::max();
1047
166
            return _add_rule(rule_name, "\"\\\"\" " + build_repetition(char_rule, min_len, max_len) + " \"\\\"\"");
1048
166
        }
1049
595k
        if (schema_type == "integer" && (schema.contains("minimum") || schema.contains("exclusiveMinimum") || schema.contains("maximum") || schema.contains("exclusiveMaximum"))) {
1050
2.93k
            int64_t min_value = std::numeric_limits<int64_t>::min();
1051
2.93k
            int64_t max_value = std::numeric_limits<int64_t>::max();
1052
2.93k
            if (schema.contains("minimum")) {
1053
2.02k
                min_value = schema["minimum"].get<int64_t>();
1054
2.02k
            } else if (schema.contains("exclusiveMinimum")) {
1055
24
                min_value = schema["exclusiveMinimum"].get<int64_t>() + 1;
1056
24
            }
1057
2.93k
            if (schema.contains("maximum")) {
1058
1.66k
                max_value = schema["maximum"].get<int64_t>();
1059
1.66k
            } else if (schema.contains("exclusiveMaximum")) {
1060
2
                max_value = schema["exclusiveMaximum"].get<int64_t>() - 1;
1061
2
            }
1062
2.93k
            std::stringstream out;
1063
2.93k
            out << "(";
1064
2.93k
            build_min_max_int(min_value, max_value, out);
1065
2.93k
            out << ")";
1066
2.93k
            return _add_rule(rule_name, out.str());
1067
2.93k
        }
1068
592k
        if (schema.empty() || schema_type == "object") {
1069
5.48k
            return _add_rule(rule_name, _add_primitive("object", PRIMITIVE_RULES.at("object")));
1070
5.48k
        }
1071
587k
        if (schema_type.is_null() && schema.is_object()) {
1072
            // No type constraint and no recognized structural keywords (e.g. {"description": "..."}).
1073
            // Per JSON Schema semantics this is equivalent to {} and accepts any value.
1074
8.11k
            return _add_rule(rule_name, _add_primitive("value", PRIMITIVE_RULES.at("value")));
1075
8.11k
        }
1076
579k
        if (!schema_type.is_string() || PRIMITIVE_RULES.find(schema_type.get<std::string>()) == PRIMITIVE_RULES.end()) {
1077
578k
            _errors.push_back("Unrecognized schema: " + schema.dump());
1078
578k
            return "";
1079
578k
        }
1080
        // TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero
1081
1.23k
        return _add_primitive(rule_name == "root" ? "root" : schema_type.get<std::string>(), PRIMITIVE_RULES.at(schema_type.get<std::string>()));
1082
579k
    }
1083
1084
4.89k
    void check_errors() {
1085
4.89k
        if (!_errors.empty()) {
1086
3.84k
            throw std::invalid_argument("JSON schema conversion failed:\n" + string_join(_errors, "\n"));
1087
3.84k
        }
1088
1.04k
        if (!_warnings.empty()) {
1089
281
            fprintf(stderr, "WARNING: JSON schema conversion was incomplete: %s\n", string_join(_warnings, "; ").c_str());
1090
281
        }
1091
1.04k
    }
1092
1093
1.04k
    std::string format_grammar() {
1094
1.04k
        std::stringstream ss;
1095
14.8k
        for (const auto & kv : _rules) {
1096
14.8k
            ss << kv.first << " ::= " << kv.second << '\n';
1097
14.8k
        }
1098
1.04k
        return ss.str();
1099
1.04k
    }
1100
};
1101
1102
// common_schema_info implementation (pimpl)
1103
1104
common_schema_info::common_schema_info()
1105
0
    : impl_(std::make_unique<common_schema_converter>(
1106
0
        [](const std::string &) { return json(); },
1107
0
        false)) {}
1108
1109
0
common_schema_info::~common_schema_info() = default;
1110
1111
0
common_schema_info::common_schema_info(common_schema_info &&) noexcept = default;
1112
0
common_schema_info & common_schema_info::operator=(common_schema_info &&) noexcept = default;
1113
1114
0
void common_schema_info::resolve_refs(nlohmann::ordered_json & schema) {
1115
0
    impl_->resolve_refs(schema, "");
1116
0
}
1117
1118
// Determines if a JSON schema can resolve to a string type through any path.
1119
// Some models emit raw string values rather than JSON-encoded strings for string parameters.
1120
// If any branch of the schema (via oneOf, anyOf, $ref, etc.) permits a string, this returns
1121
// true, allowing callers to handle the value as a raw string for simplicity.
1122
0
bool common_schema_info::resolves_to_string(const nlohmann::ordered_json & schema) {
1123
0
    std::unordered_set<std::string> visited_refs;
1124
1125
0
    std::function<bool(const json &)> check = [&](const json & s) -> bool {
1126
0
        if (!s.is_object()) {
1127
0
            return false;
1128
0
        }
1129
1130
        // Handle $ref
1131
0
        if (s.contains("$ref")) {
1132
0
            const std::string & ref = s["$ref"];
1133
0
            if (visited_refs.find(ref) != visited_refs.end()) {
1134
                // Circular reference, assume not a string to be safe
1135
0
                return false;
1136
0
            }
1137
0
            visited_refs.insert(ref);
1138
0
            auto it = impl_->_refs.find(ref);
1139
0
            if (it != impl_->_refs.end()) {
1140
0
                return check(it->second);
1141
0
            }
1142
0
            return false;
1143
0
        }
1144
1145
        // Check type field
1146
0
        if (s.contains("type")) {
1147
0
            const json & schema_type = s["type"];
1148
0
            if (schema_type.is_string()) {
1149
0
                if (schema_type == "string") {
1150
0
                    return true;
1151
0
                }
1152
0
            } else if (schema_type.is_array()) {
1153
                // Type can be an array like ["string", "null"]
1154
0
                for (const auto & t : schema_type) {
1155
0
                    if (t == "string") {
1156
0
                        return true;
1157
0
                    }
1158
0
                }
1159
0
            }
1160
0
        }
1161
1162
        // Check oneOf/anyOf - if any alternative can be a string
1163
0
        if (s.contains("oneOf")) {
1164
0
            for (const auto & alt : s["oneOf"]) {
1165
0
                if (check(alt)) {
1166
0
                    return true;
1167
0
                }
1168
0
            }
1169
0
        }
1170
0
        if (s.contains("anyOf")) {
1171
0
            for (const auto & alt : s["anyOf"]) {
1172
0
                if (check(alt)) {
1173
0
                    return true;
1174
0
                }
1175
0
            }
1176
0
        }
1177
1178
        // Check allOf - all components must be compatible with string type
1179
0
        if (s.contains("allOf")) {
1180
0
            bool all_string = true;
1181
0
            for (const auto & component : s["allOf"]) {
1182
0
                if (!check(component)) {
1183
0
                    all_string = false;
1184
0
                    break;
1185
0
                }
1186
0
            }
1187
0
            if (all_string) {
1188
0
                return true;
1189
0
            }
1190
0
        }
1191
1192
        // Check const - if the constant value is a string
1193
0
        if (s.contains("const")) {
1194
0
            if (s["const"].is_string()) {
1195
0
                return true;
1196
0
            }
1197
0
        }
1198
1199
        // Check enum - if any enum value is a string
1200
0
        if (s.contains("enum")) {
1201
0
            for (const auto & val : s["enum"]) {
1202
0
                if (val.is_string()) {
1203
0
                    return true;
1204
0
                }
1205
0
            }
1206
0
        }
1207
1208
        // String-specific keywords imply string type
1209
0
        if (s.contains("pattern") || s.contains("minLength") || s.contains("maxLength")) {
1210
0
            return true;
1211
0
        }
1212
1213
        // Check format - many formats imply string
1214
0
        if (s.contains("format")) {
1215
0
            const std::string & fmt = s["format"];
1216
0
            if (fmt == "date" || fmt == "time" || fmt == "date-time" ||
1217
0
                fmt == "uri" || fmt == "email" || fmt == "hostname" ||
1218
0
                fmt == "ipv4" || fmt == "ipv6" || fmt == "uuid" ||
1219
0
                fmt.find("uuid") == 0) {
1220
0
                return true;
1221
0
            }
1222
0
        }
1223
1224
0
        return false;
1225
0
    };
1226
1227
0
    return check(schema);
1228
0
}
1229
1230
4.95k
std::string json_schema_to_grammar(const json & schema, bool force_gbnf) {
1231
#ifdef LLAMA_USE_LLGUIDANCE
1232
    if (!force_gbnf) {
1233
        return "%llguidance {}\nstart: %json " + schema.dump();
1234
    }
1235
#else
1236
4.95k
    (void)force_gbnf;
1237
4.95k
#endif // LLAMA_USE_LLGUIDANCE
1238
4.95k
    return build_grammar([&](const common_grammar_builder & callbacks) {
1239
4.95k
        auto copy = schema;
1240
4.95k
        callbacks.resolve_refs(copy);
1241
4.95k
        callbacks.add_schema("", copy);
1242
4.95k
    });
1243
4.95k
}
1244
1245
4.95k
std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options) {
1246
4.95k
    common_schema_converter converter([&](const std::string &) { return json(); }, options.dotall);
1247
4.95k
    common_grammar_builder builder {
1248
4.95k
        /* .add_rule = */ [&](const std::string & name, const std::string & rule) {
1249
0
            return converter._add_rule(name, rule);
1250
0
        },
1251
4.95k
        /* .add_schema = */ [&](const std::string & name, const nlohmann::ordered_json & schema) {
1252
4.94k
            return converter.visit(schema, name == "root" ? "" : name);
1253
4.94k
        },
1254
4.95k
        /* .resolve_refs = */ [&](nlohmann::ordered_json & schema) {
1255
4.95k
            converter.resolve_refs(schema, "");
1256
4.95k
        }
1257
4.95k
    };
1258
4.95k
    cb(builder);
1259
4.95k
    converter.check_errors();
1260
4.95k
    return converter.format_grammar();
1261
4.95k
}