packages/sql-faker/tests/Unit/Compiler/Bison/BisonParserTest.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\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use RuntimeException;
12use SqlFaker\Compiler\Bison\Ast\BisonAlternativeNode;
13use SqlFaker\Compiler\Bison\Ast\BisonAst;
14use SqlFaker\Compiler\Bison\Ast\BisonDefineDeclaration;
15use SqlFaker\Compiler\Bison\Ast\BisonExpectDeclaration;
16use SqlFaker\Compiler\Bison\Ast\BisonParamDeclaration;
17use SqlFaker\Compiler\Bison\Ast\BisonPrecedenceDeclaration;
18use SqlFaker\Compiler\Bison\Ast\BisonRuleNode;
19use SqlFaker\Compiler\Bison\Ast\BisonStartDeclaration;
20use SqlFaker\Compiler\Bison\Ast\BisonSymbolForm;
21use SqlFaker\Compiler\Bison\Ast\BisonSymbolNode;
22use SqlFaker\Compiler\Bison\Ast\BisonTokenDeclaration;
23use SqlFaker\Compiler\Bison\Ast\BisonTokenDefinition;
24use SqlFaker\Compiler\Bison\Ast\BisonTypeDeclaration;
25use SqlFaker\Compiler\Bison\Ast\BisonUnknownDeclaration;
26use SqlFaker\Compiler\Bison\BisonParser;
27use SqlFaker\Compiler\Bison\BisonPreamble;
28use SqlFaker\Compiler\Bison\BisonPreambleReader;
29use SqlFaker\Compiler\Bison\BisonStartSymbol;
30use SqlFaker\Compiler\Bison\Directive\BisonDeclarationBoundary;
31use SqlFaker\Compiler\Bison\Directive\BisonDirectiveReaderChain;
32use SqlFaker\Compiler\Bison\Directive\DefineDirectiveReader;
33use SqlFaker\Compiler\Bison\Directive\ExpectDirectiveReader;
34use SqlFaker\Compiler\Bison\Directive\ParamDirectiveReader;
35use SqlFaker\Compiler\Bison\Directive\PrecedenceDirectiveReader;
36use SqlFaker\Compiler\Bison\Directive\StartDirectiveReader;
37use SqlFaker\Compiler\Bison\Directive\TokenDirectiveReader;
38use SqlFaker\Compiler\Bison\Directive\TypeDirectiveReader;
39use SqlFaker\Compiler\Bison\Directive\UnknownDirectiveReader;
40use SqlFaker\Compiler\Bison\Lexer\ActionScanner;
41use SqlFaker\Compiler\Bison\Lexer\BisonLexeme;
42use SqlFaker\Compiler\Bison\Lexer\BisonLexer;
43use SqlFaker\Compiler\Bison\Lexer\BisonScannerChain;
44use SqlFaker\Compiler\Bison\Lexer\BisonToken;
45use SqlFaker\Compiler\Bison\Lexer\BisonTokenStream;
46use SqlFaker\Compiler\Bison\Lexer\BisonTrivia;
47use SqlFaker\Compiler\Bison\Lexer\DirectiveScanner;
48use SqlFaker\Compiler\Bison\Lexer\IdentifierScanner;
49use SqlFaker\Compiler\Bison\Lexer\NumberScanner;
50use SqlFaker\Compiler\Bison\Lexer\PunctuationScanner;
51use SqlFaker\Compiler\Bison\Lexer\QuotedLiteralScanner;
52use SqlFaker\Compiler\Bison\Lexer\SourceCursor;
53use SqlFaker\Compiler\Bison\Lexer\TypeTagScanner;
54use SqlFaker\Compiler\Bison\Rule\BisonAlternativeDraft;
55use SqlFaker\Compiler\Bison\Rule\BisonAlternativeReader;
56use SqlFaker\Compiler\Bison\Rule\BisonRuleReader;
57use SqlFaker\Compiler\GrammarParseException;
58
59#[CoversClass(BisonParser::class)]
60#[CoversClass(BisonLexer::class)]
61#[CoversClass(BisonToken::class)]
62#[CoversClass(BisonLexeme::class)]
63#[CoversClass(BisonAst::class)]
64#[CoversClass(BisonRuleNode::class)]
65#[CoversClass(BisonAlternativeNode::class)]
66#[CoversClass(BisonSymbolNode::class)]
67#[CoversClass(BisonSymbolForm::class)]
68#[CoversClass(BisonDefineDeclaration::class)]
69#[CoversClass(BisonExpectDeclaration::class)]
70#[CoversClass(BisonParamDeclaration::class)]
71#[CoversClass(BisonPrecedenceDeclaration::class)]
72#[CoversClass(BisonStartDeclaration::class)]
73#[CoversClass(BisonTokenDeclaration::class)]
74#[CoversClass(BisonTokenDefinition::class)]
75#[CoversClass(BisonTypeDeclaration::class)]
76#[CoversClass(BisonUnknownDeclaration::class)]
77#[UsesClass(GrammarParseException::class)]
78#[UsesClass(SourceCursor::class)]
79#[UsesClass(BisonPreamble::class)]
80#[UsesClass(BisonPreambleReader::class)]
81#[UsesClass(BisonStartSymbol::class)]
82#[UsesClass(BisonDeclarationBoundary::class)]
83#[UsesClass(BisonDirectiveReaderChain::class)]
84#[UsesClass(DefineDirectiveReader::class)]
85#[UsesClass(ExpectDirectiveReader::class)]
86#[UsesClass(ParamDirectiveReader::class)]
87#[UsesClass(PrecedenceDirectiveReader::class)]
88#[UsesClass(StartDirectiveReader::class)]
89#[UsesClass(TokenDirectiveReader::class)]
90#[UsesClass(TypeDirectiveReader::class)]
91#[UsesClass(UnknownDirectiveReader::class)]
92#[UsesClass(ActionScanner::class)]
93#[UsesClass(BisonScannerChain::class)]
94#[UsesClass(BisonTokenStream::class)]
95#[UsesClass(BisonTrivia::class)]
96#[UsesClass(DirectiveScanner::class)]
97#[UsesClass(IdentifierScanner::class)]
98#[UsesClass(NumberScanner::class)]
99#[UsesClass(PunctuationScanner::class)]
100#[UsesClass(QuotedLiteralScanner::class)]
101#[UsesClass(TypeTagScanner::class)]
102#[UsesClass(BisonAlternativeDraft::class)]
103#[UsesClass(BisonAlternativeReader::class)]
104#[UsesClass(BisonRuleReader::class)]
105#[UsesClass(BisonLexer::class)]
106final class BisonParserTest extends TestCase
107{
108 public function testParseMinimalGrammar(): void
109 {
110 $input = <<<'BISON'
111%%
112rule: TOKEN;
113BISON;
114
115 $ast = (new BisonParser())->parse($input);
116
117 self::assertNull($ast->prologue);
118 self::assertSame([], $ast->declarations);
119 self::assertCount(1, $ast->rules);
120 self::assertSame('rule', $ast->rules[0]->name);
121 self::assertNull($ast->epilogue);
122 }
123
124 public function testParseNoRulesThrowsException(): void
125 {
126 $input = <<<'BISON'
127%token TOKEN
128%%
129BISON;
130
131 $this->expectException(GrammarParseException::class);
132 $this->expectExceptionMessage('No grammar rules parsed from the Bison grammar.');
133
134 (new BisonParser())->parse($input);
135 }
136
137 public function testParseWithPrologue(): void
138 {
139 $input = <<<'BISON'
140%{
141#include <stdio.h>
142%}
143%%
144rule: TOKEN;
145BISON;
146
147 $ast = (new BisonParser())->parse($input);
148
149 self::assertNotNull($ast->prologue);
150 self::assertStringContainsString('#include <stdio.h>', $ast->prologue);
151 }
152
153 public function testParseWithEpilogue(): void
154 {
155 $input = <<<'BISON'
156%%
157rule: TOKEN;
158%%
159epilogue_code
160BISON;
161
162 $ast = (new BisonParser())->parse($input);
163
164 self::assertNotNull($ast->epilogue);
165 self::assertStringContainsString('epilogue_code', $ast->epilogue);
166 }
167
168 public function testParseWithoutEpilogue(): void
169 {
170 $input = <<<'BISON'
171%%
172rule: TOKEN;
173BISON;
174
175 $ast = (new BisonParser())->parse($input);
176
177 self::assertNull($ast->epilogue);
178 }
179
180 public function testParseStartDirective(): void
181 {
182 $input = <<<'BISON'
183%start program
184%%
185program: statement;
186BISON;
187
188 $ast = (new BisonParser())->parse($input);
189
190 self::assertCount(1, $ast->declarations);
191 self::assertInstanceOf(BisonStartDeclaration::class, $ast->declarations[0]);
192 self::assertSame('program', $ast->declarations[0]->symbol);
193 }
194
195 public function testParseStartDirectiveWithoutIdentifier(): void
196 {
197 $input = <<<'BISON'
198%start
199%token TOKEN
200%%
201rule: TOKEN;
202BISON;
203
204 $ast = (new BisonParser())->parse($input);
205
206 self::assertCount(1, $ast->declarations);
207 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
208 }
209
210 public function testParseTokenDirective(): void
211 {
212 $input = <<<'BISON'
213%token TOKEN1 TOKEN2
214%%
215rule: TOKEN1;
216BISON;
217
218 $ast = (new BisonParser())->parse($input);
219
220 self::assertCount(1, $ast->declarations);
221 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
222 $decl = $ast->declarations[0];
223 self::assertNull($decl->typeTag);
224 self::assertCount(2, $decl->tokens);
225 self::assertSame('TOKEN1', $decl->tokens[0]->name);
226 self::assertSame('TOKEN2', $decl->tokens[1]->name);
227 }
228
229 public function testParseTokenDirectiveWithTypeTag(): void
230 {
231 $input = <<<'BISON'
232%token <num> NUMBER
233%%
234rule: NUMBER;
235BISON;
236
237 $ast = (new BisonParser())->parse($input);
238
239 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
240 self::assertSame('num', $ast->declarations[0]->typeTag);
241 }
242
243 public function testParseTokenDirectiveWithNumber(): void
244 {
245 $input = <<<'BISON'
246%token TOKEN 258
247%%
248rule: TOKEN;
249BISON;
250
251 $ast = (new BisonParser())->parse($input);
252
253 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
254 self::assertSame(258, $ast->declarations[0]->tokens[0]->number);
255 }
256
257 public function testParseTokenDirectiveWithAlias(): void
258 {
259 $input = <<<'BISON'
260%token TOKEN "token"
261%%
262rule: TOKEN;
263BISON;
264
265 $ast = (new BisonParser())->parse($input);
266
267 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
268 self::assertSame('token', $ast->declarations[0]->tokens[0]->alias);
269 }
270
271 public function testParseTokenDirectiveWithNumberAndAlias(): void
272 {
273 $input = <<<'BISON'
274%token TOKEN 258 "token"
275%%
276rule: TOKEN;
277BISON;
278
279 $ast = (new BisonParser())->parse($input);
280
281 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
282 $token = $ast->declarations[0]->tokens[0];
283 self::assertSame('TOKEN', $token->name);
284 self::assertSame(258, $token->number);
285 self::assertSame('token', $token->alias);
286 }
287
288 public function testParseTypeDirective(): void
289 {
290 $input = <<<'BISON'
291%type <node> expr term
292%%
293expr: term;
294BISON;
295
296 $ast = (new BisonParser())->parse($input);
297
298 self::assertCount(1, $ast->declarations);
299 self::assertInstanceOf(BisonTypeDeclaration::class, $ast->declarations[0]);
300 $decl = $ast->declarations[0];
301 self::assertSame('node', $decl->typeTag);
302 self::assertSame(['expr', 'term'], $decl->symbols);
303 }
304
305 public function testParseTypeDirectiveWithoutTypeTag(): void
306 {
307 $input = <<<'BISON'
308%type expr
309%token TOKEN
310%%
311rule: TOKEN;
312BISON;
313
314 $ast = (new BisonParser())->parse($input);
315
316 self::assertCount(1, $ast->declarations);
317 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
318 }
319
320 #[DataProvider('providerPrecedenceDirectives')]
321 public function testParsePrecedenceDirective(string $directive, string $expectedAssociativity): void
322 {
323 $input = <<<BISON
324{$directive} '+' '-'
325%%
326rule: TOKEN;
327BISON;
328
329 $ast = (new BisonParser())->parse($input);
330
331 self::assertCount(1, $ast->declarations);
332 self::assertInstanceOf(BisonPrecedenceDeclaration::class, $ast->declarations[0]);
333 $decl = $ast->declarations[0];
334 self::assertSame($expectedAssociativity, $decl->associativity);
335 self::assertSame(['+', '-'], $decl->symbols);
336 }
337
338 public function testParsePrecedenceDirectiveWithTypeTag(): void
339 {
340 $input = <<<'BISON'
341%left <op> PLUS MINUS
342%%
343rule: TOKEN;
344BISON;
345
346 $ast = (new BisonParser())->parse($input);
347
348 self::assertInstanceOf(BisonPrecedenceDeclaration::class, $ast->declarations[0]);
349 self::assertSame('op', $ast->declarations[0]->typeTag);
350 }
351
352 public function testParsePrecedenceDirectiveWithIdentifiers(): void
353 {
354 $input = <<<'BISON'
355%left PLUS MINUS
356%%
357rule: TOKEN;
358BISON;
359
360 $ast = (new BisonParser())->parse($input);
361
362 self::assertInstanceOf(BisonPrecedenceDeclaration::class, $ast->declarations[0]);
363 self::assertSame(['PLUS', 'MINUS'], $ast->declarations[0]->symbols);
364 }
365
366 public function testParseParseParamDirective(): void
367 {
368 $input = <<<'BISON'
369%parse-param { void *scanner }
370%%
371rule: TOKEN;
372BISON;
373
374 $ast = (new BisonParser())->parse($input);
375
376 self::assertCount(1, $ast->declarations);
377 self::assertInstanceOf(BisonParamDeclaration::class, $ast->declarations[0]);
378 $decl = $ast->declarations[0];
379 self::assertSame('parse-param', $decl->kind);
380 self::assertStringContainsString('void *scanner', $decl->code);
381 }
382
383 public function testParseLexParamDirective(): void
384 {
385 $input = <<<'BISON'
386%lex-param { void *scanner }
387%%
388rule: TOKEN;
389BISON;
390
391 $ast = (new BisonParser())->parse($input);
392
393 self::assertInstanceOf(BisonParamDeclaration::class, $ast->declarations[0]);
394 self::assertSame('lex-param', $ast->declarations[0]->kind);
395 }
396
397 public function testParseParamDirectiveWithoutAction(): void
398 {
399 $input = <<<'BISON'
400%parse-param
401%token TOKEN
402%%
403rule: TOKEN;
404BISON;
405
406 $ast = (new BisonParser())->parse($input);
407
408 self::assertCount(1, $ast->declarations);
409 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
410 }
411
412 public function testParseExpectDirective(): void
413 {
414 $input = <<<'BISON'
415%expect 5
416%%
417rule: TOKEN;
418BISON;
419
420 $ast = (new BisonParser())->parse($input);
421
422 self::assertCount(1, $ast->declarations);
423 self::assertInstanceOf(BisonExpectDeclaration::class, $ast->declarations[0]);
424 self::assertSame(5, $ast->declarations[0]->count);
425 }
426
427 public function testParseExpectDirectiveWithoutNumber(): void
428 {
429 $input = <<<'BISON'
430%expect
431%token TOKEN
432%%
433rule: TOKEN;
434BISON;
435
436 $ast = (new BisonParser())->parse($input);
437
438 self::assertCount(1, $ast->declarations);
439 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
440 }
441
442 public function testParseDefineDirectiveWithIdentifierValue(): void
443 {
444 $input = <<<'BISON'
445%define api.pure full
446%%
447rule: TOKEN;
448BISON;
449
450 $ast = (new BisonParser())->parse($input);
451
452 self::assertCount(1, $ast->declarations);
453 self::assertInstanceOf(BisonDefineDeclaration::class, $ast->declarations[0]);
454 $decl = $ast->declarations[0];
455 self::assertSame('api.pure', $decl->name);
456 self::assertSame('full', $decl->value);
457 }
458
459 public function testParseDefineDirectiveWithStringValue(): void
460 {
461 $input = <<<'BISON'
462%define api.prefix "yy"
463%%
464rule: TOKEN;
465BISON;
466
467 $ast = (new BisonParser())->parse($input);
468
469 self::assertInstanceOf(BisonDefineDeclaration::class, $ast->declarations[0]);
470 self::assertSame('yy', $ast->declarations[0]->value);
471 }
472
473 public function testParseDefineDirectiveWithNumberValue(): void
474 {
475 $input = <<<'BISON'
476%define parse.lac 1
477%%
478rule: TOKEN;
479BISON;
480
481 $ast = (new BisonParser())->parse($input);
482
483 self::assertInstanceOf(BisonDefineDeclaration::class, $ast->declarations[0]);
484 self::assertSame('1', $ast->declarations[0]->value);
485 }
486
487 public function testParseDefineDirectiveWithoutValue(): void
488 {
489 $input = <<<'BISON'
490%define api.pure
491%%
492rule: TOKEN;
493BISON;
494
495 $ast = (new BisonParser())->parse($input);
496
497 self::assertInstanceOf(BisonDefineDeclaration::class, $ast->declarations[0]);
498 self::assertSame('api.pure', $ast->declarations[0]->name);
499 self::assertNull($ast->declarations[0]->value);
500 }
501
502 public function testParseDefineDirectiveWithoutIdentifier(): void
503 {
504 $input = <<<'BISON'
505%define
506%token TOKEN
507%%
508rule: TOKEN;
509BISON;
510
511 $ast = (new BisonParser())->parse($input);
512
513 self::assertCount(1, $ast->declarations);
514 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
515 }
516
517 public function testParseUnknownDirective(): void
518 {
519 $input = <<<'BISON'
520%unknown foo bar
521%%
522rule: TOKEN;
523BISON;
524
525 $ast = (new BisonParser())->parse($input);
526
527 self::assertCount(1, $ast->declarations);
528 self::assertInstanceOf(BisonUnknownDeclaration::class, $ast->declarations[0]);
529 $decl = $ast->declarations[0];
530 self::assertSame('%unknown', $decl->directive);
531 self::assertSame('foo bar', $decl->content);
532 }
533
534 public function testParseRuleSingleAlternative(): void
535 {
536 $input = <<<'BISON'
537%%
538rule: A B C;
539BISON;
540
541 $ast = (new BisonParser())->parse($input);
542
543 self::assertCount(1, $ast->rules);
544 $rule = $ast->rules[0];
545 self::assertSame('rule', $rule->name);
546 self::assertCount(1, $rule->alternatives);
547 self::assertCount(3, $rule->alternatives[0]->symbols);
548 }
549
550 public function testParseRuleMultipleAlternatives(): void
551 {
552 $input = <<<'BISON'
553%%
554rule: A | B | C;
555BISON;
556
557 $ast = (new BisonParser())->parse($input);
558
559 self::assertCount(1, $ast->rules);
560 self::assertCount(3, $ast->rules[0]->alternatives);
561 }
562
563 public function testParseRuleWithAction(): void
564 {
565 $input = <<<'BISON'
566%%
567rule: A { $$ = $1; };
568BISON;
569
570 $ast = (new BisonParser())->parse($input);
571
572 self::assertNotNull($ast->rules[0]->alternatives[0]->action);
573 self::assertStringContainsString('$$ = $1', $ast->rules[0]->alternatives[0]->action);
574 }
575
576 public function testParseRuleWithPrecIdentifier(): void
577 {
578 $input = <<<'BISON'
579%%
580rule: A %prec UMINUS;
581BISON;
582
583 $ast = (new BisonParser())->parse($input);
584
585 self::assertSame('UMINUS', $ast->rules[0]->alternatives[0]->prec);
586 }
587
588 public function testParseRuleWithPrecCharLiteral(): void
589 {
590 $input = <<<'BISON'
591%%
592rule: A %prec '-';
593BISON;
594
595 $ast = (new BisonParser())->parse($input);
596
597 self::assertSame('-', $ast->rules[0]->alternatives[0]->prec);
598 }
599
600 public function testParseRuleWithPrecWithoutSymbol(): void
601 {
602 $input = <<<'BISON'
603%%
604rule: A %prec;
605BISON;
606
607 $ast = (new BisonParser())->parse($input);
608
609 self::assertNull($ast->rules[0]->alternatives[0]->prec);
610 }
611
612 public function testParseRuleWithDprec(): void
613 {
614 $input = <<<'BISON'
615%%
616rule: A %dprec 1;
617BISON;
618
619 $ast = (new BisonParser())->parse($input);
620
621 self::assertSame(1, $ast->rules[0]->alternatives[0]->dprec);
622 }
623
624 public function testParseRuleWithDprecWithoutNumber(): void
625 {
626 $input = <<<'BISON'
627%%
628rule: A %dprec;
629BISON;
630
631 $ast = (new BisonParser())->parse($input);
632
633 self::assertNull($ast->rules[0]->alternatives[0]->dprec);
634 }
635
636 public function testParseRuleWithMerge(): void
637 {
638 $input = <<<'BISON'
639%%
640rule: A %merge <merge_func>;
641BISON;
642
643 $ast = (new BisonParser())->parse($input);
644
645 self::assertSame('merge_func', $ast->rules[0]->alternatives[0]->merge);
646 }
647
648 public function testParseRuleWithMergeWithoutTypeTag(): void
649 {
650 $input = <<<'BISON'
651%%
652rule: A %merge;
653BISON;
654
655 $ast = (new BisonParser())->parse($input);
656
657 self::assertNull($ast->rules[0]->alternatives[0]->merge);
658 }
659
660 public function testParseRuleWithEmpty(): void
661 {
662 $input = <<<'BISON'
663%%
664rule: %empty;
665BISON;
666
667 $ast = (new BisonParser())->parse($input);
668
669 self::assertCount(1, $ast->rules);
670 self::assertCount(0, $ast->rules[0]->alternatives[0]->symbols);
671 }
672
673 public function testParseRuleWithCharLiteralSymbol(): void
674 {
675 $input = <<<'BISON'
676%%
677rule: '+' A '-';
678BISON;
679
680 $ast = (new BisonParser())->parse($input);
681
682 $symbols = $ast->rules[0]->alternatives[0]->symbols;
683 self::assertCount(3, $symbols);
684 self::assertSame(BisonSymbolForm::CharLiteral, $symbols[0]->type);
685 self::assertSame('+', $symbols[0]->value);
686 self::assertSame(BisonSymbolForm::Identifier, $symbols[1]->type);
687 self::assertSame(BisonSymbolForm::CharLiteral, $symbols[2]->type);
688 }
689
690 public function testParseRuleTerminatedBySemicolon(): void
691 {
692 $input = <<<'BISON'
693%%
694rule: A;
695BISON;
696
697 $ast = (new BisonParser())->parse($input);
698
699 self::assertCount(1, $ast->rules);
700 }
701
702 public function testParseRuleTerminatedByNextRule(): void
703 {
704 $input = <<<'BISON'
705%%
706rule1: A
707rule2: B;
708BISON;
709
710 $ast = (new BisonParser())->parse($input);
711
712 self::assertCount(2, $ast->rules);
713 self::assertSame('rule1', $ast->rules[0]->name);
714 self::assertSame('rule2', $ast->rules[1]->name);
715 }
716
717 public function testParseRuleTerminatedByPercentPercent(): void
718 {
719 $input = <<<'BISON'
720%%
721rule: A
722%%
723epilogue
724BISON;
725
726 $ast = (new BisonParser())->parse($input);
727
728 self::assertCount(1, $ast->rules);
729 self::assertNotNull($ast->epilogue);
730 }
731
732 public function testParseRuleTerminatedByEof(): void
733 {
734 $input = <<<'BISON'
735%%
736rule: A
737BISON;
738
739 $ast = (new BisonParser())->parse($input);
740
741 self::assertCount(1, $ast->rules);
742 }
743
744 public function testParseIdentifierNotFollowedByColonIsSkipped(): void
745 {
746 $input = <<<'BISON'
747%%
748orphan
749rule: A;
750BISON;
751
752 $ast = (new BisonParser())->parse($input);
753
754 self::assertCount(1, $ast->rules);
755 self::assertSame('rule', $ast->rules[0]->name);
756 }
757
758 public function testParseMultipleRules(): void
759 {
760 $input = <<<'BISON'
761%%
762rule1: A;
763rule2: B;
764rule3: C;
765BISON;
766
767 $ast = (new BisonParser())->parse($input);
768
769 self::assertCount(3, $ast->rules);
770 }
771
772 public function testParseUnknownDirectiveInRule(): void
773 {
774 $input = <<<'BISON'
775%%
776rule: A %unknown B;
777BISON;
778
779 $ast = (new BisonParser())->parse($input);
780
781 self::assertCount(1, $ast->rules);
782 $symbols = $ast->rules[0]->alternatives[0]->symbols;
783 self::assertCount(2, $symbols);
784 self::assertSame('A', $symbols[0]->value);
785 self::assertSame('B', $symbols[1]->value);
786 }
787
788 public function testParseComplexGrammar(): void
789 {
790 $input = <<<'BISON'
791%{
792#include <stdio.h>
793%}
794
795%token <num> NUMBER
796%token <str> STRING
797%token PLUS MINUS
798
799%type <node> expr term
800
801%left PLUS MINUS
802%right UMINUS
803
804%start program
805
806%%
807
808program: expr;
809
810expr: expr PLUS term { $$ = $1 + $3; }
811 | expr MINUS term { $$ = $1 - $3; }
812 | term
813 ;
814
815term: NUMBER
816 | '-' term %prec UMINUS { $$ = -$2; }
817 ;
818
819%%
820
821epilogue_section
822BISON;
823
824 $ast = (new BisonParser())->parse($input);
825
826 self::assertNotNull($ast->prologue);
827 self::assertCount(7, $ast->declarations);
828 self::assertCount(3, $ast->rules);
829 self::assertNotNull($ast->epilogue);
830 }
831
832 public function testParseFileSuccess(): void
833 {
834 $tempFile = tempnam(sys_get_temp_dir(), 'bison_test_');
835 self::assertNotFalse($tempFile);
836
837 try {
838 file_put_contents($tempFile, "%%\nrule: A;");
839
840 $ast = (new BisonParser())->parseFile($tempFile);
841
842 self::assertCount(1, $ast->rules);
843 } finally {
844 unlink($tempFile);
845 }
846 }
847
848 public function testParseFileNotFound(): void
849 {
850 $this->expectException(RuntimeException::class);
851 $this->expectExceptionMessage('Failed to read:');
852
853 @(new BisonParser())->parseFile('/nonexistent/path/file.yy');
854 }
855
856 public function testParseEmptyDeclarationsSection(): void
857 {
858 $input = <<<'BISON'
859%%
860rule: A;
861BISON;
862
863 $ast = (new BisonParser())->parse($input);
864
865 self::assertSame([], $ast->declarations);
866 }
867
868 public function testParseSkipsUnknownTokensInDeclarations(): void
869 {
870 $input = <<<'BISON'
871123
872%token TOKEN
873%%
874rule: TOKEN;
875BISON;
876
877 $ast = (new BisonParser())->parse($input);
878
879 self::assertCount(1, $ast->declarations);
880 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
881 }
882
883 public function testParseSkipsUnknownTokensInRules(): void
884 {
885 $input = <<<'BISON'
886%%
887123
888rule: A;
889BISON;
890
891 $ast = (new BisonParser())->parse($input);
892
893 self::assertCount(1, $ast->rules);
894 }
895
896 public function testParseMultipleDeclarations(): void
897 {
898 $input = <<<'BISON'
899%token TOKEN1
900%token TOKEN2
901%start rule
902%%
903rule: TOKEN1;
904BISON;
905
906 $ast = (new BisonParser())->parse($input);
907
908 self::assertCount(3, $ast->declarations);
909 }
910
911 public function testParseTokenDirectiveSkipsNonIdentifiers(): void
912 {
913 $input = <<<'BISON'
914%token <type> 123 TOKEN
915%%
916rule: TOKEN;
917BISON;
918
919 $ast = (new BisonParser())->parse($input);
920
921 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
922 self::assertCount(1, $ast->declarations[0]->tokens);
923 self::assertSame('TOKEN', $ast->declarations[0]->tokens[0]->name);
924 }
925
926 public function testParseTypeDirectiveSkipsNonIdentifiers(): void
927 {
928 $input = <<<'BISON'
929%type <node> 123 expr
930%%
931expr: A;
932BISON;
933
934 $ast = (new BisonParser())->parse($input);
935
936 self::assertInstanceOf(BisonTypeDeclaration::class, $ast->declarations[0]);
937 self::assertSame(['expr'], $ast->declarations[0]->symbols);
938 }
939
940 public function testParsePrecedenceDirectiveSkipsNonSymbols(): void
941 {
942 $input = <<<'BISON'
943%left 123 PLUS
944%%
945rule: A;
946BISON;
947
948 $ast = (new BisonParser())->parse($input);
949
950 self::assertInstanceOf(BisonPrecedenceDeclaration::class, $ast->declarations[0]);
951 self::assertSame(['PLUS'], $ast->declarations[0]->symbols);
952 }
953
954 public function testParseRuleSkipsUnknownTokens(): void
955 {
956 $input = <<<'BISON'
957%%
958rule: A 123 B;
959BISON;
960
961 $ast = (new BisonParser())->parse($input);
962
963 $symbols = $ast->rules[0]->alternatives[0]->symbols;
964 self::assertCount(2, $symbols);
965 self::assertSame('A', $symbols[0]->value);
966 self::assertSame('B', $symbols[1]->value);
967 }
968
969 public function testParseEmptyTokenDirective(): void
970 {
971 $input = <<<'BISON'
972%token
973%start rule
974%%
975rule: A;
976BISON;
977
978 $ast = (new BisonParser())->parse($input);
979
980 self::assertCount(2, $ast->declarations);
981 self::assertInstanceOf(BisonTokenDeclaration::class, $ast->declarations[0]);
982 self::assertCount(0, $ast->declarations[0]->tokens);
983 }
984
985 public function testParseDeclarationsSectionEndsAtEof(): void
986 {
987 $input = '%token TOKEN';
988
989 $this->expectException(GrammarParseException::class);
990 $this->expectExceptionMessage('No grammar rules parsed from the Bison grammar.');
991
992 (new BisonParser())->parse($input);
993 }
994
995 public function testParseRulesSectionEndsAtEof(): void
996 {
997 $input = <<<'BISON'
998%%
999rule: A
1000BISON;
1001
1002 $ast = (new BisonParser())->parse($input);
1003
1004 self::assertCount(1, $ast->rules);
1005 self::assertNull($ast->epilogue);
1006 }
1007
1008 public function testParseMultipleProloguesLastWins(): void
1009 {
1010 $input = <<<'BISON'
1011%{
1012first_prologue
1013%}
1014%{
1015second_prologue
1016%}
1017%%
1018rule: A;
1019BISON;
1020
1021 $ast = (new BisonParser())->parse($input);
1022
1023 self::assertNotNull($ast->prologue);
1024 self::assertStringContainsString('second_prologue', $ast->prologue);
1025 self::assertStringNotContainsString('first_prologue', $ast->prologue);
1026 }
1027
1028 public function testParsePrologueAfterDeclarations(): void
1029 {
1030 $input = <<<'BISON'
1031%token TOKEN
1032%{
1033prologue_after
1034%}
1035%start rule
1036%%
1037rule: TOKEN;
1038BISON;
1039
1040 $ast = (new BisonParser())->parse($input);
1041
1042 self::assertNotNull($ast->prologue);
1043 self::assertStringContainsString('prologue_after', $ast->prologue);
1044 self::assertCount(2, $ast->declarations);
1045 }
1046
1047 public function testParseMultipleActionsLastWins(): void
1048 {
1049 $input = <<<'BISON'
1050%%
1051rule: A { first_action } { second_action };
1052BISON;
1053
1054 $ast = (new BisonParser())->parse($input);
1055
1056 $action = $ast->rules[0]->alternatives[0]->action;
1057 self::assertNotNull($action);
1058 self::assertStringContainsString('second_action', $action);
1059 self::assertStringNotContainsString('first_action', $action);
1060 }
1061
1062 public function testParseMidRuleAction(): void
1063 {
1064 $input = <<<'BISON'
1065%%
1066rule: A { mid_action } B;
1067BISON;
1068
1069 $ast = (new BisonParser())->parse($input);
1070
1071 $alt = $ast->rules[0]->alternatives[0];
1072 self::assertCount(2, $alt->symbols);
1073 self::assertSame('A', $alt->symbols[0]->value);
1074 self::assertSame('B', $alt->symbols[1]->value);
1075 self::assertNotNull($alt->action);
1076 self::assertStringContainsString('mid_action', $alt->action);
1077 }
1078
1079 public function testParseEmptyAlternativeWithoutEmpty(): void
1080 {
1081 $input = <<<'BISON'
1082%%
1083rule: A | | B;
1084BISON;
1085
1086 $ast = (new BisonParser())->parse($input);
1087
1088 self::assertCount(3, $ast->rules[0]->alternatives);
1089 self::assertCount(1, $ast->rules[0]->alternatives[0]->symbols);
1090 self::assertCount(0, $ast->rules[0]->alternatives[1]->symbols);
1091 self::assertCount(1, $ast->rules[0]->alternatives[2]->symbols);
1092 }
1093
1094 public function testParseRuleWithPrecAndAction(): void
1095 {
1096 $input = <<<'BISON'
1097%%
1098rule: A %prec UMINUS { action_code };
1099BISON;
1100
1101 $ast = (new BisonParser())->parse($input);
1102
1103 $alt = $ast->rules[0]->alternatives[0];
1104 self::assertSame('UMINUS', $alt->prec);
1105 self::assertNotNull($alt->action);
1106 self::assertStringContainsString('action_code', $alt->action);
1107 }
1108
1109 public function testParseRuleWithDprecAndMerge(): void
1110 {
1111 $input = <<<'BISON'
1112%%
1113rule: A %dprec 1 %merge <func>;
1114BISON;
1115
1116 $ast = (new BisonParser())->parse($input);
1117
1118 $alt = $ast->rules[0]->alternatives[0];
1119 self::assertSame(1, $alt->dprec);
1120 self::assertSame('func', $alt->merge);
1121 }
1122
1123 public function testParseEpilogueMultipleTokensJoinedWithSpace(): void
1124 {
1125 $input = <<<'BISON'
1126%%
1127rule: A;
1128%%
1129token1 token2 token3
1130BISON;
1131
1132 $ast = (new BisonParser())->parse($input);
1133
1134 self::assertNotNull($ast->epilogue);
1135 self::assertSame('token1 token2 token3', $ast->epilogue);
1136 }
1137
1138 public function testParseActionBeforePrec(): void
1139 {
1140 $input = <<<'BISON'
1141%%
1142rule: A { action_first } %prec TOKEN;
1143BISON;
1144
1145 $ast = (new BisonParser())->parse($input);
1146
1147 $alt = $ast->rules[0]->alternatives[0];
1148 self::assertNotNull($alt->action);
1149 self::assertSame('TOKEN', $alt->prec);
1150 }
1151
1152 public function testParsePrecBeforeAction(): void
1153 {
1154 $input = <<<'BISON'
1155%%
1156rule: A %prec TOKEN { action_after };
1157BISON;
1158
1159 $ast = (new BisonParser())->parse($input);
1160
1161 $alt = $ast->rules[0]->alternatives[0];
1162 self::assertSame('TOKEN', $alt->prec);
1163 self::assertNotNull($alt->action);
1164 self::assertStringContainsString('action_after', $alt->action);
1165 }
1166
1167 public function testParseRuleWithAllDirectives(): void
1168 {
1169 $input = <<<'BISON'
1170%%
1171rule: A %prec TOKEN %dprec 2 %merge <func> { action };
1172BISON;
1173
1174 $ast = (new BisonParser())->parse($input);
1175
1176 $alt = $ast->rules[0]->alternatives[0];
1177 self::assertSame('TOKEN', $alt->prec);
1178 self::assertSame(2, $alt->dprec);
1179 self::assertSame('func', $alt->merge);
1180 self::assertNotNull($alt->action);
1181 self::assertStringContainsString('action', $alt->action);
1182 }
1183
1184 public function testParseMultipleRulesWithSameName(): void
1185 {
1186 $input = <<<'BISON'
1187%%
1188rule: A;
1189rule: B;
1190rule: C;
1191BISON;
1192
1193 $ast = (new BisonParser())->parse($input);
1194
1195 self::assertCount(3, $ast->rules);
1196 self::assertSame('rule', $ast->rules[0]->name);
1197 self::assertSame('rule', $ast->rules[1]->name);
1198 self::assertSame('rule', $ast->rules[2]->name);
1199 self::assertSame('A', $ast->rules[0]->alternatives[0]->symbols[0]->value);
1200 self::assertSame('B', $ast->rules[1]->alternatives[0]->symbols[0]->value);
1201 self::assertSame('C', $ast->rules[2]->alternatives[0]->symbols[0]->value);
1202 }
1203
1204 /**
1205 * @return iterable<string, array{string, string}>
1206 */
1207 public static function providerPrecedenceDirectives(): iterable
1208 {
1209 yield 'left' => ['%left', 'left'];
1210 yield 'right' => ['%right', 'right'];
1211 yield 'nonassoc' => ['%nonassoc', 'nonassoc'];
1212 yield 'precedence' => ['%precedence', 'precedence'];
1213 }
1214}
1215