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