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