char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aldehir Rojascommon/peg : implement ac parser for stricter grammar generation (#24869)52b3df002

char/aliceai-t5-moe
19.0 KiB517 linesraw
1#include "tests.h"
2
3#include "json-schema-to-grammar.h"
4
5#include <regex>
6
7static std::string trim_leading_space(const std::string & s) {
8    static const std::regex leading_ws_re = std::regex(R"((^|\n)\s+)");
9    return std::regex_replace(s, leading_ws_re, "$1");
10}
11
12static void assert_gbnf_equal(testing & t, const std::string & expected, const std::string & actual) {
13    t.assert_equal("gbnf are equal", trim_leading_space(expected), trim_leading_space(actual));
14}
15
16void test_gbnf_generation(testing &t) {
17    t.test("literal grammar generation", [](testing &t) {
18        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
19            return p.literal("hello");
20        });
21
22        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
23            parser.build_grammar(builder);
24        });
25
26        assert_gbnf_equal(t, R"""(
27            root ::= "hello"
28            space ::= | " " | "\n"{1,2} [ \t]{0,20}
29        )""", gbnf);
30    });
31
32    t.test("char class grammar", [](testing &t) {
33        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
34            return p.chars("[a-z]", 1, 1);
35        });
36
37        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
38            parser.build_grammar(builder);
39        });
40
41        assert_gbnf_equal(t, R"""(
42            root ::= [a-z]
43            space ::= | " " | "\n"{1,2} [ \t]{0,20}
44        )""", gbnf);
45    });
46
47    t.test("sequence grammar", [](testing &t) {
48        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
49            return p.literal("hello") + p.literal(" ") + p.literal("world");
50        });
51
52        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
53            parser.build_grammar(builder);
54        });
55
56        assert_gbnf_equal(t, R"""(
57            root ::= "hello" " " "world"
58            space ::= | " " | "\n"{1,2} [ \t]{0,20}
59        )""", gbnf);
60    });
61
62    t.test("choice grammar", [](testing &t) {
63        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
64            return p.literal("cat") | p.literal("dog");
65        });
66
67        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
68            parser.build_grammar(builder);
69        });
70
71        assert_gbnf_equal(t, R"""(
72            root ::= "cat" | "dog"
73            space ::= | " " | "\n"{1,2} [ \t]{0,20}
74        )""", gbnf);
75    });
76
77    t.test("one_or_more grammar", [](testing &t) {
78        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
79            return p.one_or_more(p.literal("a"));
80        });
81
82        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
83            parser.build_grammar(builder);
84        });
85
86        assert_gbnf_equal(t, R"""(
87            root ::= "a"+
88            space ::= | " " | "\n"{1,2} [ \t]{0,20}
89        )""", gbnf);
90    });
91
92    t.test("zero_or_more grammar", [](testing &t) {
93        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
94            return p.zero_or_more(p.literal("a"));
95        });
96
97        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
98            parser.build_grammar(builder);
99        });
100
101        assert_gbnf_equal(t, R"""(
102            root ::= "a"*
103            space ::= | " " | "\n"{1,2} [ \t]{0,20}
104        )""", gbnf);
105    });
106
107    t.test("optional grammar", [](testing &t) {
108        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
109            return p.literal("hello") + p.optional(p.literal(" world"));
110        });
111
112        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
113            parser.build_grammar(builder);
114        });
115
116        assert_gbnf_equal(t, R"""(
117            root ::= "hello" " world"?
118            space ::= | " " | "\n"{1,2} [ \t]{0,20}
119        )""", gbnf);
120    });
121
122    t.test("until grammar", [](testing &t) {
123        auto parser = build_peg_parser([](common_peg_parser_builder & p)  {
124            return p.until("</tag>");
125        });
126
127        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
128            parser.build_grammar(builder);
129        });
130
131        assert_gbnf_equal(t, R"""(
132            root ::= until-0
133            space ::= | " " | "\n"{1,2} [ \t]{0,20}
134            until-0 ::= | [<] until-0-01 | [^<] until-0
135            until-0-01 ::= | [<] until-0-01 | [/] until-0-02 | [^/<] until-0
136            until-0-02 ::= | [<] until-0-01 | [t] until-0-03 | [^<t] until-0
137            until-0-03 ::= | [<] until-0-01 | [a] until-0-04 | [^<a] until-0
138            until-0-04 ::= | [<] until-0-01 | [g] until-0-05 | [^<g] until-0
139            until-0-05 ::= | [<] until-0-01 | [^<>] until-0
140        )""", gbnf);
141    });
142
143    t.test("until grammar overlapping delimiter", [](testing &t) {
144        auto parser = build_peg_parser([](common_peg_parser_builder & p)  {
145            return p.until("\n</parameter>\n");
146        });
147
148        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
149            parser.build_grammar(builder);
150        });
151
152        assert_gbnf_equal(t, R"""(
153            root ::= until-0
154            space ::= | " " | "\n"{1,2} [ \t]{0,20}
155            until-0 ::= | [\n] until-0-01 | [^\n] until-0
156            until-0-01 ::= | [\n] until-0-01 | [<] until-0-02 | [^\n<] until-0
157            until-0-02 ::= | [\n] until-0-01 | [/] until-0-03 | [^\n/] until-0
158            until-0-03 ::= | [\n] until-0-01 | [p] until-0-04 | [^\np] until-0
159            until-0-04 ::= | [\n] until-0-01 | [a] until-0-05 | [^\na] until-0
160            until-0-05 ::= | [\n] until-0-01 | [r] until-0-06 | [^\nr] until-0
161            until-0-06 ::= | [\n] until-0-01 | [a] until-0-07 | [^\na] until-0
162            until-0-07 ::= | [\n] until-0-01 | [m] until-0-08 | [^\nm] until-0
163            until-0-08 ::= | [\n] until-0-01 | [e] until-0-09 | [^\ne] until-0
164            until-0-09 ::= | [\n] until-0-01 | [t] until-0-10 | [^\nt] until-0
165            until-0-10 ::= | [\n] until-0-01 | [e] until-0-11 | [^\ne] until-0
166            until-0-11 ::= | [\n] until-0-01 | [r] until-0-12 | [^\nr] until-0
167            until-0-12 ::= | [\n] until-0-01 | [>] until-0-13 | [^\n>] until-0
168            until-0-13 ::= | [^\n] until-0
169        )""", gbnf);
170    });
171
172    // DeepSeek-V3.2 tag prefix. The DSML token (|DSML|) embeds U+FF5C,
173    // so the delimiter mixes ASCII and multi-byte codepoints.
174    t.test("until grammar unicode delimiter", [](testing &t) {
175        auto parser = build_peg_parser([](common_peg_parser_builder & p)  {
176            return p.until("<|DSML|");
177        });
178
179        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
180            parser.build_grammar(builder);
181        });
182
183        assert_gbnf_equal(t, R"""(
184            root ::= until-0
185            space ::= | " " | "\n"{1,2} [ \t]{0,20}
186            until-0 ::= | [<] until-0-01 | [^<] until-0
187            until-0-01 ::= | [<] until-0-01 | [\uFF5C] until-0-02 | [^<\uFF5C] until-0
188            until-0-02 ::= | [<] until-0-01 | [D] until-0-03 | [^<D] until-0
189            until-0-03 ::= | [<] until-0-01 | [S] until-0-04 | [^<S] until-0
190            until-0-04 ::= | [<] until-0-01 | [M] until-0-05 | [^<M] until-0
191            until-0-05 ::= | [<] until-0-01 | [L] until-0-06 | [^<L] until-0
192            until-0-06 ::= | [<] until-0-01 | [^<\uFF5C] until-0
193        )""", gbnf);
194    });
195
196    t.test("until grammar multiple delimiters", [](testing &t) {
197        auto parser = build_peg_parser([](common_peg_parser_builder & p)  {
198            return p.until_one_of({"ab", "cd", "ef"});
199        });
200
201        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
202            parser.build_grammar(builder);
203        });
204
205        assert_gbnf_equal(t, R"""(
206            root ::= until-0
207            space ::= | " " | "\n"{1,2} [ \t]{0,20}
208            until-0 ::= | [a] until-0-01 | [c] until-0-03 | [e] until-0-05 | [^ace] until-0
209            until-0-01 ::= | [a] until-0-01 | [c] until-0-03 | [e] until-0-05 | [^abce] until-0
210            until-0-03 ::= | [a] until-0-01 | [c] until-0-03 | [e] until-0-05 | [^acde] until-0
211            until-0-05 ::= | [a] until-0-01 | [c] until-0-03 | [e] until-0-05 | [^acef] until-0
212        )""", gbnf);
213    });
214
215    t.test("ac grammar", [](testing &t) {
216        auto parser = build_peg_parser([](common_peg_parser_builder & p)  {
217            return p.ac(p.until("</tag>") + p.literal("</tag>"), "</tag>");
218        });
219
220        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
221            parser.build_grammar(builder);
222        });
223
224        assert_gbnf_equal(t, R"""(
225            ac-3 ::= [<] ac-3-01 | [^<] ac-3
226            ac-3-01 ::= [<] ac-3-01 | [/] ac-3-02 | [^/<] ac-3
227            ac-3-02 ::= [<] ac-3-01 | [t] ac-3-03 | [^<t] ac-3
228            ac-3-03 ::= [<] ac-3-01 | [a] ac-3-04 | [^<a] ac-3
229            ac-3-04 ::= [<] ac-3-01 | [g] ac-3-05 | [^<g] ac-3
230            ac-3-05 ::= [>] | [<] ac-3-01 | [^<>] ac-3
231            root ::= ac-3
232            space ::= | " " | "\n"{1,2} [ \t]{0,20}
233        )""", gbnf);
234    });
235
236    t.test("ac grammar terminates at first delimiter", [](testing &t) {
237        auto parser = build_peg_parser([](common_peg_parser_builder & p)  {
238            return p.ac(p.until("\n</parameter>\n") + p.literal("\n</parameter>\n"), "\n</parameter>\n");
239        });
240
241        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
242            parser.build_grammar(builder);
243        });
244
245        assert_gbnf_equal(t, R"""(
246            ac-3 ::= [\n] ac-3-01 | [^\n] ac-3
247            ac-3-01 ::= [\n] ac-3-01 | [<] ac-3-02 | [^\n<] ac-3
248            ac-3-02 ::= [\n] ac-3-01 | [/] ac-3-03 | [^\n/] ac-3
249            ac-3-03 ::= [\n] ac-3-01 | [p] ac-3-04 | [^\np] ac-3
250            ac-3-04 ::= [\n] ac-3-01 | [a] ac-3-05 | [^\na] ac-3
251            ac-3-05 ::= [\n] ac-3-01 | [r] ac-3-06 | [^\nr] ac-3
252            ac-3-06 ::= [\n] ac-3-01 | [a] ac-3-07 | [^\na] ac-3
253            ac-3-07 ::= [\n] ac-3-01 | [m] ac-3-08 | [^\nm] ac-3
254            ac-3-08 ::= [\n] ac-3-01 | [e] ac-3-09 | [^\ne] ac-3
255            ac-3-09 ::= [\n] ac-3-01 | [t] ac-3-10 | [^\nt] ac-3
256            ac-3-10 ::= [\n] ac-3-01 | [e] ac-3-11 | [^\ne] ac-3
257            ac-3-11 ::= [\n] ac-3-01 | [r] ac-3-12 | [^\nr] ac-3
258            ac-3-12 ::= [\n] ac-3-01 | [>] ac-3-13 | [^\n>] ac-3
259            ac-3-13 ::= [\n] | [^\n] ac-3
260            root ::= ac-3
261            space ::= | " " | "\n"{1,2} [ \t]{0,20}
262        )""", gbnf);
263    });
264
265    t.test("ac grammar multiple delimiters", [](testing &t) {
266        auto parser = build_peg_parser([](common_peg_parser_builder & p)  {
267            return p.ac(p.eps(), std::vector<std::string>{"ab", "cd", "ef"});
268        });
269
270        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
271            parser.build_grammar(builder);
272        });
273
274        assert_gbnf_equal(t, R"""(
275            ac-1 ::= [a] ac-1-01 | [c] ac-1-03 | [e] ac-1-05 | [^ace] ac-1
276            ac-1-01 ::= [b] | [a] ac-1-01 | [c] ac-1-03 | [e] ac-1-05 | [^abce] ac-1
277            ac-1-03 ::= [d] | [a] ac-1-01 | [c] ac-1-03 | [e] ac-1-05 | [^acde] ac-1
278            ac-1-05 ::= [f] | [a] ac-1-01 | [c] ac-1-03 | [e] ac-1-05 | [^acef] ac-1
279            root ::= ac-1
280            space ::= | " " | "\n"{1,2} [ \t]{0,20}
281        )""", gbnf);
282    });
283
284    t.test("complex expressions with parentheses", [](testing &t) {
285        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
286            return p.one_or_more(p.literal("a") | p.literal("b"));
287        });
288
289        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
290            parser.build_grammar(builder);
291        });
292
293        assert_gbnf_equal(t, R"""(
294            root ::= ("a" | "b")+
295            space ::= | " " | "\n"{1,2} [ \t]{0,20}
296        )""", gbnf);
297    });
298
299    t.test("rule references", [](testing &t) {
300        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
301            auto digit = p.rule("digit", p.chars("[0-9]", 1, 1));
302            return p.one_or_more(digit);
303        });
304
305        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
306            parser.build_grammar(builder);
307        });
308
309        assert_gbnf_equal(t, R"""(
310            digit ::= [0-9]
311            root ::= digit+
312            space ::= | " " | "\n"{1,2} [ \t]{0,20}
313        )""", gbnf);
314    });
315
316    t.test("escaping in literals", [](testing &t) {
317        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
318            return p.literal("hello\nworld\n!");
319        });
320
321        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
322            parser.build_grammar(builder);
323        });
324
325        assert_gbnf_equal(t, R"""(
326            root ::= "hello\nworld\n!"
327            space ::= | " " | "\n"{1,2} [ \t]{0,20}
328        )""", gbnf);
329    });
330
331    t.test("operator<< (whitespace insertion)", [](testing &t) {
332        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
333            return p.literal("hello") << p.literal("world");
334        });
335
336        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
337            parser.build_grammar(builder);
338        });
339
340        assert_gbnf_equal(t, R"""(
341            root ::= "hello" space "world"
342            space ::= | " " | "\n"{1,2} [ \t]{0,20}
343        )""", gbnf);
344    });
345
346    t.test("emit only reachable rules", [](testing &t) {
347        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
348            p.rule("orphan", p.literal("orphan"));
349            return p.literal("hello") + p.rule("child", p.literal(" world"));
350        });
351
352        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
353            parser.build_grammar(builder);
354        });
355
356        assert_gbnf_equal(t, R"""(
357            child ::= " world"
358            root ::= "hello" child
359            space ::= | " " | "\n"{1,2} [ \t]{0,20}
360        )""", gbnf);
361    });
362
363    t.test("tagged choice inside sequence gets parenthesized", [](testing &t) {
364        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
365            return p.literal("a") + p.tag("t", p.literal("b") | p.literal("c"));
366        });
367
368        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
369            parser.build_grammar(builder);
370        });
371
372        assert_gbnf_equal(t, R"""(
373            root ::= "a" ("b" | "c")
374            space ::= | " " | "\n"{1,2} [ \t]{0,20}
375        )""", gbnf);
376    });
377
378    t.test("tagged sequence inside choice gets parenthesized", [](testing &t) {
379        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
380            return p.tag("t", p.literal("a") + p.literal("b")) | p.literal("c");
381        });
382
383        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
384            parser.build_grammar(builder);
385        });
386
387        assert_gbnf_equal(t, R"""(
388            root ::= "a" "b" | "c"
389            space ::= | " " | "\n"{1,2} [ \t]{0,20}
390        )""", gbnf);
391    });
392
393    t.test("atomic choice inside repetition gets parenthesized", [](testing &t) {
394        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
395            return p.one_or_more(p.atomic(p.literal("a") | p.literal("b")));
396        });
397
398        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
399            parser.build_grammar(builder);
400        });
401
402        assert_gbnf_equal(t, R"""(
403            root ::= ("a" | "b")+
404            space ::= | " " | "\n"{1,2} [ \t]{0,20}
405        )""", gbnf);
406    });
407
408    t.test("silent parser emits nothing in gbnf", [](testing &t) {
409        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
410            return p.literal("hello") + p.gbnf(p.literal("world"), "");
411        });
412
413        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
414            parser.build_grammar(builder);
415        });
416
417        assert_gbnf_equal(t, R"""(
418            root ::= "hello"
419            space ::= | " " | "\n"{1,2} [ \t]{0,20}
420        )""", gbnf);
421    });
422
423    t.test("silent choice inside sequence emits nothing", [](testing &t) {
424        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
425            return p.literal("a") + p.gbnf(p.literal("b") | p.literal("c"), "") + p.literal("d");
426        });
427
428        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
429            parser.build_grammar(builder);
430        });
431
432        assert_gbnf_equal(t, R"""(
433            root ::= "a" "d"
434            space ::= | " " | "\n"{1,2} [ \t]{0,20}
435        )""", gbnf);
436    });
437
438    t.test("silent wrapped in tag emits nothing", [](testing &t) {
439        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
440            return p.literal("a") + p.tag("t", p.gbnf(p.literal("b"), ""));
441        });
442
443        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
444            parser.build_grammar(builder);
445        });
446
447        assert_gbnf_equal(t, R"""(
448            root ::= "a"
449            space ::= | " " | "\n"{1,2} [ \t]{0,20}
450        )""", gbnf);
451    });
452
453    t.test("gbnf parser emits custom grammar", [](testing &t) {
454        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
455            return p.literal("a") + p.gbnf(p.literal("b"), "[a-z]+");
456        });
457
458        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
459            parser.build_grammar(builder);
460        });
461
462        assert_gbnf_equal(t, R"""(
463            root ::= "a" [a-z]+
464            space ::= | " " | "\n"{1,2} [ \t]{0,20}
465        )""", gbnf);
466    });
467
468    t.test("nested transparent wrappers get parenthesized", [](testing &t) {
469        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
470            return p.literal("x") + p.tag("outer", p.atomic(p.literal("a") | p.literal("b")));
471        });
472
473        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
474            parser.build_grammar(builder);
475        });
476
477        assert_gbnf_equal(t, R"""(
478            root ::= "x" ("a" | "b")
479            space ::= | " " | "\n"{1,2} [ \t]{0,20}
480        )""", gbnf);
481    });
482
483    t.test("emit only trigger rules (and references)", [](testing &t) {
484        auto parser = build_peg_parser([](common_peg_parser_builder & p) {
485            auto rule1 = p.rule("rule-1", p.literal("a") + p.ref("rule-2"));
486            p.rule("rule-2", p.literal("b") + p.ref("rule-3"), true);
487            p.rule("rule-3", p.literal("c") + p.ref("rule-4"));
488            p.rule("rule-4", p.literal("d"), true);
489            return rule1;
490        });
491
492        auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
493            parser.build_grammar(builder);
494        });
495
496        assert_gbnf_equal(t, R"""(
497            root ::= rule-1
498            rule-1 ::= "a" rule-2
499            rule-2 ::= "b" rule-3
500            rule-3 ::= "c" rule-4
501            rule-4 ::= "d"
502            space ::= | " " | "\n"{1,2} [ \t]{0,20}
503        )""", gbnf);
504
505        auto gbnf_lazy = build_grammar([&](const common_grammar_builder & builder) {
506            parser.build_grammar(builder, true);
507        });
508
509        assert_gbnf_equal(t, R"""(
510            root ::= rule-2 | rule-4
511            rule-2 ::= "b" rule-3
512            rule-3 ::= "c" rule-4
513            rule-4 ::= "d"
514            space ::= | " " | "\n"{1,2} [ \t]{0,20}
515        )""", gbnf_lazy);
516    });
517}