packages/sql-faker/tests/Unit/Compiler/Bison/GrammarCompilerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Compiler\Bison;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Compiler\Bison\Ast\BisonAlternativeNode;
11use SqlFaker\Compiler\Bison\Ast\BisonAst;
12use SqlFaker\Compiler\Bison\Ast\BisonRuleNode;
13use SqlFaker\Compiler\Bison\Ast\BisonStartDeclaration;
14use SqlFaker\Compiler\Bison\Ast\BisonSymbolForm;
15use SqlFaker\Compiler\Bison\Ast\BisonSymbolNode;
16use SqlFaker\Compiler\Bison\Ast\BisonTokenDeclaration;
17use SqlFaker\Compiler\Bison\Ast\BisonTokenDefinition;
18use SqlFaker\Compiler\Bison\GrammarCompiler;
19use SqlFaker\Compiler\UnknownSymbolException;
20use SqlFaker\Grammar\Model\NonTerminal;
21use SqlFaker\Grammar\Model\Terminal;
22
23#[CoversClass(BisonAlternativeNode::class)]
24#[CoversClass(BisonAst::class)]
25#[CoversClass(BisonRuleNode::class)]
26#[CoversClass(BisonStartDeclaration::class)]
27#[CoversClass(BisonSymbolNode::class)]
28#[CoversClass(BisonTokenDeclaration::class)]
29#[CoversClass(BisonTokenDefinition::class)]
30#[CoversClass(GrammarCompiler::class)]
31#[CoversClass(NonTerminal::class)]
32#[CoversClass(Terminal::class)]
33#[CoversClass(UnknownSymbolException::class)]
34#[CoversClass(\SqlFaker\Grammar\Model\Grammar::class)]
35#[CoversClass(NonTerminal::class)]
36#[CoversClass(\SqlFaker\Grammar\Model\Production::class)]
37#[CoversClass(\SqlFaker\Grammar\Model\ProductionRule::class)]
38#[UsesClass(GrammarCompiler::class)]
39#[UsesClass(\SqlFaker\Grammar\Model\Grammar::class)]
40#[UsesClass(BisonSymbolForm::class)]
41final class GrammarCompilerTest extends TestCase
42{
43 public function testCompile(): void
44 {
45 $ast = new BisonAst(
46 startSymbol: 'start',
47 prologue: null,
48 declarations: [],
49 rules: [
50 new BisonRuleNode('start', [
51 new BisonAlternativeNode(
52 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'expr')],
53 null,
54 null,
55 null,
56 null
57 ),
58 ]),
59 new BisonRuleNode('expr', [
60 new BisonAlternativeNode([], null, null, null, null),
61 ]),
62 ],
63 epilogue: null
64 );
65
66 $compiler = new GrammarCompiler();
67 $grammar = $compiler->compile($ast);
68
69 self::assertSame('start', $grammar->startSymbol);
70 self::assertCount(2, $grammar->ruleMap);
71 self::assertArrayHasKey('start', $grammar->ruleMap);
72 self::assertArrayHasKey('expr', $grammar->ruleMap);
73 }
74
75 public function testCompileMultipleAlternatives(): void
76 {
77 $ast = new BisonAst(
78 startSymbol: 'expr',
79 prologue: null,
80 declarations: [
81 new BisonTokenDeclaration(null, [
82 new BisonTokenDefinition('NUM', null, null),
83 ]),
84 ],
85 rules: [
86 new BisonRuleNode('expr', [
87 new BisonAlternativeNode(
88 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'NUM')],
89 null,
90 null,
91 null,
92 null
93 ),
94 new BisonAlternativeNode(
95 [
96 new BisonSymbolNode(BisonSymbolForm::Identifier, 'expr'),
97 new BisonSymbolNode(BisonSymbolForm::CharLiteral, '+'),
98 new BisonSymbolNode(BisonSymbolForm::Identifier, 'expr'),
99 ],
100 null,
101 null,
102 null,
103 null
104 ),
105 ]),
106 ],
107 epilogue: null
108 );
109
110 $compiler = new GrammarCompiler();
111 $grammar = $compiler->compile($ast);
112
113 self::assertCount(2, $grammar->ruleMap['expr']->alternatives);
114 self::assertCount(1, $grammar->ruleMap['expr']->alternatives[0]->symbols);
115 self::assertCount(3, $grammar->ruleMap['expr']->alternatives[1]->symbols);
116 self::assertInstanceOf(Terminal::class, $grammar->ruleMap['expr']->alternatives[1]->symbols[1]);
117 self::assertSame('+', $grammar->ruleMap['expr']->alternatives[1]->symbols[1]->value);
118 }
119
120 public function testCompileMergesSameNameRules(): void
121 {
122 $ast = new BisonAst(
123 startSymbol: 'stmt',
124 prologue: null,
125 declarations: [],
126 rules: [
127 new BisonRuleNode('stmt', [
128 new BisonAlternativeNode(
129 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'select')],
130 null,
131 null,
132 null,
133 null
134 ),
135 ]),
136 new BisonRuleNode('stmt', [
137 new BisonAlternativeNode(
138 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'insert')],
139 null,
140 null,
141 null,
142 null
143 ),
144 ]),
145 new BisonRuleNode('select', [
146 new BisonAlternativeNode([], null, null, null, null),
147 ]),
148 new BisonRuleNode('insert', [
149 new BisonAlternativeNode([], null, null, null, null),
150 ]),
151 ],
152 epilogue: null
153 );
154
155 $compiler = new GrammarCompiler();
156 $grammar = $compiler->compile($ast);
157
158 self::assertArrayHasKey('stmt', $grammar->ruleMap);
159 self::assertCount(2, $grammar->ruleMap['stmt']->alternatives);
160 }
161
162 public function testCompileThrowsOnUnknownSymbol(): void
163 {
164 $ast = new BisonAst(
165 startSymbol: 'start',
166 prologue: null,
167 declarations: [],
168 rules: [
169 new BisonRuleNode('start', [
170 new BisonAlternativeNode(
171 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'UNKNOWN')],
172 null,
173 null,
174 null,
175 null
176 ),
177 ]),
178 ],
179 epilogue: null
180 );
181
182 $compiler = new GrammarCompiler();
183
184 $this->expectException(UnknownSymbolException::class);
185 $this->expectExceptionMessage('Unknown symbol: UNKNOWN');
186
187 $compiler->compile($ast);
188 }
189
190 public function testCompilePreservesBisonStructure(): void
191 {
192 $ast = new BisonAst(
193 startSymbol: 'start',
194 prologue: null,
195 declarations: [],
196 rules: [
197 new BisonRuleNode('start', [
198 new BisonAlternativeNode(
199 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'expr')],
200 null,
201 null,
202 null,
203 null
204 ),
205 ]),
206 new BisonRuleNode('expr', [
207 new BisonAlternativeNode([], null, null, null, null),
208 ]),
209 ],
210 epilogue: null
211 );
212
213 $compiler = new GrammarCompiler();
214 $grammar = $compiler->compile($ast);
215
216 self::assertSame('start', $grammar->startSymbol);
217 self::assertCount(2, $grammar->ruleMap);
218 self::assertArrayHasKey('start', $grammar->ruleMap);
219 self::assertArrayHasKey('expr', $grammar->ruleMap);
220 }
221
222 public function testCompileWithMultipleAlternatives(): void
223 {
224 $ast = new BisonAst(
225 startSymbol: 'expr',
226 prologue: null,
227 declarations: [
228 new BisonTokenDeclaration(null, [
229 new BisonTokenDefinition('NUM', null, null),
230 ]),
231 ],
232 rules: [
233 new BisonRuleNode('expr', [
234 new BisonAlternativeNode(
235 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'NUM')],
236 null,
237 null,
238 null,
239 null
240 ),
241 new BisonAlternativeNode(
242 [
243 new BisonSymbolNode(BisonSymbolForm::Identifier, 'expr'),
244 new BisonSymbolNode(BisonSymbolForm::CharLiteral, '+'),
245 new BisonSymbolNode(BisonSymbolForm::Identifier, 'expr'),
246 ],
247 null,
248 null,
249 null,
250 null
251 ),
252 ]),
253 ],
254 epilogue: null
255 );
256
257 $compiler = new GrammarCompiler();
258 $grammar = $compiler->compile($ast);
259
260 self::assertCount(2, $grammar->ruleMap['expr']->alternatives);
261
262 $firstAlt = $grammar->ruleMap['expr']->alternatives[0];
263 self::assertCount(1, $firstAlt->symbols);
264
265 $secondAlt = $grammar->ruleMap['expr']->alternatives[1];
266 self::assertCount(3, $secondAlt->symbols);
267 self::assertInstanceOf(Terminal::class, $secondAlt->symbols[1]);
268 self::assertSame('+', $secondAlt->symbols[1]->value);
269 }
270
271 public function testCompileMergesSameNameRulesPreservesBisonStructure(): void
272 {
273 $ast = new BisonAst(
274 startSymbol: 'stmt',
275 prologue: null,
276 declarations: [],
277 rules: [
278 new BisonRuleNode('stmt', [
279 new BisonAlternativeNode(
280 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'select')],
281 null,
282 null,
283 null,
284 null
285 ),
286 ]),
287 new BisonRuleNode('stmt', [
288 new BisonAlternativeNode(
289 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'insert')],
290 null,
291 null,
292 null,
293 null
294 ),
295 ]),
296 new BisonRuleNode('select', [
297 new BisonAlternativeNode([], null, null, null, null),
298 ]),
299 new BisonRuleNode('insert', [
300 new BisonAlternativeNode([], null, null, null, null),
301 ]),
302 ],
303 epilogue: null
304 );
305
306 $compiler = new GrammarCompiler();
307 $grammar = $compiler->compile($ast);
308
309 self::assertArrayHasKey('stmt', $grammar->ruleMap);
310 self::assertCount(2, $grammar->ruleMap['stmt']->alternatives);
311 }
312
313 public function testCompileWithEmptyProduction(): void
314 {
315 $ast = new BisonAst(
316 startSymbol: 'opt',
317 prologue: null,
318 declarations: [],
319 rules: [
320 new BisonRuleNode('opt', [
321 new BisonAlternativeNode([], null, null, null, null),
322 ]),
323 ],
324 epilogue: null
325 );
326
327 $compiler = new GrammarCompiler();
328 $grammar = $compiler->compile($ast);
329
330 self::assertCount(1, $grammar->ruleMap['opt']->alternatives);
331 self::assertSame([], $grammar->ruleMap['opt']->alternatives[0]->symbols);
332 }
333
334 public function testCompileIgnoresNonTokenDeclarations(): void
335 {
336 $ast = new BisonAst(
337 startSymbol: 'start',
338 prologue: null,
339 declarations: [
340 new BisonStartDeclaration('start'),
341 ],
342 rules: [
343 new BisonRuleNode('start', [
344 new BisonAlternativeNode(
345 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'expr')],
346 null,
347 null,
348 null,
349 null
350 ),
351 ]),
352 new BisonRuleNode('expr', [
353 new BisonAlternativeNode([], null, null, null, null),
354 ]),
355 ],
356 epilogue: null
357 );
358
359 $compiler = new GrammarCompiler();
360 $grammar = $compiler->compile($ast);
361
362 self::assertCount(2, $grammar->ruleMap);
363 }
364
365 public function testCompileDistinguishesTerminalsAndNonTerminals(): void
366 {
367 $ast = new BisonAst(
368 startSymbol: 'list',
369 prologue: null,
370 declarations: [
371 new BisonTokenDeclaration(null, [
372 new BisonTokenDefinition('TOKEN', null, null),
373 ]),
374 ],
375 rules: [
376 new BisonRuleNode('list', [
377 new BisonAlternativeNode(
378 [
379 new BisonSymbolNode(BisonSymbolForm::Identifier, 'item'),
380 new BisonSymbolNode(BisonSymbolForm::CharLiteral, ','),
381 new BisonSymbolNode(BisonSymbolForm::Identifier, 'item'),
382 ],
383 null,
384 null,
385 null,
386 null
387 ),
388 ]),
389 new BisonRuleNode('item', [
390 new BisonAlternativeNode(
391 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'TOKEN')],
392 null,
393 null,
394 null,
395 null
396 ),
397 ]),
398 ],
399 epilogue: null
400 );
401
402 $compiler = new GrammarCompiler();
403 $grammar = $compiler->compile($ast);
404
405 $symbols = $grammar->ruleMap['list']->alternatives[0]->symbols;
406
407 self::assertInstanceOf(NonTerminal::class, $symbols[0]);
408 self::assertSame('item', $symbols[0]->value);
409
410 self::assertInstanceOf(Terminal::class, $symbols[1]);
411 self::assertSame(',', $symbols[1]->value);
412
413 self::assertInstanceOf(NonTerminal::class, $symbols[2]);
414
415 $itemSymbols = $grammar->ruleMap['item']->alternatives[0]->symbols;
416 self::assertInstanceOf(Terminal::class, $itemSymbols[0]);
417 self::assertSame('TOKEN', $itemSymbols[0]->value);
418 }
419
420 public function testCompileUsesStartSymbolFromAst(): void
421 {
422 $ast = new BisonAst(
423 startSymbol: 'my_custom_start',
424 prologue: null,
425 declarations: [],
426 rules: [
427 new BisonRuleNode('my_custom_start', [
428 new BisonAlternativeNode([], null, null, null, null),
429 ]),
430 ],
431 epilogue: null
432 );
433
434 $compiler = new GrammarCompiler();
435 $grammar = $compiler->compile($ast);
436
437 self::assertSame('my_custom_start', $grammar->startSymbol);
438 }
439
440 public function testCompileDiscardsActionAndPrecedence(): void
441 {
442 $ast = new BisonAst(
443 startSymbol: 'expr',
444 prologue: null,
445 declarations: [
446 new BisonTokenDeclaration(null, [
447 new BisonTokenDefinition('NUM', null, null),
448 ]),
449 ],
450 rules: [
451 new BisonRuleNode('expr', [
452 new BisonAlternativeNode(
453 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'NUM')],
454 '{ $$ = $1; }',
455 'UMINUS',
456 1,
457 '<merge_func>'
458 ),
459 ]),
460 ],
461 epilogue: null
462 );
463
464 $compiler = new GrammarCompiler();
465 $grammar = $compiler->compile($ast);
466
467 self::assertCount(1, $grammar->ruleMap);
468 self::assertCount(1, $grammar->ruleMap['expr']->alternatives);
469
470 $production = $grammar->ruleMap['expr']->alternatives[0];
471 self::assertCount(1, $production->symbols);
472 self::assertInstanceOf(Terminal::class, $production->symbols[0]);
473 self::assertSame('NUM', $production->symbols[0]->value);
474 }
475
476 public function testCompileWithMultipleDistinctRules(): void
477 {
478 $ast = new BisonAst(
479 startSymbol: 'program',
480 prologue: null,
481 declarations: [],
482 rules: [
483 new BisonRuleNode('program', [
484 new BisonAlternativeNode(
485 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'stmt_list')],
486 null,
487 null,
488 null,
489 null
490 ),
491 ]),
492 new BisonRuleNode('stmt_list', [
493 new BisonAlternativeNode(
494 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'stmt')],
495 null,
496 null,
497 null,
498 null
499 ),
500 ]),
501 new BisonRuleNode('stmt', [
502 new BisonAlternativeNode(
503 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'expr')],
504 null,
505 null,
506 null,
507 null
508 ),
509 ]),
510 new BisonRuleNode('expr', [
511 new BisonAlternativeNode([], null, null, null, null),
512 ]),
513 ],
514 epilogue: null
515 );
516
517 $compiler = new GrammarCompiler();
518 $grammar = $compiler->compile($ast);
519
520 self::assertCount(4, $grammar->ruleMap);
521 self::assertArrayHasKey('program', $grammar->ruleMap);
522 self::assertArrayHasKey('stmt_list', $grammar->ruleMap);
523 self::assertArrayHasKey('stmt', $grammar->ruleMap);
524 self::assertArrayHasKey('expr', $grammar->ruleMap);
525 }
526
527 public function testCompileWithEmptyDeclarationsAndSingleRule(): void
528 {
529 $ast = new BisonAst(
530 startSymbol: 'start',
531 prologue: null,
532 declarations: [],
533 rules: [
534 new BisonRuleNode('start', [
535 new BisonAlternativeNode([], null, null, null, null),
536 ]),
537 ],
538 epilogue: null
539 );
540
541 $compiler = new GrammarCompiler();
542 $grammar = $compiler->compile($ast);
543
544 self::assertCount(1, $grammar->ruleMap);
545 }
546
547 public function testCompileIgnoresPrologueAndEpilogue(): void
548 {
549 $prologue = '%{ #include <stdio.h> %}';
550 $epilogue = 'int main() { return 0; }';
551
552 $ast = new BisonAst(
553 startSymbol: 'start',
554 prologue: $prologue,
555 declarations: [],
556 rules: [
557 new BisonRuleNode('start', [
558 new BisonAlternativeNode([], null, null, null, null),
559 ]),
560 ],
561 epilogue: $epilogue
562 );
563
564 $compiler = new GrammarCompiler();
565 $grammar = $compiler->compile($ast);
566
567 self::assertSame('start', $grammar->startSymbol);
568 self::assertCount(1, $grammar->ruleMap);
569 }
570
571 public function testCompileThrowsOnUnknownSymbolPreservesBisonStructure(): void
572 {
573 $ast = new BisonAst(
574 startSymbol: 'start',
575 prologue: null,
576 declarations: [],
577 rules: [
578 new BisonRuleNode('start', [
579 new BisonAlternativeNode(
580 [new BisonSymbolNode(BisonSymbolForm::Identifier, 'UNKNOWN')],
581 null,
582 null,
583 null,
584 null
585 ),
586 ]),
587 ],
588 epilogue: null
589 );
590
591 $compiler = new GrammarCompiler();
592
593 $this->expectException(UnknownSymbolException::class);
594 $this->expectExceptionMessage('Unknown symbol: UNKNOWN');
595
596 $compiler->compile($ast);
597 }
598
599 public function testDeclarationsIndexesTokensAndIgnoresStartDeclarations(): void
600 {
601 $token = new BisonTokenDefinition('SELECT', 10, 'select');
602 $ast = new BisonAst('stmt', null, [
603 new BisonStartDeclaration('stmt'),
604 new BisonTokenDeclaration(null, [$token]),
605 ], [], null);
606
607 self::assertSame(['SELECT' => $token], (new GrammarCompiler())->declarations($ast));
608 }
609}
610