6
#include "llama-grammar.h"
7
#include "json-schema-to-grammar.h"
13
using json = nlohmann::ordered_json;
15
static llama_grammar * build_grammar(const std::string & grammar_str) {
16
return llama_grammar_init_impl(nullptr, grammar_str.c_str(), "root");
19
static bool test_build_grammar_fails(const std::string & grammar_str) {
20
fprintf(stderr, "⚫ Testing failure for grammar: %s\n", grammar_str.c_str());
21
bool grammar_fails = false;
22
llama_grammar * grammar = build_grammar(grammar_str);
23
if (grammar != nullptr) {
24
fprintf(stderr, " ❌ Expected build failure, but succeeded\n");
27
fprintf(stdout, " ✅︎\n");
32
static bool match_string(const std::string & input, llama_grammar * grammar) {
33
const auto cpts = unicode_cpts_from_utf8(input);
35
const llama_grammar_rules & rules = llama_grammar_get_rules (grammar);
36
llama_grammar_stacks & stacks_cur = llama_grammar_get_stacks(grammar);
38
for (const auto & cpt : cpts) {
39
const llama_grammar_stacks stacks_prev = llama_grammar_get_stacks(grammar); // copy
41
llama_grammar_accept(rules, stacks_prev, cpt, stacks_cur);
43
if (stacks_cur.empty()) {
44
// no stacks means that the grammar failed to match at this point
49
for (const auto & stack : stacks_cur) {
51
// An empty stack means that the grammar has been completed
59
static void test(const std::string & test_desc, const std::string & grammar_str, const std::vector<std::string> & passing_strings, const std::vector<std::string> & failing_strings) {
60
fprintf(stderr, "⚫ Testing %s\n%s\n", test_desc.c_str(), grammar_str.c_str());
63
auto * grammar = build_grammar(grammar_str);
65
// Save the original grammar stacks so that we can reset after every new string we want to test
66
const llama_grammar_stacks stacks_org = llama_grammar_get_stacks(grammar);
68
llama_grammar_stacks & stacks_cur = llama_grammar_get_stacks(grammar);
70
fprintf(stderr, " 🔵 Valid strings:\n");
73
for (const auto & test_string : passing_strings) {
74
fprintf(stderr, " \"%s\" ", test_string.c_str());
77
bool matched = match_string(test_string, grammar);
80
fprintf(stderr, "❌ (failed to match)\n");
82
// DEBUG: Write strings to files so that we can analyze more easily with gbnf-validator program to see exactly where things failed.
83
// DEBUG: Write the grammar_str to test-grammar-integration.grammar.gbnf
84
FILE* grammar_file = fopen("test-grammar-integration.grammar.gbnf", "w");
86
fprintf(grammar_file, "%s", grammar_str.c_str());
90
// DEBUG: Write the test string to test-grammar-integration.string.txt
91
FILE* string_file = fopen("test-grammar-integration.string.txt", "w");
93
fprintf(string_file, "%s", test_string.c_str());
97
fprintf(stderr, "\n NOTE: Debug grammar file generated. To analyze this failure in detail, run the following command: ./llama-gbnf-validator test-grammar-integration.grammar.gbnf test-grammar-integration.string.txt\n\n");
99
fprintf(stdout, "✅︎\n");
104
// Reset the grammar stacks
105
stacks_cur = stacks_org;
108
fprintf(stderr, " 🟠 Invalid strings:\n");
111
for (const auto & test_string : failing_strings) {
112
fprintf(stderr, " \"%s\" ", test_string.c_str());
115
bool matched = match_string(test_string, grammar);
118
fprintf(stderr, "❌ (incorrectly matched)\n");
120
fprintf(stdout, "✅︎\n");
124
// Reset the grammar stacks
125
stacks_cur = stacks_org;
128
// Clean up allocated memory
129
llama_grammar_free_impl(grammar);
131
static void test_grammar(const std::string & test_desc, const std::string & grammar_str, const std::vector<std::string> & passing_strings, const std::vector<std::string> & failing_strings) {
132
test(test_desc + ". Grammar: " + grammar_str, grammar_str, passing_strings, failing_strings);
134
static void test_schema(const std::string & test_desc, const std::string & schema_str, const std::vector<std::string> & passing_strings, const std::vector<std::string> & failing_strings) {
135
test(test_desc + ". Schema: " + schema_str, json_schema_to_grammar(json::parse(schema_str)), passing_strings, failing_strings);
138
static void test_simple_grammar() {
157
"-100000000000000000000000000000000",
158
"100000000000000000000000000000000",
362
"exclusive min / max",
366
"exclusiveMinimum": 0,
367
"exclusiveMaximum": 10000
383
// Test case for a simple grammar
388
expr ::= term ("+" term)*
390
number ::= [0-9]+)""",
407
static void test_complex_grammar() {
408
// Test case for a more complex grammar, with both failure strings and success strings
410
"medium complexity grammar",
414
expression ::= term ws (("+"|"-") ws term)*
415
term ::= factor ws (("*"|"/") ws factor)*
416
factor ::= number | variable | "(" expression ")" | function-call
418
variable ::= [a-zA-Z_][a-zA-Z0-9_]*
419
function-call ::= variable ws "(" (expression ("," ws expression)*)? ")"
420
ws ::= [ \t\n\r]?)""",
438
"a * (b + c) - d / e",
441
"123*456*789-123/456+789*123",
442
"123+456*789-123/456+789*123-456/789+123*456-789/123+456*789-123/456+789*123-456"
463
"123+456*789-123/456+789*123-456/789+123*456-789/123+456*789-123/456+789*123-456/",
468
static void test_special_chars() {
469
// A collection of tests to exercise special characters such as "."
471
"special characters",
474
root ::= ... "abc" ...
480
// NOTE: Also ensures that multi-byte characters still count as a single character
495
static void test_quantifiers() {
496
// A collection of tests to exercise * + and ? quantifiers
501
R"""(root ::= "a"*)""",
507
"aaaaaaaaaaaaaaaaaa",
508
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
516
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"
522
R"""(root ::= "a"+)""",
527
"aaaaaaaaaaaaaaaaaa",
528
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
537
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"
543
R"""(root ::= "a"?)""",
561
root ::= cons+ vowel* cons? (vowel cons)*
563
cons ::= [bcdfghjklmnpqrstvwxyz]
583
"simple exact repetition",
602
"simple min repetition",
621
"simple max repetition",
640
"min / max repetition",
643
root ::= ("0x" [A-F0-9]{2} " "?){3,5}
648
"0xFF 0x12 0xAB 0x00 0x00",
655
"0xFF 0x12 0xAB 0x00 0x00 0x00",
660
static void test_failure_missing_root() {
661
fprintf(stderr, "⚫ Testing missing root node:\n");
662
// Test case for a grammar that is missing a root rule
663
const std::string grammar_str = R"""(
665
expr ::= term ("+" term)*
667
number ::= [0-9]+)""";
669
llama_grammar_parser parsed_grammar;
670
parsed_grammar.parse(grammar_str.c_str());
672
// Ensure we parsed correctly
673
assert(!parsed_grammar.rules.empty());
675
// Ensure we do NOT have a root node
676
assert(parsed_grammar.symbol_ids.find("root") == parsed_grammar.symbol_ids.end());
677
fprintf(stderr, " ✅︎ Passed\n");
680
static void test_failure_missing_reference() {
681
fprintf(stderr, "⚫ Testing missing reference node:\n");
683
// Test case for a grammar that is missing a referenced rule
684
const std::string grammar_str =
686
expr ::= term ("+" term)*
688
number ::= [0-9]+)""";
690
fprintf(stderr, " Expected error: ");
692
llama_grammar_parser parsed_grammar;
693
parsed_grammar.parse(grammar_str.c_str());
695
// Ensure we did NOT parsed correctly
696
assert(parsed_grammar.rules.empty());
698
fprintf(stderr, " End of expected error.\n");
699
fprintf(stderr, " ✅︎ Passed\n");
702
static void test_failure_left_recursion() {
703
fprintf(stderr, "⚫ Testing left recursion detection:\n");
705
// Test simple left recursion detection
706
const std::string simple_str = R"""(root ::= "a" | root "a")""";
707
assert(test_build_grammar_fails(simple_str));
709
// Test more complicated left recursion detection
710
const std::string medium_str = R"""(
712
asdf ::= "a" | asdf "a"
714
assert(test_build_grammar_fails(medium_str));
716
// Test even more complicated left recursion detection
717
const std::string hard_str = R"""(
719
asdf ::= "a" | foo "b"
720
foo ::= "c" | asdf "d" | "e")""";
721
assert(test_build_grammar_fails(hard_str));
723
// Test yet even more complicated left recursion detection
724
const std::string hardest_str = R"""(
726
asdf ::= "a" | foo "b"
727
foo ::= "c" | empty asdf "d" | "e"
728
empty ::= "blah" | )""";
729
assert(test_build_grammar_fails(hardest_str));
731
fprintf(stderr, " ✅︎ Passed\n");
734
static void test_json_schema() {
735
// Note that this is similar to the regular grammar tests,
736
// but we convert each json schema to a grammar before parsing.
737
// Otherwise, this test structure is the same.
740
"empty schema (object)",
748
R"""({"foo": "bar"})""",
761
"exotic formats (list)",
765
{ "format": "date" },
766
{ "format": "uuid" },
767
{ "format": "time" },
768
{ "format": "date-time" }
773
// "{}", // NOTE: This string passes for this schema on https://www.jsonschemavalidator.net/ -- should it?
774
// "[]", // NOTE: This string passes for this schema on https://www.jsonschemavalidator.net/ -- should it?
775
R"""(["2012-04-23", "12345678-1234-1234-1234-1234567890ab", "18:25:43.511Z", "2012-04-23T18:25:43.511Z"])""",
776
//R"""(["2012-04-23","12345678-1234-1234-1234-1234567890ab"])""", // NOTE: This string passes for this schema on https://www.jsonschemavalidator.net/ -- should it?
777
//R"""({"foo": "bar"})""", // NOTE: This string passes for this schema on https://www.jsonschemavalidator.net/ -- should it?
781
R"""(["foo", "bar"])""",
782
R"""(["12345678-1234-1234-1234-1234567890ab"])""",
801
R"""("foo": "bar")""",
806
"string w/ min length 1",
821
R"""("foo": "bar")""",
826
"string w/ min length 3",
847
"string w/ max length",
868
"string w/ min & max length",
920
R"""(1234567890123456)""",
927
R"""(12345678901234567 )""",
970
"enum": ["red", "amber", "green", null, 42, ["foo"]]
992
"pattern": "^[a-zA-Z0-9_-]*$"
997
R"""("He_llo-12")""",
1002
R"""("Hello World")""",
1007
"pattern with escapes",
1010
"pattern": "^a\\^\\$\\.\\[\\]\\(\\)\\|\\{\\}\\*\\+\\?b$"
1014
R"""("a^$.[]()|{}*+?b")""",
1027
"type": ["array", "null"],
1028
"items": { "type": "string" }
1036
"[\"foo\", \"bar\"]",
1052
"type": ["number", "integer"]
1060
R"""([1, 2, 3, 4])""",
1061
R"""([1, 2, 3, 4, 5])""",
1066
R"""([1, 2, 3, 4, 5, 6])""",
1071
// Properties (from: https://json-schema.org/understanding-json-schema/reference/object#properties)
1073
"object properties",
1078
"number": { "type": "number" },
1079
"street_name": { "type": "string" },
1080
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
1085
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type":"Avenue"})""",
1086
// "By default, leaving out properties is valid"
1087
R"""({ "street_name": "Pennsylvania" })""",
1088
R"""({ "number": 1600, "street_name": "Pennsylvania" })""",
1089
// "By extension, even an empty object is valid"
1091
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" })""",
1095
// Change datatype from number to string
1096
R"""({ "number": "1600", "street_name": "Pennsylvania", "street_type":"Avenue"})""",
1097
// Reorder properties
1098
R"""({ "street_name": "Pennsylvania", "number": 1600 })""",
1099
// Reorder properties
1100
R"""({ "number": "1600", "street_name": "Pennsylvania", "street_type":"Avenue"})""",
1101
// "Additional properties default to false for generation, even though the spec says true.
1102
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type":"Avenue", "direction":"NW"})""",
1108
"additional properties can't override other properties",
1111
"a": {"type": "integer"},
1112
"b": {"type": "integer"}
1114
"additionalProperties": true
1120
R"""({"a": 42, "c": ""})""",
1121
R"""({"a_": ""})""",
1127
R"""({"a": "", "b": ""})""",
1131
// Properties (from: https://json-schema.org/understanding-json-schema/reference/object#properties)
1133
"object properties, additionalProperties: true",
1138
"number": { "type": "number" },
1139
"street_name": { "type": "string" },
1140
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
1142
"additionalProperties": true
1146
// "By extension, even an empty object is valid"
1148
R"""({"number":1600,"street_name":"Pennsylvania","street_type":"Avenue"})""",
1149
// "By default, leaving out properties is valid"
1150
R"""({ "street_name": "Pennsylvania" })""",
1151
R"""({ "number": 1600, "street_name": "Pennsylvania" })""",
1152
// "By default, providing additional properties is valid"
1153
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type":"Avenue", "direction":"NW"})""",
1154
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" })""",
1158
// Change datatype from number to string
1159
R"""({ "number": "1600", "street_name": "Pennsylvania", "street_type":"Avenue"})""",
1160
// Reorder properties
1161
R"""({ "street_name": "Pennsylvania", "number": 1600, "street_type":"Avenue"})""",
1165
// Additional properties: false
1167
"required + optional props each in original order",
1172
"number": { "type": "number" },
1173
"street_name": { "type": "string" },
1174
"street_type": { "enum": ["Street", "Avenue", "Boulevard"] }
1176
"additionalProperties": false
1180
R"""({ "street_name": "Pennsylvania" })""",
1181
R"""({ "number": 1600, "street_type":"Avenue"})""",
1182
R"""({ "number": 1600, "street_name": "Pennsylvania" })""",
1183
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type":"Avenue"})""",
1184
// Spaces are permitted around enum values
1185
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue" })""",
1189
// Reorder properties
1190
R"""({ "street_type": "Avenue", "number": 1600 })""",
1192
R"""({ "number": 1600, "street_name": "Pennsylvania", "street_type": "Avenue", "direction": "NW" })""",
1197
"required + optional props each in original order",
1201
"b": {"type": "string"},
1202
"a": {"type": "string"},
1203
"d": {"type": "string"},
1204
"c": {"type": "string"}
1206
"required": ["a", "b"],
1207
"additionalProperties": false
1211
R"""({"b": "foo", "a": "bar"})""",
1212
R"""({"b":"foo","a":"bar","d":"qux"})""",
1213
R"""({"b":"foo", "a":"bar", "d":"qux", "c":"baz"})""",
1217
R"""({"a": "foo", "b": "bar"})""",
1218
R"""({"b": "bar"})""",
1219
R"""({"a": "foo", "c": "baz"})""",
1220
R"""({"a":"foo", "b":"bar", "c":"baz", "d":"qux"})""",
1224
// NOTE: Example from https://json-schema.org/learn/getting-started-step-by-step#define-required-properties
1229
"$schema": "https://json-schema.org/draft/2020-12/schema",
1230
"$id": "https://example.com/product.schema.json",
1232
"description": "A product from Acme's catalog",
1236
"description": "The unique identifier for a product",
1240
"description": "Name of the product",
1244
"description": "The price of the product",
1246
"exclusiveMinimum": 0
1249
"description": "Tags for the product",
1270
"required": [ "length", "width", "height" ]
1273
"required": [ "productId", "productName", "price" ]
1277
R"""({"productId": 1, "productName": "A green door", "price": 12.50})""",
1278
R"""({"productId": 1, "productName": "A green door", "price": 12.50, "tags": ["home", "green"]})""",
1279
R"""({"productId": 1, "productName": "A green door", "price": 12.50, "tags": ["home", "green"], "dimensions": {"length": 785, "width": 250.5, "height": -0.359}})""",
1283
R"""({})""", // Missing all required properties
1284
R"""({"productName": "A green door", "price": 12.50, "productId": 1})""", // Out of order properties
1285
// TODO: The following line should fail, but currently it passes. `exclusiveMinimum` is not supported, as it would likely be too difficult to implement.
1286
// Perhaps special checks for minimum and maximum values of 0 could be added (since that's relatively easy to do with grammars), but anything else would likely be too complex.
1287
// R"""({"productId": 1, "productName": "A green door", "price": -12.50})""",
1288
R"""({"productId": 1, "productName": "A green door"})""", // Missing required property (price)
1289
R"""({"productName": "A green door", "price": 12.50})""", // Missing required property (productId)
1290
R"""({"productId": 1, "productName": "A green door", "price": 12.50, "tags": []})""", // tags is empty, but minItems is 1
1291
R"""({"productId": 1, "productName": "A green door", "price": 12.50, "dimensions": {"length": 785, "width": 250.5, "height": -0.359}, "tags": ["home", "green"]})""", // Tags and dimensions are out of order
1292
// TODO: The following line should fail, but currently it passes. `uniqueItems` is not supported, as it would likely be too difficult to implement.
1293
// R"""({"productId": 1, "productName": "A green door", "price": 12.50, "tags": ["home", "green", "home"]})""",
1299
fprintf(stdout, "Running grammar integration tests...\n");
1300
test_simple_grammar();
1301
test_complex_grammar();
1302
test_special_chars();
1304
test_failure_missing_root();
1305
test_failure_missing_reference();
1306
test_failure_left_recursion();
1308
fprintf(stdout, "All tests passed.\n");